1use argon2::{
33 password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
34 Argon2,
35};
36use serde::{Deserialize, Serialize};
37use tracing::{debug, info};
38
39use crate::{Result, Secret, SecretsError, SecretsStore};
40
41const CREDENTIALS_SCOPE: &str = "credentials";
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46struct StoredCredential {
47 hash: String,
49 roles: Vec<String>,
51}
52
53pub struct CredentialStore<S: SecretsStore> {
58 store: S,
59}
60
61impl<S: SecretsStore> std::fmt::Debug for CredentialStore<S> {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 f.debug_struct("CredentialStore")
64 .field("store", &"<secrets store>")
65 .finish()
66 }
67}
68
69impl<S: SecretsStore> CredentialStore<S> {
70 pub fn new(store: S) -> Self {
72 Self { store }
73 }
74
75 pub async fn validate(&self, api_key: &str, api_secret: &str) -> Result<Option<Vec<String>>> {
87 let secret = match self.store.get_secret(CREDENTIALS_SCOPE, api_key).await {
89 Ok(s) => s,
90 Err(SecretsError::NotFound { .. }) => {
91 debug!(api_key = %api_key, "Credential not found");
92 return Ok(None);
93 }
94 Err(e) => return Err(e),
95 };
96
97 let stored: StoredCredential = serde_json::from_str(secret.expose()).map_err(|e| {
99 SecretsError::Storage(format!("corrupt credential record for '{api_key}': {e}"))
100 })?;
101
102 let parsed_hash = PasswordHash::new(&stored.hash).map_err(|e| {
104 SecretsError::Storage(format!("invalid password hash for '{api_key}': {e}"))
105 })?;
106
107 let argon2 = Argon2::default();
108 if argon2
109 .verify_password(api_secret.as_bytes(), &parsed_hash)
110 .is_ok()
111 {
112 debug!(api_key = %api_key, "Credential validated successfully");
113 Ok(Some(stored.roles))
114 } else {
115 debug!(api_key = %api_key, "Invalid password");
116 Ok(None)
117 }
118 }
119
120 pub async fn create_api_key(
133 &self,
134 api_key: &str,
135 password: &str,
136 roles: &[&str],
137 ) -> Result<()> {
138 let salt = SaltString::generate(&mut OsRng);
140 let argon2 = Argon2::default();
141 let hash = argon2
142 .hash_password(password.as_bytes(), &salt)
143 .map_err(|e| SecretsError::Encryption(format!("failed to hash password: {e}")))?
144 .to_string();
145
146 let credential = StoredCredential {
147 hash,
148 roles: roles.iter().map(|r| (*r).to_string()).collect(),
149 };
150
151 let json = serde_json::to_string(&credential)
152 .map_err(|e| SecretsError::Storage(format!("failed to serialise credential: {e}")))?;
153
154 self.store
155 .set_secret(CREDENTIALS_SCOPE, api_key, &Secret::new(json))
156 .await?;
157
158 info!(api_key = %api_key, roles = ?roles, "Created API key credential");
159 Ok(())
160 }
161
162 pub async fn delete_api_key(&self, api_key: &str) -> Result<()> {
170 self.store.delete_secret(CREDENTIALS_SCOPE, api_key).await?;
171 info!(api_key = %api_key, "Deleted API key credential");
172 Ok(())
173 }
174
175 pub async fn exists(&self, api_key: &str) -> Result<bool> {
180 self.store.exists(CREDENTIALS_SCOPE, api_key).await
181 }
182
183 pub async fn set_roles(&self, api_key: &str, roles: &[&str]) -> Result<()> {
195 let secret = self.store.get_secret(CREDENTIALS_SCOPE, api_key).await?;
196 let mut stored: StoredCredential = serde_json::from_str(secret.expose()).map_err(|e| {
197 SecretsError::Storage(format!("corrupt credential record for '{api_key}': {e}"))
198 })?;
199
200 stored.roles = roles.iter().map(|r| (*r).to_string()).collect();
201
202 let json = serde_json::to_string(&stored)
203 .map_err(|e| SecretsError::Storage(format!("failed to serialise credential: {e}")))?;
204
205 self.store
206 .set_secret(CREDENTIALS_SCOPE, api_key, &Secret::new(json))
207 .await?;
208
209 info!(api_key = %api_key, roles = ?roles, "Updated credential roles");
210 Ok(())
211 }
212
213 pub async fn ensure_admin(&self, api_key: &str, password: &str) -> Result<bool> {
229 if self.exists(api_key).await? {
230 debug!(api_key = %api_key, "Admin credential already exists");
231 return Ok(false);
232 }
233
234 self.create_api_key(api_key, password, &["admin"]).await?;
235 Ok(true)
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::{EncryptionKey, PersistentSecretsStore};
243
244 async fn create_test_store() -> (PersistentSecretsStore, tempfile::TempDir) {
245 let temp_dir = tempfile::tempdir().unwrap();
246 let db_path = temp_dir.path().join("test_creds.sqlite");
247 let key = EncryptionKey::generate();
248 let store = PersistentSecretsStore::open(&db_path, key).await.unwrap();
249 (store, temp_dir)
250 }
251
252 #[tokio::test]
253 async fn test_create_and_validate() {
254 let (store, _temp) = create_test_store().await;
255 let cred_store = CredentialStore::new(store);
256
257 cred_store
258 .create_api_key("test-key", "test-secret", &["admin", "reader"])
259 .await
260 .unwrap();
261
262 let roles = cred_store
264 .validate("test-key", "test-secret")
265 .await
266 .unwrap();
267 assert!(roles.is_some());
268 let roles = roles.unwrap();
269 assert!(roles.contains(&"admin".to_string()));
270 assert!(roles.contains(&"reader".to_string()));
271 }
272
273 #[tokio::test]
274 async fn test_validate_wrong_password() {
275 let (store, _temp) = create_test_store().await;
276 let cred_store = CredentialStore::new(store);
277
278 cred_store
279 .create_api_key("test-key", "correct-password", &["admin"])
280 .await
281 .unwrap();
282
283 let roles = cred_store
284 .validate("test-key", "wrong-password")
285 .await
286 .unwrap();
287 assert!(roles.is_none());
288 }
289
290 #[tokio::test]
291 async fn test_validate_nonexistent_key() {
292 let (store, _temp) = create_test_store().await;
293 let cred_store = CredentialStore::new(store);
294
295 let roles = cred_store
296 .validate("nonexistent", "password")
297 .await
298 .unwrap();
299 assert!(roles.is_none());
300 }
301
302 #[tokio::test]
303 async fn test_exists() {
304 let (store, _temp) = create_test_store().await;
305 let cred_store = CredentialStore::new(store);
306
307 assert!(!cred_store.exists("test-key").await.unwrap());
308
309 cred_store
310 .create_api_key("test-key", "password", &["admin"])
311 .await
312 .unwrap();
313
314 assert!(cred_store.exists("test-key").await.unwrap());
315 }
316
317 #[tokio::test]
318 async fn test_delete_api_key() {
319 let (store, _temp) = create_test_store().await;
320 let cred_store = CredentialStore::new(store);
321
322 cred_store
323 .create_api_key("delete-me", "password", &["admin"])
324 .await
325 .unwrap();
326 assert!(cred_store.exists("delete-me").await.unwrap());
327
328 cred_store.delete_api_key("delete-me").await.unwrap();
329 assert!(!cred_store.exists("delete-me").await.unwrap());
330 }
331
332 #[tokio::test]
333 async fn test_set_roles_preserves_hash() {
334 let (store, _temp) = create_test_store().await;
335 let cred_store = CredentialStore::new(store);
336
337 cred_store
338 .create_api_key("alice@example.com", "hunter2hunter2", &["user"])
339 .await
340 .unwrap();
341
342 cred_store
343 .set_roles("alice@example.com", &["admin"])
344 .await
345 .unwrap();
346
347 let roles = cred_store
349 .validate("alice@example.com", "hunter2hunter2")
350 .await
351 .unwrap()
352 .expect("should validate");
353 assert_eq!(roles, vec!["admin".to_string()]);
354 }
355
356 #[tokio::test]
357 async fn test_set_roles_missing_errors() {
358 let (store, _temp) = create_test_store().await;
359 let cred_store = CredentialStore::new(store);
360
361 let err = cred_store
362 .set_roles("nonexistent", &["admin"])
363 .await
364 .unwrap_err();
365 assert!(
366 matches!(err, SecretsError::NotFound { .. }),
367 "unexpected: {err}"
368 );
369 }
370
371 #[tokio::test]
372 async fn test_ensure_admin_creates() {
373 let (store, _temp) = create_test_store().await;
374 let cred_store = CredentialStore::new(store);
375
376 let created = cred_store
377 .ensure_admin("admin", "admin-password")
378 .await
379 .unwrap();
380 assert!(created);
381
382 let roles = cred_store
384 .validate("admin", "admin-password")
385 .await
386 .unwrap();
387 assert!(roles.is_some());
388 assert!(roles.unwrap().contains(&"admin".to_string()));
389 }
390
391 #[tokio::test]
392 async fn test_ensure_admin_skips_existing() {
393 let (store, _temp) = create_test_store().await;
394 let cred_store = CredentialStore::new(store);
395
396 cred_store
398 .create_api_key("admin", "original-password", &["admin"])
399 .await
400 .unwrap();
401
402 let created = cred_store
404 .ensure_admin("admin", "new-password")
405 .await
406 .unwrap();
407 assert!(!created);
408
409 let roles = cred_store
411 .validate("admin", "original-password")
412 .await
413 .unwrap();
414 assert!(roles.is_some());
415
416 let roles = cred_store.validate("admin", "new-password").await.unwrap();
418 assert!(roles.is_none());
419 }
420
421 #[tokio::test]
422 async fn test_overwrite_credential() {
423 let (store, _temp) = create_test_store().await;
424 let cred_store = CredentialStore::new(store);
425
426 cred_store
427 .create_api_key("key", "password1", &["reader"])
428 .await
429 .unwrap();
430
431 cred_store
433 .create_api_key("key", "password2", &["admin"])
434 .await
435 .unwrap();
436
437 let roles = cred_store.validate("key", "password1").await.unwrap();
439 assert!(roles.is_none());
440
441 let roles = cred_store.validate("key", "password2").await.unwrap();
443 assert!(roles.is_some());
444 let roles = roles.unwrap();
445 assert!(roles.contains(&"admin".to_string()));
446 assert!(!roles.contains(&"reader".to_string()));
447 }
448}