fix(mcp): resolve key.access_group_ids → MCP servers (ungated)#29195
Conversation
A teamless virtual key whose unified access_group_ids grant an MCP-granting access group now sees and can call that server instead of getting an empty list / 403. The key path previously read only the legacy object_permission; this folds key.access_group_ids into the key's base scope (ungated), mirroring can_key_call_model's fallback. The gated assigned_*-checked override is unchanged.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR adds the missing ungated resolution of
Confidence Score: 5/5The change is a read-time permission expansion scoped to a single helper function, with no writes, schema changes, or modifications to existing auth paths. The fix is a focused one-file change that makes the key's access_group_ids → MCP resolution consistent with already-approved patterns used by models and agents. The previously raised concerns about missing expand_permission_list at the early-return and full-union sites have been addressed at the source. All four new tests are properly mocked, no existing tests are weakened, and the gated extras path is unchanged. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py | Adds ungated key.access_group_ids → MCP server resolution to _get_allowed_mcp_servers_for_key; previously flagged expand_permission_list gap is now addressed at the source; refactors import hoisting and the prisma_client is not None guard cleanly. |
| tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py | Adds 4 new unit/integration tests (ungated resolve, union with object_permission, empty short-circuit, end-to-end repro); all use mocks — no real network calls; existing tests are untouched. |
Reviews (2): Last reviewed commit: "fix(mcp): expand name/alias entries in k..." | Re-trigger Greptile
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
access_mcp_server_ids may hold server names/aliases, not just ids. The new ungated key path now runs them through expand_permission_list at the source, so the early-return and union branches both surface resolved ids — matching the legacy object_permission path and the gated extras path.
|
@greptileai re-review Addressed the alias-expansion feedback: |
909a5f5
into
litellm_internal_staging
The previous commit moved key.access_group_ids resolution out of the intersected key ceiling (_get_allowed_mcp_servers_for_key) and into the ungated additive grant path (_get_key_access_group_mcp_server_extras), unioned on top of the team ceiling. Five tests from #28890/#29195 still asserted the old gated / in-key-scope contract and failed: - _get_allowed_mcp_servers_for_key now returns the object_permission ceiling only and never resolves access_group_ids; two tests now assert the group resolver is not called from that path (with and without an object_permission present). - The extras path is ungated, so a group whose assigned_team_ids / assigned_key_ids exclude the caller still contributes its servers. - The end-to-end test asserts the grant surfaces via the extras path rather than the base key path. - Dropped test_key_access_group_ids_empty_returns_no_extras; the empty case is already covered by the extras family's no-groups test.
…gnment (#29313) * fix(mcp): make key.access_group_ids grants additive over team ceiling A key whose unified access_group_ids grant a private MCP server was having that grant intersected against its team's MCP ceiling, so a key in a team scoped to other servers (or with no own scope) lost the granted server entirely. Resolve access_group_ids once as ungated additive grants and union them on top of the key/team ceiling instead of folding them into the key scope that gets intersected. * test(mcp): align key access-group tests with additive-grant model The previous commit moved key.access_group_ids resolution out of the intersected key ceiling (_get_allowed_mcp_servers_for_key) and into the ungated additive grant path (_get_key_access_group_mcp_server_extras), unioned on top of the team ceiling. Five tests from #28890/#29195 still asserted the old gated / in-key-scope contract and failed: - _get_allowed_mcp_servers_for_key now returns the object_permission ceiling only and never resolves access_group_ids; two tests now assert the group resolver is not called from that path (with and without an object_permission present). - The extras path is ungated, so a group whose assigned_team_ids / assigned_key_ids exclude the caller still contributes its servers. - The end-to-end test asserts the grant surfaces via the extras path rather than the base key path. - Dropped test_key_access_group_ids_empty_returns_no_extras; the empty case is already covered by the extras family's no-groups test. * feat(auth): gate member access-group assignment on keys behind opt-in Non-admin team members could attach access_group_ids to keys they create or update, letting them self-grant resources (MCP servers/models) the team admin never intended. Add an opt-in KEY_ACCESS_GROUP_ASSIGNMENT team-member permission (default-deny) enforced at /key/generate and /key/update; proxy and team admins bypass. Surfaces automatically as a checkbox in the team Member Permissions UI. * fix(auth): gate access-group assignment on /key/regenerate too RegenerateKeyRequest inherits access_group_ids and prepare_key_update_data persists it, so a non-admin key owner could self-grant access groups by regenerating. Apply the same opt-in member gate using the existing key's team. * test(auth): cover member access-group gate and additive MCP grants Add unit tests for enforce_member_can_assign_access_groups (deny without opt-in, allow with opt-in, and proxy-admin / team-admin / non-team-key bypasses) and for _get_key_access_group_mcp_server_extras (no-auth and no-resolved-servers return empty, resolved ids are expanded, errors degrade to no grants).
…AI#29195) * fix(mcp): resolve key.access_group_ids → MCP servers (ungated) A teamless virtual key whose unified access_group_ids grant an MCP-granting access group now sees and can call that server instead of getting an empty list / 403. The key path previously read only the legacy object_permission; this folds key.access_group_ids into the key's base scope (ungated), mirroring can_key_call_model's fallback. The gated assigned_*-checked override is unchanged. * fix(mcp): expand name/alias entries in key access-group servers access_mcp_server_ids may hold server names/aliases, not just ids. The new ungated key path now runs them through expand_permission_list at the source, so the early-return and union branches both surface resolved ids — matching the legacy object_permission path and the gated extras path.
…gnment (BerriAI#29313) * fix(mcp): make key.access_group_ids grants additive over team ceiling A key whose unified access_group_ids grant a private MCP server was having that grant intersected against its team's MCP ceiling, so a key in a team scoped to other servers (or with no own scope) lost the granted server entirely. Resolve access_group_ids once as ungated additive grants and union them on top of the key/team ceiling instead of folding them into the key scope that gets intersected. * test(mcp): align key access-group tests with additive-grant model The previous commit moved key.access_group_ids resolution out of the intersected key ceiling (_get_allowed_mcp_servers_for_key) and into the ungated additive grant path (_get_key_access_group_mcp_server_extras), unioned on top of the team ceiling. Five tests from BerriAI#28890/BerriAI#29195 still asserted the old gated / in-key-scope contract and failed: - _get_allowed_mcp_servers_for_key now returns the object_permission ceiling only and never resolves access_group_ids; two tests now assert the group resolver is not called from that path (with and without an object_permission present). - The extras path is ungated, so a group whose assigned_team_ids / assigned_key_ids exclude the caller still contributes its servers. - The end-to-end test asserts the grant surfaces via the extras path rather than the base key path. - Dropped test_key_access_group_ids_empty_returns_no_extras; the empty case is already covered by the extras family's no-groups test. * feat(auth): gate member access-group assignment on keys behind opt-in Non-admin team members could attach access_group_ids to keys they create or update, letting them self-grant resources (MCP servers/models) the team admin never intended. Add an opt-in KEY_ACCESS_GROUP_ASSIGNMENT team-member permission (default-deny) enforced at /key/generate and /key/update; proxy and team admins bypass. Surfaces automatically as a checkbox in the team Member Permissions UI. * fix(auth): gate access-group assignment on /key/regenerate too RegenerateKeyRequest inherits access_group_ids and prepare_key_update_data persists it, so a non-admin key owner could self-grant access groups by regenerating. Apply the same opt-in member gate using the existing key's team. * test(auth): cover member access-group gate and additive MCP grants Add unit tests for enforce_member_can_assign_access_groups (deny without opt-in, allow with opt-in, and proxy-admin / team-admin / non-team-key bypasses) and for _get_key_access_group_mcp_server_extras (no-auth and no-resolved-servers return empty, resolved ids are expanded, errors degrade to no grants).
Summary
A teamless virtual key whose unified
access_group_idsreference an MCP-granting access group now discovers and can call that server, instead of getting an emptyGET /v1/mcp/serverlist and403 access_deniedon tool routes. Previously the key path read only the legacyobject_permission; the only place that resolvedkey.access_group_idsfor MCP was the gated extras path, which requires the group to list the key inassigned_key_ids/assigned_team_ids.This adds the missing ungated base resolution:
_get_allowed_mcp_servers_for_keynow foldskey.access_group_ids → access_group.access_mcp_server_idsinto the key's own scope, mirroringcan_key_call_model's ungated fallback on the model side. The gated, team-ceiling-busting override (_get_key_access_group_mcp_server_extras) is unchanged, so the anti-escalation guard still applies where it should. Net semantics now match models and the team path.screenshots
before - wasnt able to use it
after - was able to use it
Test plan
tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py::test_key_access_group_ids_resolves_mcp_servers_ungated— teamless key, group grants server with emptyassigned_*→ server returned...::test_key_access_group_ids_union_with_object_permission— legacyobject_permission∪ access-group servers...::test_key_access_group_ids_empty_returns_no_extras— emptyaccess_group_idsshort-circuits to[]...::test_get_allowed_mcp_servers_key_access_group_base_end_to_end— gated extras still[], but base path grants the server131 passed(uv run pytest .../test_user_api_key_auth_mcp.py)GET /v1/mcp/serverreturns[]+tools/list403before, returns the server +200afterResolves LIT-3399