Monorepo for Tangled

rbac2: rbac enforcer rewrite

- use repo AT-URI as identifier
- use `dom` field rather than `obj` to filter by repository
So now it's "user with role A in repo B can do action D to object C"
where `A,B,C,D` are `sub,dom,obj,act`.

Many parts are missing, but should be enough to test this new RBAC
enforcer package in spindle.

Related issue: <https://tangled.org/tangled.org/core/issues/282>

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

boltless.me 779ff00a 3a99e385

verified
+722 -2
+30 -2
flake.nix
··· 91 91 spindle = self.callPackage ./nix/pkgs/spindle.nix {}; 92 92 knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {}; 93 93 knot = self.callPackage ./nix/pkgs/knot.nix {}; 94 + did-method-plc = self.callPackage ./nix/pkgs/did-method-plc.nix {}; 95 + bluesky-jetstream = self.callPackage ./nix/pkgs/bluesky-jetstream.nix {}; 96 + bluesky-relay = self.callPackage ./nix/pkgs/bluesky-relay.nix {}; 97 + tap = self.callPackage ./nix/pkgs/tap.nix {}; 94 98 }); 95 99 in { 96 100 overlays.default = final: prev: { 97 - inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview; 101 + inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview did-method-plc bluesky-jetstream bluesky-relay tap; 98 102 }; 99 103 100 104 packages = forAllSystems (system: let ··· 103 107 staticPackages = mkPackageSet pkgs.pkgsStatic; 104 108 crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic; 105 109 in { 106 - inherit (packages) appview appview-static-files lexgen goat spindle knot knot-unwrapped sqlite-lib; 110 + inherit (packages) appview appview-static-files lexgen goat spindle knot knot-unwrapped sqlite-lib did-method-plc bluesky-jetstream bluesky-relay tap; 107 111 108 112 pkgsStatic-appview = staticPackages.appview; 109 113 pkgsStatic-knot = staticPackages.knot; ··· 302 306 imports = [./nix/modules/spindle.nix]; 303 307 304 308 services.tangled.spindle.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.spindle; 309 + }; 310 + nixosModules.did-method-plc = { 311 + lib, 312 + pkgs, 313 + ... 314 + }: { 315 + imports = [./nix/modules/did-method-plc.nix]; 316 + services.did-method-plc.package = lib.mkDefault self.packages.${pkgs.system}.did-method-plc; 317 + }; 318 + nixosModules.bluesky-relay = { 319 + lib, 320 + pkgs, 321 + ... 322 + }: { 323 + imports = [./nix/modules/bluesky-relay.nix]; 324 + services.bluesky-relay.package = lib.mkDefault self.packages.${pkgs.system}.bluesky-relay; 325 + }; 326 + nixosModules.bluesky-jetstream = { 327 + lib, 328 + pkgs, 329 + ... 330 + }: { 331 + imports = [./nix/modules/bluesky-jetstream.nix]; 332 + services.bluesky-jetstream.package = lib.mkDefault self.packages.${pkgs.system}.bluesky-jetstream; 305 333 }; 306 334 }; 307 335 }
+64
nix/modules/bluesky-jetstream.nix
··· 1 + { 2 + config, 3 + pkgs, 4 + lib, 5 + ... 6 + }: let 7 + cfg = config.services.bluesky-jetstream; 8 + in 9 + with lib; { 10 + options.services.bluesky-jetstream = { 11 + enable = mkEnableOption "jetstream server"; 12 + package = mkPackageOption pkgs "bluesky-jetstream" {}; 13 + 14 + # dataDir = mkOption { 15 + # type = types.str; 16 + # default = "/var/lib/jetstream"; 17 + # description = "directory to store data (pebbleDB)"; 18 + # }; 19 + livenessTtl = mkOption { 20 + type = types.int; 21 + default = 15; 22 + description = "time to restart when no event detected (seconds)"; 23 + }; 24 + websocketUrl = mkOption { 25 + type = types.str; 26 + default = "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos"; 27 + description = "full websocket path to the ATProto SubscribeRepos XRPC endpoint"; 28 + }; 29 + }; 30 + config = mkIf cfg.enable { 31 + systemd.services.bluesky-jetstream = { 32 + description = "bluesky jetstream"; 33 + after = ["network.target" "pds.service"]; 34 + wantedBy = ["multi-user.target"]; 35 + 36 + serviceConfig = { 37 + User = "jetstream"; 38 + Group = "jetstream"; 39 + StateDirectory = "jetstream"; 40 + StateDirectoryMode = "0755"; 41 + # preStart = '' 42 + # mkdir -p "${cfg.dataDir}" 43 + # chown -R jetstream:jetstream "${cfg.dataDir}" 44 + # ''; 45 + # WorkingDirectory = cfg.dataDir; 46 + Environment = [ 47 + "JETSTREAM_DATA_DIR=/var/lib/jetstream/data" 48 + "JETSTREAM_LIVENESS_TTL=${toString cfg.livenessTtl}s" 49 + "JETSTREAM_WS_URL=${cfg.websocketUrl}" 50 + ]; 51 + ExecStart = getExe cfg.package; 52 + Restart = "always"; 53 + RestartSec = 5; 54 + }; 55 + }; 56 + users = { 57 + users.jetstream = { 58 + group = "jetstream"; 59 + isSystemUser = true; 60 + }; 61 + groups.jetstream = {}; 62 + }; 63 + }; 64 + }
+48
nix/modules/bluesky-relay.nix
··· 1 + { 2 + config, 3 + pkgs, 4 + lib, 5 + ... 6 + }: let 7 + cfg = config.services.bluesky-relay; 8 + in 9 + with lib; { 10 + options.services.bluesky-relay = { 11 + enable = mkEnableOption "relay server"; 12 + package = mkPackageOption pkgs "bluesky-relay" {}; 13 + }; 14 + config = mkIf cfg.enable { 15 + systemd.services.bluesky-relay = { 16 + description = "bluesky relay"; 17 + after = ["network.target" "pds.service"]; 18 + wantedBy = ["multi-user.target"]; 19 + 20 + serviceConfig = { 21 + User = "relay"; 22 + Group = "relay"; 23 + StateDirectory = "relay"; 24 + StateDirectoryMode = "0755"; 25 + Environment = [ 26 + "RELAY_ADMIN_PASSWORD=password" 27 + "RELAY_PLC_HOST=https://plc.tngl.boltless.dev" 28 + "DATABASE_URL=sqlite:///var/lib/relay/relay.sqlite" 29 + "RELAY_IP_BIND=:2470" 30 + "RELAY_PERSIST_DIR=/var/lib/relay" 31 + "RELAY_DISABLE_REQUEST_CRAWL=0" 32 + "RELAY_INITIAL_SEQ_NUMBER=1" 33 + "RELAY_ALLOW_INSECURE_HOSTS=1" 34 + ]; 35 + ExecStart = "${getExe cfg.package} serve"; 36 + Restart = "always"; 37 + RestartSec = 5; 38 + }; 39 + }; 40 + users = { 41 + users.relay = { 42 + group = "relay"; 43 + isSystemUser = true; 44 + }; 45 + groups.relay = {}; 46 + }; 47 + }; 48 + }
+76
nix/modules/did-method-plc.nix
··· 1 + { 2 + config, 3 + pkgs, 4 + lib, 5 + ... 6 + }: let 7 + cfg = config.services.did-method-plc; 8 + in 9 + with lib; { 10 + options.services.did-method-plc = { 11 + enable = mkEnableOption "did-method-plc server"; 12 + package = mkPackageOption pkgs "did-method-plc" {}; 13 + }; 14 + config = mkIf cfg.enable { 15 + services.postgresql = { 16 + enable = true; 17 + package = pkgs.postgresql_14; 18 + ensureDatabases = ["plc"]; 19 + ensureUsers = [ 20 + { 21 + name = "pg"; 22 + # ensurePermissions."DATABASE plc" = "ALL PRIVILEGES"; 23 + } 24 + ]; 25 + authentication = '' 26 + local all all trust 27 + host all all 127.0.0.1/32 trust 28 + ''; 29 + }; 30 + systemd.services.did-method-plc = { 31 + description = "did-method-plc"; 32 + 33 + after = ["postgresql.service"]; 34 + wants = ["postgresql.service"]; 35 + wantedBy = ["multi-user.target"]; 36 + 37 + environment = let 38 + db_creds_json = builtins.toJSON { 39 + username = "pg"; 40 + password = ""; 41 + host = "127.0.0.1"; 42 + port = 5432; 43 + }; 44 + in { 45 + # TODO: inherit from config 46 + DEBUG_MODE = "1"; 47 + LOG_ENABLED = "true"; 48 + LOG_LEVEL = "debug"; 49 + LOG_DESTINATION = "1"; 50 + ENABLE_MIGRATIONS = "true"; 51 + DB_CREDS_JSON = db_creds_json; 52 + DB_MIGRATE_CREDS_JSON = db_creds_json; 53 + PLC_VERSION = "0.0.1"; 54 + PORT = "8080"; 55 + }; 56 + 57 + serviceConfig = { 58 + ExecStart = getExe cfg.package; 59 + User = "plc"; 60 + Group = "plc"; 61 + StateDirectory = "plc"; 62 + StateDirectoryMode = "0755"; 63 + Restart = "always"; 64 + 65 + # Hardening 66 + }; 67 + }; 68 + users = { 69 + users.plc = { 70 + group = "plc"; 71 + isSystemUser = true; 72 + }; 73 + groups.plc = {}; 74 + }; 75 + }; 76 + }
+20
nix/pkgs/bluesky-jetstream.nix
··· 1 + { 2 + buildGoModule, 3 + fetchFromGitHub, 4 + }: 5 + buildGoModule { 6 + pname = "bluesky-jetstream"; 7 + version = "0.1.0"; 8 + src = fetchFromGitHub { 9 + owner = "bluesky-social"; 10 + repo = "jetstream"; 11 + rev = "7d7efa58d7f14101a80ccc4f1085953948b7d5de"; 12 + sha256 = "sha256-1e9SL/8gaDPMA4YZed51ffzgpkptbMd0VTbTTDbPTFw="; 13 + }; 14 + subPackages = ["cmd/jetstream"]; 15 + vendorHash = "sha256-/21XJQH6fo9uPzlABUAbdBwt1O90odmppH6gXu2wkiQ="; 16 + doCheck = false; 17 + meta = { 18 + mainProgram = "jetstream"; 19 + }; 20 + }
+20
nix/pkgs/bluesky-relay.nix
··· 1 + { 2 + buildGoModule, 3 + fetchFromGitHub, 4 + }: 5 + buildGoModule { 6 + pname = "bluesky-relay"; 7 + version = "0.1.0"; 8 + src = fetchFromGitHub { 9 + owner = "boltlessengineer"; 10 + repo = "indigo"; 11 + rev = "b769ea60b7dde5e2bd0b8ee3ce8462a0c0e596fe"; 12 + sha256 = "sha256-jHRY825TBYaH1WkKFUoNbo4UlMSyuHvCGjYPiBnKo44="; 13 + }; 14 + subPackages = ["cmd/relay"]; 15 + vendorHash = "sha256-UOedwNYnM8Jx6B7Y9tFcZX8IeUBESAFAPTRYk7n0yo8="; 16 + doCheck = false; 17 + meta = { 18 + mainProgram = "relay"; 19 + }; 20 + }
+65
nix/pkgs/did-method-plc.nix
··· 1 + # inspired by https://github.com/NixOS/nixpkgs/blob/333bfb7c258fab089a834555ea1c435674c459b4/pkgs/by-name/ga/gatsby-cli/package.nix 2 + { 3 + lib, 4 + stdenv, 5 + fetchFromGitHub, 6 + fetchYarnDeps, 7 + yarnConfigHook, 8 + yarnBuildHook, 9 + nodejs, 10 + makeBinaryWrapper, 11 + }: 12 + stdenv.mkDerivation (finalAttrs: { 13 + pname = "did-method-plc"; 14 + version = "0.0.1"; 15 + 16 + src = fetchFromGitHub { 17 + owner = "did-method-plc"; 18 + repo = "did-method-plc"; 19 + rev = "158ba5535ac3da4fd4309954bde41deab0b45972"; 20 + sha256 = "sha256-O5smubbrnTDMCvL6iRyMXkddr5G7YHxkQRVMRULHanQ="; 21 + }; 22 + postPatch = '' 23 + # remove dd-trace dependency 24 + sed -i '3d' packages/server/service/index.js 25 + ''; 26 + 27 + yarnOfflineCache = fetchYarnDeps { 28 + yarnLock = finalAttrs.src + "/yarn.lock"; 29 + hash = "sha256-g8GzaAbWSnWwbQjJMV2DL5/ZlWCCX0sRkjjvX3tqU4Y="; 30 + }; 31 + 32 + nativeBuildInputs = [ 33 + yarnConfigHook 34 + yarnBuildHook 35 + nodejs 36 + makeBinaryWrapper 37 + ]; 38 + yarnBuildScript = "lerna"; 39 + yarnBuildFlags = [ 40 + "run" 41 + "build" 42 + "--scope" 43 + "@did-plc/server" 44 + "--include-dependencies" 45 + ]; 46 + 47 + installPhase = '' 48 + runHook preInstall 49 + 50 + mkdir -p $out/lib/node_modules/ 51 + mv packages/ $out/lib/packages/ 52 + mv node_modules/* $out/lib/node_modules/ 53 + 54 + makeWrapper ${lib.getExe nodejs} $out/bin/plc \ 55 + --add-flags $out/lib/packages/server/service/index.js \ 56 + --add-flags --enable-source-maps \ 57 + --set NODE_PATH $out/lib/node_modules 58 + 59 + runHook postInstall 60 + ''; 61 + 62 + meta = { 63 + mainProgram = "plc"; 64 + }; 65 + })
+20
nix/pkgs/tap.nix
··· 1 + { 2 + buildGoModule, 3 + fetchFromGitHub, 4 + }: 5 + buildGoModule { 6 + pname = "tap"; 7 + version = "0.1.0"; 8 + src = fetchFromGitHub { 9 + owner = "bluesky-social"; 10 + repo = "indigo"; 11 + rev = "f92cb29224fcc60f666b20ee3514e431a58ff811"; 12 + sha256 = "sha256-35ltXnq0SJeo3j33D7Nndbcnw5XWBJLRrmZ+nCmZVQw="; 13 + }; 14 + subPackages = ["cmd/tap"]; 15 + vendorHash = "sha256-UOedwNYnM8Jx6B7Y9tFcZX8IeUBESAFAPTRYk7n0yo8="; 16 + doCheck = false; 17 + meta = { 18 + mainProgram = "tap"; 19 + }; 20 + }
+144
rbac2/rbac2.go
··· 1 + package rbac2 2 + 3 + import ( 4 + "database/sql" 5 + "fmt" 6 + 7 + adapter "github.com/Blank-Xu/sql-adapter" 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + "github.com/casbin/casbin/v2" 10 + "github.com/casbin/casbin/v2/model" 11 + "github.com/casbin/casbin/v2/util" 12 + "tangled.org/core/api/tangled" 13 + ) 14 + 15 + const ( 16 + Model = ` 17 + [request_definition] 18 + r = sub, dom, obj, act 19 + 20 + [policy_definition] 21 + p = sub, dom, obj, act 22 + 23 + [role_definition] 24 + g = _, _, _ 25 + 26 + [policy_effect] 27 + e = some(where (p.eft == allow)) 28 + 29 + [matchers] 30 + m = g(r.sub, p.sub, r.dom) && keyMatch4(r.dom, p.dom) && r.obj == p.obj && r.act == p.act 31 + ` 32 + ) 33 + 34 + type Enforcer struct { 35 + e *casbin.Enforcer 36 + } 37 + 38 + func NewEnforcer(path string) (*Enforcer, error) { 39 + m, err := model.NewModelFromString(Model) 40 + if err != nil { 41 + return nil, err 42 + } 43 + 44 + db, err := sql.Open("sqlite3", path+"?_foreign_keys=1") 45 + if err != nil { 46 + return nil, err 47 + } 48 + 49 + a, err := adapter.NewAdapter(db, "sqlite3", "acl") 50 + if err != nil { 51 + return nil, err 52 + } 53 + 54 + e, err := casbin.NewEnforcer(m, a) 55 + if err != nil { 56 + return nil, err 57 + } 58 + 59 + if err := seedTangledPolicies(e); err != nil { 60 + return nil, err 61 + } 62 + 63 + return &Enforcer{e}, nil 64 + } 65 + 66 + func seedTangledPolicies(e *casbin.Enforcer) error { 67 + // policies 68 + aturi := func(nsid string) string { 69 + return fmt.Sprintf("at://{did}/%s/{rkey}", nsid) 70 + } 71 + 72 + _, err := e.AddPoliciesEx([][]string{ 73 + // sub | dom | obj | act 74 + {"repo:owner", aturi(tangled.RepoNSID), "/", "write"}, 75 + {"repo:owner", aturi(tangled.RepoNSID), "/collaborator", "write"}, // invite 76 + {"repo:collaborator", aturi(tangled.RepoNSID), "/settings", "write"}, 77 + {"repo:collaborator", aturi(tangled.RepoNSID), "/git", "write"}, // git push 78 + 79 + {"server:owner", "/knot/{did}", "/member", "write"}, // invite 80 + {"server:member", "/knot/{did}", "/git", "write"}, 81 + 82 + {"server:owner", "/spindle/{did}", "/member", "write"}, // invite 83 + }) 84 + if err != nil { 85 + return err 86 + } 87 + 88 + // grouping policies 89 + // TODO(boltless): define our own matcher to replace keyMatch4 90 + e.AddNamedDomainMatchingFunc("g", "keyMatch4", util.KeyMatch4) 91 + _, err = e.AddGroupingPoliciesEx([][]string{ 92 + // sub | role | dom 93 + {"repo:owner", "repo:collaborator", aturi(tangled.RepoNSID)}, 94 + 95 + // using '/knot/' prefix here because knot/spindle identifiers don't 96 + // include the collection type 97 + {"server:owner", "server:member", "/knot/{did}"}, 98 + {"server:owner", "server:member", "/spindle/{did}"}, 99 + }) 100 + return err 101 + } 102 + 103 + func (e *Enforcer) hasImplicitRoleForUser(name string, role string, domain ...string) (bool, error) { 104 + roles, err := e.e.GetImplicitRolesForUser(name, domain...) 105 + if err != nil { 106 + return false, err 107 + } 108 + for _, r := range roles { 109 + if r == role { 110 + return true, nil 111 + } 112 + } 113 + return false, nil 114 + } 115 + 116 + // setRoleForUser sets single user role for specified domain. 117 + // All existing users with that role will be removed. 118 + func (e *Enforcer) setRoleForUser(name string, role string, domain ...string) error { 119 + currentUsers, err := e.e.GetUsersForRole(role, domain...) 120 + if err != nil { 121 + return err 122 + } 123 + 124 + for _, oldUser := range currentUsers { 125 + _, err = e.e.DeleteRoleForUser(oldUser, role, domain...) 126 + if err != nil { 127 + return err 128 + } 129 + } 130 + 131 + _, err = e.e.AddRoleForUser(name, role, domain...) 132 + return err 133 + } 134 + 135 + // validateAtUri enforeces AT-URI to have valid did as authority and match collection NSID. 136 + func validateAtUri(uri syntax.ATURI, expected string) error { 137 + if !uri.Authority().IsDID() { 138 + return fmt.Errorf("expected at-uri with did") 139 + } 140 + if expected != "" && uri.Collection().String() != expected { 141 + return fmt.Errorf("incorrect repo at-uri collection nsid '%s' (expected '%s')", uri.Collection(), expected) 142 + } 143 + return nil 144 + }
+115
rbac2/rbac2_test.go
··· 1 + package rbac2_test 2 + 3 + import ( 4 + "testing" 5 + 6 + "github.com/bluesky-social/indigo/atproto/syntax" 7 + _ "github.com/mattn/go-sqlite3" 8 + "github.com/stretchr/testify/assert" 9 + "tangled.org/core/rbac2" 10 + ) 11 + 12 + func setup(t *testing.T) *rbac2.Enforcer { 13 + enforcer, err := rbac2.NewEnforcer(":memory:") 14 + assert.NoError(t, err) 15 + 16 + return enforcer 17 + } 18 + 19 + func TestRepoOwnerPermissions(t *testing.T) { 20 + var ( 21 + e = setup(t) 22 + ok bool 23 + err error 24 + fooRepo = syntax.ATURI("at://did:plc:foo/sh.tangled.repo/reporkey") 25 + fooUser = syntax.DID("did:plc:foo") 26 + ) 27 + 28 + assert.NoError(t, e.AddRepo(fooRepo)) 29 + 30 + ok, err = e.IsRepoOwner(fooUser, fooRepo) 31 + assert.NoError(t, err) 32 + assert.True(t, ok, "repo author should be repo owner") 33 + 34 + ok, err = e.IsRepoWriteAllowed(fooUser, fooRepo) 35 + assert.NoError(t, err) 36 + assert.True(t, ok, "repo owner should be able to modify the repo itself") 37 + 38 + ok, err = e.IsRepoCollaborator(fooUser, fooRepo) 39 + assert.NoError(t, err) 40 + assert.True(t, ok, "repo owner should inherit role role:collaborator") 41 + 42 + ok, err = e.IsRepoSettingsWriteAllowed(fooUser, fooRepo) 43 + assert.NoError(t, err) 44 + assert.True(t, ok, "repo owner should inherit collaborator permissions") 45 + } 46 + 47 + func TestRepoCollaboratorPermissions(t *testing.T) { 48 + var ( 49 + e = setup(t) 50 + ok bool 51 + err error 52 + fooRepo = syntax.ATURI("at://did:plc:foo/sh.tangled.repo/reporkey") 53 + barUser = syntax.DID("did:plc:bar") 54 + ) 55 + 56 + assert.NoError(t, e.AddRepo(fooRepo)) 57 + assert.NoError(t, e.AddRepoCollaborator(barUser, fooRepo)) 58 + 59 + ok, err = e.IsRepoCollaborator(barUser, fooRepo) 60 + assert.NoError(t, err) 61 + assert.True(t, ok, "should set repo collaborator") 62 + 63 + ok, err = e.IsRepoSettingsWriteAllowed(barUser, fooRepo) 64 + assert.NoError(t, err) 65 + assert.True(t, ok, "repo collaborator should be able to edit repo settings") 66 + 67 + ok, err = e.IsRepoWriteAllowed(barUser, fooRepo) 68 + assert.NoError(t, err) 69 + assert.False(t, ok, "repo collaborator shouldn't be able to modify the repo itself") 70 + } 71 + 72 + func TestGetByRole(t *testing.T) { 73 + var ( 74 + e = setup(t) 75 + err error 76 + fooRepo = syntax.ATURI("at://did:plc:foo/sh.tangled.repo/reporkey") 77 + owner = syntax.DID("did:plc:foo") 78 + collaborator1 = syntax.DID("did:plc:bar") 79 + collaborator2 = syntax.DID("did:plc:baz") 80 + ) 81 + 82 + assert.NoError(t, e.AddRepo(fooRepo)) 83 + assert.NoError(t, e.AddRepoCollaborator(collaborator1, fooRepo)) 84 + assert.NoError(t, e.AddRepoCollaborator(collaborator2, fooRepo)) 85 + 86 + collaborators, err := e.GetRepoCollaborators(fooRepo) 87 + assert.NoError(t, err) 88 + assert.ElementsMatch(t, []syntax.DID{ 89 + owner, 90 + collaborator1, 91 + collaborator2, 92 + }, collaborators) 93 + } 94 + 95 + func TestSpindleOwnerPermissions(t *testing.T) { 96 + var ( 97 + e = setup(t) 98 + ok bool 99 + err error 100 + spindle = syntax.DID("did:web:spindle.example.com") 101 + owner = syntax.DID("did:plc:foo") 102 + member = syntax.DID("did:plc:bar") 103 + ) 104 + 105 + assert.NoError(t, e.SetSpindleOwner(owner, spindle)) 106 + assert.NoError(t, e.AddSpindleMember(member, spindle)) 107 + 108 + ok, err = e.IsSpindleMemberInviteAllowed(owner, spindle) 109 + assert.NoError(t, err) 110 + assert.True(t, ok, "spindle owner can invite members") 111 + 112 + ok, err = e.IsSpindleMemberInviteAllowed(member, spindle) 113 + assert.NoError(t, err) 114 + assert.False(t, ok, "spindle member cannot invite members") 115 + }
+91
rbac2/repo.go
··· 1 + package rbac2 2 + 3 + import ( 4 + "slices" 5 + "strings" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + "tangled.org/core/api/tangled" 9 + ) 10 + 11 + // AddRepo adds new repo with its owner to rbac enforcer 12 + func (e *Enforcer) AddRepo(repo syntax.ATURI) error { 13 + if err := validateAtUri(repo, tangled.RepoNSID); err != nil { 14 + return err 15 + } 16 + user := repo.Authority() 17 + 18 + return e.setRoleForUser(user.String(), "repo:owner", repo.String()) 19 + } 20 + 21 + // DeleteRepo deletes all policies related to the repo 22 + func (e *Enforcer) DeleteRepo(repo syntax.ATURI) error { 23 + if err := validateAtUri(repo, tangled.RepoNSID); err != nil { 24 + return err 25 + } 26 + 27 + _, err := e.e.DeleteDomains(repo.String()) 28 + return err 29 + } 30 + 31 + // AddRepoCollaborator adds new collaborator to the repo 32 + func (e *Enforcer) AddRepoCollaborator(user syntax.DID, repo syntax.ATURI) error { 33 + if err := validateAtUri(repo, tangled.RepoNSID); err != nil { 34 + return err 35 + } 36 + 37 + _, err := e.e.AddRoleForUser(user.String(), "repo:collaborator", repo.String()) 38 + return err 39 + } 40 + 41 + // RemoveRepoCollaborator removes the collaborator from the repo. 42 + // This won't remove inherited roles like repository owner. 43 + func (e *Enforcer) RemoveRepoCollaborator(user syntax.DID, repo syntax.ATURI) error { 44 + if err := validateAtUri(repo, tangled.RepoNSID); err != nil { 45 + return err 46 + } 47 + 48 + _, err := e.e.DeleteRoleForUser(user.String(), "repo:collaborator", repo.String()) 49 + return err 50 + } 51 + 52 + func (e *Enforcer) GetRepoCollaborators(repo syntax.ATURI) ([]syntax.DID, error) { 53 + var collaborators []syntax.DID 54 + members, err := e.e.GetImplicitUsersForRole("repo:collaborator", repo.String()) 55 + if err != nil { 56 + return nil, err 57 + } 58 + for _, m := range members { 59 + if !strings.HasPrefix(m, "did:") { // skip non-user subjects like 'repo:owner' 60 + continue 61 + } 62 + collaborators = append(collaborators, syntax.DID(m)) 63 + } 64 + 65 + slices.Sort(collaborators) 66 + return slices.Compact(collaborators), nil 67 + } 68 + 69 + func (e *Enforcer) IsRepoOwner(user syntax.DID, repo syntax.ATURI) (bool, error) { 70 + return e.e.HasRoleForUser(user.String(), "repo:owner", repo.String()) 71 + } 72 + 73 + func (e *Enforcer) IsRepoCollaborator(user syntax.DID, repo syntax.ATURI) (bool, error) { 74 + return e.hasImplicitRoleForUser(user.String(), "repo:collaborator", repo.String()) 75 + } 76 + 77 + func (e *Enforcer) IsRepoWriteAllowed(user syntax.DID, repo syntax.ATURI) (bool, error) { 78 + return e.e.Enforce(user.String(), repo.String(), "#/", "write") 79 + } 80 + 81 + func (e *Enforcer) IsRepoSettingsWriteAllowed(user syntax.DID, repo syntax.ATURI) (bool, error) { 82 + return e.e.Enforce(user.String(), repo.String(), "#/settings", "write") 83 + } 84 + 85 + func (e *Enforcer) IsRepoCollaboratorInviteAllowed(user syntax.DID, repo syntax.ATURI) (bool, error) { 86 + return e.e.Enforce(user.String(), repo.String(), "#/collaborator", "write") 87 + } 88 + 89 + func (e *Enforcer) IsRepoGitPushAllowed(user syntax.DID, repo syntax.ATURI) (bool, error) { 90 + return e.e.Enforce(user.String(), repo.String(), "#/git", "write") 91 + }
+29
rbac2/spindle.go
··· 1 + package rbac2 2 + 3 + import "github.com/bluesky-social/indigo/atproto/syntax" 4 + 5 + func (e *Enforcer) SetSpindleOwner(user syntax.DID, spindle syntax.DID) error { 6 + return e.setRoleForUser(user.String(), "server:owner", intoSpindle(spindle)) 7 + } 8 + 9 + func (e *Enforcer) IsSpindleMember(user syntax.DID, spindle syntax.DID) (bool, error) { 10 + return e.e.HasRoleForUser(user.String(), "server:member", spindle.String()) 11 + } 12 + 13 + func (e *Enforcer) AddSpindleMember(user syntax.DID, spindle syntax.DID) error { 14 + _, err := e.e.AddRoleForUser(user.String(), "server:member", intoSpindle(spindle)) 15 + return err 16 + } 17 + 18 + func (e *Enforcer) RemoveSpindleMember(user syntax.DID, spindle syntax.DID) error { 19 + _, err := e.e.DeleteRoleForUser(user.String(), "server:member", intoSpindle(spindle)) 20 + return err 21 + } 22 + 23 + func (e *Enforcer) IsSpindleMemberInviteAllowed(user syntax.DID, spindle syntax.DID) (bool, error) { 24 + return e.e.Enforce(user.String(), intoSpindle(spindle), "#/member", "write") 25 + } 26 + 27 + func intoSpindle(did syntax.DID) string { 28 + return "/spindle/" + did.String() 29 + }