Monorepo for Tangled tangled.org
5

Configure Feed

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

rbac,knotserver,spindle: txn + querier for member acl

Lewis: May this revision serve well! <lewis@tangled.org>

author
Lewis
committer
Tangled
date (May 27, 2026, 10:34 AM +0300) commit 38475e4b parent d0d2c9ac change-id uvyulxtp
+342 -33
+28
knotserver/db/db.go
··· 24 24 Exec(query string, args ...any) (sql.Result, error) 25 25 } 26 26 27 + func (d *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error) { 28 + return d.db.BeginTx(ctx, opts) 29 + } 30 + 31 + func (d *DB) Exec(query string, args ...any) (sql.Result, error) { 32 + return d.db.Exec(query, args...) 33 + } 34 + 35 + func (d *DB) QueryRow(query string, args ...any) *sql.Row { 36 + return d.db.QueryRow(query, args...) 37 + } 38 + 27 39 func Setup(ctx context.Context, dbPath string) (*DB, error) { 28 40 // https://github.com/mattn/go-sqlite3#connection-string 29 41 opts := []string{ ··· 173 185 alter table repo_keys_new rename to repo_keys; 174 186 create unique index if not exists idx_repo_keys_owner_repo 175 187 on repo_keys(owner_did, repo_name); 188 + `) 189 + return mErr 190 + }); err != nil { 191 + return nil, err 192 + } 193 + 194 + if err := orm.RunMigration(conn, logger, "create-knot-members", func(tx *sql.Tx) error { 195 + _, mErr := tx.ExecContext(ctx, ` 196 + create table if not exists knot_members ( 197 + id integer primary key autoincrement, 198 + did text not null, 199 + rkey text not null, 200 + subject text not null, 201 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 202 + unique (did, rkey) 203 + ); 176 204 `) 177 205 return mErr 178 206 }); err != nil {
+4 -4
knotserver/db/known_dids.go
··· 1 1 package db 2 2 3 - func (d *DB) AddDid(did string) error { 4 - _, err := d.db.Exec(`insert or ignore into known_dids (did) values (?)`, did) 3 + func AddDid(q DBTX, did string) error { 4 + _, err := q.Exec(`insert or ignore into known_dids (did) values (?)`, did) 5 5 return err 6 6 } 7 7 8 - func (d *DB) RemoveDid(did string) error { 9 - _, err := d.db.Exec(`delete from known_dids where did = ?`, did) 8 + func RemoveDid(q DBTX, did string) error { 9 + _, err := q.Exec(`delete from known_dids where did = ?`, did) 10 10 return err 11 11 } 12 12
+99
knotserver/db/member.go
··· 1 + package db 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + "tangled.org/core/orm" 9 + ) 10 + 11 + type KnotMember struct { 12 + Id int 13 + Did syntax.DID 14 + Rkey string 15 + Subject syntax.DID 16 + } 17 + 18 + func (d *DB) IsMigrationApplied(name string) (bool, error) { 19 + var exists bool 20 + err := d.db.QueryRow( 21 + `select exists (select 1 from migrations where name = ?)`, 22 + name, 23 + ).Scan(&exists) 24 + return exists, err 25 + } 26 + 27 + func (d *DB) ApplyKnotMembersBackfill(ctx context.Context, rows []KnotMember, migrationName string) error { 28 + conn, err := d.db.Conn(ctx) 29 + if err != nil { 30 + return err 31 + } 32 + defer conn.Close() 33 + 34 + return orm.RunMigration(conn, d.logger, migrationName, func(tx *sql.Tx) error { 35 + for _, m := range rows { 36 + if _, err := tx.ExecContext(ctx, 37 + `insert or ignore into known_dids (did) values (?)`, 38 + m.Subject, 39 + ); err != nil { 40 + return err 41 + } 42 + if _, err := tx.ExecContext(ctx, 43 + `insert or ignore into knot_members (did, rkey, subject) values (?, ?, ?)`, 44 + m.Did, m.Rkey, m.Subject, 45 + ); err != nil { 46 + return err 47 + } 48 + } 49 + return nil 50 + }) 51 + } 52 + 53 + func AddKnotMember(q DBTX, member KnotMember) error { 54 + _, err := q.Exec( 55 + `insert or ignore into knot_members (did, rkey, subject) values (?, ?, ?)`, 56 + member.Did, 57 + member.Rkey, 58 + member.Subject, 59 + ) 60 + return err 61 + } 62 + 63 + func RemoveKnotMember(q DBTX, ownerDid, rkey string) error { 64 + _, err := q.Exec( 65 + "delete from knot_members where did = ? and rkey = ?", 66 + ownerDid, 67 + rkey, 68 + ) 69 + return err 70 + } 71 + 72 + func CountKnotMembersBySubject(q DBTX, subject string) (int, error) { 73 + var count int 74 + err := q.QueryRow( 75 + `select count(*) from knot_members where subject = ?`, 76 + subject, 77 + ).Scan(&count) 78 + return count, err 79 + } 80 + 81 + func GetKnotMember(q DBTX, did, rkey string) (*KnotMember, error) { 82 + query := 83 + `select id, did, rkey, subject 84 + from knot_members 85 + where did = ? and rkey = ?` 86 + 87 + var member KnotMember 88 + err := q.QueryRow(query, did, rkey).Scan( 89 + &member.Id, 90 + &member.Did, 91 + &member.Rkey, 92 + &member.Subject, 93 + ) 94 + if err != nil { 95 + return nil, err 96 + } 97 + 98 + return &member, nil 99 + }
+2 -2
knotserver/router.go
··· 194 194 } 195 195 196 196 // remove existing owner 197 - if err = h.db.RemoveDid(existingOwner); err != nil { 197 + if err = db.RemoveDid(h.db, existingOwner); err != nil { 198 198 return err 199 199 } 200 200 if err = h.e.RemoveKnotOwner(rbacDomain, existingOwner); err != nil { ··· 205 205 return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", h.c.Server.DBPath) 206 206 } 207 207 208 - if err = h.db.AddDid(cfgOwner); err != nil { 208 + if err = db.AddDid(h.db, cfgOwner); err != nil { 209 209 return fmt.Errorf("failed to add owner to DB: %w", err) 210 210 } 211 211 if err := h.e.AddKnotOwner(rbacDomain, cfgOwner); err != nil {
+22 -2
rbac/rbac.go
··· 126 126 } 127 127 128 128 func (e *Enforcer) AddKnotMember(domain, member string) error { 129 - return e.addMember(domain, member) 129 + _, err := e.addMember(domain, member) 130 + return err 130 131 } 131 132 132 133 func (e *Enforcer) RemoveKnotMember(domain, member string) error { 134 + _, err := e.removeMember(domain, member) 135 + return err 136 + } 137 + 138 + func (e *Enforcer) TryAddKnotMember(domain, member string) (bool, error) { 139 + return e.addMember(domain, member) 140 + } 141 + 142 + func (e *Enforcer) TryRemoveKnotMember(domain, member string) (bool, error) { 133 143 return e.removeMember(domain, member) 134 144 } 135 145 ··· 142 152 } 143 153 144 154 func (e *Enforcer) AddSpindleMember(domain, member string) error { 145 - return e.addMember(intoSpindle(domain), member) 155 + _, err := e.addMember(intoSpindle(domain), member) 156 + return err 146 157 } 147 158 148 159 func (e *Enforcer) RemoveSpindleMember(domain, member string) error { 160 + _, err := e.removeMember(intoSpindle(domain), member) 161 + return err 162 + } 163 + 164 + func (e *Enforcer) TryAddSpindleMember(domain, member string) (bool, error) { 165 + return e.addMember(intoSpindle(domain), member) 166 + } 167 + 168 + func (e *Enforcer) TryRemoveSpindleMember(domain, member string) (bool, error) { 149 169 return e.removeMember(intoSpindle(domain), member) 150 170 } 151 171
+41
rbac/txn.go
··· 1 + package rbac 2 + 3 + import ( 4 + "database/sql" 5 + "log/slog" 6 + "slices" 7 + ) 8 + 9 + type Txn struct { 10 + SQLTx *sql.Tx 11 + undos []func() error 12 + committed bool 13 + } 14 + 15 + func NewTxn(sqlTx *sql.Tx) *Txn { 16 + return &Txn{SQLTx: sqlTx} 17 + } 18 + 19 + func (t *Txn) AddUndo(undo func() error) { 20 + t.undos = append(t.undos, undo) 21 + } 22 + 23 + func (t *Txn) Commit() error { 24 + if err := t.SQLTx.Commit(); err != nil { 25 + return err 26 + } 27 + t.committed = true 28 + return nil 29 + } 30 + 31 + func (t *Txn) Cleanup(l *slog.Logger) { 32 + if t.committed { 33 + return 34 + } 35 + t.SQLTx.Rollback() 36 + for _, undo := range slices.Backward(t.undos) { 37 + if err := undo(); err != nil { 38 + l.Error("failed to reverse ACL change", "err", err) 39 + } 40 + } 41 + }
+19 -6
rbac/util.go
··· 34 34 return err 35 35 } 36 36 37 - func (e *Enforcer) addMember(domain, member string) error { 38 - _, err := e.E.AddGroupingPolicy(member, "server:member", domain) 39 - return err 37 + func (e *Enforcer) addMember(domain, member string) (bool, error) { 38 + return e.E.AddGroupingPolicy(member, "server:member", domain) 40 39 } 41 40 42 - func (e *Enforcer) removeMember(domain, member string) error { 43 - _, err := e.E.RemoveGroupingPolicy(member, "server:member", domain) 44 - return err 41 + func (e *Enforcer) removeMember(domain, member string) (bool, error) { 42 + return e.E.RemoveGroupingPolicy(member, "server:member", domain) 45 43 } 46 44 47 45 func (e *Enforcer) isRole(user, role, domain string) (bool, error) { ··· 57 55 58 56 func (e *Enforcer) isInviteAllowed(user, domain string) (bool, error) { 59 57 return e.E.Enforce(user, domain, domain, "server:invite") 58 + } 59 + 60 + func (e *Enforcer) HasAnyPolicyForUser(user string) (bool, error) { 61 + pPolicies, err := e.E.GetFilteredNamedPolicy("p", 0, user) 62 + if err != nil { 63 + return false, err 64 + } 65 + if len(pPolicies) > 0 { 66 + return true, nil 67 + } 68 + gPolicies, err := e.E.GetFilteredNamedGroupingPolicy("g", 0, user) 69 + if err != nil { 70 + return false, err 71 + } 72 + return len(gPolicies) > 0, nil 60 73 } 61 74 62 75 func checkRepoFormat(repo string) error {
+106 -2
spindle/db/db.go
··· 4 4 "context" 5 5 "database/sql" 6 6 "log/slog" 7 + "slices" 7 8 "strings" 8 9 9 10 _ "github.com/mattn/go-sqlite3" ··· 13 14 14 15 type DB struct { 15 16 *sql.DB 17 + } 18 + 19 + type DBTX interface { 20 + QueryRow(query string, args ...any) *sql.Row 21 + Exec(query string, args ...any) (sql.Result, error) 16 22 } 17 23 18 24 func Make(ctx context.Context, dbPath string) (*DB, error) { ··· 84 90 created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 85 91 86 92 -- constraints 87 - unique (did, instance, subject) 93 + unique (did, rkey) 88 94 ); 89 95 90 96 -- status event for a single workflow ··· 112 118 } 113 119 114 120 func runMigrations(_ context.Context, conn *sql.Conn, logger *slog.Logger) error { 115 - return orm.RunMigration(conn, logger, "repos-to-repo-did", func(tx *sql.Tx) error { 121 + if err := orm.RunMigration(conn, logger, "repos-to-repo-did", func(tx *sql.Tx) error { 116 122 var hasName int 117 123 if err := tx.QueryRow( 118 124 `select count(*) from pragma_table_info('repos') where name = 'name'`, ··· 162 168 on repo_collaborators(repo_did); 163 169 `) 164 170 return err 171 + }); err != nil { 172 + return err 173 + } 174 + 175 + return orm.RunMigration(conn, logger, "spindle-members-unique-on-rkey", func(tx *sql.Tx) error { 176 + hasTarget, err := hasUniqueIndex(tx, "spindle_members", []string{"did", "rkey"}) 177 + if err != nil { 178 + return err 179 + } 180 + if hasTarget { 181 + return nil 182 + } 183 + 184 + var totalRows, distinctRows int 185 + if err := tx.QueryRow(`select count(*) from spindle_members`).Scan(&totalRows); err != nil { 186 + return err 187 + } 188 + if err := tx.QueryRow(`select count(*) from (select 1 from spindle_members group by did, rkey)`).Scan(&distinctRows); err != nil { 189 + return err 190 + } 191 + if dropped := totalRows - distinctRows; dropped > 0 { 192 + logger.Warn("dropping duplicate (did, rkey) rows during spindle_members rebuild", "dropped", dropped, "kept", distinctRows) 193 + } 194 + 195 + _, err = tx.Exec(` 196 + create table spindle_members_new ( 197 + id integer primary key autoincrement, 198 + did text not null, 199 + rkey text not null, 200 + instance text not null, 201 + subject text not null, 202 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 203 + unique (did, rkey) 204 + ); 205 + 206 + insert into spindle_members_new (id, did, rkey, instance, subject, created) 207 + select id, did, rkey, instance, subject, created 208 + from spindle_members sm 209 + where id = ( 210 + select max(id) from spindle_members 211 + where did = sm.did and rkey = sm.rkey 212 + ); 213 + 214 + drop table spindle_members; 215 + alter table spindle_members_new rename to spindle_members; 216 + `) 217 + return err 165 218 }) 219 + } 220 + 221 + func hasUniqueIndex(tx *sql.Tx, table string, cols []string) (bool, error) { 222 + rows, err := tx.Query( 223 + `select name from pragma_index_list(?) where "unique" = 1`, 224 + table, 225 + ) 226 + if err != nil { 227 + return false, err 228 + } 229 + defer rows.Close() 230 + 231 + var indexNames []string 232 + for rows.Next() { 233 + var name string 234 + if err := rows.Scan(&name); err != nil { 235 + return false, err 236 + } 237 + indexNames = append(indexNames, name) 238 + } 239 + if err := rows.Err(); err != nil { 240 + return false, err 241 + } 242 + 243 + wantSorted := slices.Clone(cols) 244 + slices.Sort(wantSorted) 245 + 246 + for _, name := range indexNames { 247 + colRows, err := tx.Query( 248 + `select name from pragma_index_info(?) order by seqno`, 249 + name, 250 + ) 251 + if err != nil { 252 + return false, err 253 + } 254 + var got []string 255 + for colRows.Next() { 256 + var c string 257 + if err := colRows.Scan(&c); err != nil { 258 + colRows.Close() 259 + return false, err 260 + } 261 + got = append(got, c) 262 + } 263 + colRows.Close() 264 + slices.Sort(got) 265 + if slices.Equal(got, wantSorted) { 266 + return true, nil 267 + } 268 + } 269 + return false, nil 166 270 } 167 271 168 272 func (d *DB) SaveLastTimeUs(lastTimeUs int64) error {
+4 -4
spindle/db/known_dids.go
··· 1 1 package db 2 2 3 - func (d *DB) AddDid(did string) error { 4 - _, err := d.Exec(`insert or ignore into known_dids (did) values (?)`, did) 3 + func AddDid(q DBTX, did string) error { 4 + _, err := q.Exec(`insert or ignore into known_dids (did) values (?)`, did) 5 5 return err 6 6 } 7 7 8 - func (d *DB) RemoveDid(did string) error { 9 - _, err := d.Exec(`delete from known_dids where did = ?`, did) 8 + func RemoveDid(q DBTX, did string) error { 9 + _, err := q.Exec(`delete from known_dids where did = ?`, did) 10 10 return err 11 11 } 12 12
+17 -13
spindle/db/member.go
··· 1 1 package db 2 2 3 3 import ( 4 - "time" 5 - 6 4 "github.com/bluesky-social/indigo/atproto/syntax" 7 5 ) 8 6 ··· 12 10 Rkey string // rkey of the record 13 11 Instance string 14 12 Subject syntax.DID // the member being added 15 - Created time.Time 16 13 } 17 14 18 - func AddSpindleMember(db *DB, member SpindleMember) error { 19 - _, err := db.Exec( 15 + func AddSpindleMember(q DBTX, member SpindleMember) error { 16 + _, err := q.Exec( 20 17 `insert or ignore into spindle_members (did, rkey, instance, subject) values (?, ?, ?, ?)`, 21 18 member.Did, 22 19 member.Rkey, ··· 26 23 return err 27 24 } 28 25 29 - func RemoveSpindleMember(db *DB, owner_did, rkey string) error { 30 - _, err := db.Exec( 26 + func RemoveSpindleMember(q DBTX, ownerDid, rkey string) error { 27 + _, err := q.Exec( 31 28 "delete from spindle_members where did = ? and rkey = ?", 32 - owner_did, 29 + ownerDid, 33 30 rkey, 34 31 ) 35 32 return err 36 33 } 37 34 38 - func GetSpindleMember(db *DB, did, rkey string) (*SpindleMember, error) { 35 + func CountSpindleMembersBySubject(q DBTX, subject string) (int, error) { 36 + var count int 37 + err := q.QueryRow( 38 + `select count(*) from spindle_members where subject = ?`, 39 + subject, 40 + ).Scan(&count) 41 + return count, err 42 + } 43 + 44 + func GetSpindleMember(q DBTX, did, rkey string) (*SpindleMember, error) { 39 45 query := 40 - `select id, did, rkey, instance, subject, created 46 + `select id, did, rkey, instance, subject 41 47 from spindle_members 42 48 where did = ? and rkey = ?` 43 49 44 50 var member SpindleMember 45 - var createdAt string 46 - err := db.QueryRow(query, did, rkey).Scan( 51 + err := q.QueryRow(query, did, rkey).Scan( 47 52 &member.Id, 48 53 &member.Did, 49 54 &member.Rkey, 50 55 &member.Instance, 51 56 &member.Subject, 52 - &createdAt, 53 57 ) 54 58 if err != nil { 55 59 return nil, err