From 9240d3c37557aba25e7c55ce120f0269867d2200 Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Thu, 23 Jul 2026 19:35:14 +0900 Subject: [PATCH] finish repo acl changes Signed-off-by: Seongmin Lee --- spindle/config/config.go | 1 + spindle/db/repos.go | 84 +++-------- spindle/db/repos_test.go | 143 ------------------ spindle/embedtap.go | 3 +- spindle/server.go | 68 ++------- spindle/tapclient.go | 33 +++-- spindle/tapclient_test.go | 301 +++----------------------------------- spindle/xrpc/xrpc_test.go | 21 ++- tapc/tap.go | 3 + 9 files changed, 90 insertions(+), 567 deletions(-) delete mode 100644 spindle/db/repos_test.go diff --git a/spindle/config/config.go b/spindle/config/config.go index 634c0f7c..47255a1e 100644 --- a/spindle/config/config.go +++ b/spindle/config/config.go @@ -24,6 +24,7 @@ type Server struct { QueueSize int `env:"QUEUE_SIZE, default=100"` MaxJobCount int `env:"MAX_JOB_COUNT, default=2"` // max number of pipelines that run at a time DockerSocket string `env:"DOCKER_SOCKET"` // path to a docker socket to expose to workflow containers + InviteOnly bool `env:"INVITE_ONLY, default=true"` } type Tap struct { diff --git a/spindle/db/repos.go b/spindle/db/repos.go index 5c37f2a0..997992b4 100644 --- a/spindle/db/repos.go +++ b/spindle/db/repos.go @@ -15,47 +15,37 @@ type Repo struct { CreatedAt string } -func (d *DB) AddRepo(repo Repo) error { +func (d *DB) UpsertRepo(repo Repo) error { var createdAt sql.NullString if repo.CreatedAt != "" { createdAt = sql.NullString{String: repo.CreatedAt, Valid: true} } _, err := d.Exec( - `insert into repos (knot, owner, rkey, repo_did, created_at) - values (?, ?, ?, ?, ?) - on conflict(owner, rkey) do update set - knot = excluded.knot, - repo_did = excluded.repo_did, - created_at = coalesce(excluded.created_at, repos.created_at)`, - repo.Knot, repo.Owner.String(), repo.Rkey.String(), repo.RepoDid.String(), createdAt, + `insert or replace into repos (repo_did, knot, owner, rkey, created_at) + values (?, ?, ?, ?, ?)`, + repo.RepoDid, repo.Knot, repo.Owner, repo.Rkey, createdAt, ) return err } -func (d *DB) CollapseRepoSiblings(owner, repoDid syntax.DID) (int64, error) { - res, err := d.Exec( - `delete from repos - where owner = ? - and repo_did = ? - and ( - (created_at is null and exists ( - select 1 from repos r2 - where r2.owner = repos.owner - and r2.repo_did = repos.repo_did - and r2.created_at is not null - and r2.rkey <> repos.rkey - )) - or (created_at is not null and created_at < ( - select max(created_at) from repos - where owner = ? and repo_did = ? and created_at is not null - )) - )`, - owner.String(), repoDid.String(), owner.String(), repoDid.String(), - ) +func (d *DB) RepoOwners() ([]syntax.DID, error) { + repos, err := d.AllRepos() if err != nil { - return 0, err + return nil, err + } + seen := make(map[syntax.DID]struct{}, len(repos)) + dids := make([]syntax.DID, 0, len(repos)) + for _, r := range repos { + if r.Owner == "" { + continue + } + if _, ok := seen[r.Owner]; ok { + continue + } + seen[r.Owner] = struct{}{} + dids = append(dids, r.Owner) } - return res.RowsAffected() + return dids, nil } func (d *DB) Knots() ([]string, error) { @@ -94,33 +84,6 @@ func scanRepo(row interface{ Scan(...any) error }) (*Repo, error) { }, nil } -func (d *DB) SiblingRkeysForRepoDid(owner, repoDid syntax.DID, excludeRkey syntax.RecordKey) ([]string, error) { - rows, err := d.Query( - `select rkey from repos - where owner = ? - and coalesce(repo_did, '') = ? - and rkey <> ?`, - owner.String(), repoDid.String(), excludeRkey.String(), - ) - if err != nil { - return nil, err - } - defer rows.Close() - - var collect func(acc []string) ([]string, error) - collect = func(acc []string) ([]string, error) { - if !rows.Next() { - return acc, rows.Err() - } - var r string - if err := rows.Scan(&r); err != nil { - return acc, err - } - return collect(append(acc, r)) - } - return collect(nil) -} - func (d *DB) GetRepoByDid(repoDid syntax.DID) (*Repo, error) { return scanRepo(d.QueryRow( `select knot, owner, rkey, repo_did from repos where repo_did = ?`, @@ -128,13 +91,6 @@ func (d *DB) GetRepoByDid(repoDid syntax.DID) (*Repo, error) { )) } -func (d *DB) GetRepoByOwnerRkey(owner syntax.DID, rkey syntax.RecordKey) (*Repo, error) { - return scanRepo(d.QueryRow( - `select knot, owner, rkey, repo_did from repos where owner = ? and rkey = ?`, - owner.String(), rkey.String(), - )) -} - func (d *DB) AllRepos() ([]Repo, error) { rows, err := d.Query(`select knot, owner, rkey, repo_did from repos`) if err != nil { diff --git a/spindle/db/repos_test.go b/spindle/db/repos_test.go deleted file mode 100644 index d631c29c..00000000 --- a/spindle/db/repos_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package db - -import ( - "context" - "path/filepath" - "testing" - - "github.com/bluesky-social/indigo/atproto/syntax" -) - -func newTestDB(t *testing.T) *DB { - t.Helper() - d, err := Make(context.Background(), filepath.Join(t.TempDir(), "spindle.db")) - if err != nil { - t.Fatalf("Make: %v", err) - } - t.Cleanup(func() { d.Close() }) - return d -} - -func TestCollapseRepoSiblings_DeletesStaleNullCreatedAtWithDifferentRkey(t *testing.T) { - d := newTestDB(t) - owner := syntax.DID("did:plc:akshay") - repoDid := syntax.DID("did:plc:boltless") - - if _, err := d.Exec(`insert into repos (knot, owner, rkey, repo_did, created_at) values - ('k', ?, 'stale-bogus-rkey', ?, null), - ('k', ?, 'fresh-pds-rkey', ?, '2024-06-01T00:00:00Z')`, - owner.String(), repoDid.String(), - owner.String(), repoDid.String()); err != nil { - t.Fatalf("seed: %v", err) - } - - n, err := d.CollapseRepoSiblings(owner, repoDid) - if err != nil { - t.Fatalf("CollapseRepoSiblings: %v", err) - } - if n != 1 { - t.Errorf("expected 1 stale row deleted, got %d", n) - } - - var rkey string - if err := d.QueryRow(`select rkey from repos where owner = ? and repo_did = ?`, - owner.String(), repoDid.String()).Scan(&rkey); err != nil { - t.Fatalf("query: %v", err) - } - if rkey != "fresh-pds-rkey" { - t.Errorf("expected fresh row preserved, got rkey=%q", rkey) - } -} - -func TestCollapseRepoSiblings_KeepsNullCreatedAtWhenAlone(t *testing.T) { - d := newTestDB(t) - owner := syntax.DID("did:plc:akshay") - repoDid := syntax.DID("did:plc:boltless") - - if _, err := d.Exec(`insert into repos (knot, owner, rkey, repo_did, created_at) values - ('k', ?, 'sole-row', ?, null)`, - owner.String(), repoDid.String()); err != nil { - t.Fatalf("seed: %v", err) - } - - n, err := d.CollapseRepoSiblings(owner, repoDid) - if err != nil { - t.Fatalf("CollapseRepoSiblings: %v", err) - } - if n != 0 { - t.Errorf("expected 0 deletions when only NULL row exists, got %d", n) - } - - var count int - if err := d.QueryRow(`select count(*) from repos where owner = ?`, owner.String()).Scan(&count); err != nil { - t.Fatalf("count: %v", err) - } - if count != 1 { - t.Errorf("sole NULL row should survive, got %d remaining", count) - } -} - -func TestCollapseRepoSiblings_OlderTimestampLoses(t *testing.T) { - d := newTestDB(t) - owner := syntax.DID("did:plc:akshay") - repoDid := syntax.DID("did:plc:boltless") - - if _, err := d.Exec(`insert into repos (knot, owner, rkey, repo_did, created_at) values - ('k', ?, 'older-rkey', ?, '2024-01-01T00:00:00Z'), - ('k', ?, 'newer-rkey', ?, '2024-06-01T00:00:00Z')`, - owner.String(), repoDid.String(), - owner.String(), repoDid.String()); err != nil { - t.Fatalf("seed: %v", err) - } - - n, err := d.CollapseRepoSiblings(owner, repoDid) - if err != nil { - t.Fatalf("CollapseRepoSiblings: %v", err) - } - if n != 1 { - t.Errorf("expected older row collapsed, got %d", n) - } - - var rkey string - if err := d.QueryRow(`select rkey from repos where owner = ? and repo_did = ?`, - owner.String(), repoDid.String()).Scan(&rkey); err != nil { - t.Fatalf("query: %v", err) - } - if rkey != "newer-rkey" { - t.Errorf("expected newer row preserved, got rkey=%q", rkey) - } -} - -func TestCollapseRepoSiblings_KeepsNullRowWithMatchingRkey(t *testing.T) { - d := newTestDB(t) - owner := syntax.DID("did:plc:akshay") - repoDid := syntax.DID("did:plc:boltless") - - if _, err := d.Exec(`insert into repos (knot, owner, rkey, repo_did, created_at) values - ('k', ?, 'matched-rkey', ?, null)`, - owner.String(), repoDid.String()); err != nil { - t.Fatalf("seed: %v", err) - } - - if err := d.AddRepo(Repo{ - Knot: "k", - Owner: owner, - Rkey: "matched-rkey", - RepoDid: repoDid, - CreatedAt: "2024-06-01T00:00:00Z", - }); err != nil { - t.Fatalf("AddRepo upsert: %v", err) - } - - if _, err := d.CollapseRepoSiblings(owner, repoDid); err != nil { - t.Fatalf("CollapseRepoSiblings: %v", err) - } - - var count int - if err := d.QueryRow(`select count(*) from repos where owner = ?`, owner.String()).Scan(&count); err != nil { - t.Fatalf("count: %v", err) - } - if count != 1 { - t.Errorf("upserted row should be the single survivor, got %d", count) - } -} diff --git a/spindle/embedtap.go b/spindle/embedtap.go index f1cc11d6..7d29f58e 100644 --- a/spindle/embedtap.go +++ b/spindle/embedtap.go @@ -63,7 +63,8 @@ func newEmbeddedTapConfig(cfg *config.Config) tap.Config { RepoFetchTimeout: 5 * time.Minute, IdentityCacheSize: 50_000, EventCacheSize: 10_000, - CollectionFilters: []string{tangled.RepoNSID, tangled.RepoCollaboratorNSID}, + FullNetworkMode: !cfg.Server.InviteOnly, + CollectionFilters: []string{tangled.RepoNSID}, AdminPassword: cfg.Server.Tap.AdminPassword, RetryTimeout: 60 * time.Second, } diff --git a/spindle/server.go b/spindle/server.go index db984cea..b185ed0d 100644 --- a/spindle/server.go +++ b/spindle/server.go @@ -119,23 +119,18 @@ func New(ctx context.Context, cfg *config.Config, d *db.DB, engines map[string]m // pull records are created by arbitrary users too, same hack as in tap jc.ExemptCollection(tangled.RepoPullNSID) - // Check if the spindle knows about any Dids; - dids, err := d.ListAllowedMembers() - if err != nil { - return nil, fmt.Errorf("failed to get all dids: %w", err) - } - for _, d := range dids { - jc.AddDid(d) - } - - knownRepos, err := d.AllRepos() - if err != nil { - return nil, fmt.Errorf("failed to get known repos: %w", err) - } - for _, r := range knownRepos { - if r.Owner != "" { - jc.AddDid(r.Owner.String()) + if cfg.Server.InviteOnly { + // listen to allowed users + members, err := d.ListAllowedMembers() + if err != nil { + return nil, fmt.Errorf("failed to get all members: %w", err) } + for _, d := range members { + jc.AddDid(d) + } + } else { + // public spindle. listen to full network + jc.ExemptCollection(tangled.RepoNSID) } resolver := idresolver.DefaultResolver(cfg.Server.PlcUrl) @@ -291,31 +286,6 @@ func (s *Spindle) Start(ctx context.Context) error { return http.ListenAndServe(s.cfg.Server.ListenAddr, s.Router()) } -func (s *Spindle) declareTapInterest(ctx context.Context) { - repos, err := s.db.AllRepos() - if err != nil { - s.l.Warn("tap declare: failed to load known repos", "err", err) - return - } - seen := make(map[syntax.DID]struct{}, len(repos)) - dids := make([]syntax.DID, 0, len(repos)) - for _, r := range repos { - if r.Owner == "" { - continue - } - if _, ok := seen[r.Owner]; ok { - continue - } - seen[r.Owner] = struct{}{} - dids = append(dids, r.Owner) - } - if err := s.tap.AddOwnerDIDs(ctx, dids); err != nil { - s.l.Warn("tap declare: AddRepos rejected", "count", len(dids), "err", err) - return - } - s.l.Info("tap declare: known owner DIDs registered", "count", len(dids)) -} - func Run(ctx context.Context) error { cfg, err := config.Load(ctx) if err != nil { @@ -442,7 +412,7 @@ func (s *Spindle) processKnotStream(ctx context.Context, src eventconsumer.Sourc return nil } -func (s *Spindle) ingestKnotCollaborator(ctx context.Context, l *slog.Logger, src eventconsumer.Source, msg eventstream.Event) error { +func (s *Spindle) ingestKnotCollaborator(_ context.Context, l *slog.Logger, src eventconsumer.Source, msg eventstream.Event) error { var rec knotdb.RepoCollaboratorUpdate if err := json.Unmarshal(msg.EventJson, &rec); err != nil { l.Error("error unmarshalling collaboratorUpdate", "err", err) @@ -853,17 +823,3 @@ func ensureGitVersion() error { } return nil } - -func (s *Spindle) resolvePipelineRepoDid(repo *tangled.Pipeline_TriggerRepo) (syntax.DID, error) { - if repo.RepoDid == nil || *repo.RepoDid == "" { - return "", fmt.Errorf("pipeline trigger missing repoDid") - } - repoDid, err := syntax.ParseDID(*repo.RepoDid) - if err != nil { - return "", fmt.Errorf("parse repoDid %s: %w", *repo.RepoDid, err) - } - if _, err := s.db.GetRepoByDid(repoDid); err != nil { - return "", fmt.Errorf("unknown repoDid %s: %w", repoDid, err) - } - return repoDid, nil -} diff --git a/spindle/tapclient.go b/spindle/tapclient.go index 7e23d089..c6e99fab 100644 --- a/spindle/tapclient.go +++ b/spindle/tapclient.go @@ -37,13 +37,6 @@ func NewTapClient(s *Spindle) *Tap { } } -func (t *Tap) AddOwnerDIDs(ctx context.Context, dids []syntax.DID) error { - if len(dids) == 0 { - return nil - } - return t.tap.AddRepos(ctx, dids) -} - func (t *Tap) Start(connCtx context.Context) { go t.tap.Connect(connCtx, &tapc.SimpleIndexer{ EventHandler: t.processEvent, @@ -52,7 +45,23 @@ func (t *Tap) Start(connCtx context.Context) { } func (t *Tap) onConnect(ctx context.Context) { - t.spindle.declareTapInterest(ctx) + l := t.logger + if t.spindle.cfg.Server.InviteOnly { + // listen to owners of registered repositories + owners, err := t.spindle.db.RepoOwners() + if err != nil { + l.Warn("tap declare: failed to load known repos", "err", err) + return + } + if err := t.tap.AddRepos(ctx, owners); err != nil { + l.Warn("tap declare: AddRepos rejected", "count", len(owners), "err", err) + return + } + l.Info("tap declare: known owner DIDs registered", "count", len(owners)) + } else { + // public spindle. listen to full network + l.Info("tap declare: listening to full network") + } } func (t *Tap) processEvent(ctx context.Context, evt tapc.Event) error { @@ -143,7 +152,7 @@ func (t *Tap) processRepo(ctx context.Context, evt *tapc.RecordEventData) error CreatedAt: record.CreatedAt, } - if err := t.spindle.db.AddRepo(repo); err != nil { + if err := t.spindle.db.UpsertRepo(repo); err != nil { l.Error("failed to add repo row", "err", err) return fmt.Errorf("add repo: %w", err) } @@ -161,12 +170,6 @@ func (t *Tap) processRepo(ctx context.Context, evt *tapc.RecordEventData) error } migrateLegacyRepoSecrets(ctx, t.spindle.db, t.spindle.vault, l, ownerDid, legacyName, rkey, repoDid) - if removed, err := t.spindle.db.CollapseRepoSiblings(ownerDid, repoDid); err != nil { - l.Warn("collapse rename siblings failed", "err", err) - } else if removed > 0 { - l.Info("collapsed rename leftovers", "owner", ownerDid, "repo_did", repoDid, "removed", removed) - } - if e := t.spindle.embedTap; e == nil || !e.closed.Load() { if err := t.tap.AddRepos(ctx, []syntax.DID{ownerDid}); err != nil { l.Warn("tap AddRepos rejected", "did", ownerDid, "err", err) diff --git a/spindle/tapclient_test.go b/spindle/tapclient_test.go index a46a9d3c..12ec5ea9 100644 --- a/spindle/tapclient_test.go +++ b/spindle/tapclient_test.go @@ -72,20 +72,16 @@ func TestProcessRepo_MembershipCheck(t *testing.T) { ownerDid := syntax.DID("did:plc:memberowner") nonMemberDid := syntax.DID("did:plc:nonmemberowner") - repoDid := "did:plc:testrepo123" + repoDid := syntax.DID("did:plc:testrepo123") - err := e.AddSpindle(rbac.ThisServer) - if err != nil { - t.Fatalf("AddSpindle: %v", err) - } - err = e.AddSpindleMember(rbac.ThisServer, ownerDid.String()) + err := e.AddSpindleMember(rbac.ThisServer, ownerDid.String()) if err != nil { t.Fatalf("AddSpindleMember: %v", err) } recNonMember := tangled.Repo{ Knot: "knot.test", - RepoDid: &repoDid, + RepoDid: (*string)(&repoDid), Spindle: &cfg.Server.Hostname, CreatedAt: time.Now().Format(time.RFC3339), } @@ -103,14 +99,14 @@ func TestProcessRepo_MembershipCheck(t *testing.T) { t.Fatalf("processRepo returned error for non-member: %v", err) } - _, err = d.GetRepoByOwnerRkey(nonMemberDid, "test-repo-rkey") + _, err = d.GetRepoByDid(repoDid) if err == nil { t.Fatal("repo for non-member was registered in DB, expected rejection") } recMember := tangled.Repo{ Knot: "knot.test", - RepoDid: &repoDid, + RepoDid: (*string)(&repoDid), Spindle: &cfg.Server.Hostname, CreatedAt: time.Now().Format(time.RFC3339), } @@ -133,7 +129,7 @@ func TestProcessRepo_MembershipCheck(t *testing.T) { } } -func TestProcessPull_PushAllowedCheck(t *testing.T) { +func TestProcessPull_IsCollaboratorCheck(t *testing.T) { d, e := newTestSpindleDB(t) cfg := &config.Config{} @@ -158,7 +154,7 @@ func TestProcessPull_PushAllowedCheck(t *testing.T) { pusherDid := syntax.DID("did:plc:pusher") repoDid := syntax.DID("did:plc:testrepo123") - err := d.AddRepo(db.Repo{ + err := d.UpsertRepo(db.Repo{ Knot: "knot.test", Owner: repoOwnerDid, Rkey: "test-repo-rkey", @@ -169,11 +165,11 @@ func TestProcessPull_PushAllowedCheck(t *testing.T) { t.Fatalf("AddRepo: %v", err) } - err = e.AddRepo(repoOwnerDid.String(), rbac.ThisServer, repoDid.String()) + err = e.SetRepoOwner(repoOwnerDid, repoDid) if err != nil { t.Fatalf("AddRepo permissions: %v", err) } - err = e.AddCollaborator(pusherDid.String(), rbac.ThisServer, repoDid.String()) + err = e.AddRepoCollaborator(pusherDid, repoDid) if err != nil { t.Fatalf("AddCollaborator: %v", err) } @@ -251,13 +247,9 @@ func TestProcessRepo_HijackRepoDidCheck(t *testing.T) { aliceDid := syntax.DID("did:plc:alice") bobDid := syntax.DID("did:plc:bob") - repoDid := "did:plc:sharedrepo" + repoDid := syntax.DID("did:plc:sharedrepo") - err := e.AddSpindle(rbac.ThisServer) - if err != nil { - t.Fatalf("AddSpindle: %v", err) - } - err = e.AddSpindleMember(rbac.ThisServer, aliceDid.String()) + err := e.AddSpindleMember(rbac.ThisServer, aliceDid.String()) if err != nil { t.Fatalf("AddSpindleMember alice: %v", err) } @@ -266,7 +258,7 @@ func TestProcessRepo_HijackRepoDidCheck(t *testing.T) { t.Fatalf("AddSpindleMember bob: %v", err) } - err = d.AddRepo(db.Repo{ + err = d.UpsertRepo(db.Repo{ Knot: "knot.test", Owner: aliceDid, Rkey: "alice-repo", @@ -280,7 +272,7 @@ func TestProcessRepo_HijackRepoDidCheck(t *testing.T) { // bob tries to register alice's repo did, must reject the hijack recBob := tangled.Repo{ Knot: "knot.test", - RepoDid: &repoDid, + RepoDid: (*string)(&repoDid), Spindle: &cfg.Server.Hostname, CreatedAt: time.Now().Format(time.RFC3339), } @@ -298,154 +290,12 @@ func TestProcessRepo_HijackRepoDidCheck(t *testing.T) { t.Fatalf("processRepo returned error on duplicate repoDid hijack attempt: %v", err) } - _, err = d.GetRepoByOwnerRkey(bobDid, "bob-repo") + _, err = d.GetRepoByDid(repoDid) if err == nil { t.Fatal("bob successfully hijacked alice's repoDid in DB, expected rejection") } } -func TestProcessCollaborator_RBAC(t *testing.T) { - d, e := newTestSpindleDB(t) - - cfg := &config.Config{} - cfg.Server.Hostname = "spindle.test" - - ownerDid := syntax.DID("did:plc:repoowner") - otherDid := syntax.DID("did:plc:otheractor") - subjectDid := syntax.DID("did:plc:collabsubject") - repoDid := syntax.DID("did:plc:testrepo123") - - h, err := syntax.ParseHandle("collabsubject.test") - if err != nil { - t.Fatalf("syntax.ParseHandle: %v", err) - } - mockIdent := &identity.Identity{ - DID: subjectDid, - Handle: h, - } - resolver := idresolver.NewMockResolver(&mockDirectory{ident: mockIdent}) - - jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) - if jcerr != nil { - t.Fatalf("NewJetstreamClient: %v", jcerr) - } - s := &Spindle{ - db: d, - e: e, - l: slog.Default(), - cfg: cfg, - res: resolver, - jc: jc, - rootCtx: context.Background(), - } - - tap := &Tap{ - spindle: s, - logger: slog.Default(), - } - - err = d.AddRepo(db.Repo{ - Knot: "knot.test", - Owner: ownerDid, - Rkey: "test-repo-rkey", - RepoDid: repoDid, - CreatedAt: time.Now().Format(time.RFC3339), - }) - if err != nil { - t.Fatalf("AddRepo: %v", err) - } - - collabRecord := tangled.RepoCollaborator{ - Subject: subjectDid.String(), - Repo: repoDid.String(), - } - collabRecordJson, _ := json.Marshal(collabRecord) - - err = tap.processCollaborator(context.Background(), &tapc.RecordEventData{ - Live: true, - Did: otherDid, - Rkey: "collab-rkey-1", - Collection: syntax.NSID(tangled.RepoCollaboratorNSID), - Action: tapc.RecordCreateAction, - Record: collabRecordJson, - }) - if err != nil { - t.Fatalf("processCollaborator returned error: %v", err) - } - - _, err = d.GetRepoCollaborator(otherDid, "collab-rkey-1") - if err == nil { - t.Fatal("collaborator from non-owner was registered in DB") - } - - err = tap.processCollaborator(context.Background(), &tapc.RecordEventData{ - Live: true, - Did: ownerDid, - Rkey: "collab-rkey-2", - Collection: syntax.NSID(tangled.RepoCollaboratorNSID), - Action: tapc.RecordCreateAction, - Record: collabRecordJson, - }) - if err != nil { - t.Fatalf("processCollaborator returned error: %v", err) - } - _, err = d.GetRepoCollaborator(ownerDid, "collab-rkey-2") - if err == nil { - t.Fatal("collaborator registered despite missing Casbin invite permission") - } - - err = e.AddRepo(ownerDid.String(), rbac.ThisServer, repoDid.String()) - if err != nil { - t.Fatalf("AddRepo permissions: %v", err) - } - - err = tap.processCollaborator(context.Background(), &tapc.RecordEventData{ - Live: true, - Did: ownerDid, - Rkey: "collab-rkey-3", - Collection: syntax.NSID(tangled.RepoCollaboratorNSID), - Action: tapc.RecordCreateAction, - Record: collabRecordJson, - }) - if err != nil { - t.Fatalf("processCollaborator failed for authorized owner: %v", err) - } - - c, err := d.GetRepoCollaborator(ownerDid, "collab-rkey-3") - if err != nil { - t.Fatalf("GetRepoCollaborator error: %v", err) - } - if c.Subject != subjectDid || c.RepoDid != repoDid { - t.Fatalf("unexpected collaborator: %+v", c) - } - - ok, err := e.IsRepoCollaborator(subjectDid.String(), rbac.ThisServer, repoDid.String()) - if err != nil || !ok { - t.Fatalf("Casbin policy for collaborator missing or err: %v", err) - } - - err = tap.processCollaborator(context.Background(), &tapc.RecordEventData{ - Live: true, - Did: ownerDid, - Rkey: "collab-rkey-3", - Collection: syntax.NSID(tangled.RepoCollaboratorNSID), - Action: tapc.RecordDeleteAction, - }) - if err != nil { - t.Fatalf("delete collaborator process returned error: %v", err) - } - - _, err = d.GetRepoCollaborator(ownerDid, "collab-rkey-3") - if err == nil { - t.Fatal("collaborator DB row remained after deletion") - } - - ok, err = e.IsRepoCollaborator(subjectDid.String(), rbac.ThisServer, repoDid.String()) - if err != nil || ok { - t.Fatal("Casbin policy for collaborator remained after deletion") - } -} - func TestTeardownRepo_RBAC(t *testing.T) { d, e := newTestSpindleDB(t) @@ -474,7 +324,7 @@ func TestTeardownRepo_RBAC(t *testing.T) { repoDid := syntax.DID("did:plc:testrepo123") collabDid := syntax.DID("did:plc:collab") - err := d.AddRepo(db.Repo{ + err := d.UpsertRepo(db.Repo{ Knot: "knot.test", Owner: ownerDid, Rkey: "test-repo-rkey", @@ -485,22 +335,12 @@ func TestTeardownRepo_RBAC(t *testing.T) { t.Fatalf("AddRepo DB: %v", err) } - err = e.AddRepo(ownerDid.String(), rbac.ThisServer, repoDid.String()) + err = e.SetRepoOwner(ownerDid, repoDid) if err != nil { t.Fatalf("AddRepo policy: %v", err) } - err = d.AddRepoCollaborator(db.RepoCollaborator{ - OwnerDid: ownerDid, - Rkey: "collab-rkey", - Subject: collabDid, - RepoDid: repoDid, - }) - if err != nil { - t.Fatalf("AddCollaborator DB: %v", err) - } - - err = e.AddCollaborator(collabDid.String(), rbac.ThisServer, repoDid.String()) + err = e.AddRepoCollaborator(collabDid, repoDid) if err != nil { t.Fatalf("AddCollaborator policy: %v", err) } @@ -516,25 +356,17 @@ func TestTeardownRepo_RBAC(t *testing.T) { t.Fatalf("processRepo delete returned error: %v", err) } - _, err = d.GetRepoByOwnerRkey(ownerDid, "test-repo-rkey") + _, err = d.GetRepoByDid(repoDid) if err == nil { t.Fatal("repo remained in DB after delete") } - collabs, err := d.ListCollaboratorsByRepoDid(repoDid) - if err != nil { - t.Fatalf("ListCollaboratorsByRepoDid: %v", err) - } - if len(collabs) > 0 { - t.Fatal("collaborators remained in DB after delete") - } - - ok, err := e.IsRepoOwner(ownerDid.String(), rbac.ThisServer, repoDid.String()) + ok, err := e.IsRepoOwner(ownerDid, repoDid) if err != nil || ok { t.Fatal("repo owner policy remained in Casbin after delete") } - ok, err = e.IsRepoCollaborator(collabDid.String(), rbac.ThisServer, repoDid.String()) + ok, err = e.IsRepoCollaborator(collabDid, repoDid) if err != nil || ok { t.Fatal("collaborator policy remained in Casbin after delete") } @@ -569,7 +401,7 @@ func TestProcessRepo_ForgeDeleteRejection(t *testing.T) { bobDid := syntax.DID("did:plc:bob") repoDid := syntax.DID("did:plc:sharedrepo") - err := d.AddRepo(db.Repo{ + err := d.UpsertRepo(db.Repo{ Knot: "knot.test", Owner: aliceDid, Rkey: "test-repo-rkey", @@ -580,7 +412,7 @@ func TestProcessRepo_ForgeDeleteRejection(t *testing.T) { t.Fatalf("AddRepo DB: %v", err) } - err = e.AddRepo(aliceDid.String(), rbac.ThisServer, repoDid.String()) + err = e.SetRepoOwner(aliceDid, repoDid) if err != nil { t.Fatalf("AddRepo policy: %v", err) } @@ -597,98 +429,13 @@ func TestProcessRepo_ForgeDeleteRejection(t *testing.T) { t.Fatalf("processRepo returned error on delete: %v", err) } - _, err = d.GetRepoByOwnerRkey(aliceDid, "test-repo-rkey") + _, err = d.GetRepoByDid(repoDid) if err != nil { t.Fatalf("Alice's repo was deleted or error: %v", err) } - ok, err := e.IsRepoOwner(aliceDid.String(), rbac.ThisServer, repoDid.String()) + ok, err := e.IsRepoOwner(aliceDid, repoDid) if err != nil || !ok { t.Fatal("Alice's owner policy was removed from Casbin by forged delete") } } - -func TestProcessCollaborator_ForgeDeleteRejection(t *testing.T) { - d, e := newTestSpindleDB(t) - - cfg := &config.Config{} - cfg.Server.Hostname = "spindle.test" - - jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) - if jcerr != nil { - t.Fatalf("NewJetstreamClient: %v", jcerr) - } - - s := &Spindle{ - db: d, - e: e, - l: slog.Default(), - cfg: cfg, - res: idresolver.DefaultResolver("https://plc.test"), - jc: jc, - rootCtx: context.Background(), - } - - tap := &Tap{ - spindle: s, - logger: slog.Default(), - } - - ownerDid := syntax.DID("did:plc:repoowner") - bobDid := syntax.DID("did:plc:bob") - collabDid := syntax.DID("did:plc:collab") - repoDid := syntax.DID("did:plc:testrepo123") - - err := d.AddRepo(db.Repo{ - Knot: "knot.test", - Owner: ownerDid, - Rkey: "test-repo-rkey", - RepoDid: repoDid, - CreatedAt: time.Now().Format(time.RFC3339), - }) - if err != nil { - t.Fatalf("AddRepo: %v", err) - } - - err = e.AddRepo(ownerDid.String(), rbac.ThisServer, repoDid.String()) - if err != nil { - t.Fatalf("AddRepo permissions: %v", err) - } - - err = d.AddRepoCollaborator(db.RepoCollaborator{ - OwnerDid: ownerDid, - Rkey: "collab-rkey", - Subject: collabDid, - RepoDid: repoDid, - }) - if err != nil { - t.Fatalf("AddRepoCollaborator: %v", err) - } - - err = e.AddCollaborator(collabDid.String(), rbac.ThisServer, repoDid.String()) - if err != nil { - t.Fatalf("AddCollaborator policy: %v", err) - } - - // bob tries to delete alice's collaborator, must reject forged delete - err = tap.processCollaborator(context.Background(), &tapc.RecordEventData{ - Live: true, - Did: bobDid, - Rkey: "collab-rkey", - Collection: syntax.NSID(tangled.RepoCollaboratorNSID), - Action: tapc.RecordDeleteAction, - }) - if err != nil { - t.Fatalf("processCollaborator delete returned error: %v", err) - } - - _, err = d.GetRepoCollaborator(ownerDid, "collab-rkey") - if err != nil { - t.Fatalf("collaborator was deleted from DB: %v", err) - } - - ok, err := e.IsRepoCollaborator(collabDid.String(), rbac.ThisServer, repoDid.String()) - if err != nil || !ok { - t.Fatal("collaborator policy was removed from Casbin by forged delete") - } -} diff --git a/spindle/xrpc/xrpc_test.go b/spindle/xrpc/xrpc_test.go index 9aec07e5..38960959 100644 --- a/spindle/xrpc/xrpc_test.go +++ b/spindle/xrpc/xrpc_test.go @@ -16,7 +16,7 @@ import ( "github.com/bluesky-social/indigo/atproto/syntax" "tangled.org/core/api/tangled" "tangled.org/core/idresolver" - "tangled.org/core/rbac" + "tangled.org/core/rbac/v2" "tangled.org/core/spindle/config" "tangled.org/core/spindle/db" "tangled.org/core/spindle/models" @@ -44,7 +44,6 @@ func newTestXrpcDB(t *testing.T) (*db.DB, *rbac.Enforcer) { if err != nil { t.Fatalf("rbac.NewEnforcer: %v", err) } - e.E.EnableAutoSave(true) return d, e } @@ -56,7 +55,7 @@ func TestTriggerPipeline_RBAC(t *testing.T) { pusherDid := syntax.DID("did:plc:pusher") repoDid := syntax.DID("did:plc:testrepo123") - err := d.AddRepo(db.Repo{ + err := d.UpsertRepo(db.Repo{ Knot: "knot.test", Owner: repoOwnerDid, Rkey: "test-repo-rkey", @@ -67,11 +66,11 @@ func TestTriggerPipeline_RBAC(t *testing.T) { t.Fatalf("AddRepo: %v", err) } - err = e.AddRepo(repoOwnerDid.String(), rbac.ThisServer, repoDid.String()) + err = e.SetRepoOwner(repoOwnerDid, repoDid) if err != nil { t.Fatalf("AddRepo permissions: %v", err) } - err = e.AddCollaborator(pusherDid.String(), rbac.ThisServer, repoDid.String()) + err = e.AddRepoCollaborator(pusherDid, repoDid) if err != nil { t.Fatalf("AddCollaborator: %v", err) } @@ -149,7 +148,7 @@ func TestCancelPipeline_RBAC(t *testing.T) { pusherDid := syntax.DID("did:plc:pusher") repoDid := syntax.DID("did:plc:testrepo123") - err := d.AddRepo(db.Repo{ + err := d.UpsertRepo(db.Repo{ Knot: "knot.test", Owner: repoOwnerDid, Rkey: "test-repo-rkey", @@ -160,11 +159,11 @@ func TestCancelPipeline_RBAC(t *testing.T) { t.Fatalf("AddRepo: %v", err) } - err = e.AddRepo(repoOwnerDid.String(), rbac.ThisServer, repoDid.String()) + err = e.SetRepoOwner(repoOwnerDid, repoDid) if err != nil { t.Fatalf("AddRepo permissions: %v", err) } - err = e.AddCollaborator(pusherDid.String(), rbac.ThisServer, repoDid.String()) + err = e.AddRepoCollaborator(pusherDid, repoDid) if err != nil { t.Fatalf("AddCollaborator: %v", err) } @@ -260,7 +259,7 @@ func TestSecrets_RBAC(t *testing.T) { pusherDid := syntax.DID("did:plc:pusher") repoDid := syntax.DID("did:plc:testrepo123") - err := d.AddRepo(db.Repo{ + err := d.UpsertRepo(db.Repo{ Knot: "knot.test", Owner: repoOwnerDid, Rkey: "test-repo-rkey", @@ -271,11 +270,11 @@ func TestSecrets_RBAC(t *testing.T) { t.Fatalf("AddRepo: %v", err) } - err = e.AddRepo(repoOwnerDid.String(), rbac.ThisServer, repoDid.String()) + err = e.SetRepoOwner(repoOwnerDid, repoDid) if err != nil { t.Fatalf("AddRepo permissions: %v", err) } - err = e.AddCollaborator(pusherDid.String(), rbac.ThisServer, repoDid.String()) + err = e.AddRepoCollaborator(pusherDid, repoDid) if err != nil { t.Fatalf("AddCollaborator: %v", err) } diff --git a/tapc/tap.go b/tapc/tap.go index c44ca19b..71279808 100644 --- a/tapc/tap.go +++ b/tapc/tap.go @@ -41,6 +41,9 @@ func NewClient(url, adminPassword string) Client { } func (c *Client) AddRepos(ctx context.Context, dids []syntax.DID) error { + if len(dids) == 0 { + return nil + } body, err := json.Marshal(map[string][]syntax.DID{"dids": dids}) if err != nil { return err -- 2.51.2