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

Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 155 additions & 75 deletions coderd/rbac/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{

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

// 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,
},
})...,
)
// 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.
Comment thread
Emyrk marked this conversation as resolved.
})

if org.ShareableWorkspaceOwners != ShareableWorkspaceOwnersEveryone {
memberPerms = append(memberPerms, Permission{
Expand Down Expand Up @@ -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{

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

// 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,
},

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,
},
})...,
)
// 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}
}
Loading