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> = 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> = 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()) }