Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/vector_store_endpoints/utils.py: 22%

225 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1import json 

2import re 

3from collections.abc import Iterable, Mapping 

4from types import MappingProxyType 

5from typing import Final, Literal 

6 

7from fastapi import HTTPException, Request 

8 

9import litellm 

10from litellm._logging import verbose_proxy_logger 

11from litellm.proxy._experimental.mcp_server.ui_session_utils import ( 

12 is_ui_session_credential, 

13 resolve_ui_session_team_ids, 

14) 

15from litellm.proxy._types import ( 

16 LiteLLM_ObjectPermissionTable, 

17 LitellmUserRoles, 

18 UserAPIKeyAuth, 

19) 

20from litellm.types.utils import LlmProviders 

21from litellm.types.vector_stores import LiteLLM_ManagedVectorStore 

22from litellm.utils import ProviderConfigManager 

23 

24 

25def _normalize_litellm_params( 

26 vector_store: LiteLLM_ManagedVectorStore, 

27) -> LiteLLM_ManagedVectorStore: 

28 litellm_params: Final = vector_store.get("litellm_params") 

29 if isinstance(litellm_params, str): 29 ↛ 30line 29 didn't jump to line 30 because the condition on line 29 was never true

30 normalized: Final = LiteLLM_ManagedVectorStore(**dict(vector_store)) 

31 try: 

32 parsed: Final = json.loads(litellm_params) 

33 normalized["litellm_params"] = parsed if isinstance(parsed, dict) else {} 

34 except (TypeError, ValueError): 

35 normalized["litellm_params"] = {} 

36 return normalized 

37 return vector_store 

38 

39 

40def _is_proxy_admin(user_api_key_dict: UserAPIKeyAuth) -> bool: 

41 return ( 

42 user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN 

43 or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value 

44 ) 

45 

46 

47def assert_proxy_admin_for_vector_store_index_management( 

48 user_api_key_dict: UserAPIKeyAuth, 

49 *, 

50 operation: Literal["create", "delete", "update", "list"] = "create", 

51) -> None: 

52 """Raise 403 unless the caller is a proxy admin.""" 

53 if _is_proxy_admin(user_api_key_dict): 53 ↛ 55line 53 didn't jump to line 55 because the condition on line 53 was always true

54 return 

55 raise HTTPException( 

56 status_code=403, 

57 detail=(f"Only proxy admins can {operation} vector store indexes. Contact your LiteLLM administrator."), 

58 ) 

59 

60 

61def _suffix_after_index_name(request_path: str, index_name: str) -> str | None: 

62 """Return the path suffix after ``/indexes/{index_name}``, or None if absent.""" 

63 match: Final = re.search(rf"/indexes/{re.escape(index_name)}(?=$|[/?])", request_path) 

64 if match is None: 

65 return None 

66 return request_path[match.end() :] 

67 

68 

69def _is_vector_store_index_lifecycle_request( 

70 request_method: str, 

71 request_path: str, 

72 index_name: str, 

73) -> bool: 

74 """ 

75 True when the request creates or deletes a search index itself (not documents). 

76 

77 Examples (admin-only): 

78 - DELETE /azure_ai/indexes/my-index 

79 - PUT /azure_ai/indexes/my-index 

80 - POST /azure_ai/indexes 

81 """ 

82 if request_method not in ("POST", "PUT", "DELETE", "PATCH"): 

83 return False 

84 

85 suffix: Final = _suffix_after_index_name(request_path, index_name) 

86 if suffix is not None: 

87 # Document operations live under /indexes/{name}/docs/... 

88 if suffix.startswith("/docs"): 

89 return False 

90 # DELETE/PUT/PATCH on /indexes/{name} itself is index lifecycle. 

91 if suffix == "" or suffix.startswith("?"): 

92 return True 

93 

94 # POST /indexes (create index at service level; no index name in path). 

95 normalized: Final = request_path.split("?", 1)[0].rstrip("/") 

96 if request_method == "POST" and normalized.endswith("/indexes"): 

97 return True 

98 

99 return False 

100 

101 

102def _object_permission_allows_vector_store( 

103 object_permission: LiteLLM_ObjectPermissionTable | None, 

104 vector_store_id: str, 

105) -> bool: 

106 """Returns True if an object permission explicitly allowlists the vector store.""" 

107 if object_permission is None: 

108 return False 

109 allowed: Final = object_permission.vector_stores 

