diff --git a/spindle/config/config.go b/spindle/config/config.go --- a/spindle/config/config.go +++ b/spindle/config/config.go @@ -9,17 +9,17 @@ "github.com/sethvargo/go-envconfig" ) type Server struct { - ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:6555"` - DBPath string `env:"DB_PATH, default=spindle.db"` - Hostname string `env:"HOSTNAME, required"` - JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"` - PlcUrl string `env:"PLC_URL, default=https://plc.directory"` - Dev bool `env:"DEV, default=false"` - Owner string `env:"OWNER, required"` - Secrets Secrets `env:",prefix=SECRETS_"` - LogDir string `env:"LOG_DIR, default=/var/log/spindle"` - QueueSize int `env:"QUEUE_SIZE, default=100"` - MaxJobCount int `env:"MAX_JOB_COUNT, default=2"` // max number of jobs that run at a time + ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:6555"` + DBPath string `env:"DB_PATH, default=spindle.db"` + Hostname string `env:"HOSTNAME, required"` + JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"` + PlcUrl string `env:"PLC_URL, default=https://plc.directory"` + Dev bool `env:"DEV, default=false"` + Owner syntax.DID `env:"OWNER, required"` + Secrets Secrets `env:",prefix=SECRETS_"` + LogDir string `env:"LOG_DIR, default=/var/log/spindle"` + QueueSize int `env:"QUEUE_SIZE, default=100"` + MaxJobCount int `env:"MAX_JOB_COUNT, default=2"` // max number of jobs that run at a time } func (s Server) Did() syntax.DID { diff --git a/spindle/ingester.go b/spindle/ingester.go --- a/spindle/ingester.go +++ b/spindle/ingester.go @@ -9,15 +9,12 @@ "time" "tangled.org/core/api/tangled" "tangled.org/core/eventconsumer" - "tangled.org/core/rbac" "tangled.org/core/spindle/db" comatproto "github.com/bluesky-social/indigo/api/atproto" - "github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/xrpc" "github.com/bluesky-social/jetstream/pkg/models" - securejoin "github.com/cyphar/filepath-securejoin" ) type Ingester func(ctx context.Context, e *models.Event) error @@ -79,7 +76,7 @@ l.Error("domain mismatch", "domain", recordInstance, "expected", domain) return fmt.Errorf("domain mismatch: %s != %s", record.Instance, domain) } - ok, err := s.e.IsSpindleInviteAllowed(did, rbacDomain) + ok, err := s.e.IsSpindleMemberInviteAllowed(syntax.DID(did), s.cfg.Server.Did()) if err != nil || !ok { l.Error("failed to add member", "did", did, "error", err) return fmt.Errorf("failed to enforce permissions: %w", err) @@ -96,7 +93,7 @@ l.Error("failed to add member", "error", err) return fmt.Errorf("failed to add member: %w", err) } - if err := s.e.AddSpindleMember(rbacDomain, record.Subject); err != nil { + if err := s.e.AddSpindleMember(syntax.DID(record.Subject), s.cfg.Server.Did()); err != nil { l.Error("failed to add member", "error", err) return fmt.Errorf("failed to add member: %w", err) } @@ -122,7 +119,7 @@ l.Error("failed to remove member", "error", err) return fmt.Errorf("failed to remove member: %w", err) } - if err := s.e.RemoveSpindleMember(rbacDomain, record.Subject.String()); err != nil { + if err := s.e.RemoveSpindleMember(record.Subject, s.cfg.Server.Did()); err != nil { l.Error("failed to add member", "error", err) return fmt.Errorf("failed to add member: %w", err) } @@ -176,23 +173,16 @@ l.Error("failed to add repo", "error", err) return fmt.Errorf("failed to add repo: %w", err) } - didSlashRepo, err := securejoin.SecureJoin(did, record.Name) - if err != nil { - return err - } + repoAt := syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", did, e.Commit.Collection, e.Commit.RKey)) // add repo to rbac - if err := s.e.AddRepo(did, rbac.ThisServer, didSlashRepo); err != nil { + if err := s.e.AddRepo(repoAt); err != nil { l.Error("failed to add repo to enforcer", "error", err) return fmt.Errorf("failed to add repo: %w", err) } // add collaborators to rbac - owner, err := s.res.ResolveIdent(ctx, did) - if err != nil || owner.Handle.IsInvalidHandle() { - return err - } - if err := s.fetchAndAddCollaborators(ctx, owner, didSlashRepo); err != nil { + if err := s.fetchAndAddCollaborators(ctx, repoAt); err != nil { return err } @@ -234,32 +224,13 @@ l.Info("rejecting record, invalid repoAt", "repoAt", record.Repo) return nil } - // TODO: get rid of this entirely - // resolve this aturi to extract the repo record - owner, err := s.res.ResolveIdent(ctx, repoAt.Authority().String()) - if err != nil || owner.Handle.IsInvalidHandle() { - return fmt.Errorf("failed to resolve handle: %w", err) - } - - xrpcc := xrpc.Client{ - Host: owner.PDSEndpoint(), - } - - resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) - if err != nil { - return err - } - - repo := resp.Value.Val.(*tangled.Repo) - didSlashRepo, _ := securejoin.SecureJoin(owner.DID.String(), repo.Name) - // check perms for this user - if ok, err := s.e.IsCollaboratorInviteAllowed(owner.DID.String(), rbac.ThisServer, didSlashRepo); !ok || err != nil { + if ok, err := s.e.IsRepoCollaboratorInviteAllowed(syntax.DID(e.Did), repoAt); !ok || err != nil { return fmt.Errorf("insufficient permissions: %w", err) } // add collaborator to rbac - if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, didSlashRepo); err != nil { + if err := s.e.AddRepoCollaborator(syntax.DID(record.Subject), repoAt); err != nil { l.Error("failed to add repo to enforcer", "error", err) return fmt.Errorf("failed to add repo: %w", err) } @@ -269,16 +240,21 @@ } return nil } -func (s *Spindle) fetchAndAddCollaborators(ctx context.Context, owner *identity.Identity, didSlashRepo string) error { +func (s *Spindle) fetchAndAddCollaborators(ctx context.Context, repo syntax.ATURI) error { l := s.l.With("component", "ingester", "handler", "fetchAndAddCollaborators") l.Info("fetching and adding existing collaborators") + ident, err := s.res.ResolveIdent(ctx, repo.Authority().String()) + if err != nil || ident.Handle.IsInvalidHandle() { + return fmt.Errorf("failed to resolve handle: %w", err) + } + xrpcc := xrpc.Client{ - Host: owner.PDSEndpoint(), + Host: ident.PDSEndpoint(), } - resp, err := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoCollaboratorNSID, "", 50, owner.DID.String(), false) + resp, err := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoCollaboratorNSID, "", 50, ident.DID.String(), false) if err != nil { return err } @@ -290,7 +266,7 @@ continue } record := r.Value.Val.(*tangled.RepoCollaborator) - if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, didSlashRepo); err != nil { + if err := s.e.AddRepoCollaborator(syntax.DID(record.Subject), syntax.ATURI(record.Repo)); err != nil { l.Error("failed to add repo to enforcer", "error", err) errors.Join(errs, fmt.Errorf("failed to add repo: %w", err)) } diff --git a/spindle/server.go b/spindle/server.go --- a/spindle/server.go +++ b/spindle/server.go @@ -18,7 +18,7 @@ "tangled.org/core/idresolver" "tangled.org/core/jetstream" "tangled.org/core/log" "tangled.org/core/notifier" - "tangled.org/core/rbac" + "tangled.org/core/rbac2" "tangled.org/core/spindle/config" "tangled.org/core/spindle/db" "tangled.org/core/spindle/engine" @@ -32,15 +32,11 @@ ) //go:embed motd var defaultMotd []byte - -const ( - rbacDomain = "thisserver" -) type Spindle struct { jc *jetstream.JetstreamClient db *db.DB - e *rbac.Enforcer + e *rbac2.Enforcer l *slog.Logger n *notifier.Notifier engs map[string]models.Engine @@ -62,11 +58,10 @@ if err != nil { return nil, fmt.Errorf("failed to setup db: %w", err) } - e, err := rbac.NewEnforcer(cfg.Server.DBPath) + e, err := rbac2.NewEnforcer(cfg.Server.DBPath) if err != nil { return nil, fmt.Errorf("failed to setup rbac enforcer: %w", err) } - e.E.EnableAutoSave(true) n := notifier.New() @@ -107,7 +102,7 @@ jc, err := jetstream.NewJetstreamClient(cfg.Server.JetstreamEndpoint, "spindle", collections, nil, log.SubLogger(logger, "jetstream"), d, true, true) if err != nil { return nil, fmt.Errorf("failed to setup jetstream client: %w", err) } - jc.AddDid(cfg.Server.Owner) + jc.AddDid(cfg.Server.Owner.String()) // Check if the spindle knows about any Dids; dids, err := d.GetAllDids() @@ -134,11 +129,7 @@ vault: vault, motd: defaultMotd, } - err = e.AddSpindle(rbacDomain) - if err != nil { - return nil, fmt.Errorf("failed to set rbac domain: %w", err) - } - err = spindle.configureOwner() + err = e.SetSpindleOwner(spindle.cfg.Server.Owner, spindle.cfg.Server.Did()) if err != nil { return nil, err } @@ -201,7 +192,7 @@ return s.n } // Enforcer returns the RBAC enforcer instance. -func (s *Spindle) Enforcer() *rbac.Enforcer { +func (s *Spindle) Enforcer() *rbac2.Enforcer { return s.e } @@ -400,35 +391,3 @@ } return nil } - -func (s *Spindle) configureOwner() error { - cfgOwner := s.cfg.Server.Owner - - existing, err := s.e.GetSpindleUsersByRole("server:owner", rbacDomain) - if err != nil { - return err - } - - switch len(existing) { - case 0: - // no owner configured, continue - case 1: - // find existing owner - existingOwner := existing[0] - - // no ownership change, this is okay - if existingOwner == s.cfg.Server.Owner { - break - } - - // remove existing owner - err = s.e.RemoveSpindleOwner(rbacDomain, existingOwner) - if err != nil { - return nil - } - default: - return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", s.cfg.Server.DBPath) - } - - return s.e.AddSpindleOwner(rbacDomain, cfgOwner) -} diff --git a/spindle/xrpc/add_secret.go b/spindle/xrpc/add_secret.go --- a/spindle/xrpc/add_secret.go +++ b/spindle/xrpc/add_secret.go @@ -11,7 +11,6 @@ "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/xrpc" securejoin "github.com/cyphar/filepath-securejoin" "tangled.org/core/api/tangled" - "tangled.org/core/rbac" "tangled.org/core/spindle/secrets" xrpcerr "tangled.org/core/xrpc/errors" ) @@ -68,7 +67,7 @@ fail(xrpcerr.GenericError(err)) return } - if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { + if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil { l.Error("insufficent permissions", "did", actorDid.String()) writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) return diff --git a/spindle/xrpc/list_secrets.go b/spindle/xrpc/list_secrets.go --- a/spindle/xrpc/list_secrets.go +++ b/spindle/xrpc/list_secrets.go @@ -11,7 +11,6 @@ "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/xrpc" securejoin "github.com/cyphar/filepath-securejoin" "tangled.org/core/api/tangled" - "tangled.org/core/rbac" "tangled.org/core/spindle/secrets" xrpcerr "tangled.org/core/xrpc/errors" ) @@ -63,7 +62,7 @@ fail(xrpcerr.GenericError(err)) return } - if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { + if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil { l.Error("insufficent permissions", "did", actorDid.String()) writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) return diff --git a/spindle/xrpc/owner.go b/spindle/xrpc/owner.go --- a/spindle/xrpc/owner.go +++ b/spindle/xrpc/owner.go @@ -9,7 +9,7 @@ xrpcerr "tangled.org/core/xrpc/errors" ) func (x *Xrpc) Owner(w http.ResponseWriter, r *http.Request) { - owner := x.Config.Server.Owner + owner := x.Config.Server.Owner.String() if owner == "" { writeError(w, xrpcerr.OwnerNotFoundError, http.StatusInternalServerError) return diff --git a/spindle/xrpc/pipeline_cancelPipeline.go b/spindle/xrpc/pipeline_cancelPipeline.go --- a/spindle/xrpc/pipeline_cancelPipeline.go +++ b/spindle/xrpc/pipeline_cancelPipeline.go @@ -6,12 +6,8 @@ "fmt" "net/http" "strings" - "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/bluesky-social/indigo/xrpc" - securejoin "github.com/cyphar/filepath-securejoin" "tangled.org/core/api/tangled" - "tangled.org/core/rbac" "tangled.org/core/spindle/models" xrpcerr "tangled.org/core/xrpc/errors" ) @@ -53,28 +49,7 @@ fail(xrpcerr.InvalidRepoError(input.Repo)) return } - ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String()) - if err != nil || ident.Handle.IsInvalidHandle() { - 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(xrpcerr.GenericError(err)) - return - } - - repo := resp.Value.Val.(*tangled.Repo) - didSlashRepo, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) - if err != nil { - fail(xrpcerr.GenericError(err)) - return - } - - // TODO: fine-grained role based control - isRepoOwner, err := x.Enforcer.IsRepoOwner(actorDid.String(), rbac.ThisServer, didSlashRepo) + isRepoOwner, err := x.Enforcer.IsRepoOwner(actorDid, repoAt) if err != nil || !isRepoOwner { fail(xrpcerr.AccessControlError(actorDid.String())) return diff --git a/spindle/xrpc/remove_secret.go b/spindle/xrpc/remove_secret.go --- a/spindle/xrpc/remove_secret.go +++ b/spindle/xrpc/remove_secret.go @@ -10,7 +10,6 @@ "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/xrpc" securejoin "github.com/cyphar/filepath-securejoin" "tangled.org/core/api/tangled" - "tangled.org/core/rbac" "tangled.org/core/spindle/secrets" xrpcerr "tangled.org/core/xrpc/errors" ) @@ -62,7 +61,7 @@ fail(xrpcerr.GenericError(err)) return } - if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { + if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil { l.Error("insufficent permissions", "did", actorDid.String()) writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) return diff --git a/spindle/xrpc/xrpc.go b/spindle/xrpc/xrpc.go --- a/spindle/xrpc/xrpc.go +++ b/spindle/xrpc/xrpc.go @@ -11,7 +11,7 @@ "tangled.org/core/api/tangled" "tangled.org/core/idresolver" "tangled.org/core/notifier" - "tangled.org/core/rbac" + "tangled.org/core/rbac2" "tangled.org/core/spindle/config" "tangled.org/core/spindle/db" "tangled.org/core/spindle/models" @@ -25,7 +25,7 @@ type Xrpc struct { Logger *slog.Logger Db *db.DB - Enforcer *rbac.Enforcer + Enforcer *rbac2.Enforcer Engines map[string]models.Engine Config *config.Config Resolver *idresolver.Resolver