diff --git a/litellm/proxy/management_endpoints/scim/scim_v2.py b/litellm/proxy/management_endpoints/scim/scim_v2.py index 3f7abdd544a..e3b3225084b 100644 --- a/litellm/proxy/management_endpoints/scim/scim_v2.py +++ b/litellm/proxy/management_endpoints/scim/scim_v2.py @@ -506,8 +506,9 @@ async def _refresh_scim_updated_team_cache( Best-effort: the DB write has already committed, so a cache backend failure must not fail the SCIM response and trigger an IdP retry of a successful write. - On failure both the current and previous alias keys are evicted alongside the - id key, so alias-based auth cannot keep serving the pre-write organization policy. + On failure the current keys are evicted so auth re-reads the DB, and after a + rename the previous alias key is always evicted, whether or not the refresh + succeeded, so alias-based auth cannot keep serving the pre-write organization policy. """ from litellm.proxy.proxy_server import proxy_logging_obj, user_api_key_cache @@ -531,13 +532,13 @@ async def _refresh_scim_updated_team_cache( user_api_key_cache=user_api_key_cache, proxy_logging_obj=proxy_logging_obj, ) - if previous_team_alias is not None and previous_team_alias != updated_team.team_alias: - await delete_cache_team_object( - team_id=updated_team.team_id, - team_alias=previous_team_alias, - user_api_key_cache=user_api_key_cache, - proxy_logging_obj=proxy_logging_obj, - ) + if previous_team_alias is not None and previous_team_alias != updated_team.team_alias: + await delete_cache_team_object( + team_id=updated_team.team_id, + team_alias=previous_team_alias, + user_api_key_cache=user_api_key_cache, + proxy_logging_obj=proxy_logging_obj, + ) def _team_with_final_roster(existing_team: LiteLLM_TeamTable, final_member_ids: Iterable[str]) -> LiteLLM_TeamTable: diff --git a/tests/test_litellm/proxy/management_endpoints/scim/test_scim_v2_endpoints.py b/tests/test_litellm/proxy/management_endpoints/scim/test_scim_v2_endpoints.py index afd13948e08..a6c579a2f9c 100644 --- a/tests/test_litellm/proxy/management_endpoints/scim/test_scim_v2_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/scim/test_scim_v2_endpoints.py @@ -5996,6 +5996,39 @@ async def test_refresh_cache_failure_evicts_stale_team_entry(mocker: MockerFixtu assert evicted == [("team-1", "Engineering-Platform"), ("team-1", "Sales")] +@pytest.mark.asyncio +async def test_successful_refresh_after_rename_evicts_previous_alias(mocker: MockerFixture): # test-quality-ok: the observable is the previous-alias eviction the helper must trigger; the eviction's cache delete is covered by auth_checks tests + """A successful refresh only writes the new keys, so after a rename the + previous alias cache key must still be evicted or alias-based auth keeps + serving the pre-rename organization policy until TTL expiry.""" + from litellm.proxy._types import LiteLLM_TeamTable, Member + from litellm.proxy.management_endpoints.scim.scim_v2 import ( + _refresh_scim_updated_team_cache, + ) + + updated_team = LiteLLM_TeamTable( + team_id="team-1", + team_alias="Engineering-Platform", + organization_id="org-eng", + members=["user1"], + members_with_roles=[Member(user_id="user1", role="user")], + metadata={}, + ) + mocker.patch( # test-quality-ok: pins the refresh success path under test + "litellm.proxy.management_endpoints.scim.scim_v2._refresh_cached_team", + AsyncMock(), + ) + evict_mock = mocker.patch( # test-quality-ok: asserts the eviction the helper must trigger + "litellm.proxy.management_endpoints.scim.scim_v2.delete_cache_team_object", + AsyncMock(), + ) + + await _refresh_scim_updated_team_cache(updated_team, "Sales") + + evicted = [(call.kwargs["team_id"], call.kwargs["team_alias"]) for call in evict_mock.await_args_list] + assert evicted == [("team-1", "Sales")] + + @pytest.mark.asyncio async def test_org_validation_sees_final_roster_not_removed_members(mocker: MockerFixture): # test-quality-ok: the observable is the roster snapshot the destination-org validator receives; the validator's auto-add is covered by team_endpoints tests """A PUT that moves a team into a mapped organization while removing a member