Skip to content

fix(rbac): derive create ownership from the authenticated caller - #7296

Open
msureshkumar88 wants to merge 11 commits into
mainfrom
fix/create-ownership-from-auth-context
Open

msureshkumar88 wants to merge 11 commits into
mainfrom
fix/create-ownership-from-auth-context

Conversation

@msureshkumar88

@msureshkumar88 msureshkumar88 commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

🐛 Bug-fix PR

📌 Summary

Create routes and admin tool paths now derive record ownership from the authenticated caller. They no longer accept ownership fields from the request payload without authorization.

The update routes, the gRPC create route and gateway discovery follow the same rule. A caller cannot learn about records in a team the caller cannot use. Records that gateway discovery creates keep the gateway's team and owner.

🔁 Reproduction Steps

Found in an internal review. No public issue is linked.

🐞 Root Cause

  • The create routes applied the team checks to the top-level team_id only.
  • The register_* services preferred the payload team_id and owner_email over the values the route resolved.
  • Admin tool creation and admin bulk tool import depended on that payload fallback.
  • The update services checked name and URI conflicts before the target team. A 409 could confirm that a name or URI exists in another team.
  • The gRPC create route stored the payload team_id without a membership check.
  • Gateway discovery created resources and prompts without the gateway team_id and owner_email.

💡 Fix Description

  • Add _resolve_create_scope() in mcpgateway/main.py. POST /servers, /resources, /prompts and /tools use it.
    • It merges the nested team_id/visibility with the top-level fields.
    • It then applies the public-only check, the token-team check and the token_teams membership check.
    • It also checks the create permission in the resolved team with check_permission_inline().
  • register_server, register_resource, register_prompt, register_tool and the bulk variants ignore the payload team_id/owner_email. The owner is the owner_email argument, or else created_by.
  • Admin add-tool passes the verified team and the caller as owner.
  • Admin bulk tool import assigns items to the token team or to the caller's personal team, the same as /import. Each item runs the public-visibility check.
  • update_server, update_resource and update_prompt check ownership, then the target team, then the name or URI conflict.
  • POST /admin/grpc returns 403 when the payload team_id is not a team of the caller.
  • Discovered resources and prompts take team_id and owner_email from the gateway.

Behavior changes

  • A nested team_id that differs from the top-level team_id returns 403.
  • A nested team_id outside the token's teams returns 403. A nested team_id where the caller lacks the create permission also returns 403.
  • A public-only token with nested visibility of team or private returns 403.
  • The payload owner_email is ignored. The owner is the caller.
  • POST /resources honors the nested visibility. The OpenAPI default of the top-level visibility for resources and prompts is now unset. The effective default stays public.
  • For prompts, the top-level visibility wins over the nested value, the same as for the other kinds.
  • Admin bulk tool import ignores the item team_id/owner_email.
  • An update that names a team the caller cannot use returns the team validation error (400). It returned 409 when the name or URI also existed in that team.
  • An update by a non-owner returns the permission error. It returned 409 when the name or URI conflicted.
  • POST /admin/grpc with a team_id outside the caller's teams returns 403.
  • New resources and prompts from gateway discovery carry the gateway team and owner. Existing rows do not change, and no migration is added.

Requests inside the caller's own team keep their status codes and response bodies.

📏 Reviewability

  • This PR has one clear purpose
  • The linked issue is not labeled triage (no public issue)
  • Unrelated bugs or improvements are tracked in separate issues/PRs
  • Tests are included with the code they validate
  • If AI-assisted, I understand and can explain the generated changes

🧪 Verification

Check Command Status
Lint make ruff interrogate pylint ✅ pylint 10.00/10. One existing error in rate_limit_middleware.py, not changed here.
Unit tests make test ✅ Two test_config.py CSRF tests fail under xdist and pass in isolation. This PR does not change config.
Coverage make coverage + diff-cover --compare-branch=origin/main ✅ 98% total, 100% of changed lines
Live gateway tests/live_gateway/e2e/test_create_ownership_scope.py ✅ 8/8
Live gateway tests/live_gateway/e2e/test_server_create_nested_team_override.py ✅ 25/25
Unit tests (changed services, test_admin.py, test_main.py) and make ruff pytest -n 4 on those files ✅ After the last commit. make test, make coverage and the lint run above predate it.
Docker stack E2E make docker-prod-rust testing-up + make test-e2e Not run locally

