diff --git a/knotserver/handler.go b/knotserver/handler.go index ee479d64..76e7ec00 100644 --- a/knotserver/handler.go +++ b/knotserver/handler.go @@ -16,6 +16,7 @@ import ( tlog "tangled.sh/tangled.sh/core/log" "tangled.sh/tangled.sh/core/notifier" "tangled.sh/tangled.sh/core/rbac" + "tangled.sh/tangled.sh/core/types" ) type Handle struct { @@ -170,14 +171,17 @@ func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, j func (h *Handle) XrpcRouter() http.Handler { logger := tlog.New("knots") + serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String()) + xrpc := &xrpc.Xrpc{ - Config: h.c, - Db: h.db, - Ingester: h.jc, - Enforcer: h.e, - Logger: logger, - Notifier: h.n, - Resolver: h.resolver, + Config: h.c, + Db: h.db, + Ingester: h.jc, + Enforcer: h.e, + Logger: logger, + Notifier: h.n, + Resolver: h.resolver, + ServiceAuth: serviceAuth, } return xrpc.Router() } diff --git a/knotserver/xrpc/router.go b/knotserver/xrpc/router.go index 5d2191a1..d4d01c99 100644 --- a/knotserver/xrpc/router.go +++ b/knotserver/xrpc/router.go @@ -1,12 +1,9 @@ package xrpc import ( - "context" "encoding/json" - "fmt" "log/slog" "net/http" - "strings" "tangled.sh/tangled.sh/core/api/tangled" "tangled.sh/tangled.sh/core/idresolver" @@ -15,134 +12,35 @@ import ( "tangled.sh/tangled.sh/core/knotserver/db" "tangled.sh/tangled.sh/core/notifier" "tangled.sh/tangled.sh/core/rbac" + xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" + "tangled.sh/tangled.sh/core/xrpc/serviceauth" - "github.com/bluesky-social/indigo/atproto/auth" "github.com/go-chi/chi/v5" ) type Xrpc struct { - Config *config.Config - Db *db.DB - Ingester *jetstream.JetstreamClient - Enforcer *rbac.Enforcer - Logger *slog.Logger - Notifier *notifier.Notifier - Resolver *idresolver.Resolver + Config *config.Config + Db *db.DB + Ingester *jetstream.JetstreamClient + Enforcer *rbac.Enforcer + Logger *slog.Logger + Notifier *notifier.Notifier + Resolver *idresolver.Resolver + ServiceAuth *serviceauth.ServiceAuth } func (x *Xrpc) Router() http.Handler { r := chi.NewRouter() - r.With(x.VerifyServiceAuth).Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch) + r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch) return r } -func (x *Xrpc) VerifyServiceAuth(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - l := x.Logger.With("url", r.URL) - - token := r.Header.Get("Authorization") - token = strings.TrimPrefix(token, "Bearer ") - - s := auth.ServiceAuthValidator{ - Audience: x.Config.Server.Did().String(), - Dir: x.Resolver.Directory(), - } - - did, err := s.Validate(r.Context(), token, nil) - if err != nil { - l.Error("signature verification failed", "err", err) - writeError(w, AuthError(err), http.StatusForbidden) - return - } - - r = r.WithContext( - context.WithValue(r.Context(), ActorDid, did), - ) - - next.ServeHTTP(w, r) - }) -} - -type XrpcError struct { - Tag string `json:"error"` - Message string `json:"message"` -} - -func NewXrpcError(opts ...ErrOpt) XrpcError { - x := XrpcError{} - for _, o := range opts { - o(&x) - } - - return x -} - -type ErrOpt = func(xerr *XrpcError) - -func WithTag(tag string) ErrOpt { - return func(xerr *XrpcError) { - xerr.Tag = tag - } -} - -func WithMessage[S ~string](s S) ErrOpt { - return func(xerr *XrpcError) { - xerr.Message = string(s) - } -} - -func WithError(e error) ErrOpt { - return func(xerr *XrpcError) { - xerr.Message = e.Error() - } -} - -var MissingActorDidError = NewXrpcError( - WithTag("MissingActorDid"), - WithMessage("actor DID not supplied"), -) - -var AuthError = func(err error) XrpcError { - return NewXrpcError( - WithTag("Auth"), - WithError(fmt.Errorf("signature verification failed: %w", err)), - ) -} - -var InvalidRepoError = func(r string) XrpcError { - return NewXrpcError( - WithTag("InvalidRepo"), - WithError(fmt.Errorf("supplied at-uri is not a repo: %s", r)), - ) -} - -var AccessControlError = func(d string) XrpcError { - return NewXrpcError( - WithTag("AccessControl"), - WithError(fmt.Errorf("DID does not have sufficent access permissions for this operation: %s", d)), - ) -} - -var GitError = func(e error) XrpcError { - return NewXrpcError( - WithTag("Git"), - WithError(fmt.Errorf("git error: %w", e)), - ) -} - -func GenericError(err error) XrpcError { - return NewXrpcError( - WithTag("Generic"), - WithError(err), - ) -} - // this is slightly different from http_util::write_error to follow the spec: // // the json object returned must include an "error" and a "message" -func writeError(w http.ResponseWriter, e XrpcError, status int) { +func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(e) diff --git a/knotserver/xrpc/set_default_branch.go b/knotserver/xrpc/set_default_branch.go index 539444ae..73ecaef5 100644 --- a/knotserver/xrpc/set_default_branch.go +++ b/knotserver/xrpc/set_default_branch.go @@ -12,74 +12,76 @@ import ( "tangled.sh/tangled.sh/core/api/tangled" "tangled.sh/tangled.sh/core/knotserver/git" "tangled.sh/tangled.sh/core/rbac" + + xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" ) const ActorDid string = "ActorDid" func (x *Xrpc) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { l := x.Logger - fail := func(e XrpcError) { + fail := func(e xrpcerr.XrpcError) { l.Error("failed", "kind", e.Tag, "error", e.Message) writeError(w, e, http.StatusBadRequest) } actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) if !ok { - fail(MissingActorDidError) + fail(xrpcerr.MissingActorDidError) return } var data tangled.RepoSetDefaultBranch_Input if err := json.NewDecoder(r.Body).Decode(&data); err != nil { - fail(GenericError(err)) + fail(xrpcerr.GenericError(err)) return } // unfortunately we have to resolve repo-at here repoAt, err := syntax.ParseATURI(data.Repo) if err != nil { - fail(InvalidRepoError(data.Repo)) + fail(xrpcerr.InvalidRepoError(data.Repo)) return } // resolve this aturi to extract the repo record ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String()) if err != nil || ident.Handle.IsInvalidHandle() { - fail(GenericError(fmt.Errorf("failed to resolve handle: %w", err))) + fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) return } xrpcc := xrpc.Client{Host: ident.PDSEndpoint()} resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) if err != nil { - fail(GenericError(err)) + fail(xrpcerr.GenericError(err)) return } repo := resp.Value.Val.(*tangled.Repo) didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) if err != nil { - fail(GenericError(err)) + fail(xrpcerr.GenericError(err)) return } if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { l.Error("insufficent permissions", "did", actorDid.String()) - writeError(w, AccessControlError(actorDid.String()), http.StatusUnauthorized) + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) return } path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) gr, err := git.PlainOpen(path) if err != nil { - fail(InvalidRepoError(data.Repo)) + fail(xrpcerr.GenericError(err)) return } err = gr.SetDefaultBranch(data.DefaultBranch) if err != nil { l.Error("setting default branch", "error", err.Error()) - writeError(w, GitError(err), http.StatusInternalServerError) + writeError(w, xrpcerr.GitError(err), http.StatusInternalServerError) return }