Lands plan tasks 2.6 (PolicyStore wraps load_from_dir / save_local), 2.7
(PolicyChanged D-Bus signal on the AuthForge interface), and 2.8 (notify-based
inotify watcher in main.rs that emits the signal on any change in the policy.d
directory). Bundled because watcher → emit signal → wraps PolicyStore is one
data flow.
- daemon/src/storage/{mod,policy}.rs — PolicyStore::{load,save,watch}; watch
returns a (RecommendedWatcher, watch::Receiver) so the caller keeps the
watcher alive.
- daemon/src/dbus.rs — adds #[zbus(signal)] policy_changed; integration test
via p2p connection asserts the signal arrives within 2s.
- daemon/src/main.rs — spawns a task that ticks PolicyChanged on every
rx.changed(), keyed off AUTHFORGE_POLICY_DIR env var (default
/etc/authforge/policy.d). Watcher leaked via std::mem::forget; daemon
lifetime = process lifetime.
Test count: 17/17 daemon (was 14) + 13/13 common.
89 lines
2.7 KiB
Rust
89 lines
2.7 KiB
Rust
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, PolicyError> {
|
|
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<notify::Event>| {
|
|
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();
|
|
}
|
|
}
|