110 if not allowed: 

111 return False 

112 return vector_store_id in allowed 

113 

114 

115async def _get_object_permission_for_id( 

116 object_permission_id: str | None, 

117) -> LiteLLM_ObjectPermissionTable | None: 

118 """Load an object permission record by id, using the shared cache/DB helper.""" 

119 if not object_permission_id: 

120 return None 

121 

122 from litellm.proxy.auth.auth_checks import get_object_permission 

123 from litellm.proxy.proxy_server import ( 

124 prisma_client, 

125 proxy_logging_obj, 

126 user_api_key_cache, 

127 ) 

128 

129 if prisma_client is None: 

130 return None 

131 

132 try: 

133 return await get_object_permission( 

134 object_permission_id=object_permission_id, 

135 prisma_client=prisma_client, 

136 user_api_key_cache=user_api_key_cache, 

137 proxy_logging_obj=proxy_logging_obj, 

138 ) 

139 except Exception as e: 

140 verbose_proxy_logger.debug( 

141 "Failed to load object_permission id=%s: %s", 

142 object_permission_id, 

143 e, 

144 ) 

145 return None 

146 

147 

148async def can_user_access_vector_store( 

149 vector_store: LiteLLM_ManagedVectorStore, 

150 user_api_key_dict: UserAPIKeyAuth, 

151) -> bool: 

152 """ 

153 Returns True if the caller is allowed to access this managed vector store. 

154 

155 Access is granted (first match wins) when any of the following is true: 

156 1. The caller's role is PROXY_ADMIN. 

157 2. The vector store has no team_id (legacy behavior - accessible to all). 

158 3. The caller's key-level object_permission.vector_stores explicitly lists 

159 this vector store id. 

160 4. The caller's team-level object_permission.vector_stores explicitly lists 

161 this vector store id. 

162 5. The caller's team_id matches the vector store's team_id. 

163 

164 A dashboard session credential is evaluated against the same effective 

165 contexts as listing (its own grants plus each real team of the user). 

166 Otherwise access is denied. 

167 """ 

168 if _is_proxy_admin(user_api_key_dict): 168 ↛ 171line 168 didn't jump to line 171 because the condition on line 168 was always true

169 return True 

170 

171 if vector_store.get("team_id") is None: 

172 return True 

173 

174 auth_contexts: Final = await _vector_store_auth_contexts(user_api_key_dict) 

175 return await _is_vector_store_granted_to_any(vector_store, auth_contexts) 

176 

177 

178async def _is_vector_store_granted( 

179 vector_store: LiteLLM_ManagedVectorStore, 

180 user_api_key_dict: UserAPIKeyAuth, 

181) -> bool: 

182 vector_store_id: Final = vector_store.get("vector_store_id") or "" 

183 

184 key_object_permission = user_api_key_dict.object_permission 

185 if key_object_permission is None: 

186 key_object_permission = await _get_object_permission_for_id(user_api_key_dict.object_permission_id) 

187 if _object_permission_allows_vector_store(key_object_permission, vector_store_id): 

188 return True 

189 

190 team_object_permission: LiteLLM_ObjectPermissionTable | None = user_api_key_dict.team_object_permission 

191 if team_object_permission is None: 

192 team_object_permission = await _get_object_permission_for_id(user_api_key_dict.team_object_permission_id) 

193 if _object_permission_allows_vector_store(team_object_permission, vector_store_id): 

194 return True 

195 

196 return user_api_key_dict.team_id is not None and user_api_key_dict.team_id == vector_store.get("team_id") 

197 

198 

199async def _team_auth_context(team_id: str, user_api_key_dict: UserAPIKeyAuth) -> UserAPIKeyAuth: 

200 from litellm.proxy.auth.auth_checks import get_team_object 

201 from litellm.proxy.proxy_server import ( 

202 prisma_client, 

203 proxy_logging_obj, 

204 user_api_key_cache, 

205 ) 

206 

207 team: Final = await get_team_object( 

208 team_id=team_id, 

209 prisma_client=prisma_client, 

210 user_api_key_cache=user_api_key_cache, 

211 parent_otel_span=user_api_key_dict.parent_otel_span, 

212 proxy_logging_obj=proxy_logging_obj, 

213 ) 

