use serde::{Deserialize, Serialize};
use tracing::{debug, info};
use super::peer_types::Address;
use crate::{Error, Result, Transaction, auth::Permission, entry::ID, store::Table};
pub(super) const BOOTSTRAP_REQUESTS_SUBTREE: &str = "bootstrap_requests";
pub(super) struct BootstrapRequestManager<'a> {
op: &'a Transaction,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BootstrapRequest {
pub tree_id: ID,
pub requesting_pubkey: String,
pub requesting_key_name: String,
pub requested_permission: Permission,
pub timestamp: String,
pub status: RequestStatus,
pub peer_address: Address,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum RequestStatus {
Pending,
Approved {
approved_by: String,
approval_time: String,
},
Rejected {
rejected_by: String,
rejection_time: String,
},
}
impl<'a> BootstrapRequestManager<'a> {
pub(super) fn new(op: &'a Transaction) -> Self {
Self { op }
}
pub(super) fn store_request(&self, request: BootstrapRequest) -> Result<String> {
let requests = self
.op
.get_store::<Table<BootstrapRequest>>(BOOTSTRAP_REQUESTS_SUBTREE)?;
debug!(tree_id = %request.tree_id, "Storing bootstrap request");
let request_id = requests.insert(request.clone())?;
info!(request_id = %request_id, tree_id = %request.tree_id, "Successfully stored bootstrap request");
Ok(request_id)
}
pub(super) fn get_request(&self, request_id: &str) -> Result<Option<BootstrapRequest>> {
let requests = self
.op
.get_store::<Table<BootstrapRequest>>(BOOTSTRAP_REQUESTS_SUBTREE)?;
match requests.get(request_id) {
Ok(request) => Ok(Some(request)),
Err(Error::Store(crate::store::StoreError::KeyNotFound { .. })) => Ok(None),
Err(e) => Err(e),
}
}
fn filter_requests(
&self,
status_filter: &RequestStatus,
) -> Result<Vec<(String, BootstrapRequest)>> {
let requests = self
.op
.get_store::<Table<BootstrapRequest>>(BOOTSTRAP_REQUESTS_SUBTREE)?;
let results = requests.search(|request| {
std::mem::discriminant(status_filter) == std::mem::discriminant(&request.status)
})?;
Ok(results)
}
pub(super) fn pending_requests(&self) -> Result<Vec<(String, BootstrapRequest)>> {
self.filter_requests(&RequestStatus::Pending)
}
pub(super) fn approved_requests(&self) -> Result<Vec<(String, BootstrapRequest)>> {
self.filter_requests(&RequestStatus::Approved {
approved_by: String::new(),
approval_time: String::new(),
})
}
pub(super) fn rejected_requests(&self) -> Result<Vec<(String, BootstrapRequest)>> {
self.filter_requests(&RequestStatus::Rejected {
rejected_by: String::new(),
rejection_time: String::new(),
})
}
pub(super) fn update_status(&self, request_id: &str, new_status: RequestStatus) -> Result<()> {
let requests = self
.op
.get_store::<Table<BootstrapRequest>>(BOOTSTRAP_REQUESTS_SUBTREE)?;
let mut request = requests.get(request_id)?;
request.status = new_status;
requests.set(request_id, request)?;
debug!(request_id = %request_id, "Updated bootstrap request status");
Ok(())
}
}
pub(super) fn current_timestamp() -> String {
chrono::Utc::now().to_rfc3339()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
Database, Instance, auth::types::Permission, backend::database::InMemory,
instance::LegacyInstanceOps, sync::DEVICE_KEY_NAME,
};
fn create_test_sync_tree() -> (Instance, Database) {
let backend = Box::new(InMemory::new());
let instance = Instance::open(backend).expect("Failed to create test instance");
let mut sync_settings = crate::crdt::Doc::new();
sync_settings.set_string("name", "_sync");
sync_settings.set_string("type", "sync_settings");
let database = instance
.new_database(sync_settings, DEVICE_KEY_NAME)
.unwrap();
(instance, database)
}
fn create_test_request() -> BootstrapRequest {
BootstrapRequest {
tree_id: ID::from_bytes("test_tree_id"),
requesting_pubkey: "ed25519:test_public_key".to_string(),
requesting_key_name: "laptop_key".to_string(),
requested_permission: Permission::Write(5),
timestamp: current_timestamp(),
status: RequestStatus::Pending,
peer_address: Address {
transport_type: "http".to_string(),
address: "127.0.0.1:8080".to_string(),
},
}
}
#[test]
fn test_store_and_get_request() {
let (_instance, sync_tree) = create_test_sync_tree();
let op = sync_tree.new_transaction().unwrap();
let manager = BootstrapRequestManager::new(&op);
let request = create_test_request();
let request_id = manager.store_request(request.clone()).unwrap();
let retrieved = manager.get_request(&request_id).unwrap().unwrap();
assert_eq!(retrieved.tree_id, request.tree_id);
assert_eq!(retrieved.requesting_pubkey, request.requesting_pubkey);
assert_eq!(retrieved.requesting_key_name, request.requesting_key_name);
assert_eq!(retrieved.requested_permission, request.requested_permission);
assert_eq!(retrieved.status, request.status);
assert_eq!(retrieved.peer_address, request.peer_address);
}
#[test]
fn test_list_requests() {
let (_instance, sync_tree) = create_test_sync_tree();
let op = sync_tree.new_transaction().unwrap();
let manager = BootstrapRequestManager::new(&op);
let request1 = create_test_request();
let mut request2 = create_test_request();
request2.status = RequestStatus::Approved {
approved_by: "admin".to_string(),
approval_time: current_timestamp(),
};
manager.store_request(request1).unwrap();
manager.store_request(request2).unwrap();
let pending_requests = manager.pending_requests().unwrap();
assert_eq!(pending_requests.len(), 1);
let approved_requests = manager.approved_requests().unwrap();
assert_eq!(approved_requests.len(), 1);
assert!(matches!(
pending_requests[0].1.status,
RequestStatus::Pending
));
assert!(matches!(
approved_requests[0].1.status,
RequestStatus::Approved { .. }
));
}
#[test]
fn test_update_status() {
let (_instance, sync_tree) = create_test_sync_tree();
let op = sync_tree.new_transaction().unwrap();
let manager = BootstrapRequestManager::new(&op);
let request = create_test_request();
let request_id = manager.store_request(request).unwrap();
let new_status = RequestStatus::Approved {
approved_by: "admin".to_string(),
approval_time: current_timestamp(),
};
manager
.update_status(&request_id, new_status.clone())
.unwrap();
let updated_request = manager.get_request(&request_id).unwrap().unwrap();
assert_eq!(updated_request.status, new_status);
}
#[test]
fn test_get_nonexistent_request() {
let (_instance, sync_tree) = create_test_sync_tree();
let op = sync_tree.new_transaction().unwrap();
let manager = BootstrapRequestManager::new(&op);
let result = manager.get_request("nonexistent").unwrap();
assert!(result.is_none());
}
}