eidetica/sync/bootstrap.rs
1//! Bootstrap sync operations and request management.
2
3use tracing::info;
4
5use super::{
6 Address, BootstrapRequest, DatabaseTicket, RequestStatus, Sync, SyncError,
7 bootstrap_request_manager::BootstrapRequestManager,
8};
9use crate::{
10 Database, Result,
11 auth::{Permission, crypto::PublicKey, types::AuthKey},
12 database::DatabaseKey,
13 entry::ID,
14};
15
16impl Sync {
17 // === Bootstrap Sync Methods ===
18 //
19 // Bootstrap sync allows a device to request access to a database it doesn't
20 // have permission to yet. The device sends its public key and requested
21 // permission level to the peer, creating a pending bootstrap request that
22 // an administrator can approve or reject.
23 //
24 // Use `sync_with_peer_for_bootstrap_with_key()` with User API managed keys.
25
26 /// Internal helper for bootstrap sync operations.
27 ///
28 /// This method contains the common logic for bootstrap scenarios where the local
29 /// device doesn't have access to the target tree yet and needs to request
30 /// permission during the initial sync.
31 ///
32 /// # Arguments
33 /// * `address` - The transport address of the peer to sync with
34 /// * `tree_id` - The ID of the tree to sync
35 /// * `requesting_public_key` - The formatted public key string for authentication
36 /// * `requesting_key_name` - The name/ID of the requesting key
37 /// * `requested_permission` - The permission level being requested
38 ///
39 /// # Returns
40 /// A Result indicating success or failure.
41 ///
42 /// # Errors
43 /// * `SyncError::InvalidPublicKey` if the public key is empty or malformed
44 /// * `SyncError::InvalidKeyName` if the key name is empty
45 async fn sync_with_peer_for_bootstrap_internal(
46 &self,
47 address: &Address,
48 tree_id: &ID,
49 requesting_public_key: &PublicKey,
50 requesting_key_name: &str,
51 requested_permission: Permission,
52 ) -> Result<()> {
53 // Validate key name is not empty
54 if requesting_key_name.is_empty() {
55 return Err(SyncError::InvalidKeyName {
56 reason: "Key name cannot be empty".to_string(),
57 }
58 .into());
59 }
60
61 // Connect to peer if not already connected
62 let peer_pubkey = self.connect_to_peer(address).await?;
63
64 // Store the address for this peer
65 self.add_peer_address(&peer_pubkey, address.clone()).await?;
66
67 // Sync tree with authentication
68 self.sync_tree_with_peer_auth(
69 &peer_pubkey,
70 tree_id,
71 Some(requesting_public_key),
72 Some(requesting_key_name),
73 Some(requested_permission),
74 )
75 .await?;
76
77 Ok(())
78 }
79
80 /// Sync with a peer for bootstrap using a user-provided public key.
81 ///
82 /// This method is specifically designed for bootstrap scenarios where the local
83 /// device doesn't have access to the target tree yet and needs to request
84 /// permission during the initial sync. The public key is provided directly
85 /// rather than looked up from backend storage, making it compatible with
86 /// User API managed keys.
87 ///
88 /// # Arguments
89 /// * `address` - The transport address of the peer to sync with
90 /// * `tree_id` - The ID of the tree to sync
91 /// * `requesting_public_key` - The formatted public key string (e.g., "ed25519:base64...")
92 /// * `requesting_key_name` - The name/ID of the requesting key for audit trail
93 /// * `requested_permission` - The permission level being requested
94 ///
95 /// # Returns
96 /// A Result indicating success or failure.
97 ///
98 /// # Example
99 /// ```rust,ignore
100 /// // With User API managed keys:
101 /// let public_key = user.get_public_key(user_key_id)?;
102 /// sync.sync_with_peer_for_bootstrap_with_key(
103 /// &Address::http("127.0.0.1:8080"),
104 /// &tree_id,
105 /// &public_key,
106 /// user_key_id,
107 /// Permission::Write(5),
108 /// ).await?;
109 /// ```
110 pub async fn sync_with_peer_for_bootstrap_with_key(
111 &self,
112 address: &Address,
113 tree_id: &ID,
114 requesting_public_key: &PublicKey,
115 requesting_key_name: &str,
116 requested_permission: Permission,
117 ) -> Result<()> {
118 // Delegate to internal method
119 self.sync_with_peer_for_bootstrap_internal(
120 address,
121 tree_id,
122 requesting_public_key,
123 requesting_key_name,
124 requested_permission,
125 )
126 .await
127 }
128
129 /// Bootstrap with a peer using a [`DatabaseTicket`].
130 ///
131 /// Tries every address hint in the ticket concurrently. Succeeds if at
132 /// least one address connects and syncs; returns the last error if all
133 /// fail.
134 ///
135 /// # Arguments
136 /// * `ticket` - A ticket containing the database ID and address hints.
137 /// * `requesting_public_key` - The formatted public key string for authentication.
138 /// * `requesting_key_name` - The name/ID of the requesting key.
139 /// * `requested_permission` - The permission level being requested.
140 ///
141 /// # Errors
142 /// Returns [`SyncError::InvalidAddress`] if the ticket has no address hints.
143 /// Returns the last sync error if no address succeeded.
144 pub async fn bootstrap_with_ticket(
145 &self,
146 ticket: &DatabaseTicket,
147 requesting_public_key: &PublicKey,
148 requesting_key_name: &str,
149 requested_permission: Permission,
150 ) -> Result<()> {
151 let database_id = ticket.database_id().clone();
152 let pubkey = requesting_public_key.clone();
153 let key_name = requesting_key_name.to_string();
154 self.try_addresses_concurrently(ticket.addresses(), |sync, addr| {
155 let db_id = database_id.clone();
156 let pubkey = pubkey.clone();
157 let key_name = key_name.clone();
158 async move {
159 sync.sync_with_peer_for_bootstrap_internal(
160 &addr,
161 &db_id,
162 &pubkey,
163 &key_name,
164 requested_permission,
165 )
166 .await
167 }
168 })
169 .await
170 }
171
172 // === Bootstrap Request Management Methods ===
173
174 /// Get all pending bootstrap requests.
175 ///
176 /// # Returns
177 /// A vector of (request_id, bootstrap_request) pairs for pending requests.
178 pub async fn pending_bootstrap_requests(&self) -> Result<Vec<(String, BootstrapRequest)>> {
179 let txn = self.sync_tree.new_transaction().await?;
180 let manager = BootstrapRequestManager::new(&txn);
181 manager.pending_requests().await
182 }
183
184 /// Get all approved bootstrap requests.
185 ///
186 /// # Returns
187 /// A vector of (request_id, bootstrap_request) pairs for approved requests.
188 pub async fn approved_bootstrap_requests(&self) -> Result<Vec<(String, BootstrapRequest)>> {
189 let txn = self.sync_tree.new_transaction().await?;
190 let manager = BootstrapRequestManager::new(&txn);
191 manager.approved_requests().await
192 }
193
194 /// Get all rejected bootstrap requests.
195 ///
196 /// # Returns
197 /// A vector of (request_id, bootstrap_request) pairs for rejected requests.
198 pub async fn rejected_bootstrap_requests(&self) -> Result<Vec<(String, BootstrapRequest)>> {
199 let txn = self.sync_tree.new_transaction().await?;
200 let manager = BootstrapRequestManager::new(&txn);
201 manager.rejected_requests().await
202 }
203
204 /// Get a specific bootstrap request by ID.
205 ///
206 /// # Arguments
207 /// * `request_id` - The unique identifier of the request
208 ///
209 /// # Returns
210 /// A tuple of (request_id, bootstrap_request) if found, None otherwise.
211 pub async fn get_bootstrap_request(
212 &self,
213 request_id: &str,
214 ) -> Result<Option<(String, BootstrapRequest)>> {
215 let txn = self.sync_tree.new_transaction().await?;
216 let manager = BootstrapRequestManager::new(&txn);
217
218 match manager.get_request(request_id).await? {
219 Some(request) => Ok(Some((request_id.to_string(), request))),
220 None => Ok(None),
221 }
222 }
223
224 /// Approve a bootstrap request using a `DatabaseKey`.
225 ///
226 /// This variant allows approval using keys that are not stored in the backend,
227 /// such as user keys managed in memory.
228 ///
229 /// # Arguments
230 /// * `request_id` - The unique identifier of the request to approve
231 /// * `key` - The `DatabaseKey` to use for the transaction and audit trail
232 ///
233 /// # Returns
234 /// Result indicating success or failure of the approval operation.
235 ///
236 /// # Errors
237 /// Returns `SyncError::InsufficientPermission` if the approving key does not have
238 /// Admin permission on the target database.
239 pub async fn approve_bootstrap_request_with_key(
240 &self,
241 request_id: &str,
242 key: &DatabaseKey,
243 ) -> Result<()> {
244 // Load the request from sync database
245 let sync_op = self.sync_tree.new_transaction().await?;
246 let manager = BootstrapRequestManager::new(&sync_op);
247
248 let request = manager
249 .get_request(request_id)
250 .await?
251 .ok_or_else(|| SyncError::RequestNotFound(request_id.to_string()))?;
252
253 // Validate request is still pending
254 if !matches!(request.status, RequestStatus::Pending) {
255 return Err(SyncError::InvalidRequestState {
256 request_id: request_id.to_string(),
257 current_status: format!("{:?}", request.status),
258 expected_status: "Pending".to_string(),
259 }
260 .into());
261 }
262
263 // Load the existing database with the user's signing key
264 let database = Database::open(self.instance()?, &request.tree_id, key.clone()).await?;
265
266 // Explicitly check that the approving user has Admin permission
267 // This provides clear error messages and fails fast before modifying the database
268 let permission = database.current_permission().await?;
269 if !permission.can_admin() {
270 return Err(SyncError::InsufficientPermission {
271 request_id: request_id.to_string(),
272 required_permission: "Admin".to_string(),
273 actual_permission: permission,
274 }
275 .into());
276 }
277
278 // Create transaction - this will use the provided signing key
279 let tx = database.new_transaction().await?;
280
281 // Get settings store and update auth configuration
282 let settings_store = tx.get_settings()?;
283
284 // Create the auth key for the requesting device
285 // Keys are stored by pubkey, with name as optional metadata
286 let auth_key = AuthKey::active(
287 Some(&request.requesting_key_name), // name metadata
288 request.requested_permission,
289 );
290
291 // Add the new key to auth settings using SettingsStore API
292 // Store by pubkey (this provides proper upsert behavior and validation)
293 settings_store
294 .set_auth_key(&request.requesting_pubkey, auth_key)
295 .await?;
296
297 // Commit will validate that the user's key has Admin permission
298 // If this fails, it means the user lacks the necessary permission
299 tx.commit().await?;
300
301 // Update request status to approved
302 let approver_id = key.identity().display_id();
303 let approval_time = self
304 .instance
305 .upgrade()
306 .ok_or(SyncError::InstanceDropped)?
307 .clock()
308 .now_rfc3339();
309 manager
310 .update_status(
311 request_id,
312 RequestStatus::Approved {
313 approved_by: approver_id.to_string(),
314 approval_time,
315 },
316 )
317 .await?;
318 sync_op.commit().await?;
319
320 info!(
321 request_id = %request_id,
322 tree_id = %request.tree_id,
323 approved_by = %approver_id,
324 "Bootstrap request approved and key added to database using user-provided key"
325 );
326
327 Ok(())
328 }
329
330 /// Reject a bootstrap request using a `DatabaseKey` with Admin permission validation.
331 ///
332 /// This variant allows rejection using keys that are not stored in the backend,
333 /// such as user keys managed in memory. It validates that the rejecting user has
334 /// Admin permission on the target database before allowing the rejection.
335 ///
336 /// # Arguments
337 /// * `request_id` - The unique identifier of the request to reject
338 /// * `key` - The `DatabaseKey` to use for permission validation and audit trail
339 ///
340 /// # Returns
341 /// Result indicating success or failure of the rejection operation.
342 ///
343 /// # Errors
344 /// Returns `SyncError::InsufficientPermission` if the rejecting key does not have
345 /// Admin permission on the target database.
346 pub async fn reject_bootstrap_request_with_key(
347 &self,
348 request_id: &str,
349 key: &DatabaseKey,
350 ) -> Result<()> {
351 // Load the request from sync database
352 let sync_op = self.sync_tree.new_transaction().await?;
353 let manager = BootstrapRequestManager::new(&sync_op);
354
355 let request = manager
356 .get_request(request_id)
357 .await?
358 .ok_or_else(|| SyncError::RequestNotFound(request_id.to_string()))?;
359
360 // Validate request is still pending
361 if !matches!(request.status, RequestStatus::Pending) {
362 return Err(SyncError::InvalidRequestState {
363 request_id: request_id.to_string(),
364 current_status: format!("{:?}", request.status),
365 expected_status: "Pending".to_string(),
366 }
367 .into());
368 }
369
370 // Load the existing database with the user's signing key to validate permissions
371 let database = Database::open(self.instance()?, &request.tree_id, key.clone()).await?;
372
373 // Check that the rejecting user has Admin permission
374 let permission = database.current_permission().await?;
375 if !permission.can_admin() {
376 return Err(SyncError::InsufficientPermission {
377 request_id: request_id.to_string(),
378 required_permission: "Admin".to_string(),
379 actual_permission: permission,
380 }
381 .into());
382 }
383
384 // User has Admin permission, proceed with rejection
385 let rejecter_id = key.identity().display_id();
386 let rejection_time = self
387 .instance
388 .upgrade()
389 .ok_or(SyncError::InstanceDropped)?
390 .clock()
391 .now_rfc3339();
392 manager
393 .update_status(
394 request_id,
395 RequestStatus::Rejected {
396 rejected_by: rejecter_id.to_string(),
397 rejection_time,
398 },
399 )
400 .await?;
401 sync_op.commit().await?;
402
403 info!(
404 request_id = %request_id,
405 tree_id = %request.tree_id,
406 rejected_by = %rejecter_id,
407 "Bootstrap request rejected by user with Admin permission"
408 );
409
410 Ok(())
411 }
412}