Replace the random-number stub at dbus.rs:132 with state.issue_recovery,
add ListRecoveryCodes (returns Vec<RecoveryCodeSummary>) and RevokeRecoveryCode.
New polkit actions list-recovery / revoke-recovery (auth_admin_keep). Adds
4 D-Bus integration tests; the previously-stub generate-code test now
exercises the real Argon2id-backed write path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-line file format (expires_unix\nargon2id-PHC) keeps the C PAM module
parser trivial — no json-c link needed. Atomic temp+rename means a
concurrent reader never sees a half-written file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract the inline path-traversal check from PendingStore into a shared
storage::safe_user::join_user_segment helper. RecoveryStore (next) reuses it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands plan tasks 2.15 (AppState refactor with StorageConfig + open()) and 2.16
(dbus.rs tests switched to tempdir-backed AppState; storage errors threaded
through D-Bus methods as Failed). Bundled because the AppState surface change
forces dbus.rs adjustments in the same commit.
- daemon/src/state.rs: AppState::open(StorageConfig) replaces with_fixtures().
StorageConfig.from_env_or_defaults() reads AUTHFORGE_POLICY_DIR /
_PENDING_DIR / _USERDB env vars (defaults: /etc/authforge/policy.d,
/var/lib/authforge/pending, /var/lib/authforge/users.db). State delegates
list/add/remove credentials to CredsPathResolver + CredentialsStore picked
per-call from current Policy; pending and userdb operate independently.
- daemon/src/main.rs: opens state via env-driven config; reuses cfg.policy_dir
for the watcher to keep one source of truth.
- daemon/src/dbus.rs: every write method maps StateError to fdo::Error::Failed.
p2p_pair seeds 00-test.conf with [storage] backend = central pointing into
the tempdir so credential writes don't try to touch /home/<user>/...
(alice/bob/carol aren't real accounts in tests).
- Renamed: list_credentials_returns_fixture_for_alice ->
list_credentials_after_enroll. Removed: with_fixtures().
- .gitignore: add .claude/ so leftover Phase 1 worktree state isn't committed.
Test count: 26/26 daemon tests green (was 17). Common: 13/13. Clippy + fmt clean.
Lands plan tasks 2.10 (pam_u2f line format parser via CredEntry::from_line/
to_line, credId extraction), 2.11 (CredentialsStore add/remove/list with
idempotent add-by-credId), and 2.12 (CredsPathResolver dispatching central vs
per-user paths). Bundled because the three pieces compose into one storage
boundary.
CredEntry treats post-username chunks as opaque blobs split on ':', preserving
pam_u2f's full record on round-trip. credId = first comma-separated field of a
blob. add() is idempotent on credId match (Phase 3 may decide to refresh
publicKey on re-enroll; out of scope here).
Path resolution: Central -> Storage.central_path verbatim. PerUser -> getpwnam
via nix, with a /home/<user>/... fallback if NSS errors (CI users, distro
quirks); pam_u2f does the real lookup at auth time, so the fallback only
matters for write-on-enroll where the user does exist.
7 tests added; clippy + fmt clean.
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.