Skip to content

fix: key MCP server management authz on server grant ids - #5909

Merged
daviddanialy merged 3 commits into
mainfrom
mcp-server-scoped-management-authz
Sep 1, 2026
Merged

fix: key MCP server management authz on server grant ids#5909
daviddanialy merged 3 commits into
mainfrom
mcp-server-scoped-management-authz

Conversation

@daviddanialy

@daviddanialy daviddanialy commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Summary

MCP server management endpoints (mcpservers.get, listToolFilters, list, update, delete) previously authorized mcp:read/mcp:write with 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:

  • New grantResourceID(id, toolsetID) helper mirrors the grant-id invariant used everywhere else (toolset id when toolset-backed, else the mcp_servers row id).
  • Get / tool-filter reads check mcp:read against the loaded row's grant id (project id stays as a selector dimension, so project-wide and wildcard grants keep matching).
  • Update / delete check mcp:write against 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.
  • Listing replaces the blanket project check with a per-server authz.Filter, same as toolsets.List — callers see exactly the servers they hold mcp:read for; no matching grants returns an empty list rather than 403.
  • Create stays project-keyed: a per-server grant cannot authorize creating new servers.

New rbac_scoped_test.go covers 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:

  1. Get/update/delete on an existing server without a matching grant now return forbidden after the project-scoped row lookup; nonexistent ids return not found (the toolsets surface already behaves this way).
  2. A nonstandard grant shape — mcp:read/mcp:write with resource_id set 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_id dimension) 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:write grant 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

  • Adds a grantResourceID helper to derive the grant resource id consistently across endpoints.
  • Get, tool-filter reads, update, and delete now check mcp:read/mcp:write against the server's grant id; the project id remains a selector dimension.
  • Update and delete reject unauthorized callers before the transaction takes FOR UPDATE locks by pre-checking against a non-locking read, then re-checking against the locked row.
  • Update authorizes against the row's existing backing, not the backing the payload may switch to.
  • Listing filters per-server via authz.Filter, so callers see only servers they hold mcp:read for; no matching grants returns an empty list instead of 403.
  • Create stays project-keyed: a per-server grant cannot create new servers.
  • Get/update/delete on an existing server without a matching grant now return 403 after the project-scoped row lookup; nonexistent ids return 404. A nonstandard grant shape with resource_id set to a project uuid no longer matches, but the canonical project-wide form (resource_id='*' + project_id dimension) is unaffected.
  • Adds rbac_scoped_test.go covering 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.

Review in cubic

@daviddanialy
daviddanialy requested a review from a team as a code owner August 31, 2026 18:08
@daviddanialy daviddanialy added bug Something isn't working go Pull requests that update go code labels Aug 31, 2026
@changeset-bot

changeset-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 251cf4e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
server Patch

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

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Comment thread server/internal/mcpservers/rbac_scoped_test.go
Comment thread server/internal/mcpservers/impl.go
Comment thread server/internal/mcpservers/rbac_scoped_test.go
@blacksmith-sh

This comment has been minimized.

@daviddanialy
daviddanialy added this pull request to the merge queue Sep 1, 2026
Merged via the queue into main with commit 858f8e7 Sep 1, 2026
85 of 88 checks passed
@daviddanialy
daviddanialy deleted the mcp-server-scoped-management-authz branch September 1, 2026 15:57
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

bug Something isn't working go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants