fraiseql_secrets/secrets_manager/
mod.rs1use std::{fmt, path::PathBuf, sync::Arc, time::Duration};
9
10use chrono::{DateTime, Utc};
11use tracing::{info, warn};
12
13pub mod backends;
14pub mod types;
15
16pub use backends::{EnvBackend, FileBackend, VaultBackend};
17pub use types::{Secret, SecretsBackend};
18
19#[derive(Debug, Clone)]
21pub enum SecretsBackendConfig {
22 File {
24 path: PathBuf,
26 },
27 Env,
29 Vault {
31 addr: String,
33 auth: VaultAuth,
35 namespace: Option<String>,
37 tls_verify: bool,
39 },
40}
41
42#[derive(Debug, Clone)]
44pub enum VaultAuth {
45 Token(String),
47 AppRole {
49 role_id: String,
51 secret_id: String,
53 },
54}
55
56pub async fn create_secrets_manager(
63 config: SecretsBackendConfig,
64) -> Result<Arc<SecretsManager>, SecretsError> {
65 let backend: Arc<dyn SecretsBackend> = match config {
66 SecretsBackendConfig::File { path } => {
67 info!(path = %path.display(), "Initializing file secrets backend");
68 Arc::new(FileBackend::new(path))
69 },
70 SecretsBackendConfig::Env => {
71 info!("Initializing environment variable secrets backend");
72 Arc::new(EnvBackend::new())
73 },
74 SecretsBackendConfig::Vault {
75 addr,
76 auth,
77 namespace,
78 tls_verify,
79 } => {
80 info!(addr = %addr, "Initializing Vault secrets backend");
81 let mut vault = match auth {
82 VaultAuth::Token(token) => VaultBackend::new(&addr, &token),
83 VaultAuth::AppRole { role_id, secret_id } => {
84 VaultBackend::with_approle(&addr, &role_id, &secret_id).await?
85 },
86 };
87 if let Some(ns) = namespace {
88 vault = vault.with_namespace(ns);
89 }
90 vault = vault.with_tls_verify(tls_verify);
91 Arc::new(vault)
92 },
93 };
94 Ok(Arc::new(SecretsManager::new(backend)))
95}
96
97pub struct SecretsManager {
99 backend: Arc<dyn SecretsBackend>,
100}
101
102impl SecretsManager {
103 pub fn new(backend: Arc<dyn SecretsBackend>) -> Self {
105 SecretsManager { backend }
106 }
107
108 pub async fn get_secret(&self, name: &str) -> Result<String, SecretsError> {
110 self.backend.get_secret(name).await
111 }
112
113 pub async fn get_secret_with_expiry(
118 &self,
119 name: &str,
120 ) -> Result<(String, DateTime<Utc>), SecretsError> {
121 self.backend.get_secret_with_expiry(name).await
122 }
123
124 pub async fn rotate_secret(&self, name: &str) -> Result<String, SecretsError> {
128 self.backend.rotate_secret(name).await
129 }
130}
131
132pub struct LeaseRenewalTask {
137 manager: Arc<SecretsManager>,
138 check_interval: Duration,
139 cancel_rx: tokio::sync::watch::Receiver<bool>,
140 tracked_keys: Vec<String>,
141}
142
143impl LeaseRenewalTask {
144 pub fn new(
148 manager: Arc<SecretsManager>,
149 tracked_keys: Vec<String>,
150 check_interval: Duration,
151 ) -> (Self, tokio::sync::watch::Sender<bool>) {
152 let (cancel_tx, cancel_rx) = tokio::sync::watch::channel(false);
153 (
154 Self {
155 manager,
156 check_interval,
157 cancel_rx,
158 tracked_keys,
159 },
160 cancel_tx,
161 )
162 }
163
164 pub async fn run(mut self) {
168 info!(
169 interval_secs = self.check_interval.as_secs(),
170 keys = self.tracked_keys.len(),
171 "Lease renewal task started"
172 );
173 loop {
174 tokio::select! {
175 result = self.cancel_rx.changed() => {
176 if result.is_err() || *self.cancel_rx.borrow() {
177 info!("Lease renewal task stopped");
178 break;
179 }
180 },
181 () = tokio::time::sleep(self.check_interval) => {
182 self.renew_expiring_leases().await;
183 }
184 }
185 }
186 }
187
188 async fn renew_expiring_leases(&self) {
189 for key in &self.tracked_keys {
190 match self.manager.get_secret_with_expiry(key).await {
191 Ok((_, expiry)) => {
192 let remaining = expiry - Utc::now();
193 if remaining
195 < chrono::Duration::seconds(
196 (self.check_interval.as_secs() as f64 * 0.2) as i64,
197 )
198 {
199 match self.manager.rotate_secret(key).await {
200 Ok(_) => info!(key = %key, "Lease renewed"),
201 Err(e) => warn!(key = %key, error = %e, "Lease renewal failed"),
202 }
203 }
204 },
205 Err(e) => {
206 warn!(key = %key, error = %e, "Failed to check lease expiry");
207 },
208 }
209 }
210 }
211}
212
213#[derive(Debug, Clone)]
215pub enum SecretsError {
216 NotFound(String),
218 BackendError(String),
220 ValidationError(String),
222 EncryptionError(String),
224 RotationError(String),
226 ConnectionError(String),
228 ExpiredCredential,
230}
231
232impl fmt::Display for SecretsError {
233 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
234 match self {
235 SecretsError::NotFound(msg) => write!(f, "Secret not found: {msg}"),
236 SecretsError::BackendError(msg) => write!(f, "Backend error: {msg}"),
237 SecretsError::ValidationError(msg) => write!(f, "Validation error: {msg}"),
238 SecretsError::EncryptionError(msg) => write!(f, "Encryption error: {msg}"),
239 SecretsError::RotationError(msg) => write!(f, "Rotation error: {msg}"),
240 SecretsError::ConnectionError(msg) => write!(f, "Connection error: {msg}"),
241 SecretsError::ExpiredCredential => write!(f, "Credential expired"),
242 }
243 }
244}
245
246impl std::error::Error for SecretsError {}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[tokio::test]
253 async fn test_create_secrets_manager_file_backend() {
254 let dir = tempfile::tempdir().unwrap();
255 let secret_path = dir.path().join("db_password");
256 tokio::fs::write(&secret_path, "s3cret").await.unwrap();
257
258 let manager = create_secrets_manager(SecretsBackendConfig::File {
259 path: dir.path().to_path_buf(),
260 })
261 .await
262 .unwrap();
263
264 let value = manager.get_secret("db_password").await.unwrap();
265 assert_eq!(value, "s3cret");
266 }
267
268 #[tokio::test]
269 async fn test_create_secrets_manager_env_backend() {
270 let key = "FRAISEQL_TEST_SM_SECRET_FACTORY";
272 temp_env::async_with_vars([(key, Some("env_value"))], async {
273 let manager = create_secrets_manager(SecretsBackendConfig::Env).await.unwrap();
274 let value = manager.get_secret(key).await.unwrap();
275 assert_eq!(value, "env_value");
276 })
277 .await;
278 }
279}