forked from tangled.org/core
this repo has no description
1package xrpc 2 3import ( 4 "context" 5 _ "embed" 6 "encoding/json" 7 "fmt" 8 "log/slog" 9 "net/http" 10 "strings" 11 12 "github.com/bluesky-social/indigo/atproto/auth" 13 "github.com/go-chi/chi/v5" 14 15 "tangled.sh/tangled.sh/core/api/tangled" 16 "tangled.sh/tangled.sh/core/idresolver" 17 "tangled.sh/tangled.sh/core/rbac" 18 "tangled.sh/tangled.sh/core/spindle/config" 19 "tangled.sh/tangled.sh/core/spindle/db" 20 "tangled.sh/tangled.sh/core/spindle/models" 21 "tangled.sh/tangled.sh/core/spindle/secrets" 22) 23 24const ActorDid string = "ActorDid" 25 26type Xrpc struct { 27 Logger *slog.Logger 28 Db *db.DB 29 Enforcer *rbac.Enforcer 30 Engines map[string]models.Engine 31 Config *config.Config 32 Resolver *idresolver.Resolver 33 Vault secrets.Manager 34} 35 36func (x *Xrpc) Router() http.Handler { 37 r := chi.NewRouter() 38 39 r.With(x.VerifyServiceAuth).Post("/"+tangled.RepoAddSecretNSID, x.AddSecret) 40 r.With(x.VerifyServiceAuth).Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret) 41 r.With(x.VerifyServiceAuth).Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets) 42 43 return r 44} 45 46func (x *Xrpc) VerifyServiceAuth(next http.Handler) http.Handler { 47 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 48 l := x.Logger.With("url", r.URL) 49 50 token := r.Header.Get("Authorization") 51 token = strings.TrimPrefix(token, "Bearer ") 52 53 s := auth.ServiceAuthValidator{ 54 Audience: x.Config.Server.Did().String(), 55 Dir: x.Resolver.Directory(), 56 } 57 58 did, err := s.Validate(r.Context(), token, nil) 59 if err != nil { 60 l.Error("signature verification failed", "err", err) 61 writeError(w, AuthError(err), http.StatusForbidden) 62 return 63 } 64 65 r = r.WithContext( 66 context.WithValue(r.Context(), ActorDid, did), 67 ) 68 69 next.ServeHTTP(w, r) 70 }) 71} 72 73type XrpcError struct { 74 Tag string `json:"error"` 75 Message string `json:"message"` 76} 77 78func NewXrpcError(opts ...ErrOpt) XrpcError { 79 x := XrpcError{} 80 for _, o := range opts { 81 o(&x) 82 } 83 84 return x 85} 86 87type ErrOpt = func(xerr *XrpcError) 88 89func WithTag(tag string) ErrOpt { 90 return func(xerr *XrpcError) { 91 xerr.Tag = tag 92 } 93} 94 95func WithMessage[S ~string](s S) ErrOpt { 96 return func(xerr *XrpcError) { 97 xerr.Message = string(s) 98 } 99} 100 101func WithError(e error) ErrOpt { 102 return func(xerr *XrpcError) { 103 xerr.Message = e.Error() 104 } 105} 106 107var MissingActorDidError = NewXrpcError( 108 WithTag("MissingActorDid"), 109 WithMessage("actor DID not supplied"), 110) 111 112var AuthError = func(err error) XrpcError { 113 return NewXrpcError( 114 WithTag("Auth"), 115 WithError(fmt.Errorf("signature verification failed: %w", err)), 116 ) 117} 118 119var InvalidRepoError = func(r string) XrpcError { 120 return NewXrpcError( 121 WithTag("InvalidRepo"), 122 WithError(fmt.Errorf("supplied at-uri is not a repo: %s", r)), 123 ) 124} 125 126func GenericError(err error) XrpcError { 127 return NewXrpcError( 128 WithTag("Generic"), 129 WithError(err), 130 ) 131} 132 133var AccessControlError = func(d string) XrpcError { 134 return NewXrpcError( 135 WithTag("AccessControl"), 136 WithError(fmt.Errorf("DID does not have sufficent access permissions for this operation: %s", d)), 137 ) 138} 139 140// this is slightly different from http_util::write_error to follow the spec: 141// 142// the json object returned must include an "error" and a "message" 143func writeError(w http.ResponseWriter, e XrpcError, status int) { 144 w.Header().Set("Content-Type", "application/json") 145 w.WriteHeader(status) 146 json.NewEncoder(w).Encode(e) 147}