feat: add group and user ACLs to MCP server configs (#27944)

Adds group and user ACLs to org-scoped MCP server configs so
organizations can restrict specific MCP servers to subsets of members,
mirroring the template ACL pattern.

## Summary

- Migration adds `group_acl`/`user_acl` JSONB columns (nested `{"<id>":
{"permissions": [...]}}` shape) and seeds every existing config with its
organization's Everyone group read entry (the Everyone group ID equals
the org ID), so member access is unchanged by default. Creation seeds
the same entry.
- The blanket org-member read grant from the base PR is replaced by ACL
evaluation: Rego requires org membership for every ACL grant, and
`GetAuthorizedMCPServerConfigs` compiles ACL-aware SQL filters.
- New `ActionShare` (org admins) gates `GET|PATCH
/api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl`
(nested under the organization like the rest of the config surface).
PATCH validates principals against the config's organization, merges
sparse updates under a row lock, stamps `updated_by`/`updated_at`, and
is audited as a Write with `Old` captured before authorization. A config
deleted concurrently between the middleware fetch and the locked
re-fetch is concealed as 404, matching the update and delete handlers.
ACL columns are tracked in the audit table.
- ACL management is available in all editions (no enterprise
entitlement), documented in the MCP servers page. Revoking an ACL does
not retro-strip already-selected configs from existing chats; new
selection is blocked at chat create.
- Force On respects the ACL: the forced set is loaded as the chat owner,
so a `force_on` server whose ACL denies the owner never attaches at
create, send, or generation time.
- No rolling-upgrade machinery: upgrades run in scheduled maintenance
downtime, so the migration only backfills existing rows; the API sets
the Everyone read grant explicitly on every insert.

Stacked on #27943. Part of the MCP org-separation stack.

Closes https://linear.app/codercom/issue/CODAGT-712

UAT: verified on a dogfood instance with two members and a custom group:
Everyone-seed default visibility, group grant with Everyone removal
(non-member loses list/fetch/selection), foreign-principal rejection,
non-admin share denial (audited 403 / concealed 404), user_acl restore,
and audited ACL diffs.

> Mux (AI agent) authored this PR on Mike's behalf.

