eidetica/user/session/mod.rs
1//! User session management
2//!
3//! Represents an authenticated user session with decrypted keys.
4//!
5//! # API Overview
6//!
7//! The User API is organized into three areas for managing Databases:
8//!
9//! ## Database Lifecycle
10//!
11//! - **`create_database()`** - Create a new database
12//! - **`open_database()`** - Open an existing database
13//! - **`open_database_with_key()`** - Open with an explicitly chosen user key
14//! - **`find_database()`** - Search for databases by name
15//!
16//! ## Tracked Databases
17//!
18//! Manage your personal list of tracked databases:
19//!
20//! - **`databases()`** - List all tracked databases
21//! - **`database()`** - Get a specific tracked database
22//! - **`track_database()`** - Add or update a tracked database (upsert)
23//! - **`untrack_database()`** - Remove a database from your tracked list
24//! - **`is_sync_enabled()`** - Check this user's sync preference for a database
25//!
26//! ## Key-Database Mappings
27//!
28//! Control which keys access which databases:
29//!
30//! - **`map_key()`** - Map a key to a SigKey identifier for a database
31//! - **`key_mapping()`** - Get the SigKey mapping for a key-database pair
32//! - **`find_key()`** - Find which key can access a database
33//!
34//! This explicit approach ensures predictable behavior and avoids ambiguity about which
35//! keys have access to which databases.
36
37use std::collections::HashMap;
38
39use std::sync::Arc;
40
41use super::{UserKeyManager, admin::InstanceAdmin, types::UserInfo};
42use crate::{
43 Database, Error, Instance, Result, Transaction,
44 auth::{Permission, SigKey, crypto::PublicKey},
45 crdt::Doc,
46 database::DatabaseKey,
47 entry::ID,
48 instance::{InstanceError, backend::Backend},
49 store::Table,
50 sync::{BootstrapRequest, DatabaseTicket, Sync, SyncError},
51 user::{SyncSettings, TrackedDatabase, UserError},
52};
53
54mod builder;
55#[cfg(test)]
56mod tests;
57
58pub use builder::DatabaseBuilder;
59
60/// User session object, returned after successful login
61///
62/// Represents an authenticated user with decrypted private keys loaded in memory.
63/// The User struct provides access to key management, tracked databases, and
64/// bootstrap approval operations.
65pub struct User {
66 /// Stable internal user UUID (Table primary key)
67 user_uuid: String,
68
69 /// Username (login identifier)
70 username: String,
71
72 /// User's private database (contains encrypted keys and tracked databases)
73 user_database: Database,
74
75 /// Instance reference for database operations
76 instance: Instance,
77
78 /// Decrypted user keys (in memory only during session)
79 key_manager: UserKeyManager,
80
81 /// User info (cached from _users database)
82 user_info: UserInfo,
83}
84
85impl std::fmt::Debug for User {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.debug_struct("User")
88 .field("user_uuid", &self.user_uuid)
89 .field("username", &self.username)
90 .field("user_database", &self.user_database)
91 .field("instance", &self.instance)
92 .field("key_manager", &"<KeyManager [sensitive]>")
93 .field("user_info", &self.user_info)
94 .finish()
95 }
96}
97
98impl User {
99 /// Create a new User session
100 ///
101 /// This is an internal constructor used after successful login.
102 /// Use `Instance::login_user()` to create a User session.
103 ///
104 /// # Arguments
105 /// * `user_uuid` - Internal UUID (Table primary key)
106 /// * `user_info` - User information from _users database
107 /// * `user_database` - The user's private database
108 /// * `instance` - Instance reference
109 /// * `key_manager` - Initialized key manager with decrypted keys
110 #[allow(dead_code)]
111 pub(crate) fn new(
112 user_uuid: String,
113 user_info: UserInfo,
114 user_database: Database,
115 instance: Instance,
116 key_manager: UserKeyManager,
117 ) -> Self {
118 Self {
119 user_uuid,
120 username: user_info.username.clone(),
121 user_database,
122 instance,
123 key_manager,
124 user_info,
125 }
126 }
127
128 // === Basic Session Methods ===
129
130 /// Get the internal user UUID (stable identifier)
131 pub fn user_uuid(&self) -> &str {
132 &self.user_uuid
133 }
134
135 /// Get the username (login identifier)
136 pub fn username(&self) -> &str {
137 &self.username
138 }
139
140 /// Get a reference to the user's database
141 pub fn user_database(&self) -> &Database {
142 &self.user_database
143 }
144
145 /// Get a reference to the backend seam.
146 pub fn backend(&self) -> &Arc<dyn Backend> {
147 self.instance.backend()
148 }
149
150 /// Get a reference to the user info
151 pub fn user_info(&self) -> &UserInfo {
152 &self.user_info
153 }
154
155 /// Whether this user is an instance admin.
156 ///
157 /// "Instance admin" means the user's default pubkey holds `Admin` in the
158 /// `_users` system database's `auth_settings`. The `admin`/`admin` user
159 /// created during instance bootstrap is auto-promoted; subsequent users
160 /// land as non-admins until promoted via
161 /// [`InstanceAdmin::grant_instance_admin`](crate::user::InstanceAdmin::grant_instance_admin).
162 ///
163 /// Returns `false` if the key resolution fails (key not in auth_settings,
164 /// non-Admin permission, etc.). For the capability handle rather than a
165 /// bool, use [`Self::admin`].
166 pub async fn is_admin(&self) -> Result<bool> {
167 self.admin_check().await
168 }
169
170 /// Obtain the instance-admin capability view.
171 ///
172 /// Returns an [`InstanceAdmin`] only if this user's default key holds
173 /// `Admin` on the `_users` system database. All admin-gated operations
174 /// (creating users, listing users, promoting admins) live on the
175 /// returned view, so the privilege boundary is explicit at the call site
176 /// and those operations need no further permission check of their own.
177 ///
178 /// # Errors
179 /// - [`UserError::InsufficientPermissions`] if this user is not an
180 /// instance admin.
181 pub async fn admin(&self) -> Result<InstanceAdmin<'_>> {
182 if self.admin_check().await? {
183 Ok(InstanceAdmin::new(self))
184 } else {
185 Err(UserError::InsufficientPermissions.into())
186 }
187 }
188
189 /// Single source of truth for "is this user an instance admin".
190 ///
191 /// Reads the `_users` `auth_settings` snapshot via the user's **session
192 /// key** (not the device key), so it behaves identically on local and
193 /// remote instances. Returns `Ok(false)` for a non-admin or unresolved
194 /// key; propagates real infrastructure errors. Shared by
195 /// [`Self::is_admin`] and [`Self::admin`].
196 async fn admin_check(&self) -> Result<bool> {
197 let default_pubkey =
198 self.key_manager
199 .get_default_key_id()
200 .ok_or_else(|| UserError::KeyNotFound {
201 key_id: "<default>".to_string(),
202 })?;
203 let signing_key = self.default_signing_key()?;
204 let users_db = self.instance.users_db_for_session(&signing_key).await?;
205 let tx = users_db.new_transaction().await?;
206 let settings = tx.get_settings()?;
207 let auth = settings.auth_snapshot().await?;
208 match auth.get_key_by_pubkey(&default_pubkey) {
209 Ok(key) => Ok(matches!(key.permissions(), Permission::Admin(_))),
210 Err(_) => Ok(false),
211 }
212 }
213
214 /// The user's default signing key (decrypted, in-session).
215 ///
216 /// Shared by admin / system-DB paths that must sign as the user
217 /// directly, rather than via a tracked-database SigKey mapping.
218 pub(crate) fn default_signing_key(&self) -> Result<crate::auth::crypto::PrivateKey> {
219 let key_id =
220 self.key_manager
221 .get_default_key_id()
222 .ok_or_else(|| UserError::KeyNotFound {
223 key_id: "<default>".to_string(),
224 })?;
225 self.key_manager
226 .get_signing_key(&key_id)
227 .cloned()
228 .ok_or_else(|| {
229 UserError::KeyNotFound {
230 key_id: key_id.to_string(),
231 }
232 .into()
233 })
234 }
235
236 /// Instance reference (internal — for admin / system-DB helpers).
237 pub(crate) fn instance(&self) -> &Instance {
238 &self.instance
239 }
240
241 /// Logout (consumes self and clears decrypted keys from memory)
242 ///
243 /// After logout, all decrypted keys are zeroized and the session is ended.
244 /// Keys are automatically cleared when the User is dropped.
245 pub fn logout(self) -> Result<()> {
246 // Consume self, all keys are stored in other Types that zeroize themselves on drop
247 Ok(())
248 }
249
250 // === Key Manager Access (Internal) ===
251
252 /// Get a reference to the key manager (for internal use)
253 #[allow(dead_code)]
254 pub(crate) fn key_manager(&self) -> &UserKeyManager {
255 &self.key_manager
256 }
257
258 /// Get a mutable reference to the key manager (for internal use)
259 #[allow(dead_code)]
260 pub(crate) fn key_manager_mut(&mut self) -> &mut UserKeyManager {
261 &mut self.key_manager
262 }
263
264 // === Database Operations (User Context) ===
265
266 /// Start building a new database via the chainable [`DatabaseBuilder`] API.
267 ///
268 /// The builder collects settings, key policy, and store initializers, then
269 /// produces a fully-initialized database in a single genesis entry when
270 /// [`DatabaseBuilder::build`] is called.
271 ///
272 /// For the lower-level direct constructor see [`Self::create_database`].
273 pub fn new_database(&mut self) -> DatabaseBuilder<'_> {
274 DatabaseBuilder::new(self)
275 }
276
277 /// Create a new database with explicit key selection.
278 ///
279 /// This method requires you to specify which key should be used to create and manage
280 /// the database, providing explicit control over key-database relationships.
281 ///
282 /// # Arguments
283 /// * `settings` - Initial database settings (metadata, name, etc.)
284 /// * `key_id` - The ID of the key to use for this database (public key string)
285 ///
286 /// # Returns
287 /// The created Database
288 ///
289 /// # Errors
290 /// - Returns an error if the specified key_id doesn't exist
291 /// - Returns an error if the key cannot be retrieved
292 ///
293 /// # Example
294 /// ```rust,ignore
295 /// // Get available keys
296 /// let keys = user.list_keys()?;
297 /// let key_id = &keys[1]; // Use the second key
298 ///
299 /// // Create database with explicit key selection
300 /// let mut settings = Doc::new();
301 /// settings.set("name", "My Database");
302 /// let database = user.new_database(settings, key_id)?;
303 /// ```
304 pub async fn create_database(&mut self, settings: Doc, key_id: &PublicKey) -> Result<Database> {
305 self.create_database_with_init(settings, key_id, async |_| Ok(()))
306 .await
307 }
308
309 /// Creates a new database with an initialization callback that runs inside
310 /// the genesis transaction.
311 ///
312 /// This is the underlying constructor used by [`Self::create_database`] and by
313 /// [`Self::new_database`] (the builder API). The callback receives the
314 /// genesis transaction after `_settings` and `_root` have been staged but
315 /// before commit, allowing additional subtrees to be written into the same
316 /// entry that establishes the database root. See
317 /// [`Database::create_with_init`] for details on the atomicity guarantee.
318 ///
319 /// After the genesis entry commits, this method performs the standard
320 /// user-side tracking write (key-database mapping, `TrackedDatabase` entry)
321 /// in a separate transaction on the user's own system database.
322 pub async fn create_database_with_init<F>(
323 &mut self,
324 settings: Doc,
325 key_id: &PublicKey,
326 init: F,
327 ) -> Result<Database>
328 where
329 F: AsyncFnOnce(&Transaction) -> Result<()>,
330 {
331 use crate::user::types::{SyncSettings, UserKey};
332
333 // Get the signing key from UserKeyManager
334 let signing_key = self
335 .key_manager
336 .get_signing_key(key_id)
337 .ok_or_else(|| UserError::KeyNotFound {
338 key_id: key_id.to_string(),
339 })?
340 .clone();
341
342 // Create the database with the provided key directly
343 let database =
344 Database::create_with_init(&self.instance, signing_key, settings, init).await?;
345
346 // Store the mapping in UserKey and track the database
347 let tx = self.user_database.new_transaction().await?;
348 let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
349
350 // Find the key metadata in the database
351 let (uuid_primary_key, mut metadata) = keys_table
352 .search(|uk| &uk.key_id == key_id)
353 .await?
354 .into_iter()
355 .next()
356 .ok_or_else(|| UserError::KeyNotFound {
357 key_id: key_id.to_string(),
358 })?;
359
360 // Add the database sigkey mapping (None = default pubkey identity)
361 metadata
362 .database_sigkeys
363 .insert(database.root_id().clone(), None);
364
365 // Update the key in user database using the UUID primary key
366 keys_table.set(&uuid_primary_key, metadata.clone()).await?;
367
368 // Also track the database in the databases table
369 let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
370 let tracked = TrackedDatabase {
371 database_id: database.root_id().clone(),
372 key_id: key_id.clone(),
373 sync_settings: SyncSettings::disabled(),
374 };
375 databases_table
376 .set(&database.root_id().to_string(), tracked)
377 .await?;
378
379 tx.commit().await?;
380
381 // Update the in-memory key manager with the updated metadata
382 self.key_manager.add_key(metadata)?;
383
384 Ok(database.with_user_database(self.user_database.clone()))
385 }
386
387 /// Open an existing database by its root ID using this user's keys.
388 ///
389 /// This method automatically:
390 /// 1. Finds an appropriate key that has access to the database
391 /// 2. Retrieves the decrypted SigningKey from the UserKeyManager
392 /// 3. Gets the SigKey mapping for this database
393 /// 4. Creates a Database instance configured with the user's key
394 ///
395 /// The returned Database will use the user's provided key for all operations,
396 /// without requiring backend key lookups.
397 ///
398 /// # Arguments
399 /// * `root_id` - The root entry ID of the database
400 ///
401 /// # Returns
402 /// The opened Database configured to use this user's keys
403 ///
404 /// # Errors
405 /// - Returns an error if no key is found for the database
406 /// - Returns an error if no SigKey mapping exists
407 /// - Returns an error if the key is not in the UserKeyManager
408 pub async fn open_database(&self, root_id: &ID) -> Result<Database> {
409 // Find an appropriate key for this database
410 let key_id =
411 self.find_key(root_id)?
412 .ok_or_else(|| super::errors::UserError::NoKeyForDatabase {
413 database_id: root_id.clone(),
414 })?;
415
416 self.open_database_with_key(root_id, &key_id).await
417 }
418
419 /// Open an existing database with an explicitly chosen key.
420 ///
421 /// Equivalent to `open_database`, but selects the user's signing key by
422 /// public key instead of relying on `find_key`'s iteration order. Use this
423 /// when the user holds multiple authorized keys for a database and you
424 /// need writes signed by a specific one (e.g. agent-as-signer scenarios
425 /// where cryptographic provenance matters, not just authorization).
426 ///
427 /// The `key_id` is purely a selector into this user's `UserKeyManager`;
428 /// no crypto material is passed in.
429 ///
430 /// # Arguments
431 /// * `root_id` - The root entry ID of the database
432 /// * `key_id` - Public key of the user-held key to sign with
433 ///
434 /// # Returns
435 /// The opened Database configured to use the specified key
436 ///
437 /// # Errors
438 /// - Returns an error if the root entry does not exist
439 /// - Returns an error if the user does not hold a key with that pubkey
440 /// - Returns an error if the key has no SigKey mapping for this database
441 pub async fn open_database_with_key(
442 &self,
443 root_id: &ID,
444 key_id: &PublicKey,
445 ) -> Result<Database> {
446 // Get the SigningKey from UserKeyManager
447 let signing_key = self.key_manager.get_signing_key(key_id).ok_or_else(|| {
448 super::errors::UserError::KeyNotFound {
449 key_id: key_id.to_string(),
450 }
451 })?;
452
453 // Get the SigKey mapping for this database
454 let sigkey = self.key_mapping(key_id, root_id)?.ok_or_else(|| {
455 super::errors::UserError::NoSigKeyMapping {
456 key_id: key_id.to_string(),
457 database_id: root_id.clone(),
458 }
459 })?;
460
461 // Create Database with user-provided key using resolved SigKey identity.
462 //
463 // On a connected (remote) instance, the read path must travel as the
464 // user's per-DB identity so the daemon's per-tree gate sees a key the
465 // tree actually authorises. `Database::open` would clone the
466 // instance's session backend, which on a remote instance carries the
467 // connection's login pubkey — and the user's login key is not a member
468 // of every tree they hold a per-DB key for. Route through
469 // `Database::open_remote` with the per-DB
470 // identity instead, after proving possession of the per-DB key to
471 // the daemon so the identity sits in the connection's session
472 // keyset.
473 let key = DatabaseKey::with_identity(signing_key.clone(), sigkey.clone());
474 // A SigKey mapping exists (resolved above), so the database is one this
475 // user has requested access to. If its root entry isn't present, the
476 // access is still pending — the bootstrap request hasn't been approved or
477 // the database hasn't synced yet. Surface that explicitly instead of a
478 // bare backend "not found", on both the remote and local open paths.
479 #[cfg(all(unix, feature = "service"))]
480 if let Some(conn) = self.instance.remote_connection() {
481 conn.register_session_key(signing_key).await?;
482 return match Database::open_remote(&self.instance, conn, root_id, sigkey).await {
483 Ok(database) => Ok(database
484 .with_key(key)
485 .with_user_database(self.user_database.clone())),
486 Err(e) if e.is_not_found() => {
487 Err(super::errors::UserError::DatabaseAccessPending {
488 database_id: root_id.clone(),
489 }
490 .into())
491 }
492 Err(e) => Err(e),
493 };
494 }
495 match Database::open(&self.instance, root_id).await {
496 Ok(database) => Ok(database
497 .with_key(key)
498 .with_user_database(self.user_database.clone())),
499 Err(e) if e.is_not_found() => Err(super::errors::UserError::DatabaseAccessPending {
500 database_id: root_id.clone(),
501 }
502 .into()),
503 Err(e) => Err(e),
504 }
505 }
506
507 /// Find databases by name among the user's tracked databases.
508 ///
509 /// Searches only the databases this user has tracked for those matching the given name.
510 ///
511 /// # Arguments
512 /// * `name` - Database name to search for
513 ///
514 /// # Returns
515 /// Vector of matching databases from the user's tracked list
516 pub async fn find_database(&self, name: impl AsRef<str>) -> Result<Vec<Database>> {
517 let name = name.as_ref();
518 let tracked = self.databases().await?;
519 let mut matching = Vec::new();
520
521 for tracked_db in tracked {
522 if let Ok(database) = self.open_database(&tracked_db.database_id).await
523 && let Ok(db_name) = database.get_name().await
524 && db_name == name
525 {
526 matching.push(database);
527 }
528 }
529
530 if matching.is_empty() {
531 Err(UserError::DatabaseNotFoundByName {
532 name: name.to_string(),
533 }
534 .into())
535 } else {
536 Ok(matching)
537 }
538 }
539
540 /// Find which key can access a database.
541 ///
542 /// Searches this user's keys to find one that can access the specified database.
543 /// Considers the SigKey mappings stored in user key metadata.
544 ///
545 /// Returns the key_id of a suitable key, preferring keys with mappings for this database.
546 ///
547 /// # Arguments
548 /// * `database_id` - The ID of the database
549 ///
550 /// # Returns
551 /// Some(key_id) if a suitable key is found, None if no keys can access this database
552 pub fn find_key(&self, database_id: &ID) -> Result<Option<PublicKey>> {
553 // Iterate through all keys and find ones with SigKey mappings for this database
554 for key_id in self.key_manager.list_key_ids() {
555 if let Some(metadata) = self.key_manager.get_key_metadata(&key_id)
556 && metadata.database_sigkeys.contains_key(database_id)
557 {
558 return Ok(Some(key_id));
559 }
560 }
561
562 // No key found with mapping for this database
563 Ok(None)
564 }
565
566 /// Get the resolved SigKey mapping for a key in a specific database.
567 ///
568 /// Users map their private keys to SigKey identifiers on a per-database basis.
569 /// This retrieves the resolved SigKey that a specific key uses in
570 /// a specific database's authentication settings.
571 ///
572 /// Internally, `None` in the stored mapping means "default pubkey identity",
573 /// which this method resolves to the concrete `SigKey::from_pubkey(...)` value.
574 ///
575 /// # Arguments
576 /// * `key_id` - The user's key identifier
577 /// * `database_id` - The database ID
578 ///
579 /// # Returns
580 /// `Ok(Some(sigkey))` if a mapping exists (resolved to concrete SigKey),
581 /// `Ok(None)` if no mapping is configured for this database
582 ///
583 /// # Errors
584 /// Returns an error if the key_id doesn't exist in the UserKeyManager
585 pub fn key_mapping(&self, key_id: &PublicKey, database_id: &ID) -> Result<Option<SigKey>> {
586 let metadata = self.key_manager.get_key_metadata(key_id).ok_or_else(|| {
587 super::errors::UserError::KeyNotFound {
588 key_id: key_id.to_string(),
589 }
590 })?;
591
592 match metadata.database_sigkeys.get(database_id) {
593 None => Ok(None), // no mapping exists
594 Some(None) => {
595 // Default: pubkey identity derived directly from key_id
596 Ok(Some(SigKey::from_pubkey(key_id)))
597 }
598 Some(Some(sigkey)) => Ok(Some(sigkey.clone())),
599 }
600 }
601
602 /// Map a key to a SigKey identity for a specific database.
603 ///
604 /// Registers that this user's key should be used with a specific SigKey identity
605 /// when interacting with a database. This is typically used when a user has been
606 /// granted access to a database and needs to configure their local key to work with it.
607 ///
608 /// If the provided SigKey matches the default pubkey identity for this key,
609 /// it is normalized to `None` internally (compact storage for the common case).
610 ///
611 /// # Multi-Key Support
612 ///
613 /// **Note**: A database may have mappings to multiple keys. This is useful for
614 /// multi-device scenarios where the same user wants to access a database from
615 /// different devices, each with their own key.
616 ///
617 /// # Arguments
618 /// * `key_id` - The user's key identifier (public key)
619 /// * `database_id` - The database ID
620 /// * `sigkey` - The SigKey identity to use for this database
621 ///
622 /// # Errors
623 /// Returns an error if the key_id doesn't exist in the user database
624 pub async fn map_key(
625 &mut self,
626 key_id: &PublicKey,
627 database_id: &ID,
628 sigkey: SigKey,
629 ) -> Result<()> {
630 let tx = self.user_database.new_transaction().await?;
631 self.map_key_in_txn(&tx, key_id, database_id, sigkey)
632 .await?;
633 tx.commit().await?;
634 Ok(())
635 }
636
637 /// Internal helper: Add a SigKey mapping within an existing transaction
638 ///
639 /// This is used internally by methods that manage their own transactions.
640 /// For external use, call `map_key()` instead.
641 ///
642 /// Normalizes the stored value: if the sigkey matches the default pubkey
643 /// identity for this key, stores `None` instead of `Some(sigkey)`.
644 async fn map_key_in_txn(
645 &mut self,
646 tx: &Transaction,
647 key_id: &PublicKey,
648 database_id: &ID,
649 sigkey: SigKey,
650 ) -> Result<()> {
651 use crate::store::Table;
652 use crate::user::types::UserKey;
653
654 let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
655
656 // Find the key metadata in the database
657 let (uuid_primary_key, mut metadata) = keys_table
658 .search(|uk| &uk.key_id == key_id)
659 .await?
660 .into_iter()
661 .next()
662 .ok_or_else(|| super::errors::UserError::KeyNotFound {
663 key_id: key_id.to_string(),
664 })?;
665
666 // Normalize: if the sigkey matches the default pubkey identity, store None
667 let default_sigkey = SigKey::from_pubkey(key_id);
668 let stored = if sigkey == default_sigkey {
669 None
670 } else {
671 Some(sigkey)
672 };
673
674 // Add the database sigkey mapping
675 metadata
676 .database_sigkeys
677 .insert(database_id.clone(), stored);
678
679 // Update the key in user database using the UUID primary key
680 keys_table.set(&uuid_primary_key, metadata.clone()).await?;
681
682 // Update the in-memory key manager with the updated metadata
683 self.key_manager.add_key(metadata)?;
684
685 Ok(())
686 }
687
688 /// Internal helper: Validate key and set up SigKey mapping within an existing transaction
689 ///
690 /// This validates that a key exists and has access to a database, discovers the appropriate
691 /// SigKey, and creates the mapping. Used by track_database (which has upsert behavior).
692 async fn validate_and_map_key_in_txn(
693 &mut self,
694 tx: &Transaction,
695 database_id: &ID,
696 key_id: &PublicKey,
697 ) -> Result<()> {
698 // Verify the key exists
699 if self.key_manager.get_signing_key(key_id).is_none() {
700 return Err(UserError::KeyNotFound {
701 key_id: key_id.to_string(),
702 }
703 .into());
704 }
705
706 // Discover available SigKeys for this public key
707 let available_sigkeys = Database::find_sigkeys(&self.instance, database_id, key_id).await?;
708
709 if available_sigkeys.is_empty() {
710 return Err(UserError::NoSigKeyFound {
711 key_id: key_id.to_string(),
712 database_id: database_id.clone(),
713 }
714 .into());
715 }
716
717 // Select the first SigKey (highest permission, since find_sigkeys returns sorted list)
718 let (sigkey, _permission) = &available_sigkeys[0];
719
720 // Store the discovered SigKey directly (map_key_in_txn normalizes to None if default)
721 self.map_key_in_txn(tx, key_id, database_id, sigkey.clone())
722 .await?;
723
724 Ok(())
725 }
726
727 // === Key Management (User Context) ===
728
729 /// Add a new private key to this user's keyring.
730 ///
731 /// Generates a new Ed25519 keypair, encrypts it (for password-protected users)
732 /// or stores it unencrypted (for passwordless users), and adds it to the user's
733 /// key database.
734 ///
735 /// # Arguments
736 /// * `display_name` - Optional display name for the key
737 ///
738 /// # Returns
739 /// The key ID (public key string)
740 pub async fn add_private_key(&mut self, display_name: Option<&str>) -> Result<PublicKey> {
741 use crate::auth::crypto::generate_keypair;
742 use crate::store::Table;
743 use crate::user::types::{KeyStorage, UserKey};
744
745 // Generate new keypair
746 let (private_key, public_key) = generate_keypair();
747
748 // Get current timestamp using the instance's clock
749 let timestamp = self.instance.clock().now_secs();
750
751 // Prepare UserKey based on encryption type
752 let user_key = if let Some(encryption_key) = self.key_manager.encryption_key() {
753 // Password-protected user: encrypt the key
754 use crate::user::crypto::encrypt_private_key;
755 let (ciphertext, nonce) = encrypt_private_key(&private_key, encryption_key)?;
756
757 UserKey {
758 key_id: public_key.clone(),
759 storage: KeyStorage::Encrypted {
760 algorithm: "aes-256-gcm".to_string(),
761 ciphertext,
762 nonce,
763 },
764 display_name: display_name.map(|s| s.to_string()),
765 created_at: timestamp,
766 last_used: None,
767 is_default: false, // New keys are not default
768 database_sigkeys: HashMap::new(),
769 }
770 } else {
771 // Passwordless user: store unencrypted
772 UserKey {
773 key_id: public_key.clone(),
774 storage: KeyStorage::Unencrypted { key: private_key },
775 display_name: display_name.map(|s| s.to_string()),
776 created_at: timestamp,
777 last_used: None,
778 is_default: false, // New keys are not default
779 database_sigkeys: HashMap::new(),
780 }
781 };
782
783 // Store in user database
784 let tx = self.user_database.new_transaction().await?;
785 let keys_table = tx.get_store::<Table<UserKey>>("keys").await?;
786 keys_table.insert(user_key.clone()).await?;
787 tx.commit().await?;
788
789 // Add to in-memory key manager
790 self.key_manager.add_key(user_key)?;
791
792 Ok(public_key)
793 }
794
795 /// List all key IDs owned by this user.
796 ///
797 /// Keys are returned sorted by creation timestamp (oldest first), making the
798 /// first key in the list the "default" key created when the user was set up.
799 ///
800 /// # Returns
801 /// Vector of PublicKeys sorted by creation time
802 pub fn list_keys(&self) -> Result<Vec<PublicKey>> {
803 Ok(self.key_manager.list_key_ids())
804 }
805
806 /// Get the default key.
807 ///
808 /// Returns the key marked as is_default=true, or falls back to the oldest key
809 /// by creation timestamp if no default is explicitly set.
810 ///
811 /// # Returns
812 /// The PublicKey of the default key
813 ///
814 /// # Errors
815 /// Returns an error if no keys exist
816 pub fn get_default_key(&self) -> Result<PublicKey> {
817 self.key_manager
818 .get_default_key_id()
819 .ok_or_else(|| Error::from(InstanceError::AuthenticationRequired))
820 }
821
822 /// Get the display name set for a key.
823 ///
824 /// Display names are caller-supplied human-readable labels passed to
825 /// `add_private_key`. They have no cryptographic significance and are
826 /// not unique across keys. Returns `None` if the user does not hold a
827 /// key with this pubkey, or if it was added without a display name.
828 ///
829 /// # Arguments
830 /// * `key_id` - Public key of a key held by this user
831 pub fn key_display_name(&self, key_id: &PublicKey) -> Option<&str> {
832 self.key_manager
833 .get_key_metadata(key_id)
834 .and_then(|metadata| metadata.display_name.as_deref())
835 }
836
837 /// Find user-held keys whose display name matches `name`.
838 ///
839 /// Display names are not unique, so this returns every key whose
840 /// `display_name` exactly matches the provided string. Keys without a
841 /// display name are never returned. Returns an empty `Vec` when there
842 /// are no matches.
843 ///
844 /// # Arguments
845 /// * `name` - Exact display name to match
846 pub fn find_keys_by_display_name(&self, name: &str) -> Vec<PublicKey> {
847 self.key_manager
848 .list_key_ids()
849 .into_iter()
850 .filter(|key_id| {
851 self.key_manager
852 .get_key_metadata(key_id)
853 .and_then(|metadata| metadata.display_name.as_deref())
854 == Some(name)
855 })
856 .collect()
857 }
858
859 /// Get a signing key by its ID.
860 ///
861 /// Hands out key material from this session's unlocked key manager. Callers
862 /// that sign on the user's behalf need it — signing a sync request as the
863 /// key whose access they are claiming, for example.
864 ///
865 /// # Arguments
866 /// * `key_id` - The public key identifier
867 ///
868 /// # Returns
869 /// The PrivateKey if found
870 pub fn get_signing_key(&self, key_id: &PublicKey) -> Result<crate::auth::crypto::PrivateKey> {
871 self.key_manager
872 .get_signing_key(key_id)
873 .cloned()
874 .ok_or_else(|| {
875 UserError::KeyNotFound {
876 key_id: key_id.to_string(),
877 }
878 .into()
879 })
880 }
881
882 // === Bootstrap Request Management (User Context) ===
883
884 /// Get all pending bootstrap requests from the sync system.
885 ///
886 /// This is a convenience method that requires the Instance's Sync to be initialized.
887 ///
888 /// # Arguments
889 /// * `sync` - Reference to the Instance's Sync object
890 ///
891 /// # Returns
892 /// A vector of (request_id, bootstrap_request) pairs for pending requests
893 pub async fn pending_bootstrap_requests(
894 &self,
895 sync: &Sync,
896 ) -> Result<Vec<(String, BootstrapRequest)>> {
897 sync.pending_bootstrap_requests().await
898 }
899
900 /// Approve a bootstrap request and add the requesting key to the target database.
901 ///
902 /// The approving key must have Admin permission on the target database.
903 ///
904 /// # Arguments
905 /// * `sync` - Mutable reference to the Instance's Sync object
906 /// * `request_id` - The unique identifier of the request to approve
907 /// * `approving_key_id` - The ID of this user's key to use for approval (must have Admin permission)
908 ///
909 /// # Returns
910 /// Result indicating success or failure of the approval operation
911 ///
912 /// # Errors
913 /// - Returns an error if the user doesn't own the specified approving key
914 /// - Returns an error if the approving key doesn't have Admin permission on the target database
915 /// - Returns an error if the request doesn't exist or isn't pending
916 /// - Returns an error if the key addition to the database fails
917 pub async fn approve_bootstrap_request(
918 &self,
919 sync: &Sync,
920 request_id: &str,
921 approving_key_id: &PublicKey,
922 ) -> Result<()> {
923 // Get the signing key from the key manager
924 let signing_key = self
925 .key_manager
926 .get_signing_key(approving_key_id)
927 .ok_or_else(|| super::errors::UserError::KeyNotFound {
928 key_id: approving_key_id.to_string(),
929 })?;
930
931 // Delegate to Sync layer with the user-provided key
932 // The Sync layer will validate permissions when committing the transaction
933 let key = DatabaseKey::new(signing_key.clone());
934 sync.approve_bootstrap_request_with_key(request_id, &key)
935 .await?;
936
937 Ok(())
938 }
939
940 /// Reject a bootstrap request.
941 ///
942 /// This method marks the request as rejected. The requesting device will not
943 /// be granted access to the target database. Requires Admin permission on the
944 /// target database to prevent unauthorized users from disrupting the bootstrap protocol.
945 ///
946 /// # Arguments
947 /// * `sync` - Mutable reference to the Instance's Sync object
948 /// * `request_id` - The unique identifier of the request to reject
949 /// * `rejecting_key_id` - The ID of this user's key (for permission validation and audit trail)
950 ///
951 /// # Returns
952 /// Result indicating success or failure of the rejection operation
953 ///
954 /// # Errors
955 /// - Returns an error if the user doesn't own the specified rejecting key
956 /// - Returns an error if the request doesn't exist or isn't pending
957 /// - Returns an error if the rejecting key lacks Admin permission on the target database
958 pub async fn reject_bootstrap_request(
959 &self,
960 sync: &Sync,
961 request_id: &str,
962 rejecting_key_id: &PublicKey,
963 ) -> Result<()> {
964 // Get the signing key from the key manager
965 let signing_key = self
966 .key_manager
967 .get_signing_key(rejecting_key_id)
968 .ok_or_else(|| super::errors::UserError::KeyNotFound {
969 key_id: rejecting_key_id.to_string(),
970 })?;
971
972 // Delegate to Sync layer with the user-provided key
973 // The Sync layer will validate Admin permission on the target database
974 let key = DatabaseKey::new(signing_key.clone());
975 sync.reject_bootstrap_request_with_key(request_id, &key)
976 .await?;
977
978 Ok(())
979 }
980
981 /// Request access to a database from a peer (bootstrap sync).
982 ///
983 /// This convenience method initiates a bootstrap sync request to access a database
984 /// that this user doesn't have locally yet. The user's key will be sent to the peer
985 /// to request the specified permission level.
986 ///
987 /// This is useful for multi-device scenarios where a user wants to access their
988 /// existing database from a new device, or when requesting access to a database
989 /// shared by another user.
990 ///
991 /// On a successful (already-authorized) request the database is synced and its
992 /// SigKey mapping is recorded, so [`open_database`](Self::open_database) works
993 /// immediately — no separate [`track_database`](Self::track_database) call is
994 /// needed just to open it. The supplied `sync_settings` are recorded with
995 /// the database, including when a pending request is retried after approval.
996 ///
997 /// When approval is still pending the request returns
998 /// [`SyncError::BootstrapPending`] but a provisional mapping is recorded;
999 /// opening the database before approval then fails with
1000 /// [`UserError::DatabaseAccessPending`] rather than a cryptic backend error.
1001 ///
1002 /// # Arguments
1003 /// * `sync` - Reference to the Instance's Sync object
1004 /// * `ticket` - A ticket containing the database ID and address hints
1005 /// * `key_id` - The ID of this user's key to use for the request
1006 /// * `requested_permission` - The permission level being requested
1007 /// * `sync_settings` - This user's durable synchronization preferences for
1008 /// the database once its access mapping is recorded
1009 /// * `metadata` - Optional free-form context for the approver to inspect on
1010 /// the stored bootstrap request when deciding whether to grant access
1011 ///
1012 /// # Returns
1013 /// Result indicating success or failure of the bootstrap request
1014 ///
1015 /// # Errors
1016 /// - Returns an error if the user doesn't own the specified key
1017 /// - Returns an error if all addresses in the ticket fail
1018 /// - Returns an error if the bootstrap sync fails
1019 ///
1020 /// # Example
1021 /// ```rust,ignore
1022 /// // Request write access to a shared database
1023 /// let user_key_id = user.get_default_key()?;
1024 /// let ticket: DatabaseTicket = "eidetica:?db=sha256:abc...&pr=http:192.168.1.1:8080".parse()?;
1025 /// user.request_database_access(
1026 /// &sync,
1027 /// &ticket,
1028 /// &user_key_id,
1029 /// Permission::Write(5),
1030 /// SyncSettings::on_commit(),
1031 /// None,
1032 /// ).await?;
1033 ///
1034 /// // After approval, the database can be opened
1035 /// let database = user.open_database(ticket.database_id())?;
1036 /// ```
1037 pub async fn request_database_access(
1038 &mut self,
1039 sync: &Sync,
1040 ticket: &DatabaseTicket,
1041 key_id: &PublicKey,
1042 requested_permission: Permission,
1043 sync_settings: SyncSettings,
1044 metadata: Option<Doc>,
1045 ) -> Result<()> {
1046 // The request is signed with this key, so the peer can tell an actual
1047 // key holder from someone naming a key they don't have.
1048 let signing_key = self
1049 .key_manager
1050 .get_signing_key(key_id)
1051 .ok_or_else(|| super::errors::UserError::KeyNotFound {
1052 key_id: key_id.to_string(),
1053 })?
1054 .clone();
1055
1056 let key_name = key_id.to_string();
1057 let database_id = ticket.database_id().clone();
1058
1059 let result = sync
1060 .bootstrap_with_ticket(
1061 ticket,
1062 &signing_key,
1063 &key_name,
1064 requested_permission,
1065 metadata,
1066 )
1067 .await;
1068
1069 self.record_database_access(&database_id, key_id, sync_settings, result)
1070 .await
1071 }
1072
1073 /// Record the User-layer SigKey mapping for a bootstrap whose network phase
1074 /// has already completed, given the [`Result`] returned by
1075 /// [`Sync::bootstrap_with_ticket`](crate::sync::Sync::bootstrap_with_ticket).
1076 ///
1077 /// [`request_database_access`](Self::request_database_access) is the usual
1078 /// entry point and performs the network bootstrap for you. This split-out
1079 /// method exists for callers that hold a coarse lock around the `User` (e.g.
1080 /// a daemon's per-session lock): they can run the network round-trip without
1081 /// the lock held and re-acquire it only for this cheap, local mapping write,
1082 /// so a slow or hung peer never blocks the rest of the session. The mapping
1083 /// semantics are identical to `request_database_access`.
1084 ///
1085 /// The `bootstrap_result` is consumed and its outcome re-raised unchanged so
1086 /// callers can react to [`SyncError::BootstrapPending`] et al.
1087 pub async fn record_database_access(
1088 &mut self,
1089 database_id: &ID,
1090 key_id: &PublicKey,
1091 sync_settings: SyncSettings,
1092 bootstrap_result: Result<()>,
1093 ) -> Result<()> {
1094 // Bootstrap grants access at the sync layer (auth + entries) but does not
1095 // establish the User-layer SigKey mapping that `open_database`/`find_key`
1096 // rely on. Establish it here so a successful request leaves the database
1097 // openable — previously the caller had to call `track_database` manually,
1098 // and omitting it left the database unopenable ("No key found").
1099 match bootstrap_result {
1100 Ok(()) => {
1101 // Access was already authorized and the database is now synced, so
1102 // its auth settings are local: discover the real SigKey (which may
1103 // be a direct, global-wildcard, or delegated key) and record it.
1104 self.track_database(database_id.clone(), key_id, sync_settings)
1105 .await?;
1106 Ok(())
1107 }
1108 Err(e) => {
1109 // Awaiting manual approval: the database isn't synced yet, so the
1110 // SigKey can't be discovered. On approval the key is added by
1111 // pubkey (see `Sync::approve_bootstrap_request_with_key`), so its
1112 // SigKey will be the default pubkey identity — record that mapping
1113 // provisionally. Until the database syncs, opening it surfaces
1114 // `UserError::DatabaseAccessPending`. The pending error is
1115 // re-raised unchanged so callers can react to it.
1116 if let Error::Sync(sync_err) = &e
1117 && matches!(sync_err.as_ref(), SyncError::BootstrapPending { .. })
1118 {
1119 self.record_pending_database_access(database_id, key_id, sync_settings)
1120 .await?;
1121 }
1122 Err(e)
1123 }
1124 }
1125 }
1126
1127 /// Store a pending bootstrap's provisional key mapping and preferences in
1128 /// one transaction. Unlike [`Self::track_database`], this does not try to
1129 /// discover authorization that cannot exist until approval.
1130 async fn record_pending_database_access(
1131 &mut self,
1132 database_id: &ID,
1133 key_id: &PublicKey,
1134 sync_settings: SyncSettings,
1135 ) -> Result<()> {
1136 let tx = self.user_database.new_transaction().await?;
1137 self.map_key_in_txn(&tx, key_id, database_id, SigKey::from_pubkey(key_id))
1138 .await?;
1139 let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
1140 let tracked = TrackedDatabase {
1141 database_id: database_id.clone(),
1142 key_id: key_id.clone(),
1143 sync_settings,
1144 };
1145 databases_table
1146 .set(&database_id.to_string(), tracked)
1147 .await?;
1148 tx.commit().await?;
1149
1150 if let Some(sync) = self.instance.sync() {
1151 sync.sync_user(&self.user_uuid, self.user_database.root_id())
1152 .await?;
1153 }
1154
1155 Ok(())
1156 }
1157
1158 // === Tracked Databases ===
1159
1160 /// Track a database, adding it to this user's list with auto-discovery of SigKeys.
1161 ///
1162 /// This method adds an existing database to your tracked list, or updates it if
1163 /// already tracked (upsert behavior).
1164 ///
1165 /// When tracking:
1166 /// 1. Uses Database::find_sigkeys() to discover which SigKey the user can use
1167 /// 2. Automatically selects the SigKey with highest permission
1168 /// 3. Stores the key mapping and sync settings
1169 ///
1170 /// The sync_settings indicate your sync preferences, but do not automatically
1171 /// configure sync. Use the Sync module's peer and tree methods to set up actual
1172 /// sync relationships.
1173 ///
1174 /// # Arguments
1175 /// * `database_id` - ID of the database to track
1176 /// * `key_id` - Which user key to use for this database
1177 /// * `sync_settings` - Sync preferences for this database
1178 ///
1179 /// # Returns
1180 /// Result indicating success or failure
1181 ///
1182 /// # Errors
1183 /// - Returns `NoSigKeyFound` if no SigKey can be found for the specified key
1184 /// - Returns `KeyNotFound` if the specified key_id doesn't exist
1185 pub async fn track_database(
1186 &mut self,
1187 database_id: impl Into<ID>,
1188 key_id: &PublicKey,
1189 sync_settings: SyncSettings,
1190 ) -> Result<()> {
1191 let tracked = TrackedDatabase {
1192 database_id: database_id.into(),
1193 key_id: key_id.clone(),
1194 sync_settings,
1195 };
1196 // Single transaction for all operations
1197 let tx = self.user_database.new_transaction().await?;
1198 let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
1199
1200 // Use database ID as the key - check if it already exists (O(1))
1201 let db_id_key = tracked.database_id.to_string();
1202 let existing = databases_table.get(&db_id_key).await.ok();
1203
1204 // Determine if we need to validate and setup key mapping
1205 let needs_key_validation = match &existing {
1206 Some(existing) => existing.key_id != tracked.key_id, // Key changed
1207 None => true, // New database
1208 };
1209
1210 // Validate key and set up mapping if needed
1211 if needs_key_validation {
1212 self.validate_and_map_key_in_txn(&tx, &tracked.database_id, &tracked.key_id)
1213 .await?;
1214 }
1215
1216 // Store using database ID as explicit key (not using insert's auto-generated UUID)
1217 databases_table.set(&db_id_key, tracked).await?;
1218
1219 // Single commit for all changes
1220 tx.commit().await?;
1221
1222 // Update sync system to immediately recompute combined settings
1223 // This ensures automatic sync works right away, without waiting for background worker
1224 if let Some(sync) = self.instance.sync() {
1225 // Auto-sync user tracking if not already synced
1226 // This is idempotent - safe to call multiple times
1227 sync.sync_user(&self.user_uuid, self.user_database.root_id())
1228 .await?;
1229 }
1230
1231 Ok(())
1232 }
1233
1234 /// List all tracked databases.
1235 ///
1236 /// Returns all databases this user has added to their tracked list.
1237 ///
1238 /// # Returns
1239 /// Vector of TrackedDatabase entries
1240 pub async fn databases(&self) -> Result<Vec<TrackedDatabase>> {
1241 let databases_table = self
1242 .user_database
1243 .get_store_viewer::<Table<TrackedDatabase>>("databases")
1244 .await?;
1245
1246 // Get all entries from the table (returns Vec<(key, value)>)
1247 let all_entries = databases_table.search(|_| true).await?;
1248
1249 // Extract just the values
1250 let tracked: Vec<TrackedDatabase> = all_entries.into_iter().map(|(_key, db)| db).collect();
1251
1252 Ok(tracked)
1253 }
1254
1255 /// Get a specific tracked database by ID.
1256 ///
1257 /// # Arguments
1258 /// * `database_id` - The ID of the database
1259 ///
1260 /// # Returns
1261 /// The TrackedDatabase if it's in the user's tracked list
1262 ///
1263 /// # Errors
1264 /// Returns `DatabaseNotTracked` if the database is not in the user's list
1265 pub async fn database(&self, database_id: &ID) -> Result<TrackedDatabase> {
1266 let databases_table = self
1267 .user_database()
1268 .get_store_viewer::<Table<TrackedDatabase>>("databases")
1269 .await?;
1270
1271 // Direct O(1) lookup using database ID as key
1272 let db_id_key = database_id.to_string();
1273 databases_table.get(&db_id_key).await.map_err(|_| {
1274 UserError::DatabaseNotTracked {
1275 database_id: database_id.clone(),
1276 }
1277 .into()
1278 })
1279 }
1280
1281 /// Check whether this user has sync enabled for a tracked database.
1282 ///
1283 /// Returns this user's own preference, not the host's combined state.
1284 /// A `false` result means this user is not personally sharing the database;
1285 /// another user on the same instance may still have sync enabled for it.
1286 ///
1287 /// Returns `Ok(false)` for databases not tracked by this user.
1288 pub async fn is_sync_enabled(&self, database_id: &ID) -> Result<bool> {
1289 let databases_table = self
1290 .user_database()
1291 .get_store_viewer::<Table<TrackedDatabase>>("databases")
1292 .await?;
1293 let db_id_key = database_id.to_string();
1294 match databases_table.get(&db_id_key).await {
1295 Ok(tracked) => Ok(tracked.sync_settings.sync_enabled),
1296 Err(_) => Ok(false),
1297 }
1298 }
1299
1300 /// Stop tracking a database.
1301 ///
1302 /// This removes the database from the user's tracked list.
1303 /// It does not delete the database itself, remove key mappings, or delete any data.
1304 ///
1305 /// # Arguments
1306 /// * `database_id` - The ID of the database to stop tracking
1307 ///
1308 /// # Errors
1309 /// Returns `DatabaseNotTracked` if the database is not in the user's list
1310 pub async fn untrack_database(&mut self, database_id: &ID) -> Result<()> {
1311 let tx = self.user_database.new_transaction().await?;
1312 let databases_table = tx.get_store::<Table<TrackedDatabase>>("databases").await?;
1313
1314 // Direct O(1) delete using database ID as key
1315 let db_id_key = database_id.to_string();
1316
1317 // Verify it exists before deleting
1318 if databases_table.get(&db_id_key).await.is_err() {
1319 return Err(UserError::DatabaseNotTracked {
1320 database_id: database_id.clone(),
1321 }
1322 .into());
1323 }
1324
1325 // Delete using database ID as key
1326 databases_table.delete(&db_id_key).await?;
1327 tx.commit().await?;
1328
1329 Ok(())
1330 }
1331}