* 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>
201 lines
4.5 KiB
Rust
201 lines
4.5 KiB
Rust
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<String>,
|
|
#[arg(long)]
|
|
nickname: Option<String>,
|
|
},
|
|
/// List enrolled credentials for a user.
|
|
List {
|
|
#[arg(long)]
|
|
user: Option<String>,
|
|
},
|
|
/// Remove a credential by ID.
|
|
Remove {
|
|
#[arg(long)]
|
|
user: Option<String>,
|
|
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<String>,
|
|
},
|
|
/// 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<String>,
|
|
},
|
|
Clear {
|
|
user: String,
|
|
},
|
|
List,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum RecoveryCmd {
|
|
Generate { user: String },
|
|
List,
|
|
Revoke { 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);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_recovery_list_takes_no_args() {
|
|
let c = Cli::try_parse_from(["authforgectl", "recovery", "list"]).unwrap();
|
|
assert!(matches!(
|
|
c.cmd,
|
|
Cmd::Recovery {
|
|
cmd: RecoveryCmd::List
|
|
}
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_recovery_revoke_with_user() {
|
|
let c = Cli::try_parse_from(["authforgectl", "recovery", "revoke", "alice"]).unwrap();
|
|
match c.cmd {
|
|
Cmd::Recovery {
|
|
cmd: RecoveryCmd::Revoke { user },
|
|
} => assert_eq!(user, "alice"),
|
|
_ => panic!("wrong subcommand"),
|
|
}
|
|
}
|
|
}
|