forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

spindle/xrpc: use new top-level xrpc packages

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

anirudh.fi cf8fa325 41a57161

verified
+11 -7
spindle/server.go
···
"tangled.sh/tangled.sh/core/spindle/queue"
"tangled.sh/tangled.sh/core/spindle/secrets"
"tangled.sh/tangled.sh/core/spindle/xrpc"
+
"tangled.sh/tangled.sh/core/xrpc/serviceauth"
)
//go:embed motd
···
func (s *Spindle) XrpcRouter() http.Handler {
logger := s.l.With("route", "xrpc")
+
serviceAuth := serviceauth.NewServiceAuth(s.l, s.res, s.cfg.Server.Did().String())
+
x := xrpc.Xrpc{
-
Logger: logger,
-
Db: s.db,
-
Enforcer: s.e,
-
Engine: s.eng,
-
Config: s.cfg,
-
Resolver: s.res,
-
Vault: s.vault,
+
Logger: logger,
+
Db: s.db,
+
Enforcer: s.e,
+
Engine: s.eng,
+
Config: s.cfg,
+
Resolver: s.res,
+
Vault: s.vault,
+
ServiceAuth: serviceAuth,
}
return x.Router()
+11 -10
spindle/xrpc/add_secret.go
···
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/rbac"
"tangled.sh/tangled.sh/core/spindle/secrets"
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
)
func (x *Xrpc) AddSecret(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.RepoAddSecret_Input
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
-
fail(GenericError(err))
+
fail(xrpcerr.GenericError(err))
return
}
if err := secrets.ValidateKey(data.Key); 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 := atproto.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(repo.Owner, repo.Name)
if err != nil {
-
fail(GenericError(err))
+
fail(xrpcerr.GenericError(err))
return
}
if ok, err := x.Enforcer.IsSettingsAllowed(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
}
···
err = x.Vault.AddSecret(r.Context(), secret)
if err != nil {
l.Error("failed to add secret to vault", "did", actorDid.String(), "err", err)
-
writeError(w, GenericError(err), http.StatusInternalServerError)
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
return
}
+10 -9
spindle/xrpc/list_secrets.go
···
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/rbac"
"tangled.sh/tangled.sh/core/spindle/secrets"
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
)
func (x *Xrpc) ListSecrets(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
}
repoParam := r.URL.Query().Get("repo")
if repoParam == "" {
-
fail(GenericError(fmt.Errorf("empty params")))
+
fail(xrpcerr.GenericError(fmt.Errorf("empty params")))
return
}
// unfortunately we have to resolve repo-at here
repoAt, err := syntax.ParseATURI(repoParam)
if err != nil {
-
fail(InvalidRepoError(repoParam))
+
fail(xrpcerr.InvalidRepoError(repoParam))
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 := atproto.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(repo.Owner, repo.Name)
if err != nil {
-
fail(GenericError(err))
+
fail(xrpcerr.GenericError(err))
return
}
if ok, err := x.Enforcer.IsSettingsAllowed(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
}
ls, err := x.Vault.GetSecretsLocked(r.Context(), secrets.DidSlashRepo(didPath))
if err != nil {
l.Error("failed to get secret from vault", "did", actorDid.String(), "err", err)
-
writeError(w, GenericError(err), http.StatusInternalServerError)
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
return
}
+10 -9
spindle/xrpc/remove_secret.go
···
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/rbac"
"tangled.sh/tangled.sh/core/spindle/secrets"
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
)
func (x *Xrpc) RemoveSecret(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.RepoRemoveSecret_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 := atproto.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(repo.Owner, repo.Name)
if err != nil {
-
fail(GenericError(err))
+
fail(xrpcerr.GenericError(err))
return
}
if ok, err := x.Enforcer.IsSettingsAllowed(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
}
···
err = x.Vault.RemoveSecret(r.Context(), secret)
if err != nil {
l.Error("failed to remove secret from vault", "did", actorDid.String(), "err", err)
-
writeError(w, GenericError(err), http.StatusInternalServerError)
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
return
}
+14 -109
spindle/xrpc/xrpc.go
···
package xrpc
import (
-
"context"
_ "embed"
"encoding/json"
-
"fmt"
"log/slog"
"net/http"
-
"strings"
-
"github.com/bluesky-social/indigo/atproto/auth"
"github.com/go-chi/chi/v5"
"tangled.sh/tangled.sh/core/api/tangled"
···
"tangled.sh/tangled.sh/core/spindle/db"
"tangled.sh/tangled.sh/core/spindle/engine"
"tangled.sh/tangled.sh/core/spindle/secrets"
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
+
"tangled.sh/tangled.sh/core/xrpc/serviceauth"
)
const ActorDid string = "ActorDid"
type Xrpc struct {
-
Logger *slog.Logger
-
Db *db.DB
-
Enforcer *rbac.Enforcer
-
Engine *engine.Engine
-
Config *config.Config
-
Resolver *idresolver.Resolver
-
Vault secrets.Manager
+
Logger *slog.Logger
+
Db *db.DB
+
Enforcer *rbac.Enforcer
+
Engine *engine.Engine
+
Config *config.Config
+
Resolver *idresolver.Resolver
+
Vault secrets.Manager
+
ServiceAuth *serviceauth.ServiceAuth
}
func (x *Xrpc) Router() http.Handler {
r := chi.NewRouter()
-
r.With(x.VerifyServiceAuth).Post("/"+tangled.RepoAddSecretNSID, x.AddSecret)
-
r.With(x.VerifyServiceAuth).Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret)
-
r.With(x.VerifyServiceAuth).Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets)
+
r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoAddSecretNSID, x.AddSecret)
+
r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret)
+
r.With(x.ServiceAuth.VerifyServiceAuth).Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets)
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)),
-
)
-
}
-
-
func GenericError(err error) XrpcError {
-
return NewXrpcError(
-
WithTag("Generic"),
-
WithError(err),
-
)
-
}
-
-
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)),
-
)
-
}
-
// 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)