use std::{collections::BTreeMap, sync::Arc}; use litellm_core_utils::settings::{Lookup, ProcessEnvironment}; use litellm_host_python::{from_py, json_object_field, to_py}; use litellm_secrets::{ KeyManagementSettings, KeyManagementSystem, Secret, SecretManager, load_native_manager, read_secret_from_python_manager, }; use litellm_secrets_types::PythonSecretRead; use pyo3::{ exceptions::{PyAttributeError, PyRuntimeError, PyValueError}, prelude::*, types::PyDict, }; use crate::execution::{run_async_value, run_sync_value}; #[derive(Clone, PartialEq)] struct Configuration { system: KeyManagementSystem, settings: KeyManagementSettings, environment: BTreeMap, enterprise_enabled: bool, } #[pyclass(frozen, name = "_SecretManagerRuntime")] pub(crate) struct NativeSecretManager { backend: SecretManager, configuration: Configuration, pid: u32, } impl NativeSecretManager { pub(super) fn backend(&self) -> PyResult { if self.pid != std::process::id() { return Err(PyRuntimeError::new_err( "native secret manager must be recreated after fork", )); } Ok(self.backend.clone()) } fn build(py: Python<'_>, configuration: Configuration) -> PyResult { let values = configuration.environment.clone(); let environment: Arc = Arc::new(move |name: &str| values.get(name).cloned()); let system = configuration.system; let settings = configuration.settings.clone(); let enterprise_enabled = configuration.enterprise_enabled; let http_config = crate::http::call_config(py, &PyDict::new(py), false)?; let backend = run_sync_value(py, async move { load_native_manager( crate::http::pool(), &http_config, system, settings, environment, enterprise_enabled, ) .await .map_err(|error| PyValueError::new_err(error.to_string())) })?; Ok(Self { backend, configuration, pid: std::process::id(), }) } } #[pymethods] impl NativeSecretManager { #[staticmethod] #[pyo3(signature = (system, environment, settings=None, enterprise_enabled=false))] fn from_config( py: Python<'_>, system: &str, environment: BTreeMap, settings: Option<&Bound<'_, PyAny>>, enterprise_enabled: bool, ) -> PyResult { let system = serde_json::from_value(serde_json::Value::String(system.to_owned())) .map_err(|_| PyValueError::new_err("unknown secret manager system"))?; let settings = parse_settings(settings)?; Self::build( py, Configuration { system, settings, environment, enterprise_enabled, }, ) } #[staticmethod] pub(super) fn from_client(client: &Bound<'_, PyAny>) -> PyResult>> { let py = client.py(); if let Ok(native) = client.extract::>() { native.borrow(py).backend()?; return Ok(Some(native)); } let config = py .import("litellm.rust_bridge.secret_manager")? .getattr("native_secret_manager_config")? .call1((client,))?; if config.is_none() { return Ok(None); } if !config.getattr("owner_type")?.is(client.get_type()) { return Ok(None); } let methods = config .getattr("methods")? .extract::)>>()?; for (name, original) in methods { let current = client.getattr(name.as_str())?; let implementation = optional_attribute(¤t, "__func__")?.unwrap_or(current); if !implementation.is(original.bind(py)) { return Ok(None); } } let environment_attributes: BTreeMap = config .getattr("environment_attributes")? .extract::>()? .into_iter() .collect(); let captured = config .getattr("environment")? .extract::>()?; let overrides = environment_attributes .iter() .map(|(key, attribute)| { let value = attribute_path(client, attribute)?; Ok(( key.clone(), if value.is_none() { None } else { Some(value.str()?.extract::()?) }, )) }) .collect::>>()?; let settings = from_py::>(&config.getattr("settings")?) .map_err(|_| PyValueError::new_err("invalid secret manager settings"))?; let attributes = config .getattr("settings_attributes")? .extract::>()?; let setting_overrides = attributes .into_iter() .map(|name| { let value = from_py::(&client.getattr(name.as_str())?)?; Ok((name, value)) }) .collect::>>()?; let configuration = Configuration { system: serde_json::from_value(serde_json::Value::String( config.getattr("system")?.extract()?, )) .map_err(|_| PyValueError::new_err("unknown secret manager system"))?, settings: serde_json::from_value(serde_json::Value::Object( settings.into_iter().chain(setting_overrides).collect(), )) .map_err(|_| PyValueError::new_err("invalid secret manager settings"))?, environment: captured .into_iter() .filter(|(key, _)| !environment_attributes.contains_key(key)) .chain( overrides .into_iter() .filter_map(|(key, value)| value.map(|value| (key, value))), ) .collect(), enterprise_enabled: config.getattr("enterprise_enabled")?.extract()?, }; if let Some(native) = cached(client, &configuration)? { return Ok(Some(native)); } let runtime = Self::build(py, configuration)?; if let Some(native) = cached(client, &runtime.configuration)? { return Ok(Some(native)); } let native = Py::new(py, runtime)?; client.setattr("_litellm_native_secret_manager", native.bind(py))?; Ok(Some(native)) } #[getter] fn system(&self) -> String { serde_json::to_value(self.configuration.system) .expect("serializable system") .as_str() .expect("string system") .to_owned() } #[pyo3(signature = (name, settings=None))] fn read_secret( &self, py: Python<'_>, name: String, settings: Option<&Bound<'_, PyAny>>, ) -> PyResult> { let backend = self.backend()?; let settings = settings .map(|value| parse_settings(Some(value))) .transpose()? .unwrap_or_else(|| self.configuration.settings.clone()); run_sync_value(py, async move { read_secret_from_python_manager(&backend, &name, &settings, &ProcessEnvironment) .await .map_err(|error| python_read_error(backend.system(), &name, error)) .and_then(|value| python_secret_value(value, &name)) }) } #[pyo3(signature = (secret_name, optional_params=None, timeout=None, primary_secret_name=None))] fn sync_read_secret( &self, py: Python<'_>, secret_name: String, optional_params: Option<&Bound<'_, PyAny>>, timeout: Option<&Bound<'_, PyAny>>, primary_secret_name: Option, ) -> PyResult> { let backend = self.backend()?; let request = super::provider::read_request( self.configuration.system, secret_name, optional_params, timeout, primary_secret_name, true, )?; run_sync_value(py, async move { super::operations::read_python_provider(&backend, &request, &ProcessEnvironment) .await .map_err(|error| PyValueError::new_err(error.to_string())) .and_then(|value| python_secret_value(value, &request.secret_name)) }) } #[pyo3(signature = (secret_name, optional_params=None, timeout=None, primary_secret_name=None))] fn async_read_secret<'py>( &self, py: Python<'py>, secret_name: String, optional_params: Option<&Bound<'py, PyAny>>, timeout: Option<&Bound<'py, PyAny>>, primary_secret_name: Option, ) -> PyResult> { let backend = self.backend()?; let request = super::provider::read_request( self.configuration.system, secret_name, optional_params, timeout, primary_secret_name, false, )?; run_async_value(py, async move { super::operations::read_python_provider(&backend, &request, &ProcessEnvironment) .await .map_err(|error| PyValueError::new_err(error.to_string())) .and_then(|value| python_secret_value(value, &request.secret_name)) }) } #[pyo3(signature = (secret_name, secret_value, description=None, optional_params=None, timeout=None, tags=None))] #[expect( clippy::too_many_arguments, reason = "preserves the Python secret-manager write signature" )] fn async_write_secret<'py>( &self, py: Python<'py>, secret_name: String, secret_value: String, description: Option<&Bound<'py, PyAny>>, optional_params: Option<&Bound<'py, PyAny>>, timeout: Option<&Bound<'py, PyAny>>, tags: Option<&Bound<'py, PyAny>>, ) -> PyResult> { let backend = self.backend()?; let _ = tags; let context = litellm_secrets_types::SecretWriteContext { operation: super::provider::mutation_context( self.configuration.system, optional_params, timeout, )?, description: if self.configuration.system == KeyManagementSystem::HashicorpVault { description .filter(|value| !value.is_none()) .map(|value| { if value.is_truthy()? { value.extract().map(Some) } else { Ok(None) } }) .transpose()? .flatten() } else { None }, ..litellm_secrets_types::SecretWriteContext::default() }; let error_context = super::vault::ErrorContext::capture(py, self.configuration.system, timeout)?; run_async_value(py, async move { super::mutation::mutation_value( super::operations::write_python_provider_with_context( &backend, &secret_name, &litellm_secrets::SecretValue::new(secret_value), &context, ) .await, &error_context, ) }) } #[pyo3(signature = (secret_name, recovery_window_in_days=None, optional_params=None, timeout=None))] fn async_delete_secret<'py>( &self, py: Python<'py>, secret_name: String, recovery_window_in_days: Option<&Bound<'py, PyAny>>, optional_params: Option<&Bound<'py, PyAny>>, timeout: Option<&Bound<'py, PyAny>>, ) -> PyResult> { let backend = self.backend()?; let _ = recovery_window_in_days; let context = super::provider::mutation_context(self.configuration.system, optional_params, timeout)?; let error_context = super::vault::ErrorContext::capture(py, self.configuration.system, timeout)?; run_async_value(py, async move { super::mutation::mutation_value( super::operations::delete_python_provider_with_context( &backend, &secret_name, &context, ) .await, &error_context, ) }) } #[pyo3(signature = (current_secret_name, new_secret_name, new_secret_value, optional_params=None, timeout=None))] fn async_rotate_secret<'py>( &self, py: Python<'py>, current_secret_name: String, new_secret_name: String, new_secret_value: String, optional_params: Option<&Bound<'py, PyAny>>, timeout: Option<&Bound<'py, PyAny>>, ) -> PyResult> { let backend = self.backend()?; let context = super::provider::mutation_context(self.configuration.system, optional_params, timeout)?; let error_context = super::vault::ErrorContext::capture(py, self.configuration.system, timeout)?; run_async_value(py, async move { super::mutation::mutation_value( super::operations::rotate_python_provider_with_context( &backend, ¤t_secret_name, &new_secret_name, &litellm_secrets::SecretValue::new(new_secret_value), &context, ) .await, &error_context, ) }) } #[pyo3(signature = (name, settings=None))] fn read_secret_async<'py>( &self, py: Python<'py>, name: String, settings: Option<&Bound<'py, PyAny>>, ) -> PyResult> { let backend = self.backend()?; let settings = settings .map(|value| parse_settings(Some(value))) .transpose()? .unwrap_or_else(|| self.configuration.settings.clone()); run_async_value(py, async move { read_secret_from_python_manager(&backend, &name, &settings, &ProcessEnvironment) .await .map_err(|error| python_read_error(backend.system(), &name, error)) .and_then(|value| python_secret_value(value, &name)) }) } } fn optional_attribute<'py>( object: &Bound<'py, PyAny>, name: &str, ) -> PyResult>> { match object.getattr(name) { Ok(value) => Ok(Some(value)), Err(error) if error.is_instance_of::(object.py()) => Ok(None), Err(error) => Err(error), } } fn attribute_path<'py>(object: &Bound<'py, PyAny>, path: &str) -> PyResult> { match path.split_once('.') { Some((head, tail)) => attribute_path(&object.getattr(head)?, tail), None => object.getattr(path), } } fn cached( client: &Bound<'_, PyAny>, configuration: &Configuration, ) -> PyResult>> { let Some(value) = optional_attribute(client, "_litellm_native_secret_manager")? else { return Ok(None); }; let native = value.extract::>()?; let same_configuration = native.borrow(client.py()).pid == std::process::id() && &native.borrow(client.py()).configuration == configuration; Ok(same_configuration.then_some(native)) } fn parse_settings(value: Option<&Bound<'_, PyAny>>) -> PyResult { value .map(|value| { serde_json::from_value(from_py::(value)?) .map_err(|_| PyValueError::new_err("invalid secret manager settings")) }) .transpose() .map(Option::unwrap_or_default) } fn python_secret_value(payload: PythonSecretRead, name: &str) -> PyResult> { let value = match payload { PythonSecretRead::Value(value) => value, PythonSecretRead::PrimaryJson(document) => { return Python::attach(|py| json_object_field(py, document.expose(), name)); } }; let value = match value { None => serde_json::Value::Null, Some(Secret::String(value)) => serde_json::Value::String(value.expose().to_owned()), Some(Secret::Bool(value)) => serde_json::Value::Bool(value), Some(Secret::Json(value)) => value, }; Python::attach(|py| to_py(py, &value)) } fn python_read_error( system: KeyManagementSystem, name: &str, error: litellm_secrets::Error, ) -> PyErr { let message = match (system, error) { (KeyManagementSystem::Cyberark, litellm_secrets::Error::ManagedSecretMissing) => { format!("No secret found in CyberArk Secret Manager for {name}") } (_, error) => error.to_string(), }; PyValueError::new_err(message) }