Skip to main content

actix_security_core/http/security/
user_details.rs

1//! User Details Service for loading users from any data source.
2//!
3//! # Spring Security Equivalent
4//! Similar to Spring Security's `UserDetailsService` and `UserDetailsManager` interfaces.
5//!
6//! # Features
7//! - Async trait for loading users
8//! - Support for any data source (database, LDAP, API, etc.)
9//! - User management operations (create, update, delete)
10//! - Caching layer support
11//!
12//! # Example
13//! ```rust,ignore
14//! use actix_security_core::http::security::user_details::{UserDetailsService, UserDetailsError};
15//! use async_trait::async_trait;
16//!
17//! struct MyUserDetailsService {
18//!     pool: PgPool,
19//! }
20//!
21//! #[async_trait]
22//! impl UserDetailsService for MyUserDetailsService {
23//!     async fn load_user_by_username(&self, username: &str) -> Result<Option<User>, UserDetailsError> {
24//!         // Load from database...
25//!         Ok(Some(user))
26//!     }
27//! }
28//! ```
29
30use crate::http::security::crypto::PasswordEncoder;
31use crate::http::security::User;
32use async_trait::async_trait;
33use std::collections::HashMap;
34use std::sync::Arc;
35use std::time::{Duration, Instant};
36use tokio::sync::RwLock;
37
38// =============================================================================
39// User Details Error
40// =============================================================================
41
42/// Errors that can occur when loading or managing user details.
43#[derive(Debug)]
44pub enum UserDetailsError {
45    /// User not found
46    NotFound,
47    /// User already exists
48    AlreadyExists,
49    /// Invalid credentials
50    InvalidCredentials,
51    /// Account is disabled
52    AccountDisabled,
53    /// Account is locked
54    AccountLocked,
55    /// Account is expired
56    AccountExpired,
57    /// Credentials are expired
58    CredentialsExpired,
59    /// Database or storage error
60    StorageError(String),
61    /// Other error
62    Other(String),
63}
64
65impl std::fmt::Display for UserDetailsError {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            UserDetailsError::NotFound => write!(f, "User not found"),
69            UserDetailsError::AlreadyExists => write!(f, "User already exists"),
70            UserDetailsError::InvalidCredentials => write!(f, "Invalid credentials"),
71            UserDetailsError::AccountDisabled => write!(f, "Account is disabled"),
72            UserDetailsError::AccountLocked => write!(f, "Account is locked"),
73            UserDetailsError::AccountExpired => write!(f, "Account is expired"),
74            UserDetailsError::CredentialsExpired => write!(f, "Credentials are expired"),
75            UserDetailsError::StorageError(e) => write!(f, "Storage error: {}", e),
76            UserDetailsError::Other(e) => write!(f, "Error: {}", e),
77        }
78    }
79}
80
81impl std::error::Error for UserDetailsError {}
82
83// =============================================================================
84// User Details Service Trait
85// =============================================================================
86
87/// Async trait for loading user details from any data source.
88///
89/// # Spring Security Equivalent
90/// Similar to `UserDetailsService` in Spring Security.
91///
92/// # Example
93/// ```rust,ignore
94/// use actix_security_core::http::security::user_details::{UserDetailsService, UserDetailsError};
95/// use async_trait::async_trait;
96///
97/// struct DatabaseUserDetailsService {
98///     pool: sqlx::PgPool,
99/// }
100///
101/// #[async_trait]
102/// impl UserDetailsService for DatabaseUserDetailsService {
103///     async fn load_user_by_username(&self, username: &str) -> Result<Option<User>, UserDetailsError> {
104///         let row = sqlx::query!("SELECT * FROM users WHERE username = $1", username)
105///             .fetch_optional(&self.pool)
106///             .await
107///             .map_err(|e| UserDetailsError::StorageError(e.to_string()))?;
108///
109///         Ok(row.map(|r| User::with_encoded_password(&r.username, r.password)
110///             .roles(&r.roles.split(',').map(String::from).collect::<Vec<_>>())))
111///     }
112/// }
113/// ```
114#[async_trait]
115pub trait UserDetailsService: Send + Sync {
116    /// Load user by username.
117    ///
118    /// Returns `Ok(Some(user))` if found, `Ok(None)` if not found,
119    /// or `Err(...)` if an error occurred.
120    async fn load_user_by_username(&self, username: &str)
121        -> Result<Option<User>, UserDetailsError>;
122
123    /// Check if a user exists.
124    async fn user_exists(&self, username: &str) -> Result<bool, UserDetailsError> {
125        Ok(self.load_user_by_username(username).await?.is_some())
126    }
127}
128
129// =============================================================================
130// User Details Manager Trait
131// =============================================================================
132
133/// Extended trait for managing users (CRUD operations).
134///
135/// # Spring Security Equivalent
136/// Similar to `UserDetailsManager` in Spring Security.
137///
138/// # Example
139/// ```rust,ignore
140/// #[async_trait]
141/// impl UserDetailsManager for DatabaseUserDetailsService {
142///     async fn create_user(&self, user: &User) -> Result<(), UserDetailsError> {
143///         sqlx::query!("INSERT INTO users ...")
144///             .execute(&self.pool)
145///             .await
146///             .map_err(|e| UserDetailsError::StorageError(e.to_string()))?;
147///         Ok(())
148///     }
149///     // ... other methods
150/// }
151/// ```
152#[async_trait]
153pub trait UserDetailsManager: UserDetailsService {
154    /// Create a new user.
155    async fn create_user(&self, user: &User) -> Result<(), UserDetailsError>;
156
157    /// Update an existing user.
158    async fn update_user(&self, user: &User) -> Result<(), UserDetailsError>;
159
160    /// Delete a user by username.
161    async fn delete_user(&self, username: &str) -> Result<(), UserDetailsError>;
162
163    /// Change user's password.
164    ///
165    /// # Arguments
166    /// * `username` - The username
167    /// * `old_password` - The current password (for verification)
168    /// * `new_password` - The new password (should be encoded)
169    async fn change_password(
170        &self,
171        username: &str,
172        old_password: &str,
173        new_password: &str,
174    ) -> Result<(), UserDetailsError>;
175}
176
177// =============================================================================
178// In-Memory User Details Service
179// =============================================================================
180
181/// In-memory implementation of UserDetailsService.
182///
183/// Useful for testing or small applications.
184#[derive(Clone)]
185pub struct InMemoryUserDetailsService {
186    users: Arc<RwLock<HashMap<String, User>>>,
187}
188
189impl Default for InMemoryUserDetailsService {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195impl InMemoryUserDetailsService {
196    /// Create a new in-memory service.
197    pub fn new() -> Self {
198        Self {
199            users: Arc::new(RwLock::new(HashMap::new())),
200        }
201    }
202
203    /// Add a user.
204    pub async fn add_user(&self, user: User) {
205        let mut users = self.users.write().await;
206        users.insert(user.get_username().to_string(), user);
207    }
208
209    /// Add multiple users.
210    pub async fn add_users(&self, users: Vec<User>) {
211        let mut store = self.users.write().await;
212        for user in users {
213            store.insert(user.get_username().to_string(), user);
214        }
215    }
216}
217
218#[async_trait]
219impl UserDetailsService for InMemoryUserDetailsService {
220    async fn load_user_by_username(
221        &self,
222        username: &str,
223    ) -> Result<Option<User>, UserDetailsError> {
224        let users = self.users.read().await;
225        Ok(users.get(username).cloned())
226    }
227}
228
229#[async_trait]
230impl UserDetailsManager for InMemoryUserDetailsService {
231    async fn create_user(&self, user: &User) -> Result<(), UserDetailsError> {
232        let mut users = self.users.write().await;
233        let username = user.get_username().to_string();
234        if users.contains_key(&username) {
235            return Err(UserDetailsError::AlreadyExists);
236        }
237        users.insert(username, user.clone());
238        Ok(())
239    }
240
241    async fn update_user(&self, user: &User) -> Result<(), UserDetailsError> {
242        let mut users = self.users.write().await;
243        let username = user.get_username().to_string();
244        if !users.contains_key(&username) {
245            return Err(UserDetailsError::NotFound);
246        }
247        users.insert(username, user.clone());
248        Ok(())
249    }
250
251    async fn delete_user(&self, username: &str) -> Result<(), UserDetailsError> {
252        let mut users = self.users.write().await;
253        if users.remove(username).is_none() {
254            return Err(UserDetailsError::NotFound);
255        }
256        Ok(())
257    }
258
259    async fn change_password(
260        &self,
261        username: &str,
262        _old_password: &str,
263        new_password: &str,
264    ) -> Result<(), UserDetailsError> {
265        let mut users = self.users.write().await;
266        match users.get_mut(username) {
267            Some(user) => {
268                // Create new user with updated password
269                let updated = User::new(user.get_username().to_string(), new_password.to_string())
270                    .roles(user.get_roles())
271                    .authorities(user.get_authorities());
272                *user = updated;
273                Ok(())
274            }
275            None => Err(UserDetailsError::NotFound),
276        }
277    }
278}
279
280// =============================================================================
281// Caching User Details Service
282// =============================================================================
283
284/// Cached entry for user details.
285struct CachedUser {
286    user: User,
287    cached_at: Instant,
288}
289
290/// Caching wrapper for UserDetailsService.
291///
292/// Caches loaded users for a configurable duration to reduce database calls.
293///
294/// # Example
295/// ```rust,ignore
296/// let cached_service = CachingUserDetailsService::new(my_service)
297///     .ttl(Duration::from_secs(300));  // Cache for 5 minutes
298/// ```
299pub struct CachingUserDetailsService<S>
300where
301    S: UserDetailsService,
302{
303    inner: S,
304    cache: Arc<RwLock<HashMap<String, CachedUser>>>,
305    ttl: Duration,
306}
307
308impl<S> CachingUserDetailsService<S>
309where
310    S: UserDetailsService,
311{
312    /// Create a new caching service with default TTL (5 minutes).
313    pub fn new(inner: S) -> Self {
314        Self {
315            inner,
316            cache: Arc::new(RwLock::new(HashMap::new())),
317            ttl: Duration::from_secs(300),
318        }
319    }
320
321    /// Set the cache TTL (time-to-live).
322    pub fn ttl(mut self, ttl: Duration) -> Self {
323        self.ttl = ttl;
324        self
325    }
326
327    /// Clear the cache.
328    pub async fn clear_cache(&self) {
329        let mut cache = self.cache.write().await;
330        cache.clear();
331    }
332
333    /// Invalidate a specific user from cache.
334    pub async fn invalidate(&self, username: &str) {
335        let mut cache = self.cache.write().await;
336        cache.remove(username);
337    }
338
339    /// Check if a cached entry is still valid.
340    fn is_valid(&self, entry: &CachedUser) -> bool {
341        entry.cached_at.elapsed() < self.ttl
342    }
343}
344
345#[async_trait]
346impl<S> UserDetailsService for CachingUserDetailsService<S>
347where
348    S: UserDetailsService + Send + Sync,
349{
350    async fn load_user_by_username(
351        &self,
352        username: &str,
353    ) -> Result<Option<User>, UserDetailsError> {
354        // Check cache first
355        {
356            let cache = self.cache.read().await;
357            if let Some(cached) = cache.get(username) {
358                if self.is_valid(cached) {
359                    return Ok(Some(cached.user.clone()));
360                }
361            }
362        }
363
364        // Load from inner service
365        let result = self.inner.load_user_by_username(username).await?;
366
367        // Cache the result if found
368        if let Some(ref user) = result {
369            let mut cache = self.cache.write().await;
370            cache.insert(
371                username.to_string(),
372                CachedUser {
373                    user: user.clone(),
374                    cached_at: Instant::now(),
375                },
376            );
377        }
378
379        Ok(result)
380    }
381}
382
383// =============================================================================
384// User Details Authenticator
385// =============================================================================
386
387/// Authenticator that uses a UserDetailsService for credential validation.
388///
389/// # Spring Equivalent
390/// Similar to `DaoAuthenticationProvider` in Spring Security.
391///
392/// # Example
393/// ```rust,ignore
394/// let authenticator = UserDetailsAuthenticator::new(
395///     my_user_details_service,
396///     Argon2PasswordEncoder::new(),
397/// );
398///
399/// // Authenticate user
400/// let user = authenticator.authenticate("username", "password").await?;
401/// ```
402#[derive(Clone)]
403pub struct UserDetailsAuthenticator<S, E>
404where
405    S: UserDetailsService + Clone,
406    E: PasswordEncoder + Clone,
407{
408    service: Arc<S>,
409    encoder: Arc<E>,
410}
411
412impl<S, E> UserDetailsAuthenticator<S, E>
413where
414    S: UserDetailsService + Clone,
415    E: PasswordEncoder + Clone,
416{
417    /// Create a new authenticator with the given service and encoder.
418    pub fn new(service: S, encoder: E) -> Self {
419        Self {
420            service: Arc::new(service),
421            encoder: Arc::new(encoder),
422        }
423    }
424
425    /// Authenticate a user with username and password.
426    pub async fn authenticate(
427        &self,
428        username: &str,
429        password: &str,
430    ) -> Result<User, UserDetailsError> {
431        // Load user
432        let user = self
433            .service
434            .load_user_by_username(username)
435            .await?
436            .ok_or(UserDetailsError::NotFound)?;
437
438        // Verify password
439        if self.encoder.matches(password, user.get_password()) {
440            Ok(user)
441        } else {
442            Err(UserDetailsError::InvalidCredentials)
443        }
444    }
445
446    /// Get the user details service.
447    pub fn service(&self) -> &S {
448        &self.service
449    }
450
451    /// Get the password encoder.
452    pub fn encoder(&self) -> &E {
453        &self.encoder
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460
461    fn test_user() -> User {
462        User::new("testuser".to_string(), "password".to_string())
463            .roles(&["USER".into()])
464            .authorities(&["read".into()])
465    }
466
467    #[tokio::test]
468    async fn test_in_memory_service() {
469        let service = InMemoryUserDetailsService::new();
470        let user = test_user();
471
472        // Add user
473        service.add_user(user.clone()).await;
474
475        // Load user
476        let loaded = service.load_user_by_username("testuser").await.unwrap();
477        assert!(loaded.is_some());
478        assert_eq!(loaded.unwrap().get_username(), "testuser");
479
480        // User exists
481        assert!(service.user_exists("testuser").await.unwrap());
482        assert!(!service.user_exists("unknown").await.unwrap());
483    }
484
485    #[tokio::test]
486    async fn test_in_memory_manager() {
487        let service = InMemoryUserDetailsService::new();
488        let user = test_user();
489
490        // Create user
491        service.create_user(&user).await.unwrap();
492        assert!(service.user_exists("testuser").await.unwrap());
493
494        // Duplicate create fails
495        let result = service.create_user(&user).await;
496        assert!(matches!(result, Err(UserDetailsError::AlreadyExists)));
497
498        // Update user
499        let updated =
500            User::new("testuser".to_string(), "newpass".to_string()).roles(&["ADMIN".into()]);
501        service.update_user(&updated).await.unwrap();
502
503        let loaded = service
504            .load_user_by_username("testuser")
505            .await
506            .unwrap()
507            .unwrap();
508        assert!(loaded.has_role("ADMIN"));
509
510        // Delete user
511        service.delete_user("testuser").await.unwrap();
512        assert!(!service.user_exists("testuser").await.unwrap());
513
514        // Delete non-existent fails
515        let result = service.delete_user("testuser").await;
516        assert!(matches!(result, Err(UserDetailsError::NotFound)));
517    }
518
519    #[tokio::test]
520    async fn test_caching_service() {
521        let inner = InMemoryUserDetailsService::new();
522        inner.add_user(test_user()).await;
523
524        let cached = CachingUserDetailsService::new(inner).ttl(Duration::from_secs(60));
525
526        // First load (from inner)
527        let user1 = cached.load_user_by_username("testuser").await.unwrap();
528        assert!(user1.is_some());
529
530        // Second load (from cache)
531        let user2 = cached.load_user_by_username("testuser").await.unwrap();
532        assert!(user2.is_some());
533
534        // Invalidate cache
535        cached.invalidate("testuser").await;
536
537        // Load again (from inner after invalidation)
538        let user3 = cached.load_user_by_username("testuser").await.unwrap();
539        assert!(user3.is_some());
540    }
541}