use std::collections::BTreeMap;
use std::fmt;
use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, Zeroizing};
mod env;
mod file;
mod handoff;
mod keyring;
mod memory;
#[cfg(all(
feature = "native-keyring",
unix,
not(any(target_os = "macos", target_os = "ios", target_os = "android"))
))]
mod secret_service_lock;
pub use env::EnvSecretProvider;
pub use file::{FileSecretProvider, SECRET_FILE_PATH_ENV};
pub use handoff::{with_parent_secret_handoff, ParentSecretHandoff, PARENT_SECRET_HANDOFF_OPTION};
pub use keyring::{
keychain_interaction_allowed, KeyringSecretProvider, NativeKeyring, NativeKeyringAvailability,
NativeKeyringError, NativeKeyringUnavailable, SECRET_INTERACTIVE_ENV,
};
pub use memory::MemorySecretProvider;
pub const DEFAULT_SECRET_PROVIDER_CHAIN: &str = "env,keyring";
pub const SECRET_PROVIDER_CHAIN_ENV: &str = "HARN_SECRET_PROVIDERS";
pub const SECRET_REF_SCHEME: &str = "harn-secret://";
pub const DEFAULT_SECRET_NAMESPACE: &str = "harn.provider_auth";
pub const SECRET_NAMESPACE_ENV: &str = "HARN_SECRET_NAMESPACE";
pub const CONNECTOR_OAUTH_TOKEN_SECRET_NAME: &str = "oauth-token";
pub const CONNECTOR_ACCESS_TOKEN_SECRET_NAME: &str = "access-token";
pub const CONNECTOR_REFRESH_TOKEN_SECRET_NAME: &str = "refresh-token";
const RUNTIME_PROVENANCE_SECRET_NAMESPACE: &str = "provenance";
const SCOPED_RUNTIME_PROVENANCE_SECRET_NAMESPACE: &str = "harn.provenance";
tokio::task_local! {
static ACTIVE_SECRET_PROVIDER: Arc<dyn SecretProvider>;
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
pub enum SecretVersion {
#[default]
Latest,
Exact(u64),
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
pub struct SecretId {
pub namespace: String,
pub name: String,
#[serde(default)]
pub version: SecretVersion,
}
impl SecretId {
pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
Self {
namespace: namespace.into(),
name: name.into(),
version: SecretVersion::Latest,
}
}
pub fn with_version(mut self, version: SecretVersion) -> Self {
self.version = version;
self
}
}
impl fmt::Display for SecretId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.namespace.is_empty() {
write!(f, "{}", self.name)?;
} else {
write!(f, "{}/{}", self.namespace, self.name)?;
}
match self.version {
SecretVersion::Latest => Ok(()),
SecretVersion::Exact(version) => write!(f, "@{version}"),
}
}
}
pub fn parse_secret_ref(raw: &str) -> Result<Option<SecretId>, SecretError> {
let trimmed = raw.trim();
let Some(rest) = trimmed.strip_prefix(SECRET_REF_SCHEME) else {
return Ok(None);
};
let (base, version) = match rest.rsplit_once('@') {
Some((base, version_text)) => {
let version = version_text.parse::<u64>().map_err(|_| {
SecretError::InvalidInput(format!(
"invalid secret reference version in '{trimmed}'"
))
})?;
(base, SecretVersion::Exact(version))
}
None => (rest, SecretVersion::Latest),
};
let (namespace, name) = base.split_once('/').ok_or_else(|| {
SecretError::InvalidInput(format!(
"invalid secret reference '{trimmed}': expected {SECRET_REF_SCHEME}<namespace>/<name>"
))
})?;
if namespace.trim().is_empty() || name.trim().is_empty() {
return Err(SecretError::InvalidInput(format!(
"invalid secret reference '{trimmed}': namespace and name must be non-empty"
)));
}
Ok(Some(
SecretId::new(namespace.trim(), name.trim()).with_version(version),
))
}
pub fn parse_secret_id(raw: &str) -> Result<SecretId, SecretError> {
if let Some(id) = parse_secret_ref(raw)? {
return Ok(id);
}
parse_secret_id_body(raw.trim(), raw)
}
fn parse_secret_id_body(body: &str, original: &str) -> Result<SecretId, SecretError> {
let (base, version) = match body.rsplit_once('@') {
Some((base, version_text)) => {
let version = version_text.parse::<u64>().map_err(|_| {
SecretError::InvalidInput(format!("invalid secret id version in '{original}'"))
})?;
(base, SecretVersion::Exact(version))
}
None => (body, SecretVersion::Latest),
};
let (namespace, name) = base.split_once('/').ok_or_else(|| {
SecretError::InvalidInput(format!(
"invalid secret id '{original}': expected <namespace>/<name>"
))
})?;
if namespace.trim().is_empty() || name.trim().is_empty() {
return Err(SecretError::InvalidInput(format!(
"invalid secret id '{original}': namespace and name must be non-empty"
)));
}
Ok(SecretId::new(namespace.trim(), name.trim()).with_version(version))
}
pub fn connector_oauth_token_id(provider: &str) -> SecretId {
SecretId::new(provider, CONNECTOR_OAUTH_TOKEN_SECRET_NAME)
}
pub fn connector_access_token_id(provider: &str) -> SecretId {
SecretId::new(provider, CONNECTOR_ACCESS_TOKEN_SECRET_NAME)
}
pub fn connector_refresh_token_id(provider: &str) -> SecretId {
SecretId::new(provider, CONNECTOR_REFRESH_TOKEN_SECRET_NAME)
}
pub fn resolve_secret_ref_to_string(raw: &str) -> Result<Option<String>, SecretError> {
let Some(id) = parse_secret_ref(raw)? else {
return Ok(None);
};
let secret = if let Ok(provider) = ACTIVE_SECRET_PROVIDER.try_with(Arc::clone) {
futures::executor::block_on(provider.get(&id))?
} else {
let chain = configured_secret_chain()?;
futures::executor::block_on(chain.get(&id))?
};
let rendered = secret.with_exposed(|bytes| {
std::str::from_utf8(bytes)
.map(str::to_string)
.map_err(|error| {
SecretError::InvalidInput(format!(
"secret reference '{id}' resolved to non-UTF-8 bytes: {error}"
))
})
})?;
Ok(Some(rendered))
}
pub fn secret_ref_is_present(raw: &str) -> Result<Option<bool>, SecretError> {
let Some(id) = parse_secret_ref(raw)? else {
return Ok(None);
};
let present = if let Ok(provider) = ACTIVE_SECRET_PROVIDER.try_with(Arc::clone) {
futures::executor::block_on(provider.contains(&id))?
} else {
let chain = configured_secret_chain()?;
futures::executor::block_on(chain.contains(&id))?
};
Ok(Some(present))
}
pub async fn with_active_secret_provider<T>(
provider: Option<Arc<dyn SecretProvider>>,
operation: impl Future<Output = T>,
) -> T {
match provider {
Some(provider) => ACTIVE_SECRET_PROVIDER.scope(provider, operation).await,
None => operation.await,
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct SecretMeta {
pub id: SecretId,
pub provider: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct RotationHandle {
pub provider: String,
pub id: SecretId,
pub from_version: Option<u64>,
pub to_version: Option<u64>,
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SecretScope {
Tenant { id: Option<String> },
Workspace { id: String },
System,
Custom { kind: String, id: Option<String> },
}
impl Default for SecretScope {
fn default() -> Self {
Self::Tenant { id: None }
}
}
impl SecretScope {
pub fn tenant(id: Option<String>) -> Self {
Self::Tenant { id }
}
pub fn workspace(id: impl Into<String>) -> Self {
Self::Workspace { id: id.into() }
}
pub fn system() -> Self {
Self::System
}
pub fn custom(kind: impl Into<String>, id: Option<String>) -> Self {
Self::Custom {
kind: kind.into(),
id,
}
}
pub fn namespace(&self) -> String {
match self {
Self::Tenant { id: Some(id) } if !id.is_empty() => format!("harn.tenant.{id}"),
Self::Tenant { .. } => "harn.tenant".to_string(),
Self::Workspace { id } => format!("harn.workspace.{id}"),
Self::System => "harn.system".to_string(),
Self::Custom { kind, id: Some(id) } if !id.is_empty() => {
format!("harn.{kind}.{id}")
}
Self::Custom { kind, .. } => format!("harn.{kind}"),
}
}
pub fn kind(&self) -> &str {
match self {
Self::Tenant { .. } => "tenant",
Self::Workspace { .. } => "workspace",
Self::System => "system",
Self::Custom { kind, .. } => kind.as_str(),
}
}
pub fn id(&self) -> Option<&str> {
match self {
Self::Tenant { id } | Self::Custom { id, .. } => id.as_deref(),
Self::Workspace { id } => Some(id.as_str()),
Self::System => None,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct SecretWriteOptions {
pub ttl: Option<Duration>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct SecretRotationOptions {
pub grace: Option<Duration>,
pub ttl: Option<Duration>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct SecretAuditContext {
pub request_id: Option<String>,
pub actor_subject: Option<String>,
pub actor_kind: Option<String>,
}
#[derive(Debug)]
pub struct SecretReadRequest {
pub id: SecretId,
pub scope: SecretScope,
pub audit: SecretAuditContext,
}
#[derive(Debug)]
pub struct SecretDeleteRequest {
pub id: SecretId,
pub scope: SecretScope,
pub audit: SecretAuditContext,
}
#[derive(Debug)]
pub struct SecretWriteRequest {
pub id: SecretId,
pub scope: SecretScope,
pub value: SecretBytes,
pub options: SecretWriteOptions,
pub audit: SecretAuditContext,
}
#[derive(Debug)]
pub struct SecretRotateRequest {
pub id: SecretId,
pub scope: SecretScope,
pub value: SecretBytes,
pub options: SecretRotationOptions,
pub audit: SecretAuditContext,
}
#[derive(Debug)]
pub struct SecretLeaseRequest {
pub id: SecretId,
pub scope: SecretScope,
pub duration: Duration,
pub audit: SecretAuditContext,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct SecretWriteReceipt {
pub provider: String,
pub id: SecretId,
pub scope: SecretScope,
pub version: Option<u64>,
pub expires_at_unix_ms: Option<i64>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct SecretRotationReceipt {
pub provider: String,
pub id: SecretId,
pub scope: SecretScope,
pub from_version: Option<u64>,
pub to_version: Option<u64>,
pub grace_until_unix_ms: Option<i64>,
pub expires_at_unix_ms: Option<i64>,
}
#[derive(Debug)]
pub struct SecretLeaseGrant {
pub provider: String,
pub id: SecretId,
pub scope: SecretScope,
pub lease_id: String,
pub value: SecretBytes,
pub expires_at_unix_ms: i64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ConsultedSecretProvider {
pub provider: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub locator: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SecretProviderExclusion {
ChainOverride { variable: String, value: String },
ParentHandoff,
}
impl fmt::Display for SecretProviderExclusion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ParentHandoff => write!(f, "excluded by parent secret handoff"),
Self::ChainOverride { variable, value } => {
write!(f, "disabled by {variable}={value}")
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ExcludedSecretProvider {
pub provider: String,
pub reason: SecretProviderExclusion,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct SecretAbsence {
pub id: SecretId,
pub consulted: Vec<ConsultedSecretProvider>,
#[serde(default)]
pub excluded: Vec<ExcludedSecretProvider>,
}
impl fmt::Display for SecretAbsence {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "secret '{}' not found in providers: ", self.id)?;
if self.consulted.is_empty() {
write!(f, "(none)")?;
}
for (index, consulted) in self.consulted.iter().enumerate() {
if index > 0 {
write!(f, ", ")?;
}
write!(f, "{}", consulted.provider)?;
if let Some(locator) = &consulted.locator {
write!(f, " ({locator})")?;
}
}
for excluded in &self.excluded {
write!(f, "; {} {}", excluded.provider, excluded.reason)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum SecretError {
NotFound {
provider: String,
id: SecretId,
},
NotFoundInChain(SecretAbsence),
Unsupported {
provider: String,
operation: &'static str,
},
Backend {
provider: String,
message: String,
},
AccessDenied {
operation: String,
id: SecretId,
message: String,
},
InvalidConfig(String),
InvalidInput(String),
NoProviders {
namespace: String,
},
NeedsUserApproval {
provider: String,
id: SecretId,
},
All(Vec<SecretError>),
}
impl SecretError {
pub fn is_not_found(&self) -> bool {
match self {
Self::NotFound { .. } | Self::NotFoundInChain(_) => true,
Self::All(errors) => !errors.is_empty() && errors.iter().all(Self::is_not_found),
_ => false,
}
}
pub fn needs_user_approval(&self) -> bool {
match self {
Self::NeedsUserApproval { .. } => true,
Self::All(errors) => errors.iter().any(Self::needs_user_approval),
_ => false,
}
}
}
impl fmt::Display for SecretError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotFound { provider, id } => {
write!(f, "{provider}: secret '{id}' not found")
}
Self::NotFoundInChain(absence) => write!(f, "{absence}"),
Self::Unsupported {
provider,
operation,
} => write!(f, "{provider}: operation '{operation}' is unsupported"),
Self::Backend { provider, message } => write!(f, "{provider}: {message}"),
Self::AccessDenied {
operation,
id,
message,
} => write!(f, "secret {operation} denied for '{id}': {message}"),
Self::InvalidConfig(message) => write!(f, "{message}"),
Self::InvalidInput(message) => write!(f, "{message}"),
Self::NoProviders { namespace } => {
write!(
f,
"no secret providers configured for namespace '{namespace}'"
)
}
Self::NeedsUserApproval { provider, id } => write!(
f,
"{provider}: reading secret '{id}' needs approval in a system dialog, which \
this non-interactive process does not show; provide the credential through \
the environment or set {SECRET_PROVIDER_CHAIN_ENV}=env"
),
Self::All(errors) => {
let rendered = errors
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("; ");
write!(f, "all secret providers failed: {rendered}")
}
}
}
}
impl std::error::Error for SecretError {}
#[derive(Default)]
struct SecretBuffer {
bytes: Vec<u8>,
#[cfg(test)]
drop_probe: Option<std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>>,
}
impl SecretBuffer {
fn new(bytes: Vec<u8>) -> Self {
Self {
bytes,
#[cfg(test)]
drop_probe: None,
}
}
fn as_slice(&self) -> &[u8] {
&self.bytes
}
#[cfg(test)]
fn attach_drop_probe(&mut self, probe: std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>) {
self.drop_probe = Some(probe);
}
}
impl std::ops::Deref for SecretBuffer {
type Target = [u8];
fn deref(&self) -> &Self::Target {
self.as_slice()
}
}
impl Zeroize for SecretBuffer {
fn zeroize(&mut self) {
self.bytes.zeroize();
}
}
impl Drop for SecretBuffer {
fn drop(&mut self) {
#[cfg(test)]
if let Some(probe) = &self.drop_probe {
*probe.lock().expect("drop probe poisoned") = Some(self.bytes.clone());
}
}
}
pub struct SecretBytes(Zeroizing<SecretBuffer>);
impl SecretBytes {
pub fn new(bytes: Vec<u8>) -> Self {
Self(Zeroizing::new(SecretBuffer::new(bytes)))
}
pub fn len(&self) -> usize {
self.0.as_slice().len()
}
pub fn is_empty(&self) -> bool {
self.0.as_slice().is_empty()
}
pub fn with_exposed<R>(&self, f: impl FnOnce(&[u8]) -> R) -> R {
f(self.0.as_slice())
}
pub fn reborrow(&self) -> Self {
self.with_exposed(|bytes| Self::new(bytes.to_vec()))
}
#[cfg(test)]
pub(crate) fn attach_drop_probe(
&mut self,
probe: std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>,
) {
self.0.attach_drop_probe(probe);
}
}
impl fmt::Debug for SecretBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SecretBytes {{ redacted: {} bytes }}", self.len())
}
}
impl Serialize for SecretBytes {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&format!("<redacted:{} bytes>", self.len()))
}
}
impl From<Vec<u8>> for SecretBytes {
fn from(value: Vec<u8>) -> Self {
Self::new(value)
}
}
impl From<String> for SecretBytes {
fn from(value: String) -> Self {
Self::new(value.into_bytes())
}
}
impl From<&str> for SecretBytes {
fn from(value: &str) -> Self {
Self::new(value.as_bytes().to_vec())
}
}
impl From<&[u8]> for SecretBytes {
fn from(value: &[u8]) -> Self {
Self::new(value.to_vec())
}
}
#[async_trait]
pub trait SecretProvider: Send + Sync {
async fn get(&self, id: &SecretId) -> Result<SecretBytes, SecretError>;
async fn put(&self, id: &SecretId, value: SecretBytes) -> Result<(), SecretError>;
async fn rotate(&self, id: &SecretId) -> Result<RotationHandle, SecretError>;
async fn list(&self, prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError>;
async fn contains(&self, id: &SecretId) -> Result<bool, SecretError> {
match self.get(id).await {
Ok(_) => Ok(true),
Err(error) if error.is_not_found() => Ok(false),
Err(error) => Err(error),
}
}
async fn read_scoped(&self, request: SecretReadRequest) -> Result<SecretBytes, SecretError> {
ensure_scoped_secret_access_allowed("read", &request.id)?;
self.get(&request.id).await
}
async fn write_scoped(
&self,
request: SecretWriteRequest,
) -> Result<SecretWriteReceipt, SecretError> {
ensure_scoped_secret_access_allowed("write", &request.id)?;
if request.options.ttl.is_some() {
return Err(SecretError::Unsupported {
provider: self.namespace().to_string(),
operation: "write_ttl",
});
}
self.put(&request.id, request.value).await?;
Ok(SecretWriteReceipt {
provider: self.namespace().to_string(),
id: request.id,
scope: request.scope,
version: None,
expires_at_unix_ms: None,
})
}
async fn delete_scoped(&self, request: SecretDeleteRequest) -> Result<(), SecretError> {
ensure_scoped_secret_access_allowed("delete", &request.id)?;
let _ = request;
Err(SecretError::Unsupported {
provider: self.namespace().to_string(),
operation: "delete",
})
}
async fn rotate_scoped(
&self,
request: SecretRotateRequest,
) -> Result<SecretRotationReceipt, SecretError> {
ensure_scoped_secret_access_allowed("rotate", &request.id)?;
let _ = request;
Err(SecretError::Unsupported {
provider: self.namespace().to_string(),
operation: "rotate_to",
})
}
async fn lease_scoped(
&self,
request: SecretLeaseRequest,
) -> Result<SecretLeaseGrant, SecretError> {
ensure_scoped_secret_access_allowed("lease", &request.id)?;
let _ = request;
Err(SecretError::Unsupported {
provider: self.namespace().to_string(),
operation: "lease",
})
}
fn namespace(&self) -> &str;
fn supports_versions(&self) -> bool;
fn locator(&self, _id: &SecretId) -> Option<String> {
None
}
fn kind(&self) -> &str {
self.namespace()
}
fn persists_writes(&self) -> bool {
true
}
}
pub fn ensure_scoped_secret_access_allowed(
operation: impl Into<String>,
id: &SecretId,
) -> Result<(), SecretError> {
if is_runtime_reserved_secret_namespace(&id.namespace) {
return Err(SecretError::AccessDenied {
operation: operation.into(),
id: id.clone(),
message: format!(
"namespace `{}` is reserved for Harn runtime provenance signing and is not accessible through agent-scoped secret APIs",
id.namespace
),
});
}
Ok(())
}
pub fn is_runtime_reserved_secret_namespace(namespace: &str) -> bool {
let namespace = namespace.trim_matches('.');
namespace == RUNTIME_PROVENANCE_SECRET_NAMESPACE
|| namespace == SCOPED_RUNTIME_PROVENANCE_SECRET_NAMESPACE
|| namespace
.strip_prefix(SCOPED_RUNTIME_PROVENANCE_SECRET_NAMESPACE)
.is_some_and(|suffix| suffix.starts_with('.'))
}
pub struct ChainSecretProvider {
namespace: String,
providers: Vec<Arc<dyn SecretProvider>>,
excluded: Vec<ExcludedSecretProvider>,
}
impl ChainSecretProvider {
pub fn new(namespace: impl Into<String>, providers: Vec<Arc<dyn SecretProvider>>) -> Self {
Self {
namespace: namespace.into(),
providers,
excluded: Vec::new(),
}
}
pub fn with_excluded(mut self, excluded: Vec<ExcludedSecretProvider>) -> Self {
self.excluded = excluded;
self
}
pub fn providers(&self) -> &[Arc<dyn SecretProvider>] {
&self.providers
}
pub fn excluded(&self) -> &[ExcludedSecretProvider] {
&self.excluded
}
async fn put_to_write_target(
&self,
id: &SecretId,
value: SecretBytes,
) -> Result<String, SecretError> {
if self.providers.is_empty() {
return Err(SecretError::NoProviders {
namespace: self.namespace.clone(),
});
}
let persistent = self
.providers
.iter()
.filter(|provider| provider.persists_writes())
.collect::<Vec<_>>();
let targets = if persistent.is_empty() {
self.providers.iter().collect::<Vec<_>>()
} else {
persistent
};
let mut errors = Vec::new();
for provider in targets {
match provider.put(id, value.reborrow()).await {
Ok(()) => return Ok(provider.kind().to_string()),
Err(error) => errors.push(error),
}
}
Err(SecretError::All(errors))
}
fn chain_failure(&self, id: &SecretId, errors: Vec<SecretError>) -> SecretError {
if errors.len() != self.providers.len() || !errors.iter().all(SecretError::is_not_found) {
return SecretError::All(errors);
}
let consulted = self
.providers
.iter()
.zip(&errors)
.map(|(provider, error)| ConsultedSecretProvider {
provider: match error {
SecretError::NotFound { provider, .. } => provider.clone(),
_ => provider.namespace().to_string(),
},
locator: provider.locator(id),
})
.collect();
SecretError::NotFoundInChain(SecretAbsence {
id: id.clone(),
consulted,
excluded: self.excluded.clone(),
})
}
}
#[async_trait]
impl SecretProvider for ChainSecretProvider {
async fn get(&self, id: &SecretId) -> Result<SecretBytes, SecretError> {
if self.providers.is_empty() {
return Err(SecretError::NoProviders {
namespace: self.namespace.clone(),
});
}
let mut errors = Vec::new();
for provider in &self.providers {
match provider.get(id).await {
Ok(secret) => return Ok(secret),
Err(error) => errors.push(error),
}
}
Err(self.chain_failure(id, errors))
}
async fn contains(&self, id: &SecretId) -> Result<bool, SecretError> {
if self.providers.is_empty() {
return Err(SecretError::NoProviders {
namespace: self.namespace.clone(),
});
}
let mut errors = Vec::new();
for provider in &self.providers {
match provider.contains(id).await {
Ok(true) => return Ok(true),
Ok(false) => {}
Err(error) => errors.push(error),
}
}
if errors.is_empty() {
Ok(false)
} else {
Err(SecretError::All(errors))
}
}
async fn put(&self, id: &SecretId, value: SecretBytes) -> Result<(), SecretError> {
self.put_to_write_target(id, value).await.map(|_| ())
}
async fn write_scoped(
&self,
request: SecretWriteRequest,
) -> Result<SecretWriteReceipt, SecretError> {
ensure_scoped_secret_access_allowed("write", &request.id)?;
if request.options.ttl.is_some() {
return Err(SecretError::Unsupported {
provider: self.namespace.clone(),
operation: "write_ttl",
});
}
let provider = self.put_to_write_target(&request.id, request.value).await?;
Ok(SecretWriteReceipt {
provider,
id: request.id,
scope: request.scope,
version: None,
expires_at_unix_ms: None,
})
}
async fn rotate(&self, id: &SecretId) -> Result<RotationHandle, SecretError> {
if self.providers.is_empty() {
return Err(SecretError::NoProviders {
namespace: self.namespace.clone(),
});
}
let mut errors = Vec::new();
for provider in &self.providers {
match provider.rotate(id).await {
Ok(handle) => return Ok(handle),
Err(error) => errors.push(error),
}
}
Err(SecretError::All(errors))
}
async fn list(&self, prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError> {
if self.providers.is_empty() {
return Err(SecretError::NoProviders {
namespace: self.namespace.clone(),
});
}
let mut errors = Vec::new();
let mut merged = BTreeMap::<SecretId, SecretMeta>::new();
for provider in &self.providers {
match provider.list(prefix).await {
Ok(items) => {
for item in items {
merged.entry(item.id.clone()).or_insert(item);
}
}
Err(error) => errors.push(error),
}
}
if merged.is_empty() && !errors.is_empty() {
return Err(SecretError::All(errors));
}
Ok(merged.into_values().collect())
}
async fn delete_scoped(&self, request: SecretDeleteRequest) -> Result<(), SecretError> {
ensure_scoped_secret_access_allowed("delete", &request.id)?;
if self.providers.is_empty() {
return Err(SecretError::NoProviders {
namespace: self.namespace.clone(),
});
}
let mut errors = Vec::new();
let mut any_ok = false;
for provider in &self.providers {
match provider
.delete_scoped(SecretDeleteRequest {
id: request.id.clone(),
scope: request.scope.clone(),
audit: request.audit.clone(),
})
.await
{
Ok(()) => any_ok = true,
Err(error) if error.is_not_found() => any_ok = true,
Err(
error @ SecretError::Unsupported {
operation: "delete",
..
},
) => {
match provider.contains(&request.id).await {
Ok(false) => any_ok = true,
Ok(true) => errors.push(error),
Err(error) => errors.push(error),
}
}
Err(error) => errors.push(error),
}
}
if any_ok && errors.is_empty() {
Ok(())
} else {
Err(SecretError::All(errors))
}
}
fn namespace(&self) -> &str {
&self.namespace
}
fn supports_versions(&self) -> bool {
self.providers
.iter()
.any(|provider| provider.supports_versions())
}
}
pub fn configured_secret_namespace() -> String {
match std::env::var(SECRET_NAMESPACE_ENV) {
Ok(namespace) if !namespace.trim().is_empty() => namespace,
_ => DEFAULT_SECRET_NAMESPACE.to_string(),
}
}
pub fn configured_secret_chain() -> Result<ChainSecretProvider, SecretError> {
configured_default_chain(configured_secret_namespace())
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct SecretChainPlan {
pub providers: Vec<String>,
pub excluded: Vec<ExcludedSecretProvider>,
pub override_value: Option<String>,
}
impl SecretChainPlan {
pub fn from_value(value: Option<&str>) -> Self {
let raw = value.unwrap_or(DEFAULT_SECRET_PROVIDER_CHAIN);
let providers = raw
.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
.map(str::to_string)
.collect::<Vec<_>>();
let excluded = match value {
Some(value) => DEFAULT_SECRET_PROVIDER_CHAIN
.split(',')
.filter(|default| !providers.iter().any(|name| name == default))
.map(|default| ExcludedSecretProvider {
provider: default.to_string(),
reason: SecretProviderExclusion::ChainOverride {
variable: SECRET_PROVIDER_CHAIN_ENV.to_string(),
value: value.to_string(),
},
})
.collect(),
None => Vec::new(),
};
Self {
providers,
excluded,
override_value: value.map(str::to_string),
}
}
pub fn configured() -> Self {
if handoff::active_parent_provider().is_some() {
return Self {
providers: vec!["parent-handoff".into()],
excluded: DEFAULT_SECRET_PROVIDER_CHAIN
.split(',')
.map(|provider| ExcludedSecretProvider {
provider: provider.into(),
reason: SecretProviderExclusion::ParentHandoff,
})
.collect(),
override_value: None,
};
}
Self::from_value(std::env::var(SECRET_PROVIDER_CHAIN_ENV).ok().as_deref())
}
pub fn display(&self) -> String {
let mut rendered = if self.providers.is_empty() {
"(none)".to_string()
} else {
self.providers.join(" -> ")
};
for excluded in &self.excluded {
rendered.push_str(&format!("; {} {}", excluded.provider, excluded.reason));
}
rendered
}
}
pub fn configured_default_chain(
namespace: impl Into<String>,
) -> Result<ChainSecretProvider, SecretError> {
let namespace = namespace.into();
if let Some(parent) = handoff::active_parent_provider() {
return Ok(ChainSecretProvider::new(namespace, vec![parent])
.with_excluded(SecretChainPlan::configured().excluded));
}
let plan = SecretChainPlan::configured();
let mut providers: Vec<Arc<dyn SecretProvider>> = Vec::new();
for provider_name in &plan.providers {
match provider_name.as_str() {
"env" => providers.push(Arc::new(EnvSecretProvider::new(namespace.clone()))),
"keyring" => providers.push(Arc::new(KeyringSecretProvider::new(namespace.clone()))),
"file" => {
let path = std::env::var_os(SECRET_FILE_PATH_ENV)
.filter(|path| !path.is_empty())
.ok_or_else(|| {
SecretError::InvalidConfig(format!(
"the file secret provider requires {SECRET_FILE_PATH_ENV}"
))
})?;
providers.push(Arc::new(FileSecretProvider::new(path)?));
}
other => {
return Err(SecretError::InvalidConfig(format!(
"unsupported secret provider '{other}' in {SECRET_PROVIDER_CHAIN_ENV}; expected a comma-separated list of env,keyring,file"
)))
}
}
}
Ok(ChainSecretProvider::new(namespace, providers).with_excluded(plan.excluded))
}
pub(crate) fn emit_secret_access_event(provider: &str, id: &SecretId) {
#[derive(Serialize)]
struct SecretAccessEvent<'a> {
topic: &'a str,
provider: &'a str,
id: &'a SecretId,
caller_span_id: Option<u64>,
mutation_session_id: Option<String>,
timestamp: String,
}
let event = SecretAccessEvent {
topic: "audit.secret_access",
provider,
id,
caller_span_id: crate::tracing::current_span_id(),
mutation_session_id: crate::orchestration::current_mutation_session()
.map(|session| session.session_id),
timestamp: crate::orchestration::now_unix_seconds_text(),
};
let metadata = serde_json::to_value(event)
.ok()
.and_then(|value| value.as_object().cloned())
.map(|object| object.into_iter().collect::<BTreeMap<_, _>>())
.unwrap_or_default();
crate::events::log_info_meta("secret.audit", "secret accessed", metadata);
}
#[cfg(test)]
mod tests;