feat(daemon): storage::policy + PolicyChanged signal + inotify watcher

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.
This commit is contained in:
michael
2026-04-27 06:26:49 -07:00
parent 9be8e4d0b3
commit ea70386c2f
5 changed files with 157 additions and 0 deletions

View File

@@ -1,10 +1,12 @@
use anyhow::{Context, Result};
use std::path::PathBuf;
use std::sync::Arc;
use tracing::{info, warn};
mod dbus;
mod polkit;
mod state;
mod storage;
const BUS_NAME: &str = "io.dangerousthings.AuthForge";
const OBJECT_PATH: &str = "/io/dangerousthings/AuthForge";
@@ -42,6 +44,41 @@ async fn main() -> Result<()> {
info!("listening on D-Bus as {BUS_NAME} at {OBJECT_PATH}");
// Inotify watcher → PolicyChanged signal.
let policy_dir = std::env::var_os("AUTHFORGE_POLICY_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/etc/authforge/policy.d"));
let policy_store = storage::policy::PolicyStore::new(policy_dir.clone());
match policy_store.watch() {
Ok((watcher, mut rx)) => {
let conn_for_signal = conn.clone();
tokio::spawn(async move {
while rx.changed().await.is_ok() {
let iface_ref = match conn_for_signal
.object_server()
.interface::<_, dbus::AuthForge>(OBJECT_PATH)
.await
{
Ok(r) => r,
Err(e) => {
warn!(error=%e, "no AuthForge interface; skipping signal");
continue;
}
};
if let Err(e) =
dbus::AuthForge::policy_changed(iface_ref.signal_context()).await
{
warn!(error=%e, "PolicyChanged emit failed");
}
}
});
// Daemon lives until SIGTERM; deliberately leak to keep watcher alive.
std::mem::forget(watcher);
info!("watching {} for policy changes", policy_dir.display());
}
Err(e) => warn!(error=%e, "failed to start policy.d watcher"),
}
// Park forever. systemd will SIGTERM the process when stopping the unit.
std::future::pending::<()>().await;
Ok(())