From b2771e73fa5005cccf0cf0402f21c8fbf537ee0f Mon Sep 17 00:00:00 2001 From: michael Date: Mon, 27 Apr 2026 11:13:53 -0700 Subject: [PATCH] feat(gui): pure-logic helpers for Policy tab (mode mapping, violation grouping) Co-Authored-By: Claude Opus 4.7 (1M context) --- gui/src/main.rs | 1 + gui/src/policy_form.rs | 92 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 gui/src/policy_form.rs diff --git a/gui/src/main.rs b/gui/src/main.rs index fc5f9da..f3e1cc3 100644 --- a/gui/src/main.rs +++ b/gui/src/main.rs @@ -7,6 +7,7 @@ mod enroll_dialog; mod error; mod firstrun; mod keys_page; +mod policy_form; const APP_ID: &str = "io.dangerousthings.AuthForge"; diff --git a/gui/src/policy_form.rs b/gui/src/policy_form.rs new file mode 100644 index 0000000..bd3cf72 --- /dev/null +++ b/gui/src/policy_form.rs @@ -0,0 +1,92 @@ +//! Pure helpers backing the Policy tab. Mode <-> combo index, violation +//! grouping. Widget code calls these; nothing here touches GTK. +//! +//! `dead_code` allowances mirror `bus.rs` — these helpers are wired through +//! `policy_page.rs` in Task 4; without the allow, this commit's `-D warnings` +//! gate would reject the staged-but-unused symbols. + +#![allow(dead_code)] + +use authforge_common::types::{Mode, Violation}; +use std::collections::BTreeMap; + +pub(crate) const MODE_LABELS: &[(&str, Mode)] = &[ + ("Disabled", Mode::Disabled), + ("Optional", Mode::Optional), + ("Required", Mode::Required), +]; + +pub(crate) fn mode_to_combo_index(m: Mode) -> u32 { + match m { + Mode::Disabled => 0, + Mode::Optional => 1, + Mode::Required => 2, + } +} + +pub(crate) fn combo_index_to_mode(idx: u32) -> Mode { + match idx { + 0 => Mode::Disabled, + 1 => Mode::Optional, + _ => Mode::Required, + } +} + +/// Group violations by stack, joining reasons with "; ". Used to render the +/// inline AdwBanner content when `SetPolicy(force=false)` returns violations. +pub(crate) fn group_violations_by_stack(vs: &[Violation]) -> BTreeMap { + let mut by_stack: BTreeMap> = BTreeMap::new(); + for v in vs { + by_stack + .entry(v.stack.clone()) + .or_default() + .push(format!("{}: {}", v.user, v.reason)); + } + by_stack + .into_iter() + .map(|(k, vs)| (k, vs.join("; "))) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mode_combo_roundtrips() { + for m in [Mode::Disabled, Mode::Optional, Mode::Required] { + assert_eq!(combo_index_to_mode(mode_to_combo_index(m)), m); + } + } + + #[test] + fn out_of_range_index_clamps_to_required() { + assert_eq!(combo_index_to_mode(99), Mode::Required); + } + + #[test] + fn violations_group_by_stack() { + let vs = vec![ + Violation { + user: "alice".into(), + stack: "sudo".into(), + reason: "no fido2".into(), + }, + Violation { + user: "bob".into(), + stack: "sudo".into(), + reason: "no fido2".into(), + }, + Violation { + user: "alice".into(), + stack: "sshd".into(), + reason: "no fido2".into(), + }, + ]; + let g = group_violations_by_stack(&vs); + assert_eq!(g.len(), 2); + assert!(g.get("sudo").unwrap().contains("alice")); + assert!(g.get("sudo").unwrap().contains("bob")); + assert_eq!(g.get("sshd").unwrap(), "alice: no fido2"); + } +}