214 return user_api_key_dict.model_copy( 

215 update=MappingProxyType( 

216 { 

217 "team_id": team_id, 

218 "team_object_permission": team.object_permission, 

219 "team_object_permission_id": team.object_permission_id, 

220 } 

221 ) 

222 ) 

223 

224 

225async def _vector_store_auth_contexts( 

226 user_api_key_dict: UserAPIKeyAuth, 

227) -> tuple[UserAPIKeyAuth, ...]: 

228 if not is_ui_session_credential(user_api_key_dict): 

229 return (user_api_key_dict,) 

230 session_key_context: Final = user_api_key_dict.model_copy( 

231 update=MappingProxyType({"team_id": None, "team_object_permission": None, "team_object_permission_id": None}) 

232 ) 

233 team_ids: Final = await resolve_ui_session_team_ids(user_api_key_dict) 

234 team_contexts: Final = tuple([await _team_auth_context(team_id, user_api_key_dict) for team_id in team_ids]) 

235 return (session_key_context, *team_contexts) 

236 

237 

238async def _is_vector_store_granted_to_any( 

239 vector_store: LiteLLM_ManagedVectorStore, 

240 auth_contexts: tuple[UserAPIKeyAuth, ...], 

241) -> bool: 

242 for auth_context in auth_contexts: 

243 if await _is_vector_store_granted(vector_store, auth_context): 

244 return True 

245 return False 

246 

247 

248async def filter_listable_vector_stores( 

249 vector_stores: Iterable[LiteLLM_ManagedVectorStore], 

250 user_api_key_dict: UserAPIKeyAuth, 

251) -> tuple[LiteLLM_ManagedVectorStore, ...]: 

252 """Non-admins only see stores their key, one of their teams' object_permission, or team ownership grants.""" 

253 if _is_proxy_admin(user_api_key_dict): 253 ↛ 256line 253 didn't jump to line 256 because the condition on line 253 was always true

254 return tuple(vector_stores) 

255 

256 auth_contexts: Final = await _vector_store_auth_contexts(user_api_key_dict) 

257 return tuple([vs for vs in vector_stores if await _is_vector_store_granted_to_any(vs, auth_contexts)]) 

258 

259 

260async def get_litellm_managed_vector_store( 

261 vector_store_id: str, 

262) -> LiteLLM_ManagedVectorStore | None: 

263 """ 

264 Resolve a LiteLLM-managed vector store from the registry or shared cache. 

265 

266 Provider-native vector store IDs will not be present in either location and 

267 return None, preserving direct provider behavior while still protecting 

268 LiteLLM-managed multi-tenant stores. 

269 """ 

270 if not vector_store_id: 270 ↛ 271line 270 didn't jump to line 271 because the condition on line 270 was never true

271 return None 

272 

273 if litellm.vector_store_registry is not None: 

274 try: 

275 vector_store: Final = litellm.vector_store_registry.get_litellm_managed_vector_store_from_registry( 

276 vector_store_id=vector_store_id 

277 ) 

278 if vector_store is not None: 

279 return _normalize_litellm_params(vector_store) 

280 except Exception as e: 

281 verbose_proxy_logger.warning( 

282 "Failed to resolve vector store id=%s from registry: %s", 

283 vector_store_id, 

284 e, 

285 ) 

286 raise HTTPException( 

287 status_code=500, 

288 detail="Unable to validate vector store access", 

289 ) from e 

290 

291 try: 

292 from litellm.proxy.auth.auth_checks import ( 

293 get_managed_vector_store_rows_by_uuids, 

294 ) 

295 from litellm.proxy.proxy_server import ( 

296 prisma_client, 

297 proxy_logging_obj, 

298 user_api_key_cache, 

299 ) 

300 

301 if prisma_client is None: 301 ↛ 302line 301 didn't jump to line 302 because the condition on line 301 was never true

302 return None 

303 rows: Final = await get_managed_vector_store_rows_by_uuids( 

304 uuids=[vector_store_id], 

305 prisma_client=prisma_client, 

306 user_api_key_cache=user_api_key_cache, 

307 proxy_logging_obj=proxy_logging_obj, 

308 ) 

309 if not rows: 

310 return None 

311 return _normalize_litellm_params(LiteLLM_ManagedVectorStore(**rows[0].model_dump())) 

312 except Exception as e: 

313 verbose_proxy_logger.warning( 

314 "Failed to resolve vector store id=%s from shared cache: %s", 

315 vector_store_id, 

316 e, 

317 ) 

