axum_gate/repositories/memory.rs
1//! In-memory storage implementations for development and testing.
2//!
3//! This module provides repository implementations that store all data in memory.
4//! These are ideal for development, testing, and small applications that don't
5//! require persistent storage.
6//!
7//! # Features
8//! - Zero configuration required
9//! - Fast operations (no I/O)
10//! - Perfect for unit tests and development
11//! - Thread-safe with async support
12//! - Automatic cleanup when dropped
13//!
14//! # Quick Start
15//!
16//! ```rust
17//! use axum_gate::accounts::Account;
18//! use axum_gate::prelude::{Role, Group};
19//! use axum_gate::hashing::argon2::Argon2Hasher;
20//! use axum_gate::repositories::memory::{MemoryAccountRepository, MemorySecretRepository, MemoryPermissionMappingRepository};
21//! use axum_gate::secrets::Secret;
22//! use axum_gate::accounts::AccountRepository;
23//! use axum_gate::secrets::SecretRepository;
24//! use std::sync::Arc;
25//!
26//! # tokio_test::block_on(async {
27//! // Create repositories
28//! let account_repo = Arc::new(MemoryAccountRepository::<Role, Group>::default());
29//! let secret_repo = Arc::new(MemorySecretRepository::new_with_argon2_hasher().unwrap());
30//! let mapping_repo = Arc::new(MemoryPermissionMappingRepository::default());
31//!
32//! // Create an account
33//! let account = Account::new("user@example.com", &[Role::User], &[Group::new("staff")]);
34//! let stored_account = account_repo.store_account(account).await.unwrap().unwrap();
35//!
36//! // Create corresponding secret
37//! let secret = Secret::new(&stored_account.account_id, "password", Argon2Hasher::new_recommended().unwrap()).unwrap();
38//! secret_repo.store_secret(secret).await.unwrap();
39//!
40//! // Query the account
41//! let found = account_repo.query_account_by_user_id("user@example.com").await.unwrap();
42//! assert!(found.is_some());
43//! # });
44//! ```
45//!
46//! # Creating from Existing Data
47//!
48//! ```rust
49//! use axum_gate::accounts::Account;
50//! use axum_gate::prelude::{Role, Group};
51//! use axum_gate::secrets::Secret;
52//! use axum_gate::repositories::memory::{MemoryAccountRepository, MemorySecretRepository};
53//!
54//! // Create repositories with pre-populated data
55//! let accounts = vec![
56//! Account::new("admin@example.com", &[Role::Admin], &[]),
57//! Account::new("user@example.com", &[Role::User], &[Group::new("staff")]),
58//! ];
59//! let account_repo = MemoryAccountRepository::from(accounts);
60//!
61//! let secrets = vec![/* your secrets */];
62//! let secret_repo = MemorySecretRepository::try_from(secrets).unwrap();
63//! ```
64use crate::accounts::Account;
65use crate::accounts::AccountRepository;
66use crate::authz::AccessHierarchy;
67use crate::credentials::Credentials;
68use crate::credentials::CredentialsVerifier;
69use crate::errors::{Error, Result};
70use crate::hashing::HashingService;
71use crate::hashing::argon2::Argon2Hasher;
72use crate::permissions::PermissionId;
73use crate::permissions::mapping::PermissionMapping;
74use crate::permissions::mapping::PermissionMappingRepository;
75use crate::repositories::{RepositoriesError, RepositoryOperation, RepositoryType};
76use crate::secrets::Secret;
77use crate::secrets::SecretRepository;
78use crate::verification_result::VerificationResult;
79
80use std::collections::HashMap;
81use std::sync::Arc;
82
83use tokio::sync::RwLock;
84use tracing::debug;
85use uuid::Uuid;
86
87/// In-memory repository for storing and retrieving user accounts.
88///
89/// This repository stores all account data in memory using a HashMap with the user ID
90/// as the key. It's thread-safe and supports concurrent access through async read/write locks.
91///
92/// # Performance Characteristics
93/// - O(1) lookup by user ID
94/// - Thread-safe with RwLock
95/// - No persistence (data lost when dropped)
96/// - Suitable for up to thousands of accounts
97///
98/// # Example
99/// ```rust
100/// use axum_gate::accounts::Account;
101/// use axum_gate::prelude::{Role, Group};
102/// use axum_gate::accounts::AccountRepository;
103/// use axum_gate::repositories::memory::MemoryAccountRepository;
104/// use std::sync::Arc;
105///
106/// # tokio_test::block_on(async {
107/// let repo = Arc::new(MemoryAccountRepository::<Role, Group>::default());
108///
109/// // Store an account
110/// let account = Account::new("user@example.com", &[Role::User], &[]);
111/// let stored = repo.store_account(account).await.unwrap();
112///
113/// // Query the account
114/// let found = repo.query_account_by_user_id("user@example.com").await.unwrap();
115/// assert!(found.is_some());
116/// # });
117/// ```
118#[derive(Clone)]
119pub struct MemoryAccountRepository<R, G>
120where
121 R: AccessHierarchy + Eq + Send + Sync + 'static,
122 G: Eq + Clone + Send + Sync + 'static,
123{
124 accounts: Arc<RwLock<HashMap<String, Account<R, G>>>>,
125}
126
127impl<R, G> Default for MemoryAccountRepository<R, G>
128where
129 R: AccessHierarchy + Eq + Send + Sync + 'static,
130 G: Eq + Clone + Send + Sync + 'static,
131{
132 fn default() -> Self {
133 Self {
134 accounts: Arc::new(RwLock::new(HashMap::new())),
135 }
136 }
137}
138
139impl<R, G> From<Vec<Account<R, G>>> for MemoryAccountRepository<R, G>
140where
141 R: AccessHierarchy + Eq + Send + Sync + 'static,
142 G: Eq + Clone + Send + Sync + 'static,
143{
144 fn from(value: Vec<Account<R, G>>) -> Self {
145 let mut accounts = HashMap::new();
146 for val in value {
147 let id = val.user_id.clone();
148 accounts.insert(id, val);
149 }
150 let accounts = Arc::new(RwLock::new(accounts));
151 Self { accounts }
152 }
153}
154
155impl<R, G> AccountRepository<R, G> for MemoryAccountRepository<R, G>
156where
157 Account<R, G>: Clone,
158 R: AccessHierarchy + Eq + Send + Sync + 'static,
159 G: Eq + Clone + Send + Sync + 'static,
160{
161 async fn query_account_by_user_id(&self, user_id: &str) -> Result<Option<Account<R, G>>> {
162 let read = self.accounts.read().await;
163 Ok(read.get(user_id).cloned())
164 }
165 async fn store_account(&self, account: Account<R, G>) -> Result<Option<Account<R, G>>> {
166 let id = account.user_id.clone();
167 let mut write = self.accounts.write().await;
168 write.insert(id, account.clone());
169 Ok(Some(account))
170 }
171 async fn delete_account(&self, account_id: &str) -> Result<Option<Account<R, G>>> {
172 let mut write = self.accounts.write().await;
173 if !write.contains_key(account_id) {
174 return Ok(None);
175 }
176 Ok(write.remove(account_id))
177 }
178 async fn update_account(&self, account: Account<R, G>) -> Result<Option<Account<R, G>>> {
179 self.store_account(account).await
180 }
181}
182/// In-memory repository for storing and managing user authentication secrets.
183///
184/// This repository stores password hashes and other authentication secrets in memory.
185/// It's designed to work alongside `MemoryAccountRepository` and implements both
186/// `SecretRepository` and `CredentialsVerifier` traits for complete authentication support.
187///
188/// # Security Note
189/// While this stores password hashes (not plain passwords), the data is kept in memory
190/// and will be lost when the application stops. For production use, consider persistent
191/// storage implementations.
192///
193/// # Example Usage
194/// ```rust
195/// use axum_gate::prelude::Credentials;
196/// use axum_gate::secrets::Secret; use axum_gate::verification_result::VerificationResult; use axum_gate::hashing::argon2::Argon2Hasher; use axum_gate::secrets::SecretRepository; use axum_gate::credentials::CredentialsVerifier;
197/// use axum_gate::repositories::memory::MemorySecretRepository;
198/// use uuid::Uuid;
199///
200/// # tokio_test::block_on(async {
201/// let repo = MemorySecretRepository::new_with_argon2_hasher().unwrap();
202/// let account_id = Uuid::now_v7();
203///
204/// // Store a secret (password hash)
205/// let secret = Secret::new(&account_id, "user_password", Argon2Hasher::new_recommended().unwrap()).unwrap();
206/// repo.store_secret(secret).await.unwrap();
207///
208/// // Verify credentials
209/// let credentials = Credentials::new(&account_id, "user_password");
210/// let result = repo.verify_credentials(credentials).await.unwrap();
211/// assert_eq!(result, VerificationResult::Ok);
212///
213/// // Test wrong password
214/// let wrong_creds = Credentials::new(&account_id, "wrong_password");
215/// let result = repo.verify_credentials(wrong_creds).await.unwrap();
216/// assert_eq!(result, VerificationResult::Unauthorized);
217/// # });
218/// ```
219///
220/// # Creating from Existing Data
221/// ```rust
222/// use axum_gate::secrets::Secret; use axum_gate::hashing::argon2::Argon2Hasher;
223/// use axum_gate::repositories::memory::MemorySecretRepository;
224/// use uuid::Uuid;
225///
226/// let secrets = vec![
227/// Secret::new(&Uuid::now_v7(), "admin_pass", Argon2Hasher::new_recommended().unwrap()).unwrap(),
228/// Secret::new(&Uuid::now_v7(), "user_pass", Argon2Hasher::new_recommended().unwrap()).unwrap(),
229/// ];
230/// let repo = MemorySecretRepository::try_from(secrets).unwrap();
231/// ```
232#[derive(Clone)]
233pub struct MemorySecretRepository {
234 store: Arc<RwLock<HashMap<Uuid, Secret>>>,
235 /// Precomputed dummy hash produced with the same Argon2 preset that `Secret::new`
236 /// used (via `Argon2Hasher::new_recommended()`) in this build configuration. This keeps
237 /// timing of nonexistent-account verifications aligned with existing-account
238 /// verifications to mitigate user enumeration via timing side channels.
239 dummy_hash: String,
240}
241
242impl MemorySecretRepository {
243 /// Creates a new instance with [Argon2Hasher].
244 pub fn new_with_argon2_hasher() -> Result<Self> {
245 let hasher = Argon2Hasher::new_recommended()?;
246 let dummy_hash = hasher.hash_value("dummy_password")?;
247 Ok(Self {
248 store: Arc::new(RwLock::new(HashMap::new())),
249 dummy_hash,
250 })
251 }
252}
253
254impl TryFrom<Vec<Secret>> for MemorySecretRepository {
255 type Error = crate::errors::Error;
256 fn try_from(value: Vec<Secret>) -> Result<Self> {
257 let mut store = HashMap::with_capacity(value.len());
258 value.into_iter().for_each(|v| {
259 store.insert(v.account_id, v);
260 });
261 let store = Arc::new(RwLock::new(store));
262 let dummy_hash = Argon2Hasher::new_recommended()?.hash_value("dummy_password")?;
263 Ok(Self { store, dummy_hash })
264 }
265}
266
267impl SecretRepository for MemorySecretRepository {
268 async fn store_secret(&self, secret: Secret) -> Result<bool> {
269 let already_present = {
270 let read = self.store.read().await;
271 read.contains_key(&secret.account_id)
272 };
273
274 if already_present {
275 return Err(Error::Repositories(RepositoriesError::operation_failed(
276 RepositoryType::Secret,
277 RepositoryOperation::Insert,
278 "AccountID is already present",
279 None,
280 None,
281 )));
282 }
283
284 let mut write = self.store.write().await;
285 debug!("Got write lock on secret repository.");
286
287 if write.insert(secret.account_id, secret).is_some() {
288 return Err(Error::Repositories(RepositoriesError::operation_failed(
289 RepositoryType::Secret,
290 RepositoryOperation::Insert,
291 "This should never occur because it is checked if the key is already present a few lines earlier",
292 None,
293 Some("store".to_string()),
294 )));
295 };
296 Ok(true)
297 }
298
299 async fn delete_secret(&self, id: &Uuid) -> Result<Option<Secret>> {
300 // Atomically remove and return the secret (compensating actions can reinsert it)
301 let mut write = self.store.write().await;
302 Ok(write.remove(id))
303 }
304
305 async fn update_secret(&self, secret: Secret) -> Result<()> {
306 let mut write = self.store.write().await;
307 write.insert(secret.account_id, secret);
308 Ok(())
309 }
310}
311
312impl CredentialsVerifier<Uuid> for MemorySecretRepository {
313 async fn verify_credentials(
314 &self,
315 credentials: Credentials<Uuid>,
316 ) -> Result<VerificationResult> {
317 use crate::hashing::HashingService;
318 use subtle::Choice;
319
320 let read = self.store.read().await;
321
322 // Get stored secret or use precomputed dummy hash to ensure constant-time operation
323 let (stored_secret_str, user_exists_choice) = match read.get(&credentials.id) {
324 Some(stored_secret) => (stored_secret.secret.as_str(), Choice::from(1u8)),
325 None => (self.dummy_hash.as_str(), Choice::from(0u8)),
326 };
327
328 // ALWAYS perform Argon2 verification (constant time regardless of user existence)
329 let hasher = Argon2Hasher::new_recommended()?;
330 let hash_verification_result =
331 hasher.verify_value(&credentials.secret, stored_secret_str)?;
332
333 // Convert hash verification result to Choice for constant-time operations
334 let hash_matches_choice = Choice::from(match hash_verification_result {
335 VerificationResult::Ok => 1u8,
336 VerificationResult::Unauthorized => 0u8,
337 });
338
339 // Combine results using constant-time AND operation
340 // Success only if: user exists AND password hash matches
341 let final_success_choice = user_exists_choice & hash_matches_choice;
342
343 // Convert back to VerificationResult
344 let final_result = if bool::from(final_success_choice) {
345 VerificationResult::Ok
346 } else {
347 VerificationResult::Unauthorized
348 };
349
350 Ok(final_result)
351 }
352}
353
354/// In-memory implementation of [`PermissionMappingRepository`] for development and testing.
355///
356/// This repository stores permission mappings in memory using thread-safe data structures.
357/// It's ideal for development, testing, and small applications that don't require
358/// persistent storage of permission mappings.
359///
360/// # Thread Safety
361///
362/// This implementation uses `Arc<RwLock<HashMap>>` for thread-safe access to the
363/// stored mappings. Multiple readers can access the data concurrently, while
364/// writers have exclusive access.
365///
366/// # Storage Strategy
367///
368/// Mappings are stored in two hash maps for efficient lookup:
369/// - By permission ID for reverse lookup (primary use case)
370/// - By normalized string for string-based queries
371///
372/// # Example
373///
374/// ```rust
375/// use axum_gate::permissions::mapping::PermissionMapping;
376/// use axum_gate::permissions::PermissionId; use axum_gate::repositories::memory::MemoryPermissionMappingRepository;
377/// use axum_gate::permissions::mapping::PermissionMappingRepository;
378/// use std::sync::Arc;
379///
380/// # tokio_test::block_on(async {
381/// let repo = Arc::new(MemoryPermissionMappingRepository::default());
382///
383/// // Store a mapping
384/// let mapping = PermissionMapping::from("read:api");
385/// let stored = repo.store_mapping(mapping.clone()).await.unwrap();
386/// assert!(stored.is_some());
387///
388/// // Query by ID
389/// let found = repo.query_mapping_by_id(mapping.permission_id()).await.unwrap();
390/// assert!(found.is_some());
391/// # });
392/// ```
393#[derive(Debug)]
394pub struct MemoryPermissionMappingRepository {
395 /// Storage of all mappings. Lookups are performed via iteration.
396 mappings: Arc<RwLock<Vec<PermissionMapping>>>,
397}
398
399impl Default for MemoryPermissionMappingRepository {
400 fn default() -> Self {
401 Self {
402 mappings: Arc::new(RwLock::new(Vec::new())),
403 }
404 }
405}
406
407impl From<Vec<PermissionMapping>> for MemoryPermissionMappingRepository {
408 fn from(mappings: Vec<PermissionMapping>) -> Self {
409 let mut vec: Vec<PermissionMapping> = Vec::new();
410
411 for mapping in mappings {
412 // Validate the mapping before storing
413 if let Err(e) = mapping.validate() {
414 tracing::warn!("Skipping invalid permission mapping: {}", e);
415 continue;
416 }
417
418 let id = mapping.permission_id().as_u64();
419 let normalized = mapping.normalized_string();
420
421 // avoid duplicates by id or normalized string
422 if !vec
423 .iter()
424 .any(|m| m.permission_id().as_u64() == id || m.normalized_string() == normalized)
425 {
426 vec.push(mapping);
427 }
428 }
429
430 Self {
431 mappings: Arc::new(RwLock::new(vec)),
432 }
433 }
434}
435
436impl PermissionMappingRepository for MemoryPermissionMappingRepository {
437 async fn store_mapping(&self, mapping: PermissionMapping) -> Result<Option<PermissionMapping>> {
438 // Validate the mapping first
439 if let Err(e) = mapping.validate() {
440 return Err(Error::Repositories(RepositoriesError::operation_failed(
441 RepositoryType::PermissionMapping,
442 RepositoryOperation::Insert,
443 format!("Invalid permission mapping: {}", e),
444 None,
445 Some("store".to_string()),
446 )));
447 }
448
449 let id = mapping.permission_id().as_u64();
450 let normalized = mapping.normalized_string();
451
452 // Check if mapping already exists (by ID or normalized string)
453 {
454 let vec_read = self.mappings.read().await;
455 if vec_read
456 .iter()
457 .any(|m| m.permission_id().as_u64() == id || m.normalized_string() == normalized)
458 {
459 return Ok(None); // Mapping already exists
460 }
461 }
462
463 // Store in vector
464 {
465 let mut vec_write = self.mappings.write().await;
466 vec_write.push(mapping.clone());
467 }
468
469 Ok(Some(mapping))
470 }
471
472 async fn remove_mapping_by_id(&self, id: PermissionId) -> Result<Option<PermissionMapping>> {
473 let id_u64 = id.as_u64();
474
475 let mut vec_write = self.mappings.write().await;
476 if let Some(pos) = vec_write
477 .iter()
478 .position(|m| m.permission_id().as_u64() == id_u64)
479 {
480 let removed = vec_write.swap_remove(pos);
481 Ok(Some(removed))
482 } else {
483 Ok(None)
484 }
485 }
486
487 async fn remove_mapping_by_string(
488 &self,
489 permission: &str,
490 ) -> Result<Option<PermissionMapping>> {
491 let normalized = normalize_permission(permission);
492
493 let mut vec_write = self.mappings.write().await;
494 if let Some(pos) = vec_write
495 .iter()
496 .position(|m| m.normalized_string() == normalized.as_str())
497 {
498 let removed = vec_write.swap_remove(pos);
499 Ok(Some(removed))
500 } else {
501 Ok(None)
502 }
503 }
504
505 async fn query_mapping_by_id(&self, id: PermissionId) -> Result<Option<PermissionMapping>> {
506 let vec_read = self.mappings.read().await;
507 Ok(vec_read
508 .iter()
509 .find(|m| m.permission_id().as_u64() == id.as_u64())
510 .cloned())
511 }
512
513 async fn query_mapping_by_string(&self, permission: &str) -> Result<Option<PermissionMapping>> {
514 let normalized = normalize_permission(permission);
515 let vec_read = self.mappings.read().await;
516 Ok(vec_read
517 .iter()
518 .find(|m| m.normalized_string() == normalized.as_str())
519 .cloned())
520 }
521
522 async fn list_all_mappings(&self) -> Result<Vec<PermissionMapping>> {
523 let vec_read = self.mappings.read().await;
524 Ok(vec_read.iter().cloned().collect())
525 }
526}
527
528/// Normalize a permission name (trim + lowercase).
529///
530/// This function implements the same normalization logic used in
531/// the PermissionId implementation to ensure consistency.
532fn normalize_permission(input: &str) -> String {
533 input.trim().to_lowercase()
534}