feat(daemon): D-Bus interface with all 9 stub methods + system-bus wiring
Bundles plan tasks 1.8 (read methods), 1.9 (EnrollOwn/RemoveOwn with polkit gate), 1.10 (EnrollOther, SetPolicy, pending, recovery-code), and 1.11 (main.rs system-bus registration) — they land together because Polkit::System is only constructed by main.rs, so splitting them mid-implementation would require dead_code allows that immediately reverse. Adds: - daemon/src/dbus.rs — AuthForge struct + #[zbus::interface] impl with all 9 methods. Reads (ListCredentials, GetPolicy) are unauthenticated; writes call authz() which dispatches to polkit. Includes 9 integration tests via a tokio::net::UnixStream::pair p2p connection — no system bus needed for tests. - daemon/src/main.rs — connects to system bus, picks Polkit::system or Polkit::permissive based on AUTHFORGE_POLKIT_BYPASS env var, registers the AuthForge interface at /io/dangerousthings/AuthForge, requests well-known name io.dangerousthings.AuthForge, then parks forever. - daemon/src/polkit.rs — drop dead_code allows now that System is wired. - daemon/src/state.rs — gate has_pending() behind cfg(test); production reads go through the on-disk file in later phases, not this in-memory cache. - common/src/types.rs (formatting only via rustfmt). Tests: 14/14 daemon tests pass (4 state, 1 polkit, 9 dbus). 7/7 common tests pass. cargo clippy --workspace --all-targets -D warnings clean. cargo fmt clean.
This commit is contained in:
122
Cargo.lock
generated
122
Cargo.lock
generated
@@ -91,31 +91,6 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "async-executor"
|
|
||||||
version = "1.14.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
|
|
||||||
dependencies = [
|
|
||||||
"async-task",
|
|
||||||
"concurrent-queue",
|
|
||||||
"fastrand",
|
|
||||||
"futures-lite",
|
|
||||||
"pin-project-lite",
|
|
||||||
"slab",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "async-fs"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
|
|
||||||
dependencies = [
|
|
||||||
"async-lock",
|
|
||||||
"blocking",
|
|
||||||
"futures-lite",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-io"
|
name = "async-io"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
@@ -234,6 +209,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
|
"zvariant",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -242,10 +218,17 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"authforge-common",
|
"authforge-common",
|
||||||
|
"nix 0.28.0",
|
||||||
|
"rand 0.9.4",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"zbus",
|
"zbus",
|
||||||
|
"zvariant",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -338,6 +321,12 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg_aliases"
|
name = "cfg_aliases"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -595,11 +584,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
@@ -682,6 +669,18 @@ dependencies = [
|
|||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi 5.3.0",
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@@ -690,7 +689,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi",
|
"r-efi 6.0.0",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
"wasip3",
|
"wasip3",
|
||||||
]
|
]
|
||||||
@@ -1057,6 +1056,18 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases 0.1.1",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.29.0"
|
version = "0.29.0"
|
||||||
@@ -1065,7 +1076,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases 0.2.1",
|
||||||
"libc",
|
"libc",
|
||||||
"memoffset",
|
"memoffset",
|
||||||
]
|
]
|
||||||
@@ -1237,6 +1248,12 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "r-efi"
|
name = "r-efi"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@@ -1250,8 +1267,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha",
|
"rand_chacha 0.3.1",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1261,7 +1288,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1273,6 +1310,15 @@ dependencies = [
|
|||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@@ -1546,6 +1592,7 @@ dependencies = [
|
|||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
|
"tracing",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2029,28 +2076,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
|
checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-broadcast",
|
"async-broadcast",
|
||||||
"async-executor",
|
|
||||||
"async-fs",
|
|
||||||
"async-io",
|
|
||||||
"async-lock",
|
|
||||||
"async-process",
|
"async-process",
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"async-task",
|
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"blocking",
|
|
||||||
"enumflags2",
|
"enumflags2",
|
||||||
"event-listener",
|
"event-listener",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"nix",
|
"nix 0.29.0",
|
||||||
"ordered-stream",
|
"ordered-stream",
|
||||||
"rand",
|
"rand 0.8.6",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
"sha1",
|
"sha1",
|
||||||
"static_assertions",
|
"static_assertions",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uds_windows",
|
"uds_windows",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[derive(
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, zvariant::Type)]
|
||||||
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, zvariant::Type,
|
|
||||||
)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
#[zvariant(signature = "s")]
|
#[zvariant(signature = "s")]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
@@ -12,9 +10,7 @@ pub enum Mode {
|
|||||||
Required,
|
Required,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, zvariant::Type)]
|
||||||
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, zvariant::Type,
|
|
||||||
)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
#[zvariant(signature = "s")]
|
#[zvariant(signature = "s")]
|
||||||
pub enum Method {
|
pub enum Method {
|
||||||
@@ -22,9 +18,7 @@ pub enum Method {
|
|||||||
Totp,
|
Totp,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, zvariant::Type)]
|
||||||
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, zvariant::Type,
|
|
||||||
)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
#[zvariant(signature = "s")]
|
#[zvariant(signature = "s")]
|
||||||
pub enum Transport {
|
pub enum Transport {
|
||||||
@@ -43,9 +37,7 @@ pub struct Credential {
|
|||||||
pub created_unix: u64,
|
pub created_unix: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, zvariant::Type, Default)]
|
||||||
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, zvariant::Type, Default,
|
|
||||||
)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
#[zvariant(signature = "s")]
|
#[zvariant(signature = "s")]
|
||||||
pub enum StorageBackend {
|
pub enum StorageBackend {
|
||||||
@@ -141,7 +133,10 @@ mod tests {
|
|||||||
let mut p = Policy::default();
|
let mut p = Policy::default();
|
||||||
p.stacks.insert(
|
p.stacks.insert(
|
||||||
"sudo".to_string(),
|
"sudo".to_string(),
|
||||||
StackPolicy { mode: Mode::Required, methods: vec![Method::Fido2] },
|
StackPolicy {
|
||||||
|
mode: Mode::Required,
|
||||||
|
methods: vec![Method::Fido2],
|
||||||
|
},
|
||||||
);
|
);
|
||||||
let json = serde_json::to_string(&p).unwrap();
|
let json = serde_json::to_string(&p).unwrap();
|
||||||
let back: Policy = serde_json::from_str(&json).unwrap();
|
let back: Policy = serde_json::from_str(&json).unwrap();
|
||||||
@@ -163,7 +158,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn policy_apply_result_ok_no_violations() {
|
fn policy_apply_result_ok_no_violations() {
|
||||||
let r = PolicyApplyResult { applied: true, violations: vec![] };
|
let r = PolicyApplyResult {
|
||||||
|
applied: true,
|
||||||
|
violations: vec![],
|
||||||
|
};
|
||||||
assert!(r.applied);
|
assert!(r.applied);
|
||||||
assert!(r.violations.is_empty());
|
assert!(r.violations.is_empty());
|
||||||
}
|
}
|
||||||
|
|||||||
266
daemon/src/dbus.rs
Normal file
266
daemon/src/dbus.rs
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
use crate::polkit::Polkit;
|
||||||
|
use crate::state::AppState;
|
||||||
|
use authforge_common::types::{
|
||||||
|
Credential, Method, PendingFlag, Policy, PolicyApplyResult, Transport,
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
pub struct AuthForge {
|
||||||
|
pub state: Arc<AppState>,
|
||||||
|
pub polkit: Arc<Polkit>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthForge {
|
||||||
|
async fn authz(&self, action: &str) -> zbus::fdo::Result<()> {
|
||||||
|
// Phase 1: pid 0 — permissive mode ignores it; system mode running on the
|
||||||
|
// real system bus needs sender → unique-name → pid resolution, which lands
|
||||||
|
// when CLI starts making real calls (Phase 2 / Phase 7). Until then,
|
||||||
|
// operate the daemon under AUTHFORGE_POLKIT_BYPASS=1 for smoke tests.
|
||||||
|
let pid = 0u32;
|
||||||
|
self.polkit
|
||||||
|
.check(action, pid)
|
||||||
|
.await
|
||||||
|
.map_err(|e| zbus::fdo::Error::AccessDenied(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now() -> u64 {
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus::interface(name = "io.dangerousthings.AuthForge1")]
|
||||||
|
impl AuthForge {
|
||||||
|
async fn list_credentials(&self, user: String) -> zbus::fdo::Result<Vec<Credential>> {
|
||||||
|
Ok(self.state.list_credentials(&user).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_policy(&self) -> zbus::fdo::Result<Policy> {
|
||||||
|
Ok(self.state.get_policy().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn enroll_own(&self, user: String, nickname: String) -> zbus::fdo::Result<Credential> {
|
||||||
|
self.authz("io.dangerousthings.AuthForge.enroll-own")
|
||||||
|
.await?;
|
||||||
|
let now = Self::now();
|
||||||
|
let cred = Credential {
|
||||||
|
id: format!("stub-{user}-{now}"),
|
||||||
|
nickname,
|
||||||
|
method: Method::Fido2,
|
||||||
|
transport: Transport::Usb,
|
||||||
|
created_unix: now,
|
||||||
|
};
|
||||||
|
self.state.add_credential(&user, cred.clone()).await;
|
||||||
|
Ok(cred)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_own(&self, user: String, cred_id: String) -> zbus::fdo::Result<()> {
|
||||||
|
self.authz("io.dangerousthings.AuthForge.remove-own")
|
||||||
|
.await?;
|
||||||
|
if self.state.remove_credential(&user, &cred_id).await {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(zbus::fdo::Error::Failed(format!(
|
||||||
|
"no credential {cred_id} for {user}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn enroll_other(&self, user: String, nickname: String) -> zbus::fdo::Result<Credential> {
|
||||||
|
self.authz("io.dangerousthings.AuthForge.enroll-other")
|
||||||
|
.await?;
|
||||||
|
let now = Self::now();
|
||||||
|
let cred = Credential {
|
||||||
|
id: format!("stub-{user}-{now}"),
|
||||||
|
nickname,
|
||||||
|
method: Method::Fido2,
|
||||||
|
transport: Transport::Usb,
|
||||||
|
created_unix: now,
|
||||||
|
};
|
||||||
|
self.state.add_credential(&user, cred.clone()).await;
|
||||||
|
Ok(cred)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_policy(&self, p: Policy) -> zbus::fdo::Result<PolicyApplyResult> {
|
||||||
|
self.authz("io.dangerousthings.AuthForge.set-policy")
|
||||||
|
.await?;
|
||||||
|
self.state.set_policy(p).await;
|
||||||
|
Ok(PolicyApplyResult {
|
||||||
|
applied: true,
|
||||||
|
violations: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_pending_flag(&self, user: String, flag: PendingFlag) -> zbus::fdo::Result<()> {
|
||||||
|
self.authz("io.dangerousthings.AuthForge.set-pending")
|
||||||
|
.await?;
|
||||||
|
self.state.set_pending(&user, flag).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clear_pending_flag(&self, user: String) -> zbus::fdo::Result<()> {
|
||||||
|
self.authz("io.dangerousthings.AuthForge.clear-pending")
|
||||||
|
.await?;
|
||||||
|
self.state.clear_pending(&user).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_recovery_code(&self, _user: String) -> zbus::fdo::Result<String> {
|
||||||
|
self.authz("io.dangerousthings.AuthForge.generate-recovery")
|
||||||
|
.await?;
|
||||||
|
// Stub: 8 random digits. Phase 12 replaces with Argon2id-backed real flow.
|
||||||
|
use rand::Rng;
|
||||||
|
let n: u32 = rand::rng().random_range(0..100_000_000);
|
||||||
|
Ok(format!("{n:08}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use authforge_common::types::{Method, StackPolicy};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use zbus::connection::Builder;
|
||||||
|
use zbus::Connection;
|
||||||
|
|
||||||
|
/// Build a peer-to-peer pair: server side has the AuthForge interface registered
|
||||||
|
/// at /io/dangerousthings/AuthForge, client side is a bare connection. No system
|
||||||
|
/// bus is involved.
|
||||||
|
async fn p2p_pair() -> (Connection, Connection, Arc<AppState>) {
|
||||||
|
let state = Arc::new(AppState::with_fixtures());
|
||||||
|
let auth = AuthForge {
|
||||||
|
state: state.clone(),
|
||||||
|
polkit: Arc::new(Polkit::permissive()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let guid = zbus::Guid::generate();
|
||||||
|
let (server_sock, client_sock) = tokio::net::UnixStream::pair().unwrap();
|
||||||
|
|
||||||
|
// Both sides must build concurrently — `build()` completes the handshake,
|
||||||
|
// and one side waiting on the other deadlocks. tokio::join drives them in
|
||||||
|
// parallel on this runtime.
|
||||||
|
let server_build = Builder::socket(server_sock)
|
||||||
|
.p2p()
|
||||||
|
.server(guid)
|
||||||
|
.unwrap()
|
||||||
|
.serve_at("/io/dangerousthings/AuthForge", auth)
|
||||||
|
.unwrap()
|
||||||
|
.build();
|
||||||
|
let client_build = Builder::socket(client_sock).p2p().build();
|
||||||
|
|
||||||
|
let (server, client) = tokio::join!(server_build, client_build);
|
||||||
|
(server.unwrap(), client.unwrap(), state)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn proxy(client: &Connection) -> zbus::Proxy<'_> {
|
||||||
|
zbus::Proxy::new(
|
||||||
|
client,
|
||||||
|
"io.dangerousthings.AuthForge",
|
||||||
|
"/io/dangerousthings/AuthForge",
|
||||||
|
"io.dangerousthings.AuthForge1",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn list_credentials_returns_fixture_for_alice() {
|
||||||
|
let (_srv, client, _state) = p2p_pair().await;
|
||||||
|
let p = proxy(&client).await;
|
||||||
|
let creds: Vec<Credential> = p.call("ListCredentials", &("alice",)).await.unwrap();
|
||||||
|
assert_eq!(creds.len(), 1);
|
||||||
|
assert_eq!(creds[0].method, Method::Fido2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn get_policy_returns_default() {
|
||||||
|
let (_srv, client, _state) = p2p_pair().await;
|
||||||
|
let p = proxy(&client).await;
|
||||||
|
let pol: Policy = p.call("GetPolicy", &()).await.unwrap();
|
||||||
|
assert!(pol.stacks.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn enroll_own_appends_credential() {
|
||||||
|
let (_srv, client, state) = p2p_pair().await;
|
||||||
|
let p = proxy(&client).await;
|
||||||
|
let _: Credential = p.call("EnrollOwn", &("bob", "Bob's Key")).await.unwrap();
|
||||||
|
assert_eq!(state.list_credentials("bob").await.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn remove_own_drops_credential() {
|
||||||
|
let (_srv, client, state) = p2p_pair().await;
|
||||||
|
let p = proxy(&client).await;
|
||||||
|
let _: () = p
|
||||||
|
.call("RemoveOwn", &("alice", "fixture-cred-1"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(state.list_credentials("alice").await.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn remove_own_unknown_id_errors() {
|
||||||
|
let (_srv, client, _state) = p2p_pair().await;
|
||||||
|
let p = proxy(&client).await;
|
||||||
|
let r: Result<(), zbus::Error> = p.call("RemoveOwn", &("alice", "no-such-id")).await;
|
||||||
|
assert!(r.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn enroll_other_creates_credential() {
|
||||||
|
let (_srv, client, state) = p2p_pair().await;
|
||||||
|
let p = proxy(&client).await;
|
||||||
|
let _: Credential = p.call("EnrollOther", &("carol", "Hers")).await.unwrap();
|
||||||
|
assert_eq!(state.list_credentials("carol").await.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn set_policy_replaces_state() {
|
||||||
|
let (_srv, client, state) = p2p_pair().await;
|
||||||
|
let p = proxy(&client).await;
|
||||||
|
let mut stacks = BTreeMap::new();
|
||||||
|
stacks.insert(
|
||||||
|
"sudo".to_string(),
|
||||||
|
StackPolicy {
|
||||||
|
mode: authforge_common::types::Mode::Required,
|
||||||
|
methods: vec![Method::Fido2],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let pol = Policy {
|
||||||
|
stacks,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let r: PolicyApplyResult = p.call("SetPolicy", &(pol.clone(),)).await.unwrap();
|
||||||
|
assert!(r.applied);
|
||||||
|
assert_eq!(state.get_policy().await, pol);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn set_and_clear_pending_flag() {
|
||||||
|
let (_srv, client, state) = p2p_pair().await;
|
||||||
|
let p = proxy(&client).await;
|
||||||
|
let flag = PendingFlag {
|
||||||
|
required_methods: vec![Method::Fido2],
|
||||||
|
created_unix: 0,
|
||||||
|
deadline_unix: 0,
|
||||||
|
re_enroll: false,
|
||||||
|
};
|
||||||
|
let _: () = p.call("SetPendingFlag", &("alice", flag)).await.unwrap();
|
||||||
|
assert!(state.has_pending("alice").await);
|
||||||
|
let _: () = p.call("ClearPendingFlag", &("alice",)).await.unwrap();
|
||||||
|
assert!(!state.has_pending("alice").await);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn generate_recovery_code_returns_8_digits() {
|
||||||
|
let (_srv, client, _state) = p2p_pair().await;
|
||||||
|
let p = proxy(&client).await;
|
||||||
|
let code: String = p.call("GenerateRecoveryCode", &("alice",)).await.unwrap();
|
||||||
|
assert_eq!(code.len(), 8);
|
||||||
|
assert!(code.chars().all(|c| c.is_ascii_digit()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,48 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use tracing::info;
|
use std::sync::Arc;
|
||||||
|
use tracing::{info, warn};
|
||||||
|
|
||||||
|
mod dbus;
|
||||||
mod polkit;
|
mod polkit;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
|
const BUS_NAME: &str = "io.dangerousthings.AuthForge";
|
||||||
|
const OBJECT_PATH: &str = "/io/dangerousthings/AuthForge";
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||||
.init();
|
.init();
|
||||||
info!("authforged {} starting", env!("CARGO_PKG_VERSION"));
|
info!("authforged {} starting", env!("CARGO_PKG_VERSION"));
|
||||||
// D-Bus service registration in Phase 1.
|
|
||||||
|
let conn = zbus::Connection::system()
|
||||||
|
.await
|
||||||
|
.context("connecting to system D-Bus")?;
|
||||||
|
|
||||||
|
let polkit = if std::env::var("AUTHFORGE_POLKIT_BYPASS").is_ok() {
|
||||||
|
warn!(
|
||||||
|
"AUTHFORGE_POLKIT_BYPASS set — polkit checks are DISABLED. Do not use in production."
|
||||||
|
);
|
||||||
|
polkit::Polkit::permissive()
|
||||||
|
} else {
|
||||||
|
polkit::Polkit::system(conn.clone()).await
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = Arc::new(state::AppState::with_fixtures());
|
||||||
|
let auth = dbus::AuthForge {
|
||||||
|
state: state.clone(),
|
||||||
|
polkit: Arc::new(polkit),
|
||||||
|
};
|
||||||
|
|
||||||
|
conn.object_server().at(OBJECT_PATH, auth).await?;
|
||||||
|
conn.request_name(BUS_NAME)
|
||||||
|
.await
|
||||||
|
.context("acquiring well-known bus name (another instance running?)")?;
|
||||||
|
|
||||||
|
info!("listening on D-Bus as {BUS_NAME} at {OBJECT_PATH}");
|
||||||
|
|
||||||
|
// Park forever. systemd will SIGTERM the process when stopping the unit.
|
||||||
|
std::future::pending::<()>().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,11 @@ pub enum PolkitError {
|
|||||||
Bus(#[from] zbus::Error),
|
Bus(#[from] zbus::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)] // wired up in Task 1.8.
|
|
||||||
pub enum Polkit {
|
pub enum Polkit {
|
||||||
Permissive,
|
Permissive,
|
||||||
System(Connection),
|
System(Connection),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)] // wired up in Task 1.8.
|
|
||||||
impl Polkit {
|
impl Polkit {
|
||||||
pub fn permissive() -> Self {
|
pub fn permissive() -> Self {
|
||||||
Self::Permissive
|
Self::Permissive
|
||||||
@@ -58,13 +56,18 @@ async fn system_check(conn: &Connection, action: &str, pid: u32) -> Result<(), P
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let (is_authorized, _is_challenge, _details): (bool, bool, HashMap<String, String>) = proxy
|
let (is_authorized, _is_challenge, _details): (bool, bool, HashMap<String, String>) = proxy
|
||||||
.call("CheckAuthorization", &(subject, action, details, flags, cancellation_id))
|
.call(
|
||||||
|
"CheckAuthorization",
|
||||||
|
&(subject, action, details, flags, cancellation_id),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if is_authorized {
|
if is_authorized {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(PolkitError::NotAuthorized { action: action.to_string() })
|
Err(PolkitError::NotAuthorized {
|
||||||
|
action: action.to_string(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +78,8 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn permissive_always_allows() {
|
async fn permissive_always_allows() {
|
||||||
let p = Polkit::permissive();
|
let p = Polkit::permissive();
|
||||||
p.check("io.dangerousthings.AuthForge.set-policy", 1234).await.unwrap();
|
p.check("io.dangerousthings.AuthForge.set-policy", 1234)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ struct StateInner {
|
|||||||
pending: HashMap<String, PendingFlag>,
|
pending: HashMap<String, PendingFlag>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// `dead_code` allow lifts in Task 1.8 when dbus.rs starts calling these methods.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
pub fn with_fixtures() -> Self {
|
pub fn with_fixtures() -> Self {
|
||||||
let mut inner = StateInner::default();
|
let mut inner = StateInner::default();
|
||||||
@@ -29,20 +27,36 @@ impl AppState {
|
|||||||
created_unix: 1_700_000_000,
|
created_unix: 1_700_000_000,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
Self { inner: RwLock::new(inner) }
|
Self {
|
||||||
|
inner: RwLock::new(inner),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_credentials(&self, user: &str) -> Vec<Credential> {
|
pub async fn list_credentials(&self, user: &str) -> Vec<Credential> {
|
||||||
self.inner.read().await.credentials.get(user).cloned().unwrap_or_default()
|
self.inner
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.credentials
|
||||||
|
.get(user)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_credential(&self, user: &str, c: Credential) {
|
pub async fn add_credential(&self, user: &str, c: Credential) {
|
||||||
self.inner.write().await.credentials.entry(user.to_string()).or_default().push(c);
|
self.inner
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.credentials
|
||||||
|
.entry(user.to_string())
|
||||||
|
.or_default()
|
||||||
|
.push(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_credential(&self, user: &str, cred_id: &str) -> bool {
|
pub async fn remove_credential(&self, user: &str, cred_id: &str) -> bool {
|
||||||
let mut g = self.inner.write().await;
|
let mut g = self.inner.write().await;
|
||||||
let Some(list) = g.credentials.get_mut(user) else { return false };
|
let Some(list) = g.credentials.get_mut(user) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
let before = list.len();
|
let before = list.len();
|
||||||
list.retain(|c| c.id != cred_id);
|
list.retain(|c| c.id != cred_id);
|
||||||
list.len() != before
|
list.len() != before
|
||||||
@@ -64,6 +78,10 @@ impl AppState {
|
|||||||
self.inner.write().await.pending.remove(user).is_some()
|
self.inner.write().await.pending.remove(user).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tests assert pending flag round-trips through SetPendingFlag/ClearPendingFlag
|
||||||
|
/// via this read accessor. Production readers (PAM module, GUI status) come in
|
||||||
|
/// later phases and will read the on-disk file directly, not this in-memory cache.
|
||||||
|
#[cfg(test)]
|
||||||
pub async fn has_pending(&self, user: &str) -> bool {
|
pub async fn has_pending(&self, user: &str) -> bool {
|
||||||
self.inner.read().await.pending.contains_key(user)
|
self.inner.read().await.pending.contains_key(user)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user