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 ) 10 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 23 } 24 25 func (s Server) Did() syntax.DID {
··· 9 ) 10 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 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 } 24 25 func (s Server) Did() syntax.DID {
+17 -41
spindle/ingester.go
··· 9 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/eventconsumer" 12 - "tangled.org/core/rbac" 13 "tangled.org/core/spindle/db" 14 15 comatproto "github.com/bluesky-social/indigo/api/atproto" 16 - "github.com/bluesky-social/indigo/atproto/identity" 17 "github.com/bluesky-social/indigo/atproto/syntax" 18 "github.com/bluesky-social/indigo/xrpc" 19 "github.com/bluesky-social/jetstream/pkg/models" 20 - securejoin "github.com/cyphar/filepath-securejoin" 21 ) 22 23 type Ingester func(ctx context.Context, e *models.Event) error ··· 79 return fmt.Errorf("domain mismatch: %s != %s", record.Instance, domain) 80 } 81 82 - ok, err := s.e.IsSpindleInviteAllowed(did, rbacDomain) 83 if err != nil || !ok { 84 l.Error("failed to add member", "did", did, "error", err) 85 return fmt.Errorf("failed to enforce permissions: %w", err) ··· 96 return fmt.Errorf("failed to add member: %w", err) 97 } 98 99 - if err := s.e.AddSpindleMember(rbacDomain, record.Subject); err != nil { 100 l.Error("failed to add member", "error", err) 101 return fmt.Errorf("failed to add member: %w", err) 102 } ··· 122 return fmt.Errorf("failed to remove member: %w", err) 123 } 124 125 - if err := s.e.RemoveSpindleMember(rbacDomain, record.Subject.String()); err != nil { 126 l.Error("failed to add member", "error", err) 127 return fmt.Errorf("failed to add member: %w", err) 128 } ··· 176 return fmt.Errorf("failed to add repo: %w", err) 177 } 178 179 - didSlashRepo, err := securejoin.SecureJoin(did, record.Name) 180 - if err != nil { 181 - return err 182 - } 183 184 // add repo to rbac 185 - if err := s.e.AddRepo(did, rbac.ThisServer, didSlashRepo); err != nil { 186 l.Error("failed to add repo to enforcer", "error", err) 187 return fmt.Errorf("failed to add repo: %w", err) 188 } 189 190 // 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 { 196 return err 197 } 198 ··· 234 return nil 235 } 236 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 // check perms for this user 257 - if ok, err := s.e.IsCollaboratorInviteAllowed(owner.DID.String(), rbac.ThisServer, didSlashRepo); !ok || err != nil { 258 return fmt.Errorf("insufficient permissions: %w", err) 259 } 260 261 // add collaborator to rbac 262 - if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, didSlashRepo); err != nil { 263 l.Error("failed to add repo to enforcer", "error", err) 264 return fmt.Errorf("failed to add repo: %w", err) 265 } ··· 269 return nil 270 } 271 272 - func (s *Spindle) fetchAndAddCollaborators(ctx context.Context, owner *identity.Identity, didSlashRepo string) error { 273 l := s.l.With("component", "ingester", "handler", "fetchAndAddCollaborators") 274 275 l.Info("fetching and adding existing collaborators") 276 277 xrpcc := xrpc.Client{ 278 - Host: owner.PDSEndpoint(), 279 } 280 281 - resp, err := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoCollaboratorNSID, "", 50, owner.DID.String(), false) 282 if err != nil { 283 return err 284 } ··· 290 } 291 record := r.Value.Val.(*tangled.RepoCollaborator) 292 293 - if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, didSlashRepo); err != nil { 294 l.Error("failed to add repo to enforcer", "error", err) 295 errors.Join(errs, fmt.Errorf("failed to add repo: %w", err)) 296 }
··· 9 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/eventconsumer" 12 "tangled.org/core/spindle/db" 13 14 comatproto "github.com/bluesky-social/indigo/api/atproto" 15 "github.com/bluesky-social/indigo/atproto/syntax" 16 "github.com/bluesky-social/indigo/xrpc" 17 "github.com/bluesky-social/jetstream/pkg/models" 18 ) 19 20 type Ingester func(ctx context.Context, e *models.Event) error ··· 76 return fmt.Errorf("domain mismatch: %s != %s", record.Instance, domain) 77 } 78 79 + ok, err := s.e.IsSpindleMemberInviteAllowed(syntax.DID(did), s.cfg.Server.Did()) 80 if err != nil || !ok { 81 l.Error("failed to add member", "did", did, "error", err) 82 return fmt.Errorf("failed to enforce permissions: %w", err) ··· 93 return fmt.Errorf("failed to add member: %w", err) 94 } 95 96 + if err := s.e.AddSpindleMember(syntax.DID(record.Subject), s.cfg.Server.Did()); err != nil { 97 l.Error("failed to add member", "error", err) 98 return fmt.Errorf("failed to add member: %w", err) 99 } ··· 119 return fmt.Errorf("failed to remove member: %w", err) 120 } 121 122 + if err := s.e.RemoveSpindleMember(record.Subject, s.cfg.Server.Did()); err != nil { 123 l.Error("failed to add member", "error", err) 124 return fmt.Errorf("failed to add member: %w", err) 125 } ··· 173 return fmt.Errorf("failed to add repo: %w", err) 174 } 175 176 + repoAt := syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", did, e.Commit.Collection, e.Commit.RKey)) 177 178 // add repo to rbac 179 + if err := s.e.AddRepo(repoAt); err != nil { 180 l.Error("failed to add repo to enforcer", "error", err) 181 return fmt.Errorf("failed to add repo: %w", err) 182 } 183 184 // add collaborators to rbac 185 + if err := s.fetchAndAddCollaborators(ctx, repoAt); err != nil { 186 return err 187 } 188 ··· 224 return nil 225 } 226 227 // check perms for this user 228 + if ok, err := s.e.IsRepoCollaboratorInviteAllowed(syntax.DID(e.Did), repoAt); !ok || err != nil { 229 return fmt.Errorf("insufficient permissions: %w", err) 230 } 231 232 // add collaborator to rbac 233 + if err := s.e.AddRepoCollaborator(syntax.DID(record.Subject), repoAt); err != nil { 234 l.Error("failed to add repo to enforcer", "error", err) 235 return fmt.Errorf("failed to add repo: %w", err) 236 } ··· 240 return nil 241 } 242 243 + func (s *Spindle) fetchAndAddCollaborators(ctx context.Context, repo syntax.ATURI) error { 244 l := s.l.With("component", "ingester", "handler", "fetchAndAddCollaborators") 245 246 l.Info("fetching and adding existing collaborators") 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 + 253 xrpcc := xrpc.Client{ 254 + Host: ident.PDSEndpoint(), 255 } 256 257 + resp, err := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoCollaboratorNSID, "", 50, ident.DID.String(), false) 258 if err != nil { 259 return err 260 } ··· 266 } 267 record := r.Value.Val.(*tangled.RepoCollaborator) 268 269 + if err := s.e.AddRepoCollaborator(syntax.DID(record.Subject), syntax.ATURI(record.Repo)); err != nil { 270 l.Error("failed to add repo to enforcer", "error", err) 271 errors.Join(errs, fmt.Errorf("failed to add repo: %w", err)) 272 }
+6 -47
spindle/server.go
··· 17 "tangled.org/core/jetstream" 18 "tangled.org/core/log" 19 "tangled.org/core/notifier" 20 - "tangled.org/core/rbac" 21 "tangled.org/core/spindle/config" 22 "tangled.org/core/spindle/db" 23 "tangled.org/core/spindle/engine" ··· 32 //go:embed motd 33 var motd []byte 34 35 - const ( 36 - rbacDomain = "thisserver" 37 - ) 38 - 39 type Spindle struct { 40 jc *jetstream.JetstreamClient 41 db *db.DB 42 - e *rbac.Enforcer 43 l *slog.Logger 44 n *notifier.Notifier 45 engs map[string]models.Engine ··· 59 return nil, fmt.Errorf("failed to setup db: %w", err) 60 } 61 62 - e, err := rbac.NewEnforcer(cfg.Server.DBPath) 63 if err != nil { 64 return nil, fmt.Errorf("failed to setup rbac enforcer: %w", err) 65 } 66 - e.E.EnableAutoSave(true) 67 68 n := notifier.New() 69 ··· 104 if err != nil { 105 return nil, fmt.Errorf("failed to setup jetstream client: %w", err) 106 } 107 - jc.AddDid(cfg.Server.Owner) 108 109 // Check if the spindle knows about any Dids; 110 dids, err := d.GetAllDids() ··· 130 vault: vault, 131 } 132 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() 138 if err != nil { 139 return nil, err 140 } ··· 197 } 198 199 // Enforcer returns the RBAC enforcer instance. 200 - func (s *Spindle) Enforcer() *rbac.Enforcer { 201 return s.e 202 } 203 ··· 382 383 return nil 384 } 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 - }
··· 17 "tangled.org/core/jetstream" 18 "tangled.org/core/log" 19 "tangled.org/core/notifier" 20 + "tangled.org/core/rbac2" 21 "tangled.org/core/spindle/config" 22 "tangled.org/core/spindle/db" 23 "tangled.org/core/spindle/engine" ··· 32 //go:embed motd 33 var motd []byte 34 35 type Spindle struct { 36 jc *jetstream.JetstreamClient 37 db *db.DB 38 + e *rbac2.Enforcer 39 l *slog.Logger 40 n *notifier.Notifier 41 engs map[string]models.Engine ··· 55 return nil, fmt.Errorf("failed to setup db: %w", err) 56 } 57 58 + e, err := rbac2.NewEnforcer(cfg.Server.DBPath) 59 if err != nil { 60 return nil, fmt.Errorf("failed to setup rbac enforcer: %w", err) 61 } 62 63 n := notifier.New() 64 ··· 99 if err != nil { 100 return nil, fmt.Errorf("failed to setup jetstream client: %w", err) 101 } 102 + jc.AddDid(cfg.Server.Owner.String()) 103 104 // Check if the spindle knows about any Dids; 105 dids, err := d.GetAllDids() ··· 125 vault: vault, 126 } 127 128 + err = e.SetSpindleOwner(spindle.cfg.Server.Owner, spindle.cfg.Server.Did()) 129 if err != nil { 130 return nil, err 131 } ··· 188 } 189 190 // Enforcer returns the RBAC enforcer instance. 191 + func (s *Spindle) Enforcer() *rbac2.Enforcer { 192 return s.e 193 } 194 ··· 373 374 return nil 375 }
+1 -2
spindle/xrpc/add_secret.go
··· 11 "github.com/bluesky-social/indigo/xrpc" 12 securejoin "github.com/cyphar/filepath-securejoin" 13 "tangled.org/core/api/tangled" 14 - "tangled.org/core/rbac" 15 "tangled.org/core/spindle/secrets" 16 xrpcerr "tangled.org/core/xrpc/errors" 17 ) ··· 68 return 69 } 70 71 - if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 72 l.Error("insufficent permissions", "did", actorDid.String()) 73 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 74 return
··· 11 "github.com/bluesky-social/indigo/xrpc" 12 securejoin "github.com/cyphar/filepath-securejoin" 13 "tangled.org/core/api/tangled" 14 "tangled.org/core/spindle/secrets" 15 xrpcerr "tangled.org/core/xrpc/errors" 16 ) ··· 67 return 68 } 69 70 + if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil { 71 l.Error("insufficent permissions", "did", actorDid.String()) 72 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 73 return
+1 -2
spindle/xrpc/list_secrets.go
··· 11 "github.com/bluesky-social/indigo/xrpc" 12 securejoin "github.com/cyphar/filepath-securejoin" 13 "tangled.org/core/api/tangled" 14 - "tangled.org/core/rbac" 15 "tangled.org/core/spindle/secrets" 16 xrpcerr "tangled.org/core/xrpc/errors" 17 ) ··· 63 return 64 } 65 66 - if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 67 l.Error("insufficent permissions", "did", actorDid.String()) 68 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 69 return
··· 11 "github.com/bluesky-social/indigo/xrpc" 12 securejoin "github.com/cyphar/filepath-securejoin" 13 "tangled.org/core/api/tangled" 14 "tangled.org/core/spindle/secrets" 15 xrpcerr "tangled.org/core/xrpc/errors" 16 ) ··· 62 return 63 } 64 65 + if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil { 66 l.Error("insufficent permissions", "did", actorDid.String()) 67 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 68 return
+1 -1
spindle/xrpc/owner.go
··· 9 ) 10 11 func (x *Xrpc) Owner(w http.ResponseWriter, r *http.Request) { 12 - owner := x.Config.Server.Owner 13 if owner == "" { 14 writeError(w, xrpcerr.OwnerNotFoundError, http.StatusInternalServerError) 15 return
··· 9 ) 10 11 func (x *Xrpc) Owner(w http.ResponseWriter, r *http.Request) { 12 + owner := x.Config.Server.Owner.String() 13 if owner == "" { 14 writeError(w, xrpcerr.OwnerNotFoundError, http.StatusInternalServerError) 15 return
+1 -26
spindle/xrpc/pipeline_cancelPipeline.go
··· 6 "net/http" 7 "strings" 8 9 - "github.com/bluesky-social/indigo/api/atproto" 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 - "github.com/bluesky-social/indigo/xrpc" 12 - securejoin "github.com/cyphar/filepath-securejoin" 13 "tangled.org/core/api/tangled" 14 - "tangled.org/core/rbac" 15 "tangled.org/core/spindle/models" 16 xrpcerr "tangled.org/core/xrpc/errors" 17 ) ··· 53 return 54 } 55 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) 78 if err != nil || !isRepoOwner { 79 fail(xrpcerr.AccessControlError(actorDid.String())) 80 return
··· 6 "net/http" 7 "strings" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/spindle/models" 12 xrpcerr "tangled.org/core/xrpc/errors" 13 ) ··· 49 return 50 } 51 52 + isRepoOwner, err := x.Enforcer.IsRepoOwner(actorDid, repoAt) 53 if err != nil || !isRepoOwner { 54 fail(xrpcerr.AccessControlError(actorDid.String())) 55 return
+1 -2
spindle/xrpc/remove_secret.go
··· 10 "github.com/bluesky-social/indigo/xrpc" 11 securejoin "github.com/cyphar/filepath-securejoin" 12 "tangled.org/core/api/tangled" 13 - "tangled.org/core/rbac" 14 "tangled.org/core/spindle/secrets" 15 xrpcerr "tangled.org/core/xrpc/errors" 16 ) ··· 62 return 63 } 64 65 - if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 66 l.Error("insufficent permissions", "did", actorDid.String()) 67 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 68 return
··· 10 "github.com/bluesky-social/indigo/xrpc" 11 securejoin "github.com/cyphar/filepath-securejoin" 12 "tangled.org/core/api/tangled" 13 "tangled.org/core/spindle/secrets" 14 xrpcerr "tangled.org/core/xrpc/errors" 15 ) ··· 61 return 62 } 63 64 + if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil { 65 l.Error("insufficent permissions", "did", actorDid.String()) 66 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 67 return
+2 -2
spindle/xrpc/xrpc.go
··· 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/idresolver" 13 "tangled.org/core/notifier" 14 - "tangled.org/core/rbac" 15 "tangled.org/core/spindle/config" 16 "tangled.org/core/spindle/db" 17 "tangled.org/core/spindle/models" ··· 25 type Xrpc struct { 26 Logger *slog.Logger 27 Db *db.DB 28 - Enforcer *rbac.Enforcer 29 Engines map[string]models.Engine 30 Config *config.Config 31 Resolver *idresolver.Resolver
··· 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/idresolver" 13 "tangled.org/core/notifier" 14 + "tangled.org/core/rbac2" 15 "tangled.org/core/spindle/config" 16 "tangled.org/core/spindle/db" 17 "tangled.org/core/spindle/models" ··· 25 type Xrpc struct { 26 Logger *slog.Logger 27 Db *db.DB 28 + Enforcer *rbac2.Enforcer 29 Engines map[string]models.Engine 30 Config *config.Config 31 Resolver *idresolver.Resolver