refactor(coderd/rbac): enumerate org-member and org-service-account perms (#25928)

`organization-member` was created from `allPermsExcept(...)`. This is changed to an explicit enumeration of capabilities.

- New resources no longer auto-grant to org members or service accounts.
- Adding one now requires an explicit decision in `coderd/rbac/roles.go`.
This commit is contained in:
Steven Masley
2026-06-03 08:14:11 -05:00
committed by GitHub
parent 96e3a64b12
commit f1ebc42859
+155 -75
View File
@@ -1055,44 +1055,94 @@ func OrgMemberPermissions(org OrgSettings) OrgRolePermissions {
})
}
// Uses allPermsExcept to automatically include permissions for new resources.
memberPerms := append(
allPermsExcept(
ResourceWorkspaceDormant,
ResourcePrebuiltWorkspace,
ResourceUser,
ResourceOrganizationMember,
ResourceBoundaryLog,
ResourceAibridgeInterception,
// Chat access requires the agents-access role.
ResourceChat,
),
// 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(),
Permissions(map[string][]policy.Action{
// Reduced permission set on dormant workspaces. No build,
// ssh, or exec.
ResourceWorkspaceDormant.Type: {
policy.ActionRead,
policy.ActionDelete,
policy.ActionCreate,
policy.ActionUpdate,
policy.ActionWorkspaceStop,
policy.ActionCreateAgent,
policy.ActionDeleteAgent,
policy.ActionUpdateAgent,
},
// Can read their own organization member record.
ResourceOrganizationMember.Type: {
policy.ActionRead,
},
// Members can create and update AI Bridge interceptions but
// cannot read them back.
ResourceAibridgeInterception.Type: {
policy.ActionCreate,
policy.ActionUpdate,
},
})...,
)
// 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(),
// 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.
ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionUpdate},
// Own session tokens and workspace agent auth keys.
ResourceApiKey.Type: ResourceApiKey.AvailableActions(),
// User-scoped notification surfaces. All three resources are
// addressed by WithOwner(user_id) at the call sites.
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.
})
if org.ShareableWorkspaceOwners != ShareableWorkspaceOwnersEveryone {
memberPerms = append(memberPerms, Permission{
@@ -1140,45 +1190,75 @@ func OrgServiceAccountPermissions(org OrgSettings) OrgRolePermissions {
}
// service account-scoped permissions (resources owned by the
// service account). Uses allPermsExcept to automatically include
// permissions for new resources.
memberPerms := append(
allPermsExcept(
ResourceWorkspaceDormant,
ResourcePrebuiltWorkspace,
ResourceUser,
ResourceOrganizationMember,
ResourceBoundaryLog,
ResourceAibridgeInterception,
// Chat access requires the agents-access role.
ResourceChat,
),
// 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(),
Permissions(map[string][]policy.Action{
// Reduced permission set on dormant workspaces. No build,
// ssh, or exec.
ResourceWorkspaceDormant.Type: {
policy.ActionRead,
policy.ActionDelete,
policy.ActionCreate,
policy.ActionUpdate,
policy.ActionWorkspaceStop,
policy.ActionCreateAgent,
policy.ActionDeleteAgent,
policy.ActionUpdateAgent,
},
// Can read their own organization member record.
ResourceOrganizationMember.Type: {
policy.ActionRead,
},
// Service accounts can create and update AI Bridge
// interceptions but cannot read them back.
ResourceAibridgeInterception.Type: {
policy.ActionCreate,
policy.ActionUpdate,
},
})...,
)
// 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(),
// 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.
ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionUpdate},
// Own session tokens and workspace agent auth keys.
ResourceApiKey.Type: ResourceApiKey.AvailableActions(),
// User-scoped notification surfaces. All three resources are
// addressed by WithOwner(user_id) at the call sites.
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.
})
return OrgRolePermissions{Org: orgPerms, Member: memberPerms}
}