From bec2115e752561beb6815a109127d12731d00e28 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 5 Jun 2026 10:30:31 -0500 Subject: [PATCH] refactor: extract organization-workspace-access role (#25929) Refs [PLAT-217](https://linear.app/codercom/issue/PLAT-217/rfc-for-gateway-accounts). Extracts an `organization-workspace-access` role so workspace elevation can be split off the organization-member floor without changing behavior. - New role holds the workspace-side resources currently granted by `organization-member`. - The `MinimumImplicitMember` floor preserves the existing behavior until #26027 shrinks it. - Prebuilds orchestrator inserts memberships via `dbauthz.AsSystemRestricted` and no longer needs `OrganizationMember` or `AssignOrgRole` grants.
Agent context - `coderd/rbac/roles.go`: `OrgWorkspaceAccessMemberPerms()` grants `Workspace`, `WorkspaceDormant`, `File` (Create+Read), `ProvisionerDaemon` (Create+Read), and `Task`. Deliberate omissions (`Template`, `Group`, `WorkspaceProxy`, etc.) are documented inline. - `coderd/rbac/roles_test.go`: `orgWorkspaceAccessUser` is added to `requiredSubjects`. `UserProvisionerDaemons` is split into `UserProvisionerDaemonsCreate` and `UserProvisionerDaemonsUpdateDelete` because the new role grants Create+Read only and the test framework requires uniform pass/fail per case. - `codersdk/rbacroles.go`: exposes `RoleOrganizationWorkspaceAccess`. - `enterprise/coderd/prebuilds/membership.go`: `InsertOrganizationMember` runs under `dbauthz.AsSystemRestricted`. The orchestrator never acts with the elevation role; the membership row only exists so prebuilt workspaces have a valid owner. - `coderd/database/dbauthz/dbauthz.go`: drops the now-dead `OrganizationMember` and `AssignOrgRole` permissions from the prebuilds-orchestrator role and the orchestrator's entry in `assignRoles`.
--- Coder Agents on behalf of @Emyrk. --- coderd/database/dbauthz/dbauthz.go | 7 +- coderd/rbac/roles.go | 252 ++++++++++------------ coderd/rbac/roles_test.go | 206 ++++++++++-------- codersdk/rbacroles.go | 1 + enterprise/coderd/prebuilds/membership.go | 4 +- enterprise/coderd/roles_test.go | 3 + site/src/api/typesGenerated.ts | 6 + 7 files changed, 251 insertions(+), 228 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index db2a1dbde7..afa1487747 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -537,14 +537,9 @@ var ( rbac.ResourcePrebuiltWorkspace.Type: { policy.ActionUpdate, policy.ActionDelete, }, - // Should be able to add the prebuilds system user as a member to any organization that needs prebuilds. + // Reads organization membership rows when reconciling the prebuilds user's memberships. rbac.ResourceOrganizationMember.Type: { policy.ActionRead, - policy.ActionCreate, - }, - // Needs to be able to assign roles to the system user in order to make it a member of an organization. - rbac.ResourceAssignOrgRole.Type: { - policy.ActionAssign, }, // Needs to be able to read users to determine which organizations the prebuild system user is a member of. rbac.ResourceUser.Type: { diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 2fee0942a4..f13e660b64 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -3,6 +3,7 @@ package rbac import ( "encoding/json" "errors" + "slices" "sort" "strconv" "strings" @@ -35,8 +36,7 @@ const ( orgUserAdmin string = "organization-user-admin" orgTemplateAdmin string = "organization-template-admin" orgWorkspaceCreationBan string = "organization-workspace-creation-ban" - - prebuildsOrchestrator string = "prebuilds-orchestrator" + orgWorkspaceAccess string = "organization-workspace-access" ) func init() { @@ -173,6 +173,10 @@ func RoleOrgWorkspaceCreationBan() string { return orgWorkspaceCreationBan } +func RoleOrgWorkspaceAccess() string { + return orgWorkspaceAccess +} + // ScopedRoleOrgAdmin is the org role with the organization ID func ScopedRoleOrgAdmin(organizationID uuid.UUID) RoleIdentifier { return RoleIdentifier{Name: RoleOrgAdmin(), OrganizationID: organizationID} @@ -203,6 +207,67 @@ func ScopedRoleAgentsAccess(organizationID uuid.UUID) RoleIdentifier { return RoleIdentifier{Name: RoleAgentsAccess(), OrganizationID: organizationID} } +func ScopedRoleOrgWorkspaceAccess(organizationID uuid.UUID) RoleIdentifier { + return RoleIdentifier{Name: RoleOrgWorkspaceAccess(), OrganizationID: organizationID} +} + +// OrgWorkspaceAccessMemberPerms returns the elevation perms granted by the +// organization-workspace-access role. +func OrgWorkspaceAccessMemberPerms() []Permission { + return Permissions(map[string][]policy.Action{ + ResourceWorkspace.Type: ResourceWorkspace.AvailableActions(), + + // Dormant workspaces share the workspace action set minus the + // build, ssh, and exec actions. + ResourceWorkspaceDormant.Type: { + policy.ActionRead, + policy.ActionDelete, + policy.ActionCreate, + policy.ActionUpdate, + policy.ActionWorkspaceStop, + policy.ActionCreateAgent, + policy.ActionDeleteAgent, + policy.ActionUpdateAgent, + }, + + // Upload and read template files used during workspace build + // (File.RBACObject sets WithOwner(CreatedBy)). + ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, + + // User-scoped provisioner daemons: Upsert sets + // WithOwner(tag_owner) when scope=user so members can run their + // own daemons. Read is granted for symmetry; update and delete + // stay dead at Member scope. + ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead}, + + ResourceTask.Type: ResourceTask.AvailableActions(), + + // Intentionally omitted at Member scope (resources without an + // Owner field on their RBACObject; Member-level grants never + // fire for them). Listed here because these can be common + // misconceptions: + // + // - ResourceTemplate: templates are only owned by orgs, not + // users. Users granted access via ACL and (generally) the + // "Everyone" group. + // - ResourceGroup: groups have no owner. "Groups I'm a + // member of can read themselves" is handled by the ACL + // applied implicitly in RBACObject(). + // - ResourceWorkspaceProxy, ResourceProvisionerJobs, + // ResourceWorkspaceAgentResourceMonitor, + // ResourceWorkspaceAgentDevcontainers, + // ResourceTailnetCoordinator, ResourceReplicas: these + // resources have no DB model that sets Owner; all + // production call sites use the bare resource or + // .InOrg(...) only. Access for these flows through Org + // perms on the appropriate role, or through system / + // agent / template-admin roles defined elsewhere. + // - ResourceProvisionerDaemon update/delete: only create and + // read fire at Member scope via the user-scoped Upsert + // path; other actions go through the bare InOrg path. + }) +} + func allPermsExcept(excepts ...Objecter) []Permission { resources := AllResources() var perms []Permission @@ -609,6 +674,20 @@ func ReloadBuiltinRoles(opts *RoleOptions) { }, } }, + orgWorkspaceAccess: func(organizationID uuid.UUID) Role { + return Role{ + Identifier: RoleIdentifier{Name: orgWorkspaceAccess, OrganizationID: organizationID}, + DisplayName: "Organization Workspace Access", + Site: []Permission{}, + User: []Permission{}, + ByOrgID: map[string]OrgPermissions{ + organizationID.String(): { + Org: []Permission{}, + Member: OrgWorkspaceAccessMemberPerms(), + }, + }, + } + }, // ActionDelete is intentionally excluded because hard-deletion goes through // ResourceSystem in dbpurge. agentsAccess: func(organizationID uuid.UUID) Role { @@ -651,6 +730,7 @@ var assignRoles = map[string]map[string]bool{ orgUserAdmin: true, orgTemplateAdmin: true, orgWorkspaceCreationBan: true, + orgWorkspaceAccess: true, templateAdmin: true, userAdmin: true, customSiteRole: true, @@ -667,6 +747,7 @@ var assignRoles = map[string]map[string]bool{ orgUserAdmin: true, orgTemplateAdmin: true, orgWorkspaceCreationBan: true, + orgWorkspaceAccess: true, templateAdmin: true, userAdmin: true, customSiteRole: true, @@ -674,9 +755,10 @@ var assignRoles = map[string]map[string]bool{ agentsAccess: true, }, userAdmin: { - member: true, - orgMember: true, - agentsAccess: true, + member: true, + orgMember: true, + orgWorkspaceAccess: true, + agentsAccess: true, }, orgAdmin: { orgAdmin: true, @@ -685,16 +767,14 @@ var assignRoles = map[string]map[string]bool{ orgUserAdmin: true, orgTemplateAdmin: true, orgWorkspaceCreationBan: true, + orgWorkspaceAccess: true, customOrganizationRole: true, agentsAccess: true, }, orgUserAdmin: { - orgMember: true, - agentsAccess: true, - }, - - prebuildsOrchestrator: { - orgMember: true, + orgMember: true, + orgWorkspaceAccess: true, + agentsAccess: true, }, } @@ -1055,59 +1135,19 @@ func OrgMemberPermissions(org OrgSettings) OrgRolePermissions { }) } - // Enumerate the per-member resources explicitly so new resources do - // not auto-grant to org members. Adding a resource to the codebase - // requires an explicit decision to expose it here. - // - // Member-level grants only fire when input.object.owner == - // input.subject.id (see the org_member rule in - // coderd/rbac/policy.rego). Only resources whose RBACObject() calls - // WithOwner(...) at production call sites belong here; see the - // "Intentionally omitted" block at the bottom. - memberPerms := Permissions(map[string][]policy.Action{ - // Workspace lifecycle on resources owned by this member. - ResourceWorkspace.Type: ResourceWorkspace.AvailableActions(), - - // Dormant workspaces share the workspace action set minus the - // build, ssh, and exec actions. - ResourceWorkspaceDormant.Type: { - policy.ActionRead, - policy.ActionDelete, - policy.ActionCreate, - policy.ActionUpdate, - policy.ActionWorkspaceStop, - policy.ActionCreateAgent, - policy.ActionDeleteAgent, - policy.ActionUpdateAgent, - }, - - // Upload and read template files the member created during - // workspace build (File.RBACObject sets WithOwner(CreatedBy)). - ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, - - // Create and read user-scoped provisioner daemons. The Upsert - // path in dbauthz sets WithOwner(tag_owner) when scope=user, so - // members can run their own daemons. Read is granted for - // symmetry with workspace ownership: members can inspect - // daemons they spawned even though no production call site - // currently uses the member-scope read path (read on the bare - // InOrg object continues to require Org-level perms). - ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead}, - - // Tasks ride along with workspaces and are owner-scoped. - ResourceTask.Type: ResourceTask.AvailableActions(), + // Chat access requires the agents-access role and is intentionally + // not granted in the floor. + floor := Permissions(map[string][]policy.Action{ + // Read-self org-member record. + ResourceOrganizationMember.Type: {policy.ActionRead}, // Read-self group-membership record. GroupMember.RBACObject // sets WithOwner to the user's own ID. ResourceGroupMember.Type: {policy.ActionRead}, - // Read-self org-member record. - ResourceOrganizationMember.Type: {policy.ActionRead}, - // Members can create and update AI Bridge interceptions they // initiate (dbauthz layer sets WithOwner(InitiatorID)) but - // cannot read them back. Chat access requires the - // agents-access role and is intentionally not granted here. + // cannot read them back. ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionUpdate}, // Own session tokens and workspace agent auth keys. @@ -1118,32 +1158,17 @@ func OrgMemberPermissions(org OrgSettings) OrgRolePermissions { ResourceNotificationMessage.Type: {policy.ActionRead, policy.ActionUpdate}, ResourceNotificationPreference.Type: ResourceNotificationPreference.AvailableActions(), ResourceInboxNotification.Type: ResourceInboxNotification.AvailableActions(), - - // Intentionally omitted at Member scope (resources without an - // Owner field on their RBACObject; Member-level grants never - // fire for them). Listed here so a future maintainer who sees - // these dropped relative to the legacy allPermsExcept(...) - // wildcard does not "restore" them: - // - // - ResourceTemplate: templates have no owner. Org-member - // template.use is authorized via the ACL path - // (acl_group_list[org_owner] "Everyone" group, populated - // on each template's GroupACL). - // - ResourceGroup: groups have no owner. "Groups I'm a - // member of can read themselves" is granted via the - // per-group GroupACL. - // - ResourceWorkspaceProxy, ResourceProvisionerJobs, - // ResourceWorkspaceAgentResourceMonitor, - // ResourceWorkspaceAgentDevcontainers, - // ResourceTailnetCoordinator, ResourceReplicas: these - // resources have no DB model that sets Owner; all - // production call sites use the bare resource or - // .InOrg(...) only. Access for these flows through Org - // perms on the appropriate role (e.g. ProvisionerDaemon - // above), or through system / agent / template-admin - // roles defined elsewhere. }) + // Workspace-ops elevation. Today bundled into organization-member; + // the minimum-implicit-member experiment will move the binding + // exclusively onto organization-workspace-access so a user without + // that role has only the floor. See OrgWorkspaceAccessMemberPerms + // for the perm set and the "Intentionally omitted" rationale. + elevation := OrgWorkspaceAccessMemberPerms() + + memberPerms := slices.Concat(elevation, floor) + if org.ShareableWorkspaceOwners != ShareableWorkspaceOwnersEveryone { memberPerms = append(memberPerms, Permission{ Negate: true, @@ -1189,61 +1214,18 @@ func OrgServiceAccountPermissions(org OrgSettings) OrgRolePermissions { }) } - // service account-scoped permissions (resources owned by the - // service account). Enumerated explicitly so new resources do not - // auto-grant to service accounts. - // - // Member-level grants only fire when input.object.owner == - // input.subject.id (see the org_member rule in - // coderd/rbac/policy.rego). Only resources whose RBACObject() calls - // WithOwner(...) at production call sites belong here; see the - // "Intentionally omitted" block at the bottom. - memberPerms := Permissions(map[string][]policy.Action{ - // Workspace lifecycle on resources owned by this service account. - ResourceWorkspace.Type: ResourceWorkspace.AvailableActions(), - - // Dormant workspaces share the workspace action set minus the - // build, ssh, and exec actions. - ResourceWorkspaceDormant.Type: { - policy.ActionRead, - policy.ActionDelete, - policy.ActionCreate, - policy.ActionUpdate, - policy.ActionWorkspaceStop, - policy.ActionCreateAgent, - policy.ActionDeleteAgent, - policy.ActionUpdateAgent, - }, - - // Upload and read template files the service account created - // during workspace build (File.RBACObject sets - // WithOwner(CreatedBy)). - ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, - - // Create and read user-scoped provisioner daemons. The Upsert - // path in dbauthz sets WithOwner(tag_owner) when scope=user, so - // service accounts can run their own daemons. Read is granted - // for symmetry with workspace ownership: service accounts can - // inspect daemons they spawned even though no production call - // site currently uses the member-scope read path (read on the - // bare InOrg object continues to require Org-level perms). - ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead}, - - // Tasks ride along with workspaces and are owner-scoped. - ResourceTask.Type: ResourceTask.AvailableActions(), + floor := Permissions(map[string][]policy.Action{ + // Read-self org-member record. + ResourceOrganizationMember.Type: {policy.ActionRead}, // Read-self group-membership record. GroupMember.RBACObject // sets WithOwner to the user's own ID. ResourceGroupMember.Type: {policy.ActionRead}, - // Read-self org-member record. - ResourceOrganizationMember.Type: {policy.ActionRead}, - - // Service accounts can create and update AI Bridge - // interceptions they initiate (dbauthz layer sets - // WithOwner(InitiatorID)) but cannot read them back. Chat - // access requires the agents-access role and is intentionally - // not granted here. + // Service accounts can create and update AI Bridge interceptions + // they initiate (dbauthz layer sets WithOwner(InitiatorID)) but + // cannot read them back. Chat access requires the agents-access + // role and is intentionally not granted here. ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionUpdate}, // Own session tokens and workspace agent auth keys. @@ -1254,11 +1236,11 @@ func OrgServiceAccountPermissions(org OrgSettings) OrgRolePermissions { ResourceNotificationMessage.Type: {policy.ActionRead, policy.ActionUpdate}, ResourceNotificationPreference.Type: ResourceNotificationPreference.AvailableActions(), ResourceInboxNotification.Type: ResourceInboxNotification.AvailableActions(), - - // Intentionally omitted at Member scope. See - // OrgMemberPermissions above for the rationale; the service - // account role mirrors the same partition. }) + elevation := OrgWorkspaceAccessMemberPerms() + + memberPerms := slices.Concat(elevation, floor) + return OrgRolePermissions{Org: orgPerms, Member: memberPerms} } diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 7fee71935c..a5e3d77756 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -266,6 +266,21 @@ func TestRolePermissions(t *testing.T) { } }() + orgWorkspaceAccessUser := func() authSubject { + memberRole, err := rbac.RoleByName(rbac.RoleMember()) + require.NoError(t, err) + orgWorkspaceAccessRole, err := rbac.RoleByName(rbac.ScopedRoleOrgWorkspaceAccess(orgID)) + require.NoError(t, err) + return authSubject{ + Name: "org_workspace_access", + Actor: rbac.Subject{ + ID: currentUser.String(), + Roles: rbac.Roles{memberRole, orgWorkspaceAccessRole}, + Scope: rbac.ScopeAll, + }.WithCachedASTValue(), + } + }() + orgMemberMe := func() authSubject { memberRole, err := rbac.RoleByName(rbac.RoleMember()) require.NoError(t, err) @@ -305,7 +320,7 @@ func TestRolePermissions(t *testing.T) { // requiredSubjects are required to be asserted in each test case. This is // to make sure one is not forgotten. requiredSubjects := []authSubject{ - memberMe, owner, agentsAccessUser, + memberMe, owner, agentsAccessUser, orgWorkspaceAccessUser, orgAdmin, otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin, templateAdmin, userAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, } @@ -328,7 +343,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceUserObject(currentUser), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgUserAdmin, otherOrgAdmin, otherOrgUserAdmin, orgAdmin}, + true: {owner, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgUserAdmin, otherOrgAdmin, otherOrgUserAdmin, orgAdmin, orgWorkspaceAccessUser}, false: { orgTemplateAdmin, orgAuditor, otherOrgAuditor, otherOrgTemplateAdmin, @@ -341,7 +356,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceUser, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, userAdmin}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser}, }, }, { @@ -350,7 +365,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin, orgAdminBanWorkspace}, + true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin, orgAdminBanWorkspace, orgWorkspaceAccessUser}, false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin}, }, }, @@ -360,7 +375,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionUpdate}, Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, orgAdminBanWorkspace}, + true: {owner, orgAdmin, orgAdminBanWorkspace, orgWorkspaceAccessUser}, false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, @@ -370,7 +385,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionDelete}, Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin}, + true: {owner, orgAdmin, orgWorkspaceAccessUser}, false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace}, }, }, @@ -381,7 +396,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspace.InOrg(orgID).WithOwner(policy.WildcardSymbol), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin}, + false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgWorkspaceAccessUser}, }, }, { @@ -390,7 +405,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionSSH}, Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner}, + true: {owner, orgWorkspaceAccessUser}, false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, @@ -400,7 +415,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionApplicationConnect}, Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner}, + true: {owner, orgWorkspaceAccessUser}, false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, }, }, @@ -409,7 +424,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreateAgent, policy.ActionDeleteAgent}, Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin}, + true: {owner, orgAdmin, orgWorkspaceAccessUser}, false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace}, }, }, @@ -418,7 +433,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionUpdateAgent}, Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, orgAdminBanWorkspace}, + true: {owner, orgAdmin, orgAdminBanWorkspace, orgWorkspaceAccessUser}, false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, @@ -430,7 +445,7 @@ func TestRolePermissions(t *testing.T) { InOrg(orgID). WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, orgAdminBanWorkspace}, + true: {owner, orgAdmin, orgAdminBanWorkspace, orgWorkspaceAccessUser}, false: { memberMe, agentsAccessUser, setOtherOrg, templateAdmin, userAdmin, @@ -452,6 +467,7 @@ func TestRolePermissions(t *testing.T) { userAdmin, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace, + orgWorkspaceAccessUser, }, }, }, @@ -461,7 +477,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceTemplate.WithID(templateID).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin}, - false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, userAdmin}, + false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -470,7 +486,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceTemplate.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAuditor, orgAdmin, templateAdmin, orgTemplateAdmin}, - false: {setOtherOrg, orgUserAdmin, memberMe, agentsAccessUser, userAdmin}, + false: {setOtherOrg, orgUserAdmin, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -481,7 +497,7 @@ func TestRolePermissions(t *testing.T) { }), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin}, - false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, agentsAccessUser, userAdmin}, + false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -492,7 +508,7 @@ func TestRolePermissions(t *testing.T) { true: {owner, templateAdmin}, // Org template admins can only read org scoped files. // File scope is currently not org scoped :cry: - false: {setOtherOrg, orgTemplateAdmin, orgAdmin, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin}, + false: {setOtherOrg, orgTemplateAdmin, orgAdmin, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin, orgWorkspaceAccessUser}, }, }, { @@ -500,7 +516,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionRead}, Resource: rbac.ResourceFile.WithID(fileID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, agentsAccessUser, templateAdmin}, + true: {owner, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser}, false: {setOtherOrg, setOrgNotMe, userAdmin}, }, }, @@ -510,7 +526,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOrganization, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -519,7 +535,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {setOtherOrg, orgTemplateAdmin, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, orgTemplateAdmin, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -528,7 +544,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin, auditor, orgAuditor, userAdmin, orgUserAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser}, + false: {setOtherOrg, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, }, }, { @@ -537,7 +553,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignOrgRole, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, userAdmin, memberMe, agentsAccessUser, templateAdmin}, + false: {setOtherOrg, setOrgNotMe, userAdmin, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser}, }, }, { @@ -546,7 +562,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignRole, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, userAdmin}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser}, }, }, { @@ -554,7 +570,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceAssignRole, AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {setOtherOrg, setOrgNotMe, owner, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + true: {setOtherOrg, setOrgNotMe, owner, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, false: {}, }, }, @@ -564,7 +580,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, userAdmin, orgUserAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, { @@ -573,7 +589,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -582,7 +598,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAssignOrgRole.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, orgUserAdmin, userAdmin, templateAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser, orgAuditor, orgTemplateAdmin}, + false: {setOtherOrg, memberMe, agentsAccessUser, orgAuditor, orgTemplateAdmin, orgWorkspaceAccessUser}, }, }, { @@ -590,7 +606,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete, policy.ActionUpdate}, Resource: rbac.ResourceApiKey.WithID(apiKeyID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, agentsAccessUser}, + true: {owner, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, false: {setOtherOrg, setOrgNotMe, templateAdmin, userAdmin}, }, }, @@ -602,7 +618,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceInboxNotification.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, templateAdmin, userAdmin, memberMe, agentsAccessUser}, + false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, templateAdmin, userAdmin, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, }, }, { @@ -610,7 +626,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionReadPersonal, policy.ActionUpdatePersonal}, Resource: rbac.ResourceUserObject(currentUser), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, agentsAccessUser, userAdmin}, + true: {owner, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser}, false: {setOtherOrg, setOrgNotMe, templateAdmin}, }, }, @@ -620,7 +636,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOrganizationMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, userAdmin, orgUserAdmin}, - false: {setOtherOrg, orgTemplateAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin}, + false: {setOtherOrg, orgTemplateAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, orgWorkspaceAccessUser}, }, }, { @@ -629,7 +645,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOrganizationMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgUserAdmin, orgTemplateAdmin}, - false: {memberMe, agentsAccessUser, setOtherOrg}, + false: {memberMe, agentsAccessUser, setOtherOrg, orgWorkspaceAccessUser}, }, }, { @@ -641,7 +657,7 @@ func TestRolePermissions(t *testing.T) { }), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, templateAdmin, orgUserAdmin, orgTemplateAdmin, orgAuditor, agentsAccessUser}, + true: {owner, orgAdmin, templateAdmin, orgUserAdmin, orgTemplateAdmin, orgAuditor, agentsAccessUser, orgWorkspaceAccessUser}, false: {setOtherOrg, memberMe, userAdmin}, }, }, @@ -655,7 +671,7 @@ func TestRolePermissions(t *testing.T) { }), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, userAdmin, orgUserAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, { @@ -668,7 +684,7 @@ func TestRolePermissions(t *testing.T) { }), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, - false: {setOtherOrg, memberMe, agentsAccessUser}, + false: {setOtherOrg, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, }, }, { @@ -677,7 +693,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceGroupMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser}, + false: {setOtherOrg, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, }, }, { @@ -686,7 +702,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceGroupMember.WithID(adminID).InOrg(orgID).WithOwner(adminID.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser}, + false: {setOtherOrg, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, }, }, { @@ -694,7 +710,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {orgAdmin, owner, templateAdmin, orgTemplateAdmin}, + true: {orgAdmin, owner, templateAdmin, orgTemplateAdmin, orgWorkspaceAccessUser}, false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, orgUserAdmin, orgAuditor}, }, }, @@ -703,7 +719,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionWorkspaceStop, policy.ActionCreateAgent, policy.ActionDeleteAgent, policy.ActionUpdateAgent}, Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {orgAdmin, owner}, + true: {orgAdmin, owner, orgWorkspaceAccessUser}, false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, @@ -713,7 +729,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, userAdmin, owner, templateAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, userAdmin, owner, templateAdmin, orgWorkspaceAccessUser}, }, }, { @@ -721,7 +737,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionWorkspaceStart, policy.ActionWorkspaceStop}, Resource: rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin}, + true: {owner, orgAdmin, orgWorkspaceAccessUser}, false: {setOtherOrg, userAdmin, templateAdmin, memberMe, agentsAccessUser, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, @@ -731,7 +747,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourcePrebuiltWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(database.PrebuildsSystemUserID.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin}, - false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, { @@ -739,7 +755,7 @@ func TestRolePermissions(t *testing.T) { Actions: crud, Resource: rbac.ResourceTask.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin}, + true: {owner, orgAdmin, orgWorkspaceAccessUser}, false: {setOtherOrg, userAdmin, templateAdmin, memberMe, agentsAccessUser, orgTemplateAdmin, orgUserAdmin, orgAuditor}, }, }, @@ -750,7 +766,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceLicense, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -759,7 +775,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceDeploymentStats, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -768,7 +784,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceDeploymentConfig, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -777,7 +793,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceDebugInfo, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -786,7 +802,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceReplicas, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -795,7 +811,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceTailnetCoordinator, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -804,7 +820,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAuditLog, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -813,7 +829,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, templateAdmin, orgAdmin, orgTemplateAdmin}, - false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, agentsAccessUser, userAdmin}, + false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, agentsAccessUser, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -822,16 +838,25 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, templateAdmin, orgAdmin, orgTemplateAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin}, + false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin, orgWorkspaceAccessUser}, }, }, { - Name: "UserProvisionerDaemons", - Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}, + Name: "UserProvisionerDaemonsCreate", + Actions: []policy.Action{policy.ActionCreate}, + Resource: rbac.ResourceProvisionerDaemon.WithOwner(currentUser.String()).InOrg(orgID), + AuthorizeMap: map[bool][]hasAuthSubjects{ + true: {owner, templateAdmin, orgTemplateAdmin, orgAdmin, orgWorkspaceAccessUser}, + false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgUserAdmin, orgAuditor}, + }, + }, + { + Name: "UserProvisionerDaemonsUpdateDelete", + Actions: []policy.Action{policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceProvisionerDaemon.WithOwner(currentUser.String()).InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, templateAdmin, orgTemplateAdmin, orgAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgUserAdmin, orgAuditor}, + false: {orgWorkspaceAccessUser, setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgUserAdmin, orgAuditor}, }, }, { @@ -840,7 +865,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceProvisionerJobs.InOrg(orgID), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgTemplateAdmin, orgAdmin}, - false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, { @@ -849,7 +874,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceSystem, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -858,7 +883,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOauth2App, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -866,7 +891,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceOauth2App, AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + true: {owner, setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, false: {}, }, }, @@ -876,7 +901,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOauth2AppSecret, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -885,7 +910,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceOauth2AppCodeToken, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -894,7 +919,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceWorkspaceProxy, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -902,7 +927,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead}, Resource: rbac.ResourceWorkspaceProxy, AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + true: {owner, setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, false: {}, }, }, @@ -913,7 +938,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate}, Resource: rbac.ResourceNotificationPreference.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {memberMe, agentsAccessUser, owner}, + true: {orgWorkspaceAccessUser, memberMe, agentsAccessUser, owner}, false: { userAdmin, orgUserAdmin, templateAdmin, orgAuditor, orgTemplateAdmin, @@ -930,7 +955,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - memberMe, agentsAccessUser, userAdmin, orgUserAdmin, templateAdmin, + orgWorkspaceAccessUser, memberMe, agentsAccessUser, userAdmin, orgUserAdmin, templateAdmin, orgAuditor, orgTemplateAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, orgAdmin, otherOrgAdmin, @@ -949,6 +974,7 @@ func TestRolePermissions(t *testing.T) { orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, + orgWorkspaceAccessUser, }, }, }, @@ -962,7 +988,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - memberMe, agentsAccessUser, templateAdmin, orgUserAdmin, userAdmin, + orgWorkspaceAccessUser, memberMe, agentsAccessUser, templateAdmin, orgUserAdmin, userAdmin, orgAdmin, orgAuditor, orgTemplateAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, otherOrgAdmin, @@ -975,7 +1001,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete}, Resource: rbac.ResourceWebpushSubscription.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, agentsAccessUser}, + true: {owner, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, false: {orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin}, }, }, @@ -990,6 +1016,7 @@ func TestRolePermissions(t *testing.T) { memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgAuditor, otherOrgAuditor, otherOrgTemplateAdmin, + orgWorkspaceAccessUser, }, }, }, @@ -1003,6 +1030,7 @@ func TestRolePermissions(t *testing.T) { userAdmin, memberMe, agentsAccessUser, orgAuditor, orgUserAdmin, otherOrgAuditor, otherOrgUserAdmin, + orgWorkspaceAccessUser, }, }, }, @@ -1011,7 +1039,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate}, Resource: rbac.ResourceWorkspace.AnyOrganization().WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, orgAdmin, otherOrgAdmin}, + true: {owner, orgAdmin, otherOrgAdmin, orgWorkspaceAccessUser}, false: { memberMe, agentsAccessUser, userAdmin, templateAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin, @@ -1025,7 +1053,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceCryptoKey, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -1039,6 +1067,7 @@ func TestRolePermissions(t *testing.T) { memberMe, agentsAccessUser, templateAdmin, orgAuditor, orgTemplateAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, + orgWorkspaceAccessUser, }, }, }, @@ -1054,6 +1083,7 @@ func TestRolePermissions(t *testing.T) { memberMe, agentsAccessUser, templateAdmin, orgAuditor, orgTemplateAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin, + orgWorkspaceAccessUser, }, }, }, @@ -1069,6 +1099,7 @@ func TestRolePermissions(t *testing.T) { orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, + orgWorkspaceAccessUser, }, }, }, @@ -1084,6 +1115,7 @@ func TestRolePermissions(t *testing.T) { orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, + orgWorkspaceAccessUser, }, }, }, @@ -1093,7 +1125,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceConnectionLog, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, // Only the user themselves can access their own secrets — no one else. @@ -1102,7 +1134,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceUserSecret.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {memberMe, agentsAccessUser}, + true: {memberMe, agentsAccessUser, orgWorkspaceAccessUser}, false: { owner, orgAdmin, otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin, @@ -1117,7 +1149,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead, policy.ActionDelete}, Resource: rbac.ResourceUserSkill.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, agentsAccessUser}, + true: {owner, memberMe, agentsAccessUser, orgWorkspaceAccessUser}, false: { orgAdmin, otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin, @@ -1130,7 +1162,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate}, Resource: rbac.ResourceUserSkill.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {memberMe, agentsAccessUser}, + true: {memberMe, agentsAccessUser, orgWorkspaceAccessUser}, false: { owner, orgAdmin, otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin, @@ -1151,6 +1183,7 @@ func TestRolePermissions(t *testing.T) { orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin, + orgWorkspaceAccessUser, }, }, }, @@ -1160,7 +1193,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate}, Resource: rbac.ResourceAibridgeInterception.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {owner, memberMe, agentsAccessUser}, + true: {orgWorkspaceAccessUser, owner, memberMe, agentsAccessUser}, false: { orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, @@ -1177,7 +1210,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, auditor}, false: { - memberMe, agentsAccessUser, + orgWorkspaceAccessUser, memberMe, agentsAccessUser, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, @@ -1196,7 +1229,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - memberMe, agentsAccessUser, + orgWorkspaceAccessUser, memberMe, agentsAccessUser, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, @@ -1214,7 +1247,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, false: { - memberMe, agentsAccessUser, + orgWorkspaceAccessUser, memberMe, agentsAccessUser, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, @@ -1227,7 +1260,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, Resource: rbac.ResourceBoundaryUsage, AuthorizeMap: map[bool][]hasAuthSubjects{ - false: {owner, setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {owner, setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -1235,7 +1268,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate, policy.ActionRead}, Resource: rbac.ResourceAiSeat, AuthorizeMap: map[bool][]hasAuthSubjects{ - false: {owner, setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {owner, setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -1244,7 +1277,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceAiModelPrice, AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner}, - false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin}, + false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgWorkspaceAccessUser}, }, }, { @@ -1255,7 +1288,7 @@ func TestRolePermissions(t *testing.T) { Actions: []policy.Action{policy.ActionCreate}, Resource: rbac.ResourceBoundaryLog.WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ - true: {memberMe, agentsAccessUser}, + true: {orgWorkspaceAccessUser, memberMe, agentsAccessUser}, false: { owner, orgAdmin, otherOrgAdmin, @@ -1275,7 +1308,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {}, false: { - owner, memberMe, agentsAccessUser, + orgWorkspaceAccessUser, owner, memberMe, agentsAccessUser, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, auditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, @@ -1292,7 +1325,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {}, false: { - owner, memberMe, agentsAccessUser, + orgWorkspaceAccessUser, owner, memberMe, agentsAccessUser, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, auditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, @@ -1308,7 +1341,7 @@ func TestRolePermissions(t *testing.T) { AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, auditor}, false: { - memberMe, agentsAccessUser, + orgWorkspaceAccessUser, memberMe, agentsAccessUser, orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, @@ -1322,7 +1355,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, agentsAccessUser}, - false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, { @@ -1331,7 +1364,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin, agentsAccessUser}, - false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, orgMemberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, { @@ -1340,7 +1373,7 @@ func TestRolePermissions(t *testing.T) { Resource: rbac.ResourceChat.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()), AuthorizeMap: map[bool][]hasAuthSubjects{ true: {owner, orgAdmin}, - false: {setOtherOrg, memberMe, orgMemberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor}, + false: {setOtherOrg, memberMe, orgMemberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgWorkspaceAccessUser}, }, }, } @@ -1497,6 +1530,7 @@ func TestListRoles(t *testing.T) { fmt.Sprintf("organization-user-admin:%s", orgID.String()), fmt.Sprintf("organization-template-admin:%s", orgID.String()), fmt.Sprintf("organization-workspace-creation-ban:%s", orgID.String()), + fmt.Sprintf("organization-workspace-access:%s", orgID.String()), fmt.Sprintf("agents-access:%s", orgID.String()), }, orgRoleNames) diff --git a/codersdk/rbacroles.go b/codersdk/rbacroles.go index c48c5cf95c..71b82c6340 100644 --- a/codersdk/rbacroles.go +++ b/codersdk/rbacroles.go @@ -15,4 +15,5 @@ const ( RoleOrganizationTemplateAdmin string = "organization-template-admin" RoleOrganizationUserAdmin string = "organization-user-admin" RoleOrganizationWorkspaceCreationBan string = "organization-workspace-creation-ban" + RoleOrganizationWorkspaceAccess string = "organization-workspace-access" ) diff --git a/enterprise/coderd/prebuilds/membership.go b/enterprise/coderd/prebuilds/membership.go index 8a8120d026..a0a1a2b4eb 100644 --- a/enterprise/coderd/prebuilds/membership.go +++ b/enterprise/coderd/prebuilds/membership.go @@ -9,6 +9,7 @@ import ( "cdr.dev/slog/v3" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/quartz" ) @@ -63,7 +64,8 @@ func (s StoreMembershipReconciler) ReconcileAll(ctx context.Context, userID uuid // Add user to org if needed if !orgStatus.HasPrebuildUser { - _, err = s.store.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{ + //nolint:gocritic // Must use AsSystemRestricted when creating a new org member as it also assigns roles. + _, err = s.store.InsertOrganizationMember(dbauthz.AsSystemRestricted(ctx), database.InsertOrganizationMemberParams{ OrganizationID: orgStatus.OrganizationID, UserID: userID, CreatedAt: s.clock.Now(), diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index 562f35ab02..e2cc4df5bb 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -505,6 +505,7 @@ func TestListRoles(t *testing.T) { {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: false, {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: false, {Name: codersdk.RoleOrganizationWorkspaceCreationBan, OrganizationID: owner.OrganizationID}: false, + {Name: codersdk.RoleOrganizationWorkspaceAccess, OrganizationID: owner.OrganizationID}: false, {Name: codersdk.RoleAgentsAccess, OrganizationID: owner.OrganizationID}: false, }), }, @@ -539,6 +540,7 @@ func TestListRoles(t *testing.T) { {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: true, {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: true, {Name: codersdk.RoleOrganizationWorkspaceCreationBan, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationWorkspaceAccess, OrganizationID: owner.OrganizationID}: true, {Name: codersdk.RoleAgentsAccess, OrganizationID: owner.OrganizationID}: true, }), }, @@ -573,6 +575,7 @@ func TestListRoles(t *testing.T) { {Name: codersdk.RoleOrganizationTemplateAdmin, OrganizationID: owner.OrganizationID}: true, {Name: codersdk.RoleOrganizationUserAdmin, OrganizationID: owner.OrganizationID}: true, {Name: codersdk.RoleOrganizationWorkspaceCreationBan, OrganizationID: owner.OrganizationID}: true, + {Name: codersdk.RoleOrganizationWorkspaceAccess, OrganizationID: owner.OrganizationID}: true, {Name: codersdk.RoleAgentsAccess, OrganizationID: owner.OrganizationID}: true, }), }, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index b57f604d28..a07ff74c87 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -7337,6 +7337,12 @@ export const RoleOrganizationTemplateAdmin = "organization-template-admin"; */ export const RoleOrganizationUserAdmin = "organization-user-admin"; +// From codersdk/rbacroles.go +/** + * Ideally these roles would be generated from the rbac/roles.go package. + */ +export const RoleOrganizationWorkspaceAccess = "organization-workspace-access"; + // From codersdk/rbacroles.go /** * Ideally these roles would be generated from the rbac/roles.go package.