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