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 #[must_use]
83 pub fn store(&self) -> &S {
84 &self.store
85 }
86
87 pub async fn validate(&self, api_key: &str, api_secret: &str) -> Result<Option<Vec<String>>> {
99 let secret = match self.store.get_secret(CREDENTIALS_SCOPE, api_key).await {
101 Ok(s) => s,
102 Err(SecretsError::NotFound { .. }) => {
103 debug!(api_key = %api_key, "Credential not found");
104 return Ok(None);
105 }
106 Err(e) => return Err(e),
107 };
108
109 let stored: StoredCredential = serde_json::from_str(secret.expose()).map_err(|e| {
111 SecretsError::Storage(format!("corrupt credential record for '{api_key}': {e}"))
112 })?;
113
114 let parsed_hash = PasswordHash::new(&stored.hash).map_err(|e| {
116 SecretsError::Storage(format!("invalid password hash for '{api_key}': {e}"))
117 })?;
118
119 let argon2 = Argon2::default();
120 if argon2
121 .verify_password(api_secret.as_bytes(), &parsed_hash)
122 .is_ok()
123 {
124 debug!(api_key = %api_key, "Credential validated successfully");
125 Ok(Some(stored.roles))
126 } else {
127 debug!(api_key = %api_key, "Invalid password");
128 Ok(None)
129 }
130 }
131
132 pub async fn create_api_key(
145 &self,
146 api_key: &str,
147 password: &str,
148 roles: &[&str],
149 ) -> Result<()> {
150 let salt = SaltString::generate(&mut OsRng);
152 let argon2 = Argon2::default();
153 let hash = argon2
154 .hash_password(password.as_bytes(), &salt)
155 .map_err(|e| SecretsError::Encryption(format!("failed to hash password: {e}")))?
156 .to_string();
157
158 let credential = StoredCredential {
159 hash,
160 roles: roles.iter().map(|r| (*r).to_string()).collect(),
161 };
162
163 let json = serde_json::to_string(&credential)
164 .map_err(|e| SecretsError::Storage(format!("failed to serialise credential: {e}")))?;
165
166 self.store
167 .set_secret(CREDENTIALS_SCOPE, api_key, &Secret::new(json))
168 .await?;
169
170 info!(api_key = %api_key, roles = ?roles, "Created API key credential");
171 Ok(())
172 }
173
174 pub async fn delete_api_key(&self, api_key: &str) -> Result<()> {
182 self.store.delete_secret(CREDENTIALS_SCOPE, api_key).await?;
183 info!(api_key = %api_key, "Deleted API key credential");
184 Ok(())
185 }
186
187 pub async fn exists(&self, api_key: &str) -> Result<bool> {
192 self.store.exists(CREDENTIALS_SCOPE, api_key).await
193 }
194
195 pub async fn set_roles(&self, api_key: &str, roles: &[&str]) -> Result<()> {
207 let secret = self.store.get_secret(CREDENTIALS_SCOPE, api_key).await?;
208 let mut stored: StoredCredential = serde_json::from_str(secret.expose()).map_err(|e| {
209 SecretsError::Storage(format!("corrupt credential record for '{api_key}': {e}"))
210 })?;
211
212 stored.roles = roles.iter().map(|r| (*r).to_string()).collect();
213
214 let json = serde_json::to_string(&stored)
215 .map_err(|e| SecretsError::Storage(format!("failed to serialise credential: {e}")))?;
216
217 self.store
218 .set_secret(CREDENTIALS_SCOPE, api_key, &Secret::new(json))
219 .await?;
220
221 info!(api_key = %api_key, roles = ?roles, "Updated credential roles");
222 Ok(())
223 }
224
225 pub async fn ensure_admin(&self, api_key: &str, password: &str) -> Result<bool> {
241 if self.exists(api_key).await? {
242 debug!(api_key = %api_key, "Admin credential already exists");
243 return Ok(false);
244 }
245
246 self.create_api_key(api_key, password, &["admin"]).await?;
247 Ok(true)
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use crate::{EncryptionKey, PersistentSecretsStore};
255 use zlayer_paths::ZLayerDirs;
256
257 async fn create_test_store() -> (PersistentSecretsStore, zlayer_types::Scratch) {
258 let temp_dir = ZLayerDirs::system_default()
259 .scratch_dir("create-test-store-")
260 .unwrap();
261 let db_path = temp_dir.path().join("test_creds.sqlite");
262 let key = EncryptionKey::generate();
263 let store = PersistentSecretsStore::open(&db_path, key).await.unwrap();
264 (store, temp_dir)
265 }
266
267 #[tokio::test]
268 async fn test_create_and_validate() {
269 let (store, _temp) = create_test_store().await;
270 let cred_store = CredentialStore::new(store);
271
272 cred_store
273 .create_api_key("test-key", "test-secret", &["admin", "reader"])
274 .await
275 .unwrap();
276
277 let roles = cred_store
279 .validate("test-key", "test-secret")
280 .await
281 .unwrap();
282 assert!(roles.is_some());
283 let roles = roles.unwrap();
284 assert!(roles.contains(&"admin".to_string()));
285 assert!(roles.contains(&"reader".to_string()));
286 }
287
288 #[tokio::test]
289 async fn test_validate_wrong_password() {
290 let (store, _temp) = create_test_store().await;
291 let cred_store = CredentialStore::new(store);
292
293 cred_store
294 .create_api_key("test-key", "correct-password", &["admin"])
295 .await
296 .unwrap();
297
298 let roles = cred_store
299 .validate("test-key", "wrong-password")
300 .await
301 .unwrap();
302 assert!(roles.is_none());
303 }
304
305 #[tokio::test]
306 async fn test_validate_nonexistent_key() {
307 let (store, _temp) = create_test_store().await;
308 let cred_store = CredentialStore::new(store);
309
310 let roles = cred_store
311 .validate("nonexistent", "password")
312 .await
313 .unwrap();
314 assert!(roles.is_none());
315 }
316
317 #[tokio::test]
318 async fn test_exists() {
319 let (store, _temp) = create_test_store().await;
320 let cred_store = CredentialStore::new(store);
321
322 assert!(!cred_store.exists("test-key").await.unwrap());
323
324 cred_store
325 .create_api_key("test-key", "password", &["admin"])
326 .await
327 .unwrap();
328
329 assert!(cred_store.exists("test-key").await.unwrap());
330 }
331
332 #[tokio::test]
333 async fn test_delete_api_key() {
334 let (store, _temp) = create_test_store().await;
335 let cred_store = CredentialStore::new(store);
336
337 cred_store
338 .create_api_key("delete-me", "password", &["admin"])
339 .await
340 .unwrap();
341 assert!(cred_store.exists("delete-me").await.unwrap());
342
343 cred_store.delete_api_key("delete-me").await.unwrap();
344 assert!(!cred_store.exists("delete-me").await.unwrap());
345 }
346
347 #[tokio::test]
348 async fn test_set_roles_preserves_hash() {
349 let (store, _temp) = create_test_store().await;
350 let cred_store = CredentialStore::new(store);
351
352 cred_store
353 .create_api_key("alice@example.com", "hunter2hunter2", &["user"])
354 .await
355 .unwrap();
356
357 cred_store
358 .set_roles("alice@example.com", &["admin"])
359 .await
360 .unwrap();
361
362 let roles = cred_store
364 .validate("alice@example.com", "hunter2hunter2")
365 .await
366 .unwrap()
367 .expect("should validate");
368 assert_eq!(roles, vec!["admin".to_string()]);
369 }
370
371 #[tokio::test]
372 async fn test_set_roles_missing_errors() {
373 let (store, _temp) = create_test_store().await;
374 let cred_store = CredentialStore::new(store);
375
376 let err = cred_store
377 .set_roles("nonexistent", &["admin"])
378 .await
379 .unwrap_err();
380 assert!(
381 matches!(err, SecretsError::NotFound { .. }),
382 "unexpected: {err}"
383 );
384 }
385
386 #[tokio::test]
387 async fn test_ensure_admin_creates() {
388 let (store, _temp) = create_test_store().await;
389 let cred_store = CredentialStore::new(store);
390
391 let created = cred_store
392 .ensure_admin("admin", "admin-password")
393 .await
394 .unwrap();
395 assert!(created);
396
397 let roles = cred_store
399 .validate("admin", "admin-password")
400 .await
401 .unwrap();
402 assert!(roles.is_some());
403 assert!(roles.unwrap().contains(&"admin".to_string()));
404 }
405
406 #[tokio::test]
407 async fn test_ensure_admin_skips_existing() {
408 let (store, _temp) = create_test_store().await;
409 let cred_store = CredentialStore::new(store);
410
411 cred_store
413 .create_api_key("admin", "original-password", &["admin"])
414 .await
415 .unwrap();
416
417 let created = cred_store
419 .ensure_admin("admin", "new-password")
420 .await
421 .unwrap();
422 assert!(!created);
423
424 let roles = cred_store
426 .validate("admin", "original-password")
427 .await
428 .unwrap();
429 assert!(roles.is_some());
430
431 let roles = cred_store.validate("admin", "new-password").await.unwrap();
433 assert!(roles.is_none());
434 }
435
436 #[tokio::test]
437 async fn test_overwrite_credential() {
438 let (store, _temp) = create_test_store().await;
439 let cred_store = CredentialStore::new(store);
440
441 cred_store
442 .create_api_key("key", "password1", &["reader"])
443 .await
444 .unwrap();
445
446 cred_store
448 .create_api_key("key", "password2", &["admin"])
449 .await
450 .unwrap();
451
452 let roles = cred_store.validate("key", "password1").await.unwrap();
454 assert!(roles.is_none());
455
456 let roles = cred_store.validate("key", "password2").await.unwrap();
458 assert!(roles.is_some());
459 let roles = roles.unwrap();
460 assert!(roles.contains(&"admin".to_string()));
461 assert!(!roles.contains(&"reader".to_string()));
462 }
463}