feat(daemon): EnrollTotp + IsTotpEnrolled + RevokeTotp D-Bus methods

This commit is contained in:
michael
2026-04-27 11:29:16 -07:00
parent 5fa81e5757
commit 8acf3f8f45
2 changed files with 89 additions and 0 deletions

View File

@@ -4,6 +4,8 @@ use authforge_common::policy::Policy;
use authforge_common::types::{
Credential, PendingFlag, PendingStatus, PolicyApplyResult, RecoveryCodeSummary,
};
#[cfg(feature = "totp")]
use authforge_common::types::TotpEnrollment;
use std::sync::Arc;
pub struct AuthForge {
@@ -174,6 +176,35 @@ impl AuthForge {
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))
}
#[cfg(feature = "totp")]
async fn enroll_totp(&self, user: String) -> zbus::fdo::Result<TotpEnrollment> {
self.authz("io.dangerousthings.AuthForge.enroll-totp")
.await?;
self.state
.enroll_totp(&user)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))
}
#[cfg(feature = "totp")]
async fn is_totp_enrolled(&self, user: String) -> zbus::fdo::Result<bool> {
// No polkit gate — pure read; daemon's pattern is gates on writes only.
self.state
.is_totp_enrolled(&user)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))
}
#[cfg(feature = "totp")]
async fn revoke_totp(&self, user: String) -> zbus::fdo::Result<bool> {
self.authz("io.dangerousthings.AuthForge.revoke-totp")
.await?;
self.state
.revoke_totp(&user)
.await
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))
}
/// Emitted by main.rs whenever the inotify watcher on the policy.d/ dir
/// reports any change. Subscribers (GUI) reload via `GetPolicy`.
#[zbus(signal)]
@@ -520,4 +551,42 @@ central_path = "{}"
.expect("signal not received within 2s")
.expect("stream closed before signal");
}
#[cfg(feature = "totp")]
#[tokio::test]
async fn enroll_totp_returns_otpauth_uri() {
let (_srv, client, _state, _tmp) = p2p_pair().await;
let p = proxy(&client).await;
let e: TotpEnrollment = p.call("EnrollTotp", &("alice",)).await.unwrap();
assert_eq!(e.user, "alice");
assert!(e.otpauth_uri.starts_with("otpauth://totp/AuthForge:alice?"));
assert!(e
.secret_b32
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit()));
}
#[cfg(feature = "totp")]
#[tokio::test]
async fn is_totp_enrolled_round_trips() {
let (_srv, client, _state, _tmp) = p2p_pair().await;
let p = proxy(&client).await;
let before: bool = p.call("IsTotpEnrolled", &("alice",)).await.unwrap();
assert!(!before);
let _: TotpEnrollment = p.call("EnrollTotp", &("alice",)).await.unwrap();
let after: bool = p.call("IsTotpEnrolled", &("alice",)).await.unwrap();
assert!(after);
}
#[cfg(feature = "totp")]
#[tokio::test]
async fn revoke_totp_removes_enrollment() {
let (_srv, client, _state, _tmp) = p2p_pair().await;
let p = proxy(&client).await;
let _: TotpEnrollment = p.call("EnrollTotp", &("alice",)).await.unwrap();
let removed: bool = p.call("RevokeTotp", &("alice",)).await.unwrap();
assert!(removed);
let after: bool = p.call("IsTotpEnrolled", &("alice",)).await.unwrap();
assert!(!after);
}
}

View File

@@ -97,4 +97,24 @@
</defaults>
</action>
<action id="io.dangerousthings.AuthForge.enroll-totp">
<description>Enroll a TOTP secret</description>
<message>Authentication is required to enroll a TOTP secret.</message>
<defaults>
<allow_any>auth_self_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>auth_self_keep</allow_active>
</defaults>
</action>
<action id="io.dangerousthings.AuthForge.revoke-totp">
<description>Revoke a TOTP enrollment</description>
<message>Administrator authentication is required to revoke a TOTP enrollment.</message>
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>