🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

feat: gate workspace ACL grants on matching member capability - #27281

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

feat: gate workspace ACL grants on matching member capability#27281
jscottmiller wants to merge 6 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

Gates workspace ACL entries on a per-action capability precondition: the rego policy only grants an ACL entry's action while the subject holds that same action (or the wildcard) 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. Subjects whose roles carry only some of an entry's actions receive partial effect.

There is no new action or schema change: the precondition matches the granted action itself against the recipient's member-level permissions ("you may receive X on others' workspaces only where your roles grant you X on your own"), so capability and receipt cannot drift apart. Design discussion and alternatives considered (binary use_shared sentinel, separate incoming-mask permission key, universal matching) are written up in coder-internal scott-misc:rfcs/gateway-accounts/design-acl-capability-gating.md; an earlier sentinel-based implementation is preserved on scott/gateway-3-use-shared-v1.

Mechanism

Opt-in per type via an explicit GatedACL bool on the policy PermissionDefinition; no per-type rego edits. The flag is derived from the object type 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 acl_action_orgs depends only on subject roles, object type, and the action — all known at prepare time — the precondition resolves during partial evaluation and list-query SQL filters enforce it; 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. Member-level workspace actions travel with organization-workspace-access and, while MinimumImplicitMember is off, with the elevation bundled into organization-member, so default deployments see no behavior change. acl_action_orgs follows standard vote semantics: a negated member-level permission for an action removes the org for that action even when a positive grant exists, while other granted actions keep working. Ungated types (templates, chats) keep ACL-only grants.

API surface

  • PATCH /workspaces/:id/acl validates user recipients by simulating the grant: each granted action is authorized against the workspace's RBAC object with the proposed entry attached, and recipients for whom no action would authorize are rejected with a validation error, so shares don't silently grant nothing. Partial capability is accepted with partial effect. Recipient existence and role resolution run as system and fail closed when roles cannot be resolved. Group entries are intentionally not validated at share time (membership changes after the share); eligibility is enforced per-member at access time.
  • License seat counting (user_limit under permission-based licensing) counts users holding any member-level workspace action, since such users can operate workspaces shared with them even without workspace-create.

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 feature.

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: exclude AI Bridge usage from AI Governance seat counting #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 matching member capability #27281 (this PR): gates workspace ACL grants on matching member-level capability, 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.

@github-actions

Copy link
Copy Markdown

Docs preview

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

@jscottmiller
jscottmiller force-pushed the scott/gateway-2-ai-gateway-role branch from 2e8e016 to 60c04e2 Compare July 16, 2026 20:28
@jscottmiller
jscottmiller force-pushed the scott/gateway-3-use-shared branch from 12f701f to 9864191 Compare July 17, 2026 02:03
@jscottmiller
jscottmiller force-pushed the scott/gateway-2-ai-gateway-role branch from 8c0e6ca to 2e8e016 Compare July 17, 2026 02:04
@jscottmiller
jscottmiller force-pushed the scott/gateway-3-use-shared branch 2 times, most recently from d405949 to 061ff57 Compare July 19, 2026 23:00
@jscottmiller jscottmiller changed the title feat: gate workspace ACL grants on a use_shared capability feat: gate workspace ACL grants on matching member capability Jul 19, 2026
@jscottmiller
jscottmiller force-pushed the scott/gateway-2-ai-gateway-role branch 3 times, most recently from 77954f0 to 99b478e Compare July 20, 2026 15:51
@jscottmiller
jscottmiller force-pushed the scott/gateway-3-use-shared branch 2 times, most recently from 3593109 to 5a894e0 Compare July 20, 2026 21:29
…ility

ACL entries on gated resource types (GatedACL in the policy permission
definition; workspace and workspace_dormant) only take effect per
action: the subject must hold the granted action, or the wildcard, at
the member level in the object's organization (acl_action_orgs, with
standard vote semantics). Revoking the roles that carry an action
revokes it on everything shared with the subject, evaluated live on
every authorization; subjects holding only some of an entry's actions
receive partial effect.

The gating flag reaches the policy as the derived input field
acl_use_gated, computed from the object type in Go and injected on all
three rego input paths, so the precondition resolves at prepare time
and list-query SQL filters enforce it too. Ungated types (templates,
chats) keep ACL-only grants.
patchWorkspaceACL authorizes each granted action for the recipient
against the workspace's RBAC object with the proposed ACL entry
attached, rejecting recipients for whom no action would authorize so
shares don't silently grant nothing. Partial capability is accepted
with partial effect, matching the per-action policy. Recipient
existence and role resolution run as system, failing closed when roles
cannot be resolved; group entries stay unvalidated since membership
changes after the share and is enforced per-member at access time.

The dormant-workspace sharing test documents desired behavior and is
skipped until DormantRBAC() carries workspace ACLs.
…pace seats

A user whose roles carry any member-level workspace action can operate
workspaces shared with them even without workspace-create, so seat
counting probes the full workspace action set. This also counts
workspace-access users under a creation ban, who keep their existing
workspaces.
…d to end

A recipient holding every workspace action except a member-level-negated
update receives an admin share: the share is accepted, read and list
work, and the negated action is denied at access time. The recipient is
built from a system custom role because dbauthz only permits member-key
permissions on system roles, and negation avoids the MinimumImplicitMember
global that concurrent test servers reset.

putWorkspaceAutostart now maps authorization denials to 404 like its
sibling handlers; it previously returned 500 for subjects with read but
not update, which per-action ACL grants make a common state.
…idens

Evaluates the policy with raw JSON input omitting the derived
acl_use_gated field: an ungated type's ACL grant is denied for a subject
without matching member-level actions (with a present-field control
proving the deny comes from the missing field), while gated-type grants
keep working for capable subjects via acl_action_orgs. Guards the
explicit == false comparison in acl_use_precondition against truthiness
refactors and missed injection sites.
Deduplicates the gating definition (canonical on GatedACL), drops
feature-framing from the rego and share-validation comments, and removes
external doc references from the skipped dormancy test.
@jscottmiller
jscottmiller force-pushed the scott/gateway-3-use-shared branch from 5a894e0 to 77c9afc Compare July 21, 2026 04:12
@jscottmiller

Copy link
Copy Markdown
Contributor Author

This is now out of scope for the gateway accounts MVP. This work may be resurrected later.

@github-actions github-actions Bot locked and limited conversation to collaborators Jul 26, 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.

1 participant