From 9cea15d10d8e15b11fc695a3242693dc553d66b7 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Tue, 12 Aug 2025 11:04:03 +0000 Subject: [PATCH] xrpc/{errors,serviceauth}: split shared xrpc code to top-level package Signed-off-by: Anirudh Oppiliappan --- xrpc/errors/errors.go | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ xrpc/serviceauth/service_auth.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 file(s) changed, 168 insertion(s)(+), 0 deletion(s)(-) diff --git a/xrpc/errors/errors.go b/xrpc/errors/errors.go new file mode 100644 --- /dev/null +++ b/xrpc/errors/errors.go @@ -0,0 +1,103 @@ +package errors + +import ( + "encoding/json" + "fmt" +) + +type XrpcError struct { + Tag string `json:"error"` + Message string `json:"message"` +} + +func (x XrpcError) Error() string { + if x.Message != "" { + return fmt.Sprintf("%s: %s", x.Tag, x.Message) + } + return x.Tag +} + +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 GitError = func(e error) XrpcError { + return NewXrpcError( + WithTag("Git"), + WithError(fmt.Errorf("git error: %w", e)), + ) +} + +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 RepoExistsError = func(r string) XrpcError { + return NewXrpcError( + WithTag("RepoExists"), + WithError(fmt.Errorf("repo already exists: %s", r)), + ) +} + +func GenericError(err error) XrpcError { + return NewXrpcError( + WithTag("Generic"), + WithError(err), + ) +} + +func Unmarshal(errStr string) (XrpcError, error) { + var xerr XrpcError + err := json.Unmarshal([]byte(errStr), &xerr) + if err != nil { + return XrpcError{}, fmt.Errorf("failed to unmarshal XrpcError: %w", err) + } + return xerr, nil +} diff --git a/xrpc/serviceauth/service_auth.go b/xrpc/serviceauth/service_auth.go new file mode 100644 --- /dev/null +++ b/xrpc/serviceauth/service_auth.go @@ -0,0 +1,65 @@ +package serviceauth + +import ( + "context" + "encoding/json" + "log/slog" + "net/http" + "strings" + + "github.com/bluesky-social/indigo/atproto/auth" + "tangled.sh/tangled.sh/core/idresolver" + xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" +) + +const ActorDid string = "ActorDid" + +type ServiceAuth struct { + logger *slog.Logger + resolver *idresolver.Resolver + audienceDid string +} + +func NewServiceAuth(logger *slog.Logger, resolver *idresolver.Resolver, audienceDid string) *ServiceAuth { + return &ServiceAuth{ + logger: logger, + resolver: resolver, + audienceDid: audienceDid, + } +} + +func (sa *ServiceAuth) VerifyServiceAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + l := sa.logger.With("url", r.URL) + + token := r.Header.Get("Authorization") + token = strings.TrimPrefix(token, "Bearer ") + + s := auth.ServiceAuthValidator{ + Audience: sa.audienceDid, + Dir: sa.resolver.Directory(), + } + + did, err := s.Validate(r.Context(), token, nil) + if err != nil { + l.Error("signature verification failed", "err", err) + writeError(w, xrpcerr.AuthError(err), http.StatusForbidden) + return + } + + r = r.WithContext( + context.WithValue(r.Context(), ActorDid, did), + ) + + next.ServeHTTP(w, r) + }) +} + +// 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 xrpcerr.XrpcError, status int) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(e) +} -- tangled.sh