318 raise HTTPException( 

319 status_code=500, 

320 detail="Unable to validate vector store access", 

321 ) from e 

322 

323 

324async def assert_user_can_access_vector_store( 

325 vector_store: LiteLLM_ManagedVectorStore, 

326 user_api_key_dict: UserAPIKeyAuth, 

327 detail: str = "Access denied: You do not have permission to access this vector store", 

328) -> None: 

329 """Raise 403 unless the caller can access the resolved vector store.""" 

330 if not await can_user_access_vector_store(vector_store, user_api_key_dict): 330 ↛ 331line 330 didn't jump to line 331 because the condition on line 330 was never true

331 raise HTTPException(status_code=403, detail=detail) 

332 

333 

334async def assert_user_can_access_vector_store_id( 

335 vector_store_id: str, 

336 user_api_key_dict: UserAPIKeyAuth, 

337 detail: str = "Access denied: You do not have permission to access this vector store", 

338) -> LiteLLM_ManagedVectorStore | None: 

339 """ 

340 Resolve a managed vector store id and enforce ownership if it exists. 

341 

342 Unknown ids are treated as provider-native ids and are not rejected here. 

343 """ 

344 vector_store: Final = await get_litellm_managed_vector_store(vector_store_id=vector_store_id) 

345 if vector_store is not None: 

346 await assert_user_can_access_vector_store( 

347 vector_store=vector_store, 

348 user_api_key_dict=user_api_key_dict, 

349 detail=detail, 

350 ) 

351 return vector_store 

352 

353 

354def _does_endpoint_match(endpoint_path: str, request_path: str) -> bool: 

355 if endpoint_path in request_path: 

356 return True 

357 if "{" in endpoint_path: 

358 prefix: Final = endpoint_path.split("{", 1)[0] 

359 if prefix and prefix in request_path: 

360 return True 

361 return False 

362 

363 

364def check_vector_store_permission( 

365 index_name: str, 

366 permission: str, 

367 key_metadata: Mapping[str, object] | None, 

368 team_metadata: Mapping[str, object] | None, 

369) -> bool: 

370 """ 

371 Check if a specific permission is allowed for a given vector store index. 

372 

373 Args: 

374 index_name: The name of the vector store index 

375 permission: The permission to check (e.g., "read", "write") 

376 key_metadata: Metadata from the API key 

377 team_metadata: Metadata from the team 

378 

379 Returns: 

380 True if the permission is allowed, False otherwise 

381 

382 Example metadata format: 

383 "metadata": { 

384 "allowed_vector_store_indexes": [ 

385 { 

386 "index_name": "dall-e-3", 

387 "index_permissions": ["write"] 

388 } 

389 ] 

390 } 

391 """ 

392 # Check both key_metadata and team_metadata 

393 for metadata in [key_metadata, team_metadata]: 

394 if metadata is None: 

395 continue 

396 

397 allowed_indexes = metadata.get("allowed_vector_store_indexes") 

398 if not allowed_indexes or not isinstance(allowed_indexes, list): 

399 continue 

400 

401 # Look for matching index 

402 for index_config in allowed_indexes: 

403 if not isinstance(index_config, dict): 

404 continue 

405 

406 if index_config.get("index_name") == index_name: 

407 index_permissions = index_config.get("index_permissions", []) 

408 if isinstance(index_permissions, list) and permission in index_permissions: 

409 return True 

410 

411 return False 

412 

413 

414def is_allowed_to_call_vector_store_endpoint( 

415 provider: LlmProviders, 

416 index_name: str, 

417 request: Request, 

418 user_api_key_dict: UserAPIKeyAuth, 

419) -> Literal[True] | None: 

420 """ 

421 Check if the user is allowed to call the vector store endpoint. 

422 

423 Cover: 

424 1. Creating a vector store index 

425 2. Reading a vector store index (Search / List / Get) 

426 """ 

427 if ( 

428 user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN 

429 or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value 

430 ): 

431 return True 

432 # check what allowed permissions are for the key 

433 key_metadata: Final = user_api_key_dict.metadata 

434 team_metadata: Final = user_api_key_dict.team_metadata 

435 

436 provider_config: Final = ProviderConfigManager.get_provider_vector_stores_config(provider=provider) 

437 if provider_config is None: 

438 return None 

439 

440 provider_vector_store_endpoints: Final = provider_config.get_vector_store_endpoints_by_type() 

