Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Merged
Emyrk merged 3 commits into
mainfrom
gateway-accounts/enumerate-org-member
Jun 3, 2026
Merged

refactor(coderd/rbac): enumerate org-member and org-service-account perms#25928
Emyrk merged 3 commits into
mainfrom
gateway-accounts/enumerate-org-member

Conversation

@Emyrk

@Emyrk Emyrk commented Jun 1, 2026

Copy link
Copy Markdown
Member

Refs #25936.

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.
  • Behavior-preserving: same resources, same actions, same sharing-restricted negate.

Note: This also needs to be done for the member deployment wide role. This PR only does it for the organization member role.


Coder Agents on behalf of @Emyrk.

Comment thread coderd/rbac/roles.go
Comment on lines -1058 to +1120
// 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},
})

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This diff should be a functional no-op. Just moving to a declaring the perms rather than implying them through a * pattern

Emyrk commented Jun 1, 2026

Copy link
Copy Markdown
Member Author

Perm diff audit

The legacy allPermsExcept(...) wildcard in OrgMemberPermissions granted these member-scoped perms that the new explicit enumeration drops. Each row needs a justification before the floor shrink lands (later in the gateway-accounts stack).

Audit harness: branch gateway-accounts/audit-perm-diff, test TestAuditPermDiff_AllPermsExceptVsExplicit. Regenerated against PR head 5253fe8e71 (enumeration + dead-perm cleanup + provisioner_daemon.{create, read} fix). 5 new rows vs the original draft (resources whose RBACObject has no WithOwner(...) at any production call site, dropped in 0e23625c25); 5 action sets expanded (group, organization, provisioner_daemon, template, workspace_proxy).

Org perms block has no diff. One dump covers organization-member and organization-service-account under all three ShareableWorkspaceOwners settings; see the test doc comment for the symmetry argument.

# Resource Dropped actions Justification
1 ai_model_price read, update deployment_wide resource
2 ai_provider create, delete, read, update deployment_wide resource
3 ai_seat create, read deployment_wide resource
4 assign_org_role assign, create, delete, read, unassign, update org/deployment scoped, not member
5 assign_role assign, read, unassign org/deployment scoped, not member
6 audit_log create, read org/deployment scoped, not member
7 boundary_usage delete, read, update deployment_wide resource
8 connection_log read, update deployment/audit scoped, not member
9 crypto_key create, delete, read, update deployment_wide resource
10 debug_info read deployment_wide resource
11 deployment_config read, update deployment_wide resource
12 deployment_stats read deployment_wide resource
13 group create, delete, read, update org scoped (groups have no Owner; per-group ACL handles "groups I'm in")
14 idpsync_settings read, update deploymen/org scoped
15 license create, delete, read deployment_wide resource
16 notification_message create, delete user/deployment scoped
17 notification_template read, update deployment scoped
18 oauth2_app create, delete, read, update deployment scoped
19 oauth2_app_code_token create, delete, read deployment scoped
20 oauth2_app_secret create, delete, read, update deployment scoped
21 organization create, delete, read, update read is granted via Org perms; CRUD requires deployment level perms
22 provisioner_daemon delete, update org/deployment scoped; create and read are member-effective via the user-scoped Upsert path and stay on the floor
23 provisioner_jobs create, read, update no DB model; admin/system scoped, not member
24 replicas read deployment_wide resource
25 system create, delete, read, update obvious
26 tailnet_coordinator create, delete, read, update system/agent scoped, not member
27 template create, delete, read, update, use, view_insights deployment/org scope (through ACLs usually)
28 usage_event create, read, update deployment_wide resource (billing/telemetry)
29 user_secret create, delete, read, update user-scoped (granted via site member User perms)
30 user_skill create, delete, read, update user-scoped (granted via site member User perms)
31 webpush_subscription create, delete, read user-scoped (granted via site member User perms)
32 workspace_agent_devcontainers create agent scoped, not member
33 workspace_agent_resource_monitor create, read, update agent scoped, not member
34 workspace_proxy create, delete, read, update deployment scoped, not member

Resources kept on the floor (not in this diff): organization_member.read, aibridge_interception.{create, update}, api_key.*, notification_message.{read, update}, notification_preference.*, inbox_notification.*, provisioner_daemon.{create, read} (user-scoped Upsert path sets WithOwner(tag_owner); read granted for symmetry with create even though no production call site exercises Member-scope read today), plus the sharing-restricted negate.

