This commit is contained in:
AlbertNeuralTrust 2026-10-20 09:51:00 -07:00 • committed by GitHub
commit 7ff9ac5855
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 93 additions and 4 deletions

View file

@ -2331,6 +2331,26 @@ async def test_custom_code_guardrail(
)
_GUARDRAIL_METADATA_ADAPTER: Final = TypeAdapter(Mapping[str, object])
def _metadata_fields(value: object) -> Mapping[str, object]:
try:
return _GUARDRAIL_METADATA_ADAPTER.validate_python(value)
except ValidationError:
return {}
def _guardrail_request_metadata(caller: object, proxy: object) -> Mapping[str, object]:
caller_fields: Final = tuple(
(key, value) for key, value in _metadata_fields(caller).items() if not key.startswith("user_api_key_")
)
key_identity: Final = tuple(
(key, value) for key, value in _metadata_fields(proxy).items() if key.startswith("user_api_key_")
)
return dict((*caller_fields, *key_identity))
def _execution_timeout_response(timeout: float) -> TestCustomCodeGuardrailResponse:
return TestCustomCodeGuardrailResponse(
success=False,
@ -2493,9 +2513,16 @@ async def apply_guardrail(
if litellm_logging_obj is not None:
_patch_logging_obj_for_guardrail(litellm_logging_obj, request)
metadata: Final = _guardrail_request_metadata(
request.metadata, _GUARDRAIL_METADATA_ADAPTER.validate_python(data).get("metadata")
)
request_data: Final[dict] = {
**({"messages": request.messages} if request.messages is not None else {}),
**({"metadata": request.metadata} if request.metadata is not None else {}),
key: value
for key, value in (
("messages", request.messages),
("metadata", metadata if request.metadata is not None or metadata else None),
)
if value is not None
}
_input_type: Final = _resolve_guardrail_input_type(active_guardrail, request.input_type)
guardrailed_inputs: Final = await active_guardrail.apply_guardrail(

View file

@ -2167,7 +2167,7 @@ async def test_apply_guardrail_invokes_logging_pipeline(mocker):
}
def _patch_apply_guardrail_env(mocker, guardrail_result):
def _patch_apply_guardrail_env(mocker, guardrail_result, processed_data=None):
mock_guardrail = mocker.Mock()
mock_guardrail.apply_guardrail = AsyncMock(return_value=guardrail_result)
@ -2180,7 +2180,7 @@ def _patch_apply_guardrail_env(mocker, guardrail_result):
mock_logging_obj.model_call_details = {}
mock_processor = mocker.Mock()
mock_processor.common_processing_pre_call_logic = AsyncMock(
return_value=({"guardrail_name": "test-guardrail"}, mock_logging_obj)
return_value=(processed_data or {"guardrail_name": "test-guardrail"}, mock_logging_obj)
)
mocker.patch(
"litellm.proxy.common_request_processing.ProxyBaseLLMRequestProcessing",
@ -2251,6 +2251,68 @@ async def test_apply_guardrail_forwards_metadata_and_messages_together(mocker):
)
@pytest.mark.asyncio
async def test_apply_guardrail_replaces_caller_identity_with_the_authenticated_key(mocker):
"""Identity fields come from the proxy's own sanitized metadata, never from
the caller, so a request cannot impersonate another key or probe its policy."""
mock_guardrail = _patch_apply_guardrail_env(
mocker,
{"texts": ["ok"]},
processed_data={
"guardrail_name": "test-guardrail",
"metadata": {
"route": "/apply_guardrail",
"user_api_key_alias": "billing-app",
"user_api_key_user_id": "u-1",
},
},
)
request = ApplyGuardrailRequest(
guardrail_name="test-guardrail",
text="hello",
metadata={
"forbidden_topics": ["tax"],
"user_api_key_alias": "someone-else",
"user_api_key_user_email": "victim@example.com",
},
)
await apply_guardrail(
fastapi_request=mocker.Mock(),
request=request,
user_api_key_dict=UserAPIKeyAuth(key_alias="billing-app", user_id="u-1"),
)
forwarded = mock_guardrail.apply_guardrail.await_args.kwargs["request_data"]["metadata"]
assert forwarded == {
"forbidden_topics": ["tax"],
"user_api_key_alias": "billing-app",
"user_api_key_user_id": "u-1",
}
@pytest.mark.asyncio
async def test_apply_guardrail_attaches_key_identity_when_caller_sends_no_metadata(mocker):
"""A caller that sends no metadata still gets the authenticated identity
forwarded, the same shape every LLM route gives a guardrail."""
mock_guardrail = _patch_apply_guardrail_env(
mocker,
{"texts": ["ok"]},
processed_data={"guardrail_name": "test-guardrail", "metadata": {"user_api_key_alias": "billing-app"}},
)
request = ApplyGuardrailRequest(guardrail_name="test-guardrail", text="hello")
await apply_guardrail(
fastapi_request=mocker.Mock(),
request=request,
user_api_key_dict=UserAPIKeyAuth(key_alias="billing-app"),
)
assert mock_guardrail.apply_guardrail.await_args.kwargs["request_data"] == {
"metadata": {"user_api_key_alias": "billing-app"}
}
@pytest.mark.asyncio
async def test_apply_guardrail_omits_metadata_when_not_sent(mocker):
"""Without metadata, request_data stays empty (backward-compatible)."""