eidetica/account/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 accounts, listing accounts,
5//! promoting other admins.
6//!
7//! It is obtained via [`AccountSession::admin`](crate::account::AccountSession::admin), which only
8//! constructs it when the account 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 = account.admin().await?; // Err if not an instance admin
15//! admin.create_account(NewAccount::passwordless("alice")).await?;
16//! ```
17//!
18//! Promotion signs system Auth with the account's session key. Native login
19//! registration/listing use instance-admin-gated RPC on connected instances;
20//! passwords and plaintext signing keys remain client-side.
21
22use super::{session::AccountSession, system_databases};
23use crate::{Database, NewAccount, Result, auth::crypto::PublicKey, entry::ID};
24
25/// Instance-admin capability view over an [`AccountSession`] session.
26///
27/// Obtain via [`AccountSession::admin`](crate::account::AccountSession::admin). See the
28/// [module docs](self) for the rationale behind the separate type.
29#[derive(Debug)]
30pub struct InstanceAdmin<'a> {
31 user: &'a AccountSession,
32}
33
34impl<'a> InstanceAdmin<'a> {
35 /// Wrap an account session as an admin view.
36 ///
37 /// Only [`AccountSession::admin`](crate::account::AccountSession::admin) calls this, and only
38 /// after confirming the account holds instance-admin — do not construct
39 /// `InstanceAdmin` any other way.
40 pub(crate) fn new(user: &'a AccountSession) -> Self {
41 Self { user }
42 }
43
44 /// Create a new account.
45 ///
46 /// Authors the Account database with its new key and registers its wrapped
47 /// credentials in native local storage. Connected registration is gated by
48 /// the admin's session key; passwords and plaintext keys are not
49 /// sent to the daemon. First login retains the existing device-Read grant.
50 ///
51 /// Returns the new instance-local login ID (see
52 /// [`AccountSession::login_id`]), not the Account root ID. Use
53 /// [`Instance::login_account`] to obtain an [`AccountSession`] session for the new
54 /// account; materialising it directly here would require the admin's
55 /// connection to read the new account's own tree over the wire, where the
56 /// session gate (correctly) rejects the admin as a non-member.
57 ///
58 /// # Arguments
59 /// * `new_account` - Username and password string (including empty) for the account to create.
60 /// See [`NewAccount`].
61 ///
62 /// [`Instance::login_account`]: crate::Instance::login_account
63 pub async fn create_account(&self, new_account: NewAccount) -> Result<String> {
64 let instance = self.user.instance();
65 let signing_key = self.user.default_signing_key()?;
66 let users_db = instance.users_db_for_session(&signing_key).await?;
67 let (login_id, _user_info, _root_key) = system_databases::create_account(
68 &users_db,
69 instance,
70 &new_account.username,
71 &new_account.password,
72 )
73 .await?;
74 Ok(login_id)
75 }
76
77 /// List the login names of all accounts registered on this instance.
78 ///
79 /// Reads native local login names. Connected requests require instance-admin
80 /// on the daemon's `_users` Auth and return no credential records.
81 pub async fn list_accounts(&self) -> Result<Vec<String>> {
82 let signing_key = self.user.default_signing_key()?;
83 let users_db = self
84 .user
85 .instance()
86 .users_db_for_session(&signing_key)
87 .await?;
88 system_databases::list_accounts(&users_db).await
89 }
90
91 /// Grant instance-admin to another key.
92 ///
93 /// Adds `new_admin` as `Admin(0)` on the system databases that gate
94 /// instance-level admin operations (`_users` and `_databases`), with the
95 /// write signed by this admin's own key. The first instance admin is
96 /// created automatically during instance bootstrap; this is how every
97 /// subsequent admin is promoted.
98 ///
99 /// Idempotent: re-granting an existing admin re-asserts the same
100 /// `Admin(0)` entry.
101 pub async fn grant_instance_admin(&self, new_admin: &PublicKey) -> Result<()> {
102 let users_db = self
103 .admin_keyed_system_db(self.user.instance().users_db_id())
104 .await?;
105 let databases_db = self
106 .admin_keyed_system_db(self.user.instance().databases_db_id())
107 .await?;
108 system_databases::grant_admin_on_system_dbs(&users_db, &databases_db, new_admin).await
109 }
110
111 /// Open a system database keyed by this admin's default signing key.
112 ///
113 /// Unlike `AccountSession::open_database`, this does not require a tracked-database
114 /// SigKey mapping: the instance-admin bootstrap writes the account's pubkey
115 /// straight into the system DB's `auth_settings`, so the default-pubkey
116 /// identity resolves directly. Routes through
117 /// [`Instance::open_system_db_for_session`], so reads go over the wire on
118 /// a remote instance instead of hitting the client's empty local backend.
119 async fn admin_keyed_system_db(&self, root_id: &ID) -> Result<Database> {
120 let signing_key = self.user.default_signing_key()?;
121 self.user
122 .instance()
123 .open_system_db_for_session(root_id, &signing_key)
124 .await
125 }
126}