mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
fix(guardrails): Alice WonderFence — admin metadata wins over request metadata
Caller-supplied metadata.alice_wonderfence_app_id / alice_wonderfence_api_key no longer outrank admin-pinned key/team metadata. Adds allow_request_metadata_override (default False) as an explicit opt-in for trusted-gateway deployments — even when enabled, key/team metadata still wins. Closes the high-severity precedence inversion flagged on PR #26901 (review comment r3226452019). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d79dd99f63
commit
41278a27b1
5 changed files with 234 additions and 75 deletions
|
|
@ -46,6 +46,10 @@ def initialize_guardrail(
|
|||
init_kwargs["block_message"] = litellm_params.block_message
|
||||
if litellm_params.debug is not None:
|
||||
init_kwargs["debug"] = litellm_params.debug
|
||||
if litellm_params.allow_request_metadata_override is not None:
|
||||
init_kwargs["allow_request_metadata_override"] = (
|
||||
litellm_params.allow_request_metadata_override
|
||||
)
|
||||
|
||||
wonderfence_guardrail = WonderFenceGuardrail(**init_kwargs)
|
||||
|
||||
|
|
|
|||
|
|
@ -59,20 +59,27 @@ class WonderFenceBlockedError(Exception):
|
|||
class WonderFenceGuardrail(CustomGuardrail):
|
||||
"""Alice WonderFence guardrail handler using the V2 SDK client.
|
||||
|
||||
``api_key`` and ``app_id`` are resolved per request from request metadata,
|
||||
API-key metadata, or team metadata. ``api_key`` falls back to a configured
|
||||
default; ``app_id`` has no default and must be supplied per request.
|
||||
``api_key`` and ``app_id`` are resolved per request from API-key metadata,
|
||||
team metadata, optionally request metadata, with ``api_key`` falling back
|
||||
to a configured default. ``app_id`` has no default.
|
||||
|
||||
Resolution order for ``api_key``:
|
||||
1. Request metadata: ``metadata.alice_wonderfence_api_key``
|
||||
2. API key metadata: ``user_api_key_metadata.alice_wonderfence_api_key``
|
||||
3. Team metadata: ``user_api_key_team_metadata.alice_wonderfence_api_key``
|
||||
1. API key metadata: ``user_api_key_metadata.alice_wonderfence_api_key``
|
||||
2. Team metadata: ``user_api_key_team_metadata.alice_wonderfence_api_key``
|
||||
3. Request metadata: ``metadata.alice_wonderfence_api_key`` (only when
|
||||
``allow_request_metadata_override=True``)
|
||||
4. Default: configured ``api_key`` or ``ALICE_API_KEY`` env var
|
||||
|
||||
Resolution order for ``app_id`` (no default — error if missing):
|
||||
1. Request metadata: ``metadata.alice_wonderfence_app_id``
|
||||
2. API key metadata: ``user_api_key_metadata.alice_wonderfence_app_id``
|
||||
3. Team metadata: ``user_api_key_team_metadata.alice_wonderfence_app_id``
|
||||
1. API key metadata: ``user_api_key_metadata.alice_wonderfence_app_id``
|
||||
2. Team metadata: ``user_api_key_team_metadata.alice_wonderfence_app_id``
|
||||
3. Request metadata: ``metadata.alice_wonderfence_app_id`` (only when
|
||||
``allow_request_metadata_override=True``)
|
||||
|
||||
Admin-pinned credentials (key/team metadata) always win over request
|
||||
metadata so a caller cannot bypass their assigned WonderFence app.
|
||||
``allow_request_metadata_override`` defaults to False; enable only for
|
||||
trusted-gateway deployments that need request-level overrides.
|
||||
|
||||
A V2 SDK client is cached per resolved ``api_key`` (LRU).
|
||||
"""
|
||||
|
|
@ -89,6 +96,7 @@ class WonderFenceGuardrail(CustomGuardrail):
|
|||
debug: bool = False,
|
||||
max_cached_clients: Optional[int] = None,
|
||||
connection_pool_limit: Optional[int] = None,
|
||||
allow_request_metadata_override: bool = False,
|
||||
event_hook: Optional[
|
||||
Union[GuardrailEventHooks, List[GuardrailEventHooks], Mode]
|
||||
] = None,
|
||||
|
|
@ -113,6 +121,11 @@ class WonderFenceGuardrail(CustomGuardrail):
|
|||
keyed by api_key). Default 10. Env: ALICE_MAX_CACHED_CLIENTS.
|
||||
connection_pool_limit: Max connections per SDK client HTTP pool.
|
||||
Env: ALICE_CONNECTION_POOL_LIMIT.
|
||||
allow_request_metadata_override: When True, allow per-request
|
||||
``metadata.alice_wonderfence_api_key`` /
|
||||
``metadata.alice_wonderfence_app_id`` as a last-resort source
|
||||
(after API-key and team metadata). Defaults to False so
|
||||
caller-controlled fields cannot bypass admin-pinned credentials.
|
||||
event_hook: Event hook mode.
|
||||
default_on: Whether the guardrail is enabled by default.
|
||||
"""
|
||||
|
|
@ -142,6 +155,7 @@ class WonderFenceGuardrail(CustomGuardrail):
|
|||
self.platform = platform
|
||||
self.fail_open = fail_open
|
||||
self.block_message = block_message
|
||||
self.allow_request_metadata_override = allow_request_metadata_override
|
||||
|
||||
if debug:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
|
@ -216,7 +230,13 @@ class WonderFenceGuardrail(CustomGuardrail):
|
|||
)
|
||||
|
||||
def _resolve_api_key(self, request_data: dict) -> str:
|
||||
"""Resolve api_key from request → key → team metadata, falling back to default.
|
||||
"""Resolve api_key from key → team → (request, when opt-in) → default.
|
||||
|
||||
Admin-pinned sources (API-key and team metadata) take precedence over
|
||||
request-body metadata so a caller cannot bypass their assigned
|
||||
WonderFence credentials. Request metadata is consulted only when
|
||||
``allow_request_metadata_override`` is True, and even then only after
|
||||
the admin-controlled sources.
|
||||
|
||||
The LiteLLM framework copies key/team metadata from ``UserAPIKeyAuth``
|
||||
into ``data['metadata']`` under ``user_api_key_metadata`` and
|
||||
|
|
@ -225,10 +245,6 @@ class WonderFenceGuardrail(CustomGuardrail):
|
|||
"""
|
||||
metadata = self._get_metadata(request_data)
|
||||
|
||||
req_api_key = metadata.get("alice_wonderfence_api_key")
|
||||
if req_api_key:
|
||||
return req_api_key
|
||||
|
||||
key_metadata = metadata.get("user_api_key_metadata") or {}
|
||||
if isinstance(key_metadata, dict) and key_metadata.get(
|
||||
"alice_wonderfence_api_key"
|
||||
|
|
@ -241,21 +257,28 @@ class WonderFenceGuardrail(CustomGuardrail):
|
|||
):
|
||||
return team_metadata["alice_wonderfence_api_key"]
|
||||
|
||||
if self.allow_request_metadata_override:
|
||||
req_api_key = metadata.get("alice_wonderfence_api_key")
|
||||
if req_api_key:
|
||||
return req_api_key
|
||||
|
||||
if self.api_key:
|
||||
return self.api_key
|
||||
|
||||
raise WonderFenceMissingSecrets(
|
||||
"No alice_wonderfence_api_key found in request metadata, API-key "
|
||||
"metadata, team metadata, or default config (ALICE_API_KEY)."
|
||||
"No alice_wonderfence_api_key found in API-key metadata, team "
|
||||
"metadata, request metadata (when allow_request_metadata_override "
|
||||
"is enabled), or default config (ALICE_API_KEY)."
|
||||
)
|
||||
|
||||
def _resolve_app_id(self, request_data: dict) -> str:
|
||||
"""Resolve app_id from request → key → team metadata. No default — raise if missing."""
|
||||
metadata = self._get_metadata(request_data)
|
||||
"""Resolve app_id from key → team → (request, when opt-in). No default.
|
||||
|
||||
req_app_id = metadata.get("alice_wonderfence_app_id")
|
||||
if req_app_id:
|
||||
return req_app_id
|
||||
Admin-pinned sources win over request-body metadata; request metadata
|
||||
is only consulted when ``allow_request_metadata_override`` is True.
|
||||
Raises ``WonderFenceMissingSecrets`` when nothing resolves.
|
||||
"""
|
||||
metadata = self._get_metadata(request_data)
|
||||
|
||||
key_metadata = metadata.get("user_api_key_metadata") or {}
|
||||
if isinstance(key_metadata, dict) and key_metadata.get(
|
||||
|
|
@ -269,9 +292,15 @@ class WonderFenceGuardrail(CustomGuardrail):
|
|||
):
|
||||
return team_metadata["alice_wonderfence_app_id"]
|
||||
|
||||
if self.allow_request_metadata_override:
|
||||
req_app_id = metadata.get("alice_wonderfence_app_id")
|
||||
if req_app_id:
|
||||
return req_app_id
|
||||
|
||||
raise WonderFenceMissingSecrets(
|
||||
"No alice_wonderfence_app_id found in request metadata, API-key "
|
||||
"metadata, or team metadata. app_id must be provided per request."
|
||||
"No alice_wonderfence_app_id found in API-key metadata, team "
|
||||
"metadata, or request metadata (when allow_request_metadata_override "
|
||||
"is enabled). app_id must be provided per request."
|
||||
)
|
||||
|
||||
def _build_analysis_context(self, request_data: dict) -> Any:
|
||||
|
|
|
|||
|
|
@ -9,9 +9,15 @@
|
|||
# ALICE_CONNECTION_POOL_LIMIT - Optional: HTTP pool size per client
|
||||
# OPENAI_API_KEY - API key for OpenAI
|
||||
#
|
||||
# Per-request / per-key / per-team metadata keys:
|
||||
# Per-key / per-team metadata keys (admin-controlled):
|
||||
# alice_wonderfence_api_key - overrides default API key (optional)
|
||||
# alice_wonderfence_app_id - REQUIRED — must be set on request, key, or team
|
||||
# alice_wonderfence_app_id - REQUIRED — must be set on key or team
|
||||
#
|
||||
# Per-request metadata keys (caller-controlled, OFF by default):
|
||||
# metadata.alice_wonderfence_api_key / metadata.alice_wonderfence_app_id are
|
||||
# ignored unless allow_request_metadata_override is True on the guardrail.
|
||||
# Even when enabled, key/team metadata still wins — request metadata is a
|
||||
# last-resort source only.
|
||||
|
||||
model_list:
|
||||
- model_name: gpt-4
|
||||
|
|
@ -22,7 +28,7 @@ model_list:
|
|||
guardrails:
|
||||
|
||||
# Combined pre + post with advanced knobs
|
||||
- guardrail_name: "alice-wonderfence-full-guard"
|
||||
- guardrail_name: "alice-wonderfence"
|
||||
litellm_params:
|
||||
guardrail: alice_wonderfence
|
||||
mode: ["pre_call", "post_call"]
|
||||
|
|
@ -37,9 +43,41 @@ guardrails:
|
|||
|
||||
# connection_pool_limit: 20
|
||||
|
||||
# Enable only for trusted-gateway deployments that need to forward a
|
||||
# per-tenant app_id/api_key from the request body. Even with this on,
|
||||
# admin-pinned key/team metadata still wins; request metadata is a
|
||||
# last-resort source only. Default is False — leave it off unless your
|
||||
# callers are themselves trusted infrastructure.
|
||||
allow_request_metadata_override: true
|
||||
|
||||
# Example usage
|
||||
#
|
||||
# 1. Request-level app_id override (every request must supply app_id somewhere):
|
||||
# 1. Per-API-key app_id (set at key creation — recommended default):
|
||||
#
|
||||
# curl -X POST http://localhost:4000/key/generate \
|
||||
# -H "Authorization: Bearer sk-admin" \
|
||||
# -H "Content-Type: application/json" \
|
||||
# -d '{
|
||||
# "metadata": {
|
||||
# "alice_wonderfence_app_id": "tenant-A-app",
|
||||
# "alice_wonderfence_api_key": "wf-key-for-tenant-A"
|
||||
# }
|
||||
# }'
|
||||
#
|
||||
# 2. Per-team app_id (set at team creation):
|
||||
#
|
||||
# curl -X POST http://localhost:4000/team/new \
|
||||
# -H "Authorization: Bearer sk-admin" \
|
||||
# -H "Content-Type: application/json" \
|
||||
# -d '{
|
||||
# "team_alias": "team-billing",
|
||||
# "metadata": {
|
||||
# "alice_wonderfence_app_id": "team-billing-app"
|
||||
# }
|
||||
# }'
|
||||
#
|
||||
# 3. Request-level app_id (only when allow_request_metadata_override: true on
|
||||
# the guardrail config — and even then, key/team metadata still wins):
|
||||
#
|
||||
# curl -X POST http://localhost:4000/chat/completions \
|
||||
# -H "Authorization: Bearer sk-xxx" \
|
||||
|
|
@ -53,29 +91,7 @@ guardrails:
|
|||
# }
|
||||
# }'
|
||||
#
|
||||
# 2. Per-API-key app_id (set at key creation, no per-request metadata needed):
|
||||
#
|
||||
# curl -X POST http://localhost:4000/key/generate \
|
||||
# -H "Authorization: Bearer sk-admin" \
|
||||
# -H "Content-Type: application/json" \
|
||||
# -d '{
|
||||
# "metadata": {
|
||||
# "alice_wonderfence_app_id": "tenant-A-app",
|
||||
# "alice_wonderfence_api_key": "wf-key-for-tenant-A"
|
||||
# }
|
||||
# }'
|
||||
#
|
||||
# 3. Per-team app_id (set at team creation):
|
||||
#
|
||||
# curl -X POST http://localhost:4000/team/new \
|
||||
# -H "Authorization: Bearer sk-admin" \
|
||||
# -H "Content-Type: application/json" \
|
||||
# -d '{
|
||||
# "team_alias": "team-billing",
|
||||
# "metadata": {
|
||||
# "alice_wonderfence_app_id": "team-billing-app"
|
||||
# }
|
||||
# }'
|
||||
#
|
||||
# Resolution priority (highest first): request metadata > key metadata > team metadata > config default.
|
||||
# api_key falls back to config / ALICE_API_KEY env. app_id has NO default.
|
||||
# Resolution priority (highest first): key metadata > team metadata >
|
||||
# request metadata (only if allow_request_metadata_override=true) >
|
||||
# config default. api_key falls back to config / ALICE_API_KEY env;
|
||||
# app_id has NO default.
|
||||
|
|
|
|||
|
|
@ -10,15 +10,25 @@ from .base import GuardrailConfigModel
|
|||
class WonderFenceGuardrailConfigModel(GuardrailConfigModel):
|
||||
"""Configuration parameters for the Alice WonderFence guardrail.
|
||||
|
||||
Per-request ``api_key`` and ``app_id`` are read from request / API-key /
|
||||
team metadata using these keys: ``alice_wonderfence_api_key``,
|
||||
``alice_wonderfence_app_id``. ``api_id`` has no default. ``api_key`` falls
|
||||
back to the value below or the ``ALICE_API_KEY`` env var.
|
||||
Resolution order for ``api_key`` and ``app_id`` (highest first):
|
||||
API-key metadata → team metadata → request metadata (only when
|
||||
``allow_request_metadata_override`` is True) → ``api_key`` falls back to
|
||||
the configured default below or the ``ALICE_API_KEY`` env var; ``app_id``
|
||||
has no default.
|
||||
|
||||
By default, request-body metadata is ignored so a caller cannot bypass
|
||||
an admin-pinned WonderFence ``app_id`` / ``api_key`` on their virtual
|
||||
key. Enable ``allow_request_metadata_override`` for trusted-gateway
|
||||
deployments that legitimately need request-level overrides.
|
||||
"""
|
||||
|
||||
api_key: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Default API key for WonderFence (overridable per request via metadata.alice_wonderfence_api_key). Env: ALICE_API_KEY.",
|
||||
description="Default API key for WonderFence. Overridable via API-key / team metadata, or via request metadata (alice_wonderfence_api_key) only when allow_request_metadata_override is True. Env: ALICE_API_KEY.",
|
||||
)
|
||||
allow_request_metadata_override: Optional[bool] = Field(
|
||||
default=False,
|
||||
description="When True, allow alice_wonderfence_api_key and alice_wonderfence_app_id in request metadata as a last-resort source (after API-key and team metadata). Default False so caller-controlled request fields cannot bypass admin-pinned credentials.",
|
||||
)
|
||||
api_base: Optional[str] = Field(
|
||||
default=None,
|
||||
|
|
|
|||
|
|
@ -56,9 +56,18 @@ def _make_guardrail(monkeypatch, **overrides):
|
|||
|
||||
|
||||
def _request_data(**overrides):
|
||||
"""Build a request-data dict.
|
||||
|
||||
Default metadata pins ``alice_wonderfence_app_id`` on
|
||||
``user_api_key_metadata`` (admin-controlled) so the request resolves
|
||||
cleanly under the safe-by-default precedence model. Tests that want to
|
||||
drive the value through request metadata must (a) construct a guardrail
|
||||
with ``allow_request_metadata_override=True`` and (b) pass the value via
|
||||
the ``metadata`` kwarg explicitly.
|
||||
"""
|
||||
metadata = overrides.pop("metadata", None)
|
||||
if metadata is None:
|
||||
metadata = {"alice_wonderfence_app_id": "test-app"}
|
||||
metadata = {"user_api_key_metadata": {"alice_wonderfence_app_id": "test-app"}}
|
||||
base = {"model": "gpt-4", "metadata": metadata}
|
||||
base.update(overrides)
|
||||
return base
|
||||
|
|
@ -67,12 +76,26 @@ def _request_data(**overrides):
|
|||
# ----------------------------- resolver tests -----------------------------
|
||||
|
||||
|
||||
def test_resolve_app_id_from_request_metadata(monkeypatch):
|
||||
guardrail, _ = _make_guardrail(monkeypatch)
|
||||
def test_resolve_app_id_from_request_metadata_requires_override_flag(monkeypatch):
|
||||
guardrail, _ = _make_guardrail(monkeypatch, allow_request_metadata_override=True)
|
||||
data = _request_data(metadata={"alice_wonderfence_app_id": "from-req"})
|
||||
assert guardrail._resolve_app_id(data) == "from-req"
|
||||
|
||||
|
||||
def test_resolve_app_id_request_metadata_ignored_when_override_disabled(monkeypatch):
|
||||
"""Request metadata is caller-controlled and must not satisfy app_id when
|
||||
the override flag is off — otherwise a user could bypass admin-pinned
|
||||
credentials by sending their own app_id in the request body."""
|
||||
from litellm.proxy.guardrails.guardrail_hooks.alice_wonderfence.alice_wonderfence import (
|
||||
WonderFenceMissingSecrets,
|
||||
)
|
||||
|
||||
guardrail, _ = _make_guardrail(monkeypatch) # override defaults False
|
||||
data = _request_data(metadata={"alice_wonderfence_app_id": "from-req"})
|
||||
with pytest.raises(WonderFenceMissingSecrets, match="alice_wonderfence_app_id"):
|
||||
guardrail._resolve_app_id(data)
|
||||
|
||||
|
||||
def test_resolve_app_id_from_key_metadata(monkeypatch):
|
||||
guardrail, _ = _make_guardrail(monkeypatch)
|
||||
data = _request_data(
|
||||
|
|
@ -93,8 +116,10 @@ def test_resolve_app_id_from_team_metadata(monkeypatch):
|
|||
assert guardrail._resolve_app_id(data) == "from-team"
|
||||
|
||||
|
||||
def test_resolve_app_id_priority_request_over_key_over_team(monkeypatch):
|
||||
guardrail, _ = _make_guardrail(monkeypatch)
|
||||
def test_resolve_app_id_key_beats_request_even_when_override_enabled(monkeypatch):
|
||||
"""With the override flag on, request metadata is still only a last-resort
|
||||
source — admin-pinned key metadata wins."""
|
||||
guardrail, _ = _make_guardrail(monkeypatch, allow_request_metadata_override=True)
|
||||
data = _request_data(
|
||||
metadata={
|
||||
"alice_wonderfence_app_id": "from-req",
|
||||
|
|
@ -102,7 +127,19 @@ def test_resolve_app_id_priority_request_over_key_over_team(monkeypatch):
|
|||
"user_api_key_team_metadata": {"alice_wonderfence_app_id": "from-team"},
|
||||
}
|
||||
)
|
||||
assert guardrail._resolve_app_id(data) == "from-req"
|
||||
assert guardrail._resolve_app_id(data) == "from-key"
|
||||
|
||||
|
||||
def test_resolve_app_id_team_beats_request_when_override_enabled(monkeypatch):
|
||||
"""Team metadata beats request metadata even with the override flag on."""
|
||||
guardrail, _ = _make_guardrail(monkeypatch, allow_request_metadata_override=True)
|
||||
data = _request_data(
|
||||
metadata={
|
||||
"alice_wonderfence_app_id": "from-req",
|
||||
"user_api_key_team_metadata": {"alice_wonderfence_app_id": "from-team"},
|
||||
}
|
||||
)
|
||||
assert guardrail._resolve_app_id(data) == "from-team"
|
||||
|
||||
|
||||
def test_resolve_app_id_priority_key_over_team(monkeypatch):
|
||||
|
|
@ -127,12 +164,37 @@ def test_resolve_app_id_missing_raises(monkeypatch):
|
|||
guardrail._resolve_app_id(data)
|
||||
|
||||
|
||||
def test_resolve_api_key_from_request_metadata(monkeypatch):
|
||||
guardrail, _ = _make_guardrail(monkeypatch, api_key="default")
|
||||
def test_resolve_api_key_from_request_metadata_requires_override_flag(monkeypatch):
|
||||
guardrail, _ = _make_guardrail(
|
||||
monkeypatch, api_key="default", allow_request_metadata_override=True
|
||||
)
|
||||
data = _request_data(metadata={"alice_wonderfence_api_key": "from-req"})
|
||||
assert guardrail._resolve_api_key(data) == "from-req"
|
||||
|
||||
|
||||
def test_resolve_api_key_request_metadata_ignored_when_override_disabled(monkeypatch):
|
||||
"""With override off, a caller-supplied api_key must not be honored;
|
||||
falls back to the configured default instead."""
|
||||
guardrail, _ = _make_guardrail(monkeypatch, api_key="default")
|
||||
data = _request_data(metadata={"alice_wonderfence_api_key": "from-req"})
|
||||
assert guardrail._resolve_api_key(data) == "default"
|
||||
|
||||
|
||||
def test_resolve_api_key_key_beats_request_even_when_override_enabled(monkeypatch):
|
||||
"""Admin-pinned key metadata wins over request metadata even with the
|
||||
override flag enabled."""
|
||||
guardrail, _ = _make_guardrail(
|
||||
monkeypatch, api_key="default", allow_request_metadata_override=True
|
||||
)
|
||||
data = _request_data(
|
||||
metadata={
|
||||
"alice_wonderfence_api_key": "from-req",
|
||||
"user_api_key_metadata": {"alice_wonderfence_api_key": "from-key"},
|
||||
}
|
||||
)
|
||||
assert guardrail._resolve_api_key(data) == "from-key"
|
||||
|
||||
|
||||
def test_resolve_api_key_from_key_metadata(monkeypatch):
|
||||
guardrail, _ = _make_guardrail(monkeypatch, api_key="default")
|
||||
data = _request_data(
|
||||
|
|
@ -172,10 +234,15 @@ def test_resolve_api_key_missing_everywhere_raises(monkeypatch):
|
|||
|
||||
|
||||
def test_resolve_reads_litellm_metadata_when_metadata_absent(monkeypatch):
|
||||
"""``_get_metadata`` falls back to ``litellm_metadata`` when ``metadata``
|
||||
is missing. Use admin-controlled key metadata so it resolves without
|
||||
needing the request-override flag."""
|
||||
guardrail, _ = _make_guardrail(monkeypatch)
|
||||
data = {
|
||||
"model": "gpt-4",
|
||||
"litellm_metadata": {"alice_wonderfence_app_id": "from-litellm-md"},
|
||||
"litellm_metadata": {
|
||||
"user_api_key_metadata": {"alice_wonderfence_app_id": "from-litellm-md"}
|
||||
},
|
||||
}
|
||||
assert guardrail._resolve_app_id(data) == "from-litellm-md"
|
||||
|
||||
|
|
@ -487,7 +554,9 @@ async def test_apply_guardrail_passes_app_id_per_call(guardrail_and_client):
|
|||
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["hi"]},
|
||||
request_data=_request_data(metadata={"alice_wonderfence_app_id": "tenant-A"}),
|
||||
request_data=_request_data(
|
||||
metadata={"user_api_key_metadata": {"alice_wonderfence_app_id": "tenant-A"}}
|
||||
),
|
||||
input_type="request",
|
||||
)
|
||||
kwargs = client.evaluate_prompt.call_args.kwargs
|
||||
|
|
@ -508,7 +577,9 @@ async def test_apply_guardrail_response_path_passes_app_id(monkeypatch):
|
|||
|
||||
await guardrail.apply_guardrail(
|
||||
inputs={"texts": ["resp"]},
|
||||
request_data=_request_data(metadata={"alice_wonderfence_app_id": "tenant-B"}),
|
||||
request_data=_request_data(
|
||||
metadata={"user_api_key_metadata": {"alice_wonderfence_app_id": "tenant-B"}}
|
||||
),
|
||||
input_type="response",
|
||||
)
|
||||
kwargs = client.evaluate_response.call_args.kwargs
|
||||
|
|
@ -660,7 +731,9 @@ async def test_post_call_recovers_app_id_via_logging_obj_stash(monkeypatch):
|
|||
"""Reproduces the framework gap: request body metadata is dropped before
|
||||
post_call. The logging_obj stash from the prior `input_type="request"`
|
||||
call must be used to resolve app_id."""
|
||||
guardrail, client = _make_guardrail(monkeypatch)
|
||||
guardrail, client = _make_guardrail(
|
||||
monkeypatch, allow_request_metadata_override=True
|
||||
)
|
||||
guardrail._client_cache["default-api-key"] = client
|
||||
request_obj = Mock()
|
||||
request_obj.action = "NO_ACTION"
|
||||
|
|
@ -702,7 +775,9 @@ async def test_post_call_recovers_app_id_via_logging_obj_stash(monkeypatch):
|
|||
async def test_post_call_prefers_request_data_over_stash(monkeypatch):
|
||||
"""If post_call's request_data still resolves (e.g. app_id from key/team
|
||||
metadata), use it — don't fall back to the stash."""
|
||||
guardrail, client = _make_guardrail(monkeypatch)
|
||||
guardrail, client = _make_guardrail(
|
||||
monkeypatch, allow_request_metadata_override=True
|
||||
)
|
||||
guardrail._client_cache["default-api-key"] = client
|
||||
request_obj = Mock()
|
||||
request_obj.action = "NO_ACTION"
|
||||
|
|
@ -770,9 +845,17 @@ async def test_post_call_recovers_via_sibling_stash(monkeypatch):
|
|||
`guardrails` array, LiteLLM only invokes one's during_call — but every
|
||||
instance runs post_call. The instance whose during_call did NOT fire
|
||||
must recover the stash written by the sibling that did."""
|
||||
g_writer, c_writer = _make_guardrail(monkeypatch, guardrail_name="writer")
|
||||
g_writer, c_writer = _make_guardrail(
|
||||
monkeypatch,
|
||||
guardrail_name="writer",
|
||||
allow_request_metadata_override=True,
|
||||
)
|
||||
g_writer._client_cache["default-api-key"] = c_writer
|
||||
g_reader, c_reader = _make_guardrail(monkeypatch, guardrail_name="reader")
|
||||
g_reader, c_reader = _make_guardrail(
|
||||
monkeypatch,
|
||||
guardrail_name="reader",
|
||||
allow_request_metadata_override=True,
|
||||
)
|
||||
g_reader._client_cache["default-api-key"] = c_reader
|
||||
for c in (c_writer, c_reader):
|
||||
result = Mock()
|
||||
|
|
@ -807,9 +890,17 @@ async def test_post_call_recovers_via_sibling_stash(monkeypatch):
|
|||
async def test_stash_keyed_per_guardrail_name(monkeypatch):
|
||||
"""Two alice_wonderfence instances on the same logging_obj must not
|
||||
overwrite each other's stash — they're keyed by guardrail_name."""
|
||||
g1, c1 = _make_guardrail(monkeypatch, guardrail_name="alice-a")
|
||||
g1, c1 = _make_guardrail(
|
||||
monkeypatch,
|
||||
guardrail_name="alice-a",
|
||||
allow_request_metadata_override=True,
|
||||
)
|
||||
g1._client_cache["default-api-key"] = c1
|
||||
g2, c2 = _make_guardrail(monkeypatch, guardrail_name="alice-b")
|
||||
g2, c2 = _make_guardrail(
|
||||
monkeypatch,
|
||||
guardrail_name="alice-b",
|
||||
allow_request_metadata_override=True,
|
||||
)
|
||||
g2._client_cache["default-api-key"] = c2
|
||||
for c in (c1, c2):
|
||||
result = Mock()
|
||||
|
|
@ -898,6 +989,7 @@ def test_initialize_guardrail_forwards_all_params(monkeypatch):
|
|||
debug=True,
|
||||
max_cached_clients=5,
|
||||
connection_pool_limit=20,
|
||||
allow_request_metadata_override=True,
|
||||
default_on=True,
|
||||
)
|
||||
guardrail = {"guardrail_name": "wf-init-test"}
|
||||
|
|
@ -912,6 +1004,14 @@ def test_initialize_guardrail_forwards_all_params(monkeypatch):
|
|||
assert g.block_message == "custom block"
|
||||
assert g._client_cache_maxsize == 5
|
||||
assert g._connection_pool_limit == 20
|
||||
assert g.allow_request_metadata_override is True
|
||||
|
||||
|
||||
def test_allow_request_metadata_override_defaults_false(monkeypatch):
|
||||
"""New flag must default to False so request-body metadata cannot
|
||||
bypass admin-pinned credentials out of the box."""
|
||||
guardrail, _ = _make_guardrail(monkeypatch)
|
||||
assert guardrail.allow_request_metadata_override is False
|
||||
|
||||
|
||||
def test_initialize_guardrail_missing_name_raises(monkeypatch):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue