mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
fix(proxy): evict the cached user row when SCIM or /user/delete removes a user
This commit is contained in:
parent
662e5b6e32
commit
d8ce49de06
4 changed files with 83 additions and 0 deletions
|
|
@ -2518,6 +2518,7 @@ async def delete_user(
|
|||
|
||||
## DELETE USERS
|
||||
deleted_users: Final = await _user_table(prisma_client).delete_many(where={"user_id": {"in": data.user_ids}})
|
||||
await evict_and_broadcast(cache_keys=tuple(data.user_ids), user_api_key_cache=user_api_key_cache)
|
||||
|
||||
return deleted_users
|
||||
|
||||
|
|
|
|||
|
|
@ -1871,6 +1871,10 @@ async def delete_user(
|
|||
# Delete user
|
||||
await _table(UserRepository(prisma_client)).delete(where={"user_id": user_id})
|
||||
|
||||
from litellm.proxy.proxy_server import user_api_key_cache
|
||||
|
||||
await evict_and_broadcast(cache_keys=(user_id,), user_api_key_cache=user_api_key_cache)
|
||||
|
||||
return Response(status_code=204)
|
||||
except Exception as e:
|
||||
raise handle_exception_on_proxy(e)
|
||||
|
|
|
|||
|
|
@ -601,3 +601,42 @@ async def test_scim_status_write_refreshes_user_cache(
|
|||
else:
|
||||
assert cached is None
|
||||
broadcast.assert_awaited_once_with(cache_key=user_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("failure", [None, "delete"])
|
||||
async def test_scim_delete_user_evicts_cached_user_row(failure: str | None) -> None:
|
||||
from typing import Final
|
||||
|
||||
from litellm.proxy._types import ProxyException
|
||||
from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
|
||||
|
||||
user_id: Final = "scim-deleted-user"
|
||||
saved: Final = LiteLLM_UserTable(user_id=user_id, user_email="x@example.com", teams=[], metadata={})
|
||||
client, db = _build_prisma_with_keys([], mock_user=saved.model_copy(deep=True))
|
||||
if failure == "delete":
|
||||
db.litellm_usertable.delete.side_effect = RuntimeError("user delete failed")
|
||||
cache: Final = UserApiKeyCache()
|
||||
await cache.async_set_cache(key=user_id, value=saved, model_type=LiteLLM_UserTable)
|
||||
with (
|
||||
patch("litellm.proxy.proxy_server.prisma_client", client), # test-quality-ok: substitute the database dependency
|
||||
patch("litellm.proxy.proxy_server.user_api_key_cache", cache), # test-quality-ok: exercise a real isolated cache
|
||||
patch("litellm.proxy.proxy_server.proxy_logging_obj", MagicMock()), # test-quality-ok: isolate the logging dependency
|
||||
patch( # test-quality-ok: observe the Redis publication boundary
|
||||
"litellm.proxy.common_utils.auth_cache_invalidation_pubsub.publish_auth_cache_invalidation",
|
||||
new_callable=AsyncMock,
|
||||
) as broadcast,
|
||||
):
|
||||
if failure == "delete":
|
||||
with pytest.raises(ProxyException, match="user delete failed"):
|
||||
await delete_user(user_id=user_id)
|
||||
else:
|
||||
response: Final = await delete_user(user_id=user_id)
|
||||
assert response.status_code == 204
|
||||
cached: Final = await cache.async_get_cache(key=user_id, model_type=LiteLLM_UserTable)
|
||||
if failure == "delete":
|
||||
assert cached == saved
|
||||
broadcast.assert_not_awaited()
|
||||
else:
|
||||
assert cached is None
|
||||
broadcast.assert_awaited_once_with(cache_key=user_id)
|
||||
|
|
|
|||
|
|
@ -4691,3 +4691,42 @@ async def test_user_update_hashes_and_persists_strong_password(_admin_prisma, mo
|
|||
written_data = mock_prisma_client.update_data.call_args.kwargs["data"]
|
||||
assert written_data.get("password") is not None
|
||||
assert written_data["password"] != strong_password
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_user_evicts_cached_user_rows(mocker: MockerFixture) -> None:
|
||||
from litellm.proxy._types import DeleteUserRequest, LiteLLM_UserTable
|
||||
from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
|
||||
from litellm.proxy.management_endpoints.internal_user_endpoints import delete_user
|
||||
|
||||
deleted: Final = LiteLLM_UserTable(user_id="user-gone", user_email="gone@example.test", teams=[])
|
||||
survivor: Final = LiteLLM_UserTable(user_id="user-stays", user_email="stays@example.test", teams=[])
|
||||
prisma_client: Final = mocker.MagicMock()
|
||||
prisma_client.db.litellm_usertable.find_unique = mocker.AsyncMock(return_value=deleted)
|
||||
prisma_client.db.litellm_teamtable.find_many = mocker.AsyncMock(return_value=[])
|
||||
prisma_client.db.litellm_jwtkeymapping.find_many = mocker.AsyncMock(return_value=[])
|
||||
prisma_client.db.litellm_verificationtoken.find_many = mocker.AsyncMock(return_value=[])
|
||||
prisma_client.db.litellm_verificationtoken.delete_many = mocker.AsyncMock(return_value=0)
|
||||
prisma_client.db.litellm_invitationlink.delete_many = mocker.AsyncMock(return_value=0)
|
||||
prisma_client.db.litellm_organizationmembership.delete_many = mocker.AsyncMock(return_value=0)
|
||||
prisma_client.db.litellm_teammembership.delete_many = mocker.AsyncMock(return_value=0)
|
||||
prisma_client.db.litellm_usertable.delete_many = mocker.AsyncMock(return_value=1)
|
||||
mocker.patch("litellm.proxy.proxy_server.prisma_client", prisma_client) # test-quality-ok: substitute the database dependency
|
||||
cache: Final = UserApiKeyCache()
|
||||
for row in (deleted, survivor):
|
||||
await cache.async_set_cache(key=row.user_id, value=row, model_type=LiteLLM_UserTable)
|
||||
mocker.patch("litellm.proxy.proxy_server.user_api_key_cache", cache) # test-quality-ok: exercise a real isolated cache
|
||||
mocker.patch("litellm.proxy.proxy_server.proxy_logging_obj", None) # test-quality-ok: delete_user reads it off proxy_server at call time
|
||||
broadcast: Final = mocker.patch( # test-quality-ok: observe the Redis publication boundary
|
||||
"litellm.proxy.common_utils.auth_cache_invalidation_pubsub.publish_auth_cache_invalidation",
|
||||
new_callable=mocker.AsyncMock,
|
||||
)
|
||||
|
||||
await delete_user(
|
||||
data=DeleteUserRequest(user_ids=[deleted.user_id]),
|
||||
user_api_key_dict=UserAPIKeyAuth(user_id="proxy-admin", user_role=LitellmUserRoles.PROXY_ADMIN),
|
||||
)
|
||||
|
||||
assert await cache.async_get_cache(key=deleted.user_id, model_type=LiteLLM_UserTable) is None
|
||||
assert await cache.async_get_cache(key=survivor.user_id, model_type=LiteLLM_UserTable) == survivor
|
||||
broadcast.assert_awaited_once_with(cache_key=deleted.user_id)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue