mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-13 03:59:12 +00:00
* test: migrate tests/proxy_behavior into integration and unit tiers (partial) * test(integration): run settings-dependent management tests on an owned proxy * test(integration): assert stored values in the rewritten management rows * test(integration): pin sibling team allocations against the org guaranteed throughput cap * test(integration): keep Prisma clients out of the shared integration conftest import path --------- Co-authored-by: yuneng <yuneng@berri.ai>
204 lines
7 KiB
Python
204 lines
7 KiB
Python
from typing import FrozenSet
|
|
|
|
import pytest
|
|
|
|
from litellm.proxy.utils import hash_token
|
|
|
|
from tests.integration._support.actors import TEAM_ALPHA, Actor
|
|
from tests.integration._support.actors import create_scratch_key
|
|
|
|
pytestmark = pytest.mark.asyncio(loop_scope="session")
|
|
|
|
|
|
# Pinned default visibility for /key/list (no filter params): each actor's
|
|
# expected set of seeded actor keys.
|
|
_VISIBILITY = {
|
|
Actor.PROXY_ADMIN: frozenset(Actor),
|
|
Actor.ORG_ADMIN: frozenset({Actor.ORG_ADMIN}),
|
|
Actor.TEAM_ADMIN: frozenset({Actor.TEAM_ADMIN}),
|
|
Actor.INTERNAL_USER: frozenset({Actor.INTERNAL_USER}),
|
|
Actor.OWNER: frozenset({Actor.OWNER}),
|
|
Actor.UNRELATED_SAME_ORG: frozenset({Actor.UNRELATED_SAME_ORG}),
|
|
Actor.CROSS_ORG_USER: frozenset({Actor.CROSS_ORG_USER}),
|
|
Actor.SERVICE_ACCOUNT: frozenset({Actor.SERVICE_ACCOUNT}),
|
|
}
|
|
|
|
|
|
async def _all_visible_hashes(gateway_client, caller_cleartext) -> set:
|
|
"""Walk every /key/list page — size is capped at 100 by the endpoint, so a
|
|
single request can truncate PROXY_ADMIN's view on a non-fresh DB."""
|
|
hashes: set = set()
|
|
page = 1
|
|
while True:
|
|
resp = await gateway_client.get(
|
|
f"/key/list?page={page}&size=100",
|
|
headers={"Authorization": f"Bearer {caller_cleartext}"},
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
body = resp.json()
|
|
for entry in body.get("keys", []):
|
|
tok = entry.get("token") if isinstance(entry, dict) else entry
|
|
if tok:
|
|
hashes.add(tok)
|
|
if page >= (body.get("total_pages") or 1):
|
|
return hashes
|
|
page += 1
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"actor,expected_visible",
|
|
list(_VISIBILITY.items()),
|
|
ids=[a.value for a in _VISIBILITY],
|
|
)
|
|
async def test_key_list_visibility(
|
|
actor: Actor, expected_visible: FrozenSet[Actor], gateway_client, actors
|
|
):
|
|
caller = actors.keys[actor]
|
|
hashed_to_actor = {actors.keys[a].hashed: a for a in Actor}
|
|
|
|
returned_hashes = await _all_visible_hashes(gateway_client, caller.cleartext)
|
|
visible_seeded = {
|
|
hashed_to_actor[h] for h in returned_hashes if h in hashed_to_actor
|
|
}
|
|
assert visible_seeded == set(expected_visible), (
|
|
f"{actor.value}: expected {sorted(a.value for a in expected_visible)}, "
|
|
f"got {sorted(a.value for a in visible_seeded)}"
|
|
)
|
|
|
|
|
|
async def _list_hashes(gateway_client, caller_cleartext: str, query: str) -> set:
|
|
resp = await gateway_client.get(
|
|
f"/key/list?{query}&size=100",
|
|
headers={"Authorization": f"Bearer {caller_cleartext}"},
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
hashes: set = set()
|
|
for entry in resp.json().get("keys", []):
|
|
tok = entry.get("token") if isinstance(entry, dict) else entry
|
|
if tok:
|
|
hashes.add(tok)
|
|
return hashes
|
|
|
|
|
|
async def test_key_list_admin_key_alias_substring_match(gateway_client, resources, actors):
|
|
"""A PROXY_ADMIN's key_alias filter is a case-insensitive substring match
|
|
when substring_matching=true is requested (the dashboard search box); a
|
|
narrower fragment selects the subset whose alias contains it. Substring
|
|
matching is opt-in: without the flag the filter is exact (see
|
|
test_key_list_admin_key_alias_exact_without_substring_flag)."""
|
|
admin = actors.keys[Actor.PROXY_ADMIN]
|
|
a = await create_scratch_key(
|
|
gateway_client,
|
|
admin.cleartext,
|
|
resources.prefix,
|
|
user_id=admin.user_id,
|
|
key_alias=f"{resources.prefix}-sub-a",
|
|
)
|
|
b = await create_scratch_key(
|
|
gateway_client,
|
|
admin.cleartext,
|
|
resources.prefix,
|
|
user_id=admin.user_id,
|
|
key_alias=f"{resources.prefix}-sub-b",
|
|
)
|
|
seeded = {hash_token(a), hash_token(b)}
|
|
|
|
broad = await _list_hashes(
|
|
gateway_client,
|
|
admin.cleartext,
|
|
f"key_alias={resources.prefix}-sub&substring_matching=true",
|
|
)
|
|
assert broad & seeded == seeded
|
|
|
|
narrow = await _list_hashes(
|
|
gateway_client,
|
|
admin.cleartext,
|
|
f"key_alias={resources.prefix}-sub-a&substring_matching=true",
|
|
)
|
|
assert narrow & seeded == {hash_token(a)}
|
|
|
|
|
|
async def test_key_list_admin_key_alias_exact_without_substring_flag(
|
|
gateway_client, resources, actors
|
|
):
|
|
"""Regression guard for the prior exact-match contract: without
|
|
substring_matching, even a PROXY_ADMIN's key_alias filter is exact, so a
|
|
fragment of a seeded alias does not select it."""
|
|
admin = actors.keys[Actor.PROXY_ADMIN]
|
|
full_alias = f"{resources.prefix}-exactflag"
|
|
key = await create_scratch_key(
|
|
gateway_client,
|
|
admin.cleartext,
|
|
resources.prefix,
|
|
user_id=admin.user_id,
|
|
key_alias=full_alias,
|
|
)
|
|
key_hash = hash_token(key)
|
|
|
|
exact = await _list_hashes(gateway_client, admin.cleartext, f"key_alias={full_alias}")
|
|
assert key_hash in exact
|
|
|
|
fragment = await _list_hashes(
|
|
gateway_client, admin.cleartext, f"key_alias={resources.prefix}-exactfla"
|
|
)
|
|
assert key_hash not in fragment
|
|
|
|
|
|
async def test_key_list_non_admin_key_alias_is_exact_match(
|
|
gateway_client, resources, actors
|
|
):
|
|
"""A non-admin's key_alias filter is exact-match only — substring filtering
|
|
is restricted to admins. The full alias matches; a fragment does not."""
|
|
caller = actors.keys[Actor.INTERNAL_USER]
|
|
alias = f"{resources.prefix}-exact"
|
|
key = await create_scratch_key(
|
|
gateway_client,
|
|
actors.keys[Actor.PROXY_ADMIN].cleartext,
|
|
resources.prefix,
|
|
user_id=caller.user_id,
|
|
key_alias=alias,
|
|
)
|
|
key_hash = hash_token(key)
|
|
|
|
exact = await _list_hashes(gateway_client, caller.cleartext, f"key_alias={alias}")
|
|
assert key_hash in exact
|
|
|
|
fragment = await _list_hashes(
|
|
gateway_client, caller.cleartext, f"key_alias={resources.prefix}-exac"
|
|
)
|
|
assert key_hash not in fragment
|
|
|
|
|
|
async def test_key_list_team_id_filter(gateway_client, resources, actors):
|
|
"""A team_id filter narrows the listing to keys of that team."""
|
|
admin = actors.keys[Actor.PROXY_ADMIN]
|
|
team_key = await create_scratch_key(
|
|
gateway_client,
|
|
admin.cleartext,
|
|
resources.prefix,
|
|
user_id=actors.keys[Actor.OWNER].user_id,
|
|
team_id=actors.team_alpha_id,
|
|
key_alias=f"{resources.prefix}-team",
|
|
)
|
|
no_team_key = await create_scratch_key(
|
|
gateway_client,
|
|
admin.cleartext,
|
|
resources.prefix,
|
|
user_id=admin.user_id,
|
|
key_alias=f"{resources.prefix}-noteam",
|
|
)
|
|
|
|
hashes = await _list_hashes(gateway_client, admin.cleartext, f"team_id={actors.team_alpha_id}")
|
|
assert hash_token(team_key) in hashes
|
|
assert hash_token(no_team_key) not in hashes
|
|
|
|
|
|
async def test_key_list_non_admin_cannot_filter_other_team(gateway_client, actors):
|
|
"""A non-admin filtering by a team it does not belong to is rejected 403."""
|
|
resp = await gateway_client.get(
|
|
f"/key/list?team_id={actors.team_beta_id}",
|
|
headers={
|
|
"Authorization": f"Bearer {actors.keys[Actor.INTERNAL_USER].cleartext}"
|
|
},
|
|
)
|
|
assert resp.status_code == 403, resp.text
|