Skip to main content

hyperi_rustlib/secrets/
provider.rs

1// Project:   hyperi-rustlib
2// File:      src/secrets/provider.rs
3// Purpose:   Secret provider trait and file provider implementation
4// Language:  Rust
5//
6// License:   FSL-1.1-ALv2
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9//! Secret provider trait and implementations.
10
11use std::future::Future;
12use std::path::Path;
13
14use super::error::{SecretsError, SecretsResult};
15use super::types::{SecretMetadata, SecretValue};
16
17/// Trait for secret providers.
18pub trait SecretProvider: Send + Sync {
19    /// Get a secret by path/key.
20    fn get(
21        &self,
22        path: &str,
23        key: Option<&str>,
24    ) -> impl Future<Output = SecretsResult<SecretValue>> + Send;
25
26    /// Check if the provider is healthy/reachable.
27    fn health_check(&self) -> impl Future<Output = SecretsResult<()>> + Send;
28
29    /// Provider name for logging.
30    fn name(&self) -> &'static str;
31}
32
33// ============================================================================
34// File Provider
35// ============================================================================
36
37/// Provider that loads secrets from local filesystem.
38///
39/// This provider is always available and requires no additional features.
40/// It reads secrets directly from files, making it compatible with:
41/// - Kubernetes secrets mounted as files
42/// - Docker secrets in `/run/secrets`
43/// - External Secrets Operator (ESO) synced files
44/// - Local development with file-based credentials
45#[derive(Debug, Default)]
46pub struct FileProvider;
47
48impl FileProvider {
49    /// Create a new file provider.
50    #[must_use]
51    pub fn new() -> Self {
52        Self
53    }
54
55    /// Get a secret from a file path.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if the file cannot be read.
60    pub async fn get(&self, path: &str) -> SecretsResult<SecretValue> {
61        let path = Path::new(path);
62
63        if !path.exists() {
64            return Err(SecretsError::NotFound(format!(
65                "file not found: {}",
66                path.display()
67            )));
68        }
69
70        let data = tokio::fs::read(path).await.map_err(|e| {
71            SecretsError::IoError(std::io::Error::new(
72                e.kind(),
73                format!("failed to read secret file {}: {e}", path.display()),
74            ))
75        })?;
76
77        let metadata = SecretMetadata {
78            version: None,
79            source_path: Some(path.display().to_string()),
80            provider: Some("file".into()),
81        };
82
83        Ok(SecretValue::with_metadata(data, metadata))
84    }
85}
86
87impl SecretProvider for FileProvider {
88    async fn get(&self, path: &str, _key: Option<&str>) -> SecretsResult<SecretValue> {
89        self.get(path).await
90    }
91
92    async fn health_check(&self) -> SecretsResult<()> {
93        // File provider is always healthy
94        Ok(())
95    }
96
97    fn name(&self) -> &'static str {
98        "file"
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[tokio::test]
107    async fn test_file_provider_missing_file() {
108        let provider = FileProvider::new();
109        let result = provider.get("/nonexistent/path/secret.txt").await;
110        assert!(result.is_err());
111        assert!(matches!(result, Err(SecretsError::NotFound(_))));
112    }
113
114    #[tokio::test]
115    async fn test_file_provider_read_file() {
116        let temp_dir = tempfile::tempdir().unwrap();
117        let secret_path = temp_dir.path().join("test-secret.txt");
118        std::fs::write(&secret_path, "my-secret-value").unwrap();
119
120        let provider = FileProvider::new();
121        let result = provider.get(secret_path.to_str().unwrap()).await;
122
123        assert!(result.is_ok());
124        let value = result.unwrap();
125        assert_eq!(value.as_str().unwrap(), "my-secret-value");
126        assert_eq!(value.metadata.provider.as_deref(), Some("file"));
127    }
128
129    #[tokio::test]
130    async fn test_file_provider_binary_content() {
131        let temp_dir = tempfile::tempdir().unwrap();
132        let secret_path = temp_dir.path().join("binary-secret");
133        let binary_data: Vec<u8> = vec![0x00, 0x01, 0x02, 0xFF, 0xFE];
134        std::fs::write(&secret_path, &binary_data).unwrap();
135
136        let provider = FileProvider::new();
137        let result = provider.get(secret_path.to_str().unwrap()).await;
138
139        assert!(result.is_ok());
140        let value = result.unwrap();
141        assert_eq!(value.as_bytes(), &binary_data);
142        // as_str should fail for binary data
143        assert!(value.as_str().is_err());
144    }
145
146    #[tokio::test]
147    async fn test_file_provider_health_check() {
148        let provider = FileProvider::new();
149        assert!(provider.health_check().await.is_ok());
150    }
151
152    #[test]
153    fn test_file_provider_name() {
154        let provider = FileProvider::new();
155        assert_eq!(provider.name(), "file");
156    }
157}