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

eidetica/sync/
bootstrap_request_manager.rs

1//! Bootstrap request management for the sync module.
2//!
3//! This module handles storing and managing bootstrap requests that require manual approval.
4//! Bootstrap requests are stored in the sync database as an Instance-level concern.
5
6use serde::{Deserialize, Serialize};
7use tracing::{debug, info};
8
9use super::peer_types::Address;
10use crate::{
11    Error, Result, Transaction,
12    auth::{Permission, crypto::PublicKey},
13    entry::ID,
14    store::{StoreError, Table},
15};
16
17/// Private constant for bootstrap request subtree name
18pub(super) const BOOTSTRAP_REQUESTS_SUBTREE: &str = "bootstrap_requests";
19
20/// Internal bootstrap request manager for the sync module.
21///
22/// This struct manages all bootstrap request operations for the sync module,
23/// operating on a Transaction to stage changes.
24pub(super) struct BootstrapRequestManager<'a> {
25    txn: &'a Transaction,
26}
27
28/// A bootstrap request awaiting manual approval
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
30pub struct BootstrapRequest {
31    /// The tree ID being requested for access
32    pub tree_id: ID,
33    /// Public key of the requesting device
34    pub requesting_pubkey: PublicKey,
35    /// Key name identifier for the requesting key
36    pub requesting_key_name: String,
37    /// Permission level being requested
38    pub requested_permission: Permission,
39    /// When the request was made (ISO 8601 timestamp)
40    pub timestamp: String,
41    /// Current status of the request
42    pub status: RequestStatus,
43    /// Address of the requesting peer (for future notifications)
44    pub peer_address: Address,
45}
46
47/// Status of a bootstrap request
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49pub enum RequestStatus {
50    /// Request is pending approval
51    Pending,
52    /// Request has been approved
53    Approved {
54        /// Who approved the request
55        approved_by: String,
56        /// When it was approved
57        approval_time: String,
58    },
59    /// Request has been rejected
60    Rejected {
61        /// Who rejected the request
62        rejected_by: String,
63        /// When it was rejected
64        rejection_time: String,
65    },
66}
67
68impl<'a> BootstrapRequestManager<'a> {
69    /// Create a new BootstrapRequestManager that operates on the given Transaction.
70    pub(super) fn new(txn: &'a Transaction) -> Self {
71        Self { txn }
72    }
73
74    /// Store a new bootstrap request in the sync database.
75    ///
76    /// # Arguments
77    /// * `request` - The bootstrap request to store
78    ///
79    /// # Returns
80    /// The generated UUID for the request.
81    pub(super) async fn store_request(&self, request: BootstrapRequest) -> Result<String> {
82        let requests = self
83            .txn
84            .get_store::<Table<BootstrapRequest>>(BOOTSTRAP_REQUESTS_SUBTREE)
85            .await?;
86
87        debug!(tree_id = %request.tree_id, "Storing bootstrap request");
88
89        // Insert request and get generated UUID
90        let request_id = requests.insert(request.clone()).await?;
91
92        info!(request_id = %request_id, tree_id = %request.tree_id, "Successfully stored bootstrap request");
93        Ok(request_id)
94    }
95
96    /// Get a specific bootstrap request by ID.
97    ///
98    /// # Arguments
99    /// * `request_id` - The ID of the request to retrieve
100    ///
101    /// # Returns
102    /// The bootstrap request if found, None otherwise.
103    pub(super) async fn get_request(&self, request_id: &str) -> Result<Option<BootstrapRequest>> {
104        let requests = self
105            .txn
106            .get_store::<Table<BootstrapRequest>>(BOOTSTRAP_REQUESTS_SUBTREE)
107            .await?;
108
109        match requests.get(request_id).await {
110            Ok(request) => Ok(Some(request)),
111            Err(Error::Store(ref e)) if matches!(**e, StoreError::KeyNotFound { .. }) => Ok(None),
112            Err(e) => Err(e),
113        }
114    }
115
116    /// Internal method to filter bootstrap requests by status.
117    async fn filter_requests(
118        &self,
119        status_filter: &RequestStatus,
120    ) -> Result<Vec<(String, BootstrapRequest)>> {
121        let requests = self
122            .txn
123            .get_store::<Table<BootstrapRequest>>(BOOTSTRAP_REQUESTS_SUBTREE)
124            .await?;
125
126        let results = requests
127            .search(|request| {
128                std::mem::discriminant(status_filter) == std::mem::discriminant(&request.status)
129            })
130            .await?;
131
132        Ok(results)
133    }
134
135    /// Get all pending bootstrap requests.
136    ///
137    /// # Returns
138    /// A vector of (request_id, bootstrap_request) pairs for pending requests.
139    pub(super) async fn pending_requests(&self) -> Result<Vec<(String, BootstrapRequest)>> {
140        self.filter_requests(&RequestStatus::Pending).await
141    }
142
143    /// Get all approved bootstrap requests.
144    ///
145    /// # Returns
146    /// A vector of (request_id, bootstrap_request) pairs for approved requests.
147    pub(super) async fn approved_requests(&self) -> Result<Vec<(String, BootstrapRequest)>> {
148        self.filter_requests(&RequestStatus::Approved {
149            approved_by: String::new(),
150            approval_time: String::new(),
151        })
152        .await
153    }
154
155    /// Get all rejected bootstrap requests.
156    ///
157    /// # Returns
158    /// A vector of (request_id, bootstrap_request) pairs for rejected requests.
159    pub(super) async fn rejected_requests(&self) -> Result<Vec<(String, BootstrapRequest)>> {
160        self.filter_requests(&RequestStatus::Rejected {
161            rejected_by: String::new(),
162            rejection_time: String::new(),
163        })
164        .await
165    }
166
167    /// Update the status of a bootstrap request.
168    ///
169    /// # Arguments
170    /// * `request_id` - The ID of the request to update
171    /// * `new_status` - The new status to set
172    ///
173    /// # Returns
174    /// A Result indicating success or an error.
175    pub(super) async fn update_status(
176        &self,
177        request_id: &str,
178        new_status: RequestStatus,
179    ) -> Result<()> {
180        let requests = self
181            .txn
182            .get_store::<Table<BootstrapRequest>>(BOOTSTRAP_REQUESTS_SUBTREE)
183            .await?;
184
185        // Get the existing request
186        let mut request = requests.get(request_id).await?;
187
188        // Update the status
189        request.status = new_status;
190
191        // Store the updated request
192        requests.set(request_id, request).await?;
193
194        debug!(request_id = %request_id, "Updated bootstrap request status");
195        Ok(())
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::{
203        Clock, Database, Instance, auth::types::Permission, backend::database::InMemory,
204        clock::FixedClock, crdt::Doc,
205    };
206    use std::sync::Arc;
207
208    async fn create_test_sync_tree() -> (Instance, Database, Arc<FixedClock>) {
209        let clock = Arc::new(FixedClock::default());
210        let (instance, mut user) = Instance::create_backend_with_clock(
211            Box::new(InMemory::new()),
212            clock.clone(),
213            crate::NewUser::passwordless("test"),
214        )
215        .await
216        .expect("Failed to create test instance");
217
218        let mut sync_settings = Doc::new();
219        sync_settings.set("name", "_sync");
220        sync_settings.set("type", "sync_settings");
221
222        let (database, _) = user
223            .new_database()
224            .settings(sync_settings)
225            .build()
226            .await
227            .unwrap();
228
229        (instance, database, clock)
230    }
231
232    fn create_test_request(clock: &FixedClock) -> BootstrapRequest {
233        BootstrapRequest {
234            // Use a valid, prefixed ID so parsing validates correctly
235            tree_id: ID::from_bytes("test_tree_id"),
236            requesting_pubkey: PublicKey::random(),
237            requesting_key_name: "laptop_key".to_string(),
238            requested_permission: Permission::Write(5),
239            timestamp: clock.now_rfc3339(),
240            status: RequestStatus::Pending,
241            peer_address: Address {
242                transport_type: "http".to_string(),
243                address: "127.0.0.1:8080".to_string(),
244            },
245        }
246    }
247
248    #[tokio::test]
249    async fn test_store_and_get_request() {
250        let (_instance, sync_tree, clock) = create_test_sync_tree().await;
251        let txn = sync_tree.new_transaction().await.unwrap();
252        let manager = BootstrapRequestManager::new(&txn);
253
254        let request = create_test_request(&clock);
255
256        // Store the request and get the generated UUID
257        let request_id = manager.store_request(request.clone()).await.unwrap();
258
259        // Retrieve the request
260        let retrieved = manager.get_request(&request_id).await.unwrap().unwrap();
261        assert_eq!(retrieved.tree_id, request.tree_id);
262        assert_eq!(retrieved.requesting_pubkey, request.requesting_pubkey);
263        assert_eq!(retrieved.requesting_key_name, request.requesting_key_name);
264        assert_eq!(retrieved.requested_permission, request.requested_permission);
265        assert_eq!(retrieved.status, request.status);
266        assert_eq!(retrieved.peer_address, request.peer_address);
267    }
268
269    #[tokio::test]
270    async fn test_list_requests() {
271        let (_instance, sync_tree, clock) = create_test_sync_tree().await;
272        let txn = sync_tree.new_transaction().await.unwrap();
273        let manager = BootstrapRequestManager::new(&txn);
274
275        // Store multiple requests
276        let request1 = create_test_request(&clock);
277
278        let mut request2 = create_test_request(&clock);
279        request2.status = RequestStatus::Approved {
280            approved_by: "admin".to_string(),
281            approval_time: clock.now_rfc3339(),
282        };
283
284        manager.store_request(request1).await.unwrap();
285        manager.store_request(request2).await.unwrap();
286
287        // Get pending requests
288        let pending_requests = manager.pending_requests().await.unwrap();
289        assert_eq!(pending_requests.len(), 1);
290
291        // Get approved requests
292        let approved_requests = manager.approved_requests().await.unwrap();
293        assert_eq!(approved_requests.len(), 1);
294
295        // Verify statuses
296        assert!(matches!(
297            pending_requests[0].1.status,
298            RequestStatus::Pending
299        ));
300        assert!(matches!(
301            approved_requests[0].1.status,
302            RequestStatus::Approved { .. }
303        ));
304    }
305
306    #[tokio::test]
307    async fn test_update_status() {
308        let (_instance, sync_tree, clock) = create_test_sync_tree().await;
309        let txn = sync_tree.new_transaction().await.unwrap();
310        let manager = BootstrapRequestManager::new(&txn);
311
312        let request = create_test_request(&clock);
313
314        // Store the request and get the generated UUID
315        let request_id = manager.store_request(request).await.unwrap();
316
317        // Update status to approved
318        let new_status = RequestStatus::Approved {
319            approved_by: "admin".to_string(),
320            approval_time: clock.now_rfc3339(),
321        };
322        manager
323            .update_status(&request_id, new_status.clone())
324            .await
325            .unwrap();
326
327        // Verify status was updated
328        let updated_request = manager.get_request(&request_id).await.unwrap().unwrap();
329        assert_eq!(updated_request.status, new_status);
330    }
331
332    #[tokio::test]
333    async fn test_get_nonexistent_request() {
334        let (_instance, sync_tree, _clock) = create_test_sync_tree().await;
335        let txn = sync_tree.new_transaction().await.unwrap();
336        let manager = BootstrapRequestManager::new(&txn);
337
338        let result = manager.get_request("nonexistent").await.unwrap();
339        assert!(result.is_none());
340    }
341}