feat(daemon): policy apply via pam-auth-update + lockout simulator (Phase 4+5)
Lands Lane 1 of the Phase 4+5+8+15+16 parallel cycle. Phase 4 + Phase 5 must
land together because both modify the SetPolicy code path.
- daemon/src/lockout.rs — pure simulate(new_policy, registry) -> Vec<Violation>.
Iterates Required stacks, flags users with no enrolled credential of any
required method. 5 unit tests cover: optional-mode skipped, required-with-
unenrolled flagged, any-method-satisfies, empty registry, multi-stack.
- daemon/src/policy_apply.rs — PolicyApplier renders the pam-configs profile
(Default: yes when any stack requires fido2; pam_u2f.so + pam_authforge_pending
when fido2 required, only pam_authforge_pending otherwise) and runs
pam-auth-update --package. Stash-and-restore on failure: prior profile
contents are restored and pam-auth-update re-run, so a failed apply leaves
the system in its previous PAM state. 4 unit tests including a real-process
rollback test against a failing /bin/sh shim.
- daemon/src/state.rs — AppState::set_policy(p, force) returns
PolicyApplyResult. Always runs the simulator first; if violations and !force,
returns { applied: false, violations } without writing. Otherwise persists
via PolicyStore::save and invokes PolicyApplier::apply. StorageConfig grows
pam_profile_path + pam_auth_update fields (env-var driven, tests inject a
no-op /bin/sh shim into a tempdir).
- daemon/src/dbus.rs — SetPolicy signature is now (Policy, bool) -> Result.
Wire-breaking pre-alpha; CLI updated in this commit.
- cli/src/{bus,commands}.rs — set_policy takes force flag. policy set runs
with force=false and surfaces violations as a non-zero exit + stderr list
pointing the user at policy apply --force-i-know-what-im-doing. policy
apply now actually invokes pam-auth-update via the daemon.
Test count: 42 daemon (was 33; adds 5 lockout + 4 policy_apply). 13 common.
5 cli. cargo clippy --workspace --all-targets -D warnings clean.
Plan deviation: PolicyApplier::from_env() became PolicyApplier::new(profile_path,
pam_auth_update) with the env defaults moved into StorageConfig::from_env_or_defaults.
Cleaner: state owns one source of truth for env-driven path config.
This commit is contained in:
@@ -101,21 +101,45 @@ pub(crate) async fn dispatch(json: bool, cmd: super::Cmd) -> Result<()> {
|
||||
methods: methods?,
|
||||
},
|
||||
);
|
||||
let _ = d.set_policy(&pol).await?;
|
||||
let r = d.set_policy(&pol, false).await?;
|
||||
if !r.applied {
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&r)?);
|
||||
} else {
|
||||
eprintln!("policy NOT applied: would lock out users:");
|
||||
for v in &r.violations {
|
||||
eprintln!(" - {} on {}: {}", v.user, v.stack, v.reason);
|
||||
}
|
||||
eprintln!("re-run with `authforgectl policy apply --force-i-know-what-im-doing` to override.");
|
||||
}
|
||||
anyhow::bail!("lockout simulation refused policy");
|
||||
}
|
||||
if !json {
|
||||
println!("policy updated for stack {stack}");
|
||||
}
|
||||
}
|
||||
|
||||
super::Cmd::Policy {
|
||||
cmd: super::PolicyCmd::Apply { force: _ },
|
||||
cmd: super::PolicyCmd::Apply { force },
|
||||
} => {
|
||||
// Phase 4 wires real pam-auth-update; for now we re-save current
|
||||
// policy so the daemon emits PolicyChanged.
|
||||
// Re-applies the on-disk merged policy through pam-auth-update.
|
||||
// `--force-i-know-what-im-doing` bypasses the lockout simulator.
|
||||
let pol = d.get_policy().await?;
|
||||
let _ = d.set_policy(&pol).await?;
|
||||
let r = d.set_policy(&pol, force).await?;
|
||||
if !r.applied {
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&r)?);
|
||||
} else {
|
||||
eprintln!("apply refused: would lock out users:");
|
||||
for v in &r.violations {
|
||||
eprintln!(" - {} on {}: {}", v.user, v.stack, v.reason);
|
||||
}
|
||||
eprintln!("re-run with `--force-i-know-what-im-doing` to override.");
|
||||
}
|
||||
anyhow::bail!("lockout simulation refused apply");
|
||||
}
|
||||
if !json {
|
||||
println!("policy re-applied (Phase 4 wires real pam-auth-update)");
|
||||
println!("policy applied via pam-auth-update");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,10 +205,7 @@ pub(crate) async fn dispatch(json: bool, cmd: super::Cmd) -> Result<()> {
|
||||
} => {
|
||||
let code = d.generate_recovery_code(&user).await?;
|
||||
if json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({ "user": user, "code": code })
|
||||
);
|
||||
println!("{}", serde_json::json!({ "user": user, "code": code }));
|
||||
} else {
|
||||
println!("{code}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user