Files
lijiaoqiao/llm-gateway-competitors/litellm-wheel-src/litellm/secret_managers/cyberark_secret_manager.py

351 lines
13 KiB
Python
Raw Normal View History

import base64
import os
from typing import Any, Dict, Optional, Union
from urllib.parse import quote
import httpx
import litellm
from litellm._logging import verbose_logger
from litellm.caching import InMemoryCache
from litellm.llms.custom_httpx.http_handler import (
_get_httpx_client,
get_async_httpx_client,
httpxSpecialProvider,
)
from litellm.proxy._types import KeyManagementSystem
from .base_secret_manager import BaseSecretManager
from .main import str_to_bool
class CyberArkSecretManager(BaseSecretManager):
def __init__(self):
from litellm.proxy.proxy_server import CommonProxyErrors, premium_user
# CyberArk Conjur-specific config
self.conjur_addr = os.getenv("CYBERARK_API_BASE", "http://127.0.0.1:8080")
self.conjur_account = os.getenv("CYBERARK_ACCOUNT", "default")
self.conjur_username = os.getenv("CYBERARK_USERNAME", "admin")
self.conjur_api_key = os.getenv("CYBERARK_API_KEY", "")
# Optional config for certificate-based auth
self.tls_cert_path = os.getenv("CYBERARK_CLIENT_CERT", "")
self.tls_key_path = os.getenv("CYBERARK_CLIENT_KEY", "")
# SSL verification - can be disabled for self-signed certificates
# Set CYBERARK_SSL_VERIFY=false to disable SSL verification
ssl_verify_env = str_to_bool(os.getenv("CYBERARK_SSL_VERIFY"))
self.ssl_verify: bool = ssl_verify_env if ssl_verify_env is not None else True
# Validate environment
if not self.conjur_api_key and not (self.tls_cert_path and self.tls_key_path):
raise ValueError(
"Missing CyberArk credentials. Please set CYBERARK_API_KEY or both CYBERARK_CLIENT_CERT and CYBERARK_CLIENT_KEY in your environment."
)
litellm.secret_manager_client = self
litellm._key_management_system = KeyManagementSystem.CYBERARK
# Tokens expire after ~8 minutes, so we cache for 5 minutes to be safe
_refresh_interval = int(os.environ.get("CYBERARK_REFRESH_INTERVAL", "300"))
self.cache = InMemoryCache(default_ttl=_refresh_interval)
if premium_user is not True:
raise ValueError(
f"CyberArk secret manager is only available for premium users. {CommonProxyErrors.not_premium_user.value}"
)
if not self.ssl_verify:
verbose_logger.warning(
"CyberArk SSL verification is disabled. This is insecure and should only be used for testing with self-signed certificates."
)
def _authenticate(self) -> str:
"""
Authenticate with CyberArk Conjur and get a session token.
The token is a JSON object that must be base64-encoded for use in subsequent requests.
Returns:
str: Base64-encoded session token
"""
# Check if we have a cached token
cached_token = self.cache.get_cache("cyberark_auth_token")
if cached_token is not None:
return cached_token
verbose_logger.debug("Authenticating with CyberArk Conjur...")
auth_url = f"{self.conjur_addr}/authn/{self.conjur_account}/{self.conjur_username}/authenticate"
try:
if self.tls_cert_path and self.tls_key_path:
# Certificate-based authentication - need custom client for cert
http_client = httpx.Client(
cert=(self.tls_cert_path, self.tls_key_path),
verify=self.ssl_verify,
)
resp = http_client.post(auth_url, content=self.conjur_api_key)
else:
# API key authentication
http_handler = _get_httpx_client(params={"ssl_verify": self.ssl_verify})
resp = http_handler.client.post(auth_url, content=self.conjur_api_key)
resp.raise_for_status()
# The response is a JSON token that needs to be base64-encoded
token_json = resp.text
token_b64 = base64.b64encode(token_json.encode()).decode()
verbose_logger.debug("Successfully authenticated with CyberArk Conjur.")
# Cache the token for the refresh interval
self.cache.set_cache(key="cyberark_auth_token", value=token_b64)
return token_b64
except Exception as e:
raise RuntimeError(f"Could not authenticate to CyberArk Conjur: {e}")
def _get_request_headers(self) -> dict:
"""
Get headers for CyberArk API requests including authentication.
Returns:
dict: Headers with authentication token
"""
token = self._authenticate()
return {"Authorization": f'Token token="{token}"'}
def _ensure_variable_exists(self, secret_name: str) -> None:
"""
Ensure a variable exists in CyberArk Conjur by creating a policy entry if needed.
Args:
secret_name: Name of the variable to ensure exists
"""
# In production, we'd check if the variable exists first
# For now, we'll attempt to create it and ignore if it already exists
policy_url = f"{self.conjur_addr}/policies/{self.conjur_account}/policy/root"
policy_yaml = f"- !variable {secret_name}\n"
try:
client = _get_httpx_client(params={"ssl_verify": self.ssl_verify})
resp = client.client.post(
policy_url,
headers={
**self._get_request_headers(),
"Content-Type": "application/x-yaml",
},
content=policy_yaml,
)
resp.raise_for_status()
verbose_logger.debug(f"Created policy entry for variable: {secret_name}")
except httpx.HTTPStatusError as e:
# Variable might already exist, which is fine
if e.response.status_code in [409, 422]:
verbose_logger.debug(
f"Variable {secret_name} already exists or policy conflict (expected)"
)
else:
verbose_logger.warning(
f"Could not ensure variable exists: {e.response.status_code} - {e.response.text}"
)
except Exception as e:
verbose_logger.warning(f"Error ensuring variable exists: {e}")
def get_url(self, secret_name: str) -> str:
"""
Build the URL for accessing a secret in CyberArk Conjur.
Args:
secret_name: Name of the secret (will be URL-encoded)
Returns:
str: Full URL for the secret
"""
# URL-encode the secret name to handle slashes and special characters
encoded_name = quote(secret_name, safe="")
return (
f"{self.conjur_addr}/secrets/{self.conjur_account}/variable/{encoded_name}"
)
async def async_read_secret(
self,
secret_name: str,
optional_params: Optional[dict] = None,
timeout: Optional[Union[float, httpx.Timeout]] = None,
) -> Optional[str]:
"""
Reads a secret from CyberArk Conjur using an async HTTPX client.
Args:
secret_name: Name/path of the secret to read
optional_params: Additional parameters (not used for Conjur)
timeout: Request timeout
Returns:
Optional[str]: The secret value if found, None otherwise
"""
# Check cache first
if self.cache.get_cache(secret_name) is not None:
return self.cache.get_cache(secret_name)
async_client = get_async_httpx_client(
llm_provider=httpxSpecialProvider.SecretManager,
params={"ssl_verify": self.ssl_verify},
)
try:
url = self.get_url(secret_name)
response = await async_client.get(url, headers=self._get_request_headers())
response.raise_for_status()
# CyberArk Conjur returns the raw secret value as text
secret_value = response.text
self.cache.set_cache(secret_name, secret_value)
return secret_value
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
verbose_logger.debug(
f"Secret {secret_name} not found in CyberArk Conjur"
)
else:
verbose_logger.exception(
f"Error reading secret from CyberArk Conjur: {e}"
)
return None
except Exception as e:
verbose_logger.exception(f"Error reading secret from CyberArk Conjur: {e}")
return None
def sync_read_secret(
self,
secret_name: str,
optional_params: Optional[dict] = None,
timeout: Optional[Union[float, httpx.Timeout]] = None,
) -> Optional[str]:
"""
Reads a secret from CyberArk Conjur using a sync HTTPX client.
Args:
secret_name: Name/path of the secret to read
optional_params: Additional parameters (not used for Conjur)
timeout: Request timeout
Returns:
Optional[str]: The secret value if found, None otherwise
"""
# Check cache first
if self.cache.get_cache(secret_name) is not None:
return self.cache.get_cache(secret_name)
sync_client = _get_httpx_client(params={"ssl_verify": self.ssl_verify})
try:
url = self.get_url(secret_name)
response = sync_client.client.get(url, headers=self._get_request_headers())
response.raise_for_status()
# CyberArk Conjur returns the raw secret value as text
secret_value = response.text
self.cache.set_cache(secret_name, secret_value)
return secret_value
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
verbose_logger.debug(
f"Secret {secret_name} not found in CyberArk Conjur"
)
else:
verbose_logger.exception(
f"Error reading secret from CyberArk Conjur: {e}"
)
return None
except Exception as e:
verbose_logger.exception(f"Error reading secret from CyberArk Conjur: {e}")
return None
async def async_write_secret(
self,
secret_name: str,
secret_value: str,
description: Optional[str] = None,
optional_params: Optional[dict] = None,
timeout: Optional[Union[float, httpx.Timeout]] = None,
tags: Optional[Union[dict, list]] = None,
) -> Dict[str, Any]:
"""
Writes a secret to CyberArk Conjur using an async HTTPX client.
Args:
secret_name: Name/path of the secret to write
secret_value: Value to store
description: Optional description (not used by Conjur)
optional_params: Additional parameters
timeout: Request timeout
tags: Optional tags (not used by Conjur)
Returns:
dict: Response containing status and details of the operation
"""
async_client = get_async_httpx_client(
llm_provider=httpxSpecialProvider.SecretManager,
params={"ssl_verify": self.ssl_verify},
)
try:
# Ensure the variable exists in the policy first
self._ensure_variable_exists(secret_name)
# Now set the secret value
url = self.get_url(secret_name)
response = await async_client.post(
url=url, headers=self._get_request_headers(), content=secret_value
)
response.raise_for_status()
# Update cache
self.cache.set_cache(secret_name, secret_value)
return {
"status": "success",
"message": f"Secret {secret_name} written successfully",
}
except Exception as e:
verbose_logger.exception(f"Error writing secret to CyberArk Conjur: {e}")
return {"status": "error", "message": str(e)}
async def async_delete_secret(
self,
secret_name: str,
recovery_window_in_days: Optional[int] = 7,
optional_params: Optional[dict] = None,
timeout: Optional[Union[float, httpx.Timeout]] = None,
) -> dict:
"""
CyberArk Conjur does not support direct secret deletion via API.
Secrets can only be removed through policy updates.
Args:
secret_name: Name of the secret
recovery_window_in_days: Not used
optional_params: Additional parameters
timeout: Request timeout
Returns:
dict: Response indicating operation not supported
"""
verbose_logger.warning(
"CyberArk Conjur does not support direct secret deletion. "
"Secrets must be removed through policy updates."
)
# Clear from cache
self.cache.delete_cache(secret_name)
return {
"status": "not_supported",
"message": "CyberArk Conjur does not support direct secret deletion. Use policy updates to remove variables.",
}