Files
authforge/cli/src/main.rs

247 lines
5.7 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,
},
/// Manage TOTP enrollments.
Totp {
#[command(subcommand)]
cmd: TotpCmd,
},
}
#[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 },
}
#[derive(Subcommand)]
enum TotpCmd {
/// Generate a fresh TOTP secret for a user.
Enroll { user: String },
/// Show whether a user has a TOTP secret on file.
Status { user: String },
/// Remove a user's TOTP enrollment.
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"),
}
}
#[test]
fn parse_totp_enroll() {
let c = Cli::try_parse_from(["authforgectl", "totp", "enroll", "alice"]).unwrap();
match c.cmd {
Cmd::Totp {
cmd: TotpCmd::Enroll { user },
} => assert_eq!(user, "alice"),
_ => panic!("wrong subcommand"),
}
}
#[test]
fn parse_totp_status_and_revoke() {
assert!(matches!(
Cli::try_parse_from(["authforgectl", "totp", "status", "alice"])
.unwrap()
.cmd,
Cmd::Totp {
cmd: TotpCmd::Status { .. }
}
));
assert!(matches!(
Cli::try_parse_from(["authforgectl", "totp", "revoke", "alice"])
.unwrap()
.cmd,
Cmd::Totp {
cmd: TotpCmd::Revoke { .. }
}
));
}
}