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

eidetica/user/
admin.rs

1//! Instance-admin capability view.
2//!
3//! [`InstanceAdmin`] is the gateway for operations gated by `Admin` on the
4//! `_users` / `_databases` system databases — creating users, listing users,
5//! promoting other admins.
6//!
7//! It is obtained via [`User::admin`](crate::user::User::admin), which only
8//! constructs it when the user actually holds instance-admin. Because the
9//! permission is checked at construction, the operations here perform no
10//! further check of their own, and the privilege boundary is explicit at the
11//! call site:
12//!
13//! ```ignore
14//! let admin = user.admin().await?;          // Err if not an instance admin
15//! admin.create_user(NewUser::passwordless("alice")).await?;
16//! ```
17//!
18//! Every operation signs `_users` / `_databases` writes with the user's
19//! **session key** (never the device key), so the same calls work on both
20//! local and remote instances.
21
22use super::{session::User, system_databases};
23use crate::{Database, NewUser, Result, auth::crypto::PublicKey, entry::ID};
24
25/// Instance-admin capability view over a [`User`] session.
26///
27/// Obtain via [`User::admin`](crate::user::User::admin). See the
28/// [module docs](self) for the rationale behind the separate type.
29#[derive(Debug)]
30pub struct InstanceAdmin<'a> {
31    user: &'a User,
32}
33
34impl<'a> InstanceAdmin<'a> {
35    /// Wrap a user session as an admin view.
36    ///
37    /// Only [`User::admin`](crate::user::User::admin) calls this, and only
38    /// after confirming the user holds instance-admin — do not construct
39    /// `InstanceAdmin` any other way.
40    pub(crate) fn new(user: &'a User) -> Self {
41        Self { user }
42    }
43
44    /// Create a new user account.
45    ///
46    /// Signs the `_users` write with this admin's session key and authors the
47    /// new user's database via the regular `Database::create` wire path. The
48    /// user-tree first-login bootstrap (root `UserKey` row, device-key `Read`
49    /// grant) is deferred to first login, where the new user is themselves
50    /// the connection session and can author both writes directly. The result
51    /// is the same one code path on local and remote instances — no
52    /// daemon-side RPC and no plaintext password on the wire.
53    ///
54    /// Returns the new user's UUID (stable internal identifier). Use
55    /// [`Instance::login_user`] to obtain a [`User`] session for the new
56    /// account; materialising it directly here would require the admin's
57    /// connection to read the new user's own tree over the wire, where the
58    /// session gate (correctly) rejects the admin as a non-member.
59    ///
60    /// # Arguments
61    /// * `new_user` - Username and optional password for the user to create.
62    ///   See [`NewUser`].
63    ///
64    /// [`Instance::login_user`]: crate::Instance::login_user
65    pub async fn create_user(&self, new_user: NewUser) -> Result<String> {
66        let instance = self.user.instance();
67        let signing_key = self.user.default_signing_key()?;
68        let users_db = instance.users_db_for_session(&signing_key).await?;
69        let (user_uuid, _user_info, _root_key) = system_databases::create_user(
70            &users_db,
71            instance,
72            &new_user.username,
73            new_user.password.as_deref(),
74        )
75        .await?;
76        Ok(user_uuid)
77    }
78
79    /// List all user IDs.
80    ///
81    /// Reads `_users` via the admin's session key, so it works on both local
82    /// and remote instances.
83    pub async fn list_users(&self) -> Result<Vec<String>> {
84        let signing_key = self.user.default_signing_key()?;
85        let users_db = self
86            .user
87            .instance()
88            .users_db_for_session(&signing_key)
89            .await?;
90        system_databases::list_users(&users_db).await
91    }
92
93    /// Grant instance-admin to another key.
94    ///
95    /// Adds `new_admin` as `Admin(0)` on the system databases that gate
96    /// instance-level admin operations (`_users` and `_databases`), with the
97    /// write signed by this admin's own key. The first instance admin is
98    /// created automatically during instance bootstrap; this is how every
99    /// subsequent admin is promoted.
100    ///
101    /// Idempotent: re-granting an existing admin re-asserts the same
102    /// `Admin(0)` entry.
103    pub async fn grant_instance_admin(&self, new_admin: &PublicKey) -> Result<()> {
104        let users_db = self
105            .admin_keyed_system_db(self.user.instance().users_db_id())
106            .await?;
107        let databases_db = self
108            .admin_keyed_system_db(self.user.instance().databases_db_id())
109            .await?;
110        system_databases::grant_admin_on_system_dbs(&users_db, &databases_db, new_admin).await
111    }
112
113    /// Open a system database keyed by this admin's default signing key.
114    ///
115    /// Unlike `User::open_database`, this does not require a tracked-database
116    /// SigKey mapping: the instance-admin bootstrap writes the user's pubkey
117    /// straight into the system DB's `auth_settings`, so the default-pubkey
118    /// identity resolves directly. Routes through
119    /// [`Instance::open_system_db_for_session`], so reads go over the wire on
120    /// a remote instance instead of hitting the client's empty local backend.
121    async fn admin_keyed_system_db(&self, root_id: &ID) -> Result<Database> {
122        let signing_key = self.user.default_signing_key()?;
123        self.user
124            .instance()
125            .open_system_db_for_session(root_id, &signing_key)
126            .await
127    }
128}