feat(remote): persist SPKI pins and machine configs to SQLite

- remote_machines table in sessions.db (id, label, url, token, auto_connect,
  spki_pins as JSON array, created_at, updated_at)
- session/machines.rs: save/load/delete/update_pins CRUD operations
- RemoteManager: set_session_db() + load_from_db() for startup restoration
- All mutations persist: add_machine, remove_machine, add_spki_pin,
  remove_spki_pin, TOFU auto-store — pins survive restart
- 197 cargo tests passing, 0 warnings
This commit is contained in:
Hibryda 2026-03-18 02:18:17 +01:00
parent d1463d4d1e
commit 538a31f85c
4 changed files with 170 additions and 2 deletions

View file

@ -68,12 +68,75 @@ struct RemoteMachine {
pub struct RemoteManager {
machines: Arc<Mutex<HashMap<String, RemoteMachine>>>,
session_db: Option<Arc<crate::session::SessionDb>>,
}
impl RemoteManager {
pub fn new() -> Self {
Self {
machines: Arc::new(Mutex::new(HashMap::new())),
session_db: None,
}
}
/// Set the session database for persistence. Call once after SessionDb is created.
pub fn set_session_db(&mut self, db: Arc<crate::session::SessionDb>) {
self.session_db = Some(db);
}
/// Load previously saved machines from SQLite. Call once on startup.
pub async fn load_from_db(&self) {
let db = match &self.session_db {
Some(db) => db,
None => return,
};
match db.load_remote_machines() {
Ok(records) => {
let mut machines = self.machines.lock().await;
for rec in records {
machines.insert(rec.id.clone(), RemoteMachine {
id: rec.id,
config: RemoteMachineConfig {
label: rec.label,
url: rec.url,
token: rec.token,
auto_connect: rec.auto_connect,
spki_pins: rec.spki_pins,
},
status: "disconnected".to_string(),
connection: None,
cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)),
});
}
log::info!("Loaded {} remote machines from database", machines.len());
}
Err(e) => log::warn!("Failed to load remote machines: {e}"),
}
}
/// Persist a machine config to SQLite (best-effort).
fn persist_machine(&self, machine: &RemoteMachine) {
if let Some(db) = &self.session_db {
let rec = crate::session::RemoteMachineRecord {
id: machine.id.clone(),
label: machine.config.label.clone(),
url: machine.config.url.clone(),
token: machine.config.token.clone(),
auto_connect: machine.config.auto_connect,
spki_pins: machine.config.spki_pins.clone(),
};
if let Err(e) = db.save_remote_machine(&rec) {
log::warn!("Failed to persist remote machine {}: {e}", machine.id);
}
}
}
/// Persist just the pins for a machine (best-effort).
fn persist_pins(&self, machine_id: &str, pins: &[String]) {
if let Some(db) = &self.session_db {
if let Err(e) = db.update_remote_machine_pins(machine_id, pins) {
log::warn!("Failed to persist SPKI pins for {machine_id}: {e}");
}
}
}
@ -98,6 +161,7 @@ impl RemoteManager {
connection: None,
cancelled: Arc::new(std::sync::atomic::AtomicBool::new(false)),
};
self.persist_machine(&machine);
self.machines.lock().await.insert(id.clone(), machine);
id
}
@ -114,6 +178,9 @@ impl RemoteManager {
}
machines.remove(machine_id)
.ok_or_else(|| format!("Machine {machine_id} not found"))?;
if let Some(db) = &self.session_db {
let _ = db.delete_remote_machine(machine_id);
}
Ok(())
}
@ -125,6 +192,7 @@ impl RemoteManager {
if !machine.config.spki_pins.contains(&pin) {
machine.config.spki_pins.push(pin);
}
self.persist_pins(machine_id, &machine.config.spki_pins);
Ok(())
}
@ -134,6 +202,7 @@ impl RemoteManager {
let machine = machines.get_mut(machine_id)
.ok_or_else(|| format!("Machine {machine_id} not found"))?;
machine.config.spki_pins.retain(|p| p != pin);
self.persist_pins(machine_id, &machine.config.spki_pins);
Ok(())
}
@ -188,6 +257,7 @@ impl RemoteManager {
let mut machines = self.machines.lock().await;
if let Some(machine) = machines.get_mut(machine_id) {
machine.config.spki_pins.push(hash.clone());
self.persist_pins(machine_id, &machine.config.spki_pins);
}
let _ = app.emit("remote-spki-tofu", &serde_json::json!({
"machineId": machine_id,