Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

spindle/xrpc: use new top-level xrpc packages

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

authored by

Anirudh Oppiliappan and committed by
oppiliappan
e48b561a 9cea15d1

+56 -144
+11 -7
spindle/server.go
··· 25 25 "tangled.sh/tangled.sh/core/spindle/queue" 26 26 "tangled.sh/tangled.sh/core/spindle/secrets" 27 27 "tangled.sh/tangled.sh/core/spindle/xrpc" 28 + "tangled.sh/tangled.sh/core/xrpc/serviceauth" 28 29 ) 29 30 30 31 //go:embed motd ··· 214 213 func (s *Spindle) XrpcRouter() http.Handler { 215 214 logger := s.l.With("route", "xrpc") 216 215 216 + serviceAuth := serviceauth.NewServiceAuth(s.l, s.res, s.cfg.Server.Did().String()) 217 + 217 218 x := xrpc.Xrpc{ 218 - Logger: logger, 219 - Db: s.db, 220 - Enforcer: s.e, 221 - Engines: s.engs, 222 - Config: s.cfg, 223 - Resolver: s.res, 224 - Vault: s.vault, 219 + Logger: logger, 220 + Db: s.db, 221 + Enforcer: s.e, 222 + Engines: s.engs, 223 + Config: s.cfg, 224 + Resolver: s.res, 225 + Vault: s.vault, 226 + ServiceAuth: serviceAuth, 225 227 } 226 228 227 229 return x.Router()
+11 -10
spindle/xrpc/add_secret.go
··· 13 13 "tangled.sh/tangled.sh/core/api/tangled" 14 14 "tangled.sh/tangled.sh/core/rbac" 15 15 "tangled.sh/tangled.sh/core/spindle/secrets" 16 + xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 16 17 ) 17 18 18 19 func (x *Xrpc) AddSecret(w http.ResponseWriter, r *http.Request) { 19 20 l := x.Logger 20 - fail := func(e XrpcError) { 21 + fail := func(e xrpcerr.XrpcError) { 21 22 l.Error("failed", "kind", e.Tag, "error", e.Message) 22 23 writeError(w, e, http.StatusBadRequest) 23 24 } 24 25 25 26 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 26 27 if !ok { 27 - fail(MissingActorDidError) 28 + fail(xrpcerr.MissingActorDidError) 28 29 return 29 30 } 30 31 31 32 var data tangled.RepoAddSecret_Input 32 33 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 33 - fail(GenericError(err)) 34 + fail(xrpcerr.GenericError(err)) 34 35 return 35 36 } 36 37 37 38 if err := secrets.ValidateKey(data.Key); err != nil { 38 - fail(GenericError(err)) 39 + fail(xrpcerr.GenericError(err)) 39 40 return 40 41 } 41 42 42 43 // unfortunately we have to resolve repo-at here 43 44 repoAt, err := syntax.ParseATURI(data.Repo) 44 45 if err != nil { 45 - fail(InvalidRepoError(data.Repo)) 46 + fail(xrpcerr.InvalidRepoError(data.Repo)) 46 47 return 47 48 } 48 49 49 50 // resolve this aturi to extract the repo record 50 51 ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String()) 51 52 if err != nil || ident.Handle.IsInvalidHandle() { 52 - fail(GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 53 + fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 53 54 return 54 55 } 55 56 56 57 xrpcc := xrpc.Client{Host: ident.PDSEndpoint()} 57 58 resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 58 59 if err != nil { 59 - fail(GenericError(err)) 60 + fail(xrpcerr.GenericError(err)) 60 61 return 61 62 } 62 63 63 64 repo := resp.Value.Val.(*tangled.Repo) 64 65 didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name) 65 66 if err != nil { 66 - fail(GenericError(err)) 67 + fail(xrpcerr.GenericError(err)) 67 68 return 68 69 } 69 70 70 71 if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 71 72 l.Error("insufficent permissions", "did", actorDid.String()) 72 - writeError(w, AccessControlError(actorDid.String()), http.StatusUnauthorized) 73 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 73 74 return 74 75 } 75 76 ··· 84 83 err = x.Vault.AddSecret(r.Context(), secret) 85 84 if err != nil { 86 85 l.Error("failed to add secret to vault", "did", actorDid.String(), "err", err) 87 - writeError(w, GenericError(err), http.StatusInternalServerError) 86 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 88 87 return 89 88 } 90 89
+10 -9
spindle/xrpc/list_secrets.go
··· 13 13 "tangled.sh/tangled.sh/core/api/tangled" 14 14 "tangled.sh/tangled.sh/core/rbac" 15 15 "tangled.sh/tangled.sh/core/spindle/secrets" 16 + xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 16 17 ) 17 18 18 19 func (x *Xrpc) ListSecrets(w http.ResponseWriter, r *http.Request) { 19 20 l := x.Logger 20 - fail := func(e XrpcError) { 21 + fail := func(e xrpcerr.XrpcError) { 21 22 l.Error("failed", "kind", e.Tag, "error", e.Message) 22 23 writeError(w, e, http.StatusBadRequest) 23 24 } 24 25 25 26 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 26 27 if !ok { 27 - fail(MissingActorDidError) 28 + fail(xrpcerr.MissingActorDidError) 28 29 return 29 30 } 30 31 31 32 repoParam := r.URL.Query().Get("repo") 32 33 if repoParam == "" { 33 - fail(GenericError(fmt.Errorf("empty params"))) 34 + fail(xrpcerr.GenericError(fmt.Errorf("empty params"))) 34 35 return 35 36 } 36 37 37 38 // unfortunately we have to resolve repo-at here 38 39 repoAt, err := syntax.ParseATURI(repoParam) 39 40 if err != nil { 40 - fail(InvalidRepoError(repoParam)) 41 + fail(xrpcerr.InvalidRepoError(repoParam)) 41 42 return 42 43 } 43 44 44 45 // resolve this aturi to extract the repo record 45 46 ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String()) 46 47 if err != nil || ident.Handle.IsInvalidHandle() { 47 - fail(GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 48 + fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 48 49 return 49 50 } 50 51 51 52 xrpcc := xrpc.Client{Host: ident.PDSEndpoint()} 52 53 resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 53 54 if err != nil { 54 - fail(GenericError(err)) 55 + fail(xrpcerr.GenericError(err)) 55 56 return 56 57 } 57 58 58 59 repo := resp.Value.Val.(*tangled.Repo) 59 60 didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name) 60 61 if err != nil { 61 - fail(GenericError(err)) 62 + fail(xrpcerr.GenericError(err)) 62 63 return 63 64 } 64 65 65 66 if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 66 67 l.Error("insufficent permissions", "did", actorDid.String()) 67 - writeError(w, AccessControlError(actorDid.String()), http.StatusUnauthorized) 68 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 68 69 return 69 70 } 70 71 71 72 ls, err := x.Vault.GetSecretsLocked(r.Context(), secrets.DidSlashRepo(didPath)) 72 73 if err != nil { 73 74 l.Error("failed to get secret from vault", "did", actorDid.String(), "err", err) 74 - writeError(w, GenericError(err), http.StatusInternalServerError) 75 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 75 76 return 76 77 } 77 78
+10 -9
spindle/xrpc/remove_secret.go
··· 12 12 "tangled.sh/tangled.sh/core/api/tangled" 13 13 "tangled.sh/tangled.sh/core/rbac" 14 14 "tangled.sh/tangled.sh/core/spindle/secrets" 15 + xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 15 16 ) 16 17 17 18 func (x *Xrpc) RemoveSecret(w http.ResponseWriter, r *http.Request) { 18 19 l := x.Logger 19 - fail := func(e XrpcError) { 20 + fail := func(e xrpcerr.XrpcError) { 20 21 l.Error("failed", "kind", e.Tag, "error", e.Message) 21 22 writeError(w, e, http.StatusBadRequest) 22 23 } 23 24 24 25 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 25 26 if !ok { 26 - fail(MissingActorDidError) 27 + fail(xrpcerr.MissingActorDidError) 27 28 return 28 29 } 29 30 30 31 var data tangled.RepoRemoveSecret_Input 31 32 if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 32 - fail(GenericError(err)) 33 + fail(xrpcerr.GenericError(err)) 33 34 return 34 35 } 35 36 36 37 // unfortunately we have to resolve repo-at here 37 38 repoAt, err := syntax.ParseATURI(data.Repo) 38 39 if err != nil { 39 - fail(InvalidRepoError(data.Repo)) 40 + fail(xrpcerr.InvalidRepoError(data.Repo)) 40 41 return 41 42 } 42 43 43 44 // resolve this aturi to extract the repo record 44 45 ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String()) 45 46 if err != nil || ident.Handle.IsInvalidHandle() { 46 - fail(GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 47 + fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 47 48 return 48 49 } 49 50 50 51 xrpcc := xrpc.Client{Host: ident.PDSEndpoint()} 51 52 resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 52 53 if err != nil { 53 - fail(GenericError(err)) 54 + fail(xrpcerr.GenericError(err)) 54 55 return 55 56 } 56 57 57 58 repo := resp.Value.Val.(*tangled.Repo) 58 59 didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name) 59 60 if err != nil { 60 - fail(GenericError(err)) 61 + fail(xrpcerr.GenericError(err)) 61 62 return 62 63 } 63 64 64 65 if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 65 66 l.Error("insufficent permissions", "did", actorDid.String()) 66 - writeError(w, AccessControlError(actorDid.String()), http.StatusUnauthorized) 67 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 67 68 return 68 69 } 69 70 ··· 75 74 err = x.Vault.RemoveSecret(r.Context(), secret) 76 75 if err != nil { 77 76 l.Error("failed to remove secret from vault", "did", actorDid.String(), "err", err) 78 - writeError(w, GenericError(err), http.StatusInternalServerError) 77 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 79 78 return 80 79 } 81 80
+14 -109
spindle/xrpc/xrpc.go
··· 1 1 package xrpc 2 2 3 3 import ( 4 - "context" 5 4 _ "embed" 6 5 "encoding/json" 7 - "fmt" 8 6 "log/slog" 9 7 "net/http" 10 - "strings" 11 8 12 - "github.com/bluesky-social/indigo/atproto/auth" 13 9 "github.com/go-chi/chi/v5" 14 10 15 11 "tangled.sh/tangled.sh/core/api/tangled" ··· 15 19 "tangled.sh/tangled.sh/core/spindle/db" 16 20 "tangled.sh/tangled.sh/core/spindle/models" 17 21 "tangled.sh/tangled.sh/core/spindle/secrets" 22 + xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 23 + "tangled.sh/tangled.sh/core/xrpc/serviceauth" 18 24 ) 19 25 20 26 const ActorDid string = "ActorDid" 21 27 22 28 type Xrpc struct { 23 - Logger *slog.Logger 24 - Db *db.DB 25 - Enforcer *rbac.Enforcer 26 - Engines map[string]models.Engine 27 - Config *config.Config 28 - Resolver *idresolver.Resolver 29 - Vault secrets.Manager 29 + Logger *slog.Logger 30 + Db *db.DB 31 + Enforcer *rbac.Enforcer 32 + Engines map[string]models.Engine 33 + Config *config.Config 34 + Resolver *idresolver.Resolver 35 + Vault secrets.Manager 36 + ServiceAuth *serviceauth.ServiceAuth 30 37 } 31 38 32 39 func (x *Xrpc) Router() http.Handler { 33 40 r := chi.NewRouter() 34 41 35 - r.With(x.VerifyServiceAuth).Post("/"+tangled.RepoAddSecretNSID, x.AddSecret) 36 - r.With(x.VerifyServiceAuth).Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret) 37 - r.With(x.VerifyServiceAuth).Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets) 42 + r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoAddSecretNSID, x.AddSecret) 43 + r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret) 44 + r.With(x.ServiceAuth.VerifyServiceAuth).Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets) 38 45 39 46 return r 40 - } 41 - 42 - func (x *Xrpc) VerifyServiceAuth(next http.Handler) http.Handler { 43 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 44 - l := x.Logger.With("url", r.URL) 45 - 46 - token := r.Header.Get("Authorization") 47 - token = strings.TrimPrefix(token, "Bearer ") 48 - 49 - s := auth.ServiceAuthValidator{ 50 - Audience: x.Config.Server.Did().String(), 51 - Dir: x.Resolver.Directory(), 52 - } 53 - 54 - did, err := s.Validate(r.Context(), token, nil) 55 - if err != nil { 56 - l.Error("signature verification failed", "err", err) 57 - writeError(w, AuthError(err), http.StatusForbidden) 58 - return 59 - } 60 - 61 - r = r.WithContext( 62 - context.WithValue(r.Context(), ActorDid, did), 63 - ) 64 - 65 - next.ServeHTTP(w, r) 66 - }) 67 - } 68 - 69 - type XrpcError struct { 70 - Tag string `json:"error"` 71 - Message string `json:"message"` 72 - } 73 - 74 - func NewXrpcError(opts ...ErrOpt) XrpcError { 75 - x := XrpcError{} 76 - for _, o := range opts { 77 - o(&x) 78 - } 79 - 80 - return x 81 - } 82 - 83 - type ErrOpt = func(xerr *XrpcError) 84 - 85 - func WithTag(tag string) ErrOpt { 86 - return func(xerr *XrpcError) { 87 - xerr.Tag = tag 88 - } 89 - } 90 - 91 - func WithMessage[S ~string](s S) ErrOpt { 92 - return func(xerr *XrpcError) { 93 - xerr.Message = string(s) 94 - } 95 - } 96 - 97 - func WithError(e error) ErrOpt { 98 - return func(xerr *XrpcError) { 99 - xerr.Message = e.Error() 100 - } 101 - } 102 - 103 - var MissingActorDidError = NewXrpcError( 104 - WithTag("MissingActorDid"), 105 - WithMessage("actor DID not supplied"), 106 - ) 107 - 108 - var AuthError = func(err error) XrpcError { 109 - return NewXrpcError( 110 - WithTag("Auth"), 111 - WithError(fmt.Errorf("signature verification failed: %w", err)), 112 - ) 113 - } 114 - 115 - var InvalidRepoError = func(r string) XrpcError { 116 - return NewXrpcError( 117 - WithTag("InvalidRepo"), 118 - WithError(fmt.Errorf("supplied at-uri is not a repo: %s", r)), 119 - ) 120 - } 121 - 122 - func GenericError(err error) XrpcError { 123 - return NewXrpcError( 124 - WithTag("Generic"), 125 - WithError(err), 126 - ) 127 - } 128 - 129 - var AccessControlError = func(d string) XrpcError { 130 - return NewXrpcError( 131 - WithTag("AccessControl"), 132 - WithError(fmt.Errorf("DID does not have sufficent access permissions for this operation: %s", d)), 133 - ) 134 47 } 135 48 136 49 // this is slightly different from http_util::write_error to follow the spec: 137 50 // 138 51 // the json object returned must include an "error" and a "message" 139 - func writeError(w http.ResponseWriter, e XrpcError, status int) { 52 + func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) { 140 53 w.Header().Set("Content-Type", "application/json") 141 54 w.WriteHeader(status) 142 55 json.NewEncoder(w).Encode(e)