feat(cli): authforgectl totp enroll/status/revoke
This commit is contained in:
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use authforge_common::policy::Policy;
|
use authforge_common::policy::Policy;
|
||||||
use authforge_common::types::{Credential, PendingFlag, PolicyApplyResult, RecoveryCodeSummary};
|
use authforge_common::types::{
|
||||||
|
Credential, PendingFlag, PolicyApplyResult, RecoveryCodeSummary, TotpEnrollment,
|
||||||
|
};
|
||||||
|
|
||||||
const BUS_NAME: &str = "io.dangerousthings.AuthForge";
|
const BUS_NAME: &str = "io.dangerousthings.AuthForge";
|
||||||
const OBJECT_PATH: &str = "/io/dangerousthings/AuthForge";
|
const OBJECT_PATH: &str = "/io/dangerousthings/AuthForge";
|
||||||
@@ -64,4 +66,16 @@ impl Daemon {
|
|||||||
pub async fn revoke_recovery_code(&self, user: &str) -> Result<bool> {
|
pub async fn revoke_recovery_code(&self, user: &str) -> Result<bool> {
|
||||||
Ok(self.proxy.call("RevokeRecoveryCode", &(user,)).await?)
|
Ok(self.proxy.call("RevokeRecoveryCode", &(user,)).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn enroll_totp(&self, user: &str) -> Result<TotpEnrollment> {
|
||||||
|
Ok(self.proxy.call("EnrollTotp", &(user,)).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_totp_enrolled(&self, user: &str) -> Result<bool> {
|
||||||
|
Ok(self.proxy.call("IsTotpEnrolled", &(user,)).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn revoke_totp(&self, user: &str) -> Result<bool> {
|
||||||
|
Ok(self.proxy.call("RevokeTotp", &(user,)).await?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,6 +241,52 @@ pub(crate) async fn dispatch(json: bool, cmd: super::Cmd) -> Result<()> {
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
super::Cmd::Totp {
|
||||||
|
cmd: super::TotpCmd::Enroll { user },
|
||||||
|
} => {
|
||||||
|
let e = d.enroll_totp(&user).await?;
|
||||||
|
if json {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&e)?);
|
||||||
|
} else {
|
||||||
|
println!("secret: {}", e.secret_b32);
|
||||||
|
println!("uri: {}", e.otpauth_uri);
|
||||||
|
println!();
|
||||||
|
println!("Scan the URI as a QR or enter the secret in your authenticator app.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super::Cmd::Totp {
|
||||||
|
cmd: super::TotpCmd::Status { user },
|
||||||
|
} => {
|
||||||
|
let enrolled = d.is_totp_enrolled(&user).await?;
|
||||||
|
if json {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::json!({ "user": user, "enrolled": enrolled })
|
||||||
|
);
|
||||||
|
} else if enrolled {
|
||||||
|
println!("{user}: enrolled");
|
||||||
|
} else {
|
||||||
|
println!("{user}: not enrolled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super::Cmd::Totp {
|
||||||
|
cmd: super::TotpCmd::Revoke { user },
|
||||||
|
} => {
|
||||||
|
let removed = d.revoke_totp(&user).await?;
|
||||||
|
if removed {
|
||||||
|
if !json {
|
||||||
|
println!("revoked TOTP for {user}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !json {
|
||||||
|
eprintln!("not enrolled: {user}");
|
||||||
|
}
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ enum Cmd {
|
|||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
cmd: RecoveryCmd,
|
cmd: RecoveryCmd,
|
||||||
},
|
},
|
||||||
|
/// Manage TOTP enrollments.
|
||||||
|
Totp {
|
||||||
|
#[command(subcommand)]
|
||||||
|
cmd: TotpCmd,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
@@ -99,6 +104,16 @@ enum RecoveryCmd {
|
|||||||
Revoke { user: String },
|
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]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
@@ -197,4 +212,35 @@ mod tests {
|
|||||||
_ => panic!("wrong subcommand"),
|
_ => 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 { .. }
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user