Skip to content

feat(wyctl): gsettings fallback for mfa --store/--keyprovider#334

Merged
justinjoy merged 2 commits into
mainfrom
feature/333-mfa-gsettings
May 20, 2026
Merged

feat(wyctl): gsettings fallback for mfa --store/--keyprovider#334
justinjoy merged 2 commits into
mainfrom
feature/333-mfa-gsettings

Conversation

@justinjoy

Copy link
Copy Markdown
Contributor

Closes #333. Follow-up to PR #332.

Summary

Wires wyctl mfa enroll/reset into the existing wyctl_resolve_string_option GSettings pipeline so operators can set policy-store path and keyprovider spec once and stop retyping them. Two atomic commits, both reviewer-ratified.

Commit map

  1. wyctl: read default-policy-store and default-keyprovider GSettings keys for mfa subcommands — adds two s keys (default "") to org.wyrelog.wyctl.gschema.xml; threads WyctlOptions *global_opts through run_mfa and into both subcommands; calls the existing resolver before wyctl_mfa_validate_common_options; +14 subtests (6 unit + 8 subprocess incl. defense-in-depth no-secrets-in-keyfile scan).
  2. docs: document gsettings fallback for wyctl mfa subcommands — operator-runbook MFA section gets Setting defaults via GSettings; key table extended; "paths/specs only, never secrets" policy statement extended to cover the new keys; pin backfill in tests/test-wyctl-gschema.c; corrected an inaccurate ownership-pattern comment in wyctl.c that claimed run_status-style mirroring (run_status does not rebind opts, the mfa subcommands do).

Design (locked)

  • Two new keys: default-policy-store (backs --store), default-keyprovider (backs --keyprovider). Both s, default "".
  • Precedence: CLI > GSettings > missing-flag diagnostic. Unchanged from existing wyctl subcommands.
  • --subject deliberately NOT GSettings-backed — every enrollment targets a different principal.
  • WYCTL_DISABLE_GSETTINGS=1 covers mfa uniformly with the rest of wyctl.
  • No daemon-side GSettings (intentional, per existing operator-runbook.md:903-922 rationale).

Test plan

  • meson test -C builddir --suite wyrelog green locally (75 OK / 2 skipped / 0 fail).
  • Unit: CLI wins, GSettings fills, both empty surfaces error, empty-string-as-unset symmetry, kill-switch disables fallback — for both keys.
  • Subprocess: end-to-end enroll succeeds with both CLI args, both from GSettings, mixed, and the kill-switch path.
  • Defense-in-depth: after a GSettings-backed enrollment, the dconf keyfile contains the operator-supplied path/spec but NOT the seed bytes, otpauth URI, or enrollment UUIDv7.
  • CI lanes (ubuntu, macos, windows) green.

Threat model

  • default-policy-store and default-keyprovider store only paths and spec strings. The KeyProvider key material, the TOTP seed bytes, and policy-store contents never live in GSettings — verified by the defense-in-depth test and documented in the runbook policy section.
  • The operator-UID assumption (dconf is operator-writable; a compromised operator session is already game-over) applies uniformly. Adding these two keys does not enlarge the attack surface beyond what the existing access-token-file key already accepts.

Out of scope (unchanged from issue)

  • --subject as a GSettings default
  • New env var override layers beyond WYCTL_DISABLE_GSETTINGS
  • Daemon-side GSettings
  • Validating store-path / keyprovider-spec contents at flag-resolution time

justinjoy added 2 commits May 20, 2026 13:54
…ys for mfa subcommands

Issue #333. The `wyctl mfa enroll` / `mfa reset` subcommands previously
required `--store` and `--keyprovider` on every invocation, the lone
gap in the otherwise uniform "CLI > GSettings > error" pipeline the
rest of wyctl uses (see `wyctl_resolve_string_option` and the
canonical call site in `run_status`).  Operators who set every other
default once via `gsettings set` had to keep retyping these two flags.

Changes:

* `wyrelog/wyctl/org.wyrelog.wyctl.gschema.xml` declares two new
  string keys, default empty:
    - `default-policy-store`
    - `default-keyprovider`

  Naming asymmetry note: the CLI flag is `--store`, the GSettings key
  is `default-policy-store`.  The flag is unambiguous inside an
  offline subcommand (wyctl mfa only operates on the policy store)
  but the schema key lives outside that context — wyrelog persists
  multiple kinds of stores (policy, fact, ...) and a flat
  `default-store` key would invite confusion in operator config.
  `default-keyprovider` matches the CLI flag verbatim because there
  is only one kind of keyprovider in wyrelog.

