Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/management_helpers/team_member_permission_checks.py: 38%
76 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1from typing import Final, Literal
3from litellm.proxy._types import (
4 KeyManagementRoutes,
5 LiteLLM_TeamTableCachedObj,
6 LiteLLM_VerificationToken,
7 LiteLLMRoutes,
8 LitellmUserRoles,
9 ProxyErrorTypes,
10 ProxyException,
11 UserAPIKeyAuth,
12)
13from litellm.proxy.auth.auth_checks import get_team_object
14from litellm.proxy.auth.route_checks import RouteChecks
15from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
16from litellm.proxy.utils import PrismaClient
18BASELINE_TEAM_MEMBER_PERMISSIONS: Final = [
19 KeyManagementRoutes.KEY_INFO,
20 KeyManagementRoutes.KEY_HEALTH,
21]
23DEFAULT_TEAM_MEMBER_PERMISSIONS: Final = BASELINE_TEAM_MEMBER_PERMISSIONS
26class TeamMemberPermissionChecks:
27 @staticmethod
28 def get_permissions_for_team_member(
29 team_table: LiteLLM_TeamTableCachedObj,
30 ) -> list[KeyManagementRoutes]:
31 """
32 Returns the permissions for a team member.
34 - If team has explicit permissions set (including []), use those
35 plus baseline permissions (/key/info, /key/health).
36 - If team has no permissions set (None), fall back to
37 DEFAULT_TEAM_MEMBER_PERMISSIONS.
38 """
39 if team_table.team_member_permissions is not None and isinstance(team_table.team_member_permissions, list):
40 permissions: Final = {KeyManagementRoutes(permission) for permission in team_table.team_member_permissions}
41 # Always include baseline permissions
42 permissions.update(BASELINE_TEAM_MEMBER_PERMISSIONS)
43 return list(permissions)
45 return DEFAULT_TEAM_MEMBER_PERMISSIONS
47 @staticmethod
48 def _get_list_of_route_enum_as_str(
49 route_enum: list[KeyManagementRoutes],
50 ) -> list[str]:
51 """
52 Returns a list of the route enum as a list of strings
53 """
54 return [route.value for route in route_enum]
56 @staticmethod
57 async def can_team_member_execute_key_management_endpoint(
58 user_api_key_dict: UserAPIKeyAuth,
59 route: KeyManagementRoutes,
60 prisma_client: PrismaClient,
61 user_api_key_cache: UserApiKeyCache,
62 existing_key_row: LiteLLM_VerificationToken,
63 ):
64 """
65 Main handler for checking if a team member can update a key
66 """
67 from litellm.proxy.management_endpoints.key_management_endpoints import (
68 _get_caller_team_role,
69 )
71 # 1. Don't execute these checks if the user role is proxy admin
72 if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value:
73 return
75 # 2. Check if the operation is being done on a team key
76 if existing_key_row.team_id is None:
77 return
79 # 3. Get Team Object from DB
80 team_table: Final = await get_team_object(
81 team_id=existing_key_row.team_id,
82 prisma_client=prisma_client,
83 user_api_key_cache=user_api_key_cache,
84 parent_otel_span=user_api_key_dict.parent_otel_span,
85 check_db_only=True,
86 )
88 caller_team_role: Final = _get_caller_team_role(team_table=team_table, user_api_key_dict=user_api_key_dict)
90 # 4. Check if the team member has permissions for the endpoint
91 has_permission: Final = TeamMemberPermissionChecks.does_team_member_have_permissions_for_endpoint(
92 team_member_role=caller_team_role,
93 team_table=team_table,
94 route=route,
95 )
96 if not has_permission:
97 raise ProxyException(
98 message=f"User {user_api_key_dict.user_id} does not belong to team {team_table.team_id}. Team-scoped key management endpoints can only be used for keys in your own team.",
99 type=ProxyErrorTypes.team_member_permission_error,
100 param=route,
101 code=401,
102 )
104 @staticmethod
105 def does_team_member_have_permissions_for_endpoint(
106 team_member_role: Literal["admin", "user"] | None,
107 team_table: LiteLLM_TeamTableCachedObj,
108 route: str,
109 ) -> bool | None:
110 """
111 Raises an exception if the team member does not have permissions for calling the endpoint for a team
112 """
114 # permission checks only run for non-admin users
115 # Non-Admin user trying to access information about a team's key
116 if team_member_role is None:
117 return False
118 if team_member_role == "admin":
119 return True
121 _team_member_permissions: Final = TeamMemberPermissionChecks.get_permissions_for_team_member(
122 team_table=team_table,
123 )
124 team_member_permissions = TeamMemberPermissionChecks._get_list_of_route_enum_as_str(_team_member_permissions)
126 if not RouteChecks.check_route_access(route=route, allowed_routes=team_member_permissions):
127 raise ProxyException(
128 message=f"Team member does not have permissions for endpoint: {route}. You only have access to the following endpoints: {team_member_permissions} for team {team_table.team_id}. To create keys for this team, please ask your proxy admin to check the team member permission settings and update the settings to allow team member users to create keys.",
129 type=ProxyErrorTypes.team_member_permission_error,
130 param=route,
131 code=401,
132 )
134 return True
136 @staticmethod
137 def enforce_member_can_assign_access_groups(
138 user_api_key_dict: UserAPIKeyAuth,
139 team_table: LiteLLM_TeamTableCachedObj | None,
140 access_group_ids: list[str] | None,
141 ) -> None:
142 """
143 Field-level opt-in gate: a non-admin team member may only set
144 `access_group_ids` on a (team) key if their team has opted in by adding
145 `KEY_ACCESS_GROUP_ASSIGNMENT` to `team_member_permissions`.
147 Bypassed for proxy admins, team admins, and personal (non-team) keys.
148 Default-deny: members cannot self-assign access groups until enabled.
150 Raises HTTPException(403) when a gated member attempts the assignment.
151 """
152 from fastapi import HTTPException
154 from litellm.proxy.management_endpoints.key_management_endpoints import (
155 _get_caller_team_role,
156 )
158 # No-op when the request does not assign any access groups.
159 if not access_group_ids:
160 return
162 # Proxy admins always bypass.
163 if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value: 163 ↛ 166line 163 didn't jump to line 166 because the condition on line 163 was always true
164 return
166 if team_table is None:
167 raise HTTPException(
168 status_code=403,
169 detail=(
170 "Key is not in a team. Access groups cannot be assigned to "
171 "personal keys by non-admin callers. Disallowed access groups: "
172 f"{sorted(access_group_ids)}."
173 ),
174 )
176 caller_team_role: Final = _get_caller_team_role(team_table=team_table, user_api_key_dict=user_api_key_dict)
178 # Team admins always bypass (consistent with other member-permission checks).
179 if caller_team_role == "admin":
180 return
182 permissions: Final = (
183 TeamMemberPermissionChecks._get_list_of_route_enum_as_str(
184 TeamMemberPermissionChecks.get_permissions_for_team_member(
185 team_table=team_table,
186 )
187 )
188 if caller_team_role is not None
189 else []
190 )
192 if KeyManagementRoutes.KEY_ACCESS_GROUP_ASSIGNMENT.value not in permissions:
193 raise HTTPException(
194 status_code=403,
195 detail=(
196 "Team members cannot assign access groups to keys for team "
197 f"{team_table.team_id}. Ask a team or proxy admin to enable the "
198 f"'{KeyManagementRoutes.KEY_ACCESS_GROUP_ASSIGNMENT.value}' team "
199 "member permission to allow this."
200 ),
201 )
203 @staticmethod
204 async def user_belongs_to_keys_team(
205 user_api_key_dict: UserAPIKeyAuth,
206 existing_key_row: LiteLLM_VerificationToken,
207 ) -> bool:
208 """
209 Returns True if the user belongs to the team that the key is assigned to
210 """
211 from litellm.proxy.management_endpoints.key_management_endpoints import (
212 _get_caller_team_role,
213 )
214 from litellm.proxy.proxy_server import prisma_client, user_api_key_cache
216 if existing_key_row.team_id is None:
217 return False
218 team_table: Final = await get_team_object(
219 team_id=existing_key_row.team_id,
220 prisma_client=prisma_client,
221 user_api_key_cache=user_api_key_cache,
222 parent_otel_span=user_api_key_dict.parent_otel_span,
223 check_db_only=True,
224 )
226 caller_team_role: Final = _get_caller_team_role(team_table=team_table, user_api_key_dict=user_api_key_dict)
227 return caller_team_role is not None
229 @staticmethod
230 def get_all_available_team_member_permissions() -> list[str]:
231 """
232 Returns all available team member permissions
233 """
234 all_available_permissions: Final = []
235 for route in LiteLLMRoutes.key_management_routes.value:
236 all_available_permissions.append(route)
237 return all_available_permissions
239 @staticmethod
240 def default_team_member_permissions() -> list[str]:
241 return [route.value for route in DEFAULT_TEAM_MEMBER_PERMISSIONS]