Skip to main content

openclaw_gateway/auth/
users.rs

1//! User model and storage.
2
3use std::path::Path;
4
5use argon2::{
6    Argon2,
7    password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
8};
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12use super::AuthError;
13
14/// User role for access control.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum UserRole {
18    /// Full administrative access.
19    Admin,
20    /// Can manage sessions and view all data.
21    Operator,
22    /// Read-only access.
23    Viewer,
24}
25
26impl UserRole {
27    /// Check if this role has admin privileges.
28    #[must_use]
29    pub const fn is_admin(&self) -> bool {
30        matches!(self, Self::Admin)
31    }
32
33    /// Check if this role can manage sessions.
34    #[must_use]
35    pub const fn can_manage_sessions(&self) -> bool {
36        matches!(self, Self::Admin | Self::Operator)
37    }
38
39    /// Check if this role can view data.
40    #[must_use]
41    pub const fn can_view(&self) -> bool {
42        true // All roles can view
43    }
44}
45
46impl std::fmt::Display for UserRole {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            Self::Admin => write!(f, "admin"),
50            Self::Operator => write!(f, "operator"),
51            Self::Viewer => write!(f, "viewer"),
52        }
53    }
54}
55
56impl std::str::FromStr for UserRole {
57    type Err = AuthError;
58
59    fn from_str(s: &str) -> Result<Self, Self::Err> {
60        match s.to_lowercase().as_str() {
61            "admin" => Ok(Self::Admin),
62            "operator" => Ok(Self::Operator),
63            "viewer" => Ok(Self::Viewer),
64            _ => Err(AuthError::Config(format!("Unknown role: {s}"))),
65        }
66    }
67}
68
69/// User account.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct User {
72    /// Unique user ID.
73    pub id: String,
74    /// Username for login.
75    pub username: String,
76    /// Optional email address.
77    pub email: Option<String>,
78    /// Argon2 password hash (stored in DB, not exposed in public API).
79    pub password_hash: String,
80    /// User role.
81    pub role: UserRole,
82    /// When the user was created.
83    pub created_at: DateTime<Utc>,
84    /// When the user last logged in.
85    pub last_login: Option<DateTime<Utc>>,
86    /// Whether the account is active.
87    pub active: bool,
88}
89
90impl User {
91    /// Create a new user with the given credentials.
92    ///
93    /// # Errors
94    ///
95    /// Returns error if password hashing fails.
96    pub fn new(
97        username: impl Into<String>,
98        password: &str,
99        role: UserRole,
100    ) -> Result<Self, AuthError> {
101        let username = username.into();
102        let id = format!("user_{}", uuid_v4());
103        let password_hash = hash_password(password)?;
104
105        Ok(Self {
106            id,
107            username,
108            email: None,
109            password_hash,
110            role,
111            created_at: Utc::now(),
112            last_login: None,
113            active: true,
114        })
115    }
116
117    /// Verify a password against this user's hash.
118    ///
119    /// # Errors
120    ///
121    /// Returns error if password doesn't match.
122    pub fn verify_password(&self, password: &str) -> Result<(), AuthError> {
123        verify_password(password, &self.password_hash)
124    }
125
126    /// Update the user's password.
127    ///
128    /// # Errors
129    ///
130    /// Returns error if password hashing fails.
131    pub fn set_password(&mut self, password: &str) -> Result<(), AuthError> {
132        self.password_hash = hash_password(password)?;
133        Ok(())
134    }
135
136    /// Create a safe version of user for API responses (no password hash).
137    #[must_use]
138    pub fn to_public(&self) -> PublicUser {
139        PublicUser {
140            id: self.id.clone(),
141            username: self.username.clone(),
142            email: self.email.clone(),
143            role: self.role,
144            created_at: self.created_at,
145            last_login: self.last_login,
146            active: self.active,
147        }
148    }
149}
150
151/// Public user representation (for API responses).
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct PublicUser {
154    /// Unique user ID.
155    pub id: String,
156    /// Username.
157    pub username: String,
158    /// Email address.
159    pub email: Option<String>,
160    /// User role.
161    pub role: UserRole,
162    /// When created.
163    pub created_at: DateTime<Utc>,
164    /// Last login time.
165    pub last_login: Option<DateTime<Utc>>,
166    /// Whether active.
167    pub active: bool,
168}
169
170/// User store backed by sled.
171pub struct UserStore {
172    db: sled::Db,
173    tree: sled::Tree,
174}
175
176impl UserStore {
177    /// Open or create a user store at the given path.
178    ///
179    /// # Errors
180    ///
181    /// Returns error if database cannot be opened.
182    pub fn open(path: &Path) -> Result<Self, AuthError> {
183        let db = sled::open(path.join("auth"))
184            .map_err(|e| AuthError::Storage(format!("Failed to open auth database: {e}")))?;
185
186        let tree = db
187            .open_tree("users")
188            .map_err(|e| AuthError::Storage(format!("Failed to open users tree: {e}")))?;
189
190        Ok(Self { db, tree })
191    }
192
193    /// Create a new user store with an existing sled database.
194    ///
195    /// # Errors
196    ///
197    /// Returns error if tree cannot be opened.
198    pub fn with_db(db: sled::Db) -> Result<Self, AuthError> {
199        let tree = db
200            .open_tree("users")
201            .map_err(|e| AuthError::Storage(format!("Failed to open users tree: {e}")))?;
202
203        Ok(Self { db, tree })
204    }
205
206    /// Get the underlying sled database.
207    #[must_use]
208    pub const fn db(&self) -> &sled::Db {
209        &self.db
210    }
211
212    /// Check if any users exist.
213    #[must_use]
214    pub fn is_empty(&self) -> bool {
215        self.tree.is_empty()
216    }
217
218    /// Count total users.
219    #[must_use]
220    pub fn count(&self) -> usize {
221        // Count entries that don't start with "idx:" prefix
222        self.tree
223            .iter()
224            .filter(|r| {
225                r.as_ref()
226                    .map(|(k, _)| !k.starts_with(b"idx:"))
227                    .unwrap_or(false)
228            })
229            .count()
230    }
231
232    /// Create a new user.
233    ///
234    /// # Errors
235    ///
236    /// Returns error if user already exists or storage fails.
237    pub fn create(&self, user: &User) -> Result<(), AuthError> {
238        // Check if username already exists
239        if self.get_by_username(&user.username)?.is_some() {
240            return Err(AuthError::UserExists(user.username.clone()));
241        }
242
243        let key = user.id.as_bytes();
244        let value = serde_json::to_vec(user)
245            .map_err(|e| AuthError::Storage(format!("Serialization error: {e}")))?;
246
247        self.tree
248            .insert(key, value)
249            .map_err(|e| AuthError::Storage(format!("Insert error: {e}")))?;
250
251        // Create username -> id index
252        let index_key = format!("idx:username:{}", user.username);
253        self.tree
254            .insert(index_key.as_bytes(), user.id.as_bytes())
255            .map_err(|e| AuthError::Storage(format!("Index error: {e}")))?;
256
257        self.tree
258            .flush()
259            .map_err(|e| AuthError::Storage(format!("Flush error: {e}")))?;
260
261        Ok(())
262    }
263
264    /// Get a user by ID.
265    ///
266    /// # Errors
267    ///
268    /// Returns error if storage fails.
269    pub fn get(&self, id: &str) -> Result<Option<User>, AuthError> {
270        let key = id.as_bytes();
271        match self.tree.get(key) {
272            Ok(Some(value)) => {
273                let user: User = serde_json::from_slice(&value)
274                    .map_err(|e| AuthError::Storage(format!("Deserialization error: {e}")))?;
275                Ok(Some(user))
276            }
277            Ok(None) => Ok(None),
278            Err(e) => Err(AuthError::Storage(format!("Get error: {e}"))),
279        }
280    }
281
282    /// Get a user by username.
283    ///
284    /// # Errors
285    ///
286    /// Returns error if storage fails.
287    pub fn get_by_username(&self, username: &str) -> Result<Option<User>, AuthError> {
288        let index_key = format!("idx:username:{username}");
289        match self.tree.get(index_key.as_bytes()) {
290            Ok(Some(id_bytes)) => {
291                let id = String::from_utf8_lossy(&id_bytes);
292                self.get(&id)
293            }
294            Ok(None) => Ok(None),
295            Err(e) => Err(AuthError::Storage(format!("Index lookup error: {e}"))),
296        }
297    }
298
299    /// Update an existing user.
300    ///
301    /// # Errors
302    ///
303    /// Returns error if user doesn't exist or storage fails.
304    pub fn update(&self, user: &User) -> Result<(), AuthError> {
305        // Verify user exists
306        if self.get(&user.id)?.is_none() {
307            return Err(AuthError::UserNotFound(user.id.clone()));
308        }
309
310        let key = user.id.as_bytes();
311        let value = serde_json::to_vec(user)
312            .map_err(|e| AuthError::Storage(format!("Serialization error: {e}")))?;
313
314        self.tree
315            .insert(key, value)
316            .map_err(|e| AuthError::Storage(format!("Update error: {e}")))?;
317
318        self.tree
319            .flush()
320            .map_err(|e| AuthError::Storage(format!("Flush error: {e}")))?;
321
322        Ok(())
323    }
324
325    /// Delete a user.
326    ///
327    /// # Errors
328    ///
329    /// Returns error if storage fails.
330    pub fn delete(&self, id: &str) -> Result<bool, AuthError> {
331        // Get user first to remove index
332        if let Some(user) = self.get(id)? {
333            let index_key = format!("idx:username:{}", user.username);
334            self.tree
335                .remove(index_key.as_bytes())
336                .map_err(|e| AuthError::Storage(format!("Index remove error: {e}")))?;
337        }
338
339        let removed = self
340            .tree
341            .remove(id.as_bytes())
342            .map_err(|e| AuthError::Storage(format!("Delete error: {e}")))?
343            .is_some();
344
345        self.tree
346            .flush()
347            .map_err(|e| AuthError::Storage(format!("Flush error: {e}")))?;
348
349        Ok(removed)
350    }
351
352    /// List all users.
353    ///
354    /// # Errors
355    ///
356    /// Returns error if storage fails.
357    pub fn list(&self) -> Result<Vec<User>, AuthError> {
358        let mut users = Vec::new();
359
360        for result in &self.tree {
361            let (key, value) =
362                result.map_err(|e| AuthError::Storage(format!("Iter error: {e}")))?;
363
364            // Skip index entries
365            if key.starts_with(b"idx:") {
366                continue;
367            }
368
369            let user: User = serde_json::from_slice(&value)
370                .map_err(|e| AuthError::Storage(format!("Deserialization error: {e}")))?;
371            users.push(user);
372        }
373
374        Ok(users)
375    }
376
377    /// Update last login time for a user.
378    ///
379    /// # Errors
380    ///
381    /// Returns error if user doesn't exist or storage fails.
382    pub fn update_last_login(&self, id: &str) -> Result<(), AuthError> {
383        let mut user = self
384            .get(id)?
385            .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
386
387        user.last_login = Some(Utc::now());
388        self.update(&user)
389    }
390}
391
392/// Hash a password using Argon2id.
393fn hash_password(password: &str) -> Result<String, AuthError> {
394    let salt = SaltString::generate(&mut OsRng);
395    let argon2 = Argon2::default();
396
397    argon2
398        .hash_password(password.as_bytes(), &salt)
399        .map(|h| h.to_string())
400        .map_err(|e| AuthError::Config(format!("Password hashing failed: {e}")))
401}
402
403/// Verify a password against a hash.
404fn verify_password(password: &str, hash: &str) -> Result<(), AuthError> {
405    let parsed_hash =
406        PasswordHash::new(hash).map_err(|e| AuthError::Config(format!("Invalid hash: {e}")))?;
407
408    Argon2::default()
409        .verify_password(password.as_bytes(), &parsed_hash)
410        .map_err(|_| AuthError::InvalidCredentials)
411}
412
413/// Generate a simple UUID v4.
414fn uuid_v4() -> String {
415    use rand::RngCore;
416    let mut rng = rand::thread_rng();
417    let mut bytes = [0u8; 16];
418    rng.fill_bytes(&mut bytes);
419
420    // Set version (4) and variant bits
421    bytes[6] = (bytes[6] & 0x0f) | 0x40;
422    bytes[8] = (bytes[8] & 0x3f) | 0x80;
423
424    format!(
425        "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
426        bytes[0],
427        bytes[1],
428        bytes[2],
429        bytes[3],
430        bytes[4],
431        bytes[5],
432        bytes[6],
433        bytes[7],
434        bytes[8],
435        bytes[9],
436        bytes[10],
437        bytes[11],
438        bytes[12],
439        bytes[13],
440        bytes[14],
441        bytes[15]
442    )
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448    use tempfile::TempDir;
449
450    #[test]
451    fn test_user_creation() {
452        let user = User::new("testuser", "password123", UserRole::Admin).unwrap();
453        assert_eq!(user.username, "testuser");
454        assert!(user.id.starts_with("user_"));
455        assert!(user.active);
456        assert_eq!(user.role, UserRole::Admin);
457    }
458
459    #[test]
460    fn test_password_verification() {
461        let user = User::new("testuser", "password123", UserRole::Admin).unwrap();
462        assert!(user.verify_password("password123").is_ok());
463        assert!(user.verify_password("wrongpassword").is_err());
464    }
465
466    #[test]
467    fn test_user_store() {
468        let temp_dir = TempDir::new().unwrap();
469        let store = UserStore::open(temp_dir.path()).unwrap();
470
471        assert!(store.is_empty());
472
473        let user = User::new("admin", "secret", UserRole::Admin).unwrap();
474        store.create(&user).unwrap();
475
476        assert!(!store.is_empty());
477        assert_eq!(store.count(), 1);
478
479        let loaded = store.get(&user.id).unwrap().unwrap();
480        assert_eq!(loaded.username, "admin");
481
482        let by_name = store.get_by_username("admin").unwrap().unwrap();
483        assert_eq!(by_name.id, user.id);
484    }
485
486    #[test]
487    fn test_user_roles() {
488        assert!(UserRole::Admin.is_admin());
489        assert!(!UserRole::Operator.is_admin());
490        assert!(!UserRole::Viewer.is_admin());
491
492        assert!(UserRole::Admin.can_manage_sessions());
493        assert!(UserRole::Operator.can_manage_sessions());
494        assert!(!UserRole::Viewer.can_manage_sessions());
495    }
496
497    #[test]
498    fn test_duplicate_user() {
499        let temp_dir = TempDir::new().unwrap();
500        let store = UserStore::open(temp_dir.path()).unwrap();
501
502        let user1 = User::new("admin", "secret1", UserRole::Admin).unwrap();
503        store.create(&user1).unwrap();
504
505        let user2 = User::new("admin", "secret2", UserRole::Operator).unwrap();
506        let result = store.create(&user2);
507
508        assert!(matches!(result, Err(AuthError::UserExists(_))));
509    }
510}