use authforge_common::policy::{Policy, PolicyError}; use notify::{RecommendedWatcher, RecursiveMode, Watcher}; use std::path::PathBuf; pub(crate) struct PolicyStore { dir: PathBuf, } // `save` and `load` are wired through AppState in Task 2.15. Keep them now so // the storage module is the single owner of policy I/O — main.rs constructs a // store for the watcher in 2.8. #[allow(dead_code)] impl PolicyStore { pub fn new(dir: PathBuf) -> Self { Self { dir } } pub fn load(&self) -> Result { Policy::load_from_dir(&self.dir) } pub fn save(&self, p: &Policy) -> Result<(), PolicyError> { p.save_local(&self.dir) } /// Watch `self.dir` for any change. The returned receiver yields `()` each /// time something changes. The caller must keep the `RecommendedWatcher` /// alive — drop it to stop watching. pub fn watch(&self) -> notify::Result<(RecommendedWatcher, tokio::sync::watch::Receiver<()>)> { let (tx, rx) = tokio::sync::watch::channel(()); let mut watcher = notify::recommended_watcher(move |res: notify::Result| { if res.is_ok() { let _ = tx.send(()); } })?; // Safe even if the dir doesn't yet exist — daemon may install before // anyone drops a config file in. std::fs::create_dir_all(&self.dir).ok(); watcher.watch(&self.dir, RecursiveMode::NonRecursive)?; Ok((watcher, rx)) } } #[cfg(test)] mod tests { use super::*; use authforge_common::policy::StackPolicy; use authforge_common::types::{Method, Mode}; use tempfile::tempdir; #[test] fn save_then_load_roundtrip() { let d = tempdir().unwrap(); let store = PolicyStore::new(d.path().to_path_buf()); let mut p = Policy::default(); p.stacks.insert( "sudo".to_string(), StackPolicy { mode: Mode::Required, methods: vec![Method::Fido2], }, ); store.save(&p).unwrap(); let back = store.load().unwrap(); assert_eq!(back, p); } #[tokio::test] async fn watcher_ticks_on_file_change() { let d = tempdir().unwrap(); let store = PolicyStore::new(d.path().to_path_buf()); let (_watcher, mut rx) = store.watch().unwrap(); std::fs::write( d.path().join("00-base.conf"), "[stacks.sudo]\nmode = \"required\"\nmethods = [\"fido2\"]\n", ) .unwrap(); tokio::time::timeout(std::time::Duration::from_secs(2), rx.changed()) .await .expect("no change within 2s") .unwrap(); } }