* `wyrelog/wyctl/wyctl.c` threads the existing `WyctlOptions`
  (carrying the GSettings handle opened once in `main`) into
  `run_mfa`, `run_mfa_enroll`, and `run_mfa_reset`.  Both subcommands
  call `wyctl_resolve_string_option` for `--store` and
  `--keyprovider` after `g_option_context_parse` and before
  `wyctl_mfa_validate_common_options`, mirroring the ownership
  pattern in `run_status` (g_autofree locals; opts.* rebound to the
  resolved values so the GOptionContext-owned slots are not
  reassigned).

* `--subject` is deliberately NOT a GSettings key: it changes between
  enrollments and is therefore a per-invocation identity, not a
  default.

* No third precedence tier and no new env vars.  The existing
  `WYCTL_DISABLE_GSETTINGS=1` kill switch is still the only override.

Tests (TDD):

* `tests/test-wyctl-config.c` extends the `fresh_settings` reset
  array with the two new key names and adds six unit tests covering
  CLI-wins, fall-back-to-settings, and empty-settings-is-unset for
  both keys.

* `tests/test-wyctl-mfa.c` adds eight subprocess-level functional
  tests exercising the resolver call sites in the real wyctl binary:
    - CLI --store wins over GSettings
    - GSettings supplies --store when CLI omits it
    - Both unset surfaces the existing "missing --store" diagnostic
    - WYCTL_DISABLE_GSETTINGS=1 kill switch suppresses the fallback
    - Empty-string GSettings is treated as unset (symmetry test)
    - `mfa reset` consumes the GSettings value end-to-end
    - GSettings supplies --keyprovider when CLI omits it
    - Defense-in-depth: after a fully-GSettings-resolved enrollment,
      the operator-supplied keyfile contains the store path and
      nothing else — no seed bytes, no otpauth URI, no enrollment
      UUIDv7, no subject string.  Pins the invariant that the
      GSettings keys are operator-config only, never a sink for any
      enrollment artifact.

* `tests/meson.build` wires the wyctl-mfa test against the compiled
  schema directory and the memory backend, the same env the
  wyctl-config and wyctl-basic tests already use.  The per-test
  XDG-keyfile fixtures then layer a real GSettings keyfile on top.

Documentation lives in a follow-up commit (issue #333 commit 2)
so this change remains code-only per the project's atomic-commit
policy and so the runbook does not document a non-feature.
Document the two GSettings keys (`default-policy-store`,
`default-keyprovider`) added in commit 1 of #333, and tighten the
inaccurate comment that claimed to mirror `run_status`'s ownership
pattern.

operator-runbook.md:
- New subsection "Setting defaults via GSettings" under the MFA section
  walking through the two-line gsettings setup and noting that
  --subject stays explicit and the WYCTL_DISABLE_GSETTINGS=1 kill
  switch covers mfa subcommands the same way it covers the rest of
  wyctl.
- Extended the GSettings key reference table with the two new keys
  (type `s`, default `""`), matching the row format used by the
  existing keys and matching the schema XML byte-for-byte.
- Extended the canonical "paths/specs only, never secret bytes" policy
  statement to explicitly cover the two new keys.
- Extended the precedence-rule paragraph to cross-reference the mfa
  subcommands as participants in the same resolver pipeline.

tests/test-wyctl-gschema.c:
- Added `default-policy-store` and `default-keyprovider` to
  `expected[]` (keys-and-types test) and to `string_keys[]`
  (defaults-safe test) so the schema-pin and safe-default coverage
  extend to the new keys.  Test count unchanged.

wyrelog/wyctl/wyctl.c:
- Rewrote the comment blocks above the resolver calls in
  `run_mfa_enroll` and `run_mfa_reset` to describe what the code
  actually does (overwrites the GOptionContext-owned slot with the
  resolved-and-owned local, accepting the small one-shot leak of the
  original — same pattern as elsewhere in wyctl) instead of the
  misleading "mirroring run_status" claim.  No behaviour change.
@justinjoy justinjoy merged commit 98d4884 into main May 20, 2026
3 checks passed
@justinjoy justinjoy deleted the feature/333-mfa-gsettings branch May 20, 2026 05:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wyctl: gsettings fallback for --store/--keyprovider in mfa subcommands

1 participant