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