-
Notifications
You must be signed in to change notification settings - Fork 1.5k
refactor(coderd/rbac): enumerate org-member and org-service-account perms #25928
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(), | ||
|
|
||
| // 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. | ||
|
Emyrk marked this conversation as resolved.
|
||
| }) | ||
|
|
||
| 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{ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3 [CRF-2] The Both functions now carry identical 12-entry permission maps (~60 lines each). Before the PR, both used the same one-line A shared helper (e.g.,
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair observation, but I'd keep PR 1 as-is. Context:
Happy to revisit if the experiment PR ends up keeping the floors identical. Coder Agents on behalf of @Emyrk.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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} | ||
| } | ||
There was a problem hiding this comment.
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 bothOrgMemberPermissionsandOrgServiceAccountPermissions. 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_memberonly fires wheninput.object.owner != ""andinput.subject.id == input.object.owner), so the 34 dropped resource/action pairs were indeed unreachable for ownerless resources. The external audit harness (TestAuditPermDiff_AllPermsExceptVsExplicitongateway-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
PermissionsEqualto compare, or a snapshot test of the explicit set. Without it, a future resource that addsWithOwner()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.
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