Raw dump output
=== Org perms ===
  (no diff)

=== Member perms ===
  ai_model_price
    - read
    - update
  ai_provider
    - create
    - delete
    - read
    - update
  ai_seat
    - create
    - read
  assign_org_role
    - assign
    - create
    - delete
    - read
    - unassign
    - update
  assign_role
    - assign
    - read
    - unassign
  audit_log
    - create
    - read
  boundary_usage
    - delete
    - read
    - update
  connection_log
    - read
    - update
  crypto_key
    - create
    - delete
    - read
    - update
  debug_info
    - read
  deployment_config
    - read
    - update
  deployment_stats
    - read
  group
    - create
    - delete
    - read
    - update
  idpsync_settings
    - read
    - update
  license
    - create
    - delete
    - read
  notification_message
    - create
    - delete
  notification_template
    - read
    - update
  oauth2_app
    - create
    - delete
    - read
    - update
  oauth2_app_code_token
    - create
    - delete
    - read
  oauth2_app_secret
    - create
    - delete
    - read
    - update
  organization
    - create
    - delete
    - read
    - update
  provisioner_daemon
    - delete
    - update
  provisioner_jobs
    - create
    - read
    - update
  replicas
    - read
  system
    - create
    - delete
    - read
    - update
  tailnet_coordinator
    - create
    - delete
    - read
    - update
  template
    - create
    - delete
    - read
    - update
    - use
    - view_insights
  usage_event
    - create
    - read
    - update
  user_secret
    - create
    - delete
    - read
    - update
  user_skill
    - create
    - delete
    - read
    - update
  webpush_subscription
    - create
    - delete
    - read
  workspace_agent_devcontainers
    - create
  workspace_agent_resource_monitor
    - create
    - read
    - update
  workspace_proxy
    - create
    - delete
    - read
    - update

Coder Agents on behalf of @Emyrk.

@Emyrk
Emyrk force-pushed the gateway-accounts/enumerate-org-member branch 2 times, most recently from 79be71f to 989d98f Compare June 1, 2026 21:03
@Emyrk
Emyrk marked this pull request as ready for review June 1, 2026 21:53
@Emyrk
Emyrk requested a review from geokat June 2, 2026 13:19
Emyrk added 3 commits June 2, 2026 13:19
…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.
@Emyrk

Emyrk commented Jun 2, 2026

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-06-02 14:25 UTC by @Emyrk
Spend: $3.92 / $100.00

Review history
  • R1 (2026-06-02), 1 P2, 1 P3, COMMENT. Review

deep-review v0.6.1 | Round 1 | 93b067f..5253fe8

Last posted: Round 1, 2 findings (1 P2, 1 P3), COMMENT. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P2 Open coderd/rbac/roles.go:1067 No behavioral equivalence test for security-critical refactor claiming "behavior-preserving" R1 Netero Yes
CRF-2 P3 Open coderd/rbac/roles.go:1201 Member-level permission map duplicated verbatim between OrgMemberPermissions and OrgServiceAccountPermissions R1 Netero Yes

Round log

Round 1

Netero-only. 1 P2, 1 P3. Reviewed against 93b067f..5253fe8.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread coderd/rbac/roles.go
// 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{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. We currently test all permissions stories for the roles. The regression is tested. See #25928 (comment) for more rational

Comment thread coderd/rbac/roles.go
// 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{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

🤖

@Emyrk Emyrk Jun 2, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-line floor block + slices.Concat(elevation, floor). About 70% of the visible duplication goes away there.
  • The remaining floor is kept flat on purpose. The minimum-implicit-member experiment 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 premature commonFloor() 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝️ agreed. I will leave it

@Emyrk

Emyrk commented Jun 2, 2026

Copy link
Copy Markdown
Member Author

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.

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 allPermsExcept would solidify the incorrect ideology to assume permissions. We are now moving to explicit permission listing

Comment thread coderd/rbac/roles.go
@Emyrk
Emyrk merged commit f1ebc42 into main Jun 3, 2026
49 of 51 checks passed
@Emyrk
Emyrk deleted the gateway-accounts/enumerate-org-member branch June 3, 2026 13:14
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants