eidetica/sync/handler.rs
1//! Sync request handler trait and implementation.
2//!
3//! This module contains transport-agnostic handlers that process
4//! sync requests and generate responses. These handlers can be
5//! used by any transport implementation through the SyncHandler trait.
6
7use std::collections::BTreeMap;
8
9use async_trait::async_trait;
10use tracing::{Instrument, debug, error, info, info_span, trace, warn};
11
12use super::{
13 bootstrap_request_manager::{BootstrapRequest, BootstrapRequestManager, RequestStatus},
14 peer_manager::PeerManager,
15 peer_types::Address,
16 protocol::{
17 BootstrapResponse, HandshakeRequest, HandshakeResponse, IncrementalResponse,
18 PROTOCOL_VERSION, RequestContext, SyncRequest, SyncResponse, SyncTreeRequest,
19 },
20 user_sync_manager::UserSyncManager,
21};
22use crate::{
23 Database, Entry, Error, Instance, Result, WeakInstance,
24 auth::{
25 KeyStatus, Permission,
26 crypto::{PublicKey, create_challenge_response, generate_challenge},
27 },
28 entry::ID,
29 store::SettingsStore,
30 sync::error::SyncError,
31};
32
33/// Trait for handling sync requests with database access.
34///
35/// Implementations of this trait can process sync requests and generate
36/// appropriate responses, with full access to the database backend for
37/// storing and retrieving entries.
38#[async_trait]
39pub trait SyncHandler: Send + std::marker::Sync {
40 /// Handle a sync request and generate an appropriate response.
41 ///
42 /// This is the main entry point for processing sync messages,
43 /// regardless of which transport they arrived through.
44 ///
45 /// # Arguments
46 /// * `request` - The sync request to process
47 /// * `context` - Context about the request (remote address, etc.)
48 ///
49 /// # Returns
50 /// The appropriate response for the given request.
51 async fn handle_request(&self, request: &SyncRequest, context: &RequestContext)
52 -> SyncResponse;
53}
54
55/// Default implementation of SyncHandler with database backend access.
56pub struct SyncHandlerImpl {
57 instance: WeakInstance,
58 sync_tree_id: ID,
59}
60
61impl SyncHandlerImpl {
62 /// Create a new SyncHandlerImpl with the given instance.
63 ///
64 /// # Arguments
65 /// * `instance` - Database instance for storing and retrieving entries
66 /// * `sync_tree_id` - Root ID of the sync database for storing bootstrap requests
67 pub fn new(instance: Instance, sync_tree_id: ID) -> Self {
68 Self {
69 instance: instance.downgrade(),
70 sync_tree_id,
71 }
72 }
73
74 /// Upgrade the weak instance reference to a strong reference.
75 pub(super) fn instance(&self) -> Result<Instance> {
76 self.instance
77 .upgrade()
78 .ok_or_else(|| SyncError::InstanceDropped.into())
79 }
80
81 /// Get access to the sync tree for bootstrap request management.
82 ///
83 /// # Returns
84 /// A Database instance for the sync tree with device key authentication.
85 async fn get_sync_tree(&self) -> Result<Database> {
86 // Load sync tree with the device key
87 let instance = self.instance()?;
88 let signing_key = instance.signing_key()?.clone();
89 Ok(Database::open(&instance, &self.sync_tree_id)
90 .await?
91 .with_key(signing_key))
92 }
93
94 /// Store a bootstrap request in the sync database for manual approval.
95 ///
96 /// # Arguments
97 /// * `tree_id` - ID of the tree being requested
98 /// * `requesting_key` - Public key of the requesting device
99 /// * `requesting_key_name` - Name of the requesting key
100 /// * `requested_permission` - Permission level being requested
101 ///
102 /// # Returns
103 /// The generated UUID for the stored request
104 async fn store_bootstrap_request(
105 &self,
106 tree_id: &ID,
107 requesting_key: &PublicKey,
108 requesting_key_name: &str,
109 requested_permission: &Permission,
110 ) -> Result<String> {
111 let sync_tree = self.get_sync_tree().await?;
112 let txn = sync_tree.new_transaction().await?;
113 let manager = BootstrapRequestManager::new(&txn);
114
115 let request = BootstrapRequest {
116 tree_id: tree_id.clone(),
117 requesting_pubkey: requesting_key.clone(),
118 requesting_key_name: requesting_key_name.to_string(),
119 requested_permission: *requested_permission,
120 timestamp: self.instance()?.clock().now_rfc3339(),
121 status: RequestStatus::Pending,
122 // TODO: We need to get the actual peer address from the transport layer
123 // For now, use a placeholder that will need to be fixed when implementing notifications
124 peer_address: Address {
125 transport_type: "unknown".to_string(),
126 address: "unknown".to_string(),
127 },
128 };
129
130 let request_id = manager.store_request(request).await?;
131 txn.commit().await?;
132
133 Ok(request_id)
134 }
135}
136
137#[async_trait]
138impl SyncHandler for SyncHandlerImpl {
139 async fn handle_request(
140 &self,
141 request: &SyncRequest,
142 context: &RequestContext,
143 ) -> SyncResponse {
144 match request {
145 SyncRequest::Handshake(handshake_req) => {
146 debug!("Received handshake request");
147 self.handle_handshake(handshake_req, context).await
148 }
149 SyncRequest::SyncTree(sync_req) => {
150 debug!(tree_id = %sync_req.tree_id, tips_count = sync_req.our_tips.len(), "Received sync tree request");
151 self.handle_sync_tree(sync_req, context).await
152 }
153 SyncRequest::SendEntries(entries) => {
154 // Process and store the received entries
155 let count = entries.len();
156 info!(count = count, "Received entries for synchronization");
157
158 let instance = match self.instance() {
159 Ok(i) => i,
160 Err(e) => return SyncResponse::Error(format!("Instance dropped: {e}")),
161 };
162
163 // Group entries by tree_id so we can fire callbacks per-database.
164 // BTreeMap so iteration order is deterministic (sorted by id);
165 // sender order within a tree is preserved by per-tree push order.
166 //
167 // Root entries declare an empty `tree.root` and act as their
168 // own tree_id. Non-root entries always carry a tree_id;
169 // well-formed peers should never send a non-root entry with
170 // `root() == None`. If they do, the entry ends up filed under
171 // its own id and parent-existence checks downstream reject it.
172 let mut by_tree: BTreeMap<ID, Vec<Entry>> = BTreeMap::new();
173 for entry in entries {
174 let tree_id = entry.root().unwrap_or_else(|| entry.id());
175 by_tree.entry(tree_id).or_default().push(entry.clone());
176 }
177
178 let mut stored_count = 0usize;
179 for (tree_id, tree_entries) in by_tree {
180 let batch_size = tree_entries.len();
181 // Entries arrive over the wire without per-entry signature
182 // verification; `put_remote_entries` stores them
183 // Unverified so a future re-verification pass can promote
184 // them.
185 match instance.put_remote_entries(&tree_id, tree_entries).await {
186 Ok(n) => {
187 stored_count += n;
188 debug!(tree_id = %tree_id, requested = batch_size, stored = n, "Stored entries");
189 }
190 Err(e) => {
191 error!(tree_id = %tree_id, error = %e, "Failed to store entries batch");
192 }
193 }
194 }
195
196 debug!(
197 received = count,
198 stored = stored_count,
199 "Completed entry synchronization"
200 );
201 if count <= 1 {
202 SyncResponse::Ack
203 } else {
204 SyncResponse::Count(stored_count)
205 }
206 }
207 }
208 }
209}
210
211impl SyncHandlerImpl {
212 /// Get the highest permission level a key has in the database's auth settings.
213 ///
214 /// This looks up all permissions the key has (direct + global wildcard) and returns
215 /// the highest one. Used for auto-detecting permissions during bootstrap.
216 ///
217 /// # Arguments
218 /// * `tree_id` - The database/tree ID to check auth settings for
219 /// * `requesting_pubkey` - The public key to look up
220 ///
221 /// # Returns
222 /// - `Ok(Some(Permission))` if key has any permissions
223 /// - `Ok(None)` if key not found in auth settings
224 /// - `Err` if database access fails
225 async fn get_key_highest_permission(
226 &self,
227 tree_id: &ID,
228 requesting_pubkey: &PublicKey,
229 ) -> Result<Option<Permission>> {
230 let database = Database::open(&self.instance()?, tree_id).await?;
231 let transaction = database.new_transaction().await?;
232 let settings_store = SettingsStore::new(&transaction)?;
233 let auth_settings = settings_store.auth_snapshot().await?;
234
235 let results = auth_settings.find_all_sigkeys_for_pubkey(requesting_pubkey);
236
237 if results.is_empty() {
238 return Ok(None);
239 }
240
241 // Results are sorted highest first, so take the first one
242 Ok(Some(results[0].1))
243 }
244
245 /// Check if the requesting key already has sufficient permissions through existing auth.
246 ///
247 /// This uses the AuthSettings.can_access() method to check if the requesting key
248 /// already has sufficient permissions (including through global '*' permissions).
249 ///
250 /// # Arguments
251 /// * `tree_id` - The database/tree ID to check auth settings for
252 /// * `requesting_pubkey` - The public key making the request
253 /// * `requested_permission` - The permission level being requested
254 ///
255 /// # Returns
256 /// - `Ok(true)` if key has sufficient permission
257 /// - `Ok(false)` if key lacks sufficient permission or auth check fails
258 async fn check_existing_auth_permission(
259 &self,
260 tree_id: &ID,
261 requesting_pubkey: &PublicKey,
262 requested_permission: &Permission,
263 ) -> Result<bool> {
264 let database = Database::open(&self.instance()?, tree_id).await?;
265 let settings_store = database.get_settings().await?;
266
267 let auth_settings = settings_store.auth_snapshot().await?;
268
269 // Use the AuthSettings.can_access() method to check permissions
270 if auth_settings.can_access(requesting_pubkey, requested_permission) {
271 debug!(
272 tree_id = %tree_id,
273 requesting_pubkey = %requesting_pubkey,
274 requested_permission = ?requested_permission,
275 "Key has sufficient permission for bootstrap access"
276 );
277 return Ok(true);
278 }
279
280 Ok(false)
281 }
282
283 /// Check if a database requires authentication for unauthenticated requests.
284 ///
285 /// This method checks if the database requires authentication for bootstrap requests
286 /// that don't provide credentials. A database allows unauthenticated access if:
287 /// 1. It has no auth settings configured at all (empty auth), OR
288 /// 2. It has a global `*` permission configured that allows unauthenticated access
289 ///
290 /// # Arguments
291 /// * `tree_id` - The database/tree ID to check auth configuration for
292 ///
293 /// # Returns
294 /// - `Ok(true)` if database requires authentication (has auth but no global permission)
295 /// - `Ok(false)` if database allows unauthenticated access (no auth or has global permission)
296 /// - `Err` if the check fails
297 async fn check_if_database_has_auth(&self, tree_id: &ID) -> Result<bool> {
298 let database = Database::open(&self.instance()?, tree_id).await?;
299 let transaction = database.new_transaction().await?;
300 let settings_store = SettingsStore::new(&transaction)?;
301
302 let auth_settings = settings_store.auth_snapshot().await?;
303
304 // Check if auth settings is completely empty (no auth configured)
305 if auth_settings.as_doc().is_empty() {
306 debug!(
307 tree_id = %tree_id,
308 "Database has no auth configured - allowing unauthenticated access"
309 );
310 return Ok(false); // No auth required
311 }
312
313 // Auth is configured - check if there's an Active global permission
314 if let Ok(global_key) = auth_settings.get_global_key()
315 && *global_key.status() == KeyStatus::Active
316 {
317 debug!(
318 tree_id = %tree_id,
319 global_permission = ?global_key.permissions(),
320 "Database has global permission - allowing unauthenticated access"
321 );
322 return Ok(false); // Global permission allows unauthenticated access
323 }
324
325 // Auth is configured but no global permission - require authentication
326 debug!(
327 tree_id = %tree_id,
328 "Database has auth configured without global permission - requiring authentication"
329 );
330 Ok(true) // Auth required
331 }
332
333 /// Check if a database has sync enabled by at least one user.
334 ///
335 /// This is a security-critical check that determines if a database should accept
336 /// any sync requests at all. A database is only eligible for sync if at least one
337 /// user has it in their preferences with `sync_enabled: true`.
338 ///
339 /// # Security
340 /// This method implements fail-closed behavior:
341 /// - Returns `false` on any error (no information leakage)
342 /// - Returns `false` if no users have the database in preferences
343 /// - Returns `false` if combined_settings.sync_enabled is false
344 /// - Only returns `true` if explicitly enabled
345 ///
346 /// # Arguments
347 /// * `tree_id` - The ID of the database to check
348 ///
349 /// # Returns
350 /// `true` if the database has sync enabled, `false` otherwise (including errors)
351 async fn is_database_sync_enabled(&self, tree_id: &ID) -> bool {
352 let instance = match self.instance() {
353 Ok(i) => i,
354 Err(_) => return false, // Fail closed
355 };
356
357 let signing_key = match instance.signing_key() {
358 Ok(k) => k.clone(),
359 Err(_) => return false, // Fail closed
360 };
361
362 let sync_database = match Database::open(&instance, &self.sync_tree_id).await {
363 Ok(db) => db.with_key(signing_key),
364 Err(_) => return false, // Fail closed
365 };
366
367 let transaction = match sync_database.new_transaction().await {
368 Ok(tx) => tx,
369 Err(_) => return false, // Fail closed
370 };
371
372 // Use UserSyncManager to get combined settings
373 let user_mgr = UserSyncManager::new(&transaction);
374 match user_mgr.get_combined_settings(tree_id).await {
375 Ok(Some(settings)) => settings.sync_enabled,
376 _ => false, // Fail closed: no settings or error
377 }
378 }
379
380 /// Register an incoming peer and add their addresses to the peer list.
381 ///
382 /// This method registers a peer that initiated a connection to us during handshake.
383 /// It adds both the peer-advertised addresses and the transport-provided remote address.
384 ///
385 /// # Arguments
386 /// * `peer_pubkey` - The peer's public key
387 /// * `display_name` - Optional display name for the peer
388 /// * `advertised_addresses` - Addresses the peer advertised in their handshake
389 /// * `remote_address` - The actual address from which the connection originated
390 ///
391 /// # Returns
392 /// Result indicating success or failure of registration
393 async fn register_incoming_peer(
394 &self,
395 peer_pubkey: &PublicKey,
396 display_name: Option<&str>,
397 advertised_addresses: &[Address],
398 remote_address: &Option<Address>,
399 ) -> Result<()> {
400 let sync_tree = self.get_sync_tree().await?;
401 let txn = sync_tree.new_transaction().await?;
402 let peer_manager = PeerManager::new(&txn);
403
404 // Try to register the peer (ignore if already exists)
405 match peer_manager.register_peer(peer_pubkey, display_name).await {
406 Ok(()) => {
407 info!(peer_pubkey = %peer_pubkey, "Registered new incoming peer");
408 }
409 Err(Error::Sync(ref e)) if matches!(**e, SyncError::PeerAlreadyExists(_)) => {
410 debug!(peer_pubkey = %peer_pubkey, "Peer already registered, updating addresses");
411 }
412 Err(e) => return Err(e),
413 }
414
415 // Add all advertised addresses
416 for addr in advertised_addresses {
417 if let Err(e) = peer_manager.add_address(peer_pubkey, addr.clone()).await {
418 warn!(peer_pubkey = %peer_pubkey, address = ?addr, error = %e, "Failed to add advertised address");
419 }
420 }
421
422 // Add the remote address from transport if available
423 if let Some(addr) = remote_address
424 && let Err(e) = peer_manager.add_address(peer_pubkey, addr.clone()).await
425 {
426 warn!(peer_pubkey = %peer_pubkey, address = ?addr, error = %e, "Failed to add remote address");
427 }
428
429 txn.commit().await?;
430 Ok(())
431 }
432
433 /// Track tree/peer sync relationship when a peer requests a tree.
434 ///
435 /// This method adds the tree to the peer's sync list, enabling bidirectional
436 /// sync for the requested tree. This is critical for `sync_on_commit` to work
437 /// in both directions.
438 ///
439 /// # Arguments
440 /// * `tree_id` - The ID of the tree being requested
441 /// * `peer_pubkey` - The public key of the peer requesting the tree (device key, not auth key)
442 ///
443 /// # Returns
444 /// Result indicating success or failure
445 async fn track_tree_sync_relationship(
446 &self,
447 tree_id: &ID,
448 peer_pubkey: &PublicKey,
449 ) -> Result<()> {
450 let sync_tree = self.get_sync_tree().await?;
451 let txn = sync_tree.new_transaction().await?;
452 let peer_manager = PeerManager::new(&txn);
453
454 // Add the tree sync relationship
455 peer_manager.add_tree_sync(peer_pubkey, tree_id).await?;
456 txn.commit().await?;
457
458 debug!(tree_id = %tree_id, peer_pubkey = %peer_pubkey, "Tracked tree/peer sync relationship");
459 Ok(())
460 }
461
462 /// Handle a handshake request from a peer.
463 async fn handle_handshake(
464 &self,
465 request: &HandshakeRequest,
466 context: &RequestContext,
467 ) -> SyncResponse {
468 async move {
469 debug!(
470 peer_device_id = %request.device_id,
471 peer_public_key = %request.public_key,
472 display_name = ?request.display_name,
473 protocol_version = request.protocol_version,
474 "Processing handshake request"
475 );
476
477 // Check protocol version compatibility
478 if request.protocol_version != PROTOCOL_VERSION {
479 warn!(
480 expected = PROTOCOL_VERSION,
481 received = request.protocol_version,
482 "Protocol version mismatch"
483 );
484 return SyncResponse::Error(format!(
485 "Protocol version mismatch: expected {}, got {}",
486 PROTOCOL_VERSION, request.protocol_version
487 ));
488 }
489
490 // Get device signing key from backend
491 let instance = match self.instance() {
492 Ok(i) => i,
493 Err(e) => {
494 error!(error = %e, "Failed to get instance");
495 return SyncResponse::Error(format!("Failed to get instance: {e}"));
496 }
497 };
498 let signing_key = match instance.signing_key() {
499 Ok(k) => k.clone(),
500 Err(e) => {
501 error!(error = %e, "Failed to get device key");
502 return SyncResponse::Error(format!("Failed to get device key: {e}"));
503 }
504 };
505
506 // Generate device ID and public key from signing key
507 let public_key = signing_key.public_key();
508 let device_id = public_key.clone(); // Device ID is the public key
509
510 // Sign the challenge with our device key to prove identity
511 let challenge_response = create_challenge_response(&request.challenge, &signing_key);
512
513 // Generate a new challenge for mutual authentication
514 let new_challenge = generate_challenge();
515
516 // Get available trees for discovery
517 let available_trees = self.get_available_trees().await;
518
519 // Register the peer and add their addresses to our peer list
520 match self.register_incoming_peer(&request.public_key, request.display_name.as_deref(), &request.listen_addresses, &context.remote_address).await {
521 Ok(()) => {
522 debug!(peer_pubkey = %request.public_key, "Successfully registered incoming peer");
523 }
524 Err(e) => {
525 // Log the error but don't fail the handshake - peer registration is best-effort
526 warn!(peer_pubkey = %request.public_key, error = %e, "Failed to register incoming peer");
527 }
528 }
529
530 info!(
531 our_device_id = %device_id,
532 peer_device_id = %request.device_id,
533 tree_count = available_trees.len(),
534 "Handshake completed successfully"
535 );
536
537 SyncResponse::Handshake(HandshakeResponse {
538 device_id,
539 public_key,
540 display_name: Some("Eidetica Peer".to_string()),
541 protocol_version: PROTOCOL_VERSION,
542 challenge_response,
543 new_challenge,
544 available_trees,
545 })
546 }
547 .instrument(info_span!("handle_handshake", peer = %request.device_id))
548 .await
549 }
550
551 /// Handle a unified sync tree request (bootstrap or incremental).
552 ///
553 /// This method routes between two sync modes:
554 /// 1. **Bootstrap**: When peer has no tips (empty database), sends complete tree
555 /// 2. **Incremental**: When peer has existing tips, sends only new entries
556 ///
557 /// # Bootstrap Authentication
558 /// During bootstrap, if the peer provides authentication credentials:
559 /// - `requesting_key`: Public key to add
560 /// - `requesting_key_name`: Name for the key
561 /// - `requested_permission`: Access level requested
562 ///
563 /// The handler will evaluate the bootstrap policy and either:
564 /// - Auto-approve and add the key immediately
565 /// - Store request for manual approval
566 /// - Proceed without authentication (anonymous bootstrap)
567 async fn handle_sync_tree(
568 &self,
569 request: &SyncTreeRequest,
570 context: &RequestContext,
571 ) -> SyncResponse {
572 async move {
573 trace!(tree_id = %request.tree_id, "Processing sync tree request");
574
575 // Track tree/peer sync relationship for bidirectional sync
576 // IMPORTANT: Only use context.peer_pubkey (device key from handshake)
577 // Do NOT use request.requesting_key (that's an auth key for database access)
578 if let Some(peer_pubkey) = &context.peer_pubkey {
579 if let Err(e) = self.track_tree_sync_relationship(&request.tree_id, peer_pubkey).await {
580 // Log the error but don't fail the sync - relationship tracking is best-effort
581 warn!(tree_id = %request.tree_id, peer_pubkey = %peer_pubkey, error = %e, "Failed to track tree/peer relationship");
582 }
583 } else {
584 debug!(tree_id = %request.tree_id, "No peer pubkey in context, skipping relationship tracking");
585 }
586
587 // Check if peer needs bootstrap (empty tips indicates no local data)
588 if request.our_tips.is_empty() {
589 debug!(tree_id = %request.tree_id, "Peer needs bootstrap - sending full tree");
590 return self.handle_bootstrap_request(&request.tree_id,
591 request.requesting_key.as_ref(),
592 request.requesting_key_name.as_deref(),
593 request.requested_permission).await;
594 }
595
596 // Handle incremental sync (peer has existing data, needs updates)
597 debug!(tree_id = %request.tree_id, peer_tips = request.our_tips.len(), "Handling incremental sync");
598 self.handle_incremental_sync(&request.tree_id, &request.our_tips).await
599 }
600 .instrument(info_span!("handle_sync_tree", tree = %request.tree_id))
601 .await
602 }
603
604 /// Handle bootstrap request by sending complete tree state and optionally approving auth key.
605 ///
606 /// Bootstrap is the initial synchronization when a peer has no local data for a tree.
607 /// This method:
608 /// 1. Validates the tree exists and sync is enabled
609 /// 2. Processes authentication and permission resolution
610 /// 3. Sends all entries from the tree to the peer
611 ///
612 /// # Authentication Flow
613 ///
614 /// The bootstrap process handles three authentication scenarios:
615 ///
616 /// ## 1. Explicit Permission Request
617 /// When all three auth parameters are provided (`requesting_key`, `requesting_key_name`, `requested_permission`):
618 /// - Check if key already has sufficient permissions
619 /// - If yes: Approve immediately without adding key
620 /// - If no: Store request for manual approval and return `BootstrapPending`
621 ///
622 /// ## 2. Auto-Detection
623 /// When key is provided but `requested_permission` is `None`:
624 /// - Look up key's existing permissions in database auth settings
625 /// - Uses `find_all_sigkeys_for_pubkey()` to find all permissions (direct + global wildcard)
626 /// - If key found: Use highest available permission and approve immediately
627 /// - If key not found: Reject with authentication error
628 ///
629 /// ## 3. Unauthenticated Access
630 /// When no `requesting_key` is provided:
631 /// - Only allowed if database has no auth configured or has global wildcard permission
632 /// - Otherwise rejected with authentication required error
633 ///
634 /// # Note on Key Verification
635 ///
636 /// This function does not verify that the peer actually controls the `requesting_key`.
637 /// The `requesting_key` parameter is an unverified string from the client.
638 ///
639 /// **This is not a security vulnerability** because:
640 /// - Approval only adds the public key to database auth settings
641 /// - Actual database access requires signing entries with the corresponding private key
642 /// - If an attacker claims someone else's public key, approval grants access to the
643 /// legitimate key holder (who has the private key), not the attacker
644 ///
645 /// The lack of verification may cause:
646 /// - Audit trail confusion (request appears to come from a different identity)
647 /// - Admins approving access for keys that didn't actually request it
648 ///
649 /// # Arguments
650 /// * `tree_id` - The database/tree to bootstrap
651 /// * `requesting_key` - Optional public key requesting access (unverified, but safe - see above)
652 /// * `requesting_key_name` - Optional name/identifier for the key (unverified)
653 /// * `requested_permission` - Optional permission level requested (if None, auto-detects from auth settings)
654 ///
655 /// # Returns
656 /// - `BootstrapResponse`: Contains entries and approval status (key_approved, granted_permission)
657 /// - `BootstrapPending`: Manual approval required (request queued)
658 /// - `Error`: Tree not found, auth required, key not authorized, or processing failure
659 async fn handle_bootstrap_request(
660 &self,
661 tree_id: &ID,
662 requesting_key: Option<&PublicKey>,
663 requesting_key_name: Option<&str>,
664 requested_permission: Option<Permission>,
665 ) -> SyncResponse {
666 // SECURITY: Check if database has sync enabled (FIRST CHECK - before anything else)
667 // This prevents information leakage about database existence: the gate
668 // returns false both for databases that are absent and for databases that
669 // are present-but-not-tracked-for-sync, and we deliberately respond with
670 // the same opaque "Tree not found" to peers in either case.
671 if !self.is_database_sync_enabled(tree_id).await {
672 warn!(
673 tree_id = %tree_id,
674 requesting_key = ?requesting_key,
675 requesting_key_name = ?requesting_key_name,
676 "Bootstrap request rejected: database is absent or has no user with sync enabled (responding as not-found)"
677 );
678 return SyncResponse::Error(format!("Tree not found: {tree_id}"));
679 }
680
681 // Get the root entry (to verify tree exists)
682 let instance = match self.instance() {
683 Ok(i) => i,
684 Err(e) => return SyncResponse::Error(format!("Instance dropped: {e}")),
685 };
686 let _root_entry = match instance.backend().get(tree_id).await {
687 Ok(entry) => entry,
688 Err(e) if e.is_not_found() => {
689 warn!(
690 tree_id = %tree_id,
691 requesting_key = ?requesting_key,
692 requesting_key_name = ?requesting_key_name,
693 "Bootstrap request rejected: a user has this tree marked sync-enabled but the backend has no root entry for it"
694 );
695 return SyncResponse::Error(format!("Tree not found: {tree_id}"));
696 }
697 Err(e) => {
698 error!(tree_id = %tree_id, error = %e, "Failed to get root entry");
699 return SyncResponse::Error(format!("Failed to get tree root: {e}"));
700 }
701 };
702
703 // Check if database has authentication configured
704 let auth_configured = match self.check_if_database_has_auth(tree_id).await {
705 Ok(has_auth) => has_auth,
706 Err(e) => {
707 error!(tree_id = %tree_id, error = %e, "Failed to check if database has auth");
708 return SyncResponse::Error(format!("Failed to check database auth: {e}"));
709 }
710 };
711
712 // If auth is configured but no credentials provided, reject the request
713 if auth_configured && requesting_key.is_none() {
714 warn!(
715 tree_id = %tree_id,
716 "Unauthenticated bootstrap request rejected - database requires authentication"
717 );
718 return SyncResponse::Error(
719 "Authentication required: This database requires authenticated access. \
720 Please provide credentials (requesting_key, requesting_key_name, requested_permission) \
721 to bootstrap sync.".to_string()
722 );
723 }
724
725 // Handle key approval for bootstrap requests FIRST
726 let (key_approved, granted_permission) = match (
727 requesting_key,
728 requesting_key_name,
729 requested_permission,
730 ) {
731 // Case 1: All three parameters provided - explicit permission request
732 (Some(key), Some(key_name), Some(permission)) => {
733 info!(
734 tree_id = %tree_id,
735 requesting_key = %key,
736 key_name = %key_name,
737 requested_permission = ?permission,
738 "Processing key approval request for bootstrap"
739 );
740
741 // Check if the requesting key already has sufficient permissions through existing auth
742 match self
743 .check_existing_auth_permission(tree_id, key, &permission)
744 .await
745 {
746 Ok(true) => {
747 // Key already has sufficient permission - approve without adding
748 info!(
749 tree_id = %tree_id,
750 key = %key,
751 permission = ?permission,
752 "Bootstrap approved via existing auth permission - no key added"
753 );
754 (true, Some(permission))
755 }
756 Ok(false) => {
757 // No existing permission, store request for manual approval
758 info!(tree_id = %tree_id, "Bootstrap key approval requested - storing for manual approval");
759
760 // Store the bootstrap request in sync database for manual approval
761 match self
762 .store_bootstrap_request(tree_id, key, key_name, &permission)
763 .await
764 {
765 Ok(request_id) => {
766 info!(
767 tree_id = %tree_id,
768 request_id = %request_id,
769 "Bootstrap request stored for manual approval"
770 );
771 return SyncResponse::BootstrapPending {
772 request_id,
773 message: "Bootstrap request pending manual approval"
774 .to_string(),
775 };
776 }
777 Err(e) => {
778 error!(
779 tree_id = %tree_id,
780 error = %e,
781 "Failed to store bootstrap request"
782 );
783 return SyncResponse::Error(format!(
784 "Failed to store bootstrap request: {e}"
785 ));
786 }
787 }
788 }
789 Err(e) => {
790 error!(tree_id = %tree_id, error = %e, "Failed to check global permission for bootstrap");
791 return SyncResponse::Error(format!("Global permission check failed: {e}"));
792 }
793 }
794 }
795
796 // Case 2: Key provided but permission not specified - auto-detect from auth settings
797 (Some(key), Some(_key_name), None) => {
798 info!(
799 tree_id = %tree_id,
800 requesting_key = %key,
801 "Auto-detecting permission from auth settings for bootstrap request"
802 );
803
804 match self.get_key_highest_permission(tree_id, key).await {
805 Ok(Some(permission)) => {
806 info!(
807 tree_id = %tree_id,
808 requesting_key = %key,
809 detected_permission = ?permission,
810 "Approved bootstrap using auto-detected permission from auth settings"
811 );
812 (true, Some(permission))
813 }
814 Ok(None) => {
815 warn!(
816 tree_id = %tree_id,
817 requesting_key = %key,
818 "Key not found in auth settings - rejecting bootstrap request"
819 );
820 return SyncResponse::Error(
821 "Authentication required: provided key is not authorized for this database".to_string()
822 );
823 }
824 Err(e) => {
825 error!(
826 tree_id = %tree_id,
827 requesting_key = %key,
828 error = %e,
829 "Failed to lookup key permissions"
830 );
831 return SyncResponse::Error(format!("Failed to access auth settings: {e}"));
832 }
833 }
834 }
835
836 // Case 3: No key provided, or key provided without key_name - unauthenticated access
837 _ => {
838 debug!(
839 tree_id = %tree_id,
840 "No authentication credentials provided - proceeding with unauthenticated bootstrap"
841 );
842 (false, None)
843 }
844 };
845
846 // NOW collect all entries after key approval (so we get the updated database state)
847 let all_entries = match self.collect_all_entries_for_bootstrap(tree_id).await {
848 Ok(entries) => entries,
849 Err(e) => {
850 error!(tree_id = %tree_id, error = %e, "Failed to collect all entries for bootstrap after key approval");
851 return SyncResponse::Error(format!(
852 "Failed to collect all entries for bootstrap: {e}"
853 ));
854 }
855 };
856
857 // For bootstrap, we need to send the actual root entry (tree_id) as root_entry
858 // The root_entry should always be the tree's root, not a tip
859 let instance = match self.instance() {
860 Ok(i) => i,
861 Err(e) => return SyncResponse::Error(format!("Instance dropped: {e}")),
862 };
863 let root_entry = match instance.backend().get(tree_id).await {
864 Ok(entry) => entry,
865 Err(e) => {
866 error!(tree_id = %tree_id, error = %e, "Failed to get root entry");
867 return SyncResponse::Error(format!("Failed to get root entry: {e}"));
868 }
869 };
870
871 // Filter out the root from all_entries since we send it separately as root_entry
872 let other_entries: Vec<_> = all_entries
873 .into_iter()
874 .filter(|entry| entry.id() != *tree_id)
875 .collect();
876
877 info!(
878 tree_id = %tree_id,
879 entry_count = other_entries.len() + 1,
880 key_approved = key_approved,
881 "Sending bootstrap response"
882 );
883
884 SyncResponse::Bootstrap(BootstrapResponse {
885 tree_id: tree_id.clone(),
886 root_entry,
887 all_entries: other_entries,
888 key_approved,
889 granted_permission,
890 })
891 }
892
893 /// Handle incremental sync request
894 async fn handle_incremental_sync(&self, tree_id: &ID, peer_tips: &[ID]) -> SyncResponse {
895 // SECURITY: Check if database has sync enabled (FIRST CHECK - before anything else)
896 // This prevents information leakage about database existence: the gate
897 // returns false both for databases that are absent and for databases that
898 // are present-but-not-tracked-for-sync, and we deliberately respond with
899 // the same opaque "Tree not found" to peers in either case.
900 if !self.is_database_sync_enabled(tree_id).await {
901 warn!(
902 tree_id = %tree_id,
903 peer_tip_count = peer_tips.len(),
904 "Incremental sync request rejected: database is absent or has no user with sync enabled (responding as not-found)"
905 );
906 return SyncResponse::Error(format!("Tree not found: {tree_id}"));
907 }
908
909 // Get our current tips
910 let instance = match self.instance() {
911 Ok(i) => i,
912 Err(e) => return SyncResponse::Error(format!("Instance dropped: {e}")),
913 };
914 let our_tips: Vec<ID> = match instance.backend().snapshot(tree_id).await {
915 Ok(snap) => snap.into_tips(),
916 Err(e) => {
917 error!(tree_id = %tree_id, error = %e, "Failed to get our tips");
918 return SyncResponse::Error(format!("Failed to get tips: {e}"));
919 }
920 };
921
922 // Find entries peer is missing
923 let missing_entries = match self
924 .find_missing_entries_for_peer(&our_tips, peer_tips)
925 .await
926 {
927 Ok(entries) => entries,
928 Err(e) => {
929 error!(tree_id = %tree_id, error = %e, "Failed to find missing entries");
930 return SyncResponse::Error(format!("Failed to find missing entries: {e}"));
931 }
932 };
933
934 debug!(
935 tree_id = %tree_id,
936 our_tips = our_tips.len(),
937 peer_tips = peer_tips.len(),
938 missing_count = missing_entries.len(),
939 "Sending incremental sync response"
940 );
941
942 SyncResponse::Incremental(IncrementalResponse {
943 tree_id: tree_id.clone(),
944 their_tips: our_tips,
945 missing_entries,
946 })
947 }
948}