* RecoveryCmd::List no longer takes a user — lists all active codes across users, matching the daemon's ListRecoveryCodes signature. * RecoveryCmd::Revoke <user> calls RevokeRecoveryCode; exits 1 with a stderr message when the user has no active code. * bus.rs gains list_recovery_codes() / revoke_recovery_code(user). * Two new clap-parser tests cover the new subcommand shapes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
253 lines
8.7 KiB
Rust
253 lines
8.7 KiB
Rust
use crate::bus::Daemon;
|
|
use anyhow::Result;
|
|
|
|
pub(crate) async fn dispatch(json: bool, cmd: super::Cmd) -> Result<()> {
|
|
let d = Daemon::connect().await?;
|
|
|
|
match cmd {
|
|
super::Cmd::Status => {
|
|
let pol = d.get_policy().await?;
|
|
if json {
|
|
println!("{}", serde_json::to_string_pretty(&pol)?);
|
|
} else {
|
|
println!("authforge daemon: connected");
|
|
println!("policy stacks configured: {}", pol.stacks.len());
|
|
for (name, sp) in &pol.stacks {
|
|
println!(" {}: {:?} ({:?})", name, sp.mode, sp.methods);
|
|
}
|
|
}
|
|
}
|
|
|
|
super::Cmd::List { user } => {
|
|
let user = user.unwrap_or_else(whoami_or_user);
|
|
let creds = d.list_credentials(&user).await?;
|
|
if json {
|
|
println!("{}", serde_json::to_string_pretty(&creds)?);
|
|
} else if creds.is_empty() {
|
|
println!("no credentials for {user}");
|
|
} else {
|
|
for c in creds {
|
|
println!(
|
|
"{}\t{}\t{:?}\t{:?}",
|
|
c.id, c.nickname, c.method, c.transport
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
super::Cmd::Policy {
|
|
cmd: super::PolicyCmd::Show,
|
|
} => {
|
|
let pol = d.get_policy().await?;
|
|
if json {
|
|
println!("{}", serde_json::to_string_pretty(&pol)?);
|
|
} else {
|
|
println!("{}", toml::to_string_pretty(&pol).unwrap_or_default());
|
|
}
|
|
}
|
|
|
|
// Write-side subcommands (Task C4-C5).
|
|
super::Cmd::Enroll { user, nickname } => {
|
|
let user = user.unwrap_or_else(whoami_or_user);
|
|
let nickname = nickname.unwrap_or_else(|| "Security Key".to_string());
|
|
let cred = d.enroll_own(&user, &nickname).await?;
|
|
if json {
|
|
println!("{}", serde_json::to_string_pretty(&cred)?);
|
|
} else {
|
|
println!(
|
|
"enrolled credential {} ({}) for {}",
|
|
cred.id, cred.nickname, user
|
|
);
|
|
}
|
|
}
|
|
|
|
super::Cmd::Remove { user, cred_id } => {
|
|
let user = user.unwrap_or_else(whoami_or_user);
|
|
d.remove_own(&user, &cred_id).await?;
|
|
if !json {
|
|
println!("removed credential {cred_id} for {user}");
|
|
}
|
|
}
|
|
|
|
super::Cmd::Policy {
|
|
cmd:
|
|
super::PolicyCmd::Set {
|
|
stack,
|
|
mode,
|
|
methods,
|
|
},
|
|
} => {
|
|
use authforge_common::policy::StackPolicy;
|
|
use authforge_common::types::{Method, Mode};
|
|
let m = match mode.as_str() {
|
|
"disabled" => Mode::Disabled,
|
|
"optional" => Mode::Optional,
|
|
"required" => Mode::Required,
|
|
other => anyhow::bail!("unknown mode: {other}"),
|
|
};
|
|
let methods: Result<Vec<Method>> = methods
|
|
.into_iter()
|
|
.map(|s| match s.as_str() {
|
|
"fido2" => Ok(Method::Fido2),
|
|
"totp" => Ok(Method::Totp),
|
|
other => Err(anyhow::anyhow!("unknown method: {other}")),
|
|
})
|
|
.collect();
|
|
let mut pol = d.get_policy().await?;
|
|
pol.stacks.insert(
|
|
stack.clone(),
|
|
StackPolicy {
|
|
mode: m,
|
|
methods: methods?,
|
|
},
|
|
);
|
|
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 },
|
|
} => {
|
|
// 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 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 applied via pam-auth-update");
|
|
}
|
|
}
|
|
|
|
super::Cmd::Policy {
|
|
cmd: super::PolicyCmd::Validate,
|
|
} => {
|
|
// Phase 5 wires the lockout simulator. For now: syntax round-trip.
|
|
let pol = d.get_policy().await?;
|
|
let _ = toml::to_string_pretty(&pol)?;
|
|
if !json {
|
|
println!("policy is syntactically valid");
|
|
}
|
|
}
|
|
|
|
super::Cmd::Pending {
|
|
cmd: super::PendingCmd::Set { user, methods },
|
|
} => {
|
|
use authforge_common::types::{Method, PendingFlag};
|
|
let methods: Result<Vec<Method>> = methods
|
|
.into_iter()
|
|
.map(|s| match s.as_str() {
|
|
"fido2" => Ok(Method::Fido2),
|
|
"totp" => Ok(Method::Totp),
|
|
other => Err(anyhow::anyhow!("unknown method: {other}")),
|
|
})
|
|
.collect();
|
|
let now = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_secs())
|
|
.unwrap_or(0);
|
|
let flag = PendingFlag {
|
|
required_methods: methods?,
|
|
created_unix: now,
|
|
deadline_unix: 0,
|
|
re_enroll: false,
|
|
};
|
|
d.set_pending_flag(&user, &flag).await?;
|
|
if !json {
|
|
println!("pending flag set for {user}");
|
|
}
|
|
}
|
|
|
|
super::Cmd::Pending {
|
|
cmd: super::PendingCmd::Clear { user },
|
|
} => {
|
|
d.clear_pending_flag(&user).await?;
|
|
if !json {
|
|
println!("pending flag cleared for {user}");
|
|
}
|
|
}
|
|
|
|
super::Cmd::Pending {
|
|
cmd: super::PendingCmd::List,
|
|
} => {
|
|
// ListPending lands when GUI needs it (Phase 8/12 adjustment).
|
|
if !json {
|
|
println!("(ListPending D-Bus method lands when GUI needs it)");
|
|
}
|
|
}
|
|
|
|
super::Cmd::Recovery {
|
|
cmd: super::RecoveryCmd::Generate { user },
|
|
} => {
|
|
let code = d.generate_recovery_code(&user).await?;
|
|
if json {
|
|
println!("{}", serde_json::json!({ "user": user, "code": code }));
|
|
} else {
|
|
println!("{code}");
|
|
}
|
|
}
|
|
|
|
super::Cmd::Recovery {
|
|
cmd: super::RecoveryCmd::List,
|
|
} => {
|
|
let entries = d.list_recovery_codes().await?;
|
|
if json {
|
|
println!("{}", serde_json::to_string_pretty(&entries)?);
|
|
} else if entries.is_empty() {
|
|
println!("(no active recovery codes)");
|
|
} else {
|
|
for e in &entries {
|
|
println!("{:<24} expires_unix={}", e.user, e.expires_unix);
|
|
}
|
|
}
|
|
}
|
|
|
|
super::Cmd::Recovery {
|
|
cmd: super::RecoveryCmd::Revoke { user },
|
|
} => {
|
|
let removed = d.revoke_recovery_code(&user).await?;
|
|
if removed {
|
|
if !json {
|
|
println!("revoked recovery code for {user}");
|
|
}
|
|
} else {
|
|
if !json {
|
|
eprintln!("no recovery code for {user}");
|
|
}
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn whoami_or_user() -> String {
|
|
std::env::var("USER")
|
|
.or_else(|_| std::env::var("LOGNAME"))
|
|
.unwrap_or_else(|_| "unknown".into())
|
|
}
|