1#![deny(missing_docs)]
2use async_trait::async_trait;
17use layer0::secret::SecretSource;
18use std::sync::Arc;
19use std::time::SystemTime;
20use thiserror::Error;
21use zeroize::Zeroizing;
22
23#[non_exhaustive]
25#[derive(Debug, Error)]
26pub enum SecretError {
27 #[error("secret not found: {0}")]
29 NotFound(String),
30
31 #[error("access denied: {0}")]
33 AccessDenied(String),
34
35 #[error("backend error: {0}")]
37 BackendError(String),
38
39 #[error("lease expired: {0}")]
41 LeaseExpired(String),
42
43 #[error("no resolver for source: {0}")]
46 NoResolver(String),
47
48 #[error("{0}")]
50 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
51}
52
53pub struct SecretValue {
59 inner: Zeroizing<Vec<u8>>,
60}
61
62impl SecretValue {
63 pub fn new(bytes: Vec<u8>) -> Self {
65 Self {
66 inner: Zeroizing::new(bytes),
67 }
68 }
69
70 pub fn with_bytes<R>(&self, f: impl FnOnce(&[u8]) -> R) -> R {
73 f(&self.inner)
74 }
75
76 pub fn len(&self) -> usize {
78 self.inner.len()
79 }
80
81 pub fn is_empty(&self) -> bool {
83 self.inner.is_empty()
84 }
85}
86
87impl std::fmt::Debug for SecretValue {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.write_str("[REDACTED]")
90 }
91}
92
93pub struct SecretLease {
101 pub value: SecretValue,
103 pub expires_at: Option<SystemTime>,
105 pub renewable: bool,
107 pub lease_id: Option<String>,
109}
110
111impl SecretLease {
112 pub fn permanent(value: SecretValue) -> Self {
114 Self {
115 value,
116 expires_at: None,
117 renewable: false,
118 lease_id: None,
119 }
120 }
121
122 pub fn with_ttl(value: SecretValue, ttl: std::time::Duration) -> Self {
124 Self {
125 value,
126 expires_at: Some(SystemTime::now() + ttl),
127 renewable: false,
128 lease_id: None,
129 }
130 }
131
132 pub fn is_expired(&self) -> bool {
134 self.expires_at
135 .map(|exp| SystemTime::now() > exp)
136 .unwrap_or(false)
137 }
138}
139
140impl std::fmt::Debug for SecretLease {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 f.debug_struct("SecretLease")
143 .field("value", &"[REDACTED]")
144 .field("expires_at", &self.expires_at)
145 .field("renewable", &self.renewable)
146 .field("lease_id", &self.lease_id)
147 .finish()
148 }
149}
150
151#[async_trait]
161pub trait SecretResolver: Send + Sync {
162 async fn resolve(&self, source: &SecretSource) -> Result<SecretLease, SecretError>;
164}
165
166#[derive(Debug, Clone)]
168pub enum SourceMatcher {
169 Vault,
171 Aws,
173 Gcp,
175 Azure,
177 OsKeystore,
179 Kubernetes,
181 Hardware,
183 Custom(String),
185}
186
187impl SourceMatcher {
188 pub fn matches(&self, source: &SecretSource) -> bool {
190 match (self, source) {
191 (SourceMatcher::Vault, SecretSource::Vault { .. }) => true,
192 (SourceMatcher::Aws, SecretSource::AwsSecretsManager { .. }) => true,
193 (SourceMatcher::Gcp, SecretSource::GcpSecretManager { .. }) => true,
194 (SourceMatcher::Azure, SecretSource::AzureKeyVault { .. }) => true,
195 (SourceMatcher::OsKeystore, SecretSource::OsKeystore { .. }) => true,
196 (SourceMatcher::Kubernetes, SecretSource::Kubernetes { .. }) => true,
197 (SourceMatcher::Hardware, SecretSource::Hardware { .. }) => true,
198 (SourceMatcher::Custom(name), SecretSource::Custom { provider, .. }) => {
199 name == provider
200 }
201 _ => false,
202 }
203 }
204}
205
206pub struct SecretRegistry {
215 resolvers: Vec<(SourceMatcher, Arc<dyn SecretResolver>)>,
216 event_sink: Option<Arc<dyn SecretEventSink>>,
217}
218
219impl SecretRegistry {
220 pub fn new() -> Self {
222 Self {
223 resolvers: Vec::new(),
224 event_sink: None,
225 }
226 }
227
228 pub fn with_resolver(
230 mut self,
231 matcher: SourceMatcher,
232 resolver: Arc<dyn SecretResolver>,
233 ) -> Self {
234 self.resolvers.push((matcher, resolver));
235 self
236 }
237
238 pub fn with_event_sink(mut self, sink: Arc<dyn SecretEventSink>) -> Self {
240 self.event_sink = Some(sink);
241 self
242 }
243
244 pub fn add(&mut self, matcher: SourceMatcher, resolver: Arc<dyn SecretResolver>) {
246 self.resolvers.push((matcher, resolver));
247 }
248}
249
250impl Default for SecretRegistry {
251 fn default() -> Self {
252 Self::new()
253 }
254}
255
256pub trait SecretEventSink: Send + Sync {
264 fn emit(&self, event: layer0::secret::SecretAccessEvent);
266}
267
268impl SecretRegistry {
269 pub async fn resolve_named(
279 &self,
280 credential_name: &str,
281 source: &SecretSource,
282 ) -> Result<SecretLease, SecretError> {
283 let result = self.resolve(source).await;
284 if let Some(sink) = &self.event_sink {
286 use layer0::secret::{SecretAccessEvent, SecretAccessOutcome};
287 let outcome = if result.is_ok() {
288 SecretAccessOutcome::Resolved
289 } else {
290 SecretAccessOutcome::Failed
291 };
292 let event = SecretAccessEvent::new(
293 credential_name,
294 source.clone(),
295 outcome,
296 std::time::SystemTime::now()
297 .duration_since(std::time::UNIX_EPOCH)
298 .unwrap_or_default()
299 .as_millis() as u64,
300 );
301 sink.emit(event);
302 }
303 result
304 }
305}
306
307#[async_trait]
308impl SecretResolver for SecretRegistry {
309 async fn resolve(&self, source: &SecretSource) -> Result<SecretLease, SecretError> {
312 for (matcher, resolver) in &self.resolvers {
313 if matcher.matches(source) {
314 return resolver.resolve(source).await;
315 }
316 }
317 Err(SecretError::NoResolver(source.kind().to_string()))
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn secret_value_debug_is_redacted() {
327 let secret = SecretValue::new(b"super-secret-key".to_vec());
328 let debug = format!("{:?}", secret);
329 assert_eq!(debug, "[REDACTED]");
330 assert!(!debug.contains("super-secret"));
331 }
332
333 #[test]
334 fn secret_value_with_bytes_exposes_content() {
335 let secret = SecretValue::new(b"my-api-key".to_vec());
336 secret.with_bytes(|bytes| {
337 assert_eq!(bytes, b"my-api-key");
338 });
339 }
340
341 #[test]
342 fn secret_value_len() {
343 let secret = SecretValue::new(b"12345".to_vec());
344 assert_eq!(secret.len(), 5);
345 assert!(!secret.is_empty());
346
347 let empty = SecretValue::new(vec![]);
348 assert_eq!(empty.len(), 0);
349 assert!(empty.is_empty());
350 }
351
352 #[test]
353 fn secret_lease_permanent_never_expires() {
354 let lease = SecretLease::permanent(SecretValue::new(b"key".to_vec()));
355 assert!(!lease.is_expired());
356 assert!(lease.expires_at.is_none());
357 assert!(!lease.renewable);
358 }
359
360 #[test]
361 fn secret_lease_debug_redacts_value() {
362 let lease = SecretLease::permanent(SecretValue::new(b"secret".to_vec()));
363 let debug = format!("{:?}", lease);
364 assert!(debug.contains("[REDACTED]"));
365 assert!(!debug.contains("secret"));
366 }
367
368 #[test]
369 fn source_matcher_vault() {
370 let matcher = SourceMatcher::Vault;
371 assert!(matcher.matches(&SecretSource::Vault {
372 mount: "secret".into(),
373 path: "data/key".into(),
374 }));
375 assert!(!matcher.matches(&SecretSource::OsKeystore {
376 service: "test".into(),
377 }));
378 }
379
380 #[test]
381 fn source_matcher_custom() {
382 let matcher = SourceMatcher::Custom("1password".into());
383 assert!(matcher.matches(&SecretSource::Custom {
384 provider: "1password".into(),
385 config: serde_json::json!({}),
386 }));
387 assert!(!matcher.matches(&SecretSource::Custom {
388 provider: "bitwarden".into(),
389 config: serde_json::json!({}),
390 }));
391 }
392
393 fn _assert_send_sync<T: Send + Sync>() {}
395
396 #[test]
397 fn secret_resolver_is_object_safe_send_sync() {
398 _assert_send_sync::<Box<dyn SecretResolver>>();
399 _assert_send_sync::<Arc<dyn SecretResolver>>();
400 }
401
402 #[tokio::test]
403 async fn registry_no_resolver_returns_error() {
404 let registry = SecretRegistry::new();
405 let source = SecretSource::Vault {
406 mount: "secret".into(),
407 path: "data/key".into(),
408 };
409 let result = registry.resolve(&source).await;
410 assert!(result.is_err());
411 assert!(matches!(result.unwrap_err(), SecretError::NoResolver(_)));
412 }
413
414 struct StubResolver {
416 value: &'static [u8],
417 }
418
419 #[async_trait]
420 impl SecretResolver for StubResolver {
421 async fn resolve(&self, _source: &SecretSource) -> Result<SecretLease, SecretError> {
422 Ok(SecretLease::permanent(SecretValue::new(
423 self.value.to_vec(),
424 )))
425 }
426 }
427
428 #[tokio::test]
429 async fn registry_dispatches_to_matching_resolver() {
430 let registry = SecretRegistry::new()
431 .with_resolver(
432 SourceMatcher::Vault,
433 Arc::new(StubResolver {
434 value: b"vault-secret",
435 }),
436 )
437 .with_resolver(
438 SourceMatcher::OsKeystore,
439 Arc::new(StubResolver {
440 value: b"keystore-secret",
441 }),
442 );
443
444 let vault_source = SecretSource::Vault {
445 mount: "secret".into(),
446 path: "data/key".into(),
447 };
448 let lease = registry.resolve(&vault_source).await.unwrap();
449 lease.value.with_bytes(|b| assert_eq!(b, b"vault-secret"));
450
451 let keystore_source = SecretSource::OsKeystore {
452 service: "test".into(),
453 };
454 let lease = registry.resolve(&keystore_source).await.unwrap();
455 lease
456 .value
457 .with_bytes(|b| assert_eq!(b, b"keystore-secret"));
458 }
459
460 #[test]
461 fn secret_error_display_all_variants() {
462 assert_eq!(
463 SecretError::NotFound("api-key".into()).to_string(),
464 "secret not found: api-key"
465 );
466 assert_eq!(
467 SecretError::AccessDenied("no policy".into()).to_string(),
468 "access denied: no policy"
469 );
470 assert_eq!(
471 SecretError::BackendError("timeout".into()).to_string(),
472 "backend error: timeout"
473 );
474 assert_eq!(
475 SecretError::LeaseExpired("lease-123".into()).to_string(),
476 "lease expired: lease-123"
477 );
478 assert_eq!(
479 SecretError::NoResolver("vault".into()).to_string(),
480 "no resolver for source: vault"
481 );
482 }
483}