use anyhow::Result; use clap::{Parser, Subcommand}; mod bus; mod commands; #[derive(Parser)] #[command(name = "authforgectl", version, about = "Manage authforge configuration")] struct Cli { /// Emit machine-parseable JSON. #[arg(long, global = true)] json: bool, #[command(subcommand)] cmd: Cmd, } #[derive(Subcommand)] enum Cmd { /// Show daemon + policy status. Status, /// Enroll a security key for a user (interactive on the daemon side). Enroll { #[arg(long)] user: Option, #[arg(long)] nickname: Option, }, /// List enrolled credentials for a user. List { #[arg(long)] user: Option, }, /// Remove a credential by ID. Remove { #[arg(long)] user: Option, cred_id: String, }, /// Manage MFA policy. Policy { #[command(subcommand)] cmd: PolicyCmd, }, /// Manage first-login pending flags. Pending { #[command(subcommand)] cmd: PendingCmd, }, /// Manage recovery codes. Recovery { #[command(subcommand)] cmd: RecoveryCmd, }, } #[derive(Subcommand)] enum PolicyCmd { /// Print the merged on-disk policy. Show, /// Set a stack's mode and methods. Set { stack: String, /// disabled | optional | required mode: String, #[arg(long, value_delimiter = ',')] methods: Vec, }, /// Re-apply the current policy. Apply { #[arg(long = "force-i-know-what-im-doing")] force: bool, }, /// Validate the policy without applying. Validate, } #[derive(Subcommand)] enum PendingCmd { Set { user: String, #[arg(long, value_delimiter = ',')] methods: Vec, }, Clear { user: String, }, List, } #[derive(Subcommand)] enum RecoveryCmd { Generate { user: String }, List { user: String }, } #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); commands::dispatch(cli.json, cli.cmd).await } #[cfg(test)] mod tests { use super::*; use clap::CommandFactory; #[test] fn cli_definition_is_valid() { Cli::command().debug_assert(); } #[test] fn parse_status_subcommand() { let c = Cli::try_parse_from(["authforgectl", "status"]).unwrap(); assert!(matches!(c.cmd, Cmd::Status)); } #[test] fn parse_policy_set_with_methods() { let c = Cli::try_parse_from([ "authforgectl", "policy", "set", "sudo", "required", "--methods", "fido2,totp", ]) .unwrap(); match c.cmd { Cmd::Policy { cmd: PolicyCmd::Set { stack, mode, methods, }, } => { assert_eq!(stack, "sudo"); assert_eq!(mode, "required"); assert_eq!(methods, vec!["fido2".to_string(), "totp".to_string()]); } _ => panic!("wrong subcommand"), } } #[test] fn parse_enroll_with_user_and_nickname() { let c = Cli::try_parse_from([ "authforgectl", "enroll", "--user", "alice", "--nickname", "Yellow", ]) .unwrap(); match c.cmd { Cmd::Enroll { user, nickname } => { assert_eq!(user.as_deref(), Some("alice")); assert_eq!(nickname.as_deref(), Some("Yellow")); } _ => panic!("wrong subcommand"), } } #[test] fn json_flag_is_global() { let c = Cli::try_parse_from(["authforgectl", "--json", "status"]).unwrap(); assert!(c.json); } }