refactor(coderd/rbac): enumerate org-member and org-service-account perms - #25928
Conversation
| // 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. | ||
| 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, | ||
| }, | ||
|
|
||
| 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, | ||
| }, | ||
| })..., | ||
| ) | ||
| // Workspace runtime support: proxies, agent monitors, | ||
| // devcontainer setup, and tailnet coordination. | ||
| ResourceWorkspaceProxy.Type: {policy.ActionRead}, | ||
| ResourceWorkspaceAgentResourceMonitor.Type: ResourceWorkspaceAgentResourceMonitor.AvailableActions(), | ||
| ResourceWorkspaceAgentDevcontainers.Type: ResourceWorkspaceAgentDevcontainers.AvailableActions(), | ||
| ResourceTailnetCoordinator.Type: ResourceTailnetCoordinator.AvailableActions(), | ||
|
|
||
| // Apply templates; full template lifecycle is restricted to | ||
| // template-admin. | ||
| ResourceTemplate.Type: {policy.ActionRead, policy.ActionUse}, | ||
|
|
||
| // Upload and read template files used during workspace build. | ||
| ResourceFile.Type: {policy.ActionCreate, policy.ActionRead}, | ||
|
|
||
| // Provisioner jobs back workspace builds. | ||
| ResourceProvisionerJobs.Type: ResourceProvisionerJobs.AvailableActions(), | ||
|
|
||
| // Tasks ride along with workspaces. | ||
| ResourceTask.Type: ResourceTask.AvailableActions(), | ||
|
|
||
| // Read groups and group memberships for ACL evaluation. | ||
| ResourceGroup.Type: {policy.ActionRead}, | ||
| ResourceGroupMember.Type: {policy.ActionRead}, | ||
|
|
||
| // Read-self org-member record. | ||
| ResourceOrganizationMember.Type: {policy.ActionRead}, | ||
|
|
||
| // Members can create and update AI Bridge interceptions 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. | ||
| ResourceNotificationMessage.Type: {policy.ActionRead, policy.ActionUpdate}, | ||
| ResourceNotificationPreference.Type: ResourceNotificationPreference.AvailableActions(), | ||
| ResourceInboxNotification.Type: ResourceInboxNotification.AvailableActions(), | ||
|
|
||
| // Replica metadata (read-only is the only defined action). | ||
| ResourceReplicas.Type: {policy.ActionRead}, | ||
| }) |
There was a problem hiding this comment.
This diff should be a functional no-op. Just moving to a declaring the perms rather than implying them through a * pattern
Perm diff auditThe legacy Audit harness: branch
Resources kept on the floor (not in this diff): Raw dump outputCoder Agents on behalf of @Emyrk. |
79be71f to
989d98f
Compare
…erms Replace allPermsExcept in OrgMemberPermissions and OrgServiceAccountPermissions with explicit per-resource enumerations. allPermsExcept granted wildcard actions on every resource not in its exclusion list, which auto-granted any new resource added to the codebase and made the actual perm surface hard to audit. The enumeration grants only the resources actually relevant to member/service-account operations: workspace lifecycle and runtime support, template apply, file upload/read for builds, provisioner jobs, tasks, group reads for ACL eval, org-member read-self, AI Bridge interception writes, own API keys, user-scoped notification surfaces, and replica metadata. Behavior-preserving: all rbac, dbauthz, coderd workspace/template/ user/org/notification/key/provisioner/audit/proxy/task tests, and enterprise/coderd tests pass.
Member-level perms in OrgPermissions only fire when
input.object.owner == input.subject.id (see the org_member rule in
coderd/rbac/policy.rego). Resources whose RBACObject() does not set
WithOwner(...) at production call sites can never satisfy that
condition; granting them at Member scope is dead code. PR 1's
enumeration inherited these from the legacy allPermsExcept(...)
wildcard. This commit drops them so the floor matches its documented
scope and adds an "Intentionally omitted" block in roles.go listing
each removed type and the reason it stays out, for posterity.
Removed from both OrgMemberPermissions and OrgServiceAccountPermissions
Member maps:
- ResourceTemplate {read, use}
Template.RBACObject sets InOrg and ACLs but no Owner. Org-member
template.use is granted via the "Everyone" ACL path
(acl_group_list[org_owner] populated on each template's
GroupACL); that is the rule that fires in createWorkspace, not
the Member-level grant.
- ResourceGroup {read}
Group.RBACObject sets a per-group GroupACL granting read to the
group's own ID, but no Owner. "Groups I'm a member of can read
themselves" is the ACL path. Reading other groups requires
a higher role.
- ResourceWorkspaceProxy {read}
WorkspaceProxy.RBACObject sets only WithID. All production call
sites use the bare resource; Member-level grant never fires.
- ResourceProvisionerJobs {*}
No DB model implements RBACObject. Handler call sites use
.InOrg(org.ID) only; coderd/provisionerjobs.go:100 documents
the intent as "only owners and template admins can access
provisioner jobs."
- ResourceWorkspaceAgentResourceMonitor {*}
Dbauthz call sites use the bare resource for system / telemetry
reads. Owner-scoped checks (e.g.
FetchVolumesResourceMonitorsByAgentID) route through the
workspace object instead, so the Member-level monitor grant is
never the path that authorizes.
- ResourceWorkspaceAgentDevcontainers {*}
Dbauthz call sites use the bare resource. Agent-side perms come
from system roles.
- ResourceTailnetCoordinator {*}
Dbauthz call sites use the bare resource. Tailnet ops are
granted to system / agent roles.
- ResourceReplicas {read}
Bare resource at the single call site in
enterprise/coderd/replicas.go; Member-level never fires.
Behavior-preserving: all eight grants were also dead under the
legacy allPermsExcept(...) wildcard. The rbac, dbauthz, coderd, and
enterprise/coderd test suites pass at the same scope verified for
the initial PR 1 commit.
…for user-scoped daemons
The enumerate-org-member refactor dropped ResourceProvisionerDaemon from
the Member perm sets, which broke TestProvisionerDaemonServe/UserLocal:
creating a user-scoped daemon (tags={"scope":"user","owner":<user_id>})
goes through UpsertProvisionerDaemon in dbauthz, which sets
WithOwner(tag_owner) on the RBAC object, causing the policy to evaluate
the Member-scope grant.
Add ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead}
to both the organization-member and organization-service-account Member
perm sets. Create restores the daemon-serve flow; read is granted for
symmetry with workspace ownership so members can inspect daemons they
spawned. No production call site exercises the member-scope read path
today (read on the bare InOrg object continues to require Org-level
perms), but granting it keeps the role consistent with how members own
their other workspace-adjacent resources.
Update and delete remain dead at Member scope.
989d98f to
5253fe8
Compare
|
/coder-agents-review |
|
Chat: Review posted | View chat Review history
deep-review v0.6.1 | Round 1 | Last posted: Round 1, 2 findings (1 P2, 1 P3), COMMENT. Review Finding inventoryFindings
Round logRound 1About deep-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
There was a problem hiding this comment.
First-pass review (Netero). One P2, one P3.
The refactor from allPermsExcept(...) to explicit enumeration is well-motivated: new resources no longer silently gain member-scope access. The author's external audit (TestAuditPermDiff on the gateway-accounts branch) and the Rego owner-match guard (input.object.owner != "" && input.subject.id == input.object.owner) provide good evidence that the 34 dropped resource/action pairs were unreachable. The inline documentation explaining why each resource is included or omitted is unusually thorough.
However, this is a security-critical refactor at 0% test density. The equivalence evidence lives outside this PR. A regression test that codifies the audit would close the gap.
This is a first-pass review only: these are mechanical findings from Netero. The full review panel has not yet reviewed this PR. The panel will review after these findings are addressed.
🤖 This review was automatically generated with Coder Agents.
| // 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{ |
There was a problem hiding this comment.
P2 [CRF-1] No behavioral equivalence test for a security-critical refactor.
The PR replaces allPermsExcept(...) (wildcard across ~43 resources) with explicit enumeration (12 resources) in both OrgMemberPermissions and OrgServiceAccountPermissions. 155 production lines, 0 test lines. Existing tests (TestAuthorizeDomain, TestOrgSharingPermissions) exercise authorization scenarios through the full RBAC stack but none directly asserts that the new explicit list produces the same authorization decisions as the old wildcard.
The Rego policy confirms the owner-match guard (org_member only fires when input.object.owner != "" and input.subject.id == input.object.owner), so the 34 dropped resource/action pairs were indeed unreachable for ownerless resources. The external audit harness (TestAuditPermDiff_AllPermsExceptVsExplicit on gateway-accounts/audit-perm-diff) validates this. But that test isn't in this PR.
Could we add a regression test here (or in this stack) that asserts the member-scope permission set matches expectations? Something like calling both the old and new approach and using PermissionsEqual to compare, or a snapshot test of the explicit set. Without it, a future resource that adds WithOwner() at a production call site but isn't added to this list would be a silent privilege gap caught only by end-to-end user reports.
🤖
There was a problem hiding this comment.
No. We currently test all permissions stories for the roles. The regression is tested. See #25928 (comment) for more rational
| // 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{ |
There was a problem hiding this comment.
P3 [CRF-2] The memberPerms map is duplicated verbatim between OrgMemberPermissions and OrgServiceAccountPermissions.
Both functions now carry identical 12-entry permission maps (~60 lines each). Before the PR, both used the same one-line allPermsExcept(...) call. If a resource needs to be added or removed from member scope, two sites must be updated in lockstep.
A shared helper (e.g., baseOrgMemberPerms() Permissions) that both functions call would collapse the duplication while preserving the per-function orgPerms divergence and the member-vs-service-account comment distinction. Not a correctness issue, but a maintenance-cost concern for a security-sensitive code path where divergence between the two would be a bug.
🤖
There was a problem hiding this comment.
Fair observation, but I'd keep PR 1 as-is. Context:
- PR refactor: extract organization-workspace-access role #25929 (next in the stack) extracts the workspace-ops elevation into
OrgWorkspaceAccessMemberPerms(). After that PR, each function collapses to a ~17-linefloorblock +slices.Concat(elevation, floor). About 70% of the visible duplication goes away there. - The remaining floor is kept flat on purpose. The
minimum-implicit-memberexperiment PR after refactor: extract organization-workspace-access role #25929 will shrink the member and service-account floors independently (gateway accounts lose part of the floor; service accounts may not). A prematurecommonFloor()helper makes per-role divergence harder, not easier.
Happy to revisit if the experiment PR ends up keeping the floors identical.
Coder Agents on behalf of @Emyrk.
We do not need a regression style test inside this PR. This PR is intended to be a refactor at the set of permissions today. It is a behavior change as we add new resources, and this is intentional. To solidify the |

Refs #25936.
organization-memberwas created fromallPermsExcept(...). This is changed to an explicit enumeration of capabilities.coderd/rbac/roles.go.Note: This also needs to be done for the
memberdeployment wide role. This PR only does it for the organization member role.Coder Agents on behalf of @Emyrk.