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

@@ -115,6 +115,13 @@ impl AuthForge {
let n: u32 = rand::rng().random_range(0..100_000_000);
Ok(format!("{n:08}"))
}
/// Emitted by main.rs whenever the inotify watcher on the policy.d/ dir
/// reports any change. Subscribers (GUI) reload via `GetPolicy`.
#[zbus(signal)]
pub async fn policy_changed(
signal_ctxt: &zbus::object_server::SignalContext<'_>,
) -> zbus::Result<()>;
}
#[cfg(test)]
@@ -263,4 +270,27 @@ mod tests {
assert_eq!(code.len(), 8);
assert!(code.chars().all(|c| c.is_ascii_digit()));
}
#[tokio::test]
async fn policy_changed_signal_fires_on_emit() {
use futures_util::stream::StreamExt;
let (server, client, _state) = p2p_pair().await;
let proxy = proxy(&client).await;
let mut stream = proxy.receive_signal("PolicyChanged").await.unwrap();
let iface_ref: zbus::object_server::InterfaceRef<AuthForge> = server
.object_server()
.interface("/io/dangerousthings/AuthForge")
.await
.unwrap();
AuthForge::policy_changed(iface_ref.signal_context())
.await
.unwrap();
let _msg = tokio::time::timeout(std::time::Duration::from_secs(2), stream.next())
.await
.expect("signal not received within 2s")
.expect("stream closed before signal");
}
}