feat(proxy): CyberArk Conjur secret manager configuration via Admin UI

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yassin 2026-08-27 01:39:59 +00:00
parent f677292901
commit 32051524d9
17 changed files with 2089 additions and 18 deletions

View file

@ -6091,6 +6091,109 @@
"title": "ConfigOverrideSettingsResponse",
"type": "object"
},
"CyberArkConfig": {
"description": "Configuration for CyberArk Conjur secret manager integration.",
"properties": {
"client_cert": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Path to the client TLS certificate for certificate-based authentication",
"title": "Client Cert"
},
"client_key": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Path to the client TLS private key for certificate-based authentication",
"title": "Client Key"
},
"cyberark_account": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The Conjur organization account name",
"title": "Cyberark Account"
},
"cyberark_api_base": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The address of the CyberArk Conjur server (e.g., https://conjur.example.com)",
"title": "Cyberark Api Base"
},
"cyberark_api_key": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "API key for Conjur API-key authentication",
"title": "Cyberark Api Key"
},
"cyberark_username": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The Conjur username (login) to authenticate as",
"title": "Cyberark Username"
},
"refresh_interval": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Auth token cache TTL in seconds (default: 300)",
"title": "Refresh Interval"
},
"ssl_verify": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Set to false to disable SSL verification (e.g., for self-signed certificates)",
"title": "Ssl Verify"
}
},
"title": "CyberArkConfig",
"type": "object"
},
"HTTPValidationError": {
"properties": {
"detail": {
@ -6245,6 +6348,13 @@
},
"ValidationError": {
"properties": {
"ctx": {
"title": "Context",
"type": "object"
},
"input": {
"title": "Input"
},
"loc": {
"items": {
"anyOf": [
@ -6279,10 +6389,30 @@
}
},
"paths": {
"/config_overrides/hashicorp_vault": {
"/config_overrides/cyberark": {
"delete": {
"description": "Delete Hashicorp Vault configuration. Idempotent.",
"operationId": "delete_hashicorp_vault_config_config_overrides_hashicorp_vault_delete",
"description": "Delete CyberArk Conjur configuration. Idempotent.",
"operationId": "delete_cyberark_config_config_overrides_cyberark_delete",
"parameters": [
{
"description": "The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability",
"in": "header",
"name": "litellm-changed-by",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability",
"title": "Litellm-Changed-By"
}
}
],
"responses": {
"200": {
"content": {
@ -6291,6 +6421,184 @@
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"security": [
{
"APIKeyHeader": []
}
],
"summary": "Delete Cyberark Config",
"tags": [
"config_overrides"
]
},
"get": {
"description": "Get current CyberArk Conjur configuration.\nReturns decrypted values from DB, or falls back to current env vars.\nSensitive fields are masked before leaving the server.",
"operationId": "get_cyberark_config_config_overrides_cyberark_get",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConfigOverrideSettingsResponse"
}
}
},
"description": "Successful Response"
}
},
"security": [
{
"APIKeyHeader": []
}
],
"summary": "Get Cyberark Config",
"tags": [
"config_overrides"
]
},
"post": {
"description": "Update CyberArk Conjur secret manager configuration.\nSets environment variables, encrypts sensitive fields, and stores in DB.\nReinitializes the secret manager on this pod.",
"operationId": "update_cyberark_config_config_overrides_cyberark_post",
"parameters": [
{
"description": "The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability",
"in": "header",
"name": "litellm-changed-by",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability",
"title": "Litellm-Changed-By"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CyberArkConfig"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"security": [
{
"APIKeyHeader": []
}
],
"summary": "Update Cyberark Config",
"tags": [
"config_overrides"
]
}
},
"/config_overrides/cyberark/test_connection": {
"post": {
"description": "Test the connection to the currently configured CyberArk Conjur server.\nUses the already-initialized secret manager client. Does not modify any state.",
"operationId": "test_cyberark_connection_config_overrides_cyberark_test_connection_post",
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
}
},
"security": [
{
"APIKeyHeader": []
}
],
"summary": "Test Cyberark Connection",
"tags": [
"config_overrides"
]
}
},
"/config_overrides/hashicorp_vault": {
"delete": {
"description": "Delete Hashicorp Vault configuration. Idempotent.",
"operationId": "delete_hashicorp_vault_config_config_overrides_hashicorp_vault_delete",
"parameters": [
{
"description": "The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability",
"in": "header",
"name": "litellm-changed-by",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability",
"title": "Litellm-Changed-By"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"security": [
@ -6331,6 +6639,26 @@
"post": {
"description": "Update Hashicorp Vault secret manager configuration.\nSets environment variables, encrypts sensitive fields, and stores in DB.\nReinitializes the secret manager on this pod.",
"operationId": "update_hashicorp_vault_config_config_overrides_hashicorp_vault_post",
"parameters": [
{
"description": "The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability",
"in": "header",
"name": "litellm-changed-by",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability",
"title": "Litellm-Changed-By"
}
}
],
"requestBody": {
"content": {
"application/json": {

View file

@ -3,7 +3,7 @@ import json
import os
from collections.abc import Mapping, Sequence
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Final, Protocol
from typing import TYPE_CHECKING, Final, Protocol
from fastapi import APIRouter, Depends, Header, HTTPException
from pydantic import BaseModel, TypeAdapter
@ -36,10 +36,12 @@ from litellm.repositories.table_repositories import ConfigOverridesRepository
from litellm.types.llms.custom_http import httpxSpecialProvider
from litellm.types.proxy.management_endpoints.config_overrides import (
ConfigOverrideSettingsResponse,
CyberArkConfig,
HashicorpVaultConfig,
)
if TYPE_CHECKING:
from litellm.proxy.proxy_server import ProxyConfig
from litellm.proxy.utils import PrismaClient
router: Final = APIRouter()
@ -83,18 +85,19 @@ def _log_audit_task_exception(task: "asyncio.Task[None]") -> None:
return
exc: Final = task.exception()
if exc is not None:
verbose_proxy_logger.warning("Failed to write hashicorp-vault config audit log: %s", exc)
verbose_proxy_logger.warning("Failed to write config override audit log: %s", exc)
async def _emit_hashicorp_vault_audit_log(
async def _emit_config_override_audit_log(
*,
object_id: str,
action: AUDIT_ACTIONS,
before_config: Mapping[str, object] | None,
after_config: Mapping[str, object] | None,
user_api_key_dict: UserAPIKeyAuth,
litellm_changed_by: str | None,
) -> None:
"""Emit an audit-log row for a /config_overrides/hashicorp_vault mutation.
"""Emit an audit-log row for a /config_overrides/{object_id} mutation.
Mirrors the ``store_audit_logs``-gated pattern from
``team_callback_endpoints.py``. Captured under
@ -118,7 +121,7 @@ async def _emit_hashicorp_vault_audit_log(
changed_by=litellm_changed_by or user_api_key_dict.user_id or litellm_proxy_admin_name,
changed_by_api_key=user_api_key_dict.api_key,
table_name=LitellmTableNames.CONFIG_OVERRIDES_TABLE_NAME,
object_id="hashicorp_vault",
object_id=object_id,
action=action,
updated_values=json.dumps({"config": _redact_config(after_config)}, default=str),
before_value=json.dumps({"config": _redact_config(before_config)}, default=str),
@ -150,6 +153,24 @@ HASHICORP_SENSITIVE_FIELDS: Final[set[str]] = {
"client_key",
}
# --- CyberArk Conjur constants ---
CYBERARK_ENV_VAR_MAPPING: Final[dict[str, str]] = { # mutable-ok: module-level env mapping
"cyberark_api_base": "CYBERARK_API_BASE",
"cyberark_account": "CYBERARK_ACCOUNT",
"cyberark_username": "CYBERARK_USERNAME",
"cyberark_api_key": "CYBERARK_API_KEY",
"client_cert": "CYBERARK_CLIENT_CERT",
"client_key": "CYBERARK_CLIENT_KEY",
"ssl_verify": "CYBERARK_SSL_VERIFY",
"refresh_interval": "CYBERARK_REFRESH_INTERVAL",
}
CYBERARK_SENSITIVE_FIELDS: Final[set[str]] = { # mutable-ok: module-level constant, mirrors HASHICORP_SENSITIVE_FIELDS
"cyberark_api_key",
"client_key",
}
_sensitive_masker: Final = SensitiveDataMasker()
@ -215,9 +236,12 @@ def _parse_config_value(raw: str | Mapping[str, object]) -> dict[str, object]:
return dict(raw)
def _set_env_vars(config_data: Mapping[str, object]) -> None:
"""Set HCP_VAULT_* env vars from config data. Unsets vars for missing/None/empty fields."""
for field_name, env_var_name in HASHICORP_ENV_VAR_MAPPING.items():
def _set_env_vars(
config_data: Mapping[str, object],
env_var_mapping: Mapping[str, str] = HASHICORP_ENV_VAR_MAPPING,
) -> None:
"""Set mapped env vars from config data. Unsets vars for missing/None/empty fields."""
for field_name, env_var_name in env_var_mapping.items():
value = config_data.get(field_name)
if value is not None and value != "":
os.environ[env_var_name] = str(value)
@ -225,13 +249,22 @@ def _set_env_vars(config_data: Mapping[str, object]) -> None:
os.environ.pop(env_var_name, None)
def _clear_hashicorp_vault_state(proxy_config: Any) -> None:
def _clear_hashicorp_vault_state(proxy_config: "ProxyConfig") -> None:
"""Clear all Hashicorp Vault state: env vars, secret manager, and change-detection cache."""
_set_env_vars({})
if litellm._key_management_system == KeyManagementSystem.HASHICORP_VAULT:
litellm.secret_manager_client = None
litellm._key_management_system = None
proxy_config._last_hashicorp_vault_config = None
proxy_config._last_hashicorp_vault_config = None # pyright: ignore[reportPrivateUsage] # proxy-internal change-detection cache
def _clear_cyberark_state(proxy_config: "ProxyConfig") -> None:
"""Clear all CyberArk state: env vars, secret manager, and change-detection cache."""
_set_env_vars({}, CYBERARK_ENV_VAR_MAPPING) # mutable-ok: empty payload unsets all mapped env vars
if litellm._key_management_system == KeyManagementSystem.CYBERARK: # pyright: ignore[reportPrivateUsage] # proxy-internal helper, mirrors hashicorp endpoint usage
litellm.secret_manager_client = None
litellm._key_management_system = None # pyright: ignore[reportPrivateUsage] # proxy-internal helper, mirrors hashicorp endpoint usage
proxy_config._last_cyberark_config = None # pyright: ignore[reportPrivateUsage] # proxy-internal helper, mirrors hashicorp endpoint usage
# --- Hashicorp Vault endpoints ---
@ -358,7 +391,8 @@ async def update_hashicorp_vault_config(
# row was absent or its ``config_value`` was NULL.
before_config: Final = existing_decrypted if existing_decrypted is not None else env_values
action: Final[AUDIT_ACTIONS] = "updated" if existing_record is not None else "created"
await _emit_hashicorp_vault_audit_log(
await _emit_config_override_audit_log(
object_id="hashicorp_vault",
action=action,
before_config=before_config,
after_config=config_data,
@ -484,7 +518,8 @@ async def delete_hashicorp_vault_config(
# Only emit audit log if a row was actually removed; an idempotent
# delete on a non-existent row produces no security-relevant change.
if deleted:
await _emit_hashicorp_vault_audit_log(
await _emit_config_override_audit_log(
object_id="hashicorp_vault",
action="deleted",
before_config=before_config,
after_config=None,
@ -529,7 +564,7 @@ async def test_hashicorp_vault_connection(
# Step 1: Authenticate (exercises AppRole login, TLS cert login, or direct token)
try:
headers: Final[dict[str, str]] = await asyncio.to_thread(client._get_request_headers)
headers: Final[Mapping[str, str]] = await asyncio.to_thread(client._get_request_headers)
except Exception as e:
raise HTTPException(
status_code=502,
@ -554,3 +589,312 @@ async def test_hashicorp_vault_connection(
"status": "success",
"message": f"Successfully connected to Vault at {client.vault_addr}",
}
# --- CyberArk Conjur endpoints ---
@router.post(
"/config_overrides/cyberark",
tags=["Config Overrides"], # mutable-ok: FastAPI route decorator metadata
dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI route decorator metadata
)
async def update_cyberark_config(
config: CyberArkConfig,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), # noqa: B008 # FastAPI dependency injection
litellm_changed_by: str | None = Header(
None,
description="The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability",
),
):
"""
Update CyberArk Conjur secret manager configuration.
Sets environment variables, encrypts sensitive fields, and stores in DB.
Reinitializes the secret manager on this pod.
"""
from litellm.proxy.proxy_server import prisma_client, proxy_config
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
raise HTTPException(
status_code=403,
detail="Only admin users can update config overrides",
)
if prisma_client is None:
raise HTTPException(
status_code=500,
detail=CommonProxyErrors.db_not_connected_error.value,
)
config_data: dict[str, object] = config.model_dump(exclude_none=True) # mutable-ok: merged # rebind-ok: stripped
# Merge ALL fields the user didn't send: try DB first, fall back to env vars.
# Omitted field = keep existing; empty string = clear/remove the field.
existing_record: Final = await _config_overrides_table(prisma_client).find_unique(
where={"config_type": "cyberark"} # mutable-ok: prisma where clause
)
existing_decrypted: dict[str, object] | None = None # mutable-ok: DB payload # rebind-ok: set when record exists
env_values: dict[str, str | None] = {} # mutable-ok: env snapshot # rebind-ok: populated when no DB record exists
if existing_record is not None and existing_record.config_value is not None:
existing_data: Final = _parse_config_value(existing_record.config_value)
existing_decrypted = proxy_config._decrypt_db_variables(existing_data) # pyright: ignore[reportPrivateUsage] # rebind-ok: populated when a prior record decrypts
for field in CYBERARK_ENV_VAR_MAPPING:
if field not in config_data and existing_decrypted.get(field):
config_data[field] = existing_decrypted[field]
else:
env_values = _get_current_env_values(CYBERARK_ENV_VAR_MAPPING) # rebind-ok: populated when no DB record exists
for field in CYBERARK_ENV_VAR_MAPPING:
if field not in config_data and env_values.get(field):
config_data[field] = env_values[field]
# Strip empty strings — they signal "clear this field"
config_data = {k: v for k, v in config_data.items() if v != ""} # mutable-ok: dict # rebind-ok: strip clears
has_api_base: Final = bool(config_data.get("cyberark_api_base"))
has_api_key_auth: Final = bool(config_data.get("cyberark_api_key"))
has_tls_cert_auth: Final = bool(config_data.get("client_cert") and config_data.get("client_key"))
if not has_api_base:
raise HTTPException(
status_code=400,
detail="CyberArk API Base is required",
)
if not has_api_key_auth and not has_tls_cert_auth:
raise HTTPException(
status_code=400,
detail="At least one authentication method is required: "
"provide an API Key, or both Client Certificate and Client Key",
)
# Snapshot current env vars so we can restore on failure
previous_env: Final = _get_current_env_values(CYBERARK_ENV_VAR_MAPPING)
# Set env vars and verify the secret manager can initialize before persisting
_set_env_vars(config_data, CYBERARK_ENV_VAR_MAPPING)
try:
proxy_config.initialize_secret_manager(key_management_system="cyberark")
except Exception as e: # noqa: BLE001 # any init failure must roll back env vars
_set_env_vars(previous_env, CYBERARK_ENV_VAR_MAPPING)
verbose_proxy_logger.exception("Error reinitializing CyberArk secret manager: %s", str(e))
raise HTTPException(
status_code=500,
detail=f"Failed to initialize secret manager: {e}",
)
# Only persist to DB after successful init
encrypted_data: Final = proxy_config._encrypt_env_variables(config_data) # pyright: ignore[reportPrivateUsage] # proxy-internal helper, mirrors hashicorp endpoint usage
config_value: Final = safe_dumps(encrypted_data)
await _config_overrides_table(prisma_client).upsert(
where={"config_type": "cyberark"}, # mutable-ok: prisma upsert payload
data={ # mutable-ok: prisma upsert payload
"create": { # mutable-ok: prisma upsert payload
"config_type": "cyberark",
"config_value": config_value,
},
"update": { # mutable-ok: prisma upsert payload
"config_value": config_value,
},
},
)
# Update change-detection cache so the background reload doesn't redundantly re-init
proxy_config._last_cyberark_config = safe_json_loads(config_value) # pyright: ignore[reportPrivateUsage] # proxy-internal helper, mirrors hashicorp endpoint usage
before_config: Final = existing_decrypted if existing_decrypted is not None else env_values
action: Final[AUDIT_ACTIONS] = "updated" if existing_record is not None else "created"
await _emit_config_override_audit_log(
object_id="cyberark",
action=action,
before_config=before_config,
after_config=config_data,
user_api_key_dict=user_api_key_dict,
litellm_changed_by=litellm_changed_by,
)
return { # mutable-ok: JSON response payload
"message": "CyberArk configuration updated successfully",
"status": "success",
}
@router.get(
"/config_overrides/cyberark",
tags=["Config Overrides"], # mutable-ok: FastAPI route decorator metadata
dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI route decorator metadata
response_model=ConfigOverrideSettingsResponse,
)
async def get_cyberark_config(
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), # noqa: B008 # FastAPI dependency injection
):
"""
Get current CyberArk Conjur configuration.
Returns decrypted values from DB, or falls back to current env vars.
Sensitive fields are masked before leaving the server.
"""
from litellm.proxy.management_endpoints.common_utils import (
_user_has_admin_view, # pyright: ignore[reportPrivateUsage] # proxy-internal helper, mirrors hashicorp endpoint usage
)
from litellm.proxy.proxy_server import prisma_client, proxy_config
if not _user_has_admin_view(user_api_key_dict):
raise HTTPException(
status_code=403,
detail="Only admin users can view config overrides",
)
if prisma_client is None:
raise HTTPException(
status_code=500,
detail=CommonProxyErrors.db_not_connected_error.value,
)
field_schema: Final = _build_field_schema(CyberArkConfig)
db_record: Final = await _config_overrides_table(prisma_client).find_unique(
where={"config_type": "cyberark"}
) # mutable-ok: prisma where clause
if db_record is not None and db_record.config_value is not None:
config_data: Final = _parse_config_value(db_record.config_value)
# Decrypt then mask sensitive fields so plaintext secrets are never sent to the UI
decrypted_data: Final[Mapping[str, object]] = proxy_config._decrypt_db_variables(config_data) # pyright: ignore[reportPrivateUsage] # proxy-internal helper, mirrors hashicorp endpoint usage
masked_data: Final = _mask_sensitive_fields(decrypted_data, CYBERARK_SENSITIVE_FIELDS)
return ConfigOverrideSettingsResponse(
config_type="cyberark",
values=masked_data,
field_schema=field_schema,
)
env_values: Final = _get_current_env_values(CYBERARK_ENV_VAR_MAPPING)
masked_env_values: Final = _mask_sensitive_fields(env_values, CYBERARK_SENSITIVE_FIELDS)
return ConfigOverrideSettingsResponse(
config_type="cyberark",
values=masked_env_values,
field_schema=field_schema,
)
@router.delete(
"/config_overrides/cyberark",
tags=["Config Overrides"], # mutable-ok: FastAPI route decorator metadata
dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI route decorator metadata
)
async def delete_cyberark_config(
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), # noqa: B008 # FastAPI dependency injection
litellm_changed_by: str | None = Header(
None,
description="The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability",
),
):
"""Delete CyberArk Conjur configuration. Idempotent."""
from litellm.proxy.proxy_server import prisma_client, proxy_config
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
raise HTTPException(
status_code=403,
detail="Only admin users can delete config overrides",
)
if prisma_client is None:
raise HTTPException(
status_code=500,
detail=CommonProxyErrors.db_not_connected_error.value,
)
existing_record: Final = await _config_overrides_table(prisma_client).find_unique(
where={"config_type": "cyberark"} # mutable-ok: prisma where clause
)
before_config: dict[str, object] | None = None # mutable-ok: audit snapshot # rebind-ok: set when decrypts
if existing_record is not None and existing_record.config_value is not None:
try:
before_config = proxy_config._decrypt_db_variables(_parse_config_value(existing_record.config_value)) # pyright: ignore[reportPrivateUsage] # rebind-ok: populated when the prior record decrypts
except Exception: # noqa: BLE001 # undecryptable prior config must not block deletion
before_config = None # rebind-ok: reset when decryption fails
deleted = False # rebind-ok: set true once the DB row is removed
try:
await _config_overrides_table(prisma_client).delete(
where={"config_type": "cyberark"}
) # mutable-ok: prisma where clause
deleted = True # rebind-ok: set true once the DB row is removed
except RecordNotFoundError:
verbose_proxy_logger.debug("No existing CyberArk config record to delete")
_clear_cyberark_state(proxy_config)
if deleted:
await _emit_config_override_audit_log(
object_id="cyberark",
action="deleted",
before_config=before_config,
after_config=None,
user_api_key_dict=user_api_key_dict,
litellm_changed_by=litellm_changed_by,
)
return { # mutable-ok: JSON response payload
"message": "CyberArk configuration deleted successfully",
"status": "success",
}
@router.post(
"/config_overrides/cyberark/test_connection",
tags=["Config Overrides"], # mutable-ok: FastAPI route decorator metadata
dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI route decorator metadata
)
async def test_cyberark_connection(
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), # noqa: B008 # FastAPI dependency injection
):
"""
Test the connection to the currently configured CyberArk Conjur server.
Uses the already-initialized secret manager client. Does not modify any state.
"""
from litellm.secret_managers.cyberark_secret_manager import CyberArkSecretManager
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
raise HTTPException(
status_code=403,
detail="Only admin users can test CyberArk connection",
)
client: Final = litellm.secret_manager_client
if not isinstance(client, CyberArkSecretManager):
raise HTTPException(
status_code=400,
detail="CyberArk is not configured. Save a configuration first.",
)
# Step 1: Authenticate (exercises API-key or TLS cert login)
try:
headers: Final[Mapping[str, str]] = await asyncio.to_thread(client._get_request_headers) # pyright: ignore[reportPrivateUsage] # proxy-internal helper, mirrors hashicorp endpoint usage
except Exception as e: # noqa: BLE001 # surface any auth failure as a 502 with detail
raise HTTPException(
status_code=502,
detail=f"CyberArk authentication failed: {e}",
)
# Step 2: Verify the token is valid via /whoami
try:
async_client: Final = get_async_httpx_client(
llm_provider=httpxSpecialProvider.SecretManager,
params={"ssl_verify": client.ssl_verify}, # mutable-ok: httpx client params
)
whoami_url: Final = f"{client.conjur_addr}/whoami"
response: Final = await async_client.get(whoami_url, headers=headers)
response.raise_for_status()
except Exception as e: # noqa: BLE001 # surface any connectivity/TLS failure as a 502 with detail
raise HTTPException(
status_code=502,
detail=f"CyberArk token validation failed: {e}",
)
return { # mutable-ok: JSON response payload
"status": "success",
"message": f"Successfully connected to CyberArk Conjur at {client.conjur_addr}",
}

View file

@ -4238,6 +4238,7 @@ class ProxyConfig:
self.config: dict[str, Any] = {}
self._last_semantic_filter_config: dict[str, object] | None = None
self._last_hashicorp_vault_config: dict[str, object] | None = None
self._last_cyberark_config: dict[str, object] | None = None # mutable-ok: change-detection cache
self.worker_registry: list[WorkerRegistryEntry] = []
self.config_sync_subscriber: ConfigSyncSubscriber | None = None
self.auth_cache_invalidation_subscriber: AuthCacheInvalidationSubscriber | None = None
@ -6918,6 +6919,7 @@ class ProxyConfig:
if self._should_load_db_object(object_type="config_overrides"):
await self._init_hashicorp_vault_config_override(prisma_client=prisma_client)
await self._init_cyberark_config_override(prisma_client=prisma_client)
await self._apply_safe_litellm_settings_overrides_from_db(prisma_client=prisma_client)
@ -7082,6 +7084,66 @@ class ProxyConfig:
str(e),
)
async def _init_cyberark_config_override(self, prisma_client: PrismaClient) -> None:
"""
Load CyberArk Conjur config override from DB.
Decrypts sensitive fields, sets CYBERARK_* env vars, and reinitializes the secret manager.
Called periodically via _init_non_llm_objects_in_db to sync config across pods.
"""
from litellm.proxy.management_endpoints.config_override_endpoints import (
CYBERARK_ENV_VAR_MAPPING,
_clear_cyberark_state, # pyright: ignore[reportPrivateUsage] # module-internal helper shared with the endpoint module
_get_current_env_values, # pyright: ignore[reportPrivateUsage] # module-internal helper shared with the endpoint module
_parse_config_value, # pyright: ignore[reportPrivateUsage] # module-internal helper shared with the endpoint module
_set_env_vars, # pyright: ignore[reportPrivateUsage] # module-internal helper shared with the endpoint module
)
try:
db_record: Final[_ConfigOverridesRow | None] = cast( # cast-ok: prisma Json stub is `str`, runtime dict
"_ConfigOverridesRow | None",
await call_with_db_reconnect_retry(
prisma_client,
lambda: ConfigOverridesRepository(prisma_client).table.find_unique(
where={"config_type": "cyberark"} # mutable-ok: prisma where clause
),
reason="init_cyberark_config_override_lookup_failure",
),
)
if db_record is None or db_record.config_value is None:
if self._last_cyberark_config is not None:
_clear_cyberark_state(self)
return
config_data: Final = _parse_config_value(db_record.config_value)
# Skip reinit if config hasn't changed since last poll
if self._last_cyberark_config == config_data:
return
# Decrypt all fields and set env vars
decrypted_data: Final = self._decrypt_db_variables(config_data)
# Snapshot current env vars so we can restore on failure
previous_env: Final = _get_current_env_values(CYBERARK_ENV_VAR_MAPPING)
_set_env_vars(decrypted_data, CYBERARK_ENV_VAR_MAPPING)
# Reinitialize the secret manager
try:
self.initialize_secret_manager(key_management_system="cyberark")
except Exception:
# Restore previous working env vars instead of wiping all
_set_env_vars(previous_env, CYBERARK_ENV_VAR_MAPPING)
raise
self._last_cyberark_config = config_data.copy()
verbose_proxy_logger.debug("CyberArk config override loaded from DB")
except Exception as e: # noqa: BLE001 # any DB/decrypt/init failure must not break proxy boot
verbose_proxy_logger.exception(
"Error loading CyberArk config override from DB: %s",
str(e),
)
async def check_periodic_reloads(self, prisma_client: PrismaClient):
"""
Run the admin-configured periodic model cost map reload.

View file

@ -52,6 +52,43 @@ class HashicorpVaultConfig(BaseModel):
)
class CyberArkConfig(BaseModel):
"""Configuration for CyberArk Conjur secret manager integration."""
cyberark_api_base: str | None = Field(
default=None,
description="The address of the CyberArk Conjur server (e.g., https://conjur.example.com)",
)
cyberark_account: str | None = Field(
default=None,
description="The Conjur organization account name",
)
cyberark_username: str | None = Field(
default=None,
description="The Conjur username (login) to authenticate as",
)
cyberark_api_key: str | None = Field(
default=None,
description="API key for Conjur API-key authentication",
)
client_cert: str | None = Field(
default=None,
description="Path to the client TLS certificate for certificate-based authentication",
)
client_key: str | None = Field(
default=None,
description="Path to the client TLS private key for certificate-based authentication",
)
ssl_verify: str | None = Field(
default=None,
description="Set to false to disable SSL verification (e.g., for self-signed certificates)",
)
refresh_interval: str | None = Field(
default=None,
description="Auth token cache TTL in seconds (default: 300)",
)
class ConfigOverrideSettingsResponse(BaseModel):
"""Response model for config override settings GET endpoints."""

View file

@ -11,16 +11,19 @@ import litellm
import litellm.proxy.proxy_server as ps
from litellm.proxy._types import KeyManagementSystem, LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy.management_endpoints.config_override_endpoints import (
CYBERARK_ENV_VAR_MAPPING,
HASHICORP_ENV_VAR_MAPPING,
_build_field_schema,
_set_env_vars,
)
from litellm.proxy.proxy_server import app
from litellm.types.proxy.management_endpoints.config_overrides import (
CyberArkConfig,
HashicorpVaultConfig,
)
VAULT_URL = "/config_overrides/hashicorp_vault"
CYBERARK_URL = "/config_overrides/cyberark"
@pytest.fixture
@ -67,6 +70,8 @@ def _cleanup():
app.dependency_overrides.pop(ps.user_api_key_auth, None)
for env_var in HASHICORP_ENV_VAR_MAPPING.values():
os.environ.pop(env_var, None)
for env_var in CYBERARK_ENV_VAR_MAPPING.values():
os.environ.pop(env_var, None)
def _set_admin():
@ -275,6 +280,275 @@ async def test_hashicorp_vault_validation_errors_and_access_control(
_cleanup()
@pytest.mark.asyncio
async def test_cyberark_crud_lifecycle(client, monkeypatch):
"""Create → read (masked) → partial update (merge from DB) → clear field →
delete → idempotent delete → env fallback → merge from env → schema."""
mock_prisma, mock_db = _make_mock_db()
mock_cfg = _make_mock_proxy_config()
mock_cfg._last_cyberark_config = None
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
monkeypatch.setattr(ps, "proxy_config", mock_cfg)
old_client, old_kms = litellm.secret_manager_client, litellm._key_management_system
_set_admin()
try:
# 1. POST: create with API-key auth
r = client.post(
CYBERARK_URL,
json={
"cyberark_api_base": "https://conjur.example.com",
"cyberark_account": "myorg",
"cyberark_username": "litellm-user",
"cyberark_api_key": "my-secret-api-key",
},
)
assert r.status_code == 200
assert os.environ["CYBERARK_API_BASE"] == "https://conjur.example.com"
assert os.environ["CYBERARK_API_KEY"] == "my-secret-api-key"
data = _upserted_data(mock_db)
assert data["cyberark_api_key"] == "enc_my-secret-api-key"
mock_cfg.initialize_secret_manager.assert_called_with(
key_management_system="cyberark"
)
assert mock_cfg._last_cyberark_config is not None
# 2. GET: sensitive fields masked
mock_db.find_unique = AsyncMock(return_value=_db_record(data))
r = client.get(CYBERARK_URL)
assert r.status_code == 200
vals = r.json()["values"]
assert vals["cyberark_api_base"] == "https://conjur.example.com"
assert "*" in vals["cyberark_api_key"]
assert "properties" in r.json()["field_schema"]
# 3. POST partial: omitted fields merge from DB
r = client.post(CYBERARK_URL, json={"cyberark_api_base": "https://conjur.new.com"})
assert r.status_code == 200
data = _upserted_data(mock_db)
assert data["cyberark_api_base"] == "enc_https://conjur.new.com"
assert data["cyberark_api_key"] == "enc_my-secret-api-key"
assert data["cyberark_account"] == "enc_myorg"
# 4. POST empty string: clears field, switches to cert auth
step3 = {
**data,
"client_cert": "enc_/certs/client.pem",
"client_key": "enc_/certs/client.key",
}
mock_db.find_unique = AsyncMock(return_value=_db_record(step3))
mock_db.upsert = AsyncMock(return_value=None)
r = client.post(CYBERARK_URL, json={"cyberark_api_key": ""})
assert r.status_code == 200
data = _upserted_data(mock_db)
assert "cyberark_api_key" not in data
assert data["client_cert"] == "enc_/certs/client.pem"
# 5. DELETE: clears everything
litellm.secret_manager_client = MagicMock() # test-quality-ok: endpoint hot-reloads litellm globals; test must set and restore them
litellm._key_management_system = KeyManagementSystem.CYBERARK # test-quality-ok: endpoint hot-reloads litellm globals; test must set and restore them
r = client.delete(CYBERARK_URL)
assert r.status_code == 200
assert os.environ.get("CYBERARK_API_BASE") is None
assert litellm.secret_manager_client is None
assert mock_cfg._last_cyberark_config is None
# 6. DELETE idempotent
mock_db.delete = AsyncMock(
side_effect=RecordNotFoundError(
data={"clientVersion": "0.0.0"}, message="Not found"
)
)
assert client.delete(CYBERARK_URL).status_code == 200
# 7. GET: env var fallback with masking
mock_db.find_unique = AsyncMock(return_value=None)
monkeypatch.setenv("CYBERARK_API_BASE", "https://conjur.env.com")
monkeypatch.setenv("CYBERARK_API_KEY", "env-api-key")
r = client.get(CYBERARK_URL)
vals = r.json()["values"]
assert vals["cyberark_api_base"] == "https://conjur.env.com"
assert "*" in vals["cyberark_api_key"]
# 8. POST: merge from env vars
mock_cfg.initialize_secret_manager = MagicMock()
mock_db.upsert = AsyncMock(return_value=None)
r = client.post(CYBERARK_URL, json={"cyberark_api_base": "https://conjur.merged.com"})
assert r.status_code == 200
data = _upserted_data(mock_db)
assert data["cyberark_api_key"] == "enc_env-api-key"
# 9. _build_field_schema
schema = _build_field_schema(CyberArkConfig)
assert "cyberark_api_base" in schema["properties"]
assert len(schema["properties"]["cyberark_api_base"]["description"]) > 0
finally:
litellm.secret_manager_client = old_client # test-quality-ok: endpoint hot-reloads litellm globals; test must set and restore them
litellm._key_management_system = old_kms # test-quality-ok: endpoint hot-reloads litellm globals; test must set and restore them
_cleanup()
@pytest.mark.asyncio
async def test_cyberark_validation_errors_and_access_control(client, monkeypatch):
"""Validation (missing api base, missing auth, init failure rollback),
DELETE preserves non-CyberArk secret managers, non-admin 403."""
mock_prisma, mock_db = _make_mock_db()
mock_cfg = MagicMock()
mock_cfg._last_cyberark_config = {"cyberark_api_base": "old"}
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
monkeypatch.setattr(ps, "proxy_config", mock_cfg)
old_client, old_kms = litellm.secret_manager_client, litellm._key_management_system
_set_admin()
try:
# 1. Missing cyberark_api_base → 400
r = client.post(CYBERARK_URL, json={"cyberark_api_key": "key"})
assert r.status_code == 400
assert "API Base" in r.json()["detail"]
# 2. Missing auth → 400 (cert without key is not valid auth)
r = client.post(
CYBERARK_URL,
json={"cyberark_api_base": "https://c.com", "client_cert": "/c.pem"},
)
assert r.status_code == 400
assert "authentication" in r.json()["detail"].lower()
# 3. Init failure → 500, env vars restored, nothing persisted
mock_cfg.initialize_secret_manager = MagicMock(side_effect=Exception("fail"))
monkeypatch.setenv("CYBERARK_API_BASE", "https://conjur.old.com")
monkeypatch.setenv("CYBERARK_API_KEY", "old-key")
r = client.post(
CYBERARK_URL,
json={"cyberark_api_base": "https://bad.com", "cyberark_api_key": "bad"},
)
assert r.status_code == 500
assert os.environ["CYBERARK_API_BASE"] == "https://conjur.old.com"
mock_db.upsert.assert_not_awaited()
# 4. DELETE preserves non-CyberArk secret manager
aws = MagicMock()
litellm.secret_manager_client = aws # test-quality-ok: endpoint hot-reloads litellm globals; test must set and restore them
litellm._key_management_system = KeyManagementSystem.AWS_SECRET_MANAGER # test-quality-ok: endpoint hot-reloads litellm globals; test must set and restore them
assert client.delete(CYBERARK_URL).status_code == 200
assert litellm.secret_manager_client is aws
# 5. Non-admin → 403
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
user_role=LitellmUserRoles.INTERNAL_USER, user_id="user"
)
assert client.get(CYBERARK_URL).status_code == 403
assert (
client.post(
CYBERARK_URL, json={"cyberark_api_base": "https://c.com"}
).status_code
== 403
)
assert client.delete(CYBERARK_URL).status_code == 403
assert client.post(CYBERARK_URL + "/test_connection").status_code == 403
finally:
litellm.secret_manager_client = old_client # test-quality-ok: endpoint hot-reloads litellm globals; test must set and restore them
litellm._key_management_system = old_kms # test-quality-ok: endpoint hot-reloads litellm globals; test must set and restore them
_cleanup()
@pytest.mark.asyncio
async def test_cyberark_audit_log_redacts_values(client, monkeypatch):
monkeypatch.setattr(litellm, "store_audit_logs", True)
mock_prisma, mock_db = _make_mock_db()
mock_cfg = _make_mock_proxy_config()
mock_cfg._last_cyberark_config = None
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
monkeypatch.setattr(ps, "proxy_config", mock_cfg)
_set_admin()
audit_calls = []
async def capture(request_data):
audit_calls.append(request_data)
try:
with patch( # test-quality-ok: patching proxy-internal collaborator to isolate the endpoint
"litellm.proxy.management_helpers.audit_logs.create_audit_log_for_update",
new=capture,
):
r = client.post(
CYBERARK_URL,
json={
"cyberark_api_base": "https://conjur.example.com",
"cyberark_api_key": "my-very-secret-key",
},
)
assert r.status_code == 200
for _ in range(3):
await asyncio.sleep(0)
assert len(audit_calls) == 1
log = audit_calls[0]
assert log.action == "created"
assert log.object_id == "cyberark"
assert "my-very-secret-key" not in log.updated_values
assert "conjur.example.com" not in log.updated_values
after = json.loads(log.updated_values)
assert "cyberark_api_key" in after["config"]
assert "cyberark_api_base" in after["config"]
finally:
_cleanup()
@pytest.mark.asyncio
async def test_cyberark_test_connection(client, monkeypatch):
"""400 when not configured; success path authenticates and hits /whoami."""
from litellm.secret_managers.cyberark_secret_manager import CyberArkSecretManager
old_client, old_kms = litellm.secret_manager_client, litellm._key_management_system
_set_admin()
try:
# Not configured → 400
litellm.secret_manager_client = None # test-quality-ok: endpoint hot-reloads litellm globals; test must set and restore them
r = client.post(CYBERARK_URL + "/test_connection")
assert r.status_code == 400
assert "not configured" in r.json()["detail"].lower()
# Configured → authenticates and calls /whoami
mock_manager = MagicMock(spec=CyberArkSecretManager)
mock_manager.conjur_addr = "https://conjur.example.com"
mock_manager.ssl_verify = True
mock_manager._get_request_headers = MagicMock(
return_value={"Authorization": "Token abc"}
)
litellm.secret_manager_client = mock_manager # test-quality-ok: endpoint hot-reloads litellm globals; test must set and restore them
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_http = MagicMock()
mock_http.get = AsyncMock(return_value=mock_response)
with patch( # test-quality-ok: patching proxy-internal collaborator to isolate the endpoint
"litellm.proxy.management_endpoints.config_override_endpoints.get_async_httpx_client",
return_value=mock_http,
):
r = client.post(CYBERARK_URL + "/test_connection")
assert r.status_code == 200
assert "conjur.example.com" in r.json()["message"]
called_url = mock_http.get.call_args.args[0]
assert called_url == "https://conjur.example.com/whoami"
# Auth failure → 502
mock_manager._get_request_headers = MagicMock(
side_effect=Exception("bad credentials")
)
r = client.post(CYBERARK_URL + "/test_connection")
assert r.status_code == 502
assert "authentication failed" in r.json()["detail"].lower()
finally:
litellm.secret_manager_client = old_client # test-quality-ok: endpoint hot-reloads litellm globals; test must set and restore them
litellm._key_management_system = old_kms # test-quality-ok: endpoint hot-reloads litellm globals; test must set and restore them
_cleanup()
# ── Audit-log emission for /config_overrides/hashicorp_vault ─────────────────

View file

@ -18,6 +18,7 @@ import LoggingSettings from "@/components/Settings/AdminSettings/LoggingSettings
import SSOSettings from "@/components/Settings/AdminSettings/SSOSettings/SSOSettings";
import UISettings from "@/components/Settings/AdminSettings/UISettings/UISettings";
import UserBannerSettings from "@/components/Settings/AdminSettings/UserBannerSettings/UserBannerSettings";
import CyberArk from "@/components/Settings/AdminSettings/CyberArk/CyberArk";
import HashicorpVault from "@/components/Settings/AdminSettings/HashicorpVault/HashicorpVault";
import PluginSettings from "@/components/Settings/AdminSettings/PluginSettings/PluginSettings";
import SSOModals from "@/components/SSOModals";
@ -395,6 +396,11 @@ const AdminPanel: React.FC<AdminPanelProps> = ({ proxySettings }) => {
label: "Hashicorp Vault",
children: <HashicorpVault />,
},
{
key: "cyberark",
label: "CyberArk Conjur",
children: <CyberArk />,
},
{
key: "plugins",
label: "Plugins",

View file

@ -0,0 +1,38 @@
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import { createApiClient } from "@/lib/http/client";
export interface CyberArkFieldSchema {
description?: string;
properties: Record<string, { description?: string; type?: string }>;
}
export interface CyberArkConfigResponse {
config_type: string;
values: Record<string, string | null>;
field_schema: CyberArkFieldSchema;
}
export interface CyberArkStatusResponse {
status: string;
message: string;
}
const apiClient = createApiClient({
getBaseUrl: getProxyBaseUrl,
getAuthHeaderName: getGlobalLitellmHeaderName,
});
export const getCyberArkConfig = async (accessToken: string): Promise<CyberArkConfigResponse> =>
apiClient.get<CyberArkConfigResponse>("/config_overrides/cyberark", { accessToken });
export const updateCyberArkConfig = async (
accessToken: string,
config: Record<string, string>,
): Promise<CyberArkStatusResponse> =>
apiClient.post<CyberArkStatusResponse>("/config_overrides/cyberark", { accessToken, body: config });
export const deleteCyberArkConfig = async (accessToken: string): Promise<CyberArkStatusResponse> =>
apiClient.delete<CyberArkStatusResponse>("/config_overrides/cyberark", { accessToken });
export const testCyberArkConnection = async (accessToken: string): Promise<CyberArkStatusResponse> =>
apiClient.post<CyberArkStatusResponse>("/config_overrides/cyberark/test_connection", { accessToken });

View file

@ -0,0 +1,24 @@
import { getCyberArkConfig, type CyberArkConfigResponse } from "./cyberArkApi";
import { useQuery } from "@tanstack/react-query";
import useAuthorized from "../useAuthorized";
import { createQueryKeys } from "../common/queryKeysFactory";
export const cyberArkKeys = createQueryKeys("cyberArkConfig");
export const useCyberArkConfig = () => {
const { accessToken } = useAuthorized();
const queryOptions = {
queryKey: cyberArkKeys.list({}),
queryFn: async () => {
if (!accessToken) {
throw new Error("Access token is required");
}
return getCyberArkConfig(accessToken);
},
enabled: !!accessToken,
staleTime: 60 * 60 * 1000,
gcTime: 60 * 60 * 1000,
};
return useQuery<CyberArkConfigResponse>(queryOptions);
};

View file

@ -0,0 +1,19 @@
import { deleteCyberArkConfig } from "./cyberArkApi";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { cyberArkKeys } from "./useCyberArkConfig";
export const useDeleteCyberArkConfig = (accessToken: string | null) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
if (!accessToken) {
throw new Error("Access token is required");
}
return deleteCyberArkConfig(accessToken);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: cyberArkKeys.all });
},
});
};

View file

@ -0,0 +1,19 @@
import { updateCyberArkConfig } from "./cyberArkApi";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { cyberArkKeys } from "./useCyberArkConfig";
export const useUpdateCyberArkConfig = (accessToken: string | null) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (config: Record<string, string>) => {
if (!accessToken) {
throw new Error("Access token is required");
}
return updateCyberArkConfig(accessToken, config);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: cyberArkKeys.all });
},
});
};

View file

@ -0,0 +1,78 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../../../../tests/test-utils";
import CyberArk from "./CyberArk";
const mockUseAuthorized = vi.hoisted(() => vi.fn());
const mockUseCyberArkConfig = vi.hoisted(() => vi.fn());
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: mockUseAuthorized,
}));
vi.mock("@/app/(dashboard)/hooks/configOverrides/useCyberArkConfig", () => ({
useCyberArkConfig: mockUseCyberArkConfig,
}));
vi.mock("@/app/(dashboard)/hooks/configOverrides/useDeleteCyberArkConfig", () => ({
useDeleteCyberArkConfig: () => ({ mutate: vi.fn(), isPending: false }),
}));
vi.mock("@/app/(dashboard)/hooks/configOverrides/useUpdateCyberArkConfig", () => ({
useUpdateCyberArkConfig: () => ({ mutate: vi.fn(), isPending: false }),
}));
vi.mock("./EditCyberArkModal", () => ({
default: ({ isVisible }: { isVisible: boolean }) => (isVisible ? <div>Edit CyberArk Configuration</div> : null),
}));
vi.mock("@/components/common_components/DeleteResourceModal", () => ({
default: () => null,
}));
describe("CyberArk", () => {
beforeEach(() => {
vi.clearAllMocks();
mockUseAuthorized.mockReturnValue({ accessToken: "test-token" });
const emptyConfigResult = {
data: { values: {} },
isLoading: false,
isError: false,
error: null,
};
mockUseCyberArkConfig.mockReturnValue(emptyConfigResult);
});
it("should render", () => {
renderWithProviders(<CyberArk />);
expect(screen.getByRole("heading", { name: "CyberArk Conjur" })).toBeInTheDocument();
});
it("should open the configuration editor from the empty state", async () => {
const user = userEvent.setup();
renderWithProviders(<CyberArk />);
await user.click(screen.getByRole("button", { name: /configure cyberark/i }));
expect(screen.getByText("Edit CyberArk Configuration")).toBeInTheDocument();
});
it("should display configured values and management actions", () => {
const configuredResult = {
data: { values: { cyberark_api_base: "https://conjur.example.com", cyberark_api_key: "secret" } },
isLoading: false,
isError: false,
error: null,
};
mockUseCyberArkConfig.mockReturnValue(configuredResult);
renderWithProviders(<CyberArk />);
expect(screen.getByText("https://conjur.example.com")).toBeInTheDocument();
expect(screen.getByText("Auth Method")).toBeInTheDocument();
expect(screen.getAllByText("API Key")).toHaveLength(2);
expect(screen.getByRole("button", { name: /test connection/i })).toBeInTheDocument();
});
});

View file

@ -0,0 +1,232 @@
"use client";
import { Edit, ExternalLink, Info, KeyRound, PlugZap, Trash2 } from "lucide-react";
import { useState } from "react";
import { testCyberArkConnection } from "@/app/(dashboard)/hooks/configOverrides/cyberArkApi";
import { useCyberArkConfig } from "@/app/(dashboard)/hooks/configOverrides/useCyberArkConfig";
import { useDeleteCyberArkConfig } from "@/app/(dashboard)/hooks/configOverrides/useDeleteCyberArkConfig";
import { useUpdateCyberArkConfig } from "@/app/(dashboard)/hooks/configOverrides/useUpdateCyberArkConfig";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
import { toast } from "@/lib/toast";
import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert";
import { Button } from "@/components/ui/button";
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import CyberArkEmptyPlaceholder from "./CyberArkEmptyPlaceholder";
import EditCyberArkModal from "./EditCyberArkModal";
import { FIELD_LABELS, SENSITIVE_FIELDS } from "./constants";
function detectAuthMethod(values: Record<string, unknown>): string {
if (values.cyberark_api_key) return "API Key";
if (values.client_cert && values.client_key) return "TLS Certificate";
return "None";
}
function DetailRow({ children, label }: { children: React.ReactNode; label: string }) {
return (
<div className="grid grid-cols-1 sm:grid-cols-3">
<dt className="bg-muted/50 px-4 py-3 text-sm font-medium text-foreground">{label}</dt>
<dd className="px-4 py-3 text-sm text-foreground sm:col-span-2">{children}</dd>
</div>
);
}
export default function CyberArk() {
const { accessToken } = useAuthorized();
const { data, isLoading, isError, error } = useCyberArkConfig();
const { mutate: deleteConfig, isPending: isDeleting } = useDeleteCyberArkConfig(accessToken);
const { mutate: updateConfig, isPending: isClearingField } = useUpdateCyberArkConfig(accessToken);
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [clearingField, setClearingField] = useState<string | null>(null);
const [isTesting, setIsTesting] = useState(false);
const rawValues = data?.values ?? {};
const isConfigured = Boolean(rawValues.cyberark_api_base);
const handleTestConnection = async () => {
if (!accessToken) return;
setIsTesting(true);
try {
const result = await testCyberArkConnection(accessToken);
toast.success(result.message || "Connection to CyberArk Conjur successful!");
} catch (err) {
toast.fromError(err);
} finally {
setIsTesting(false);
}
};
const handleDelete = () => {
deleteConfig(undefined, {
onSuccess: () => {
toast.success("CyberArk configuration deleted");
setIsDeleteModalOpen(false);
},
onError: (err) => toast.fromError(err),
});
};
const handleClearField = () => {
if (!clearingField) return;
updateConfig(
{ [clearingField]: "" },
{
onSuccess: () => {
toast.success(`${FIELD_LABELS[clearingField] ?? clearingField} cleared`);
setClearingField(null);
},
onError: (err) => toast.fromError(err),
},
);
};
const renderValue = (key: string) => {
const value = rawValues[key];
if (!value) return <span className="text-muted-foreground italic">Not configured</span>;
if (!SENSITIVE_FIELDS.has(key)) return <span className="font-mono text-muted-foreground">{value}</span>;
return (
<div className="flex items-center justify-between gap-2">
<span className="font-mono text-muted-foreground">{value}</span>
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label={`Clear ${FIELD_LABELS[key] ?? key}`}
onClick={() => setClearingField(key)}
>
<Trash2 className="size-3.5" />
</Button>
</div>
);
};
const fieldsToShow = Object.entries(rawValues).filter(([, value]) => value != null && value !== "");
const renderCard = () => {
if (isLoading) {
return (
<Card role="status" aria-label="Loading CyberArk configuration">
<CardContent className="space-y-3">
<Skeleton className="h-8 w-64" />
<Skeleton className="h-40 w-full" />
</CardContent>
</Card>
);
}
if (isError) {
return (
<Card>
<CardContent>
<Alert variant="error">
<AlertTitle>Could not load CyberArk configuration</AlertTitle>
{error instanceof Error && <AlertDescription>{error.message}</AlertDescription>}
</Alert>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<KeyRound className="size-6 text-muted-foreground" />
<div>
<CardTitle>
<h3>CyberArk Conjur</h3>
</CardTitle>
<CardDescription>Manage secret manager configuration</CardDescription>
</div>
</div>
{isConfigured && (
<CardAction className="flex flex-wrap gap-2">
<Button type="button" variant="outline" disabled={isTesting} onClick={handleTestConnection}>
<PlugZap />
{isTesting ? "Testing..." : "Test Connection"}
</Button>
<Button type="button" variant="outline" onClick={() => setIsEditModalVisible(true)}>
<Edit />
Edit Configuration
</Button>
<Button type="button" variant="destructive" onClick={() => setIsDeleteModalOpen(true)}>
<Trash2 />
Delete Configuration
</Button>
</CardAction>
)}
</CardHeader>
<CardContent className="space-y-6">
{isConfigured && (
<Alert variant="info">
<Info />
<AlertTitle>Configuration changes are hot-reloaded across all proxy instances</AlertTitle>
<AlertDescription>
<a
href="https://docs.litellm.ai/docs/secret_managers/cyberark"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1"
>
View documentation
<ExternalLink className="size-3" />
</a>
</AlertDescription>
</Alert>
)}
{isConfigured ? (
fieldsToShow.length > 0 && (
<dl className="divide-y divide-border overflow-hidden rounded-md border border-border">
<DetailRow label="Auth Method">{detectAuthMethod(rawValues)}</DetailRow>
{fieldsToShow.map(([key]) => (
<DetailRow key={key} label={FIELD_LABELS[key] ?? key}>
{renderValue(key)}
</DetailRow>
))}
</dl>
)
) : (
<CyberArkEmptyPlaceholder onAdd={() => setIsEditModalVisible(true)} />
)}
</CardContent>
</Card>
);
};
return (
<>
{renderCard()}
<EditCyberArkModal
isVisible={isEditModalVisible}
onCancel={() => setIsEditModalVisible(false)}
onSuccess={() => setIsEditModalVisible(false)}
/>
<DeleteResourceModal
isOpen={isDeleteModalOpen}
title="Delete CyberArk Configuration?"
message="Models using CyberArk secrets will lose access to their API keys until a new configuration is saved."
resourceInformationTitle="CyberArk Configuration"
resourceInformation={[{ label: "Conjur Server URL", value: rawValues.cyberark_api_base }]}
onCancel={() => setIsDeleteModalOpen(false)}
onOk={handleDelete}
confirmLoading={isDeleting}
/>
<DeleteResourceModal
isOpen={clearingField !== null}
title={`Clear ${clearingField ? FIELD_LABELS[clearingField] ?? clearingField : ""}?`}
message="This will remove the stored value."
resourceInformationTitle="Field"
resourceInformation={[
{ label: "Field", value: clearingField ? FIELD_LABELS[clearingField] ?? clearingField : "" },
]}
onCancel={() => setClearingField(null)}
onOk={handleClearField}
confirmLoading={isClearingField}
/>
</>
);
}

View file

@ -0,0 +1,24 @@
import { KeyRound } from "lucide-react";
import { Button } from "@/components/ui/button";
interface CyberArkEmptyPlaceholderProps {
onAdd: () => void;
}
export default function CyberArkEmptyPlaceholder({ onAdd }: CyberArkEmptyPlaceholderProps) {
return (
<div className="flex w-full flex-col items-center rounded-lg border border-dashed border-border bg-card p-12 text-center">
<div className="mb-4 flex size-12 items-center justify-center rounded-full bg-muted">
<KeyRound className="size-6 text-muted-foreground" />
</div>
<h4 className="text-base font-semibold text-foreground">No CyberArk Configuration Found</h4>
<p className="mx-auto mt-2 max-w-md text-sm text-muted-foreground">
Configure CyberArk Conjur to securely manage provider API keys and secrets for your LiteLLM deployment.
</p>
<Button size="lg" onClick={onAdd} className="mt-4">
Configure CyberArk
</Button>
</div>
);
}

View file

@ -0,0 +1,176 @@
import { fireEvent, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../../../../tests/test-utils";
import EditCyberArkModal from "./EditCyberArkModal";
import { useCyberArkConfig } from "@/app/(dashboard)/hooks/configOverrides/useCyberArkConfig";
import { useUpdateCyberArkConfig } from "@/app/(dashboard)/hooks/configOverrides/useUpdateCyberArkConfig";
vi.mock("@/app/(dashboard)/hooks/configOverrides/useCyberArkConfig", () => ({
useCyberArkConfig: vi.fn(),
}));
vi.mock("@/app/(dashboard)/hooks/configOverrides/useUpdateCyberArkConfig", () => ({
useUpdateCyberArkConfig: vi.fn(),
}));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: () => ({ accessToken: "sk-access-token" }),
}));
vi.mock("@/lib/toast", () => ({
toast: { success: vi.fn(), fromError: vi.fn() },
}));
const ALL_FIELDS = [
"cyberark_api_base",
"cyberark_account",
"cyberark_username",
"cyberark_api_key",
"client_cert",
"client_key",
"ssl_verify",
"refresh_interval",
] as const;
const propertiesFor = (fields: readonly string[]) =>
Object.fromEntries(fields.map((name) => [name, { description: `${name} description` }]));
const mutate = vi.fn();
const setup = (options?: { values?: Record<string, unknown>; fields?: readonly string[] }) => {
vi.mocked(useCyberArkConfig).mockReturnValue({
data: {
field_schema: { properties: propertiesFor(options?.fields ?? ALL_FIELDS) },
values: options?.values ?? {},
},
} as unknown as ReturnType<typeof useCyberArkConfig>);
vi.mocked(useUpdateCyberArkConfig).mockReturnValue({
mutate,
isPending: false,
} as unknown as ReturnType<typeof useUpdateCyberArkConfig>);
};
const renderModal = (onSuccess = vi.fn(), onCancel = vi.fn()) =>
renderWithProviders(<EditCyberArkModal isVisible={true} onCancel={onCancel} onSuccess={onSuccess} />);
const save = async (user: ReturnType<typeof userEvent.setup>) =>
user.click(screen.getByRole("button", { name: "Save" }));
describe("EditCyberArkModal", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("clears untouched non-sensitive fields and omits untouched sensitive fields", async () => {
setup({
values: {
cyberark_api_base: "https://conjur.example.com",
cyberark_account: "myorg",
cyberark_api_key: "super-secret-key",
client_key: "super-secret-pem",
},
});
const user = userEvent.setup();
renderModal();
await save(user);
await waitFor(() => {
expect(mutate).toHaveBeenCalledTimes(1);
});
const expectedPayload = {
cyberark_api_base: "https://conjur.example.com",
cyberark_account: "myorg",
cyberark_username: "",
client_cert: "",
ssl_verify: "",
refresh_interval: "",
};
expect(mutate.mock.calls[0][0]).toEqual(expectedPayload);
});
it("sends a sensitive field only once it is typed into", async () => {
setup({ values: { cyberark_api_base: "https://conjur.example.com", cyberark_api_key: "super-secret-key" } });
const user = userEvent.setup();
renderModal();
fireEvent.change(screen.getByLabelText("API Key"), { target: { value: "rotated-key" } });
await save(user);
await waitFor(() => {
expect(mutate).toHaveBeenCalledTimes(1);
});
expect(mutate.mock.calls[0][0]).toMatchObject({ cyberark_api_key: "rotated-key" });
});
it("never seeds a stored secret into its input", () => {
setup({ values: { cyberark_api_key: "super-secret-key", client_key: "super-secret-pem" } });
renderModal();
expect(screen.getByLabelText("API Key")).toHaveValue("");
expect(screen.getByLabelText("Client Key")).toHaveValue("");
});
it("renders only the fields the schema declares, and sends only those", async () => {
setup({
fields: ["cyberark_api_base", "cyberark_api_key"],
values: { cyberark_api_base: "https://conjur.example.com" },
});
const user = userEvent.setup();
renderModal();
expect(screen.queryByLabelText("Account")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Client Key")).not.toBeInTheDocument();
await save(user);
await waitFor(() => {
expect(mutate).toHaveBeenCalledTimes(1);
});
expect(mutate.mock.calls[0][0]).toEqual({ cyberark_api_base: "https://conjur.example.com" });
});
it("blocks the submit when the server url does not start with http", async () => {
setup({ values: {} });
const user = userEvent.setup();
renderModal();
fireEvent.change(screen.getByLabelText("Conjur Server URL"), { target: { value: "conjur.example.com" } });
await save(user);
expect(await screen.findByText("Must start with http:// or https://")).toBeInTheDocument();
expect(mutate).not.toHaveBeenCalled();
});
it("tells the admin a stored secret is kept when the field is left blank", () => {
setup({ values: { cyberark_api_key: "super-secret-key" } });
renderModal();
expect(screen.getByLabelText("API Key")).toHaveAttribute(
"placeholder",
"Leave blank to keep existing (super-secret-key)",
);
});
it("falls back to the schema description when no secret is stored yet", () => {
setup({ values: {} });
renderModal();
expect(screen.getByLabelText("API Key")).toHaveAttribute("placeholder", "cyberark_api_key description");
});
it("closes without saving when cancelled", async () => {
setup({ values: {} });
const onCancel = vi.fn();
const user = userEvent.setup();
renderModal(vi.fn(), onCancel);
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(onCancel).toHaveBeenCalledTimes(1);
expect(mutate).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,176 @@
"use client";
import { useCyberArkConfig } from "@/app/(dashboard)/hooks/configOverrides/useCyberArkConfig";
import { useUpdateCyberArkConfig } from "@/app/(dashboard)/hooks/configOverrides/useUpdateCyberArkConfig";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { toast } from "@/lib/toast";
import React, { useMemo } from "react";
import { z } from "zod/v4";
import { FieldGroup } from "@/components/ui/field";
import { FormField } from "@/components/shared/form/FormField";
import { PasswordInput } from "@/components/shared/PasswordInput";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { Separator } from "@/components/ui/separator";
import { useZodForm } from "@/lib/forms/useZodForm";
import { SENSITIVE_FIELDS, FIELD_LABELS } from "./constants";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
interface CyberArkFieldGroup {
title: string;
subtitle?: string;
fields: string[];
}
const FIELD_GROUPS: CyberArkFieldGroup[] = [
{
title: "Connection",
fields: ["cyberark_api_base", "cyberark_account", "cyberark_username"],
},
{
title: "API Key Authentication",
subtitle: "Use a Conjur API key to authenticate. Only one auth method is required.",
fields: ["cyberark_api_key"],
},
{
title: "Certificate Authentication",
subtitle: "Use a client TLS certificate and key to authenticate. Only one auth method is required.",
fields: ["client_cert", "client_key"],
},
{
title: "Advanced",
subtitle: "Optional TLS and token caching settings.",
fields: ["ssl_verify", "refresh_interval"],
},
];
type CyberArkFormValues = Record<string, string>;
const buildSchema = (fields: readonly string[]): z.ZodType<CyberArkFormValues, CyberArkFormValues> =>
z.object(
Object.fromEntries(
fields.map((name) => [
name,
name === "cyberark_api_base"
? z.string().refine((value) => value.length === 0 || /^https?:\/\/.+/.test(value), {
message: "Must start with http:// or https://",
})
: z.string(),
]),
),
) as unknown as z.ZodType<CyberArkFormValues, CyberArkFormValues>;
interface EditCyberArkModalProps {
isVisible: boolean;
onCancel: () => void;
onSuccess: () => void;
}
const EditCyberArkModal: React.FC<EditCyberArkModalProps> = ({ isVisible, onCancel, onSuccess }) => {
const { accessToken } = useAuthorized();
const { data } = useCyberArkConfig();
const { mutate, isPending } = useUpdateCyberArkConfig(accessToken);
const properties: Record<string, { description?: string }> = useMemo(
() => data?.field_schema?.properties ?? {},
[data],
);
const rawValues: Record<string, unknown> = useMemo(() => data?.values ?? {}, [data]);
const visibleFields = useMemo(
() => FIELD_GROUPS.flatMap((group) => group.fields).filter((name) => properties[name] !== undefined),
[properties],
);
const seededValues = useMemo(
() =>
Object.fromEntries(
visibleFields.map((name) => [name, SENSITIVE_FIELDS.has(name) ? "" : ((rawValues[name] ?? "") as string)]),
),
[visibleFields, rawValues],
);
const schema = useMemo(() => buildSchema(visibleFields), [visibleFields]);
const form = useZodForm(schema, { values: seededValues });
const handleSubmit = (formValues: CyberArkFormValues) => {
const config: Record<string, string> = Object.fromEntries(
Object.entries(formValues).flatMap(([key, value]) => {
if (value !== undefined && value !== null && value !== "") return [[key, value]];
if (!SENSITIVE_FIELDS.has(key)) return [[key, ""]];
return [];
}),
);
mutate(config, {
onSuccess: () => {
toast.success("CyberArk configuration updated successfully");
onSuccess();
},
onError: (err) => {
toast.fromError(err);
},
});
};
const handleCancel = () => {
form.reset(seededValues);
onCancel();
};
const renderField = (fieldName: string) => {
const fieldSchema = properties[fieldName];
if (!fieldSchema) return null;
const isSensitive = SENSITIVE_FIELDS.has(fieldName);
const existingValue = rawValues[fieldName];
const hasExistingValue = isSensitive && existingValue != null && existingValue !== "";
const placeholder = hasExistingValue ? `Leave blank to keep existing (${existingValue})` : fieldSchema?.description;
return (
<FormField key={fieldName} control={form.control} name={fieldName} label={FIELD_LABELS[fieldName] ?? fieldName}>
{({ ref, ...field }) =>
isSensitive ? (
<PasswordInput ref={ref} placeholder={placeholder} {...field} />
) : (
<Input ref={ref} placeholder={fieldSchema?.description} {...field} />
)
}
</FormField>
);
};
return (
<Dialog open={isVisible} onOpenChange={(open) => !open && handleCancel()}>
<DialogContent className="max-h-[calc(100dvh-2rem)] overflow-y-auto sm:max-w-[700px]">
<DialogHeader>
<DialogTitle>Edit CyberArk Configuration</DialogTitle>
</DialogHeader>
<form onSubmit={form.handleSubmit(handleSubmit)}>
{FIELD_GROUPS.map((group, index) => (
<div key={group.title}>
{index > 0 && <Separator className="my-6" />}
<h5 className="mb-1 text-base font-semibold text-foreground">{group.title}</h5>
{group.subtitle && <p className="mb-4 text-sm text-muted-foreground">{group.subtitle}</p>}
<FieldGroup>{group.fields.map(renderField)}</FieldGroup>
</div>
))}
</form>
<DialogFooter>
<div className="flex items-center justify-end gap-2">
<Button type="button" variant="outline" onClick={handleCancel} disabled={isPending}>
Cancel
</Button>
<Button type="button" disabled={isPending} onClick={() => void form.handleSubmit(handleSubmit)()}>
{isPending && <UiLoadingSpinner className="size-4 mr-1" />}
{isPending ? "Saving..." : "Save"}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default EditCyberArkModal;

View file

@ -0,0 +1,12 @@
export const SENSITIVE_FIELDS = new Set(["cyberark_api_key", "client_key"]);
export const FIELD_LABELS: Record<string, string> = {
cyberark_api_base: "Conjur Server URL",
cyberark_account: "Account",
cyberark_username: "Username",
cyberark_api_key: "API Key",
client_cert: "Client Certificate",
client_key: "Client Key",
ssl_verify: "SSL Verification",
refresh_interval: "Token Refresh Interval (seconds)",
};

View file

@ -2479,6 +2479,59 @@ export interface paths {
patch?: never;
trace?: never;
};
"/config_overrides/cyberark": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Get Cyberark Config
* @description Get current CyberArk Conjur configuration.
* Returns decrypted values from DB, or falls back to current env vars.
* Sensitive fields are masked before leaving the server.
*/
get: operations["get_cyberark_config_config_overrides_cyberark_get"];
put?: never;
/**
* Update Cyberark Config
* @description Update CyberArk Conjur secret manager configuration.
* Sets environment variables, encrypts sensitive fields, and stores in DB.
* Reinitializes the secret manager on this pod.
*/
post: operations["update_cyberark_config_config_overrides_cyberark_post"];
/**
* Delete Cyberark Config
* @description Delete CyberArk Conjur configuration. Idempotent.
*/
delete: operations["delete_cyberark_config_config_overrides_cyberark_delete"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/config_overrides/cyberark/test_connection": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Test Cyberark Connection
* @description Test the connection to the currently configured CyberArk Conjur server.
* Uses the already-initialized secret manager client. Does not modify any state.
*/
post: operations["test_cyberark_connection_config_overrides_cyberark_test_connection_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/config_overrides/hashicorp_vault": {
parameters: {
query?: never;
@ -24899,6 +24952,52 @@ export interface components {
/** User Id */
user_id: string;
};
/**
* CyberArkConfig
* @description Configuration for CyberArk Conjur secret manager integration.
*/
CyberArkConfig: {
/**
* Client Cert
* @description Path to the client TLS certificate for certificate-based authentication
*/
client_cert?: string | null;
/**
* Client Key
* @description Path to the client TLS private key for certificate-based authentication
*/
client_key?: string | null;
/**
* Cyberark Account
* @description The Conjur organization account name
*/
cyberark_account?: string | null;
/**
* Cyberark Api Base
* @description The address of the CyberArk Conjur server (e.g., https://conjur.example.com)
*/
cyberark_api_base?: string | null;
/**
* Cyberark Api Key
* @description API key for Conjur API-key authentication
*/
cyberark_api_key?: string | null;
/**
* Cyberark Username
* @description The Conjur username (login) to authenticate as
*/
cyberark_username?: string | null;
/**
* Refresh Interval
* @description Auth token cache TTL in seconds (default: 300)
*/
refresh_interval?: string | null;
/**
* Ssl Verify
* @description Set to false to disable SSL verification (e.g., for self-signed certificates)
*/
ssl_verify?: string | null;
};
/** DailySpendData */
DailySpendData: {
breakdown?: components["schemas"]["BreakdownMetrics"];
@ -40871,6 +40970,114 @@ export interface operations {
};
};
};
get_cyberark_config_config_overrides_cyberark_get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ConfigOverrideSettingsResponse"];
};
};
};
};
update_cyberark_config_config_overrides_cyberark_post: {
parameters: {
query?: never;
header?: {
/** @description The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability */
"litellm-changed-by"?: string | null;
};
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["CyberArkConfig"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
delete_cyberark_config_config_overrides_cyberark_delete: {
parameters: {
query?: never;
header?: {
/** @description The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability */
"litellm-changed-by"?: string | null;
};
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
test_cyberark_connection_config_overrides_cyberark_test_connection_post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
};
};
get_hashicorp_vault_config_config_overrides_hashicorp_vault_get: {
parameters: {
query?: never;
@ -40894,7 +41101,10 @@ export interface operations {
update_hashicorp_vault_config_config_overrides_hashicorp_vault_post: {
parameters: {
query?: never;
header?: never;
header?: {
/** @description The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability */
"litellm-changed-by"?: string | null;
};
path?: never;
cookie?: never;
};
@ -40927,7 +41137,10 @@ export interface operations {
delete_hashicorp_vault_config_config_overrides_hashicorp_vault_delete: {
parameters: {
query?: never;
header?: never;
header?: {
/** @description The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability */
"litellm-changed-by"?: string | null;
};
path?: never;
cookie?: never;
};
@ -40942,6 +41155,15 @@ export interface operations {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
test_hashicorp_vault_connection_config_overrides_hashicorp_vault_test_connection_post: {