📐 MCP Compliance (if relevant)

  • Matches current MCP spec
  • No breaking change to MCP clients

✅ Checklist

  • Code formatted
  • No secrets/credentials committed

@gandhipratik203

Copy link
Copy Markdown
Collaborator

mcpgateway/admin.py:15258: admin_import_tools lets a public-only token create team or private tools in the caller's personal team. /tools returns 403 for this. Apply the same deny rule here and add a deny-path test.

mcpgateway/main.py:1031: _CREATE_TEAM_DENIED also fires on RBAC denial, so its "API token" wording is wrong for session and global-role users. Use a separate 403 message for that case, for example "Missing in the requested team".

@msureshkumar88
msureshkumar88 force-pushed the fix/create-ownership-from-auth-context branch from 2e6476d to 9ed5661 Compare October 21, 2026 15:20
@msureshkumar88

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Both points are fixed in fbc4e60.

admin_import_tools and public-only tokens. The import route already rejected a public-only token. The admin guard returns 403 "Admin privileges required" before the handler runs. The handler had no deny rule of its own, so the guard was the only protection. The handler now applies the POST /tools rule:

  • Items with visibility team or private return a per-item error.
  • Public items are stored without a team.
  • A unit test covers the deny path.

_CREATE_TEAM_DENIED wording. RBAC denials now return "Missing in the requested team." The "API token" wording stays for token-scope mismatches. The status stays 403. The unit test asserts the new message.

@msureshkumar88
msureshkumar88 force-pushed the fix/create-ownership-from-auth-context branch from fbc4e60 to 8c9a5d0 Compare October 22, 2026 10:23

@gandhipratik203 gandhipratik203 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM!

Suresh Kumar Moharajan added 8 commits October 23, 2026 11:31
Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
The register methods for servers, resources, prompts and tools read
team_id and owner_email from the create payload before the function
parameters. Use the parameters only, including the duplicate-name checks
and the bulk variants. register_prompt now prefers the visibility
parameter over the payload value.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Add live-gateway tests for the server, resource, prompt and tool create routes. The tests check that a caller creates only in a team its token is scoped to, that the stored owner is the caller, and that a public-only token never assigns a team.

Let isolated_gateway accept environment overrides through indirect parametrization. The new module turns off personal teams with it.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
…review

Update paths check team access before name or URI conflicts. A caller can
no longer learn that a name or URI exists in a team the caller cannot use.

- update_server, update_resource and update_prompt check ownership, then
  the target team, then name or URI conflicts.
- The gRPC create route returns 403 when the payload team_id is not a
  team of the caller.
- Discovered resources and prompts inherit team_id and owner_email from
  the gateway.
- Add the nested-override live test, a session-token case, and unit
  tests for each change. The sibling-route test accepts a 403 refusal.

Same-team requests keep their status codes and response bodies.
Cross-team updates that returned 409 now return the team validation error.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
- Bulk tool import denies team and private tools for public-only tokens
  and stores public tools without a team, the same as POST /tools.
- _resolve_create_scope() returns "Missing <permission> in the requested
  team." when RBAC denies the create. The API-token wording stays for
  token-scope mismatches.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
@msureshkumar88
msureshkumar88 force-pushed the fix/create-ownership-from-auth-context branch from df1d5ca to a6b3c7d Compare October 23, 2026 10:32
Suresh Kumar Moharajan added 3 commits October 23, 2026 11:56
Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
_is_jwt_token() rejects tokens without three base64url parts, so the
placeholder token made these tests fail on main as well.

Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>
Signed-off-by: Suresh Kumar Moharajan <suresh.kumar.m@ibm.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants