fix(rbac): enforce team-membership check on plugin binding DELETE endpoints#4405
Open
fix(rbac): enforce team-membership check on plugin binding DELETE endpoints#4405
Conversation
…points Signed-off-by: Shoumi <shoumimukherjee@gmail.com>
…endpoints Signed-off-by: Shoumi <shoumimukherjee@gmail.com>
54c2454 to
98fb711
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug-fix PR
📌 Summary
DELETE /v1/tools/plugin_bindings/{id}andDELETE /v1/tools/plugin_bindings?binding_reference_id=...relied solely on thetools.manage_pluginsRBAC permission and did not verify that the caller's team membership overlapped with the binding'steam_id. A non-admin holdingtools.manage_pluginson any team could delete a binding belonging to a different team by guessing or observing its UUID or external reference ID. The paired upsert endpoint already enforced this check; the delete endpoints did not.🔗 Related Issue
Closes: #4311
🔁 Reproduction Steps
See #4311.
tools.manage_pluginson team-A.binding_reference_id) of a binding that belongs to team-B.DELETE /v1/tools/plugin_bindings/<team-B-binding-uuid>→ binding is deleted despite the caller having no membership in team-B.🐞 Root Cause
The router handlers (
delete_tool_plugin_binding,delete_tool_plugin_bindings_by_reference) forwarded the delete straight to the service layer without extracting the caller's team membership or passing it down. The service methods contained no team filter — they matched only onid/binding_reference_id, making any binding reachable to anyone with the permission.Affected code:
mcpgateway/routers/tool_plugin_bindings.pylines 189–286mcpgateway/services/tool_plugin_binding_service.pydelete_binding/delete_bindings_by_reference💡 Fix Description
Two orthogonal layers of defence, mirroring the existing upsert pattern:
Router-side guard — each delete handler now extracts
is_adminandteamsfromcurrent_user_ctxand computesallowed_teams = None(admin bypass) orset(user_teams)(non-admin). This is passed to the service.Service-side scoping:
delete_binding— checksbinding.team_id not in allowed_teamsbefore deleting; raisesToolPluginBindingForbiddenError(mapped to HTTP 403 by the router).delete_bindings_by_reference— appliesteam_id.in_(allowed_teams)as a SQL filter so foreign-team rows are never fetched or deleted (silent skip, consistent with bulk-operation semantics).A new
ToolPluginBindingForbiddenErrorexception class carries the denial through the service→router boundary cleanly.🧪 Verification
make lintmake testmake coverage.venv/bin/pytest tests/unit/mcpgateway/routers/test_tool_plugin_bindings.py tests/unit/mcpgateway/services/test_tool_plugin_binding_service.py -vtest_delete_non_admin_foreign_team_raises_403test_delete_by_reference_non_admin_scoped_to_own_teams📐 MCP Compliance (if relevant)
✅ Checklist
make black isort pre-commit)