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