feat: add group and user ACLs to MCP server configs by ibetitsmike · Pull Request #27944 · coder/coder · GitHub
Skip to content

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

Merged
ibetitsmike merged 47 commits into
mainfrom
mike/codagt-712-mcp-acl
Aug 19, 2026
Merged

feat: add group and user ACLs to MCP server configs#27944
ibetitsmike merged 47 commits into
mainfrom
mike/codagt-712-mcp-acl

Conversation

@ibetitsmike

@ibetitsmike ibetitsmike commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

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.

@linear-code

linear-code Bot commented Aug 8, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Docs preview

Check off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here.

@ibetitsmike ibetitsmike changed the title mike/codagt 712 mcp acl feat: add group and user ACLs to MCP server configs Aug 8, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f646cb2a18

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coderd/mcp_acl.go
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-712-mcp-acl branch from f646cb2 to 7ae5275 Compare August 8, 2026 07:41
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-712-mcp-acl branch 2 times, most recently from 049edea to d34f578 Compare August 8, 2026 08:23
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-712-mcp-acl branch from d34f578 to ae152bb Compare August 8, 2026 08:50
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-712-mcp-acl branch from ae152bb to 2582868 Compare August 10, 2026 05:25
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-712-mcp-acl branch from 2582868 to 35e7987 Compare August 10, 2026 05:37
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-712-mcp-acl branch from 35e7987 to be9dc1f Compare August 10, 2026 05:54
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-712-mcp-acl branch from be9dc1f to 81335a8 Compare August 10, 2026 06:13
@coderagents

coderagents Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Documentation Check

Updates Needed

  • docs/ai-coder/agents/platform-controls/mcp-servers.md - Auditors (users with audit-log read permission) get the full management listing of every MCP server config in the org, including disabled and ACL-restricted servers with management fields, so they can inspect audit-logged resources. Per-server access still follows the ACL: auditors cannot view a single restricted server's detail, select it, or connect to it (those return 404). The Permissions table and Access control section only cover org admins and ACL-granted members, so they should note this auditor listing visibility and that it does not grant per-server access.
  • coderd/x/chatd/ARCHITECTURE.md - The chatd MCP-authorization behavior added by this PR (forcedMCPServerConfigsForOwner, enforceForcedMCPServerIDs, enabledMCPServerConfigsForChatOrg in chatd.go/generation_preparer.go) is still not documented here.

    ⚠️ The earlier TODO(PR author) placeholder was removed, but no MCP server authorization section was written to replace it. Per AGENTS.md, chatd changes that affect the architecture must update this doc, so a real section is still needed (owner-ACL evaluation on chat creation/message updates/generation, the rbac.ScopeAll subject load, the Explore-chat immutable-snapshot rule, and the grandfathering rule for already-selected servers).


Automated review via Coder Agents

@ibetitsmike
ibetitsmike force-pushed the mike/codagt-712-mcp-acl branch from 81335a8 to 02b5fe3 Compare August 10, 2026 08:12
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 02b5fe3658

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ibetitsmike
ibetitsmike force-pushed the mike/codagt-712-mcp-acl branch from 02b5fe3 to dc8c6e3 Compare August 10, 2026 08:37
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

The auditor roles' wildcard MCPServerConfig read overrode per-server
ACLs in every row-level read: OAuth2 connect and chat server selection
admitted auditors on servers their ACL withheld. Keep the auditors'
full management listing by fetching with system access behind the
existing update-or-audit gate, and let row-level reads follow the ACL
alone.
Consolidate the duplicated ACL role-update loops into one helper that
owns UUID key canonicalization, and tighten two review-round comments.
Two accepted spellings of one UUID collapse to a single canonical key,
so conflicting roles were resolved by map iteration order. Rekey the
request maps canonically during validation and return 400 when two
keys collapse to the same principal.
The ACL PATCH handler mapped every non-authorization transaction error
to 500, so a config deleted between the middleware fetch and the locked
re-fetch surfaced as an internal error. Map the not-found case to the
same concealed 404 the update and delete handlers return.
Add godoc to the new exported ACL types and client methods, note that
the down migration cannot remove the share scope enum value, and
regenerate the TypeScript types that carry the comments.
Main claimed 000567, shifting the stack's migrations by one.
Main claimed 000568, shifting the audit migration to 000570.
Force On previously loaded the forced set with daemon scope, so a
member the server's ACL denies still received its tools in every
chat. The forced query now runs as the chat owner and the dbauthz
post-filter drops servers the owner cannot read.
… from pre-ACL replicas

During a rolling upgrade a pre-ACL replica's insert omits both ACL
columns, so the '{}' defaults would hide the new server from every
ordinary member. A BEFORE INSERT trigger backfills the same Everyone
grant the application applies when both ACLs are empty.
Follows the audit migration's move to 000571 after main took 000569.
…ure doc

Chatd architecture prose must be written by the human PR author, so
leave a scoped TODO listing the owner ACL evaluation points and
invariants the new section must cover.
The full-view list fetch bypasses row authorization with system access,
so a key scoped only to mcp_server_config:update or audit_log:read could
read config data its scope does not cover. Keep roles deciding full-view
eligibility, but require the API key scope itself to permit reading MCP
server configs before bypassing row authorization.
Upgrades run in scheduled maintenance downtime with the database locked
during migration, so no pre-ACL replica inserts configs after the
migration commits. The API sets the Everyone read grant explicitly on
every insert.
Delete-authorized callers without update or audit grants could not see
or fetch disabled configs: the list filtered them out and the detail
endpoint concealed them with 404, so a delete-only custom role could
never remove a disabled server. Keep disabled configs in the redacted
list and detail responses when the caller may delete them.
httpapi.Is404Error already returns true for dbauthz.IsNotAuthorizedError,
so the Forbidden branch after it could never execute. Concealing
authorization failures as 404 is the documented intent here, matching
the update and delete handlers.
The ACL authorization behavior does not change the chatd architecture,
so the architecture doc sections flagged for it are not needed.

Reverts a0aca79 and 91b3015.
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.

2 participants