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

eidetica/account/
system_databases.rs

1//! System database initialization for the account system
2//!
3//! Creates system Auth databases and native installation login records.
4
5use handle_trait::Handle;
6
7use super::{
8    AccountSession,
9    errors::AccountError,
10    key_manager::AccountKeyManager,
11    types::{AccountCredentials, AccountInfo, AccountKey, AccountStatus, KeyStorage},
12};
13use crate::{
14    Database, Instance, Result,
15    auth::{
16        crypto::{PrivateKey, PublicKey, generate_keypair},
17        types::{AuthKey, Permission},
18    },
19    constants::{DATABASES, INSTANCE, USERS},
20    crdt::Doc,
21};
22
23/// Whether `_users.auth_settings` already lists an instance admin.
24///
25/// "Instance admin" here means any non-device-key entry with `Admin` permission
26/// in `_users`'s auth settings. The device key is `Admin(0)` on every system
27/// DB by construction (it bootstrapped them), so we exclude it explicitly —
28/// otherwise every fresh instance would look like it already had an admin.
29pub(crate) async fn has_instance_admin(
30    users_db: &Database,
31    device_pubkey: &PublicKey,
32) -> Result<bool> {
33    let tx = users_db.new_transaction().await?;
34    let settings = tx.get_settings()?;
35    let auth = settings.auth_snapshot().await?;
36    let device_pubkey_str = device_pubkey.to_string();
37    for (pubkey_str, key) in auth.get_all_keys()? {
38        if pubkey_str == device_pubkey_str {
39            continue;
40        }
41        if matches!(key.permissions(), Permission::Admin(_)) {
42            return Ok(true);
43        }
44    }
45    Ok(false)
46}
47
48/// Add `pubkey` as `Admin(0)` to every system database that gates instance-level
49/// admin operations.
50///
51/// Today that's `_users` (account directory + auth bootstrap) and `_databases`
52/// (instance-wide database registry). `_sync` is lazily created on
53/// `enable_sync()` and out of scope here.
54///
55/// The Database handles passed in must already be opened with a signing key
56/// that holds `Admin` on each respective DB. The first-admin bootstrap path
57/// (`create_account` below) passes device-keyed handles — `Database::create`
58/// registered the device key as `Admin(0)` on each system DB. The admin
59/// promotion path (`InstanceAdmin::grant_instance_admin`) passes handles keyed by an
60/// existing admin's own key; the same write then resolves against that
61/// admin's identity.
62pub(crate) async fn grant_admin_on_system_dbs(
63    users_db: &Database,
64    databases_db: &Database,
65    pubkey: &PublicKey,
66) -> Result<()> {
67    for database in [users_db, databases_db] {
68        database
69            .with_transaction(|tx| async move {
70                let settings = tx.get_settings()?;
71                settings
72                    .set_auth_key(pubkey, AuthKey::active(Some("admin"), Permission::Admin(0)))
73                    .await
74            })
75            .await?;
76    }
77    Ok(())
78}
79
80/// Create the _instance system database
81///
82/// This database stores Instance-level configuration and metadata.
83/// Auth is bootstrapped by `Database::create` with the device key as Admin(0).
84///
85/// # Arguments
86/// * `instance` - The Instance handle
87/// * `device_signing_key` - The device's Ed25519 signing key
88///
89/// # Returns
90/// The _instance Database
91pub async fn create_instance_database(
92    instance: &Instance,
93    device_signing_key: &PrivateKey,
94) -> Result<Database> {
95    let mut settings = Doc::new();
96    settings.set("name", INSTANCE);
97    settings.set("type", "system");
98    settings.set("description", "Instance configuration and management");
99
100    Database::create(instance, device_signing_key.clone(), settings).await
101}
102
103/// Create the _users system database
104///
105/// This database gates instance-admin capabilities. Login credentials are native local records.
106/// Auth is bootstrapped by `Database::create` with the device key as Admin(0).
107///
108/// # Arguments
109/// * `instance` - The Instance handle
110/// * `device_signing_key` - The device's Ed25519 signing key
111///
112/// # Returns
113/// The created _users Database
114pub async fn create_accounts_database(
115    instance: &Instance,
116    device_signing_key: &PrivateKey,
117) -> Result<Database> {
118    let mut settings = Doc::new();
119    settings.set("name", USERS);
120    settings.set("type", "system");
121    settings.set("description", "Instance account administration");
122
123    Database::create(instance, device_signing_key.clone(), settings).await
124}
125
126/// Create the _databases tracking database
127///
128/// This database stores the database tracking information mapping
129/// database_id -> DatabaseTracking.
130/// Auth is bootstrapped by `Database::create` with the device key as Admin(0).
131///
132/// # Arguments
133/// * `instance` - The Instance handle
134/// * `device_signing_key` - The device's Ed25519 signing key
135///
136/// # Returns
137/// The created _databases Database
138pub async fn create_databases_tracking(
139    instance: &Instance,
140    device_signing_key: &PrivateKey,
141) -> Result<Database> {
142    let mut settings = Doc::new();
143    settings.set("name", DATABASES);
144    settings.set("type", "system");
145    settings.set("description", "Database tracking and registry");
146
147    Database::create(instance, device_signing_key.clone(), settings).await
148}
149
150/// Create a new account
151///
152/// This function:
153/// 1. Optionally hashes the account's password (if provided)
154/// 2. Generates a device keypair for the account
155/// 3. Creates the portable Account database (no private keys)
156/// 4. Registers AccountInfo in native local storage with a login UUID
157/// 5. **First-admin bootstrap**: if no instance admin exists yet in `_users.auth_settings`,
158///    promotes this account by adding their pubkey as `Admin(0)` to the
159///    instance-admin system DBs (`_users` and `_databases`). Subsequent
160///    accounts land as non-admins.
161///
162/// # Arguments
163/// * `users_db` - The _users system database
164/// * `instance` - The Instance handle
165/// * `username` - Unique username for login
166/// * `password` - Password string, including empty; all logins use encrypted bundles.
167///
168/// # Returns
169/// A tuple of `(login_id, AccountInfo, root_private_key)`. The private key is
170/// the one just generated for this account; callers (e.g.
171/// [`Instance::create_backend`](crate::Instance::create_backend) or
172/// [`InstanceAdmin::create_account`](crate::account::InstanceAdmin::create_account))
173/// can hand it straight to [`build_account_session`] to materialise a logged-in
174/// [`AccountSession`](crate::account::AccountSession) without re-decrypting the key from storage.
175pub async fn create_account(
176    users_db: &Database,
177    instance: &Instance,
178    username: impl AsRef<str>,
179    password: &str,
180) -> Result<(String, AccountInfo, crate::auth::crypto::PrivateKey)> {
181    let username = username.as_ref();
182    // Names are unique within this installation. The native insertion below
183    // is atomic; no credential row is ever written into _users or AccountDB.
184    if super::local_logins::list(users_db)
185        .await?
186        .iter()
187        .any(|name| name == username)
188    {
189        return Err(AccountError::LoginNameAlreadyExists {
190            username: username.to_owned(),
191        }
192        .into());
193    }
194
195    // 1. Generate default keypair for this account
196    let (user_private_key, user_public_key) = generate_keypair();
197    // Return an independent signer clone; persisted credentials are encrypted.
198    let returned_private_key = user_private_key.clone();
199
200    // 2. Create account database with the account's key as owner (Admin(0))
201    let mut user_db_settings = Doc::new();
202    user_db_settings.set("name", format!("_user_{username}"));
203    user_db_settings.set("type", "user");
204    user_db_settings.set("description", format!("Account database for {username}"));
205
206    let account_database =
207        Database::create(instance, user_private_key.clone(), user_db_settings).await?;
208    let account_id = account_database.root_id().clone();
209
210    // First login grants the existing daemon device Read access to AccountDB.
211    // It also initializes native key metadata from this local root credential;
212    // neither root nor additional private keys are authored into AccountDB.
213    let device_pubkey = instance.id();
214
215    // One authenticated locator/key payload, shared with caller-held bundles.
216    let credentials = AccountCredentials {
217        root_key_id: user_public_key.clone(),
218        bundle: KeyStorage::seal(&account_id, &[user_private_key], password)?,
219    };
220
221    // 4. Create AccountInfo
222    let account_info = AccountInfo {
223        username: username.to_string(),
224        account_id,
225        credentials,
226        created_at: instance.clock().now_secs(),
227        status: AccountStatus::Active,
228    };
229
230    let login_id = uuid::Uuid::new_v4().to_string();
231    super::local_logins::register(instance, &login_id, &account_info).await?;
232
233    // 6. First-admin bootstrap: if no instance admin exists yet, promote this
234    // account. Registration and genesis are already durable if this later step
235    // fails; callers must reconcile that partial result. The write uses the device
236    // key (the only Admin on the system DBs at bootstrap time); opening
237    // `databases_db()` here mirrors the `users_db` argument passed in and
238    // keeps the device-key signing detail inside `Instance`.
239    if !has_instance_admin(users_db, &device_pubkey).await? {
240        let databases_db = instance.databases_db().await?;
241        grant_admin_on_system_dbs(users_db, &databases_db, &user_public_key).await?;
242    }
243
244    Ok((login_id, account_info, returned_private_key))
245}
246
247/// Login an account
248///
249/// This function:
250/// 1. Finds the installation-local login record by name
251/// 2. Unlocks the authenticated credential bundle with the password
252/// 3. Opens account's private database
253/// 4. Loads and decrypts the complete account credential bundle
254/// 5. Creates AccountKeyManager with keys
255/// 6. Returns AccountSession object
256///
257/// # Arguments
258/// * `users_db` - The _users system database
259/// * `instance` - The Instance handle
260/// * `username` - Username for login
261/// * `password` - Password string; use an empty string for passwordless accounts.
262///
263/// # Returns
264/// An AccountSession object with keys loaded
265pub async fn login_account(
266    _users_db: &Database,
267    instance: &Instance,
268    username: impl AsRef<str>,
269    password: &str,
270) -> Result<super::AccountSession> {
271    let username = username.as_ref();
272
273    let (login_id, account_info) = super::local_logins::lookup(instance, username).await?;
274
275    let unlocked = account_info
276        .credentials
277        .unlock(&account_info.account_id, password)?;
278
279    let user = build_account_session(instance, &login_id, &account_info, unlocked).await?;
280
281    Ok(user)
282}
283
284/// Build a registered session from its record and client-unlocked login anchor.
285/// The guarded wrapping key unlocks the current complete native bundle without
286/// retaining the password or introducing a second per-key encryption format.
287///
288/// Shared by local login and service login after client-side root unlock.
289/// Additional keys/mappings are read only from this installation's native record.
290/// The private Account database supplies references and Auth, never credentials.
291pub(crate) async fn build_account_session(
292    instance: &Instance,
293    login_id: &str,
294    account_info: &AccountInfo,
295    unlocked: super::UnlockedCredentials,
296) -> Result<super::AccountSession> {
297    // Match the authenticated payload to the directory locator before using it.
298    if unlocked.account_id() != &account_info.account_id {
299        return Err(AccountError::InvalidLocalState.into());
300    }
301    let root_signing_key = unlocked.signer(&account_info.credentials.root_key_id)?;
302    let account_database =
303        super::AccountSigner::new(account_info.account_id.clone(), root_signing_key)
304            .open_account(instance)
305            .await?
306            .database()
307            .clone();
308
309    // The installation's native record is the only key/mapping store. Never
310    // import historical Account keys: old histories remain untouched, not safe.
311    let mut key_manager = None;
312    super::local_state::update(instance, login_id, |state| {
313        if state.credentials.is_none() {
314            state.credentials = Some(account_info.credentials.bundle.clone());
315        }
316        state
317            .keys
318            .entry(account_info.credentials.root_key_id.to_string())
319            .or_insert_with(|| AccountKey {
320                key_id: account_info.credentials.root_key_id.clone(),
321                display_name: Some("Root Key".to_string()),
322                created_at: account_info.created_at,
323                last_used: None,
324                is_default: true,
325                database_sigkeys: std::collections::HashMap::new(),
326            });
327        let current = unlocked.unlock_storage(
328            state
329                .credentials
330                .as_ref()
331                .ok_or(AccountError::InvalidLocalState)?,
332        )?;
333        current.signer(&account_info.credentials.root_key_id)?;
334        key_manager = Some(AccountKeyManager::new(
335            current,
336            state.keys.values().cloned().collect(),
337        )?);
338        state.last_login = Some(instance.clock().now_secs());
339        Ok(())
340    })
341    .await?;
342    let key_manager = key_manager.ok_or(AccountError::InvalidLocalState)?;
343
344    // 3. First-login (and recovery) bootstrap on the account's own tree.
345    // Idempotent: each check short-circuits on subsequent logins. Best
346    // effort — failure here is logged and the login proceeds, because the
347    // in-memory `key_manager` already has the root key and the next login
348    // will retry on the same conditions.
349    if let Err(e) = bootstrap_account_tree_if_needed(instance, &account_database).await {
350        tracing::debug!(
351            user = %account_info.username,
352            error = %e,
353            "user-tree first-login bootstrap skipped"
354        );
355    }
356
357    Ok(AccountSession::new(
358        login_id.to_string(),
359        account_info.clone(),
360        account_database,
361        instance.handle(),
362        key_manager,
363    ))
364}
365
366/// Preserve the existing device-Read grant for daemon replication. This is
367/// public Auth, not credential storage or a redesign of background authority.
368async fn bootstrap_account_tree_if_needed(
369    instance: &Instance,
370    account_database: &Database,
371) -> Result<()> {
372    let device_pubkey = instance.id();
373    if account_database
374        .get_settings()
375        .await?
376        .get_auth_key(&device_pubkey)
377        .await
378        .is_ok()
379    {
380        return Ok(());
381    }
382    account_database
383        .with_transaction(|tx| async move {
384            tx.get_settings()?
385                .set_auth_key(
386                    &device_pubkey,
387                    AuthKey::active(Some("device"), Permission::Read),
388                )
389                .await
390        })
391        .await?;
392    Ok(())
393}
394
395/// Look up an account's record by username, without requiring the password.
396///
397/// Used by the service daemon's challenge-response login flow: the daemon
398/// fetches the account's full `AccountInfo` (including encrypted credentials and the
399/// account's private-database id) and ships it to the client so the client can
400/// derive the KEK locally, decrypt the root key, sign the challenge, and then
401/// load the current encrypted bundle through exact-login-scoped native state.
402/// No credential read of `_users` is required. The encrypted blob is designed to survive at rest; shipping it
403/// over the local socket is the same trust boundary as filesystem read. See
404/// the Service Architecture doc § Trusted login threat model for the full
405/// rationale.
406///
407/// # Returns
408/// Tuple of `(login_id, AccountInfo)` if the account exists and is active.
409pub async fn lookup_account_record(
410    users_db: &Database,
411    username: impl AsRef<str>,
412) -> Result<(String, AccountInfo)> {
413    super::local_logins::lookup(&users_db.instance()?, username.as_ref()).await
414}
415
416/// List all accounts in the system
417///
418/// # Arguments
419/// * `users_db` - The _users system database
420///
421/// # Returns
422/// Vector of usernames
423pub async fn list_accounts(users_db: &Database) -> Result<Vec<String>> {
424    super::local_logins::list(users_db).await
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430    use crate::Instance;
431    use crate::backend::database::InMemory;
432    use crate::store::DocStore;
433    use crate::store::SettingsStore;
434
435    use std::sync::Arc;
436
437    /// Test helper: Create Instance with `admin` as the initial account and a
438    /// pinned clock, returning the instance, its device signing key, and the
439    /// just-bootstrapped admin AccountSession.
440    ///
441    /// Tests that only need the device key destructure `(instance,
442    /// device_key, _admin)`; tests that need to drive admin operations
443    /// destructure `(instance, _device_key, admin)`. Uses [`FixedClock`] for
444    /// reproducible timestamps.
445    async fn setup_instance() -> (Instance, PrivateKey, crate::account::AccountSession) {
446        use crate::clock::FixedClock;
447
448        let (instance, admin) = Instance::create_backend_with_clock(
449            Box::new(InMemory::new()),
450            Arc::new(FixedClock::default()),
451            crate::NewAccount::passwordless("admin"),
452        )
453        .await
454        .unwrap();
455
456        // Get the device key from the instance
457        let device_key = instance.signing_key().unwrap().clone();
458
459        (instance, device_key, admin)
460    }
461
462    #[tokio::test]
463    async fn test_create_instance_database() {
464        let (instance, device_key, _admin) = setup_instance().await;
465
466        let instance_db = create_instance_database(&instance, &device_key)
467            .await
468            .unwrap();
469
470        // Verify database was created
471        assert!(!instance_db.root_id().to_string().is_empty());
472
473        // Verify settings
474        let transaction = instance_db.new_transaction().await.unwrap();
475        let doc_store = transaction
476            .get_store::<DocStore>("_settings")
477            .await
478            .unwrap();
479        let name = doc_store.get_string("name").await.unwrap();
480        assert_eq!(name, INSTANCE);
481
482        // Verify auth settings - key is stored by pubkey
483        let device_pubkey = instance.id();
484        let settings_store = SettingsStore::new(&transaction).unwrap();
485        let auth_settings = settings_store.auth_snapshot().await.unwrap();
486        let device_key = auth_settings.get_key_by_pubkey(&device_pubkey).unwrap();
487        assert_eq!(device_key.permissions(), &Permission::Admin(0));
488        assert_eq!(device_key.name(), None);
489    }
490
491    #[tokio::test]
492    async fn test_create_users_database() {
493        let (instance, device_key, _admin) = setup_instance().await;
494
495        let users_db = create_accounts_database(&instance, &device_key)
496            .await
497            .unwrap();
498
499        // Verify database was created
500        assert!(!users_db.root_id().to_string().is_empty());
501
502        // Verify settings
503        let transaction = users_db.new_transaction().await.unwrap();
504        let doc_store = transaction
505            .get_store::<DocStore>("_settings")
506            .await
507            .unwrap();
508        let name = doc_store.get_string("name").await.unwrap();
509        assert_eq!(name, USERS);
510    }
511
512    #[tokio::test]
513    async fn test_create_databases_tracking() {
514        let (instance, device_key, _admin) = setup_instance().await;
515
516        let databases_db = create_databases_tracking(&instance, &device_key)
517            .await
518            .unwrap();
519
520        // Verify database was created
521        assert!(!databases_db.root_id().to_string().is_empty());
522
523        // Verify settings
524        let transaction = databases_db.new_transaction().await.unwrap();
525        let doc_store = transaction
526            .get_store::<DocStore>("_settings")
527            .await
528            .unwrap();
529        let name = doc_store.get_string("name").await.unwrap();
530        assert_eq!(name, DATABASES);
531    }
532
533    #[tokio::test]
534    async fn test_system_databases_haveadmin_auth() {
535        let (instance, device_key, _admin) = setup_instance().await;
536
537        let users_db = create_accounts_database(&instance, &device_key)
538            .await
539            .unwrap();
540
541        // Verify device key has admin access - key is stored by pubkey
542        let device_pubkey = instance.id();
543        let transaction = users_db.new_transaction().await.unwrap();
544        let settings_store = SettingsStore::new(&transaction).unwrap();
545        let auth_settings = settings_store.auth_snapshot().await.unwrap();
546        let device_key = auth_settings.get_key_by_pubkey(&device_pubkey).unwrap();
547
548        assert_eq!(device_key.permissions(), &Permission::Admin(0));
549        assert_eq!(device_key.name(), None);
550    }
551
552    #[tokio::test]
553    #[cfg_attr(miri, ignore)] // Uses Argon2 password hashing and SystemTime
554    async fn test_create_user() {
555        let (instance, device_key, _admin) = setup_instance().await;
556        let users_db = create_accounts_database(&instance, &device_key)
557            .await
558            .unwrap();
559
560        // Create an account with password
561        let (login_id, account_info, _) =
562            create_account(&users_db, &instance, "alice", "password123")
563                .await
564                .unwrap();
565
566        // Verify account info
567        assert_eq!(account_info.username, "alice");
568        assert_eq!(account_info.status, AccountStatus::Active);
569
570        assert!(matches!(
571            account_info.credentials.bundle,
572            KeyStorage::Encrypted { .. }
573        ));
574        assert!(!login_id.is_empty());
575
576        // Verify native registration is durable
577        let (stored_id, stored_user) = lookup_account_record(&users_db, &account_info.username)
578            .await
579            .unwrap();
580        assert_eq!(stored_id, login_id);
581        assert_eq!(stored_user.username, "alice");
582    }
583
584    #[tokio::test]
585    async fn test_create_user_passwordless() {
586        let (instance, device_key, _admin) = setup_instance().await;
587        let users_db = create_accounts_database(&instance, &device_key)
588            .await
589            .unwrap();
590
591        // Create a passwordless account
592        let (login_id, account_info, _) = create_account(&users_db, &instance, "bob", "")
593            .await
594            .unwrap();
595
596        // Verify account info
597        assert_eq!(account_info.username, "bob");
598        assert_eq!(account_info.status, AccountStatus::Active);
599
600        assert!(matches!(
601            account_info.credentials.bundle,
602            KeyStorage::Encrypted { .. }
603        ));
604        assert!(!login_id.is_empty());
605
606        // Verify native registration is durable
607        let (stored_id, stored_user) = lookup_account_record(&users_db, &account_info.username)
608            .await
609            .unwrap();
610        assert_eq!(stored_id, login_id);
611        assert_eq!(stored_user.username, "bob");
612    }
613
614    #[tokio::test]
615    #[cfg_attr(miri, ignore)] // Uses Argon2 password hashing and SystemTime
616    async fn test_create_duplicate_user() {
617        let (instance, device_key, _admin) = setup_instance().await;
618        let users_db = create_accounts_database(&instance, &device_key)
619            .await
620            .unwrap();
621
622        // Create first account
623        create_account(&users_db, &instance, "alice", "password123")
624            .await
625            .unwrap();
626
627        // Try to create duplicate
628        let result = create_account(&users_db, &instance, "alice", "password456").await;
629        assert!(result.is_err());
630    }
631
632    #[tokio::test]
633    #[cfg_attr(miri, ignore)] // Uses Argon2 password hashing and SystemTime
634    async fn test_login_user() {
635        let (instance, device_key, _admin) = setup_instance().await;
636        let users_db = create_accounts_database(&instance, &device_key)
637            .await
638            .unwrap();
639
640        // Create an account with password
641        create_account(&users_db, &instance, "bob", "bobpassword")
642            .await
643            .unwrap();
644
645        // Login account
646        let user = login_account(&users_db, &instance, "bob", "bobpassword")
647            .await
648            .unwrap();
649
650        // Verify account session
651        assert_eq!(user.username(), "bob");
652
653        // Verify native last_login was recorded in native local state
654        let last_login = super::super::local_state::read(&instance, user.login_id())
655            .await
656            .unwrap()
657            .last_login
658            .unwrap();
659        assert!(last_login > 0);
660    }
661
662    #[tokio::test]
663    async fn test_login_user_passwordless() {
664        let (instance, device_key, _admin) = setup_instance().await;
665        let users_db = create_accounts_database(&instance, &device_key)
666            .await
667            .unwrap();
668
669        // Create a passwordless account
670        create_account(&users_db, &instance, "charlie", "")
671            .await
672            .unwrap();
673
674        // Login account with its empty password
675        let user = login_account(&users_db, &instance, "charlie", "")
676            .await
677            .unwrap();
678
679        // Verify account session
680        assert_eq!(user.username(), "charlie");
681
682        // Verify native last_login was recorded
683        let last_login = super::super::local_state::read(&instance, user.login_id())
684            .await
685            .unwrap()
686            .last_login
687            .unwrap();
688        assert!(last_login > 0);
689    }
690
691    #[tokio::test]
692    #[cfg_attr(miri, ignore)] // Uses Argon2 password hashing and SystemTime
693    async fn test_login_wrong_password() {
694        let (instance, device_key, _admin) = setup_instance().await;
695        let users_db = create_accounts_database(&instance, &device_key)
696            .await
697            .unwrap();
698
699        // Create an account
700        create_account(&users_db, &instance, "dave", "correct_password")
701            .await
702            .unwrap();
703
704        // Try to login with wrong password
705        let result = login_account(&users_db, &instance, "dave", "wrong_password").await;
706        assert!(result.is_err());
707    }
708
709    #[tokio::test]
710    #[cfg_attr(miri, ignore)] // Uses Argon2 password hashing and SystemTime
711    async fn test_login_password_mismatch() {
712        let (instance, device_key, _admin) = setup_instance().await;
713        let users_db = create_accounts_database(&instance, &device_key)
714            .await
715            .unwrap();
716
717        // Create a passwordless account
718        create_account(&users_db, &instance, "eve", "")
719            .await
720            .unwrap();
721
722        // Try to login with password (should fail)
723        let result = login_account(&users_db, &instance, "eve", "password").await;
724        assert!(result.is_err());
725
726        // Create a password-protected account
727        create_account(&users_db, &instance, "frank", "password")
728            .await
729            .unwrap();
730
731        // Try to login with an empty password (should fail)
732        let result = login_account(&users_db, &instance, "frank", "").await;
733        assert!(result.is_err());
734    }
735
736    #[tokio::test]
737    async fn test_login_nonexistent_user() {
738        let (instance, device_key, _admin) = setup_instance().await;
739        let users_db = create_accounts_database(&instance, &device_key)
740            .await
741            .unwrap();
742
743        // Try to login account that doesn't exist
744        let result = login_account(&users_db, &instance, "nonexistent", "password").await;
745        assert!(result.is_err());
746    }
747
748    #[tokio::test]
749    #[cfg_attr(miri, ignore)] // Uses Argon2 password hashing and SystemTime
750    async fn test_list_users() {
751        let (instance, device_key, _admin) = setup_instance().await;
752        let users_db = create_accounts_database(&instance, &device_key)
753            .await
754            .unwrap();
755
756        // The installation already has its bootstrap admin.
757        let users = list_accounts(&users_db).await.unwrap();
758        assert_eq!(users, ["admin"]);
759
760        // Create some accounts (mix of password-protected and passwordless)
761        create_account(&users_db, &instance, "alice", "pass1")
762            .await
763            .unwrap();
764        create_account(&users_db, &instance, "bob", "")
765            .await
766            .unwrap();
767        create_account(&users_db, &instance, "charlie", "pass3")
768            .await
769            .unwrap();
770
771        // List accounts
772        let users = list_accounts(&users_db).await.unwrap();
773        assert_eq!(users.len(), 4);
774        assert!(users.contains(&"alice".into()));
775        assert!(users.contains(&"bob".into()));
776        assert!(users.contains(&"charlie".into()));
777    }
778
779    /// Read the auth permission for `pubkey` from a system database. Returns
780    /// `None` if the key isn't registered. Test-only helper.
781    async fn read_admin_permission(database: &Database, pubkey: &PublicKey) -> Option<Permission> {
782        let tx = database.new_transaction().await.unwrap();
783        let settings = SettingsStore::new(&tx).unwrap();
784        let auth = settings.auth_snapshot().await.unwrap();
785        auth.get_key_by_pubkey(pubkey)
786            .ok()
787            .map(|k| *k.permissions())
788    }
789
790    /// The bootstrapped admin/admin account is the instance admin: its pubkey
791    /// lands as `Admin(0)` in both `_users` and `_databases`.
792    #[tokio::test]
793    async fn test_first_user_becomes_instance_admin() {
794        let (instance, _device_key, _admin) = setup_instance().await;
795
796        // Admin is already bootstrapped — login and verify
797        let admin_user = instance.login_account("admin", "").await.unwrap();
798
799        let users_db = instance.users_db().await.unwrap();
800        let databases_db = instance.databases_db().await.unwrap();
801
802        let admin_pubkey = admin_user.key_manager().get_default_key_id().unwrap();
803
804        assert_eq!(
805            read_admin_permission(&users_db, &admin_pubkey).await,
806            Some(Permission::Admin(0)),
807            "bootstrapped admin should be Admin(0) in _users"
808        );
809        assert_eq!(
810            read_admin_permission(&databases_db, &admin_pubkey).await,
811            Some(Permission::Admin(0)),
812            "bootstrapped admin should be Admin(0) in _databases"
813        );
814    }
815
816    /// After the first account has bootstrapped, subsequent accounts land as
817    /// non-admins. This test locks in that the auto-bootstrap doesn't fire a
818    /// second time.
819    #[tokio::test]
820    async fn test_subsequent_users_are_not_admin() {
821        let (instance, _device_key, admin) = setup_instance().await;
822
823        // Admin creates two more accounts via the admin path. They land as
824        // non-admins because admin (the first account) already holds Admin(0)
825        // on the system DBs.
826        let admin_view = admin.admin().await.unwrap();
827        admin_view
828            .create_account(crate::NewAccount::passwordless("alice"))
829            .await
830            .unwrap();
831        let bob_uuid = admin_view
832            .create_account(crate::NewAccount::passwordless("bob"))
833            .await
834            .unwrap();
835
836        let users_db = instance.users_db().await.unwrap();
837        let databases_db = instance.databases_db().await.unwrap();
838
839        // Bob's root pubkey lives in `AccountInfo.credentials.root_key_id` —
840        // the native durable source of truth, even before first login.
841        let (stored_id, bob_info) = lookup_account_record(&users_db, "bob").await.unwrap();
842        assert_eq!(stored_id, bob_uuid);
843        let bob_pubkey = bob_info.credentials.root_key_id.clone();
844
845        assert!(
846            read_admin_permission(&users_db, &bob_pubkey)
847                .await
848                .is_none(),
849            "second user must not have any entry in _users.auth_settings"
850        );
851        assert!(
852            read_admin_permission(&databases_db, &bob_pubkey)
853                .await
854                .is_none(),
855            "second user must not have any entry in _databases.auth_settings"
856        );
857    }
858
859    /// `AccountSession::is_admin()` query: the bootstrapped admin is admin, newly created
860    /// accounts are not.
861    #[tokio::test]
862    async fn test_user_is_admin_query() {
863        let (instance, _device_key, admin) = setup_instance().await;
864
865        // Bootstrapped admin reports is_admin = true
866        assert!(
867            admin.is_admin().await.unwrap(),
868            "bootstrapped admin must report is_admin = true"
869        );
870
871        // A newly created account reports is_admin = false
872        admin
873            .admin()
874            .await
875            .unwrap()
876            .create_account(crate::NewAccount::passwordless("alice"))
877            .await
878            .unwrap();
879        let alice = instance.login_account("alice", "").await.unwrap();
880        assert!(
881            !alice.is_admin().await.unwrap(),
882            "newly created user must report is_admin = false"
883        );
884    }
885
886    /// The bootstrapped admin can promote another account: the new admin's pubkey
887    /// lands as `Admin(0)` on both `_users` and `_databases`, and the
888    /// promoted account then reports `is_admin()`.
889    #[tokio::test]
890    async fn test_admin_can_promote_user() {
891        let (instance, _device_key, admin) = setup_instance().await;
892
893        admin
894            .admin()
895            .await
896            .unwrap()
897            .create_account(crate::NewAccount::passwordless("bob"))
898            .await
899            .unwrap();
900        let bob = instance.login_account("bob", "").await.unwrap();
901        let bob_pubkey = bob.key_manager().get_default_key_id().unwrap();
902
903        assert!(
904            !bob.is_admin().await.unwrap(),
905            "precondition: bob starts as a non-admin"
906        );
907
908        // Admin promotes bob
909        admin
910            .admin()
911            .await
912            .unwrap()
913            .grant_instance_admin(&bob_pubkey)
914            .await
915            .unwrap();
916
917        let users_db = instance.users_db().await.unwrap();
918        let databases_db = instance.databases_db().await.unwrap();
919        assert_eq!(
920            read_admin_permission(&users_db, &bob_pubkey).await,
921            Some(Permission::Admin(0)),
922            "promoted user should be Admin(0) in _users"
923        );
924        assert_eq!(
925            read_admin_permission(&databases_db, &bob_pubkey).await,
926            Some(Permission::Admin(0)),
927            "promoted user should be Admin(0) in _databases"
928        );
929        assert!(
930            bob.is_admin().await.unwrap(),
931            "promoted user must now report is_admin = true"
932        );
933    }
934
935    /// A non-admin cannot promote anyone: the attempt fails with
936    /// `InsufficientPermissions` and writes nothing.
937    #[tokio::test]
938    async fn test_non_admin_cannot_promote() {
939        let (instance, _device_key, admin) = setup_instance().await;
940
941        // The bootstrapped `admin` is the first/only admin. Admin creates
942        // bob and charlie as non-admin accounts.
943        let admin_view = admin.admin().await.unwrap();
944        admin_view
945            .create_account(crate::NewAccount::passwordless("bob"))
946            .await
947            .unwrap();
948        admin_view
949            .create_account(crate::NewAccount::passwordless("charlie"))
950            .await
951            .unwrap();
952        let bob = instance.login_account("bob", "").await.unwrap();
953        let charlie = instance.login_account("charlie", "").await.unwrap();
954        let charlie_pubkey = charlie.key_manager().get_default_key_id().unwrap();
955
956        // A non-admin can't even obtain the admin view — the privilege
957        // boundary is enforced at `AccountSession::admin`, before any write.
958        let err = bob
959            .admin()
960            .await
961            .expect_err("non-admin must not be able to promote");
962        assert!(
963            matches!(
964                &err,
965                crate::Error::Account(e)
966                    if matches!(e.as_ref(), crate::account::AccountError::InsufficientPermissions)
967            ),
968            "expected InsufficientPermissions, got: {err:?}"
969        );
970
971        let users_db = instance.users_db().await.unwrap();
972        assert!(
973            read_admin_permission(&users_db, &charlie_pubkey)
974                .await
975                .is_none(),
976            "a failed promotion must not write to _users.auth_settings"
977        );
978    }
979
980    /// Promotion is idempotent, and a freshly-promoted admin can itself
981    /// promote further admins — exercising the admin-keyed write path
982    /// (not the device key) end to end.
983    #[tokio::test]
984    async fn test_grant_instance_admin_idempotent_and_chains() {
985        let (instance, _device_key, admin) = setup_instance().await;
986
987        admin
988            .admin()
989            .await
990            .unwrap()
991            .create_account(crate::NewAccount::passwordless("bob"))
992            .await
993            .unwrap();
994        let bob = instance.login_account("bob", "").await.unwrap();
995        let bob_pubkey = bob.key_manager().get_default_key_id().unwrap();
996
997        admin
998            .admin()
999            .await
1000            .unwrap()
1001            .grant_instance_admin(&bob_pubkey)
1002            .await
1003            .unwrap();
1004        admin
1005            .admin()
1006            .await
1007            .unwrap()
1008            .grant_instance_admin(&bob_pubkey)
1009            .await
1010            .unwrap(); // idempotent: no error
1011
1012        let users_db = instance.users_db().await.unwrap();
1013        assert_eq!(
1014            read_admin_permission(&users_db, &bob_pubkey).await,
1015            Some(Permission::Admin(0)),
1016            "re-granting an existing admin re-asserts the same entry"
1017        );
1018
1019        // bob, now an admin, promotes charlie signing with bob's *own* key —
1020        // the device key is never involved here.
1021        bob.admin()
1022            .await
1023            .unwrap()
1024            .create_account(crate::NewAccount::passwordless("charlie"))
1025            .await
1026            .unwrap();
1027        let charlie = instance.login_account("charlie", "").await.unwrap();
1028        let charlie_pubkey = charlie.key_manager().get_default_key_id().unwrap();
1029
1030        bob.admin()
1031            .await
1032            .unwrap()
1033            .grant_instance_admin(&charlie_pubkey)
1034            .await
1035            .unwrap();
1036
1037        let databases_db = instance.databases_db().await.unwrap();
1038        assert_eq!(
1039            read_admin_permission(&databases_db, &charlie_pubkey).await,
1040            Some(Permission::Admin(0)),
1041            "a promoted admin can promote further admins via their own key"
1042        );
1043    }
1044}