litellm/tests/integration/security/test_config_overrides.py
devin-ai-integration[bot] 595cf066cd
test(integration): cover config_utils endpoint contracts (#44737)
* test(integration): cover config_utils endpoint contracts

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(integration): prove config_utils contracts on the peer, Vault AppRole and cert logins, and admin-view checks

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(integration): assert no Vault traffic after config override delete

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(integration): prove the Vault cert login presents the client certificate over TLS

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(integration): cover the blank plugin_key dashboard edit and the env-only CyberArk readback

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* style(tests): ruff-sort imports and reformat config_utils integration tests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(integration): check plaintext Vault traffic in the cert phase with one drain

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(integration): probe CyberArk runtime state after DELETE with a secret-resolving chat

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(integration): barrier the Langfuse absence check on the shared export channel

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(integration): send the dashboard's blanked CyberArk field and check row, readback and runtime env

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: mateo <mateo@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-14 07:00:23 +00:00

1103 lines
54 KiB
Python

from __future__ import annotations
import base64
import json
import ssl
import uuid
from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from pathlib import Path
from typing import Final
from urllib.parse import quote
import httpx
import pytest
from integration._support.client import Gateway, object_value, string_value
from integration._support.database import read_rows, scratch_database
from integration._support.process import owned_proxy
from integration._support.tls import write_self_signed_cert
from integration._support.wire import Reply, Request, wire_server
from pydantic import JsonValue, TypeAdapter
_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
_VAULT_ENVIRONMENT: Final = (
"HCP_VAULT_ADDR",
"HCP_VAULT_TOKEN",
"HCP_VAULT_APPROLE_ROLE_ID",
"HCP_VAULT_APPROLE_SECRET_ID",
"HCP_VAULT_APPROLE_MOUNT_PATH",
"HCP_VAULT_CLIENT_CERT",
"HCP_VAULT_CLIENT_KEY",
"HCP_VAULT_CERT_ROLE",
"HCP_VAULT_NAMESPACE",
"HCP_VAULT_LOGIN_NAMESPACE",
"HCP_VAULT_SECRET_NAMESPACE",
"HCP_VAULT_MOUNT_NAME",
"HCP_VAULT_PATH_PREFIX",
)
_CYBERARK_ENVIRONMENT: Final = (
"CYBERARK_API_BASE",
"CYBERARK_ACCOUNT",
"CYBERARK_USERNAME",
"CYBERARK_API_KEY",
"CYBERARK_CLIENT_CERT",
"CYBERARK_CLIENT_KEY",
"CYBERARK_SSL_VERIFY",
"CYBERARK_REFRESH_INTERVAL",
)
_SECRET_MANAGER_ENVIRONMENT: Final = (*_VAULT_ENVIRONMENT, *_CYBERARK_ENVIRONMENT)
@contextmanager
def _config_proxy(
gateway: Gateway,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
overrides: Mapping[str, str] | None = None,
) -> Iterator[tuple[Gateway, str]]:
with scratch_database() as database_url:
with monkeypatch.context() as scoped_environment:
scoped_environment.setenv("DATABASE_URL", database_url)
with owned_proxy(
gateway,
tmp_path,
{"DATABASE_URL": database_url, **(overrides or {})},
remove_environment=_SECRET_MANAGER_ENVIRONMENT,
) as candidate:
yield candidate, database_url
def _override_values(candidate: Gateway, path: str, config_type: str) -> dict[str, JsonValue]:
response: Final = candidate.request("GET", path)
assert response.status_code == 200, response.text
body: Final = object_value(_JSON_OBJECT.validate_json(response.content))
assert body["config_type"] == config_type, response.text
return object_value(body["values"])
def _stored_override(database_url: str, config_type: str) -> dict[str, JsonValue]:
rows: Final = read_rows(
'SELECT config_value FROM "LiteLLM_ConfigOverrides" WHERE config_type = %s',
(config_type,),
database_url=database_url,
)
assert len(rows) == 1, rows
config_value: Final = rows[0]["config_value"]
return _JSON_OBJECT.validate_json(config_value) if isinstance(config_value, str) else object_value(config_value)
def _masked(value: str) -> str:
return value[:4] + "*" * (len(value) - 8) + value[-4:]
def _assert_non_admin_refusal(response: httpx.Response, route: str, user_id: str) -> None:
assert response.status_code == 401, response.text
masked_user_id: Final = user_id[:6] + "*" * (len(user_id) - 8) + user_id[-2:]
assert response.json() == {
"error": {
"message": (
"Authentication Error, Only proxy admin can be used to generate, delete, update info for "
f"new keys/users/teams. Route={route}. Your role=internal_user. Your user_id={masked_user_id}"
),
"type": "auth_error",
"param": "None",
"code": "401",
}
}, response.text
def _completion(text: str) -> Reply:
return Reply(
body=json.dumps(
{
"id": "chatcmpl-" + uuid.uuid4().hex,
"object": "chat.completion",
"created": 1,
"model": "gpt-4o-mini",
"choices": [{"index": 0, "message": {"role": "assistant", "content": text}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
}
).encode()
)
def _new_model(candidate: Gateway, name: str, provider: str, secret_name: str) -> str:
response: Final = candidate.request(
"POST",
"/model/new",
{
"model_name": name,
"litellm_params": {
"model": "openai/gpt-4o-mini",
"api_key": f"os.environ/{secret_name}",
"api_base": provider + "/v1",
},
"model_info": {},
},
)
assert response.status_code == 200, response.text
return string_value(object_value(response.json()["model_info"])["id"])
def _chat(candidate: Gateway, model: str, marker: str) -> None:
response: Final = candidate.request(
"POST",
"/v1/chat/completions",
{"model": model, "messages": [{"role": "user", "content": marker}]},
)
assert response.status_code == 200, response.text
assert response.json()["choices"][0]["message"]["content"] == marker, response.text
def test_saved_vault_config_reads_back_masked_and_refuses_non_admins(
gateway: Gateway, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
def vault_response(request: Request) -> Reply:
return Reply()
with wire_server(vault_response) as vault:
config: Final = {
"vault_addr": vault.url,
"vault_token": "vault-token-" + uuid.uuid4().hex,
"approle_role_id": "approle-role-" + uuid.uuid4().hex,
"approle_secret_id": "approle-secret-" + uuid.uuid4().hex,
"approle_mount_path": "auth/approle-integration",
"client_cert": "/integration/client.crt",
"client_key": "client-key-" + uuid.uuid4().hex,
"vault_cert_role": "integration-role",
"vault_namespace": "integration/root",
"vault_login_namespace": "integration/login",
"vault_secret_namespace": "integration/secrets",
"vault_mount_name": "integration-kv",
"vault_path_prefix": "integration/prefix",
}
with _config_proxy(gateway, tmp_path, monkeypatch) as (candidate, database_url):
saved: Final = candidate.request("POST", "/config_overrides/hashicorp_vault", config)
assert saved.status_code == 200, saved.text
assert saved.json() == {
"message": "Hashicorp Vault configuration updated successfully",
"status": "success",
}, saved.text
readback: Final = candidate.request("GET", "/config_overrides/hashicorp_vault")
assert readback.status_code == 200, readback.text
readback_body: Final = object_value(_JSON_OBJECT.validate_json(readback.content))
assert readback_body["config_type"] == "hashicorp_vault", readback.text
values: Final = object_value(readback_body["values"])
expected_values: Final = {
**config,
"vault_token": _masked(string_value(config["vault_token"])),
"approle_secret_id": _masked(string_value(config["approle_secret_id"])),
"client_key": _masked(string_value(config["client_key"])),
}
assert values == expected_values
assert all(
string_value(config[field]).encode() not in readback.content
for field in ("vault_token", "approle_secret_id", "client_key")
)
with candidate.scenario() as scenario:
non_admin_user_id: Final = "integration-vault-non-admin"
non_admin: Final = scenario.key(
user_id=scenario.user(user_id=non_admin_user_id, user_role="internal_user")
)
denied_get: Final = candidate.request("GET", "/config_overrides/hashicorp_vault", key=non_admin)
_assert_non_admin_refusal(denied_get, "/config_overrides/hashicorp_vault", non_admin_user_id)
denied_post: Final = candidate.request(
"POST", "/config_overrides/hashicorp_vault", config, key=non_admin
)
_assert_non_admin_refusal(denied_post, "/config_overrides/hashicorp_vault", non_admin_user_id)
stored_before_scoped: Final = _stored_override(database_url, "hashicorp_vault")
scoped_route_key: Final = scenario.key(
user_id=scenario.user(user_role="internal_user"),
allowed_routes=["/config_overrides/hashicorp_vault"],
)
scoped_get: Final = candidate.request("GET", "/config_overrides/hashicorp_vault", key=scoped_route_key)
assert scoped_get.status_code == 403, scoped_get.text
assert scoped_get.json() == {"detail": "Only admin users can view config overrides"}, scoped_get.text
scoped_post: Final = candidate.request(
"POST", "/config_overrides/hashicorp_vault", config, key=scoped_route_key
)
assert scoped_post.status_code == 403, scoped_post.text
assert scoped_post.json() == {"detail": "Only admin users can update config overrides"}, (
scoped_post.text
)
assert _stored_override(database_url, "hashicorp_vault") == stored_before_scoped
viewer_key: Final = scenario.key(user_id=scenario.user(user_role="proxy_admin_viewer"))
viewer_get: Final = candidate.request("GET", "/config_overrides/hashicorp_vault", key=viewer_key)
assert viewer_get.status_code == 200, viewer_get.text
viewer_values: Final = object_value(
object_value(_JSON_OBJECT.validate_json(viewer_get.content))["values"]
)
assert viewer_values == expected_values, viewer_get.text
assert all(
string_value(config[field]).encode() not in viewer_get.content
for field in ("vault_token", "approle_secret_id", "client_key")
)
viewer_post: Final = candidate.request(
"POST", "/config_overrides/hashicorp_vault", config, key=viewer_key
)
assert viewer_post.status_code == 403, viewer_post.text
assert viewer_post.json() == {
"error": {
"message": "user not allowed to access this route, role= proxy_admin_viewer. "
"Trying to access: /config_overrides/hashicorp_vault",
"type": "auth_error",
"param": "None",
"code": "403",
}
}, viewer_post.text
assert _stored_override(database_url, "hashicorp_vault") == stored_before_scoped
env_values: Final = {
"HCP_VAULT_ADDR": vault.url,
"HCP_VAULT_TOKEN": "env-vault-token-" + uuid.uuid4().hex,
"HCP_VAULT_APPROLE_ROLE_ID": "env-role-" + uuid.uuid4().hex,
"HCP_VAULT_APPROLE_SECRET_ID": "env-secret-" + uuid.uuid4().hex,
"HCP_VAULT_APPROLE_MOUNT_PATH": "auth/env-approle",
"HCP_VAULT_CLIENT_CERT": "/integration/env-client.crt",
"HCP_VAULT_CLIENT_KEY": "env-client-key-" + uuid.uuid4().hex,
"HCP_VAULT_CERT_ROLE": "env-integration-role",
"HCP_VAULT_NAMESPACE": "env/root",
"HCP_VAULT_LOGIN_NAMESPACE": "env/login",
"HCP_VAULT_SECRET_NAMESPACE": "env/secrets",
"HCP_VAULT_MOUNT_NAME": "env-kv",
"HCP_VAULT_PATH_PREFIX": "env/prefix",
}
env_config: Final = {
"vault_addr": env_values["HCP_VAULT_ADDR"],
"vault_token": env_values["HCP_VAULT_TOKEN"],
"approle_role_id": env_values["HCP_VAULT_APPROLE_ROLE_ID"],
"approle_secret_id": env_values["HCP_VAULT_APPROLE_SECRET_ID"],
"approle_mount_path": env_values["HCP_VAULT_APPROLE_MOUNT_PATH"],
"client_cert": env_values["HCP_VAULT_CLIENT_CERT"],
"client_key": env_values["HCP_VAULT_CLIENT_KEY"],
"vault_cert_role": env_values["HCP_VAULT_CERT_ROLE"],
"vault_namespace": env_values["HCP_VAULT_NAMESPACE"],
"vault_login_namespace": env_values["HCP_VAULT_LOGIN_NAMESPACE"],
"vault_secret_namespace": env_values["HCP_VAULT_SECRET_NAMESPACE"],
"vault_mount_name": env_values["HCP_VAULT_MOUNT_NAME"],
"vault_path_prefix": env_values["HCP_VAULT_PATH_PREFIX"],
}
with _config_proxy(gateway, tmp_path, monkeypatch, env_values) as (candidate, database_url):
assert (
read_rows(
'SELECT config_type FROM "LiteLLM_ConfigOverrides" WHERE config_type = %s',
("hashicorp_vault",),
database_url=database_url,
)
== []
)
env_response: Final = candidate.request("GET", "/config_overrides/hashicorp_vault")
assert env_response.status_code == 200, env_response.text
env_body: Final = object_value(_JSON_OBJECT.validate_json(env_response.content))
assert env_body["config_type"] == "hashicorp_vault", env_response.text
env_readback: Final = object_value(env_body["values"])
assert env_readback == {
**env_config,
"vault_token": _masked(string_value(env_config["vault_token"])),
"approle_secret_id": _masked(string_value(env_config["approle_secret_id"])),
"client_key": _masked(string_value(env_config["client_key"])),
}
assert all(
string_value(env_config[field]).encode() not in env_response.content
for field in ("vault_token", "approle_secret_id", "client_key")
)
def test_saved_cyberark_config_reads_back_masked_and_refuses_non_admins(
gateway: Gateway, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
with wire_server(lambda request: Reply()) as cyberark:
config: Final = {
"cyberark_api_base": cyberark.url,
"cyberark_account": "integration-account",
"cyberark_username": "integration-user",
"cyberark_api_key": "cyberark-api-key-" + uuid.uuid4().hex,
"client_cert": "/integration/client.crt",
"client_key": "cyberark-client-key-" + uuid.uuid4().hex,
"ssl_verify": "true",
"refresh_interval": "300",
}
with _config_proxy(gateway, tmp_path, monkeypatch) as (candidate, _database_url):
saved: Final = candidate.request("POST", "/config_overrides/cyberark", config)
assert saved.status_code == 200, saved.text
assert saved.json() == {
"message": "CyberArk configuration updated successfully",
"status": "success",
}, saved.text
readback: Final = candidate.request("GET", "/config_overrides/cyberark")
assert readback.status_code == 200, readback.text
readback_body: Final = object_value(_JSON_OBJECT.validate_json(readback.content))
assert readback_body["config_type"] == "cyberark", readback.text
values: Final = object_value(readback_body["values"])
assert values == {
**config,
"cyberark_api_key": _masked(string_value(config["cyberark_api_key"])),
"client_key": _masked(string_value(config["client_key"])),
}
assert all(
string_value(config[field]).encode() not in readback.content
for field in ("cyberark_api_key", "client_key")
)
with candidate.scenario() as scenario:
non_admin_user_id: Final = "integration-cyberark-non-admin"
non_admin: Final = scenario.key(
user_id=scenario.user(user_id=non_admin_user_id, user_role="internal_user")
)
denied_get: Final = candidate.request("GET", "/config_overrides/cyberark", key=non_admin)
_assert_non_admin_refusal(denied_get, "/config_overrides/cyberark", non_admin_user_id)
denied_post: Final = candidate.request("POST", "/config_overrides/cyberark", config, key=non_admin)
_assert_non_admin_refusal(denied_post, "/config_overrides/cyberark", non_admin_user_id)
stored_before_scoped: Final = _stored_override(_database_url, "cyberark")
scoped_route_key: Final = scenario.key(
user_id=scenario.user(user_role="internal_user"),
allowed_routes=["/config_overrides/cyberark"],
)
scoped_get: Final = candidate.request("GET", "/config_overrides/cyberark", key=scoped_route_key)
assert scoped_get.status_code == 403, scoped_get.text
assert scoped_get.json() == {"detail": "Only admin users can view config overrides"}, scoped_get.text
scoped_post: Final = candidate.request(
"POST", "/config_overrides/cyberark", config, key=scoped_route_key
)
assert scoped_post.status_code == 403, scoped_post.text
assert scoped_post.json() == {"detail": "Only admin users can update config overrides"}, (
scoped_post.text
)
assert _stored_override(_database_url, "cyberark") == stored_before_scoped
viewer_key: Final = scenario.key(user_id=scenario.user(user_role="proxy_admin_viewer"))
viewer_get: Final = candidate.request("GET", "/config_overrides/cyberark", key=viewer_key)
assert viewer_get.status_code == 200, viewer_get.text
viewer_values: Final = object_value(
object_value(_JSON_OBJECT.validate_json(viewer_get.content))["values"]
)
assert viewer_values == values, viewer_get.text
assert all(
string_value(config[field]).encode() not in viewer_get.content
for field in ("cyberark_api_key", "client_key")
)
viewer_post: Final = candidate.request("POST", "/config_overrides/cyberark", config, key=viewer_key)
assert viewer_post.status_code == 403, viewer_post.text
assert viewer_post.json() == {
"error": {
"message": "user not allowed to access this route, role= proxy_admin_viewer. "
"Trying to access: /config_overrides/cyberark",
"type": "auth_error",
"param": "None",
"code": "403",
}
}, viewer_post.text
assert _stored_override(_database_url, "cyberark") == stored_before_scoped
env_values: Final = {
"CYBERARK_API_BASE": cyberark.url,
"CYBERARK_ACCOUNT": "env-account",
"CYBERARK_USERNAME": "env-user",
"CYBERARK_API_KEY": "env-cyberark-api-key-" + uuid.uuid4().hex,
"CYBERARK_CLIENT_CERT": "/integration/env-client.crt",
"CYBERARK_CLIENT_KEY": "env-cyberark-client-key-" + uuid.uuid4().hex,
"CYBERARK_SSL_VERIFY": "false",
"CYBERARK_REFRESH_INTERVAL": "600",
}
env_config: Final = {
"cyberark_api_base": env_values["CYBERARK_API_BASE"],
"cyberark_account": env_values["CYBERARK_ACCOUNT"],
"cyberark_username": env_values["CYBERARK_USERNAME"],
"cyberark_api_key": env_values["CYBERARK_API_KEY"],
"client_cert": env_values["CYBERARK_CLIENT_CERT"],
"client_key": env_values["CYBERARK_CLIENT_KEY"],
"ssl_verify": env_values["CYBERARK_SSL_VERIFY"],
"refresh_interval": env_values["CYBERARK_REFRESH_INTERVAL"],
}
with _config_proxy(gateway, tmp_path, monkeypatch, env_values) as (env_candidate, env_database_url):
assert (
read_rows(
'SELECT config_type FROM "LiteLLM_ConfigOverrides" WHERE config_type = %s',
("cyberark",),
database_url=env_database_url,
)
== []
)
env_response: Final = env_candidate.request("GET", "/config_overrides/cyberark")
assert env_response.status_code == 200, env_response.text
env_body: Final = object_value(_JSON_OBJECT.validate_json(env_response.content))
assert env_body["config_type"] == "cyberark", env_response.text
assert object_value(env_body["values"]) == {
**env_config,
"cyberark_api_key": _masked(env_config["cyberark_api_key"]),
"client_key": _masked(env_config["client_key"]),
}, env_response.text
assert all(
env_config[field].encode() not in env_response.content for field in ("cyberark_api_key", "client_key")
)
def test_vault_partial_update_keeps_omitted_fields_encrypts_and_drives_secret_reads(
gateway: Gateway, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
provider_secret: Final = "vault-provider-secret-" + uuid.uuid4().hex
vault_token: Final = "vault-token-" + uuid.uuid4().hex
marker_one: Final = "vault-runtime-one-" + uuid.uuid4().hex
marker_two: Final = "vault-runtime-two-" + uuid.uuid4().hex
path_one: Final = "/v1/integration/team/kv-original/data/initial-prefix/secret-one"
path_two: Final = "/v1/integration/team/kv-updated/data/secret-two"
def vault(request: Request) -> Reply:
assert request.method == "GET"
assert request.headers["x-vault-token"] == vault_token
secrets: Final = {
path_one: provider_secret,
path_two: provider_secret,
}
if request.target not in secrets:
return Reply(status=404, body=b'{"errors":["secret not found"]}')
return Reply(body=json.dumps({"data": {"data": {"key": secrets[request.target]}}}).encode())
def provider(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/chat/completions"
body: Final = _JSON_OBJECT.validate_json(request.body)
messages: Final = body["messages"]
assert isinstance(messages, list)
message: Final = object_value(messages[0])
marker: Final = string_value(message["content"])
if marker == "after-delete":
return _completion(marker)
assert request.headers["authorization"] == f"Bearer {provider_secret}", (
"Provider request did not use the Vault-resolved secret"
)
assert marker in {marker_one, marker_two}, request.body
return _completion(marker)
with wire_server(vault) as vault_wire, wire_server(provider) as provider_wire:
with _config_proxy(gateway, tmp_path, monkeypatch) as (candidate, database_url):
full_config: Final = {
"vault_addr": vault_wire.url,
"vault_token": vault_token,
"vault_secret_namespace": "integration/team",
"vault_mount_name": "kv-original",
"vault_path_prefix": "initial-prefix",
}
full_update: Final = candidate.request("POST", "/config_overrides/hashicorp_vault", full_config)
assert full_update.status_code == 200, full_update.text
assert full_update.json() == {
"message": "Hashicorp Vault configuration updated successfully",
"status": "success",
}, full_update.text
stored_full: Final = _stored_override(database_url, "hashicorp_vault")
assert set(stored_full) == set(full_config), stored_full
serialized_full: Final = json.dumps(stored_full)
assert all(value not in serialized_full for value in full_config.values()), serialized_full
with candidate.scenario() as scenario:
model_one: Final = scenario.model(
api_key="os.environ/secret-one",
api_base=provider_wire.url + "/v1",
)
response_one: Final = candidate.request(
"POST",
"/v1/chat/completions",
{"model": model_one, "messages": [{"role": "user", "content": marker_one}]},
)
assert response_one.status_code == 200, response_one.text
assert response_one.json()["choices"][0]["message"]["content"] == marker_one, response_one.text
first_secret_reads: Final = tuple(
(request.method, request.target)
for request in vault_wire.drain()
if request.target in {path_one, path_two}
)
assert first_secret_reads == (("GET", path_one),)
partial_config: Final = {
"vault_addr": vault_wire.url,
"vault_secret_namespace": "integration/team",
"vault_mount_name": "kv-updated",
"vault_path_prefix": "",
}
partial_update: Final = candidate.request("POST", "/config_overrides/hashicorp_vault", partial_config)
assert partial_update.status_code == 200, partial_update.text
assert partial_update.json() == {
"message": "Hashicorp Vault configuration updated successfully",
"status": "success",
}, partial_update.text
stored_partial: Final = _stored_override(database_url, "hashicorp_vault")
assert set(stored_partial) == {
"vault_addr",
"vault_token",
"vault_secret_namespace",
"vault_mount_name",
}, stored_partial
serialized_partial: Final = json.dumps(stored_partial)
assert all(
value not in serialized_partial
for value in (
vault_wire.url,
vault_token,
"integration/team",
"kv-updated",
)
), serialized_partial
readback: Final = _override_values(candidate, "/config_overrides/hashicorp_vault", "hashicorp_vault")
assert readback == {
"vault_addr": vault_wire.url,
"vault_token": _masked(vault_token),
"vault_secret_namespace": "integration/team",
"vault_mount_name": "kv-updated",
}
readback_response: Final = candidate.request("GET", "/config_overrides/hashicorp_vault")
assert readback_response.status_code == 200, readback_response.text
assert vault_token not in readback_response.text, readback_response.text
model_two: Final = scenario.model(
api_key="os.environ/secret-two",
api_base=provider_wire.url + "/v1",
)
response_two: Final = candidate.request(
"POST",
"/v1/chat/completions",
{"model": model_two, "messages": [{"role": "user", "content": marker_two}]},
)
assert response_two.status_code == 200, response_two.text
assert response_two.json()["choices"][0]["message"]["content"] == marker_two, response_two.text
second_secret_reads: Final = tuple(
(request.method, request.target)
for request in vault_wire.drain()
if request.target in {path_one, path_two}
)
assert second_secret_reads == (("GET", path_two),)
provider_requests: Final = provider_wire.drain()
assert tuple((request.method, request.target) for request in provider_requests) == (
("POST", "/v1/chat/completions"),
("POST", "/v1/chat/completions"),
)
deleted: Final = candidate.request("DELETE", "/config_overrides/hashicorp_vault")
assert deleted.status_code == 200, deleted.text
assert deleted.json() == {
"message": "Hashicorp Vault configuration deleted successfully",
"status": "success",
}, deleted.text
vault_wire.drain()
assert (
read_rows(
'SELECT config_type FROM "LiteLLM_ConfigOverrides" WHERE config_type = %s',
("hashicorp_vault",),
database_url=database_url,
)
== []
)
empty_readback: Final = candidate.request("GET", "/config_overrides/hashicorp_vault")
assert empty_readback.status_code == 200, empty_readback.text
empty_values: Final = object_value(
object_value(_JSON_OBJECT.validate_json(empty_readback.content))["values"]
)
assert empty_values == {
field: None
for field in (
"vault_addr",
"vault_token",
"approle_role_id",
"approle_secret_id",
"approle_mount_path",
"client_cert",
"client_key",
"vault_cert_role",
"vault_namespace",
"vault_login_namespace",
"vault_secret_namespace",
"vault_mount_name",
"vault_path_prefix",
)
}, empty_readback.text
model_after_delete: Final = scenario.model(
api_key="os.environ/LITELLM_MASTER_KEY",
api_base=provider_wire.url + "/v1",
)
response_after_delete: Final = candidate.request(
"POST",
"/v1/chat/completions",
{
"model": model_after_delete,
"messages": [{"role": "user", "content": "after-delete"}],
},
)
assert response_after_delete.status_code == 200, response_after_delete.text
assert response_after_delete.json()["choices"][0]["message"]["content"] == "after-delete", (
response_after_delete.text
)
after_delete_requests: Final = vault_wire.drain()
assert after_delete_requests == (), after_delete_requests
assert first_secret_reads + second_secret_reads == (
("GET", path_one),
("GET", path_two),
)
def test_cyberark_incremental_update_encrypts_reloads_on_restart_and_delete_clears(
gateway: Gateway, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
provider_secret: Final = "cyberark-provider-secret-" + uuid.uuid4().hex
api_key: Final = "cyberark-api-key-" + uuid.uuid4().hex
session_token: Final = "integration-session-" + uuid.uuid4().hex
account: Final = "integration-account"
username: Final = "integration-user"
auth_path: Final = f"/authn/{account}/{username}/authenticate"
default_user_auth_path: Final = f"/authn/{account}/admin/authenticate"
secret_names: Final = (
"integration-cyberark-secret-one",
"integration-cyberark-secret-two",
"integration-cyberark-secret-three",
"integration-cyberark-secret-four",
)
encoded_secret_names: Final = frozenset(quote(name, safe="") for name in secret_names)
def cyberark(request: Request) -> Reply:
if request.method == "POST" and request.target in (auth_path, default_user_auth_path):
assert request.body == api_key.encode()
return Reply(body=session_token.encode())
expected_target: Final = "/secrets/integration-account/variable/"
assert request.method == "GET"
assert request.target.startswith(expected_target), request.target
encoded_name: Final = request.target.removeprefix(expected_target)
expected_header: Final = f'Token token="{base64.b64encode(session_token.encode()).decode()}"'
assert request.headers["authorization"] == expected_header
if encoded_name not in encoded_secret_names:
return Reply(status=404, body=b"secret not found")
return Reply(body=provider_secret.encode())
def provider(request: Request) -> Reply:
if request.method == "GET":
assert request.target == "/v1/models", request.target
return Reply(status=404)
assert (request.method, request.target) == ("POST", "/v1/chat/completions"), (
f"Unexpected provider request {request.method} {request.target}"
)
body: Final = _JSON_OBJECT.validate_json(request.body)
messages: Final = body["messages"]
assert isinstance(messages, list)
marker: Final = string_value(object_value(messages[0])["content"])
if marker == "after-delete":
return _completion(marker)
assert request.headers["authorization"] == f"Bearer {provider_secret}", (
"Provider request did not use the CyberArk-resolved secret"
)
return _completion(marker)
with (
scratch_database() as database_url,
wire_server(cyberark) as cyberark_wire,
wire_server(provider) as provider_wire,
):
monkeypatch.setenv("DATABASE_URL", database_url)
environment: Final = {"DATABASE_URL": database_url}
with owned_proxy(
gateway,
tmp_path,
environment,
remove_environment=_SECRET_MANAGER_ENVIRONMENT,
) as first:
full_config: Final = {
"cyberark_api_base": cyberark_wire.url,
"cyberark_account": account,
"cyberark_username": username,
"cyberark_api_key": api_key,
"ssl_verify": "true",
"refresh_interval": "300",
}
saved: Final = first.request("POST", "/config_overrides/cyberark", full_config)
assert saved.status_code == 200, saved.text
assert saved.json() == {
"message": "CyberArk configuration updated successfully",
"status": "success",
}, saved.text
stored_full: Final = _stored_override(database_url, "cyberark")
assert set(stored_full) == set(full_config), stored_full
serialized_full: Final = json.dumps(stored_full)
assert all(value not in serialized_full for value in full_config.values()), serialized_full
model_one: Final = "integration-cyberark-" + uuid.uuid4().hex
_new_model(first, model_one, provider_wire.url, secret_names[0])
_chat(first, model_one, "cyberark-runtime-one-" + uuid.uuid4().hex)
first_proxy_requests: Final = cyberark_wire.drain()
assert any(request.target == auth_path for request in first_proxy_requests)
assert any(request.target.endswith(quote(secret_names[0], safe="")) for request in first_proxy_requests)
partial: Final = first.request(
"POST",
"/config_overrides/cyberark",
{"cyberark_api_base": cyberark_wire.url, "refresh_interval": "301"},
)
assert partial.status_code == 200, partial.text
assert partial.json() == {
"message": "CyberArk configuration updated successfully",
"status": "success",
}, partial.text
stored_partial: Final = _stored_override(database_url, "cyberark")
assert set(stored_partial) == set(full_config), stored_partial
serialized_partial: Final = json.dumps(stored_partial)
assert all(value not in serialized_partial for value in (cyberark_wire.url, account, username, api_key)), (
serialized_partial
)
assert string_value(
_override_values(first, "/config_overrides/cyberark", "cyberark")["cyberark_api_key"]
) == _masked(api_key)
model_two: Final = "integration-cyberark-" + uuid.uuid4().hex
_new_model(first, model_two, provider_wire.url, secret_names[1])
_chat(first, model_two, "cyberark-runtime-two-" + uuid.uuid4().hex)
second_proxy_requests: Final = cyberark_wire.drain()
with owned_proxy(
gateway,
tmp_path,
environment,
remove_environment=_SECRET_MANAGER_ENVIRONMENT,
) as restarted:
model_three: Final = "integration-cyberark-" + uuid.uuid4().hex
_new_model(restarted, model_three, provider_wire.url, secret_names[2])
_chat(restarted, model_three, "cyberark-runtime-three-" + uuid.uuid4().hex)
restarted_proxy_requests: Final = cyberark_wire.drain()
known_secret_names: Final = frozenset(quote(name, safe="") for name in secret_names[:3])
observed_secret_names: Final = frozenset(
request.target.rsplit("/", 1)[-1]
for request in (
*first_proxy_requests,
*second_proxy_requests,
*restarted_proxy_requests,
)
if request.method == "GET"
and request.target.startswith("/secrets/")
and request.target.rsplit("/", 1)[-1] in known_secret_names
)
assert observed_secret_names == known_secret_names
dashboard_edit: Final = restarted.request(
"POST",
"/config_overrides/cyberark",
{
"cyberark_api_base": cyberark_wire.url,
"cyberark_account": account,
"cyberark_username": "",
"client_cert": "",
"ssl_verify": "true",
"refresh_interval": "301",
},
)
assert dashboard_edit.status_code == 200, dashboard_edit.text
assert dashboard_edit.json() == {
"message": "CyberArk configuration updated successfully",
"status": "success",
}, dashboard_edit.text
stored_dashboard_edit: Final = _stored_override(database_url, "cyberark")
assert set(stored_dashboard_edit) == set(full_config) - {"cyberark_username"}, stored_dashboard_edit
serialized_dashboard_edit: Final = json.dumps(stored_dashboard_edit)
assert all(value not in serialized_dashboard_edit for value in (cyberark_wire.url, account, api_key)), (
serialized_dashboard_edit
)
dashboard_edit_readback: Final = restarted.request("GET", "/config_overrides/cyberark")
assert dashboard_edit_readback.status_code == 200, dashboard_edit_readback.text
dashboard_edit_values: Final = object_value(
object_value(_JSON_OBJECT.validate_json(dashboard_edit_readback.content))["values"]
)
assert dashboard_edit_values == {
"cyberark_api_base": cyberark_wire.url,
"cyberark_account": account,
"cyberark_api_key": _masked(api_key),
"ssl_verify": "true",
"refresh_interval": "301",
}, dashboard_edit_readback.text
model_four: Final = "integration-cyberark-" + uuid.uuid4().hex
_new_model(restarted, model_four, provider_wire.url, secret_names[3])
_chat(restarted, model_four, "cyberark-runtime-four-" + uuid.uuid4().hex)
blanked_requests: Final = cyberark_wire.drain()
assert any(
request.method == "POST" and request.target == default_user_auth_path for request in blanked_requests
), blanked_requests
assert not any(request.target == auth_path for request in blanked_requests), blanked_requests
assert any(
request.method == "GET" and request.target.endswith(quote(secret_names[3], safe=""))
for request in blanked_requests
), blanked_requests
deleted: Final = restarted.request("DELETE", "/config_overrides/cyberark")
assert deleted.status_code == 200, deleted.text
assert deleted.json() == {
"message": "CyberArk configuration deleted successfully",
"status": "success",
}, deleted.text
assert (
read_rows(
'SELECT config_type FROM "LiteLLM_ConfigOverrides" WHERE config_type = %s',
("cyberark",),
database_url=database_url,
)
== []
)
deleted_secret_reads: Final = tuple(
request.target
for request in cyberark_wire.drain()
if request.method == "GET"
and request.target.startswith("/secrets/")
and request.target.rsplit("/", 1)[-1] in encoded_secret_names
)
assert deleted_secret_reads == ()
model_after_delete: Final = "integration-cyberark-" + uuid.uuid4().hex
_new_model(restarted, model_after_delete, provider_wire.url, "LITELLM_MASTER_KEY")
_chat(restarted, model_after_delete, "after-delete")
requests_after_delete: Final = cyberark_wire.drain()
assert requests_after_delete == (), requests_after_delete
empty_readback: Final = restarted.request("GET", "/config_overrides/cyberark")
assert empty_readback.status_code == 200, empty_readback.text
empty_values: Final = object_value(
object_value(_JSON_OBJECT.validate_json(empty_readback.content))["values"]
)
assert empty_values == {
field: None
for field in (
"cyberark_api_base",
"cyberark_account",
"cyberark_username",
"cyberark_api_key",
"client_cert",
"client_key",
"ssl_verify",
"refresh_interval",
)
}, empty_readback.text
def test_vault_approle_and_cert_logins_use_login_namespace_and_drive_secret_reads(
gateway: Gateway, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
provider_secret: Final = "vault-auth-provider-secret-" + uuid.uuid4().hex
approle_token: Final = "approle-token-" + uuid.uuid4().hex
cert_token: Final = "cert-token-" + uuid.uuid4().hex
role_id: Final = "approle-role-" + uuid.uuid4().hex
secret_id: Final = "approle-secret-" + uuid.uuid4().hex
mount_path: Final = "approle-" + uuid.uuid4().hex
cert_role: Final = "cert-role-" + uuid.uuid4().hex
base_namespace: Final = "ns/base-" + uuid.uuid4().hex[:8]
login_namespace: Final = "ns/login-" + uuid.uuid4().hex[:8]
secret_namespace: Final = "ns/secrets-" + uuid.uuid4().hex[:8]
mount_name: Final = "kv-" + uuid.uuid4().hex[:8]
secret_a: Final = "secret-a-" + uuid.uuid4().hex
secret_b: Final = "secret-b-" + uuid.uuid4().hex
marker_a: Final = "vault-approle-runtime-" + uuid.uuid4().hex
marker_b: Final = "vault-cert-runtime-" + uuid.uuid4().hex
login_approle_path: Final = f"/v1/auth/{mount_path}/login"
login_cert_path: Final = "/v1/auth/cert/login"
secret_a_path: Final = f"/v1/{secret_namespace}/{mount_name}/data/{secret_a}"
secret_b_path: Final = f"/v1/{secret_namespace}/{mount_name}/data/{secret_b}"
def vault(request: Request) -> Reply:
if request.method == "POST" and request.target == login_approle_path:
assert _JSON_OBJECT.validate_json(request.body) == {
"role_id": role_id,
"secret_id": secret_id,
}, request.body
return Reply(body=json.dumps({"auth": {"client_token": approle_token, "lease_duration": 3600}}).encode())
if request.method == "POST" and request.target == login_cert_path:
assert _JSON_OBJECT.validate_json(request.body) == {"name": cert_role}, request.body
return Reply(body=json.dumps({"auth": {"client_token": cert_token, "lease_duration": 3600}}).encode())
if request.method == "GET" and request.target in {secret_a_path, secret_b_path}:
return Reply(body=json.dumps({"data": {"data": {"key": provider_secret}}}).encode())
return Reply(status=404, body=b'{"errors":["secret not found"]}')
def provider(request: Request) -> Reply:
assert request.method == "POST"
assert request.target == "/v1/chat/completions"
body: Final = _JSON_OBJECT.validate_json(request.body)
messages: Final = body["messages"]
assert isinstance(messages, list)
marker: Final = string_value(object_value(messages[0])["content"])
if marker == "after-delete":
return _completion(marker)
assert request.headers["authorization"] == f"Bearer {provider_secret}", (
"Provider request did not use the Vault-resolved secret"
)
assert marker in {marker_a, marker_b}, request.body
return _completion(marker)
cert_dir: Final = tmp_path / "vault-client-cert"
cert_dir.mkdir()
cert_file, key_file = write_self_signed_cert(cert_dir)
client_cert_der: Final = ssl.PEM_cert_to_DER_cert(cert_file.read_text())
server_cert_dir: Final = tmp_path / "vault-server-cert"
server_cert_dir.mkdir()
server_cert, server_key = write_self_signed_cert(server_cert_dir)
presented_certs: Final[list[bytes | None]] = []
class RecordingSSLSocket(ssl.SSLSocket):
def do_handshake(self) -> None:
super().do_handshake()
presented_certs.append(self.getpeercert(binary_form=True))
vault_tls_context: Final = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
vault_tls_context.load_cert_chain(server_cert, server_key)
vault_tls_context.verify_mode = ssl.CERT_OPTIONAL
vault_tls_context.load_verify_locations(cafile=str(cert_file))
vault_tls_context.sslsocket_class = RecordingSSLSocket
with (
wire_server(vault) as vault_wire,
wire_server(vault, tls=vault_tls_context) as vault_tls_wire,
wire_server(provider) as provider_wire,
):
with _config_proxy(gateway, tmp_path, monkeypatch, {"SSL_CERT_FILE": str(server_cert)}) as (
candidate,
database_url,
):
full_config: Final = {
"vault_addr": vault_wire.url,
"approle_role_id": role_id,
"approle_secret_id": secret_id,
"approle_mount_path": mount_path,
"vault_namespace": base_namespace,
"vault_login_namespace": login_namespace,
"vault_secret_namespace": secret_namespace,
"vault_mount_name": mount_name,
}
saved: Final = candidate.request("POST", "/config_overrides/hashicorp_vault", full_config)
assert saved.status_code == 200, saved.text
assert saved.json() == {
"message": "Hashicorp Vault configuration updated successfully",
"status": "success",
}, saved.text
stored_full: Final = _stored_override(database_url, "hashicorp_vault")
assert set(stored_full) == set(full_config), stored_full
serialized_full: Final = json.dumps(stored_full)
assert all(value not in serialized_full for value in full_config.values()), serialized_full
with candidate.scenario() as scenario:
model_a: Final = scenario.model(
api_key=f"os.environ/{secret_a}",
api_base=provider_wire.url + "/v1",
)
response_a: Final = candidate.request(
"POST",
"/v1/chat/completions",
{"model": model_a, "messages": [{"role": "user", "content": marker_a}]},
)
assert response_a.status_code == 200, response_a.text
assert response_a.json()["choices"][0]["message"]["content"] == marker_a, response_a.text
approle_targets: Final = frozenset({login_approle_path, login_cert_path, secret_a_path, secret_b_path})
approle_phase: Final = tuple(
request for request in vault_wire.drain() if request.target in approle_targets
)
assert tuple((request.method, request.target) for request in approle_phase) == (
("POST", login_approle_path),
("GET", secret_a_path),
), approle_phase
assert approle_phase[0].headers["x-vault-namespace"] == login_namespace
assert _JSON_OBJECT.validate_json(approle_phase[0].body) == {
"role_id": role_id,
"secret_id": secret_id,
}
assert approle_phase[1].headers["x-vault-token"] == approle_token
partial_config: Final = {
"vault_addr": vault_tls_wire.url,
"approle_role_id": "",
"approle_secret_id": "",
"client_cert": str(cert_file),
"client_key": str(key_file),
"vault_cert_role": cert_role,
}
partial: Final = candidate.request("POST", "/config_overrides/hashicorp_vault", partial_config)
assert partial.status_code == 200, partial.text
assert partial.json() == {
"message": "Hashicorp Vault configuration updated successfully",
"status": "success",
}, partial.text
vault_wire.drain()
readback: Final = _override_values(candidate, "/config_overrides/hashicorp_vault", "hashicorp_vault")
assert readback == {
"vault_addr": vault_tls_wire.url,
"approle_mount_path": mount_path,
"client_cert": str(cert_file),
"client_key": _masked(str(key_file)),
"vault_cert_role": cert_role,
"vault_namespace": base_namespace,
"vault_login_namespace": login_namespace,
"vault_secret_namespace": secret_namespace,
"vault_mount_name": mount_name,
}, readback
model_b: Final = scenario.model(
api_key=f"os.environ/{secret_b}",
api_base=provider_wire.url + "/v1",
)
response_b: Final = candidate.request(
"POST",
"/v1/chat/completions",
{"model": model_b, "messages": [{"role": "user", "content": marker_b}]},
)
assert response_b.status_code == 200, response_b.text
assert response_b.json()["choices"][0]["message"]["content"] == marker_b, response_b.text
plain_phase_b: Final = vault_wire.drain()
assert plain_phase_b == (), plain_phase_b
cert_phase: Final = tuple(
request for request in vault_tls_wire.drain() if request.target in approle_targets
)
assert all(request.target != login_approle_path for request in cert_phase), cert_phase
assert cert_phase[0].target == login_cert_path, cert_phase
cert_logins: Final = tuple(request for request in cert_phase if request.target == login_cert_path)
assert all(
request.method == "POST"
and request.headers["x-vault-namespace"] == login_namespace
and _JSON_OBJECT.validate_json(request.body) == {"name": cert_role}
for request in cert_logins
), cert_logins
cert_secret_reads: Final = tuple(
request for request in cert_phase if request.target in {secret_a_path, secret_b_path}
)
assert secret_b_path in tuple(request.target for request in cert_secret_reads), cert_secret_reads
assert all(request.headers["x-vault-token"] == cert_token for request in cert_secret_reads), (
cert_secret_reads
)
assert client_cert_der in presented_certs, presented_certs
deleted: Final = candidate.request("DELETE", "/config_overrides/hashicorp_vault")
assert deleted.status_code == 200, deleted.text
assert deleted.json() == {
"message": "Hashicorp Vault configuration deleted successfully",
"status": "success",
}, deleted.text
vault_wire.drain()
vault_tls_wire.drain()
assert (
read_rows(
'SELECT config_type FROM "LiteLLM_ConfigOverrides" WHERE config_type = %s',
("hashicorp_vault",),
database_url=database_url,
)
== []
)
model_after_delete: Final = scenario.model(
api_key="os.environ/LITELLM_MASTER_KEY",
api_base=provider_wire.url + "/v1",
)
response_after_delete: Final = candidate.request(
"POST",
"/v1/chat/completions",
{
"model": model_after_delete,
"messages": [{"role": "user", "content": "after-delete"}],
},
)
assert response_after_delete.status_code == 200, response_after_delete.text
assert response_after_delete.json()["choices"][0]["message"]["content"] == "after-delete", (
response_after_delete.text
)
after_delete_requests: Final = vault_wire.drain()
assert after_delete_requests == (), after_delete_requests
after_delete_tls_requests: Final = vault_tls_wire.drain()
assert after_delete_tls_requests == (), after_delete_tls_requests
provider_requests: Final = provider_wire.drain()
assert tuple((request.method, request.target) for request in provider_requests) == (
("POST", "/v1/chat/completions"),
("POST", "/v1/chat/completions"),
("POST", "/v1/chat/completions"),
), provider_requests