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(())
|
||||||
|
}
|
||||||
170
cli/src/main.rs
170
cli/src/main.rs
@@ -1,27 +1,173 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
mod bus;
|
||||||
|
mod commands;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(
|
#[command(name = "authforgectl", version, about = "Manage authforge configuration")]
|
||||||
name = "authforgectl",
|
|
||||||
version,
|
|
||||||
about = "Manage authforge configuration"
|
|
||||||
)]
|
|
||||||
struct Cli {
|
struct Cli {
|
||||||
|
/// Emit machine-parseable JSON.
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
json: bool,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
cmd: Cmd,
|
cmd: Cmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Cmd {
|
enum Cmd {
|
||||||
|
/// Show daemon + policy status.
|
||||||
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]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let args = Cli::parse();
|
let cli = Cli::parse();
|
||||||
match args.cmd {
|
commands::dispatch(cli.json, cli.cmd).await
|
||||||
Cmd::Status => println!("authforgectl: not yet implemented"),
|
}
|
||||||
}
|
|
||||||
Ok(())
|
#[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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user