Skip to content

feat: gate workspace ACL grants on a use_shared capability#27281

Draft
jscottmiller wants to merge 5 commits into
scott/gateway-2-ai-gateway-rolefrom
scott/gateway-3-use-shared
Draft

feat: gate workspace ACL grants on a use_shared capability#27281
jscottmiller wants to merge 5 commits into
scott/gateway-2-ai-gateway-rolefrom
scott/gateway-3-use-shared

Conversation

@jscottmiller

@jscottmiller jscottmiller commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Adds a new workspace action, use_shared, that acts as a capability precondition for ACL entries: the rego policy only grants ACL-based access to an object while the subject holds the object type's use_shared action at the member level in the object's organization. This prevents sharing workspaces with users who lack workspace capabilities (such as AI-Gateway-only accounts) and revokes shared access live when the capability is revoked, without deleting the ACL entry; regaining the capability restores the share.

Mechanism

Declaration-driven: any resource type that declares ActionUseShared in its policy action set is gated, with no per-type rego edits. The flag is derived from policy.RBACPermissions in Go (rbac.ACLUseGated) and passed as the input field input.object.acl_use_gated, using an explicit == false comparison so input paths that omit the field fail closed. Because the flag is computed from the object type alone, it is known during partial evaluation and the precondition resolves at prepare time; residual queries and the regosql conversion are unaffected. The field is included in the AST fast path, the partial input, and Object's JSON encoding so all input paths stay consistent.

Currently gated: workspace and workspace_dormant. The capability travels with organization-workspace-access and, while MinimumImplicitMember is off, with the elevation bundled into organization-member, so default deployments see no behavior change. shared_use_orgs follows standard vote semantics: a negated member-level use_shared permission removes the org even when a positive grant exists.

API surface

  • PATCH /workspaces/:id/acl rejects user recipients who fail the capability check with a validation error, so shares don't silently grant nothing. Group entries are intentionally not validated at share time (membership changes after the share); eligibility is enforced per-member at access time.
  • use_shared is omitted from the admin workspace ACL role's stored action set: it is a role-carried capability that ACL entries never grant, and including it would break the exact-set round-trip of previously stored admin entries in convertToWorkspaceRole.
  • New api_key_scope enum values are added by migration and remain internal-only (not in the external scope catalog).

Includes a skipped desired-behavior test (TestWorkspaceSharingDormancySurvivesACL) documenting a pre-existing bug where DormantRBAC() drops ACLs; the precondition neither causes nor fixes it. Documented in coderd/rbac/POLICY.md.

Part of the gateway-accounts exploration.

Stack

Part 3 of the gateway-accounts stack. Each PR builds on the previous:

  1. feat: count only workspace-capable users toward license seats #27279: permission-based license seat counting. Behind the permission-based-licensing experiment and gated on the AI Governance add-on, user_limit counts only users the RBAC engine authorizes to create workspaces.
  2. feat: add organization-ai-gateway-access role and enforce it at the AI Gateway #27280: adds the organization-ai-gateway-access org role carrying the AI Bridge interception permissions (extracted from the member floors, backfilled into org default roles by migration) and enforces it at AI Gateway authentication; bridge usage stops claiming AI Governance seats under the experiment.
  3. feat: gate workspace ACL grants on a use_shared capability #27281 (this PR): adds a use_shared capability precondition for workspace ACL grants, so workspace sharing is ineffective for (and rejected toward) users without workspace capabilities, evaluated live on every authorization.

Related but independent: #27278 hides the Workspaces page create CTAs for users without workspace-create permission.

Adds a new action, use_shared, that acts as a capability precondition
for ACL entries. The rego policy only grants ACL-based access to an
object when the subject also holds the object type's use_shared action
at the member level in the object's organization. This prevents sharing
workspaces with users who lack workspace capabilities, such as
AI-Gateway-only accounts, and revokes shared access live when the
capability is revoked.

The mechanism is declaration-driven: any resource type that declares
ActionUseShared in its policy action set is gated, with no rego edits
per type. The flag is derived from policy.RBACPermissions in Go
(rbac.ACLUseGated) and passed as the input field
input.object.acl_use_gated. Because it is computed from the object type
alone, it is known during partial evaluation and the precondition
resolves at prepare time; residual queries and the regosql conversion
are unaffected. The field is included in the AST fast path, the partial
input, and Object's JSON encoding so all input paths stay consistent.

Currently gated: workspace and workspace_dormant. The capability
travels with organization-workspace-access (use_shared is included in
the dormant action list so ACL recipients keep access to shared
workspaces that go dormant). shared_use_orgs follows standard vote
semantics: a negated member-level use_shared permission removes the org
even when a positive grant exists.

Share-time validation in patchWorkspaceACL rejects recipients who fail
the capability check with a validation error, so shares don't silently
grant nothing; access-time enforcement still applies if the capability
is revoked later. New api_key_scope enum values are added via migration
000542 and remain internal-only (not in the external scope catalog).
Documented in POLICY.md.
… actions

The admin workspace ACL role expands from AvailableActions(), so adding
use_shared to the workspace action set silently grew the stored action
list and broke the exact-set match in convertToWorkspaceRole: admin
entries stored before the change rendered as role-less (deleted) in ACL
API responses, and a read-modify-write client would then remove the
share. use_shared is a capability precondition evaluated from the
subject's role permissions, never from ACL entries, so storing it in an
ACL action list is inert. Omitting it restores round-tripping for
pre-existing entries.
…hare time

Group membership changes after a share is created, so per-member
use_shared eligibility is enforced at access time only. A share to a
group whose members all currently lack the capability is accepted and
grants nothing until a member gains it.
not input.object.acl_use_gated evaluates true when the field is
undefined, so an input path that omitted the field would skip the ACL
use_shared precondition entirely. An explicit == false comparison is
undefined on a missing field, failing the rule and denying the ACL
grant. All current input paths set the field; behavior is unchanged
for them.
…vior, skipped

Converts the dormancy ACL reproduction into a desired-behavior test:
recipients keep access to a shared workspace through dormancy, an
admin-role recipient can wake it, and the list and direct-access views
agree. Skipped while DormantRBAC() attaches no ACLs; removing the skip
activates the test once it does.
@github-actions

Copy link
Copy Markdown

Docs preview

📖 View docs preview for docs/reference/api/members.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant