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) -> Result<Option<User>, UserDetailsError>;
121
122    /// Check if a user exists.
123    async fn user_exists(&self, username: &str) -> Result<bool, UserDetailsError> {
124        Ok(self.load_user_by_username(username).await?.is_some())
125    }
126}
127
128// =============================================================================
129// User Details Manager Trait
130// =============================================================================
131
132/// Extended trait for managing users (CRUD operations).
133///
134/// # Spring Security Equivalent
135/// Similar to `UserDetailsManager` in Spring Security.
136///
137/// # Example
138/// ```rust,ignore
139/// #[async_trait]
140/// impl UserDetailsManager for DatabaseUserDetailsService {
141///     async fn create_user(&self, user: &User) -> Result<(), UserDetailsError> {
142///         sqlx::query!("INSERT INTO users ...")
143///             .execute(&self.pool)
144///             .await
145///             .map_err(|e| UserDetailsError::StorageError(e.to_string()))?;
146///         Ok(())
147///     }
148///     // ... other methods
149/// }
150/// ```
151#[async_trait]
152pub trait UserDetailsManager: UserDetailsService {
153    /// Create a new user.
154    async fn create_user(&self, user: &User) -> Result<(), UserDetailsError>;
155
156    /// Update an existing user.
157    async fn update_user(&self, user: &User) -> Result<(), UserDetailsError>;
158
159    /// Delete a user by username.
160    async fn delete_user(&self, username: &str) -> Result<(), UserDetailsError>;
161
162    /// Change user's password.
163    ///
164    /// # Arguments
165    /// * `username` - The username
166    /// * `old_password` - The current password (for verification)
167    /// * `new_password` - The new password (should be encoded)
168    async fn change_password(
169        &self,
170        username: &str,
171        old_password: &str,
172        new_password: &str,
173    ) -> Result<(), UserDetailsError>;
174}
175
176// =============================================================================
177// In-Memory User Details Service
178// =============================================================================
179
180/// In-memory implementation of UserDetailsService.
181///
182/// Useful for testing or small applications.
183#[derive(Clone)]
184pub struct InMemoryUserDetailsService {
185    users: Arc<RwLock<HashMap<String, User>>>,
186}
187
188impl Default for InMemoryUserDetailsService {
189    fn default() -> Self {
190        Self::new()
191    }
192}
193
194impl InMemoryUserDetailsService {
195    /// Create a new in-memory service.
196    pub fn new() -> Self {
197        Self {
198            users: Arc::new(RwLock::new(HashMap::new())),
199        }
200    }
201
202    /// Add a user.
203    pub async fn add_user(&self, user: User) {
204        let mut users = self.users.write().await;
205        users.insert(user.get_username().to_string(), user);
206    }
207
208    /// Add multiple users.
209    pub async fn add_users(&self, users: Vec<User>) {
210        let mut store = self.users.write().await;
211        for user in users {
212            store.insert(user.get_username().to_string(), user);
213        }
214    }
215}
216
217#[async_trait]
218impl UserDetailsService for InMemoryUserDetailsService {
219    async fn load_user_by_username(&self, username: &str) -> Result<Option<User>, UserDetailsError> {
220        let users = self.users.read().await;
221        Ok(users.get(username).cloned())
222    }
223}
224
225#[async_trait]
226impl UserDetailsManager for InMemoryUserDetailsService {
227    async fn create_user(&self, user: &User) -> Result<(), UserDetailsError> {
228        let mut users = self.users.write().await;
229        let username = user.get_username().to_string();
230        if users.contains_key(&username) {
231            return Err(UserDetailsError::AlreadyExists);
232        }
233        users.insert(username, user.clone());
234        Ok(())
235    }
236
237    async fn update_user(&self, user: &User) -> Result<(), UserDetailsError> {
238        let mut users = self.users.write().await;
239        let username = user.get_username().to_string();
240        if !users.contains_key(&username) {
241            return Err(UserDetailsError::NotFound);
242        }
243        users.insert(username, user.clone());
244        Ok(())
245    }
246
247    async fn delete_user(&self, username: &str) -> Result<(), UserDetailsError> {
248        let mut users = self.users.write().await;
249        if users.remove(username).is_none() {
250            return Err(UserDetailsError::NotFound);
251        }
252        Ok(())
253    }
254
255    async fn change_password(
256        &self,
257        username: &str,
258        _old_password: &str,
259        new_password: &str,
260    ) -> Result<(), UserDetailsError> {
261        let mut users = self.users.write().await;
262        match users.get_mut(username) {
263            Some(user) => {
264                // Create new user with updated password
265                let updated = User::new(user.get_username().to_string(), new_password.to_string())
266                    .roles(user.get_roles())
267                    .authorities(user.get_authorities());
268                *user = updated;
269                Ok(())
270            }
271            None => Err(UserDetailsError::NotFound),
272        }
273    }
274}
275
276// =============================================================================
277// Caching User Details Service
278// =============================================================================
279
280/// Cached entry for user details.
281struct CachedUser {
282    user: User,
283    cached_at: Instant,
284}
285
286/// Caching wrapper for UserDetailsService.
287///
288/// Caches loaded users for a configurable duration to reduce database calls.
289///
290/// # Example
291/// ```rust,ignore
292/// let cached_service = CachingUserDetailsService::new(my_service)
293///     .ttl(Duration::from_secs(300));  // Cache for 5 minutes
294/// ```
295pub struct CachingUserDetailsService<S>
296where
297    S: UserDetailsService,
298{
299    inner: S,
300    cache: Arc<RwLock<HashMap<String, CachedUser>>>,
301    ttl: Duration,
302}
303
304impl<S> CachingUserDetailsService<S>
305where
306    S: UserDetailsService,
307{
308    /// Create a new caching service with default TTL (5 minutes).
309    pub fn new(inner: S) -> Self {
310        Self {
311            inner,
312            cache: Arc::new(RwLock::new(HashMap::new())),
313            ttl: Duration::from_secs(300),
314        }
315    }
316
317    /// Set the cache TTL (time-to-live).
318    pub fn ttl(mut self, ttl: Duration) -> Self {
319        self.ttl = ttl;
320        self
321    }
322
323    /// Clear the cache.
324    pub async fn clear_cache(&self) {
325        let mut cache = self.cache.write().await;
326        cache.clear();
327    }
328
329    /// Invalidate a specific user from cache.
330    pub async fn invalidate(&self, username: &str) {
331        let mut cache = self.cache.write().await;
332        cache.remove(username);
333    }
334
335    /// Check if a cached entry is still valid.
336    fn is_valid(&self, entry: &CachedUser) -> bool {
337        entry.cached_at.elapsed() < self.ttl
338    }
339}
340
341#[async_trait]
342impl<S> UserDetailsService for CachingUserDetailsService<S>
343where
344    S: UserDetailsService + Send + Sync,
345{
346    async fn load_user_by_username(&self, username: &str) -> Result<Option<User>, UserDetailsError> {
347        // Check cache first
348        {
349            let cache = self.cache.read().await;
350            if let Some(cached) = cache.get(username) {
351                if self.is_valid(cached) {
352                    return Ok(Some(cached.user.clone()));
353                }
354            }
355        }
356
357        // Load from inner service
358        let result = self.inner.load_user_by_username(username).await?;
359
360        // Cache the result if found
361        if let Some(ref user) = result {
362            let mut cache = self.cache.write().await;
363            cache.insert(
364                username.to_string(),
365                CachedUser {
366                    user: user.clone(),
367                    cached_at: Instant::now(),
368                },
369            );
370        }
371
372        Ok(result)
373    }
374}
375
376// =============================================================================
377// User Details Authenticator
378// =============================================================================
379
380/// Authenticator that uses a UserDetailsService for credential validation.
381///
382/// # Spring Equivalent
383/// Similar to `DaoAuthenticationProvider` in Spring Security.
384///
385/// # Example
386/// ```rust,ignore
387/// let authenticator = UserDetailsAuthenticator::new(
388///     my_user_details_service,
389///     Argon2PasswordEncoder::new(),
390/// );
391///
392/// // Authenticate user
393/// let user = authenticator.authenticate("username", "password").await?;
394/// ```
395#[derive(Clone)]
396pub struct UserDetailsAuthenticator<S, E>
397where
398    S: UserDetailsService + Clone,
399    E: PasswordEncoder + Clone,
400{
401    service: Arc<S>,
402    encoder: Arc<E>,
403}
404
405impl<S, E> UserDetailsAuthenticator<S, E>
406where
407    S: UserDetailsService + Clone,
408    E: PasswordEncoder + Clone,
409{
410    /// Create a new authenticator with the given service and encoder.
411    pub fn new(service: S, encoder: E) -> Self {
412        Self {
413            service: Arc::new(service),
414            encoder: Arc::new(encoder),
415        }
416    }
417
418    /// Authenticate a user with username and password.
419    pub async fn authenticate(
420        &self,
421        username: &str,
422        password: &str,
423    ) -> Result<User, UserDetailsError> {
424        // Load user
425        let user = self
426            .service
427            .load_user_by_username(username)
428            .await?
429            .ok_or(UserDetailsError::NotFound)?;
430
431        // Verify password
432        if self.encoder.matches(password, user.get_password()) {
433            Ok(user)
434        } else {
435            Err(UserDetailsError::InvalidCredentials)
436        }
437    }
438
439    /// Get the user details service.
440    pub fn service(&self) -> &S {
441        &self.service
442    }
443
444    /// Get the password encoder.
445    pub fn encoder(&self) -> &E {
446        &self.encoder
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    fn test_user() -> User {
455        User::new("testuser".to_string(), "password".to_string())
456            .roles(&["USER".into()])
457            .authorities(&["read".into()])
458    }
459
460    #[tokio::test]
461    async fn test_in_memory_service() {
462        let service = InMemoryUserDetailsService::new();
463        let user = test_user();
464
465        // Add user
466        service.add_user(user.clone()).await;
467
468        // Load user
469        let loaded = service.load_user_by_username("testuser").await.unwrap();
470        assert!(loaded.is_some());
471        assert_eq!(loaded.unwrap().get_username(), "testuser");
472
473        // User exists
474        assert!(service.user_exists("testuser").await.unwrap());
475        assert!(!service.user_exists("unknown").await.unwrap());
476    }
477
478    #[tokio::test]
479    async fn test_in_memory_manager() {
480        let service = InMemoryUserDetailsService::new();
481        let user = test_user();
482
483        // Create user
484        service.create_user(&user).await.unwrap();
485        assert!(service.user_exists("testuser").await.unwrap());
486
487        // Duplicate create fails
488        let result = service.create_user(&user).await;
489        assert!(matches!(result, Err(UserDetailsError::AlreadyExists)));
490
491        // Update user
492        let updated = User::new("testuser".to_string(), "newpass".to_string())
493            .roles(&["ADMIN".into()]);
494        service.update_user(&updated).await.unwrap();
495
496        let loaded = service.load_user_by_username("testuser").await.unwrap().unwrap();
497        assert!(loaded.has_role("ADMIN"));
498
499        // Delete user
500        service.delete_user("testuser").await.unwrap();
501        assert!(!service.user_exists("testuser").await.unwrap());
502
503        // Delete non-existent fails
504        let result = service.delete_user("testuser").await;
505        assert!(matches!(result, Err(UserDetailsError::NotFound)));
506    }
507
508    #[tokio::test]
509    async fn test_caching_service() {
510        let inner = InMemoryUserDetailsService::new();
511        inner.add_user(test_user()).await;
512
513        let cached = CachingUserDetailsService::new(inner)
514            .ttl(Duration::from_secs(60));
515
516        // First load (from inner)
517        let user1 = cached.load_user_by_username("testuser").await.unwrap();
518        assert!(user1.is_some());
519
520        // Second load (from cache)
521        let user2 = cached.load_user_by_username("testuser").await.unwrap();
522        assert!(user2.is_some());
523
524        // Invalidate cache
525        cached.invalidate("testuser").await;
526
527        // Load again (from inner after invalidation)
528        let user3 = cached.load_user_by_username("testuser").await.unwrap();
529        assert!(user3.is_some());
530    }
531}