fix: key MCP server management authz on server grant ids - #5909
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F1AvLHvF7Z5UH66JQzYPwr
🦋 Changeset detectedLatest commit: 251cf4e The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Running ultrareview automatically — This PR rewires RBAC for MCP server management endpoints to authorize against per-server grant resource IDs instead of the project ID, which could cause authorization bypass or lockout if subtly wrong, so it benefits from a deeper security-focused review.. I'll post findings when complete. |
There was a problem hiding this comment.
Ultrareview completed in 6m 15s
All reported issues were addressed across 8 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F1AvLHvF7Z5UH66JQzYPwr
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F1AvLHvF7Z5UH66JQzYPwr
Summary
MCP server management endpoints (
mcpservers.get,listToolFilters,list,update,delete) previously authorizedmcp:read/mcp:writewith the project id as the check's resource id, so a role grant scoped to a single server never matched — it granted serving/connect access but none of the management surface, while the dashboard's team-access matrix implied otherwise. This keys those checks on the server's grant resource id instead, matching the toolsets surface and the serving path:grantResourceID(id, toolsetID)helper mirrors the grant-id invariant used everywhere else (toolset id when toolset-backed, else themcp_serversrow id).mcp:readagainst the loaded row's grant id (project id stays as a selector dimension, so project-wide and wildcard grants keep matching).mcp:writeagainst the locked row's grant id before any mutation; update authorizes against the row's existing backing, not the backing the payload may switch it to.authz.Filter, same astoolsets.List— callers see exactly the servers they holdmcp:readfor; no matching grants returns an empty list rather than 403.New
rbac_scoped_test.gocovers server-scoped allow/deny for get/list/update/delete, toolset-id keying for toolset-backed servers, and read-grant-cannot-write. Pre-existing RBAC tests that asserted a 403 before the row lookup were updated to real fixtures.Behavior changes to be aware of:
mcp:read/mcp:writewithresource_idset to a project uuid — only ever matched these endpoints because the old checks used the project id as the resource id, and no longer matches. The dashboard never writes that shape; the canonical project-wide form (resource_id='*'+project_iddimension) is unaffected.Motivation
The Grant Access dialog (#5883) one-click-creates roles scoped to a single server. Adversarial review of that PR found the scoped grants were only fully effective for toolset-backed servers: for remote/tunneled servers the management endpoints checked a project-keyed selector no grant editor ever produces per-server, so a "this server only"
mcp:writegrant did not actually let the member manage that server. Aligning the management checks with the grant-id invariant makes server-scoped roles mean what the UI says they mean. A frontend follow-up can then relax the dialog's restriction that offers read/write scoped grants only for toolset-backed servers.🤖 Generated with Claude Code
https://claude.ai/code/session_01F1AvLHvF7Z5UH66JQzYPwr
Summary by cubic
Fixes MCP server management authorization so server-scoped role grants actually control management endpoints. Management checks previously authorized with the project id as the resource id, so a grant scoped to a single server never matched; they now key on the server's grant resource id (toolset id for toolset-backed servers, else the MCP server row id), aligning with the serving path and the dashboard's access matrix.
Changes
grantResourceIDhelper to derive the grant resource id consistently across endpoints.mcp:read/mcp:writeagainst the server's grant id; the project id remains a selector dimension.FOR UPDATElocks by pre-checking against a non-locking read, then re-checking against the locked row.authz.Filter, so callers see only servers they holdmcp:readfor; no matching grants returns an empty list instead of 403.resource_idset to a project uuid no longer matches, but the canonical project-wide form (resource_id='*'+project_iddimension) is unaffected.rbac_scoped_test.gocovering server-scoped allow/deny for get/list/update/delete, toolset-id keying across all endpoints, and read-grant-cannot-write; pre-existing RBAC tests were updated to use real server fixtures.Written for commit 251cf4e. Summary will update on new commits.