mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
ci(proxy-mgmt-behavior): seed scratch keys via proxy_admin actor, not master
Second CI run failed: ``/key/generate`` with explicit ``user_id`` returned 403 "User can only create keys for themselves. Got user_id=X, Your ID=None" in every test that called ``_create_scratch_key`` with a per-actor user_id. The bare master key's auth path was producing ``user_id=None`` in the fresh CI Postgres, which doesn't trigger the PROXY_ADMIN bypass in ``_user_can_only_create_keys_for_themselves`` reliably. Locally the same master key path worked, masking the issue. Fix: every ``_create_scratch_key`` helper now takes a seeder cleartext and the test bodies pass ``world.keys[Actor.PROXY_ADMIN].cleartext``. That actor was seeded with ``user_role=PROXY_ADMIN`` AND a concrete ``user_id``, so the bypass fires deterministically in both environments. No behavior shift in the matrices themselves — all 128 scenarios still pass locally; only the setup helper's auth identity changed. The bare-master smoke (test_smoke + test_scratch_teardown) is intentionally left on the master key path: those tests don't pass ``user_id`` in the body so they don't hit the user_id-mismatch gate.
This commit is contained in:
parent
1e21cb6574
commit
15c7a59d76
3 changed files with 40 additions and 17 deletions
|
|
@ -11,7 +11,6 @@ from typing import Any, Dict, Optional
|
|||
import pytest
|
||||
|
||||
from .actors import TEAM_ALPHA, TEAM_BETA, Actor
|
||||
from .conftest import MASTER_KEY
|
||||
|
||||
pytestmark = pytest.mark.asyncio(loop_scope="session")
|
||||
|
||||
|
|
@ -47,20 +46,29 @@ _SCENARIOS = [
|
|||
|
||||
async def _create_scratch_key(
|
||||
proxy_client,
|
||||
seeder_cleartext: str,
|
||||
scratch_prefix: str,
|
||||
*,
|
||||
user_id: str,
|
||||
team_id: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Seed a scratch key using the proxy_admin actor (not the bare master key).
|
||||
|
||||
The seeded proxy_admin actor's auth path produces user_role=PROXY_ADMIN
|
||||
+ a concrete user_id from the DB, which deterministically triggers the
|
||||
``_user_can_only_create_keys_for_themselves`` PROXY_ADMIN bypass. The
|
||||
bare master key takes a different auth resolution path whose behavior
|
||||
differs between fresh-CI and warm-local environments.
|
||||
"""
|
||||
body: Dict[str, Any] = {"key_alias": scratch_prefix, "user_id": user_id}
|
||||
if team_id is not None:
|
||||
body["team_id"] = team_id
|
||||
resp = await proxy_client.post(
|
||||
"/key/generate",
|
||||
headers={"Authorization": f"Bearer {MASTER_KEY}"},
|
||||
headers={"Authorization": f"Bearer {seeder_cleartext}"},
|
||||
json=body,
|
||||
)
|
||||
assert resp.status_code == 200, f"setup: master /key/generate failed: {resp.text}"
|
||||
assert resp.status_code == 200, f"setup: seeder /key/generate failed: {resp.text}"
|
||||
return resp.json()["key"]
|
||||
|
||||
|
||||
|
|
@ -84,11 +92,15 @@ async def test_key_delete_authz_matrix(
|
|||
|
||||
if target_shape == "self":
|
||||
target_cleartext = await _create_scratch_key(
|
||||
proxy_client, scratch.prefix, user_id=caller.user_id
|
||||
proxy_client,
|
||||
world.keys[Actor.PROXY_ADMIN].cleartext,
|
||||
scratch.prefix,
|
||||
user_id=caller.user_id,
|
||||
)
|
||||
elif target_shape == "owner":
|
||||
target_cleartext = await _create_scratch_key(
|
||||
proxy_client,
|
||||
world.keys[Actor.PROXY_ADMIN].cleartext,
|
||||
scratch.prefix,
|
||||
user_id=world.keys[Actor.OWNER].user_id,
|
||||
team_id=TEAM_ALPHA,
|
||||
|
|
@ -96,6 +108,7 @@ async def test_key_delete_authz_matrix(
|
|||
elif target_shape == "cross_org":
|
||||
target_cleartext = await _create_scratch_key(
|
||||
proxy_client,
|
||||
world.keys[Actor.PROXY_ADMIN].cleartext,
|
||||
scratch.prefix,
|
||||
user_id=world.keys[Actor.CROSS_ORG_USER].user_id,
|
||||
team_id=TEAM_BETA,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ from typing import Any, Dict, Optional
|
|||
import pytest
|
||||
|
||||
from .actors import TEAM_ALPHA, TEAM_BETA, Actor
|
||||
from .conftest import MASTER_KEY
|
||||
|
||||
pytestmark = pytest.mark.asyncio(loop_scope="session")
|
||||
|
||||
|
|
@ -56,20 +55,22 @@ _SCENARIOS = [
|
|||
|
||||
async def _create_scratch_key(
|
||||
proxy_client,
|
||||
seeder_cleartext: str,
|
||||
scratch_prefix: str,
|
||||
*,
|
||||
user_id: str,
|
||||
team_id: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Seeded under the proxy_admin actor (deterministic PROXY_ADMIN bypass)."""
|
||||
body: Dict[str, Any] = {"key_alias": scratch_prefix, "user_id": user_id}
|
||||
if team_id is not None:
|
||||
body["team_id"] = team_id
|
||||
resp = await proxy_client.post(
|
||||
"/key/generate",
|
||||
headers={"Authorization": f"Bearer {MASTER_KEY}"},
|
||||
headers={"Authorization": f"Bearer {seeder_cleartext}"},
|
||||
json=body,
|
||||
)
|
||||
assert resp.status_code == 200, f"setup: master /key/generate failed: {resp.text}"
|
||||
assert resp.status_code == 200, f"setup: seeder /key/generate failed: {resp.text}"
|
||||
return resp.json()["key"]
|
||||
|
||||
|
||||
|
|
@ -88,13 +89,15 @@ async def test_key_regenerate_authz_matrix(
|
|||
):
|
||||
caller = world.keys[actor]
|
||||
|
||||
seeder = world.keys[Actor.PROXY_ADMIN].cleartext
|
||||
if target_shape == "self":
|
||||
target_cleartext = await _create_scratch_key(
|
||||
proxy_client, scratch.prefix, user_id=caller.user_id
|
||||
proxy_client, seeder, scratch.prefix, user_id=caller.user_id
|
||||
)
|
||||
elif target_shape == "owner":
|
||||
target_cleartext = await _create_scratch_key(
|
||||
proxy_client,
|
||||
seeder,
|
||||
scratch.prefix,
|
||||
user_id=world.keys[Actor.OWNER].user_id,
|
||||
team_id=TEAM_ALPHA,
|
||||
|
|
@ -102,6 +105,7 @@ async def test_key_regenerate_authz_matrix(
|
|||
elif target_shape == "cross_org":
|
||||
target_cleartext = await _create_scratch_key(
|
||||
proxy_client,
|
||||
seeder,
|
||||
scratch.prefix,
|
||||
user_id=world.keys[Actor.CROSS_ORG_USER].user_id,
|
||||
team_id=TEAM_BETA,
|
||||
|
|
@ -160,7 +164,7 @@ async def test_key_path_regenerate_smoke(proxy_client, scratch, world):
|
|||
path form, and the same rotation contract holds."""
|
||||
caller = world.keys[Actor.PROXY_ADMIN]
|
||||
target_cleartext = await _create_scratch_key(
|
||||
proxy_client, scratch.prefix, user_id=caller.user_id
|
||||
proxy_client, caller.cleartext, scratch.prefix, user_id=caller.user_id
|
||||
)
|
||||
|
||||
resp = await proxy_client.post(
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ from typing import Any, Dict, List
|
|||
import pytest
|
||||
|
||||
from .actors import TEAM_ALPHA, TEAM_BETA, Actor
|
||||
from .conftest import MASTER_KEY
|
||||
|
||||
pytestmark = pytest.mark.asyncio(loop_scope="session")
|
||||
|
||||
|
|
@ -79,14 +78,20 @@ MARKER_MODEL = "behavior-pin-update-marker-model"
|
|||
|
||||
async def _create_scratch_key(
|
||||
proxy_client,
|
||||
prisma,
|
||||
seeder_cleartext: str,
|
||||
scratch_prefix: str,
|
||||
*,
|
||||
user_id: str,
|
||||
team_id: str = None,
|
||||
organization_id: str = None,
|
||||
) -> str:
|
||||
"""Seed a fresh key tagged with scratch_prefix and return its cleartext."""
|
||||
"""Seed a fresh key tagged with scratch_prefix using the proxy_admin actor.
|
||||
|
||||
Using the seeded PROXY_ADMIN actor (not the bare master key) makes the
|
||||
setup call's auth path deterministic: user_role=PROXY_ADMIN + a concrete
|
||||
user_id, which reliably triggers
|
||||
``_user_can_only_create_keys_for_themselves``'s admin bypass.
|
||||
"""
|
||||
body: Dict[str, Any] = {"key_alias": scratch_prefix, "user_id": user_id}
|
||||
if team_id is not None:
|
||||
body["team_id"] = team_id
|
||||
|
|
@ -94,10 +99,10 @@ async def _create_scratch_key(
|
|||
body["organization_id"] = organization_id
|
||||
resp = await proxy_client.post(
|
||||
"/key/generate",
|
||||
headers={"Authorization": f"Bearer {MASTER_KEY}"},
|
||||
headers={"Authorization": f"Bearer {seeder_cleartext}"},
|
||||
json=body,
|
||||
)
|
||||
assert resp.status_code == 200, f"setup: master /key/generate failed: {resp.text}"
|
||||
assert resp.status_code == 200, f"setup: seeder /key/generate failed: {resp.text}"
|
||||
return resp.json()["key"]
|
||||
|
||||
|
||||
|
|
@ -117,14 +122,15 @@ async def test_key_update_authz_matrix(
|
|||
):
|
||||
caller = world.keys[actor]
|
||||
|
||||
seeder = world.keys[Actor.PROXY_ADMIN].cleartext
|
||||
if target_shape == "self":
|
||||
target_cleartext = await _create_scratch_key(
|
||||
proxy_client, prisma, scratch.prefix, user_id=caller.user_id
|
||||
proxy_client, seeder, scratch.prefix, user_id=caller.user_id
|
||||
)
|
||||
elif target_shape == "owner":
|
||||
target_cleartext = await _create_scratch_key(
|
||||
proxy_client,
|
||||
prisma,
|
||||
seeder,
|
||||
scratch.prefix,
|
||||
user_id=world.keys[Actor.OWNER].user_id,
|
||||
team_id=TEAM_ALPHA,
|
||||
|
|
@ -132,7 +138,7 @@ async def test_key_update_authz_matrix(
|
|||
elif target_shape == "cross_org":
|
||||
target_cleartext = await _create_scratch_key(
|
||||
proxy_client,
|
||||
prisma,
|
||||
seeder,
|
||||
scratch.prefix,
|
||||
user_id=world.keys[Actor.CROSS_ORG_USER].user_id,
|
||||
team_id=TEAM_BETA,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue