fraiseql_server/secrets_manager/backends/
file.rs1use std::path::PathBuf;
5
6use chrono::{Duration, Utc};
7
8use super::super::{SecretsBackend, SecretsError};
9
10#[derive(Clone, Debug)]
27pub struct FileBackend {
28 base_path: PathBuf,
29}
30
31#[async_trait::async_trait]
32impl SecretsBackend for FileBackend {
33 async fn get_secret(&self, name: &str) -> Result<String, SecretsError> {
34 let path = self.base_path.join(name);
35
36 let content = tokio::fs::read_to_string(&path).await.map_err(|e| {
37 SecretsError::BackendError(format!(
38 "Failed to read secret from {}: {}",
39 path.display(),
40 e
41 ))
42 })?;
43
44 Ok(content.trim().to_string())
45 }
46
47 async fn get_secret_with_expiry(
48 &self,
49 name: &str,
50 ) -> Result<(String, chrono::DateTime<Utc>), SecretsError> {
51 let secret = self.get_secret(name).await?;
52 let expiry = Utc::now() + Duration::days(365);
54 Ok((secret, expiry))
55 }
56
57 async fn rotate_secret(&self, name: &str) -> Result<String, SecretsError> {
58 Err(SecretsError::RotationError(format!(
60 "Rotation not supported for file-based secret {}",
61 name
62 )))
63 }
64}
65
66impl FileBackend {
67 pub fn new<P: Into<PathBuf>>(base_path: P) -> Self {
69 FileBackend {
70 base_path: base_path.into(),
71 }
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[tokio::test]
81 async fn test_file_backend_read_secret() {
82 use tempfile::tempdir;
83
84 let dir = tempdir().unwrap();
85 let secret_file = dir.path().join("test_secret");
86 tokio::fs::write(&secret_file, "secret_content_123").await.unwrap();
87
88 let backend = FileBackend::new(dir.path());
89 let secret = backend.get_secret("test_secret").await.unwrap();
90
91 assert_eq!(secret, "secret_content_123");
92 }
93
94 #[tokio::test]
96 async fn test_file_backend_not_found() {
97 use tempfile::tempdir;
98
99 let dir = tempdir().unwrap();
100 let backend = FileBackend::new(dir.path());
101 let result = backend.get_secret("nonexistent.txt").await;
102
103 assert!(result.is_err());
104 }
105
106 #[tokio::test]
108 async fn test_file_backend_trims_whitespace() {
109 use tempfile::tempdir;
110
111 let dir = tempdir().unwrap();
112 let secret_file = dir.path().join("whitespace_secret");
113 tokio::fs::write(&secret_file, " secret_value \n").await.unwrap();
114
115 let backend = FileBackend::new(dir.path());
116 let secret = backend.get_secret("whitespace_secret").await.unwrap();
117
118 assert_eq!(secret, "secret_value");
119 }
120
121 #[tokio::test]
123 async fn test_file_backend_with_expiry() {
124 use tempfile::tempdir;
125
126 let dir = tempdir().unwrap();
127 let secret_file = dir.path().join("expiry_test");
128 tokio::fs::write(&secret_file, "value").await.unwrap();
129
130 let backend = FileBackend::new(dir.path());
131 let (secret, expiry) = backend.get_secret_with_expiry("expiry_test").await.unwrap();
132
133 assert_eq!(secret, "value");
134 assert!(expiry > Utc::now());
135 }
136
137 #[tokio::test]
139 async fn test_file_backend_rotate_not_supported() {
140 use tempfile::tempdir;
141
142 let dir = tempdir().unwrap();
143 let backend = FileBackend::new(dir.path());
144 let result = backend.rotate_secret("any_file").await;
145
146 assert!(result.is_err());
147 }
148
149 #[tokio::test]
151 async fn test_file_backend_multiple_secrets() {
152 use tempfile::tempdir;
153
154 let dir = tempdir().unwrap();
155 tokio::fs::write(dir.path().join("secret1"), "value1").await.unwrap();
156 tokio::fs::write(dir.path().join("secret2"), "value2").await.unwrap();
157
158 let backend = FileBackend::new(dir.path());
159
160 let s1 = backend.get_secret("secret1").await.unwrap();
161 let s2 = backend.get_secret("secret2").await.unwrap();
162
163 assert_eq!(s1, "value1");
164 assert_eq!(s2, "value2");
165 }
166
167 #[tokio::test]
169 async fn test_file_backend_empty_file() {
170 use tempfile::tempdir;
171
172 let dir = tempdir().unwrap();
173 tokio::fs::write(dir.path().join("empty"), "").await.unwrap();
174
175 let backend = FileBackend::new(dir.path());
176 let secret = backend.get_secret("empty").await.unwrap();
177
178 assert_eq!(secret, "");
179 }
180}