mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-12 03:48:56 +00:00
* feat(proxy): add opt-in vector_store_deny_by_default for standalone virtual keys
Adds general_settings.vector_store_deny_by_default (typed bool, default false). When enabled, a virtual key
with no team must list the requested vector store in its object_permission.vector_stores; no permission
record, null, or an empty list is denied with key_vector_store_access_denied. Omitted or false keeps the
existing behavior, including nonempty allowlist enforcement. The master key is unchanged in both modes.
Team keys and keyless callers are deferred to later increments of LIT-6035
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat(proxy): require key and team vector store grants for team keys under vector_store_deny_by_default
With the flag enabled, a virtual key on a team needs both its own grant and its team's grant for every requested vector store. A missing permission record, an empty list or an unresolved team grants nothing. Dashboard session keys and the master key keep their existing behavior, and flag-off behavior is unchanged
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* feat(proxy): require user or team vector store grants for keyless requests under vector_store_deny_by_default
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test(proxy): cover vector_store_deny_by_default through a real proxy with key, team and JWT identities
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* chore(ui): regenerate schema.d.ts for vector_store_deny_by_default
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(auth): cover path and file_search vector store ids under vector_store_deny_by_default
Strict mode now reads vector_store_ids and tools[].vector_store_ids from the request body without needing a vector store registry, so /v1/vector_stores/{id}/search and Responses file_search are checked. User grants load through the object permission cache, and the proxy admin user rebuild keeps object_permission_id
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(proxy): broadcast user entitlement cache eviction to every worker
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* refactor(auth): reuse VectorStoreRegistry id extraction for vector_store_deny_by_default
Strict mode now uses get_vector_store_ids_to_run on an empty registry when none is loaded, instead of a parallel set of request-shape helpers, and vector_store_access_check documents the policy
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test(integration): add strict vector store audit cells for routes, SDKs, workers and concurrency
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test(integration): consolidate vector_store_deny_by_default coverage to core cases
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(proxy): reject invalid vector_store_deny_by_default at config load and return 400 for malformed vector_store_ids
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(auth): read vector store key and team grants through the object permission cache
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* test(auth): return typed permission rows in request flow vector store tests
Co-authored-by: mrinal <mrinal@berri.ai>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* refactor(auth): validate vector store ids and tools as immutable sequences
Co-authored-by: mrinal <mrinal@berri.ai>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: mrinal <mrinal@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
74 lines
3.1 KiB
Python
74 lines
3.1 KiB
Python
import time
|
|
from typing import Final
|
|
|
|
import pytest
|
|
|
|
from tests.integration._support.client import Gateway, eventually
|
|
from tests.integration._support.database import read_rows
|
|
|
|
PLAIN_REQUESTS: Final = 10
|
|
STATS_FLUSH_WINDOW_SECONDS: Final = 11.0
|
|
|
|
|
|
def _object_permission_reads() -> int:
|
|
rows: Final = read_rows(
|
|
"SELECT seq_scan + idx_scan AS reads FROM pg_stat_user_tables WHERE relname = %s",
|
|
("LiteLLM_ObjectPermissionTable",),
|
|
)
|
|
reads: Final = rows[0]["reads"]
|
|
assert isinstance(reads, int), rows
|
|
return reads
|
|
|
|
|
|
def _settled_object_permission_reads(previous: int, unchanged_since: float) -> int:
|
|
changed: Final = eventually(
|
|
lambda: _object_permission_reads() != previous,
|
|
lambda drifted: drifted,
|
|
seconds=STATS_FLUSH_WINDOW_SECONDS - (time.monotonic() - unchanged_since),
|
|
return_last_on_timeout=True,
|
|
)
|
|
if not changed:
|
|
return previous
|
|
return _settled_object_permission_reads(_object_permission_reads(), time.monotonic())
|
|
|
|
|
|
def _assert_plain_chat_served(gateway: Gateway, model: str, key: str) -> None:
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
{"model": model, "messages": [{"role": "user", "content": "no vector stores"}]},
|
|
key=key,
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json()["choices"][0]["message"]["content"] == (
|
|
"Hello! This is a mock response from the fake OpenAI endpoint."
|
|
)
|
|
|
|
|
|
def _assert_forbidden_vector_store_denied(gateway: Gateway, model: str, key: str) -> None:
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
{"model": model, "messages": [{"role": "user", "content": "forbidden"}], "vector_store_ids": ["vs_forbidden"]},
|
|
key=key,
|
|
)
|
|
assert response.status_code == 401, response.text
|
|
assert response.json()["error"]["type"] == "key_vector_store_access_denied", response.text
|
|
|
|
|
|
@pytest.mark.covers("authorization.vector_store.plain_request_skips_object_permission_lookup")
|
|
def test_requests_do_not_read_object_permission_table_once_the_grant_is_cached(gateway: Gateway) -> None:
|
|
with gateway.scenario() as scenario:
|
|
model: Final = scenario.model()
|
|
key: Final = scenario.key(models=[model], object_permission={"vector_stores": ["vs_allowed"]})
|
|
before_first_use: Final = _object_permission_reads()
|
|
_assert_forbidden_vector_store_denied(gateway, model, key)
|
|
eventually(_object_permission_reads, lambda reads: reads > before_first_use, seconds=15)
|
|
baseline: Final = _settled_object_permission_reads(_object_permission_reads(), time.monotonic())
|
|
for _ in range(PLAIN_REQUESTS):
|
|
_assert_plain_chat_served(gateway, model, key)
|
|
_assert_forbidden_vector_store_denied(gateway, model, key)
|
|
after: Final = _settled_object_permission_reads(_object_permission_reads(), time.monotonic())
|
|
assert after - baseline < PLAIN_REQUESTS, (
|
|
f"{2 * PLAIN_REQUESTS} requests after the first added {after - baseline} object permission reads"
|
|
)
|