Monorepo for Tangled tangled.org

spindle: switch to rbac2

This commit won't work without following spindle rewrite to use tap and
introduce backfill because repos table is empty yet.

Signed-off-by: Seongmin Lee <git@boltless.me>

boltless.me 062f8de4 34e120e7

verified
+11 -11
spindle/config/config.go
··· 9 9 ) 10 10 11 11 type Server struct { 12 - ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:6555"` 13 - DBPath string `env:"DB_PATH, default=spindle.db"` 14 - Hostname string `env:"HOSTNAME, required"` 15 - JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"` 16 - PlcUrl string `env:"PLC_URL, default=https://plc.directory"` 17 - Dev bool `env:"DEV, default=false"` 18 - Owner string `env:"OWNER, required"` 19 - Secrets Secrets `env:",prefix=SECRETS_"` 20 - LogDir string `env:"LOG_DIR, default=/var/log/spindle"` 21 - QueueSize int `env:"QUEUE_SIZE, default=100"` 22 - MaxJobCount int `env:"MAX_JOB_COUNT, default=2"` // max number of jobs that run at a time 12 + ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:6555"` 13 + DBPath string `env:"DB_PATH, default=spindle.db"` 14 + Hostname string `env:"HOSTNAME, required"` 15 + JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"` 16 + PlcUrl string `env:"PLC_URL, default=https://plc.directory"` 17 + Dev bool `env:"DEV, default=false"` 18 + Owner syntax.DID `env:"OWNER, required"` 19 + Secrets Secrets `env:",prefix=SECRETS_"` 20 + LogDir string `env:"LOG_DIR, default=/var/log/spindle"` 21 + QueueSize int `env:"QUEUE_SIZE, default=100"` 22 + MaxJobCount int `env:"MAX_JOB_COUNT, default=2"` // max number of jobs that run at a time 23 23 } 24 24 25 25 func (s Server) Did() syntax.DID {
+17 -41
spindle/ingester.go
··· 9 9 10 10 "tangled.org/core/api/tangled" 11 11 "tangled.org/core/eventconsumer" 12 - "tangled.org/core/rbac" 13 12 "tangled.org/core/spindle/db" 14 13 15 14 comatproto "github.com/bluesky-social/indigo/api/atproto" 16 - "github.com/bluesky-social/indigo/atproto/identity" 17 15 "github.com/bluesky-social/indigo/atproto/syntax" 18 16 "github.com/bluesky-social/indigo/xrpc" 19 17 "github.com/bluesky-social/jetstream/pkg/models" 20 - securejoin "github.com/cyphar/filepath-securejoin" 21 18 ) 22 19 23 20 type Ingester func(ctx context.Context, e *models.Event) error ··· 79 76 return fmt.Errorf("domain mismatch: %s != %s", record.Instance, domain) 80 77 } 81 78 82 - ok, err := s.e.IsSpindleInviteAllowed(did, rbacDomain) 79 + ok, err := s.e.IsSpindleMemberInviteAllowed(syntax.DID(did), s.cfg.Server.Did()) 83 80 if err != nil || !ok { 84 81 l.Error("failed to add member", "did", did, "error", err) 85 82 return fmt.Errorf("failed to enforce permissions: %w", err) ··· 96 93 return fmt.Errorf("failed to add member: %w", err) 97 94 } 98 95 99 - if err := s.e.AddSpindleMember(rbacDomain, record.Subject); err != nil { 96 + if err := s.e.AddSpindleMember(syntax.DID(record.Subject), s.cfg.Server.Did()); err != nil { 100 97 l.Error("failed to add member", "error", err) 101 98 return fmt.Errorf("failed to add member: %w", err) 102 99 } ··· 122 119 return fmt.Errorf("failed to remove member: %w", err) 123 120 } 124 121 125 - if err := s.e.RemoveSpindleMember(rbacDomain, record.Subject.String()); err != nil { 122 + if err := s.e.RemoveSpindleMember(record.Subject, s.cfg.Server.Did()); err != nil { 126 123 l.Error("failed to add member", "error", err) 127 124 return fmt.Errorf("failed to add member: %w", err) 128 125 } ··· 176 173 return fmt.Errorf("failed to add repo: %w", err) 177 174 } 178 175 179 - didSlashRepo, err := securejoin.SecureJoin(did, record.Name) 180 - if err != nil { 181 - return err 182 - } 176 + repoAt := syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", did, e.Commit.Collection, e.Commit.RKey)) 183 177 184 178 // add repo to rbac 185 - if err := s.e.AddRepo(did, rbac.ThisServer, didSlashRepo); err != nil { 179 + if err := s.e.AddRepo(repoAt); err != nil { 186 180 l.Error("failed to add repo to enforcer", "error", err) 187 181 return fmt.Errorf("failed to add repo: %w", err) 188 182 } 189 183 190 184 // add collaborators to rbac 191 - owner, err := s.res.ResolveIdent(ctx, did) 192 - if err != nil || owner.Handle.IsInvalidHandle() { 193 - return err 194 - } 195 - if err := s.fetchAndAddCollaborators(ctx, owner, didSlashRepo); err != nil { 185 + if err := s.fetchAndAddCollaborators(ctx, repoAt); err != nil { 196 186 return err 197 187 } 198 188 ··· 234 224 return nil 235 225 } 236 226 237 - // TODO: get rid of this entirely 238 - // resolve this aturi to extract the repo record 239 - owner, err := s.res.ResolveIdent(ctx, repoAt.Authority().String()) 240 - if err != nil || owner.Handle.IsInvalidHandle() { 241 - return fmt.Errorf("failed to resolve handle: %w", err) 242 - } 243 - 244 - xrpcc := xrpc.Client{ 245 - Host: owner.PDSEndpoint(), 246 - } 247 - 248 - resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 249 - if err != nil { 250 - return err 251 - } 252 - 253 - repo := resp.Value.Val.(*tangled.Repo) 254 - didSlashRepo, _ := securejoin.SecureJoin(owner.DID.String(), repo.Name) 255 - 256 227 // check perms for this user 257 - if ok, err := s.e.IsCollaboratorInviteAllowed(owner.DID.String(), rbac.ThisServer, didSlashRepo); !ok || err != nil { 228 + if ok, err := s.e.IsRepoCollaboratorInviteAllowed(syntax.DID(e.Did), repoAt); !ok || err != nil { 258 229 return fmt.Errorf("insufficient permissions: %w", err) 259 230 } 260 231 261 232 // add collaborator to rbac 262 - if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, didSlashRepo); err != nil { 233 + if err := s.e.AddRepoCollaborator(syntax.DID(record.Subject), repoAt); err != nil { 263 234 l.Error("failed to add repo to enforcer", "error", err) 264 235 return fmt.Errorf("failed to add repo: %w", err) 265 236 } ··· 269 240 return nil 270 241 } 271 242 272 - func (s *Spindle) fetchAndAddCollaborators(ctx context.Context, owner *identity.Identity, didSlashRepo string) error { 243 + func (s *Spindle) fetchAndAddCollaborators(ctx context.Context, repo syntax.ATURI) error { 273 244 l := s.l.With("component", "ingester", "handler", "fetchAndAddCollaborators") 274 245 275 246 l.Info("fetching and adding existing collaborators") 276 247 248 + ident, err := s.res.ResolveIdent(ctx, repo.Authority().String()) 249 + if err != nil || ident.Handle.IsInvalidHandle() { 250 + return fmt.Errorf("failed to resolve handle: %w", err) 251 + } 252 + 277 253 xrpcc := xrpc.Client{ 278 - Host: owner.PDSEndpoint(), 254 + Host: ident.PDSEndpoint(), 279 255 } 280 256 281 - resp, err := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoCollaboratorNSID, "", 50, owner.DID.String(), false) 257 + resp, err := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoCollaboratorNSID, "", 50, ident.DID.String(), false) 282 258 if err != nil { 283 259 return err 284 260 } ··· 290 266 } 291 267 record := r.Value.Val.(*tangled.RepoCollaborator) 292 268 293 - if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, didSlashRepo); err != nil { 269 + if err := s.e.AddRepoCollaborator(syntax.DID(record.Subject), syntax.ATURI(record.Repo)); err != nil { 294 270 l.Error("failed to add repo to enforcer", "error", err) 295 271 errors.Join(errs, fmt.Errorf("failed to add repo: %w", err)) 296 272 }
+6 -47
spindle/server.go
··· 17 17 "tangled.org/core/jetstream" 18 18 "tangled.org/core/log" 19 19 "tangled.org/core/notifier" 20 - "tangled.org/core/rbac" 20 + "tangled.org/core/rbac2" 21 21 "tangled.org/core/spindle/config" 22 22 "tangled.org/core/spindle/db" 23 23 "tangled.org/core/spindle/engine" ··· 32 32 //go:embed motd 33 33 var motd []byte 34 34 35 - const ( 36 - rbacDomain = "thisserver" 37 - ) 38 - 39 35 type Spindle struct { 40 36 jc *jetstream.JetstreamClient 41 37 db *db.DB 42 - e *rbac.Enforcer 38 + e *rbac2.Enforcer 43 39 l *slog.Logger 44 40 n *notifier.Notifier 45 41 engs map[string]models.Engine ··· 59 55 return nil, fmt.Errorf("failed to setup db: %w", err) 60 56 } 61 57 62 - e, err := rbac.NewEnforcer(cfg.Server.DBPath) 58 + e, err := rbac2.NewEnforcer(cfg.Server.DBPath) 63 59 if err != nil { 64 60 return nil, fmt.Errorf("failed to setup rbac enforcer: %w", err) 65 61 } 66 - e.E.EnableAutoSave(true) 67 62 68 63 n := notifier.New() 69 64 ··· 104 99 if err != nil { 105 100 return nil, fmt.Errorf("failed to setup jetstream client: %w", err) 106 101 } 107 - jc.AddDid(cfg.Server.Owner) 102 + jc.AddDid(cfg.Server.Owner.String()) 108 103 109 104 // Check if the spindle knows about any Dids; 110 105 dids, err := d.GetAllDids() ··· 130 125 vault: vault, 131 126 } 132 127 133 - err = e.AddSpindle(rbacDomain) 134 - if err != nil { 135 - return nil, fmt.Errorf("failed to set rbac domain: %w", err) 136 - } 137 - err = spindle.configureOwner() 128 + err = e.SetSpindleOwner(spindle.cfg.Server.Owner, spindle.cfg.Server.Did()) 138 129 if err != nil { 139 130 return nil, err 140 131 } ··· 197 188 } 198 189 199 190 // Enforcer returns the RBAC enforcer instance. 200 - func (s *Spindle) Enforcer() *rbac.Enforcer { 191 + func (s *Spindle) Enforcer() *rbac2.Enforcer { 201 192 return s.e 202 193 } 203 194 ··· 382 373 383 374 return nil 384 375 } 385 - 386 - func (s *Spindle) configureOwner() error { 387 - cfgOwner := s.cfg.Server.Owner 388 - 389 - existing, err := s.e.GetSpindleUsersByRole("server:owner", rbacDomain) 390 - if err != nil { 391 - return err 392 - } 393 - 394 - switch len(existing) { 395 - case 0: 396 - // no owner configured, continue 397 - case 1: 398 - // find existing owner 399 - existingOwner := existing[0] 400 - 401 - // no ownership change, this is okay 402 - if existingOwner == s.cfg.Server.Owner { 403 - break 404 - } 405 - 406 - // remove existing owner 407 - err = s.e.RemoveSpindleOwner(rbacDomain, existingOwner) 408 - if err != nil { 409 - return nil 410 - } 411 - default: 412 - return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", s.cfg.Server.DBPath) 413 - } 414 - 415 - return s.e.AddSpindleOwner(rbacDomain, cfgOwner) 416 - }
+1 -2
spindle/xrpc/add_secret.go
··· 11 11 "github.com/bluesky-social/indigo/xrpc" 12 12 securejoin "github.com/cyphar/filepath-securejoin" 13 13 "tangled.org/core/api/tangled" 14 - "tangled.org/core/rbac" 15 14 "tangled.org/core/spindle/secrets" 16 15 xrpcerr "tangled.org/core/xrpc/errors" 17 16 ) ··· 68 67 return 69 68 } 70 69 71 - if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 70 + if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil { 72 71 l.Error("insufficent permissions", "did", actorDid.String()) 73 72 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 74 73 return
+1 -2
spindle/xrpc/list_secrets.go
··· 11 11 "github.com/bluesky-social/indigo/xrpc" 12 12 securejoin "github.com/cyphar/filepath-securejoin" 13 13 "tangled.org/core/api/tangled" 14 - "tangled.org/core/rbac" 15 14 "tangled.org/core/spindle/secrets" 16 15 xrpcerr "tangled.org/core/xrpc/errors" 17 16 ) ··· 63 62 return 64 63 } 65 64 66 - if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 65 + if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil { 67 66 l.Error("insufficent permissions", "did", actorDid.String()) 68 67 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 69 68 return
+1 -1
spindle/xrpc/owner.go
··· 9 9 ) 10 10 11 11 func (x *Xrpc) Owner(w http.ResponseWriter, r *http.Request) { 12 - owner := x.Config.Server.Owner 12 + owner := x.Config.Server.Owner.String() 13 13 if owner == "" { 14 14 writeError(w, xrpcerr.OwnerNotFoundError, http.StatusInternalServerError) 15 15 return
+1 -26
spindle/xrpc/pipeline_cancelPipeline.go
··· 6 6 "net/http" 7 7 "strings" 8 8 9 - "github.com/bluesky-social/indigo/api/atproto" 10 9 "github.com/bluesky-social/indigo/atproto/syntax" 11 - "github.com/bluesky-social/indigo/xrpc" 12 - securejoin "github.com/cyphar/filepath-securejoin" 13 10 "tangled.org/core/api/tangled" 14 - "tangled.org/core/rbac" 15 11 "tangled.org/core/spindle/models" 16 12 xrpcerr "tangled.org/core/xrpc/errors" 17 13 ) ··· 53 49 return 54 50 } 55 51 56 - ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String()) 57 - if err != nil || ident.Handle.IsInvalidHandle() { 58 - fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 59 - return 60 - } 61 - 62 - xrpcc := xrpc.Client{Host: ident.PDSEndpoint()} 63 - resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 64 - if err != nil { 65 - fail(xrpcerr.GenericError(err)) 66 - return 67 - } 68 - 69 - repo := resp.Value.Val.(*tangled.Repo) 70 - didSlashRepo, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) 71 - if err != nil { 72 - fail(xrpcerr.GenericError(err)) 73 - return 74 - } 75 - 76 - // TODO: fine-grained role based control 77 - isRepoOwner, err := x.Enforcer.IsRepoOwner(actorDid.String(), rbac.ThisServer, didSlashRepo) 52 + isRepoOwner, err := x.Enforcer.IsRepoOwner(actorDid, repoAt) 78 53 if err != nil || !isRepoOwner { 79 54 fail(xrpcerr.AccessControlError(actorDid.String())) 80 55 return
+1 -2
spindle/xrpc/remove_secret.go
··· 10 10 "github.com/bluesky-social/indigo/xrpc" 11 11 securejoin "github.com/cyphar/filepath-securejoin" 12 12 "tangled.org/core/api/tangled" 13 - "tangled.org/core/rbac" 14 13 "tangled.org/core/spindle/secrets" 15 14 xrpcerr "tangled.org/core/xrpc/errors" 16 15 ) ··· 62 61 return 63 62 } 64 63 65 - if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 64 + if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil { 66 65 l.Error("insufficent permissions", "did", actorDid.String()) 67 66 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 68 67 return
+2 -2
spindle/xrpc/xrpc.go
··· 11 11 "tangled.org/core/api/tangled" 12 12 "tangled.org/core/idresolver" 13 13 "tangled.org/core/notifier" 14 - "tangled.org/core/rbac" 14 + "tangled.org/core/rbac2" 15 15 "tangled.org/core/spindle/config" 16 16 "tangled.org/core/spindle/db" 17 17 "tangled.org/core/spindle/models" ··· 25 25 type Xrpc struct { 26 26 Logger *slog.Logger 27 27 Db *db.DB 28 - Enforcer *rbac.Enforcer 28 + Enforcer *rbac2.Enforcer 29 29 Engines map[string]models.Engine 30 30 Config *config.Config 31 31 Resolver *idresolver.Resolver