chore: initial public snapshot for github upload
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
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.",
|
||||
}
|
||||
Reference in New Issue
Block a user