Skip to main content

harn_vm/secrets/
mod.rs

1use std::collections::BTreeMap;
2use std::fmt;
3use std::sync::Arc;
4use std::time::Duration;
5
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use zeroize::{Zeroize, Zeroizing};
9
10mod env;
11mod keyring;
12
13pub use env::EnvSecretProvider;
14pub use keyring::{KeyringSecretProvider, NativeKeyring, NativeKeyringError};
15
16pub const DEFAULT_SECRET_PROVIDER_CHAIN: &str = "env,keyring";
17pub const SECRET_PROVIDER_CHAIN_ENV: &str = "HARN_SECRET_PROVIDERS";
18pub const SECRET_REF_SCHEME: &str = "harn-secret://";
19pub const SECRET_REF_CHAIN_NAMESPACE: &str = "harn.provider_auth";
20pub const CONNECTOR_OAUTH_TOKEN_SECRET_NAME: &str = "oauth-token";
21pub const CONNECTOR_ACCESS_TOKEN_SECRET_NAME: &str = "access-token";
22pub const CONNECTOR_REFRESH_TOKEN_SECRET_NAME: &str = "refresh-token";
23const RUNTIME_PROVENANCE_SECRET_NAMESPACE: &str = "provenance";
24const SCOPED_RUNTIME_PROVENANCE_SECRET_NAMESPACE: &str = "harn.provenance";
25
26#[derive(Clone, Debug, Default, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
27pub enum SecretVersion {
28    #[default]
29    Latest,
30    Exact(u64),
31}
32
33#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
34pub struct SecretId {
35    pub namespace: String,
36    pub name: String,
37    #[serde(default)]
38    pub version: SecretVersion,
39}
40
41impl SecretId {
42    pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
43        Self {
44            namespace: namespace.into(),
45            name: name.into(),
46            version: SecretVersion::Latest,
47        }
48    }
49
50    pub fn with_version(mut self, version: SecretVersion) -> Self {
51        self.version = version;
52        self
53    }
54}
55
56impl fmt::Display for SecretId {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        if self.namespace.is_empty() {
59            write!(f, "{}", self.name)?;
60        } else {
61            write!(f, "{}/{}", self.namespace, self.name)?;
62        }
63        match self.version {
64            SecretVersion::Latest => Ok(()),
65            SecretVersion::Exact(version) => write!(f, "@{version}"),
66        }
67    }
68}
69
70pub fn parse_secret_ref(raw: &str) -> Result<Option<SecretId>, SecretError> {
71    let trimmed = raw.trim();
72    let Some(rest) = trimmed.strip_prefix(SECRET_REF_SCHEME) else {
73        return Ok(None);
74    };
75    let (base, version) = match rest.rsplit_once('@') {
76        Some((base, version_text)) => {
77            let version = version_text.parse::<u64>().map_err(|_| {
78                SecretError::InvalidInput(format!(
79                    "invalid secret reference version in '{trimmed}'"
80                ))
81            })?;
82            (base, SecretVersion::Exact(version))
83        }
84        None => (rest, SecretVersion::Latest),
85    };
86    let (namespace, name) = base.split_once('/').ok_or_else(|| {
87        SecretError::InvalidInput(format!(
88            "invalid secret reference '{trimmed}': expected {SECRET_REF_SCHEME}<namespace>/<name>"
89        ))
90    })?;
91    if namespace.trim().is_empty() || name.trim().is_empty() {
92        return Err(SecretError::InvalidInput(format!(
93            "invalid secret reference '{trimmed}': namespace and name must be non-empty"
94        )));
95    }
96    Ok(Some(
97        SecretId::new(namespace.trim(), name.trim()).with_version(version),
98    ))
99}
100
101pub fn parse_secret_id(raw: &str) -> Result<SecretId, SecretError> {
102    if let Some(id) = parse_secret_ref(raw)? {
103        return Ok(id);
104    }
105    parse_secret_id_body(raw.trim(), raw)
106}
107
108fn parse_secret_id_body(body: &str, original: &str) -> Result<SecretId, SecretError> {
109    let (base, version) = match body.rsplit_once('@') {
110        Some((base, version_text)) => {
111            let version = version_text.parse::<u64>().map_err(|_| {
112                SecretError::InvalidInput(format!("invalid secret id version in '{original}'"))
113            })?;
114            (base, SecretVersion::Exact(version))
115        }
116        None => (body, SecretVersion::Latest),
117    };
118    let (namespace, name) = base.split_once('/').ok_or_else(|| {
119        SecretError::InvalidInput(format!(
120            "invalid secret id '{original}': expected <namespace>/<name>"
121        ))
122    })?;
123    if namespace.trim().is_empty() || name.trim().is_empty() {
124        return Err(SecretError::InvalidInput(format!(
125            "invalid secret id '{original}': namespace and name must be non-empty"
126        )));
127    }
128    Ok(SecretId::new(namespace.trim(), name.trim()).with_version(version))
129}
130
131pub fn connector_oauth_token_id(provider: &str) -> SecretId {
132    SecretId::new(provider, CONNECTOR_OAUTH_TOKEN_SECRET_NAME)
133}
134
135pub fn connector_access_token_id(provider: &str) -> SecretId {
136    SecretId::new(provider, CONNECTOR_ACCESS_TOKEN_SECRET_NAME)
137}
138
139pub fn connector_refresh_token_id(provider: &str) -> SecretId {
140    SecretId::new(provider, CONNECTOR_REFRESH_TOKEN_SECRET_NAME)
141}
142
143pub fn resolve_secret_ref_to_string(raw: &str) -> Result<Option<String>, SecretError> {
144    let Some(id) = parse_secret_ref(raw)? else {
145        return Ok(None);
146    };
147    let chain = configured_default_chain(SECRET_REF_CHAIN_NAMESPACE)?;
148    let secret = futures::executor::block_on(chain.get(&id))?;
149    let rendered = secret.with_exposed(|bytes| {
150        std::str::from_utf8(bytes)
151            .map(str::to_string)
152            .map_err(|error| {
153                SecretError::InvalidInput(format!(
154                    "secret reference '{id}' resolved to non-UTF-8 bytes: {error}"
155                ))
156            })
157    })?;
158    Ok(Some(rendered))
159}
160
161#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
162pub struct SecretMeta {
163    pub id: SecretId,
164    pub provider: String,
165}
166
167#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
168pub struct RotationHandle {
169    pub provider: String,
170    pub id: SecretId,
171    pub from_version: Option<u64>,
172    pub to_version: Option<u64>,
173}
174
175#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
176#[serde(rename_all = "snake_case")]
177pub enum SecretScope {
178    Tenant { id: Option<String> },
179    Workspace { id: String },
180    System,
181    Custom { kind: String, id: Option<String> },
182}
183
184impl Default for SecretScope {
185    fn default() -> Self {
186        Self::Tenant { id: None }
187    }
188}
189
190impl SecretScope {
191    pub fn tenant(id: Option<String>) -> Self {
192        Self::Tenant { id }
193    }
194
195    pub fn workspace(id: impl Into<String>) -> Self {
196        Self::Workspace { id: id.into() }
197    }
198
199    pub fn system() -> Self {
200        Self::System
201    }
202
203    pub fn custom(kind: impl Into<String>, id: Option<String>) -> Self {
204        Self::Custom {
205            kind: kind.into(),
206            id,
207        }
208    }
209
210    pub fn namespace(&self) -> String {
211        match self {
212            Self::Tenant { id: Some(id) } if !id.is_empty() => format!("harn.tenant.{id}"),
213            Self::Tenant { .. } => "harn.tenant".to_string(),
214            Self::Workspace { id } => format!("harn.workspace.{id}"),
215            Self::System => "harn.system".to_string(),
216            Self::Custom { kind, id: Some(id) } if !id.is_empty() => {
217                format!("harn.{kind}.{id}")
218            }
219            Self::Custom { kind, .. } => format!("harn.{kind}"),
220        }
221    }
222
223    pub fn kind(&self) -> &str {
224        match self {
225            Self::Tenant { .. } => "tenant",
226            Self::Workspace { .. } => "workspace",
227            Self::System => "system",
228            Self::Custom { kind, .. } => kind.as_str(),
229        }
230    }
231
232    pub fn id(&self) -> Option<&str> {
233        match self {
234            Self::Tenant { id } | Self::Custom { id, .. } => id.as_deref(),
235            Self::Workspace { id } => Some(id.as_str()),
236            Self::System => None,
237        }
238    }
239}
240
241#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
242pub struct SecretWriteOptions {
243    pub ttl: Option<Duration>,
244}
245
246#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
247pub struct SecretRotationOptions {
248    pub grace: Option<Duration>,
249    pub ttl: Option<Duration>,
250}
251
252#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
253pub struct SecretAuditContext {
254    pub request_id: Option<String>,
255    pub actor_subject: Option<String>,
256    pub actor_kind: Option<String>,
257}
258
259#[derive(Debug)]
260pub struct SecretReadRequest {
261    pub id: SecretId,
262    pub scope: SecretScope,
263    pub audit: SecretAuditContext,
264}
265
266#[derive(Debug)]
267pub struct SecretDeleteRequest {
268    pub id: SecretId,
269    pub scope: SecretScope,
270    pub audit: SecretAuditContext,
271}
272
273#[derive(Debug)]
274pub struct SecretWriteRequest {
275    pub id: SecretId,
276    pub scope: SecretScope,
277    pub value: SecretBytes,
278    pub options: SecretWriteOptions,
279    pub audit: SecretAuditContext,
280}
281
282#[derive(Debug)]
283pub struct SecretRotateRequest {
284    pub id: SecretId,
285    pub scope: SecretScope,
286    pub value: SecretBytes,
287    pub options: SecretRotationOptions,
288    pub audit: SecretAuditContext,
289}
290
291#[derive(Debug)]
292pub struct SecretLeaseRequest {
293    pub id: SecretId,
294    pub scope: SecretScope,
295    pub duration: Duration,
296    pub audit: SecretAuditContext,
297}
298
299#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
300pub struct SecretWriteReceipt {
301    pub provider: String,
302    pub id: SecretId,
303    pub scope: SecretScope,
304    pub version: Option<u64>,
305    pub expires_at_unix_ms: Option<i64>,
306}
307
308#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
309pub struct SecretRotationReceipt {
310    pub provider: String,
311    pub id: SecretId,
312    pub scope: SecretScope,
313    pub from_version: Option<u64>,
314    pub to_version: Option<u64>,
315    pub grace_until_unix_ms: Option<i64>,
316    pub expires_at_unix_ms: Option<i64>,
317}
318
319#[derive(Debug)]
320pub struct SecretLeaseGrant {
321    pub provider: String,
322    pub id: SecretId,
323    pub scope: SecretScope,
324    pub lease_id: String,
325    pub value: SecretBytes,
326    pub expires_at_unix_ms: i64,
327}
328
329#[derive(Clone, Debug, Eq, PartialEq)]
330pub enum SecretError {
331    NotFound {
332        provider: String,
333        id: SecretId,
334    },
335    Unsupported {
336        provider: String,
337        operation: &'static str,
338    },
339    Backend {
340        provider: String,
341        message: String,
342    },
343    AccessDenied {
344        operation: String,
345        id: SecretId,
346        message: String,
347    },
348    InvalidConfig(String),
349    InvalidInput(String),
350    NoProviders {
351        namespace: String,
352    },
353    All(Vec<SecretError>),
354}
355
356impl fmt::Display for SecretError {
357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358        match self {
359            Self::NotFound { provider, id } => {
360                write!(f, "{provider}: secret '{id}' not found")
361            }
362            Self::Unsupported {
363                provider,
364                operation,
365            } => write!(f, "{provider}: operation '{operation}' is unsupported"),
366            Self::Backend { provider, message } => write!(f, "{provider}: {message}"),
367            Self::AccessDenied {
368                operation,
369                id,
370                message,
371            } => write!(f, "secret {operation} denied for '{id}': {message}"),
372            Self::InvalidConfig(message) => write!(f, "{message}"),
373            Self::InvalidInput(message) => write!(f, "{message}"),
374            Self::NoProviders { namespace } => {
375                write!(
376                    f,
377                    "no secret providers configured for namespace '{namespace}'"
378                )
379            }
380            Self::All(errors) => {
381                let rendered = errors
382                    .iter()
383                    .map(ToString::to_string)
384                    .collect::<Vec<_>>()
385                    .join("; ");
386                write!(f, "all secret providers failed: {rendered}")
387            }
388        }
389    }
390}
391
392impl std::error::Error for SecretError {}
393
394#[derive(Default)]
395struct SecretBuffer {
396    bytes: Vec<u8>,
397    #[cfg(test)]
398    drop_probe: Option<std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>>,
399}
400
401impl SecretBuffer {
402    fn new(bytes: Vec<u8>) -> Self {
403        Self {
404            bytes,
405            #[cfg(test)]
406            drop_probe: None,
407        }
408    }
409
410    fn as_slice(&self) -> &[u8] {
411        &self.bytes
412    }
413
414    #[cfg(test)]
415    fn attach_drop_probe(&mut self, probe: std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>) {
416        self.drop_probe = Some(probe);
417    }
418}
419
420impl std::ops::Deref for SecretBuffer {
421    type Target = [u8];
422
423    fn deref(&self) -> &Self::Target {
424        self.as_slice()
425    }
426}
427
428impl Zeroize for SecretBuffer {
429    fn zeroize(&mut self) {
430        self.bytes.zeroize();
431    }
432}
433
434impl Drop for SecretBuffer {
435    fn drop(&mut self) {
436        #[cfg(test)]
437        if let Some(probe) = &self.drop_probe {
438            *probe.lock().expect("drop probe poisoned") = Some(self.bytes.clone());
439        }
440    }
441}
442
443pub struct SecretBytes(Zeroizing<SecretBuffer>);
444
445impl SecretBytes {
446    pub fn new(bytes: Vec<u8>) -> Self {
447        Self(Zeroizing::new(SecretBuffer::new(bytes)))
448    }
449
450    pub fn len(&self) -> usize {
451        self.0.as_slice().len()
452    }
453
454    pub fn is_empty(&self) -> bool {
455        self.0.as_slice().is_empty()
456    }
457
458    pub fn with_exposed<R>(&self, f: impl FnOnce(&[u8]) -> R) -> R {
459        f(self.0.as_slice())
460    }
461
462    pub fn reborrow(&self) -> Self {
463        self.with_exposed(|bytes| Self::new(bytes.to_vec()))
464    }
465
466    #[cfg(test)]
467    pub(crate) fn attach_drop_probe(
468        &mut self,
469        probe: std::sync::Arc<std::sync::Mutex<Option<Vec<u8>>>>,
470    ) {
471        self.0.attach_drop_probe(probe);
472    }
473}
474
475impl fmt::Debug for SecretBytes {
476    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
477        write!(f, "SecretBytes {{ redacted: {} bytes }}", self.len())
478    }
479}
480
481impl Serialize for SecretBytes {
482    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
483    where
484        S: serde::Serializer,
485    {
486        serializer.serialize_str(&format!("<redacted:{} bytes>", self.len()))
487    }
488}
489
490impl From<Vec<u8>> for SecretBytes {
491    fn from(value: Vec<u8>) -> Self {
492        Self::new(value)
493    }
494}
495
496impl From<String> for SecretBytes {
497    fn from(value: String) -> Self {
498        Self::new(value.into_bytes())
499    }
500}
501
502impl From<&str> for SecretBytes {
503    fn from(value: &str) -> Self {
504        Self::new(value.as_bytes().to_vec())
505    }
506}
507
508impl From<&[u8]> for SecretBytes {
509    fn from(value: &[u8]) -> Self {
510        Self::new(value.to_vec())
511    }
512}
513
514#[async_trait]
515pub trait SecretProvider: Send + Sync {
516    async fn get(&self, id: &SecretId) -> Result<SecretBytes, SecretError>;
517    async fn put(&self, id: &SecretId, value: SecretBytes) -> Result<(), SecretError>;
518    async fn rotate(&self, id: &SecretId) -> Result<RotationHandle, SecretError>;
519    async fn list(&self, prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError>;
520
521    async fn read_scoped(&self, request: SecretReadRequest) -> Result<SecretBytes, SecretError> {
522        ensure_scoped_secret_access_allowed("read", &request.id)?;
523        self.get(&request.id).await
524    }
525
526    async fn write_scoped(
527        &self,
528        request: SecretWriteRequest,
529    ) -> Result<SecretWriteReceipt, SecretError> {
530        ensure_scoped_secret_access_allowed("write", &request.id)?;
531        if request.options.ttl.is_some() {
532            return Err(SecretError::Unsupported {
533                provider: self.namespace().to_string(),
534                operation: "write_ttl",
535            });
536        }
537        self.put(&request.id, request.value).await?;
538        Ok(SecretWriteReceipt {
539            provider: self.namespace().to_string(),
540            id: request.id,
541            scope: request.scope,
542            version: None,
543            expires_at_unix_ms: None,
544        })
545    }
546
547    async fn delete_scoped(&self, request: SecretDeleteRequest) -> Result<(), SecretError> {
548        ensure_scoped_secret_access_allowed("delete", &request.id)?;
549        let _ = request;
550        Err(SecretError::Unsupported {
551            provider: self.namespace().to_string(),
552            operation: "delete",
553        })
554    }
555
556    async fn rotate_scoped(
557        &self,
558        request: SecretRotateRequest,
559    ) -> Result<SecretRotationReceipt, SecretError> {
560        ensure_scoped_secret_access_allowed("rotate", &request.id)?;
561        let _ = request;
562        Err(SecretError::Unsupported {
563            provider: self.namespace().to_string(),
564            operation: "rotate_to",
565        })
566    }
567
568    async fn lease_scoped(
569        &self,
570        request: SecretLeaseRequest,
571    ) -> Result<SecretLeaseGrant, SecretError> {
572        ensure_scoped_secret_access_allowed("lease", &request.id)?;
573        let _ = request;
574        Err(SecretError::Unsupported {
575            provider: self.namespace().to_string(),
576            operation: "lease",
577        })
578    }
579
580    fn namespace(&self) -> &str;
581    fn supports_versions(&self) -> bool;
582}
583
584pub fn ensure_scoped_secret_access_allowed(
585    operation: impl Into<String>,
586    id: &SecretId,
587) -> Result<(), SecretError> {
588    if is_runtime_reserved_secret_namespace(&id.namespace) {
589        return Err(SecretError::AccessDenied {
590            operation: operation.into(),
591            id: id.clone(),
592            message: format!(
593                "namespace `{}` is reserved for Harn runtime provenance signing and is not accessible through agent-scoped secret APIs",
594                id.namespace
595            ),
596        });
597    }
598    Ok(())
599}
600
601pub fn is_runtime_reserved_secret_namespace(namespace: &str) -> bool {
602    let namespace = namespace.trim_matches('.');
603    namespace == RUNTIME_PROVENANCE_SECRET_NAMESPACE
604        || namespace == SCOPED_RUNTIME_PROVENANCE_SECRET_NAMESPACE
605        || namespace
606            .strip_prefix(SCOPED_RUNTIME_PROVENANCE_SECRET_NAMESPACE)
607            .is_some_and(|suffix| suffix.starts_with('.'))
608}
609
610pub struct ChainSecretProvider {
611    namespace: String,
612    providers: Vec<Arc<dyn SecretProvider>>,
613}
614
615impl ChainSecretProvider {
616    pub fn new(namespace: impl Into<String>, providers: Vec<Arc<dyn SecretProvider>>) -> Self {
617        Self {
618            namespace: namespace.into(),
619            providers,
620        }
621    }
622
623    pub fn providers(&self) -> &[Arc<dyn SecretProvider>] {
624        &self.providers
625    }
626}
627
628#[async_trait]
629impl SecretProvider for ChainSecretProvider {
630    async fn get(&self, id: &SecretId) -> Result<SecretBytes, SecretError> {
631        if self.providers.is_empty() {
632            return Err(SecretError::NoProviders {
633                namespace: self.namespace.clone(),
634            });
635        }
636
637        let mut errors = Vec::new();
638        for provider in &self.providers {
639            match provider.get(id).await {
640                Ok(secret) => return Ok(secret),
641                Err(error) => errors.push(error),
642            }
643        }
644
645        Err(SecretError::All(errors))
646    }
647
648    async fn put(&self, id: &SecretId, value: SecretBytes) -> Result<(), SecretError> {
649        if self.providers.is_empty() {
650            return Err(SecretError::NoProviders {
651                namespace: self.namespace.clone(),
652            });
653        }
654
655        let mut last_value = Some(value);
656        let mut errors = Vec::new();
657        for (index, provider) in self.providers.iter().enumerate() {
658            let attempt_value = if index + 1 == self.providers.len() {
659                last_value
660                    .take()
661                    .expect("final secret write attempt missing value")
662            } else {
663                last_value
664                    .as_ref()
665                    .expect("intermediate secret write attempt missing value")
666                    .reborrow()
667            };
668            match provider.put(id, attempt_value).await {
669                Ok(()) => return Ok(()),
670                Err(error) => errors.push(error),
671            }
672        }
673
674        Err(SecretError::All(errors))
675    }
676
677    async fn rotate(&self, id: &SecretId) -> Result<RotationHandle, SecretError> {
678        if self.providers.is_empty() {
679            return Err(SecretError::NoProviders {
680                namespace: self.namespace.clone(),
681            });
682        }
683
684        let mut errors = Vec::new();
685        for provider in &self.providers {
686            match provider.rotate(id).await {
687                Ok(handle) => return Ok(handle),
688                Err(error) => errors.push(error),
689            }
690        }
691
692        Err(SecretError::All(errors))
693    }
694
695    async fn list(&self, prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError> {
696        if self.providers.is_empty() {
697            return Err(SecretError::NoProviders {
698                namespace: self.namespace.clone(),
699            });
700        }
701
702        let mut errors = Vec::new();
703        let mut merged = BTreeMap::<SecretId, SecretMeta>::new();
704        for provider in &self.providers {
705            match provider.list(prefix).await {
706                Ok(items) => {
707                    for item in items {
708                        merged.entry(item.id.clone()).or_insert(item);
709                    }
710                }
711                Err(error) => errors.push(error),
712            }
713        }
714
715        if merged.is_empty() && !errors.is_empty() {
716            return Err(SecretError::All(errors));
717        }
718
719        Ok(merged.into_values().collect())
720    }
721
722    async fn delete_scoped(&self, request: SecretDeleteRequest) -> Result<(), SecretError> {
723        ensure_scoped_secret_access_allowed("delete", &request.id)?;
724        if self.providers.is_empty() {
725            return Err(SecretError::NoProviders {
726                namespace: self.namespace.clone(),
727            });
728        }
729
730        // Delete from every backend that supports it so a stale copy in one
731        // provider can't resurrect a credential the caller asked to revoke.
732        // A `NotFound` counts as success — the secret is already gone there.
733        let mut errors = Vec::new();
734        let mut any_ok = false;
735        for provider in &self.providers {
736            match provider
737                .delete_scoped(SecretDeleteRequest {
738                    id: request.id.clone(),
739                    scope: request.scope.clone(),
740                    audit: request.audit.clone(),
741                })
742                .await
743            {
744                Ok(()) | Err(SecretError::NotFound { .. }) => any_ok = true,
745                Err(error) => errors.push(error),
746            }
747        }
748
749        if any_ok {
750            Ok(())
751        } else {
752            Err(SecretError::All(errors))
753        }
754    }
755
756    fn namespace(&self) -> &str {
757        &self.namespace
758    }
759
760    fn supports_versions(&self) -> bool {
761        self.providers
762            .iter()
763            .any(|provider| provider.supports_versions())
764    }
765}
766
767pub fn configured_default_chain(
768    namespace: impl Into<String>,
769) -> Result<ChainSecretProvider, SecretError> {
770    let namespace = namespace.into();
771    let configured = std::env::var(SECRET_PROVIDER_CHAIN_ENV)
772        .unwrap_or_else(|_| DEFAULT_SECRET_PROVIDER_CHAIN.to_string());
773    let mut providers: Vec<Arc<dyn SecretProvider>> = Vec::new();
774
775    for raw_name in configured.split(',') {
776        let provider_name = raw_name.trim();
777        if provider_name.is_empty() {
778            continue;
779        }
780        match provider_name {
781            "env" => providers.push(Arc::new(EnvSecretProvider::new(namespace.clone()))),
782            "keyring" => providers.push(Arc::new(KeyringSecretProvider::new(namespace.clone()))),
783            other => {
784                return Err(SecretError::InvalidConfig(format!(
785                    "unsupported secret provider '{other}' in {SECRET_PROVIDER_CHAIN_ENV}; expected a comma-separated list of env,keyring"
786                )))
787            }
788        }
789    }
790
791    Ok(ChainSecretProvider::new(namespace, providers))
792}
793
794pub(crate) fn emit_secret_access_event(provider: &str, id: &SecretId) {
795    #[derive(Serialize)]
796    struct SecretAccessEvent<'a> {
797        topic: &'a str,
798        provider: &'a str,
799        id: &'a SecretId,
800        caller_span_id: Option<u64>,
801        mutation_session_id: Option<String>,
802        timestamp: String,
803    }
804
805    let event = SecretAccessEvent {
806        topic: "audit.secret_access",
807        provider,
808        id,
809        caller_span_id: crate::tracing::current_span_id(),
810        mutation_session_id: crate::orchestration::current_mutation_session()
811            .map(|session| session.session_id),
812        timestamp: crate::orchestration::now_unix_seconds_text(),
813    };
814    let metadata = serde_json::to_value(event)
815        .ok()
816        .and_then(|value| value.as_object().cloned())
817        .map(|object| object.into_iter().collect::<BTreeMap<_, _>>())
818        .unwrap_or_default();
819    crate::events::log_info_meta("secret.audit", "secret accessed", metadata);
820}
821
822#[cfg(test)]
823mod tests {
824    use std::sync::{Arc, Mutex};
825
826    use async_trait::async_trait;
827
828    use super::*;
829
830    struct FakeProvider {
831        namespace: String,
832        result: Mutex<Vec<Result<SecretBytes, SecretError>>>,
833    }
834
835    impl FakeProvider {
836        fn new(
837            namespace: impl Into<String>,
838            result: Vec<Result<SecretBytes, SecretError>>,
839        ) -> Self {
840            Self {
841                namespace: namespace.into(),
842                result: Mutex::new(result),
843            }
844        }
845    }
846
847    #[async_trait]
848    impl SecretProvider for FakeProvider {
849        async fn get(&self, _id: &SecretId) -> Result<SecretBytes, SecretError> {
850            self.result
851                .lock()
852                .expect("fake provider poisoned")
853                .remove(0)
854        }
855
856        async fn put(&self, _id: &SecretId, _value: SecretBytes) -> Result<(), SecretError> {
857            Err(SecretError::Unsupported {
858                provider: self.namespace.clone(),
859                operation: "put",
860            })
861        }
862
863        async fn rotate(&self, _id: &SecretId) -> Result<RotationHandle, SecretError> {
864            Err(SecretError::Unsupported {
865                provider: self.namespace.clone(),
866                operation: "rotate",
867            })
868        }
869
870        async fn list(&self, _prefix: &SecretId) -> Result<Vec<SecretMeta>, SecretError> {
871            Err(SecretError::Unsupported {
872                provider: self.namespace.clone(),
873                operation: "list",
874            })
875        }
876
877        fn namespace(&self) -> &str {
878            &self.namespace
879        }
880
881        fn supports_versions(&self) -> bool {
882            false
883        }
884    }
885
886    #[test]
887    fn secret_bytes_debug_is_redacted() {
888        let secret = SecretBytes::from("abcd");
889        assert_eq!(format!("{secret:?}"), "SecretBytes { redacted: 4 bytes }");
890    }
891
892    #[test]
893    fn parse_secret_ref_accepts_namespace_name_and_version() {
894        let id = parse_secret_ref("harn-secret://provider/anthropic-api-key@7")
895            .expect("parse should succeed")
896            .expect("secret ref should be detected");
897        assert_eq!(id.namespace, "provider");
898        assert_eq!(id.name, "anthropic-api-key");
899        assert_eq!(id.version, SecretVersion::Exact(7));
900    }
901
902    #[test]
903    fn parse_secret_ref_ignores_non_refs_and_rejects_malformed_refs() {
904        assert!(parse_secret_ref("plain-api-key")
905            .expect("non-ref should be accepted")
906            .is_none());
907        assert!(parse_secret_ref("harn-secret://missing-name")
908            .expect_err("missing slash should fail")
909            .to_string()
910            .contains("invalid secret reference"));
911    }
912
913    #[test]
914    fn parse_secret_id_accepts_canonical_and_ref_forms() {
915        let canonical = parse_secret_id("google_workspace/access-token@2").expect("canonical id");
916        assert_eq!(canonical.namespace, "google_workspace");
917        assert_eq!(canonical.name, "access-token");
918        assert_eq!(canonical.version, SecretVersion::Exact(2));
919
920        let reference =
921            parse_secret_id("harn-secret://google_workspace/refresh-token").expect("ref id");
922        assert_eq!(reference, connector_refresh_token_id("google_workspace"));
923
924        assert_eq!(
925            connector_oauth_token_id("google_workspace").name,
926            CONNECTOR_OAUTH_TOKEN_SECRET_NAME
927        );
928        assert_eq!(
929            connector_access_token_id("google_workspace").name,
930            CONNECTOR_ACCESS_TOKEN_SECRET_NAME
931        );
932    }
933
934    #[test]
935    fn secret_bytes_zeroes_on_drop() {
936        let probe = Arc::new(Mutex::new(None));
937        let mut secret = SecretBytes::from("super-secret");
938        secret.attach_drop_probe(probe.clone());
939        drop(secret);
940
941        let dropped = probe
942            .lock()
943            .expect("drop probe poisoned")
944            .clone()
945            .expect("probe should capture bytes");
946        assert!(dropped.iter().all(|byte| *byte == 0));
947    }
948
949    #[tokio::test]
950    async fn chain_secret_provider_falls_through_to_next_hit() {
951        let id = SecretId::new("harn.test", "api-key");
952        let first = Arc::new(FakeProvider::new(
953            "first",
954            vec![Err(SecretError::NotFound {
955                provider: "first".to_string(),
956                id: id.clone(),
957            })],
958        ));
959        let second = Arc::new(FakeProvider::new(
960            "second",
961            vec![Ok(SecretBytes::from("value"))],
962        ));
963        let chain = ChainSecretProvider::new("harn/test", vec![first, second]);
964
965        let secret = chain.get(&id).await.expect("chain should resolve");
966        let exposed = secret.with_exposed(|bytes| bytes.to_vec());
967        assert_eq!(exposed, b"value");
968    }
969
970    #[tokio::test]
971    async fn chain_secret_provider_returns_all_errors_when_everything_fails() {
972        let id = SecretId::new("harn.test", "missing");
973        let first = Arc::new(FakeProvider::new(
974            "first",
975            vec![Err(SecretError::NotFound {
976                provider: "first".to_string(),
977                id: id.clone(),
978            })],
979        ));
980        let second = Arc::new(FakeProvider::new(
981            "second",
982            vec![Err(SecretError::Backend {
983                provider: "second".to_string(),
984                message: "boom".to_string(),
985            })],
986        ));
987        let chain = ChainSecretProvider::new("harn/test", vec![first, second]);
988
989        let error = chain.get(&id).await.expect_err("chain should fail");
990        match error {
991            SecretError::All(errors) => {
992                assert_eq!(errors.len(), 2);
993                assert!(matches!(errors[0], SecretError::NotFound { .. }));
994                assert!(matches!(errors[1], SecretError::Backend { .. }));
995            }
996            other => panic!("expected aggregated errors, got {other:?}"),
997        }
998    }
999
1000    #[tokio::test]
1001    async fn scoped_secret_access_denies_runtime_reserved_namespaces() {
1002        let chain = ChainSecretProvider::new(
1003            "harn/test",
1004            vec![Arc::new(FakeProvider::new("unused", Vec::new()))],
1005        );
1006
1007        for namespace in ["provenance", "harn.provenance", "harn.provenance.agent"] {
1008            let id = SecretId::new(namespace, "harn-cli.ed25519.seed");
1009            let error = chain
1010                .read_scoped(SecretReadRequest {
1011                    id: id.clone(),
1012                    scope: SecretScope::custom("provenance", None),
1013                    audit: SecretAuditContext::default(),
1014                })
1015                .await
1016                .expect_err("reserved namespace should be denied before backend access");
1017            match error {
1018                SecretError::AccessDenied {
1019                    operation,
1020                    id: denied_id,
1021                    message,
1022                } => {
1023                    assert_eq!(operation, "read");
1024                    assert_eq!(denied_id, id);
1025                    assert!(message.contains("reserved for Harn runtime provenance signing"));
1026                }
1027                other => panic!("expected access-denied error, got {other:?}"),
1028            }
1029        }
1030    }
1031
1032    #[tokio::test]
1033    async fn keyring_provider_round_trips_and_zeroes_on_drop() {
1034        let provider = KeyringSecretProvider::with_store(
1035            "harn.test",
1036            keyring_core::mock::Store::new().unwrap(),
1037        );
1038        let id = SecretId::new("", format!("mock-{}", uuid::Uuid::now_v7()));
1039        provider
1040            .put(&id, SecretBytes::from("round-trip-secret"))
1041            .await
1042            .expect("mock keyring write should succeed");
1043
1044        let probe = Arc::new(Mutex::new(None));
1045        let mut secret = provider
1046            .get(&id)
1047            .await
1048            .expect("mock keyring read should succeed");
1049        assert_eq!(
1050            secret.with_exposed(|bytes| bytes.to_vec()),
1051            b"round-trip-secret"
1052        );
1053        secret.attach_drop_probe(probe.clone());
1054        drop(secret);
1055
1056        let dropped = probe
1057            .lock()
1058            .expect("drop probe poisoned")
1059            .clone()
1060            .expect("probe should capture bytes");
1061        assert!(dropped.iter().all(|byte| *byte == 0));
1062
1063        provider
1064            .delete(&id)
1065            .await
1066            .expect("mock keyring delete should succeed");
1067    }
1068}