Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

eidetica/user/
system_databases.rs

1//! System database initialization for the user system
2//!
3//! Creates and manages _users and _databases system databases.
4
5use handle_trait::Handle;
6
7use super::{
8    User,
9    crypto::{derive_encryption_key, encrypt_private_key, hash_password},
10    errors::UserError,
11    key_manager::UserKeyManager,
12    types::{KeyStorage, UserInfo, UserKey, UserStatus},
13};
14use crate::{
15    Database, Instance, Result,
16    auth::{
17        crypto::{PrivateKey, generate_keypair},
18        types::{AuthKey, Permission},
19    },
20    constants::{DATABASES, INSTANCE, USERS},
21    crdt::Doc,
22    database::DatabaseKey,
23    store::Table,
24};
25
26/// Create the _instance system database
27///
28/// This database stores Instance-level configuration and metadata.
29/// Auth is bootstrapped by `Database::create` with the device key as Admin(0).
30///
31/// # Arguments
32/// * `instance` - The Instance handle
33/// * `device_signing_key` - The device's Ed25519 signing key
34///
35/// # Returns
36/// The _instance Database
37pub async fn create_instance_database(
38    instance: &Instance,
39    device_signing_key: &PrivateKey,
40) -> Result<Database> {
41    let mut settings = Doc::new();
42    settings.set("name", INSTANCE);
43    settings.set("type", "system");
44    settings.set("description", "Instance configuration and management");
45
46    Database::create(instance, device_signing_key.clone(), settings).await
47}
48
49/// Create the _users system database
50///
51/// This database stores the user directory mapping user_id -> UserInfo.
52/// Auth is bootstrapped by `Database::create` with the device key as Admin(0).
53///
54/// # Arguments
55/// * `instance` - The Instance handle
56/// * `device_signing_key` - The device's Ed25519 signing key
57///
58/// # Returns
59/// The created _users Database
60pub async fn create_users_database(
61    instance: &Instance,
62    device_signing_key: &PrivateKey,
63) -> Result<Database> {
64    let mut settings = Doc::new();
65    settings.set("name", USERS);
66    settings.set("type", "system");
67    settings.set("description", "User directory database");
68
69    Database::create(instance, device_signing_key.clone(), settings).await
70}
71
72/// Create the _databases tracking database
73///
74/// This database stores the database tracking information mapping
75/// database_id -> DatabaseTracking.
76/// Auth is bootstrapped by `Database::create` with the device key as Admin(0).
77///
78/// # Arguments
79/// * `instance` - The Instance handle
80/// * `device_signing_key` - The device's Ed25519 signing key
81///
82/// # Returns
83/// The created _databases Database
84pub async fn create_databases_tracking(
85    instance: &Instance,
86    device_signing_key: &PrivateKey,
87) -> Result<Database> {
88    let mut settings = Doc::new();
89    settings.set("name", DATABASES);
90    settings.set("type", "system");
91    settings.set("description", "Database tracking and registry");
92
93    Database::create(instance, device_signing_key.clone(), settings).await
94}
95
96/// Create a new user account
97///
98/// This function:
99/// 1. Optionally hashes the user's password (if provided)
100/// 2. Generates a device keypair for the user
101/// 3. Creates a user database for storing keys (encrypted or unencrypted)
102/// 4. Creates UserInfo and stores it in _users database with auto-generated UUID
103///
104/// # Arguments
105/// * `users_db` - The _users system database
106/// * `instance` - The Instance handle
107/// * `username` - Unique username for login
108/// * `password` - Optional password. If None, creates passwordless user (instant login, no encryption)
109///
110/// # Returns
111/// A tuple of (user_uuid, UserInfo) where user_uuid is the generated primary key
112pub async fn create_user(
113    users_db: &Database,
114    instance: &Instance,
115    username: impl AsRef<str>,
116    password: Option<&str>,
117) -> Result<(String, UserInfo)> {
118    let username = username.as_ref();
119    // FIXME: Race condition - multiple concurrent creates with same username
120    // can both succeed, creating duplicate users. This requires either:
121    // 1. Distributed locking mechanism
122    // 2. Backend-level unique constraints
123    // 3. Periodic cleanup/reconciliation process
124    // For now, duplicate detection happens at login time.
125
126    // Check if username already exists
127    let users_table = users_db
128        .get_store_viewer::<Table<UserInfo>>("users")
129        .await?;
130    let existing = users_table.search(|u| u.username == username).await?;
131    if !existing.is_empty() {
132        return Err(UserError::UsernameAlreadyExists {
133            username: username.to_string(),
134        }
135        .into());
136    }
137
138    // 1. Hash password if provided
139    let (password_hash, password_salt) = match password {
140        Some(pwd) => {
141            let (hash, salt) = hash_password(pwd)?;
142            (Some(hash), Some(salt))
143        }
144        None => (None, None),
145    };
146
147    // 2. Generate default keypair for this user (kept in memory only)
148    let (user_private_key, user_public_key) = generate_keypair();
149    // 3. Create user database with the user's key in auth (device key added automatically)
150    let mut user_db_settings = Doc::new();
151    user_db_settings.set("name", format!("_user_{username}"));
152    user_db_settings.set("type", "user");
153    user_db_settings.set("description", format!("User database for {username}"));
154
155    // Get device key for database creation (used as the signing key)
156    let device_private_key = instance.device_key().clone();
157
158    // Create database using device_key as the signing key.
159    // Database::create bootstraps auth with device key as Admin(0).
160    let user_database = Database::create(instance, device_private_key, user_db_settings).await?;
161    let user_database_id = user_database.root_id().clone();
162
163    // Add user's key as an equal owner
164    // FIXME: can we restrict the Device ID's ownership?
165    let txn = user_database.new_transaction().await?;
166    let settings = txn.get_settings()?;
167    settings
168        .set_auth_key(
169            &user_public_key,
170            AuthKey::active(Some("user"), Permission::Admin(0)),
171        )
172        .await?;
173    txn.commit().await?;
174
175    // 4. Store user's private key (encrypted or unencrypted based on password)
176    let user_key = match (password, &password_salt) {
177        (Some(pwd), Some(salt)) => {
178            // Password-protected: encrypt the key
179            let encryption_key = derive_encryption_key(pwd, salt)?;
180            let (ciphertext, nonce) = encrypt_private_key(&user_private_key, &encryption_key)?;
181
182            UserKey {
183                key_id: user_public_key.clone(),
184                storage: KeyStorage::Encrypted {
185                    algorithm: "aes-256-gcm".to_string(),
186                    ciphertext,
187                    nonce,
188                },
189                display_name: Some("Default Key".to_string()),
190                created_at: instance.clock().now_secs(),
191                last_used: None,
192                is_default: true, // First key is always default
193                database_sigkeys: std::collections::HashMap::new(),
194            }
195        }
196        _ => {
197            // Passwordless: store unencrypted
198            UserKey {
199                key_id: user_public_key.clone(),
200                storage: KeyStorage::Unencrypted {
201                    key: user_private_key,
202                },
203                display_name: Some("Default Key".to_string()),
204                created_at: instance.clock().now_secs(),
205                last_used: None,
206                is_default: true, // First key is always default
207                database_sigkeys: std::collections::HashMap::new(),
208            }
209        }
210    };
211
212    let tx = user_database.new_transaction().await?;
213    let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
214    keys_table.insert(user_key).await?;
215    tx.commit().await?;
216
217    // 5. Create UserInfo
218    let user_info = UserInfo {
219        username: username.to_string(),
220        user_database_id,
221        password_hash,
222        password_salt,
223        created_at: instance.clock().now_secs(),
224        status: UserStatus::Active,
225    };
226
227    // 6. Store UserInfo in _users database with auto-generated UUID
228    let tx = users_db.new_transaction().await?;
229    let users_table = tx.get_store::<Table<UserInfo>>("users").await?;
230    let user_uuid = users_table.insert(user_info.clone()).await?; // Generate UUID primary key
231    tx.commit().await?;
232
233    Ok((user_uuid, user_info))
234}
235
236/// Login a user
237///
238/// This function:
239/// 1. Searches for user by username in _users database
240/// 2. Verifies password (if provided and required)
241/// 3. Opens user's private database
242/// 4. Loads and decrypts user keys (or loads unencrypted for passwordless users)
243/// 5. Creates UserKeyManager with keys
244/// 6. Returns User session object
245///
246/// # Arguments
247/// * `users_db` - The _users system database
248/// * `instance` - The Instance handle
249/// * `username` - Username for login
250/// * `password` - Optional password. None for passwordless users.
251///
252/// # Returns
253/// A User session object with keys loaded
254pub async fn login_user(
255    users_db: &Database,
256    instance: &Instance,
257    username: impl AsRef<str>,
258    password: Option<&str>,
259) -> Result<super::User> {
260    let username = username.as_ref();
261
262    // 1. Search for user by username
263    let users_table = users_db
264        .get_store_viewer::<Table<UserInfo>>("users")
265        .await?;
266    let results = users_table.search(|u| u.username == username).await?;
267
268    // Check for duplicate users (race condition detection)
269    let (user_uuid, user_info) = match results.len() {
270        0 => {
271            return Err(UserError::UserNotFound {
272                username: username.to_string(),
273            }
274            .into());
275        }
276        1 => results.into_iter().next().unwrap(),
277        count => {
278            // FIXME: Multiple users with same username detected!
279            // This indicates the race condition occurred during user creation.
280            // Resolution requires manual intervention or automated cleanup.
281            return Err(UserError::DuplicateUsersDetected {
282                username: username.to_string(),
283                count,
284            }
285            .into());
286        }
287    };
288
289    // Check if user is disabled
290    if user_info.status != UserStatus::Active {
291        return Err(UserError::UserDisabled {
292            username: username.to_string(),
293        }
294        .into());
295    }
296
297    // 2. Verify password compatibility
298    let is_passwordless = user_info.password_hash.is_none();
299    match (password, is_passwordless) {
300        (Some(pwd), false) => {
301            // Password provided for password-protected user: verify it
302            let password_hash = user_info.password_hash.as_ref().unwrap();
303            super::crypto::verify_password(pwd, password_hash)?;
304        }
305        (None, true) => {
306            // No password for passwordless user: OK
307        }
308        (Some(_), true) => {
309            // Password provided for passwordless user: reject
310            return Err(UserError::InvalidPassword.into());
311        }
312        (None, false) => {
313            // No password for password-protected user: reject
314            return Err(UserError::PasswordRequired {
315                operation: "login for password-protected user".to_string(),
316            }
317            .into());
318        }
319    }
320
321    // 3. Temporarily open user's private database to read keys (unauthenticated read)
322    let temp_user_database =
323        Database::open_unauthenticated(user_info.user_database_id.clone(), instance)?;
324
325    // 4. Load keys from user database
326    let keys_table = temp_user_database
327        .get_store_viewer::<Table<UserKey>>("keys")
328        .await?;
329    let keys: Vec<UserKey> = keys_table
330        .search(|_| true)
331        .await? // Get all keys
332        .into_iter()
333        .map(|(_, key)| key)
334        .collect();
335
336    // 5. Create UserKeyManager
337    let key_manager = if let Some(pwd) = password {
338        // Password-protected: decrypt keys
339        let password_salt =
340            user_info
341                .password_salt
342                .as_ref()
343                .ok_or_else(|| UserError::PasswordRequired {
344                    operation: "decrypt keys for password-protected user".to_string(),
345                })?;
346        UserKeyManager::new(pwd, password_salt, keys)?
347    } else {
348        // Passwordless: load unencrypted keys
349        UserKeyManager::new_passwordless(keys)?
350    };
351
352    // 6. Re-open user database with the user's default key using open()
353    // This configures the database to use DatabaseKey with the user's key
354    // so all operations work without needing keys in the backend
355    let default_key_id = key_manager
356        .get_default_key_id()
357        .ok_or(UserError::NoKeysAvailable)?;
358    let default_signing_key = key_manager
359        .get_signing_key(&default_key_id)
360        .ok_or_else(|| UserError::KeyNotFound {
361            key_id: default_key_id.to_string(),
362        })?
363        .clone();
364
365    let user_database = Database::open(
366        instance.handle(),
367        &user_info.user_database_id,
368        DatabaseKey::new(default_signing_key),
369    )
370    .await?;
371
372    // 7. Update last_login in separate table
373    // TODO: this is a log, so it will grow unbounded over time and should probably be moved to a log table
374    let tx = users_db.new_transaction().await?;
375    let last_login_table = tx.get_store::<Table<i64>>("last_login").await?;
376    last_login_table
377        .set(&user_uuid, instance.clock().now_secs())
378        .await?;
379    tx.commit().await?;
380
381    // 8. Create User session
382    Ok(User::new(
383        user_uuid,
384        user_info,
385        user_database,
386        instance.handle(),
387        key_manager,
388    ))
389}
390
391/// List all users in the system
392///
393/// # Arguments
394/// * `users_db` - The _users system database
395///
396/// # Returns
397/// Vector of usernames
398pub async fn list_users(users_db: &Database) -> Result<Vec<String>> {
399    let users_table = users_db
400        .get_store_viewer::<Table<UserInfo>>("users")
401        .await?;
402    let users: Vec<UserInfo> = users_table
403        .search(|_| true)
404        .await? // Get all users
405        .into_iter()
406        .map(|(_, user)| user)
407        .collect();
408    Ok(users.into_iter().map(|u| u.username).collect())
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use crate::Instance;
415    use crate::backend::database::InMemory;
416    use crate::store::DocStore;
417    use crate::store::SettingsStore;
418
419    use std::sync::Arc;
420
421    /// Test helper: Create Instance with device key initialized
422    ///
423    /// Uses FixedClock for controllable timestamps.
424    async fn setup_instance() -> (Instance, PrivateKey) {
425        use crate::clock::FixedClock;
426
427        let backend = Arc::new(InMemory::new());
428
429        // Create Instance with FixedClock for controllable timestamps
430        let instance = Instance::create_internal(backend, Arc::new(FixedClock::default()))
431            .await
432            .unwrap();
433
434        // Get the device key from the instance
435        let device_key = instance.device_key().clone();
436
437        (instance, device_key)
438    }
439
440    #[tokio::test]
441    async fn test_create_instance_database() {
442        let (instance, device_key) = setup_instance().await;
443
444        let instance_db = create_instance_database(&instance, &device_key)
445            .await
446            .unwrap();
447
448        // Verify database was created
449        assert!(!instance_db.root_id().to_string().is_empty());
450
451        // Verify settings
452        let transaction = instance_db.new_transaction().await.unwrap();
453        let doc_store = transaction
454            .get_store::<DocStore>("_settings")
455            .await
456            .unwrap();
457        let name = doc_store.get_string("name").await.unwrap();
458        assert_eq!(name, INSTANCE);
459
460        // Verify auth settings - key is stored by pubkey
461        let device_pubkey = instance.device_key().public_key();
462        let settings_store = SettingsStore::new(&transaction).unwrap();
463        let auth_settings = settings_store.auth_snapshot().await.unwrap();
464        let device_key = auth_settings.get_key_by_pubkey(&device_pubkey).unwrap();
465        assert_eq!(device_key.permissions(), &Permission::Admin(0));
466        assert_eq!(device_key.name(), None);
467    }
468
469    #[tokio::test]
470    async fn test_create_users_database() {
471        let (instance, device_key) = setup_instance().await;
472
473        let users_db = create_users_database(&instance, &device_key).await.unwrap();
474
475        // Verify database was created
476        assert!(!users_db.root_id().to_string().is_empty());
477
478        // Verify settings
479        let transaction = users_db.new_transaction().await.unwrap();
480        let doc_store = transaction
481            .get_store::<DocStore>("_settings")
482            .await
483            .unwrap();
484        let name = doc_store.get_string("name").await.unwrap();
485        assert_eq!(name, USERS);
486    }
487
488    #[tokio::test]
489    async fn test_create_databases_tracking() {
490        let (instance, device_key) = setup_instance().await;
491
492        let databases_db = create_databases_tracking(&instance, &device_key)
493            .await
494            .unwrap();
495
496        // Verify database was created
497        assert!(!databases_db.root_id().to_string().is_empty());
498
499        // Verify settings
500        let transaction = databases_db.new_transaction().await.unwrap();
501        let doc_store = transaction
502            .get_store::<DocStore>("_settings")
503            .await
504            .unwrap();
505        let name = doc_store.get_string("name").await.unwrap();
506        assert_eq!(name, DATABASES);
507    }
508
509    #[tokio::test]
510    async fn test_system_databases_haveadmin_auth() {
511        let (instance, device_key) = setup_instance().await;
512
513        let users_db = create_users_database(&instance, &device_key).await.unwrap();
514
515        // Verify device key has admin access - key is stored by pubkey
516        let device_pubkey = instance.device_key().public_key();
517        let transaction = users_db.new_transaction().await.unwrap();
518        let settings_store = SettingsStore::new(&transaction).unwrap();
519        let auth_settings = settings_store.auth_snapshot().await.unwrap();
520        let device_key = auth_settings.get_key_by_pubkey(&device_pubkey).unwrap();
521
522        assert_eq!(device_key.permissions(), &Permission::Admin(0));
523        assert_eq!(device_key.name(), None);
524    }
525
526    #[tokio::test]
527    #[cfg_attr(miri, ignore)] // Uses Argon2 password hashing and SystemTime
528    async fn test_create_user() {
529        let (instance, device_key) = setup_instance().await;
530        let users_db = create_users_database(&instance, &device_key).await.unwrap();
531
532        // Create a user with password
533        let (user_uuid, user_info) =
534            create_user(&users_db, &instance, "alice", Some("password123"))
535                .await
536                .unwrap();
537
538        // Verify user info
539        assert_eq!(user_info.username, "alice");
540        assert_eq!(user_info.status, UserStatus::Active);
541        assert!(user_info.password_hash.is_some());
542        assert!(user_info.password_salt.is_some());
543        assert!(!user_uuid.is_empty());
544
545        // Verify user was stored in _users database
546        let users_table = users_db
547            .get_store_viewer::<Table<UserInfo>>("users")
548            .await
549            .unwrap();
550        let stored_user = users_table.get(&user_uuid).await.unwrap();
551        assert_eq!(stored_user.username, "alice");
552    }
553
554    #[tokio::test]
555    async fn test_create_user_passwordless() {
556        let (instance, device_key) = setup_instance().await;
557        let users_db = create_users_database(&instance, &device_key).await.unwrap();
558
559        // Create a passwordless user
560        let (user_uuid, user_info) = create_user(&users_db, &instance, "bob", None)
561            .await
562            .unwrap();
563
564        // Verify user info
565        assert_eq!(user_info.username, "bob");
566        assert_eq!(user_info.status, UserStatus::Active);
567        assert!(user_info.password_hash.is_none());
568        assert!(user_info.password_salt.is_none());
569        assert!(!user_uuid.is_empty());
570
571        // Verify user was stored in _users database
572        let users_table = users_db
573            .get_store_viewer::<Table<UserInfo>>("users")
574            .await
575            .unwrap();
576        let stored_user = users_table.get(&user_uuid).await.unwrap();
577        assert_eq!(stored_user.username, "bob");
578    }
579
580    #[tokio::test]
581    #[cfg_attr(miri, ignore)] // Uses Argon2 password hashing and SystemTime
582    async fn test_create_duplicate_user() {
583        let (instance, device_key) = setup_instance().await;
584        let users_db = create_users_database(&instance, &device_key).await.unwrap();
585
586        // Create first user
587        create_user(&users_db, &instance, "alice", Some("password123"))
588            .await
589            .unwrap();
590
591        // Try to create duplicate
592        let result = create_user(&users_db, &instance, "alice", Some("password456")).await;
593        assert!(result.is_err());
594    }
595
596    #[tokio::test]
597    #[cfg_attr(miri, ignore)] // Uses Argon2 password hashing and SystemTime
598    async fn test_login_user() {
599        let (instance, device_key) = setup_instance().await;
600        let users_db = create_users_database(&instance, &device_key).await.unwrap();
601
602        // Create a user with password
603        create_user(&users_db, &instance, "bob", Some("bobpassword"))
604            .await
605            .unwrap();
606
607        // Login user
608        let user = login_user(&users_db, &instance, "bob", Some("bobpassword"))
609            .await
610            .unwrap();
611
612        // Verify user session
613        assert_eq!(user.username(), "bob");
614
615        // Verify last_login was recorded in separate table
616        let last_login_table = users_db
617            .get_store_viewer::<Table<i64>>("last_login")
618            .await
619            .unwrap();
620        let last_login = last_login_table.get(user.user_uuid()).await.unwrap();
621        assert!(last_login > 0);
622    }
623
624    #[tokio::test]
625    async fn test_login_user_passwordless() {
626        let (instance, device_key) = setup_instance().await;
627        let users_db = create_users_database(&instance, &device_key).await.unwrap();
628
629        // Create a passwordless user
630        create_user(&users_db, &instance, "charlie", None)
631            .await
632            .unwrap();
633
634        // Login user without password
635        let user = login_user(&users_db, &instance, "charlie", None)
636            .await
637            .unwrap();
638
639        // Verify user session
640        assert_eq!(user.username(), "charlie");
641
642        // Verify last_login was recorded
643        let last_login_table = users_db
644            .get_store_viewer::<Table<i64>>("last_login")
645            .await
646            .unwrap();
647        let last_login = last_login_table.get(user.user_uuid()).await.unwrap();
648        assert!(last_login > 0);
649    }
650
651    #[tokio::test]
652    #[cfg_attr(miri, ignore)] // Uses Argon2 password hashing and SystemTime
653    async fn test_login_wrong_password() {
654        let (instance, device_key) = setup_instance().await;
655        let users_db = create_users_database(&instance, &device_key).await.unwrap();
656
657        // Create a user
658        create_user(&users_db, &instance, "dave", Some("correct_password"))
659            .await
660            .unwrap();
661
662        // Try to login with wrong password
663        let result = login_user(&users_db, &instance, "dave", Some("wrong_password")).await;
664        assert!(result.is_err());
665    }
666
667    #[tokio::test]
668    #[cfg_attr(miri, ignore)] // Uses Argon2 password hashing and SystemTime
669    async fn test_login_password_mismatch() {
670        let (instance, device_key) = setup_instance().await;
671        let users_db = create_users_database(&instance, &device_key).await.unwrap();
672
673        // Create a passwordless user
674        create_user(&users_db, &instance, "eve", None)
675            .await
676            .unwrap();
677
678        // Try to login with password (should fail)
679        let result = login_user(&users_db, &instance, "eve", Some("password")).await;
680        assert!(result.is_err());
681
682        // Create a password-protected user
683        create_user(&users_db, &instance, "frank", Some("password"))
684            .await
685            .unwrap();
686
687        // Try to login without password (should fail)
688        let result = login_user(&users_db, &instance, "frank", None).await;
689        assert!(result.is_err());
690    }
691
692    #[tokio::test]
693    async fn test_login_nonexistent_user() {
694        let (instance, device_key) = setup_instance().await;
695        let users_db = create_users_database(&instance, &device_key).await.unwrap();
696
697        // Try to login user that doesn't exist
698        let result = login_user(&users_db, &instance, "nonexistent", Some("password")).await;
699        assert!(result.is_err());
700    }
701
702    #[tokio::test]
703    #[cfg_attr(miri, ignore)] // Uses Argon2 password hashing and SystemTime
704    async fn test_list_users() {
705        let (instance, device_key) = setup_instance().await;
706        let users_db = create_users_database(&instance, &device_key).await.unwrap();
707
708        // Initially no users
709        let users = list_users(&users_db).await.unwrap();
710        assert_eq!(users.len(), 0);
711
712        // Create some users (mix of password-protected and passwordless)
713        create_user(&users_db, &instance, "alice", Some("pass1"))
714            .await
715            .unwrap();
716        create_user(&users_db, &instance, "bob", None)
717            .await
718            .unwrap();
719        create_user(&users_db, &instance, "charlie", Some("pass3"))
720            .await
721            .unwrap();
722
723        // List users
724        let users = list_users(&users_db).await.unwrap();
725        assert_eq!(users.len(), 3);
726        assert!(users.contains(&"alice".into()));
727        assert!(users.contains(&"bob".into()));
728        assert!(users.contains(&"charlie".into()));
729    }
730}