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

1from typing import Final, Literal 

2 

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 

17 

18BASELINE_TEAM_MEMBER_PERMISSIONS: Final = [ 

19 KeyManagementRoutes.KEY_INFO, 

20 KeyManagementRoutes.KEY_HEALTH, 

21] 

22 

23DEFAULT_TEAM_MEMBER_PERMISSIONS: Final = BASELINE_TEAM_MEMBER_PERMISSIONS 

24 

25 

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. 

33 

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) 

44 

45 return DEFAULT_TEAM_MEMBER_PERMISSIONS 

46 

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] 

55 

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 ) 

70 

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 

74 

75 # 2. Check if the operation is being done on a team key 

76 if existing_key_row.team_id is None: 

77 return 

78 

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 ) 

87 

88 caller_team_role: Final = _get_caller_team_role(team_table=team_table, user_api_key_dict=user_api_key_dict) 

89 

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 ) 

103 

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 """ 

113 

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 

120 

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) 

125 

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 ) 

133 

134 return True 

135 

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`. 

146 

147 Bypassed for proxy admins, team admins, and personal (non-team) keys. 

148 Default-deny: members cannot self-assign access groups until enabled. 

149 

150 Raises HTTPException(403) when a gated member attempts the assignment. 

151 """ 

152 from fastapi import HTTPException 

153 

154 from litellm.proxy.management_endpoints.key_management_endpoints import ( 

155 _get_caller_team_role, 

156 ) 

157 

158 # No-op when the request does not assign any access groups. 

159 if not access_group_ids: 

160 return 

161 

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 

165 

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 ) 

175 

176 caller_team_role: Final = _get_caller_team_role(team_table=team_table, user_api_key_dict=user_api_key_dict) 

177 

178 # Team admins always bypass (consistent with other member-permission checks). 

179 if caller_team_role == "admin": 

180 return 

181 

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 ) 

191 

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 ) 

202 

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 

215 

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 ) 

225 

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 

228 

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 

238 

239 @staticmethod 

240 def default_team_member_permissions() -> list[str]: 

241 return [route.value for route in DEFAULT_TEAM_MEMBER_PERMISSIONS]