mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
feat(key_management): let any authenticated user resolve a raw key via /key/info
Possession of the raw sk- key already lets the holder call /key/info with the key itself as the bearer token, so resolving raw key -> key info for any authenticated caller discloses nothing new. Lookups by hashed token remain restricted to admins, the key's owner, and teammates
This commit is contained in:
parent
0659738b3e
commit
d0c1d2be8a
2 changed files with 153 additions and 0 deletions
|
|
@ -6351,6 +6351,12 @@ async def _can_user_query_key_info(
|
|||
) -> bool:
|
||||
"""
|
||||
Helper to check if the user has access to the key's info
|
||||
|
||||
Any authenticated caller who presents the raw key value (i.e. a preimage of the
|
||||
stored token hash) is allowed: possession of the raw key already grants the
|
||||
ability to call /key/info with that key as the bearer token, so resolving
|
||||
raw key -> key info discloses nothing new. Lookups by hashed token remain
|
||||
restricted to admins, the key's owner, and the key's teammates.
|
||||
"""
|
||||
if (
|
||||
(
|
||||
|
|
@ -6359,6 +6365,7 @@ async def _can_user_query_key_info(
|
|||
)
|
||||
or user_api_key_dict.api_key == key
|
||||
or key_info.user_id == user_api_key_dict.user_id
|
||||
or (key is not None and hash_token(token=key) == key_info.token)
|
||||
or await TeamMemberPermissionChecks.user_belongs_to_keys_team(
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
existing_key_row=key_info,
|
||||
|
|
|
|||
|
|
@ -15323,3 +15323,149 @@ async def test_rotate_master_key_rotates_sso_identity_assertions(
|
|||
prisma_client=mock_prisma_client,
|
||||
new_master_key="sk-new-master-key",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_can_user_query_key_info_raw_key_possession_allows_any_user():
|
||||
"""
|
||||
Any authenticated user who presents the raw sk- key value can query that
|
||||
key's info: possessing the raw key already lets them call /key/info with
|
||||
the key itself as the bearer token, so this discloses nothing new.
|
||||
"""
|
||||
from litellm.proxy._types import hash_token
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
||||
_can_user_query_key_info,
|
||||
)
|
||||
|
||||
raw_key = "sk-raw-key-owned-by-someone-else"
|
||||
key_info = LiteLLM_VerificationToken(
|
||||
token=hash_token(raw_key),
|
||||
user_id="key-owner",
|
||||
team_id=None,
|
||||
key_alias="prod-batch-alias",
|
||||
)
|
||||
caller = UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.INTERNAL_USER,
|
||||
user_id="unrelated-user",
|
||||
api_key="hashed-caller-token",
|
||||
)
|
||||
|
||||
assert (
|
||||
await _can_user_query_key_info(
|
||||
user_api_key_dict=caller,
|
||||
key=raw_key,
|
||||
key_info=key_info,
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_can_user_query_key_info_hashed_token_still_forbidden():
|
||||
"""
|
||||
Querying by hashed token (e.g. copied from spend logs) must stay
|
||||
restricted to admins, the key's owner, and teammates.
|
||||
"""
|
||||
from litellm.proxy._types import hash_token
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
||||
_can_user_query_key_info,
|
||||
)
|
||||
|
||||
raw_key = "sk-raw-key-owned-by-someone-else"
|
||||
key_info = LiteLLM_VerificationToken(
|
||||
token=hash_token(raw_key),
|
||||
user_id="key-owner",
|
||||
team_id=None,
|
||||
key_alias="prod-batch-alias",
|
||||
)
|
||||
caller = UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.INTERNAL_USER,
|
||||
user_id="unrelated-user",
|
||||
api_key="hashed-caller-token",
|
||||
)
|
||||
|
||||
assert (
|
||||
await _can_user_query_key_info(
|
||||
user_api_key_dict=caller,
|
||||
key=hash_token(raw_key),
|
||||
key_info=key_info,
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_info_key_fn_resolves_alias_from_raw_key_for_any_user(monkeypatch):
|
||||
"""
|
||||
End-to-end through /key/info: a non-admin user unrelated to the key can
|
||||
resolve raw sk- key -> key_alias.
|
||||
"""
|
||||
from litellm.proxy._types import hash_token
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import info_key_fn
|
||||
|
||||
raw_key = "sk-raw-key-owned-by-someone-else"
|
||||
key_row = LiteLLM_VerificationToken(
|
||||
token=hash_token(raw_key),
|
||||
user_id="key-owner",
|
||||
team_id=None,
|
||||
key_alias="prod-batch-alias",
|
||||
)
|
||||
|
||||
mock_prisma_client = AsyncMock()
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.user_api_key_cache", AsyncMock())
|
||||
mock_prisma_client.db.litellm_verificationtoken.find_unique = AsyncMock(
|
||||
return_value=key_row
|
||||
)
|
||||
|
||||
caller = UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.INTERNAL_USER,
|
||||
user_id="unrelated-user",
|
||||
api_key="hashed-caller-token",
|
||||
)
|
||||
|
||||
result = await info_key_fn(key=raw_key, user_api_key_dict=caller)
|
||||
|
||||
assert result["info"]["key_alias"] == "prod-batch-alias"
|
||||
assert "token" not in result["info"]
|
||||
|
||||
find_unique_kwargs = (
|
||||
mock_prisma_client.db.litellm_verificationtoken.find_unique.call_args.kwargs
|
||||
)
|
||||
assert find_unique_kwargs["where"] == {"token": hash_token(raw_key)}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_info_key_fn_hashed_lookup_still_403_for_unrelated_user(monkeypatch):
|
||||
"""
|
||||
End-to-end through /key/info: the same unrelated user querying by hashed
|
||||
token still gets a 403.
|
||||
"""
|
||||
from litellm.proxy._types import hash_token
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import info_key_fn
|
||||
|
||||
raw_key = "sk-raw-key-owned-by-someone-else"
|
||||
key_row = LiteLLM_VerificationToken(
|
||||
token=hash_token(raw_key),
|
||||
user_id="key-owner",
|
||||
team_id=None,
|
||||
key_alias="prod-batch-alias",
|
||||
)
|
||||
|
||||
mock_prisma_client = AsyncMock()
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.user_api_key_cache", AsyncMock())
|
||||
mock_prisma_client.db.litellm_verificationtoken.find_unique = AsyncMock(
|
||||
return_value=key_row
|
||||
)
|
||||
|
||||
caller = UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.INTERNAL_USER,
|
||||
user_id="unrelated-user",
|
||||
api_key="hashed-caller-token",
|
||||
)
|
||||
|
||||
with pytest.raises(ProxyException) as exc_info:
|
||||
await info_key_fn(key=hash_token(raw_key), user_api_key_dict=caller)
|
||||
|
||||
assert int(exc_info.value.code) == 403
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue