Commit Graph

106 Commits

Author SHA1 Message Date
michael
5668ba1f69 docs: mark Phases 4, 5, 15, 16 done in roadmap 2026-04-27 08:42:19 -07:00
michael
7eae081dd7 Merge branch 'gnome-integration-lane' 2026-04-27 08:41:15 -07:00
michael
61f89abae0 Merge branch 'ansible-lane' 2026-04-27 08:41:10 -07:00
michael
5c94319bf0 feat(daemon): policy apply via pam-auth-update + lockout simulator (Phase 4+5)
Lands Lane 1 of the Phase 4+5+8+15+16 parallel cycle. Phase 4 + Phase 5 must
land together because both modify the SetPolicy code path.

- daemon/src/lockout.rs — pure simulate(new_policy, registry) -> Vec<Violation>.
  Iterates Required stacks, flags users with no enrolled credential of any
  required method. 5 unit tests cover: optional-mode skipped, required-with-
  unenrolled flagged, any-method-satisfies, empty registry, multi-stack.
- daemon/src/policy_apply.rs — PolicyApplier renders the pam-configs profile
  (Default: yes when any stack requires fido2; pam_u2f.so + pam_authforge_pending
  when fido2 required, only pam_authforge_pending otherwise) and runs
  pam-auth-update --package. Stash-and-restore on failure: prior profile
  contents are restored and pam-auth-update re-run, so a failed apply leaves
  the system in its previous PAM state. 4 unit tests including a real-process
  rollback test against a failing /bin/sh shim.
- daemon/src/state.rs — AppState::set_policy(p, force) returns
  PolicyApplyResult. Always runs the simulator first; if violations and !force,
  returns { applied: false, violations } without writing. Otherwise persists
  via PolicyStore::save and invokes PolicyApplier::apply. StorageConfig grows
  pam_profile_path + pam_auth_update fields (env-var driven, tests inject a
  no-op /bin/sh shim into a tempdir).
- daemon/src/dbus.rs — SetPolicy signature is now (Policy, bool) -> Result.
  Wire-breaking pre-alpha; CLI updated in this commit.
- cli/src/{bus,commands}.rs — set_policy takes force flag. policy set runs
  with force=false and surfaces violations as a non-zero exit + stderr list
  pointing the user at policy apply --force-i-know-what-im-doing. policy
  apply now actually invokes pam-auth-update via the daemon.

Test count: 42 daemon (was 33; adds 5 lockout + 4 policy_apply). 13 common.
5 cli. cargo clippy --workspace --all-targets -D warnings clean.

Plan deviation: PolicyApplier::from_env() became PolicyApplier::new(profile_path,
pam_auth_update) with the env defaults moved into StorageConfig::from_env_or_defaults.
Cleaner: state owns one source of truth for env-driven path config.
2026-04-27 08:41:00 -07:00
michael
407e71072d ansible: document role variables and ship example playbook
README walks through what the role does, every default, and how to
invoke it from a parent playbook. examples/playbook.yml is a runnable
copy that targets a `workstations` group with a sudo=required+fido2
stack and one pending user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 08:36:29 -07:00
michael
6e47dbd3df packaging: ship authforge-gnome-integration binary package
Declares the new arch-all binary package in debian/control (depending on
authforge-gui + gnome-control-center) and wires its single data file
(gnome-integration/io.dangerousthings.AuthForge.UsersPanel.desktop) into
debian/rules' override_dh_auto_install so it lands in
/usr/share/applications/. The metapackage already Suggests:
authforge-gnome-integration, so no change there.
2026-04-27 08:35:33 -07:00
michael
457db3ced2 ansible: implement install + policy + pending tasks
Adds the apt_repository / apt steps that pull authforge-daemon, -pam,
and -cli (with -gui gated behind authforge_install_gui), the template
step that renders /etc/authforge/policy.d/90-fleet.conf and notifies the
restart handler, and a loop that calls `authforgectl pending set` for
each entry in authforge_pending_users.

The template emits TOML that round-trips through tomllib for both the
empty-stacks default and a populated multi-stack config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 08:35:28 -07:00
michael
e638a2defd gnome-integration: add Users-panel .desktop overlay and README
Adds the data file shipped by the optional authforge-gnome-integration
package: a /usr/share/applications/.desktop overlay tagged with
X-GNOME-Settings-Panel=user-accounts so gnome-control-center surfaces a
"Configure security…" launcher inside each user's detail view. The
README documents why a .desktop overlay was picked over a JS extension
(cross-version stability, packaging simplicity) and how to verify the
integration works in GNOME 46/47.
2026-04-27 08:34:50 -07:00
michael
792dca976d ansible: scaffold dangerousthings.authforge role skeleton
Adds meta/main.yml (Galaxy metadata for Ubuntu 22.04/24.04 under the
Apache-2.0 license), defaults/main.yml covering the policy/storage/
firstrun/pending knobs the tasks layer will consume, and a single
restart-daemon handler. Tasks and template land in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 08:34:26 -07:00
michael
865e37b6aa docs: mark Phases 3, 6, 7 done in roadmap 2026-04-27 08:30:09 -07:00
michael
22938f657a feat(daemon-fido): Authenticator trait + Mock + Ctap impl + signals (Lane A)
Bundles plan tasks A2 (PamU2fCred encoder), A3 (Authenticator trait), A4
(MockAuthenticator), A5 (CtapAuthenticator wrapping ctap-hid-fido2 3.5.9),
A6 (wire enrollment through AppState::enroll), A7 (DeviceFound /
TouchRequired / EnrollmentSucceeded / EnrollmentFailed D-Bus signals).

- daemon/src/fido/format.rs — PamU2fCred -> 'kh,pk,es256,+presence' with
  hex::encode for the binary blobs and CoseType matching COSE alg -7/-8.
- daemon/src/fido/authenticator.rs — Authenticator trait with discover() and
  make_credential(rp_id, user, pin); AuthnError covers NoDevice / Cancelled /
  PinRequired / Backend.
- daemon/src/fido/mock.rs — MockAuthenticator::with_one_yubikey produces
  deterministic-but-distinct PamU2fCreds (counter-bumped per call).
- daemon/src/fido/ctap.rs — CtapAuthenticator. discover via
  ctap_hid_fido2::get_fidokey_devices(); make_credential via
  FidoKeyHidFactory::create + fk.make_credential. Heuristic error mapping
  to AuthnError variants. Real-hardware path; compile-clean gate only.
- daemon/src/state.rs — AppState::open now takes Arc<dyn Authenticator>.
  New enroll(user, nickname) replaces the Phase 2 add_credential stub: calls
  authn.make_credential, writes pam_u2f line via CredentialsStore::add,
  records enrollment in userdb, returns Credential with hex(keyHandle) as id.
- daemon/src/dbus.rs — enroll_own / enroll_other now emit TouchRequired
  before the call and EnrollmentSucceeded / EnrollmentFailed after. Removed
  the unused stub-credential builder + import baggage.
- daemon/src/main.rs — picks CtapAuthenticator for prod; tests inject Mock.

Test count: 33 daemon tests pass (was 26). Adds 2 fido::format tests, 3
fido::mock tests, 1 state::enroll_writes_real_pam_u2f_line, 1 dbus::
enrollment_succeeded_signal_fires_on_enroll_own. cargo clippy --workspace
--all-targets -D warnings clean. cargo fmt clean.

Plan deviations:
- HidInfo doesn't have a serial_number field in 3.5.9; switched to using
  product_string and HidParam::Path/VidPid for the device path label.
- FidoKeyHidFactory and LibCfg are at the crate root, not under fidokey::.
- fido/mod.rs has #![allow(dead_code)] for now: discover() and DiscoveredDevice
  fields are wired via the trait but only called from tests until Phase 8
  GUI consumes the DeviceFound signal.
2026-04-27 08:29:42 -07:00
michael
01df2109d8 chore: add ctap-hid-fido2 + hex deps for Phase 3 (Task A1) 2026-04-27 08:20:15 -07:00
michael
1c6c361d4a docs(pam): smoke-test recipe + test PAM stack file (Task B2) 2026-04-27 08:19:18 -07:00
michael
209167df22 feat(pam): real pending-flag check with path-traversal guard (Task B1)
Replaces the Phase 0 stub. Reads PAM_USER, rejects usernames containing /,
\0, .., or that are . / .. / empty. stat()s /var/lib/authforge/pending/<user>:
present -> emits the design-doc user-facing message + PAM_AUTH_ERR; ENOENT
-> PAM_IGNORE (lets the rest of the stack decide); other errno -> logs and
fails closed (PAM_AUTH_ERR).

Builds clean against libpam0g-dev with -Wall -Wextra -Werror -fPIC -O2.
Resulting .so is a 16KB stripped-with-buildid x86-64 ELF; passes file(1) sanity.
pamtester smoke recipe lands in Task B2 alongside the test PAM stack file.
2026-04-27 08:18:57 -07:00
michael
0697ba1c65 feat(cli): D-Bus client wrapper + full subcommand dispatcher
Lands plan tasks C2 (Daemon proxy wrapping all 9 D-Bus methods via
Proxy::new_owned), C3 (status/list/policy-show), C4 (enroll/remove/policy-
set/apply/validate), and C5 (pending/recovery). Bundled because dispatch
needs the bus wrapper to compile cleanly under -D warnings.

Phase 4/5/12 placeholders in the dispatcher: policy apply re-saves to trigger
PolicyChanged; policy validate is a TOML round-trip; pending list and
recovery list are no-op messages until the corresponding D-Bus methods land.

5 clap-parser tests pass (was 5; same — parser shape unchanged in this
commit). Workspace clippy clean.
2026-04-27 08:18:17 -07:00
michael
1b223fe4f9 feat(cli): authforgectl subcommand structure with clap derive (Task C1) 2026-04-27 08:16:29 -07:00
michael
6351041a6c docs: expand Phases 3+6+7 to step granularity (multi-lane bundle) 2026-04-27 08:15:20 -07:00
michael
8f4225d136 docs: add Parallel Execution Lanes section to roadmap
Captures which remaining phases are parallel-safe and which must stay
sequential. Four lanes (3 / 4+5 / 6 / 7) can land in any order right after
Phase 2 because they touch disjoint file trees: daemon/src/fido,
daemon/src/policy_apply + lockout, pam/*.c, and cli/src.

Documents practical caveats: Phase 4+5 must land together (shared SetPolicy
path), Phase 6 is the only C lane and needs libpam0g-dev, and the Phase 1
subagent-worktree cwd quirk should be retested before relying on it for
literal parallel-compute execution.
2026-04-27 07:52:30 -07:00
michael
f655d41f5c docs: mark Phase 2 done in roadmap, capture closeout notes 2026-04-27 06:45:11 -07:00
michael
24237cbf8b refactor(daemon): AppState delegates to storage modules; dbus tests use tempdir
Lands plan tasks 2.15 (AppState refactor with StorageConfig + open()) and 2.16
(dbus.rs tests switched to tempdir-backed AppState; storage errors threaded
through D-Bus methods as Failed). Bundled because the AppState surface change
forces dbus.rs adjustments in the same commit.

- daemon/src/state.rs: AppState::open(StorageConfig) replaces with_fixtures().
  StorageConfig.from_env_or_defaults() reads AUTHFORGE_POLICY_DIR /
  _PENDING_DIR / _USERDB env vars (defaults: /etc/authforge/policy.d,
  /var/lib/authforge/pending, /var/lib/authforge/users.db). State delegates
  list/add/remove credentials to CredsPathResolver + CredentialsStore picked
  per-call from current Policy; pending and userdb operate independently.
- daemon/src/main.rs: opens state via env-driven config; reuses cfg.policy_dir
  for the watcher to keep one source of truth.
- daemon/src/dbus.rs: every write method maps StateError to fdo::Error::Failed.
  p2p_pair seeds 00-test.conf with [storage] backend = central pointing into
  the tempdir so credential writes don't try to touch /home/<user>/...
  (alice/bob/carol aren't real accounts in tests).
- Renamed: list_credentials_returns_fixture_for_alice ->
  list_credentials_after_enroll. Removed: with_fixtures().
- .gitignore: add .claude/ so leftover Phase 1 worktree state isn't committed.

Test count: 26/26 daemon tests green (was 17). Common: 13/13. Clippy + fmt clean.
2026-04-27 06:44:07 -07:00
michael
90f7a0f4fc feat(daemon): storage::userdb sqlite cache for enrollment registry 2026-04-27 06:29:36 -07:00
michael
eb3362c81c feat(daemon): storage::credentials parser, CRUD, and per-user vs central path
Lands plan tasks 2.10 (pam_u2f line format parser via CredEntry::from_line/
to_line, credId extraction), 2.11 (CredentialsStore add/remove/list with
idempotent add-by-credId), and 2.12 (CredsPathResolver dispatching central vs
per-user paths). Bundled because the three pieces compose into one storage
boundary.

CredEntry treats post-username chunks as opaque blobs split on ':', preserving
pam_u2f's full record on round-trip. credId = first comma-separated field of a
blob. add() is idempotent on credId match (Phase 3 may decide to refresh
publicKey on re-enroll; out of scope here).

Path resolution: Central -> Storage.central_path verbatim. PerUser -> getpwnam
via nix, with a /home/<user>/... fallback if NSS errors (CI users, distro
quirks); pam_u2f does the real lookup at auth time, so the fallback only
matters for write-on-enroll where the user does exist.

7 tests added; clippy + fmt clean.
2026-04-27 06:29:00 -07:00
michael
dd766e3077 feat(daemon): storage::pending JSON read/write/clear with path-traversal guards 2026-04-27 06:27:30 -07:00
michael
ea70386c2f feat(daemon): storage::policy + PolicyChanged signal + inotify watcher
Lands plan tasks 2.6 (PolicyStore wraps load_from_dir / save_local), 2.7
(PolicyChanged D-Bus signal on the AuthForge interface), and 2.8 (notify-based
inotify watcher in main.rs that emits the signal on any change in the policy.d
directory). Bundled because watcher → emit signal → wraps PolicyStore is one
data flow.

- daemon/src/storage/{mod,policy}.rs — PolicyStore::{load,save,watch}; watch
  returns a (RecommendedWatcher, watch::Receiver) so the caller keeps the
  watcher alive.
- daemon/src/dbus.rs — adds #[zbus(signal)] policy_changed; integration test
  via p2p connection asserts the signal arrives within 2s.
- daemon/src/main.rs — spawns a task that ticks PolicyChanged on every
  rx.changed(), keyed off AUTHFORGE_POLICY_DIR env var (default
  /etc/authforge/policy.d). Watcher leaked via std::mem::forget; daemon
  lifetime = process lifetime.

Test count: 17/17 daemon (was 14) + 13/13 common.
2026-04-27 06:26:49 -07:00
michael
9be8e4d0b3 feat(common): Policy load_from_dir + save_local with last-wins merge
Lands plan tasks 2.3 (single-file load), 2.4 (multi-file merge cases — last-wins,
lex order, non-conf skip, missing-dir default), and 2.5 (save_local preserves
sibling files) as one logical unit.

- load_from_dir: reads *.conf in lex-ascending order, parses TOML, merges via
  last-wins on stack key, storage block, and firstrun block. Missing dir yields
  Policy::default(). Non-.conf entries silently skipped.
- save_local: writes 50-local.conf with toml::to_string_pretty; never reads
  or removes siblings. Created via create_dir_all.
- 6 parse tests added: load_single_file, last_file_wins_on_overlap,
  lex_order_not_filesystem_order, ignores_non_conf_files, missing_dir_yields_default,
  save_local_preserves_sibling_files.
2026-04-27 06:23:49 -07:00
michael
dc2c4b02cb refactor(common): move Policy types from types.rs into policy.rs 2026-04-27 06:22:54 -07:00
michael
57c97b492c chore: add notify + rusqlite + futures-util for Phase 2 storage layer 2026-04-27 06:18:13 -07:00
michael
0968bb3b4b docs: expand Phase 2 (storage layer) to step granularity 2026-04-27 06:15:51 -07:00
michael
d156935a25 docs: mark Phase 0 and Phase 1 done in roadmap, capture Phase 1 closeout notes 2026-04-27 06:09:55 -07:00
michael
c26d6ae896 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.
2026-04-26 23:04:59 -07:00
michael
df22358051 feat(daemon): add polkit authorizer with permissive + system modes 2026-04-26 20:49:21 -07:00
michael
0dec2a1236 feat(daemon): add AppState with fixture-backed in-memory store 2026-04-26 20:48:56 -07:00
michael
c78ad0dc29 chore: add daemon deps for D-Bus interface and tests 2026-04-26 20:47:46 -07:00
michael
22aa37662a feat: add PendingFlag, PolicyApplyResult, Violation types 2026-04-26 20:47:06 -07:00
michael
4d40c1a8b9 feat: add Policy, StackPolicy, Storage, Firstrun types 2026-04-26 20:46:28 -07:00
michael
156fe7533e feat: add Credential and Transport wire types 2026-04-26 20:46:07 -07:00
michael
eb35155c22 chore: add zvariant to workspace deps 2026-04-26 20:45:34 -07:00
michael
b18b5aa838 ci: ensure dbus is installed for daemon tests 2026-04-26 20:42:04 -07:00
michael
2ba5b4da98 chore(packaging): authforge-daemon depends on dbus + policykit 2026-04-26 20:42:03 -07:00
michael
0a6cbe0e1d feat(packaging): install dbus, polkit, systemd assets into authforge-daemon 2026-04-26 20:42:02 -07:00
michael
aeebfd0f40 feat(packaging): postinst that enables + starts authforge-daemon 2026-04-26 20:42:01 -07:00
michael
49def6e75c feat(packaging): polkit policy with 7 AuthForge actions 2026-04-26 20:42:00 -07:00
michael
8f35c88b54 feat(packaging): systemd unit + D-Bus activation/policy files 2026-04-26 20:41:56 -07:00
michael
31432517cc docs: expand Phase 1 to step granularity (D-Bus, systemd, polkit) 2026-04-26 17:03:13 -07:00
michael
f50c7b2e82 Rename project: ubuntu-fido -> authforge
Renames the package and all artifacts to authforge to drop the
distro-specific prefix, since the roadmap targets Ubuntu + Debian +
KDE + eventually Fedora (option C in the design).

- deb packages: authforge, authforge-{daemon,pam,cli,gui,gnome-integration}
- binaries: authforged, authforgectl, authforge (GUI)
- D-Bus name: io.dangerousthings.AuthForge
- PAM module: pam_authforge_pending.so
- Paths: /etc/authforge/, /var/lib/authforge/, /usr/share/pam-configs/authforge
- PPA: ppa:dangerousthings/authforge

Filesystem path /home/work/VSCodeProjects/ubuntu_fido/ left as-is for
historical reference; can rename later via git mv at the dir level.

Verified: cargo build/test/clippy/fmt clean, pam builds, gui builds,
all 5 debs produced.
2026-04-26 16:53:11 -07:00
michael
c6a5e942c9 Fix GUI placeholder: drop ToolbarView (libadwaita-rs 0.6 feature-gate) 2026-04-26 15:29:25 -07:00
michael
c45989b876 Add BUILDING.md documenting deferred build steps 2026-04-26 15:15:57 -07:00
michael
0cb9620e42 Apply rustfmt to cli main 2026-04-26 15:09:10 -07:00
michael
98d2502244 Add GitHub Actions CI: cargo fmt/clippy/test, PAM build, deb build 2026-04-26 14:49:48 -07:00
michael
9e3090ca88 Add Debian packaging skeleton (5 component debs, debhelper-13) 2026-04-26 14:48:00 -07:00