feat(cli): authforgectl subcommand structure with clap derive (Task C1)
This commit is contained in:
2
cli/src/bus.rs
Normal file
2
cli/src/bus.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
// Phase 3+6+7 plan: D-Bus client wrapper. Stubbed in Task C1; real methods
|
||||
// land in Task C2.
|
||||
7
cli/src/commands/mod.rs
Normal file
7
cli/src/commands/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub(crate) async fn dispatch(_json: bool, cmd: super::Cmd) -> Result<()> {
|
||||
eprintln!("authforgectl: subcommand dispatcher not yet wired in this build");
|
||||
let _ = cmd;
|
||||
Ok(())
|
||||
}
|
||||
168
cli/src/main.rs
168
cli/src/main.rs
@@ -1,27 +1,173 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
mod bus;
|
||||
mod commands;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "authforgectl",
|
||||
version,
|
||||
about = "Manage authforge configuration"
|
||||
)]
|
||||
#[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(clap::Subcommand)]
|
||||
#[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 { user: String },
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Cli::parse();
|
||||
match args.cmd {
|
||||
Cmd::Status => println!("authforgectl: not yet implemented"),
|
||||
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);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user