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}