fix(proxy): keep keys a team admin creates for a member from carrying more than LLM access

A key authenticates as its owner, so with create_member_keys on a team admin could create a key owned by a proxy admin who sits in their team and act as a proxy admin with it

The member now has to hold the internal_user or internal_user_viewer role, and the key is always an llm_api key, so the team admin never holds a secret that reaches management routes. Asking for a management or read_only key for a member is refused

The unit tests now check the permission decision directly instead of replacing generate_key_helper_fn, and the saved key is checked against a real proxy in the team admin gate matrix
This commit is contained in:
ryan-crabbe-berri 2026-10-10 14:59:50 -07:00
parent 2a65a0deae
commit 7989d6c2c7
7 changed files with 280 additions and 119 deletions

View file

@ -114,8 +114,10 @@ from litellm.proxy.management_endpoints.router_weights import validate_router_se
from litellm.proxy.management_endpoints.team_admin_field_permissions import (
team_admin_key_edit_verdict,
team_admin_key_request_or_raise,
TEAM_ADMIN_MEMBER_KEY_TYPE,
team_admin_may_edit_member_key_budgets,
team_admin_member_key_creation_or_raise,
team_admin_member_key_creation_verdict,
)
from litellm.proxy.management_helpers.access_group_key_sync import (
sync_key_access_group_membership,
@ -1216,26 +1218,35 @@ def _enforce_upperbound_key_params(
)
def _acting_as_team_admin_for_key_generation(
def _team_admin_requests_member_key(
data: GenerateKeyRequest,
team_table: LiteLLM_TeamTableCachedObj | None,
user_api_key_dict: UserAPIKeyAuth,
) -> bool:
"""Whether the caller is a team admin creating a team key owned by another member of that team.
Raises 403 when the caller administers the team but the create_member_keys permission is disabled.
"""
if (
"""Whether the caller is a team admin asking for a team key owned by another member of that team."""
return not (
user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value
or team_table is None
or data.user_id is None
or data.user_id == user_api_key_dict.user_id
or _get_user_in_team(team_table=team_table, user_id=data.user_id) is None
or not is_team_admin(user_api_key_dict=user_api_key_dict, team_obj=team_table)
):
return False
team_admin_member_key_creation_or_raise(_general_settings())
return True
)
def _member_key_route_limit(acting_as_team_admin: bool) -> dict[str, object]:
if not acting_as_team_admin:
return {}
return handle_key_type(GenerateKeyRequest(key_type=TEAM_ADMIN_MEMBER_KEY_TYPE), {})
async def _stored_user_role(prisma_client: PrismaClient | None, user_id: str | None) -> str | None:
if prisma_client is None or user_id is None:
return None
user_row: Final = await _prisma_table(UserRepository(prisma_client, use_writer=True)).find_unique(
where={"user_id": user_id}
)
return user_row.user_role if user_row is not None else None
async def _common_key_generation_helper(
@ -1252,11 +1263,19 @@ async def _common_key_generation_helper(
prisma_client,
)
acting_as_team_admin: Final = _acting_as_team_admin_for_key_generation(
acting_as_team_admin: Final = _team_admin_requests_member_key(
data=data,
team_table=team_table,
user_api_key_dict=user_api_key_dict,
)
if acting_as_team_admin:
team_admin_member_key_creation_or_raise(
team_admin_member_key_creation_verdict(
_general_settings(),
key_type=data.key_type,
owner_role=await _stored_user_role(prisma_client, data.user_id),
)
)
common_key_access_checks(
user_api_key_dict=user_api_key_dict,
data=data,
@ -1468,7 +1487,7 @@ async def _common_key_generation_helper(
data_json = data.model_dump(exclude_unset=True, exclude_none=True)
data_json = handle_key_type(data, data_json)
data_json = {**handle_key_type(data, data_json), **_member_key_route_limit(acting_as_team_admin)}
# Re-check allowed_routes after handle_key_type, since key_type can derive
# an elevated bucket (e.g. ["management_routes"]) that wasn't present in
@ -2142,7 +2161,7 @@ async def generate_key_fn(
- key_alias: Optional[str] - User defined key alias
- key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 16 characters long. If not set, a 16-digit unique sk-key is created for you.
- team_id: Optional[str] - The team id of the key
- user_id: Optional[str] - The user id of the key. Non-admin callers may only pass their own id, except a team admin creating a key in a team they administer for another member of that team, when a proxy admin has added `create_member_keys` to `team_admin_editable_team_fields`.
- user_id: Optional[str] - The user id of the key. Non-admin callers may only pass their own id, except a team admin creating a key in a team they administer for another member of that team, when a proxy admin has added `create_member_keys` to `team_admin_editable_team_fields`. That member must have the `internal_user` or `internal_user_viewer` role, and the key is always an `llm_api` key.
- agent_id: Optional[str] - The agent id associated with the key.
- organization_id: Optional[str] - The organization id of the key. If not set, and team_id is set, the organization id will be the same as the team id. If conflict, an error will be raised.
- project_id: Optional[str] - The project id of the key. When set, models and max_budget are validated against the project's limits.

View file

@ -3,7 +3,9 @@
above the organization's budget, never removing the cap), the ``projects`` permission for /project/new and
/project/update, the ``member_key_budgets`` permission for budget fields on other members' keys via
/key/update, and the ``create_member_keys`` permission for creating team keys owned by other members via
/key/generate."""
/key/generate. A key created that way authenticates as its owner, so the owner must be an ordinary internal
user and the key is confined to LLM API routes: the team admin who holds its secret gains no management
authority through it."""
from collections.abc import Mapping
from dataclasses import dataclass
@ -20,6 +22,8 @@ from litellm.models.verification_token import LiteLLM_VerificationToken
from litellm.proxy._types import (
LiteLLM_ManagementEndpoint_MetadataFields,
LiteLLM_ManagementEndpoint_MetadataFields_Premium,
LiteLLMKeyType,
LitellmUserRoles,
UpdateKeyRequest,
UpdateTeamRequest,
)
@ -32,6 +36,10 @@ TEAM_ADMIN_RAISE_MAX_BUDGET_PERMISSION: Final = "raise_max_budget"
TEAM_ADMIN_PROJECTS_PERMISSION: Final = "projects"
TEAM_ADMIN_MEMBER_KEY_BUDGETS_PERMISSION: Final = "member_key_budgets"
TEAM_ADMIN_CREATE_MEMBER_KEYS_PERMISSION: Final = "create_member_keys"
TEAM_ADMIN_MEMBER_KEY_OWNER_ROLES: Final[frozenset[str]] = frozenset(
{LitellmUserRoles.INTERNAL_USER.value, LitellmUserRoles.INTERNAL_USER_VIEW_ONLY.value}
)
TEAM_ADMIN_MEMBER_KEY_TYPE: Final = LiteLLMKeyType.LLM_API
SUPPORTED_TEAM_ADMIN_PERMISSIONS: Final[frozenset[str]] = SUPPORTED_TEAM_ADMIN_EDITABLE_TEAM_FIELDS | {
TEAM_ADMIN_RAISE_MAX_BUDGET_PERMISSION,
TEAM_ADMIN_PROJECTS_PERMISSION,
@ -119,17 +127,83 @@ def team_admin_may_create_member_keys(general_settings: Mapping[str, object]) ->
)
def team_admin_member_key_creation_or_raise(general_settings: Mapping[str, object]) -> None:
if team_admin_may_create_member_keys(general_settings):
return
raise HTTPException(
status_code=403,
detail=(
"Team admins on this proxy cannot create keys for other team members. "
f"Ask a proxy admin to enable '{TEAM_ADMIN_CREATE_MEMBER_KEYS_PERMISSION}' "
f"under {_SETTINGS_LOCATION}."
),
)
@dataclass(frozen=True, slots=True)
class TeamAdminMemberKeyCreationAllowed:
kind: Literal["allowed"] = "allowed"
@dataclass(frozen=True, slots=True)
class TeamAdminMemberKeyCreationDisabled:
kind: Literal["disabled"] = "disabled"
@dataclass(frozen=True, slots=True)
class TeamAdminMemberKeyOwnerNotPermitted:
owner_role: str | None
kind: Literal["owner_not_permitted"] = "owner_not_permitted"
@dataclass(frozen=True, slots=True)
class TeamAdminMemberKeyTypeNotPermitted:
key_type: LiteLLMKeyType
kind: Literal["key_type_not_permitted"] = "key_type_not_permitted"
TeamAdminMemberKeyCreationVerdict: TypeAlias = (
TeamAdminMemberKeyCreationAllowed
| TeamAdminMemberKeyCreationDisabled
| TeamAdminMemberKeyOwnerNotPermitted
| TeamAdminMemberKeyTypeNotPermitted
)
def team_admin_member_key_creation_verdict(
general_settings: Mapping[str, object],
key_type: LiteLLMKeyType | None,
owner_role: str | None,
) -> TeamAdminMemberKeyCreationVerdict:
"""``owner_role`` is the stored proxy role of the member the key is for, None when they have no user row."""
if not team_admin_may_create_member_keys(general_settings):
return TeamAdminMemberKeyCreationDisabled()
if owner_role not in TEAM_ADMIN_MEMBER_KEY_OWNER_ROLES:
return TeamAdminMemberKeyOwnerNotPermitted(owner_role=owner_role)
if key_type not in (None, LiteLLMKeyType.DEFAULT, TEAM_ADMIN_MEMBER_KEY_TYPE):
return TeamAdminMemberKeyTypeNotPermitted(key_type=key_type)
return TeamAdminMemberKeyCreationAllowed()
def team_admin_member_key_creation_or_raise(verdict: TeamAdminMemberKeyCreationVerdict) -> None:
match verdict:
case TeamAdminMemberKeyCreationAllowed():
return
case TeamAdminMemberKeyCreationDisabled():
raise HTTPException(
status_code=403,
detail=(
"Team admins on this proxy cannot create keys for other team members. "
f"Ask a proxy admin to enable '{TEAM_ADMIN_CREATE_MEMBER_KEYS_PERMISSION}' "
f"under {_SETTINGS_LOCATION}."
),
)
case TeamAdminMemberKeyOwnerNotPermitted(owner_role=owner_role):
raise HTTPException(
status_code=403,
detail=(
"Team admins can only create keys for team members whose proxy role is one of "
f"{sorted(TEAM_ADMIN_MEMBER_KEY_OWNER_ROLES)}, not '{owner_role}'. "
"Ask a proxy admin to create this key."
),
)
case TeamAdminMemberKeyTypeNotPermitted(key_type=key_type):
raise HTTPException(
status_code=403,
detail=(
"Keys a team admin creates for another team member are always "
f"'{TEAM_ADMIN_MEMBER_KEY_TYPE.value}' keys, not '{key_type.value}'."
),
)
case _:
assert_never(verdict)
def team_admin_may_raise_max_budget(general_settings: Mapping[str, object]) -> bool:

View file

@ -366,6 +366,7 @@ class UISettings(LiteLLMBaseModel):
"Include 'projects' to let team admins create and update projects for those teams. "
"Include 'member_key_budgets' to let team admins update budget fields on keys owned by other members of those teams. "
"Include 'create_member_keys' to let team admins create team keys owned by other members of those teams. "
"Those keys can only call LLM API routes, and the member must be an internal user. "
"Empty means team admins cannot edit team settings or manage projects at all. "
"Proxy admins and org admins are not affected."
),

View file

@ -71,6 +71,11 @@ class TeamScenario:
def member(self) -> str:
return self.scenario.member(self.team_id)
def proxy_admin_member(self) -> str:
user_id: Final = self.scenario.user(user_role="proxy_admin")
self.gateway.post("/team/member_add", {"team_id": self.team_id, "member": {"role": "user", "user_id": user_id}})
return user_id
def member_key(self) -> str:
created: Final = self.gateway.post("/key/generate", {"user_id": self.member(), "team_id": self.team_id})
return string_value(created["key"])
@ -253,6 +258,12 @@ ROUTES: Final[tuple[Route, ...]] = (
Route("key_generate_for_member_permitted",
lambda s: Call("POST", "/key/generate", {"team_id": s.team_id, "user_id": s.member()}),
team_admin=200, others=400, member=401, permission=("create_member_keys",), cleanup=_delete_key),
Route("key_generate_for_proxy_admin_member_permitted",
lambda s: Call("POST", "/key/generate", {"team_id": s.team_id, "user_id": s.proxy_admin_member()}),
team_admin=403, others=400, member=401, permission=("create_member_keys",), cleanup=_delete_key),
Route("key_generate_management_key_for_member_permitted",
lambda s: Call("POST", "/key/generate", {"team_id": s.team_id, "user_id": s.member(), "key_type": "management"}),
team_admin=403, others=400, member=401, permission=("create_member_keys",), cleanup=_delete_key),
Route("service_account_generate",
lambda s: Call("POST", "/key/service-account/generate", {"team_id": s.team_id, "key_alias": f"matrix-{uuid.uuid4().hex}"}),
team_admin=200, others=400, member=401, cleanup=_delete_key),
@ -451,3 +462,26 @@ def test_status_code(shared: TeamScenario, org_team: TeamScenario, route: Route,
) == [{"max_budget": _BUDGET_ROUTES[route.name] if response.status_code == 200 else _BUDGET_BEFORE}]
if response.status_code == 200 and route.cleanup is not None:
route.cleanup(s, object_value(response.json()))
def test_member_key_created_by_a_team_admin_reaches_llm_routes_only(shared: TeamScenario) -> None:
"""The team admin holds the secret of a key that authenticates as the member, so it must not carry the
member's management access."""
with shared.gateway.scenario() as scenario:
s: Final = replace(shared, scenario=scenario)
scenario.cleanups.enter_context(team_admin_permissions(s.gateway, ("create_member_keys",)))
member: Final = s.member()
created: Final = s.gateway.post(
"/key/generate", {"team_id": s.team_id, "user_id": member}, key=s.keys["team_admin"]
)
token: Final = string_value(created["key"])
scenario.cleanups.callback(delete_key_if_present, s.gateway, token)
own_key: Final = scenario.key(user_id=member, team_id=s.team_id)
assert read_rows(
'SELECT user_id, team_id, allowed_routes FROM "LiteLLM_VerificationToken" WHERE token = %s',
(string_value(created["token"]),),
) == [{"user_id": member, "team_id": s.team_id, "allowed_routes": ["llm_api_routes"]}]
s.gateway.chat(s.model(), key=token)
assert s.gateway.request("GET", "/user/info", key=own_key).status_code == 200
assert s.gateway.request("GET", "/user/info", key=token).status_code == 403

View file

@ -56,6 +56,8 @@ from litellm.proxy.management_endpoints.key_management_endpoints import (
_check_project_key_limits,
_check_team_key_limits,
_common_key_generation_helper,
_member_key_route_limit,
_team_admin_requests_member_key,
KeyProjectBindingError,
_effective_key_after_update,
_effective_key_for_generate,
@ -22327,97 +22329,85 @@ class TestTeamAdminMemberKeyBudgetUpdate:
assert "member_key_budgets" not in str(exc.value.detail)
class TestTeamAdminCreatesMemberKey:
"""LIT-6380: a team admin may create a team key owned by another member of that team
only when the proxy enables the 'create_member_keys' permission."""
_MEMBER_KEY_TEAM = LiteLLM_TeamTableCachedObj(
team_id="team-1",
members_with_roles=[
Member(user_id="team-admin-1", role="admin"),
Member(user_id="member-1", role="user"),
Member(user_id="member-2", role="user"),
],
)
def _team(self, members):
return LiteLLM_TeamTableCachedObj(team_id="team-1", members_with_roles=members)
def _admin_and_member(self):
return self._team([Member(user_id="team-admin-1", role="admin"), Member(user_id="member-1", role="user")])
async def _generate(self, monkeypatch, editable_fields, data, team_table, caller):
generate_key = AsyncMock(
return_value={"key": "sk-new", "expires": None, "user_id": data.user_id, "team_id": data.team_id}
)
monkeypatch.setattr(
"litellm.proxy.proxy_server.general_settings", {"team_admin_editable_team_fields": editable_fields}
)
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", MagicMock())
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", None)
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", False)
monkeypatch.setattr(
"litellm.proxy.management_endpoints.key_management_endpoints.generate_key_helper_fn", generate_key
)
await _common_key_generation_helper(
data=data,
user_api_key_dict=UserAPIKeyAuth(user_id=caller, user_role=LitellmUserRoles.INTERNAL_USER),
litellm_changed_by=None,
team_table=team_table,
)
return generate_key
@pytest.mark.asyncio
async def test_team_admin_creates_a_key_owned_by_a_member_when_enabled(self, monkeypatch):
generate_key = await self._generate(
monkeypatch,
["create_member_keys"],
GenerateKeyRequest(user_id="member-1", team_id="team-1"),
self._admin_and_member(),
caller="team-admin-1",
)
created = generate_key.call_args.kwargs
assert (created["user_id"], created["team_id"]) == ("member-1", "team-1")
@pytest.mark.asyncio
async def test_team_admin_is_pointed_at_the_setting_when_disabled(self, monkeypatch):
with pytest.raises(HTTPException) as exc:
await self._generate(
monkeypatch,
["member_key_budgets"],
GenerateKeyRequest(user_id="member-1", team_id="team-1"),
self._admin_and_member(),
caller="team-admin-1",
)
assert exc.value.status_code == 403
assert "'create_member_keys'" in str(exc.value.detail)
@pytest.mark.parametrize(
"data, team_members, caller",
[
pytest.param(
GenerateKeyRequest(user_id="member-1", team_id="team-1"),
[Member(user_id="member-2", role="user"), Member(user_id="member-1", role="user")],
"member-2",
id="ordinary_member_for_another_member",
),
pytest.param(
GenerateKeyRequest(user_id="outsider", team_id="team-1"),
[Member(user_id="team-admin-1", role="admin")],
"team-admin-1",
id="team_admin_for_a_user_outside_the_team",
),
pytest.param(
GenerateKeyRequest(user_id="member-1"),
None,
"team-admin-1",
id="personal_key_for_another_user",
),
],
def test_team_admin_asking_for_a_teammates_team_key_is_a_member_key_request():
assert _team_admin_requests_member_key(
data=GenerateKeyRequest(user_id="member-1", team_id="team-1"),
team_table=_MEMBER_KEY_TEAM,
user_api_key_dict=UserAPIKeyAuth(user_id="team-admin-1", user_role=LitellmUserRoles.INTERNAL_USER),
)
@pytest.mark.asyncio
async def test_enabling_the_permission_grants_nothing_else(self, monkeypatch, data, team_members, caller):
with pytest.raises(HTTPException) as exc:
await self._generate(
monkeypatch,
["create_member_keys"],
data,
None if team_members is None else self._team(team_members),
caller=caller,
)
assert exc.value.status_code == 403
assert "User can only create keys for themselves" in str(exc.value.detail)
@pytest.mark.parametrize(
"data, team_table, caller, caller_role",
[
pytest.param(
GenerateKeyRequest(user_id="member-1", team_id="team-1"),
_MEMBER_KEY_TEAM,
"member-2",
LitellmUserRoles.INTERNAL_USER,
id="ordinary_member_for_another_member",
),
pytest.param(
GenerateKeyRequest(user_id="outsider", team_id="team-1"),
_MEMBER_KEY_TEAM,
"team-admin-1",
LitellmUserRoles.INTERNAL_USER,
id="team_admin_for_a_user_outside_the_team",
),
pytest.param(
GenerateKeyRequest(user_id="member-1"),
None,
"team-admin-1",
LitellmUserRoles.INTERNAL_USER,
id="personal_key_for_another_user",
),
pytest.param(
GenerateKeyRequest(user_id="team-admin-1", team_id="team-1"),
_MEMBER_KEY_TEAM,
"team-admin-1",
LitellmUserRoles.INTERNAL_USER,
id="team_admin_for_themselves",
),
pytest.param(
GenerateKeyRequest(team_id="team-1"),
_MEMBER_KEY_TEAM,
"team-admin-1",
LitellmUserRoles.INTERNAL_USER,
id="team_key_with_no_owner",
),
pytest.param(
GenerateKeyRequest(user_id="member-1", team_id="team-1"),
_MEMBER_KEY_TEAM,
"team-admin-1",
LitellmUserRoles.PROXY_ADMIN,
id="proxy_admin_keeps_the_proxy_admin_path",
),
],
)
def test_other_key_requests_are_not_member_key_requests(data, team_table, caller, caller_role):
assert not _team_admin_requests_member_key(
data=data,
team_table=team_table,
user_api_key_dict=UserAPIKeyAuth(user_id=caller, user_role=caller_role),
)
def test_member_key_route_limit_confines_only_member_keys_to_llm_routes():
assert _member_key_route_limit(acting_as_team_admin=True) == {
"key_type": "llm_api",
"allowed_routes": ["llm_api_routes"],
}
assert _member_key_route_limit(acting_as_team_admin=False) == {}
@pytest.mark.asyncio

View file

@ -3,7 +3,13 @@ from fastapi import HTTPException
from litellm.models.team import BudgetLimitEntry
from litellm.models.verification_token import LiteLLM_VerificationToken
from litellm.proxy._types import LiteLLM_ModelTable, LiteLLM_TeamTable, UpdateKeyRequest, UpdateTeamRequest
from litellm.proxy._types import (
LiteLLM_ModelTable,
LiteLLM_TeamTable,
LiteLLMKeyType,
UpdateKeyRequest,
UpdateTeamRequest,
)
from litellm.proxy.management_endpoints.team_admin_field_permissions import (
TeamAdminEditAllowed,
TeamAdminEditingDisabled,
@ -21,6 +27,11 @@ from litellm.proxy.management_endpoints.team_admin_field_permissions import (
team_admin_may_manage_projects,
team_admin_may_raise_max_budget,
team_admin_member_key_creation_or_raise,
team_admin_member_key_creation_verdict,
TeamAdminMemberKeyCreationAllowed,
TeamAdminMemberKeyCreationDisabled,
TeamAdminMemberKeyOwnerNotPermitted,
TeamAdminMemberKeyTypeNotPermitted,
team_admin_request_or_raise,
)
@ -210,16 +221,48 @@ class TestTeamAdminMayCreateMemberKeys:
def test_malformed_setting_denies(self, raw):
assert team_admin_may_create_member_keys({"team_admin_editable_team_fields": raw}) is False
def test_or_raise_passes_when_granted(self):
assert team_admin_member_key_creation_or_raise({"team_admin_editable_team_fields": ["create_member_keys"]}) is None
def test_or_raise_is_a_403_naming_the_permission_and_where_to_enable_it(self):
_MEMBER_KEYS_ON = {"team_admin_editable_team_fields": ["create_member_keys"]}
class TestTeamAdminMemberKeyCreationVerdict:
@pytest.mark.parametrize("owner_role", ["internal_user", "internal_user_viewer"])
@pytest.mark.parametrize("key_type", [None, LiteLLMKeyType.DEFAULT, LiteLLMKeyType.LLM_API])
def test_an_ordinary_member_and_an_llm_key_are_allowed(self, owner_role, key_type):
verdict = team_admin_member_key_creation_verdict(_MEMBER_KEYS_ON, key_type=key_type, owner_role=owner_role)
assert verdict == TeamAdminMemberKeyCreationAllowed()
assert team_admin_member_key_creation_or_raise(verdict) is None
def test_disabled_is_a_403_naming_the_permission_and_where_to_enable_it(self):
verdict = team_admin_member_key_creation_verdict(
{"team_admin_editable_team_fields": ["member_key_budgets"]}, key_type=None, owner_role="internal_user"
)
assert verdict == TeamAdminMemberKeyCreationDisabled()
with pytest.raises(HTTPException) as exc:
team_admin_member_key_creation_or_raise({"team_admin_editable_team_fields": ["member_key_budgets"]})
team_admin_member_key_creation_or_raise(verdict)
assert exc.value.status_code == 403
assert "'create_member_keys'" in exc.value.detail
assert "Settings > UI > Team admin editable fields" in exc.value.detail
@pytest.mark.parametrize("owner_role", ["proxy_admin", "proxy_admin_viewer", "org_admin", "team", "customer", None])
def test_an_owner_who_is_not_an_ordinary_internal_user_is_refused(self, owner_role):
"""A key authenticates with its owner's role, so a privileged owner would hand that role to the team admin."""
verdict = team_admin_member_key_creation_verdict(_MEMBER_KEYS_ON, key_type=None, owner_role=owner_role)
assert verdict == TeamAdminMemberKeyOwnerNotPermitted(owner_role=owner_role)
with pytest.raises(HTTPException) as exc:
team_admin_member_key_creation_or_raise(verdict)
assert exc.value.status_code == 403
assert f"'{owner_role}'" in exc.value.detail
@pytest.mark.parametrize("key_type", [LiteLLMKeyType.MANAGEMENT, LiteLLMKeyType.READ_ONLY])
def test_a_key_type_that_reaches_beyond_llm_routes_is_refused(self, key_type):
verdict = team_admin_member_key_creation_verdict(_MEMBER_KEYS_ON, key_type=key_type, owner_role="internal_user")
assert verdict == TeamAdminMemberKeyTypeNotPermitted(key_type=key_type)
with pytest.raises(HTTPException) as exc:
team_admin_member_key_creation_or_raise(verdict)
assert exc.value.status_code == 403
assert f"'{key_type.value}'" in exc.value.detail
class TestTeamAdminMayRaiseMaxBudget:
def test_missing_setting_denies(self):

View file

@ -8333,7 +8333,7 @@ export interface paths {
* - key_alias: Optional[str] - User defined key alias
* - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 16 characters long. If not set, a 16-digit unique sk-key is created for you.
* - team_id: Optional[str] - The team id of the key
* - user_id: Optional[str] - The user id of the key. Non-admin callers may only pass their own id, except a team admin creating a key in a team they administer for another member of that team, when a proxy admin has added `create_member_keys` to `team_admin_editable_team_fields`.
* - user_id: Optional[str] - The user id of the key. Non-admin callers may only pass their own id, except a team admin creating a key in a team they administer for another member of that team, when a proxy admin has added `create_member_keys` to `team_admin_editable_team_fields`. That member must have the `internal_user` or `internal_user_viewer` role, and the key is always an `llm_api` key.
* - agent_id: Optional[str] - The agent id associated with the key.
* - organization_id: Optional[str] - The organization id of the key. If not set, and team_id is set, the organization id will be the same as the team id. If conflict, an error will be raised.
* - project_id: Optional[str] - The project id of the key. When set, models and max_budget are validated against the project's limits.