<!-- mux-attribution: model=claude-fable-5 thinking=high -->
This commit is contained in:
Michael Suchacz
2026-08-19 19:25:18 +00:00
committed by GitHub
parent 299e72ad30
commit 7ca7c30f40
46 changed files with 2114 additions and 132 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ We track the following resources:
| GroupSyncSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>auto_create_missing_groups</td><td>true</td></tr><tr><td>field</td><td>true</td></tr><tr><td>legacy_group_name_mapping</td><td>false</td></tr><tr><td>mapping</td><td>true</td></tr><tr><td>regex_filter</td><td>true</td></tr></tbody></table> |
| HealthSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>dismissed_healthchecks</td><td>true</td></tr><tr><td>id</td><td>false</td></tr></tbody></table> |
| License<br><i>create, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>exp</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwt</td><td>false</td></tr><tr><td>uploaded_at</td><td>true</td></tr><tr><td>uuid</td><td>true</td></tr></tbody></table> |
| MCPServerConfig<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>allow_in_plan_mode</td><td>true</td></tr><tr><td>api_key_header</td><td>true</td></tr><tr><td>api_key_value</td><td>true</td></tr><tr><td>api_key_value_key_id</td><td>false</td></tr><tr><td>auth_type</td><td>true</td></tr><tr><td>availability</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>custom_headers</td><td>true</td></tr><tr><td>custom_headers_key_id</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>enabled</td><td>true</td></tr><tr><td>forward_coder_headers</td><td>true</td></tr><tr><td>icon_url</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>model_intent</td><td>true</td></tr><tr><td>oauth2_auth_url</td><td>true</td></tr><tr><td>oauth2_client_id</td><td>true</td></tr><tr><td>oauth2_client_secret</td><td>true</td></tr><tr><td>oauth2_client_secret_key_id</td><td>false</td></tr><tr><td>oauth2_revocation_url</td><td>true</td></tr><tr><td>oauth2_scopes</td><td>true</td></tr><tr><td>oauth2_token_url</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>slug</td><td>true</td></tr><tr><td>tool_allow_list</td><td>true</td></tr><tr><td>tool_deny_list</td><td>true</td></tr><tr><td>transport</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>updated_by</td><td>true</td></tr><tr><td>url</td><td>true</td></tr></tbody></table> |
| MCPServerConfig<br><i>create, write, delete</i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>allow_in_plan_mode</td><td>true</td></tr><tr><td>api_key_header</td><td>true</td></tr><tr><td>api_key_value</td><td>true</td></tr><tr><td>api_key_value_key_id</td><td>false</td></tr><tr><td>auth_type</td><td>true</td></tr><tr><td>availability</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>created_by</td><td>true</td></tr><tr><td>custom_headers</td><td>true</td></tr><tr><td>custom_headers_key_id</td><td>false</td></tr><tr><td>description</td><td>true</td></tr><tr><td>display_name</td><td>true</td></tr><tr><td>enabled</td><td>true</td></tr><tr><td>forward_coder_headers</td><td>true</td></tr><tr><td>group_acl</td><td>true</td></tr><tr><td>icon_url</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>model_intent</td><td>true</td></tr><tr><td>oauth2_auth_url</td><td>true</td></tr><tr><td>oauth2_client_id</td><td>true</td></tr><tr><td>oauth2_client_secret</td><td>true</td></tr><tr><td>oauth2_client_secret_key_id</td><td>false</td></tr><tr><td>oauth2_revocation_url</td><td>true</td></tr><tr><td>oauth2_scopes</td><td>true</td></tr><tr><td>oauth2_token_url</td><td>true</td></tr><tr><td>organization_id</td><td>false</td></tr><tr><td>slug</td><td>true</td></tr><tr><td>tool_allow_list</td><td>true</td></tr><tr><td>tool_deny_list</td><td>true</td></tr><tr><td>transport</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr><tr><td>updated_by</td><td>true</td></tr><tr><td>url</td><td>true</td></tr><tr><td>user_acl</td><td>true</td></tr></tbody></table> |
| NotificationTemplate<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>actions</td><td>true</td></tr><tr><td>body_template</td><td>true</td></tr><tr><td>enabled_by_default</td><td>true</td></tr><tr><td>group</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>kind</td><td>true</td></tr><tr><td>method</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>title_template</td><td>true</td></tr></tbody></table> |
| NotificationsSettings<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>id</td><td>false</td></tr><tr><td>notifier_paused</td><td>true</td></tr></tbody></table> |
| OAuth2ProviderApp<br><i></i> | <table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody> | <tr><td>callback_url</td><td>true</td></tr><tr><td>client_id_issued_at</td><td>false</td></tr><tr><td>client_secret_expires_at</td><td>true</td></tr><tr><td>client_type</td><td>true</td></tr><tr><td>client_uri</td><td>true</td></tr><tr><td>contacts</td><td>true</td></tr><tr><td>created_at</td><td>false</td></tr><tr><td>dynamically_registered</td><td>true</td></tr><tr><td>grant_types</td><td>true</td></tr><tr><td>icon</td><td>true</td></tr><tr><td>id</td><td>false</td></tr><tr><td>jwks</td><td>true</td></tr><tr><td>jwks_uri</td><td>true</td></tr><tr><td>logo_uri</td><td>true</td></tr><tr><td>name</td><td>true</td></tr><tr><td>policy_uri</td><td>true</td></tr><tr><td>redirect_uris</td><td>true</td></tr><tr><td>registration_access_token</td><td>true</td></tr><tr><td>registration_client_uri</td><td>true</td></tr><tr><td>response_types</td><td>true</td></tr><tr><td>scope</td><td>true</td></tr><tr><td>software_id</td><td>true</td></tr><tr><td>software_version</td><td>true</td></tr><tr><td>token_endpoint_auth_method</td><td>true</td></tr><tr><td>tos_uri</td><td>true</td></tr><tr><td>updated_at</td><td>false</td></tr></tbody></table> |
@@ -44,11 +44,11 @@ organization's servers; other organizations are managed through the API.
#### Availability policies
| Policy | Behavior |
|---------------|--------------------------------------------------------|
| `force_on` | Always injected into every chat. Users cannot opt out. |
| `default_on` | Pre-selected in new chats. Users can opt out. |
| `default_off` | Available in the server list but users must opt in. |
| Policy | Behavior |
|---------------|-----------------------------------------------------------------------------------|
| `force_on` | Injected into every chat whose owner has ACL access to the server. No opting out. |
| `default_on` | Pre-selected in new chats. Users can opt out. |
| `default_off` | Available in the server list but users must opt in. |
## Authentication
@@ -168,11 +168,17 @@ wins.
## Permissions
| Action | Required role |
|-------------------------------|---------------------|
| Create, update, or delete | Organization admin |
| View enabled servers | Organization member |
| OAuth2 connect and disconnect | Organization member |
| Action | Required role |
|---------------------------|----------------------------|
| Create, update, or delete | Organization admin |
| View enabled servers | Member granted through ACL |
| OAuth2 connect | Member granted through ACL |
| OAuth2 disconnect | Token owner |
| Manage ACLs | Organization admin |
Disconnect only needs a valid session: users removed from the ACL or the
organization can still delete their stored token and revoke the provider
grant.
Members only see enabled servers in their own organizations. Sensitive fields
such as API keys and client secrets are redacted in API responses.
@@ -180,3 +186,18 @@ such as API keys and client secrets are redacted in API responses.
The **MCP servers** settings page is part of deployment settings, so opening it in the dashboard also requires permission to edit deployment configuration.
Organization admins without that permission can manage servers through the API.
Creating or updating a server with `auth_type` set to `user_oidc` also requires the `deployment_config:update` permission.
### Access control
Each server has a group and user ACL that controls which members can see and
use it. New servers grant read access to the organization's **Everyone** group,
so all members have access by default. Admins can remove the Everyone entry and
grant specific groups or users instead through the API
(`GET`/`PATCH /api/experimental/organizations/{organization}/mcp-servers/{id}/acl`); there is no ACL editor
in the settings page. ACL management is available in all editions and does not
require an enterprise entitlement. ACL changes are recorded in the audit log.
Revoking access stops a member from newly selecting the server in any chat,
but chats that already have the server selected keep using it, the same way
existing workspaces keep running after template access is revoked. To cut
off existing chats as well, disable or delete the server.
+180 -3
View File
File diff suppressed because one or more lines are too long