From e9efde9077d2ad6b74b0b6150ce081e30974e2c0 Mon Sep 17 00:00:00 2001 From: michael Date: Mon, 27 Apr 2026 09:35:06 -0700 Subject: [PATCH] feat(daemon): wire RecoveryStore into AppState with AUTHFORGE_RECOVERY_DIR override StorageConfig gains a recovery_dir field; from_env_or_defaults reads AUTHFORGE_RECOVERY_DIR (default /var/lib/authforge/recovery). AppState exposes issue/list/revoke methods that Task 6 wires through D-Bus. Test fixtures in state.rs and dbus.rs updated. Co-Authored-By: Claude Opus 4.7 (1M context) --- daemon/src/dbus.rs | 1 + daemon/src/state.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/daemon/src/dbus.rs b/daemon/src/dbus.rs index f3ec7b3..4cd6f70 100644 --- a/daemon/src/dbus.rs +++ b/daemon/src/dbus.rs @@ -220,6 +220,7 @@ central_path = "{}" crate::state::StorageConfig { policy_dir, pending_dir: tmp.path().join("pending"), + recovery_dir: tmp.path().join("recovery"), userdb_path: tmp.path().join("users.db"), pam_profile_path: tmp.path().join("authforge-pamconf"), pam_auth_update: shim, diff --git a/daemon/src/state.rs b/daemon/src/state.rs index dbaa378..b5341d9 100644 --- a/daemon/src/state.rs +++ b/daemon/src/state.rs @@ -4,6 +4,7 @@ use crate::policy_apply::{ApplyError, PolicyApplier}; use crate::storage::credentials::{CredEntry, CredentialsStore, CredsError, CredsPathResolver}; use crate::storage::pending::{PendingError, PendingStore}; use crate::storage::policy::PolicyStore; +use crate::storage::recovery::{RecoveryEntry, RecoveryStore, RecoveryStoreError}; use crate::storage::userdb::{UserDb, UserDbError}; use authforge_common::policy::{Policy, PolicyError}; use authforge_common::types::{Credential, Method, PendingFlag, PolicyApplyResult, Transport}; @@ -20,6 +21,8 @@ pub enum StateError { #[error(transparent)] Pending(#[from] PendingError), #[error(transparent)] + Recovery(#[from] RecoveryStoreError), + #[error(transparent)] Creds(#[from] CredsError), #[error(transparent)] UserDb(#[from] UserDbError), @@ -32,6 +35,7 @@ pub enum StateError { pub struct StorageConfig { pub policy_dir: PathBuf, pub pending_dir: PathBuf, + pub recovery_dir: PathBuf, pub userdb_path: PathBuf, /// Where pam-auth-update reads the AuthForge profile from. Defaults to /// `/usr/share/pam-configs/authforge`; tests / non-root runs override. @@ -51,6 +55,7 @@ impl StorageConfig { Self { policy_dir: env("AUTHFORGE_POLICY_DIR", "/etc/authforge/policy.d"), pending_dir: env("AUTHFORGE_PENDING_DIR", "/var/lib/authforge/pending"), + recovery_dir: env("AUTHFORGE_RECOVERY_DIR", "/var/lib/authforge/recovery"), userdb_path: env("AUTHFORGE_USERDB", "/var/lib/authforge/users.db"), pam_profile_path: env("AUTHFORGE_PAMCONF_PATH", "/usr/share/pam-configs/authforge"), pam_auth_update: env("AUTHFORGE_PAM_AUTH_UPDATE", "/usr/sbin/pam-auth-update"), @@ -61,6 +66,8 @@ impl StorageConfig { pub struct AppState { policy: PolicyStore, pending: PendingStore, + #[allow(dead_code)] // wired through D-Bus in Task 6. + recovery: RecoveryStore, userdb: Mutex, authn: Arc, applier: PolicyApplier, @@ -70,11 +77,13 @@ impl AppState { pub fn open(cfg: StorageConfig, authn: Arc) -> Result { let policy = PolicyStore::new(cfg.policy_dir); let pending = PendingStore::new(cfg.pending_dir); + let recovery = RecoveryStore::new(cfg.recovery_dir); let userdb = Mutex::new(UserDb::open(&cfg.userdb_path)?); let applier = PolicyApplier::new(cfg.pam_profile_path, cfg.pam_auth_update); Ok(Self { policy, pending, + recovery, userdb, authn, applier, @@ -222,6 +231,21 @@ impl AppState { Ok(self.pending.clear(user)?) } + #[allow(dead_code)] // wired through D-Bus in Task 6. + pub async fn issue_recovery(&self, user: &str) -> Result { + Ok(self.recovery.issue(user, crate::recovery::DEFAULT_TTL_SECS)?) + } + + #[allow(dead_code)] // wired through D-Bus in Task 6. + pub async fn list_recovery(&self) -> Result, StateError> { + Ok(self.recovery.list()?) + } + + #[allow(dead_code)] // wired through D-Bus in Task 6. + pub async fn revoke_recovery(&self, user: &str) -> Result { + Ok(self.recovery.revoke(user)?) + } + /// Tests assert pending flag round-trips through SetPendingFlag / /// ClearPendingFlag via this read accessor. Production readers (PAM /// module, GUI status) read the on-disk file directly in later phases. @@ -250,6 +274,7 @@ mod tests { StorageConfig { policy_dir: d.join("policy.d"), pending_dir: d.join("pending"), + recovery_dir: d.join("recovery"), userdb_path: d.join("users.db"), pam_profile_path: d.join("authforge-pamconf"), pam_auth_update: shim, @@ -309,6 +334,7 @@ mod tests { StorageConfig { policy_dir, pending_dir: d.path().join("pending"), + recovery_dir: d.path().join("recovery"), userdb_path: d.path().join("users.db"), pam_profile_path: d.path().join("authforge-pamconf"), pam_auth_update: shim,