441 

442 # Inline import — auth_utils participates in a proxy import cycle. 

443 from litellm.proxy.auth.auth_utils import get_request_route # noqa: PLC0415 

444 

445 request_route: Final = get_request_route(request) 

446 

447 if _is_vector_store_index_lifecycle_request( 

448 request_method=request.method, 

449 request_path=request_route, 

450 index_name=index_name, 

451 ): 

452 operation_label: Literal["create", "delete", "update"] = "create" 

453 if request.method == "DELETE": 

454 operation_label = "delete" 

455 elif request.method in ("PUT", "PATCH"): 

456 operation_label = "update" 

457 assert_proxy_admin_for_vector_store_index_management( 

458 user_api_key_dict, 

459 operation=operation_label, 

460 ) 

461 return True 

462 

463 # Writes are classified before reads so a path matching both patterns 

464 # requires the stronger grant (e.g. the azure batch write on an index 

465 # named "analyze*" also contains the "/analyze" read fragment) 

466 permission_type = None 

467 for endpoint in provider_vector_store_endpoints["write"]: 

468 if request.method == endpoint[0] and _does_endpoint_match(endpoint[1], request_route): 

469 permission_type = "write" 

470 break 

471 

472 if permission_type is None: 

473 for endpoint in provider_vector_store_endpoints["read"]: 

474 if request.method == endpoint[0] and _does_endpoint_match(endpoint[1], request_route): 

475 permission_type = "read" 

476 break 

477 

478 if permission_type is None: 

479 raise HTTPException( 

480 status_code=403, 

481 detail=( 

482 f"User does not have permission to call vector store endpoint " 

483 f"{index_name}. Ask your administrator to add the necessary " 

484 "permissions to your API key/Team." 

485 ), 

486 ) 

487 

488 # Check if key has specific permission for allowed_vector_store_indexes 

489 has_permission: Final = check_vector_store_permission( 

490 index_name=index_name, 

491 permission=permission_type, 

492 key_metadata=key_metadata, 

493 team_metadata=team_metadata, 

494 ) 

495 

496 if not has_permission: 

497 raise HTTPException( 

498 status_code=403, 

499 detail=f"User does not have permission to call vector store endpoint {index_name}. Ask your administrator to add the necessary permissions to your API key/Team.", 

500 ) 

501 

502 return has_permission 

503 

504 

505def is_allowed_to_call_vector_store_files_endpoint( 

506 provider: LlmProviders, 

507 vector_store_id: str, 

508 request: Request, 

509 user_api_key_dict: UserAPIKeyAuth, 

510) -> Literal[True] | None: 

511 if ( 

512 user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN 

513 or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value 

514 ): 

515 return True 

516 

517 key_metadata: Final = user_api_key_dict.metadata 

518 team_metadata: Final = user_api_key_dict.team_metadata 

519 

520 provider_config: Final = ProviderConfigManager.get_provider_vector_store_files_config(provider=provider) 

521 if provider_config is None: 

522 return None 

523 

524 provider_vector_store_endpoints: Final = provider_config.get_vector_store_file_endpoints_by_type() 

525 

526 # Inline import — auth_utils participates in a proxy import cycle. 

527 from litellm.proxy.auth.auth_utils import get_request_route # noqa: PLC0415 

528 

529 request_route: Final = get_request_route(request) 

530 

531 permission_type: str | None = None 

532 for endpoint in provider_vector_store_endpoints.get("write", ()): 

533 if request.method == endpoint[0] and _does_endpoint_match(endpoint[1], request_route): 

534 permission_type = "write" 

535 break 

536 

537 if permission_type is None: 

538 for endpoint in provider_vector_store_endpoints.get("read", ()): 

539 if request.method == endpoint[0] and _does_endpoint_match(endpoint[1], request_route): 

540 permission_type = "read" 

541 break 

542 

543 if permission_type is None: 

544 return None 

545 

546 has_permission: Final = check_vector_store_permission( 

547 index_name=vector_store_id, 

548 permission=permission_type, 

549 key_metadata=key_metadata, 

550 team_metadata=team_metadata, 

551 ) 

552 

553 if not has_permission: 

554 raise HTTPException( 

555 status_code=403, 

556 detail=f"User does not have permission to call vector store file endpoint {vector_store_id}. Ask your administrator to add the necessary permissions to your API key/Team.", 

557 ) 

558 

559 return has_permission