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