Skip to content

feat(privileged, helper): KAuth pipeline for login surface#6

Merged
manuacl merged 4 commits into
mainfrom
feat/kauth-helper
May 26, 2026
Merged

feat(privileged, helper): KAuth pipeline for login surface#6
manuacl merged 4 commits into
mainfrom
feat/kauth-helper

Conversation

@manuacl

@manuacl manuacl commented May 26, 2026

Copy link
Copy Markdown
Owner

Summary

LoginSurface rejoins the GUI. The card now shows up as the third surface in SyncEngine, with Apply routed through KAuth to a dedicated helper that has the privileges to overwrite `/etc/plasmalogin.conf`.

Three pieces:

  • `src/helper/plasma-wallpaper-sync-helper` — standalone executable inheriting `KAuth::HelperSupport`. Exposes a single `writefile(args)` slot bound to action id `dev.manuacl.plasmawallpapersync.writefile`. Refuses any path other than `/etc/plasmalogin.conf` (defense in depth), atomic write via `QSaveFile::commit()`.
  • `src/privileged/kauth/KAuthPrivilegedWriter` — concrete `PrivilegedWriter`. `writeAtomically()` builds a `KAuth::Action`, fires async `execute()`, forwards the result through `writeSucceeded` / `writeFailed`. Lives outside `src/core/` because it links `KF6::AuthCore` (deny-listed by the core/ layering guard — the seam holds).
  • `data/dev.manuacl.plasmawallpapersync.policy` — polkit policy. `allow_active=auth_admin` triggers the sudo prompt on first use per session.

`SyncEngine.addSurface(&login)` is all the integration the shell needs — Main.qml's Repeater renders the third card automatically.

Test plan

  • CI "Build and test (Fedora 42 / Qt6 / KF6)" passes (7/7 suites unchanged — no new tests because KAuth integration can't be exercised without polkit+session bus, which the CI container doesn't run)
  • CI "REUSE compliance" passes
  • Flatpak SDK build (`./dev/build.sh -- --build-only`) finishes the bundle including the new helper binary
  • Live end-to-end test requires a native install — the Flatpak install ships the policy/helper under `/app/`, where the host polkit daemon and session bus don't see them. Manual test plan: build natively in a fedora-toolbox or rpm-ostree-install the deps, `sudo make install`, run `plasma-wallpaper-sync`, pick image, tick Login, click Apply, expect polkit prompt then write to `/etc/plasmalogin.conf` then next-login wallpaper change.

Known limitation (and v3 evolution flag)

Under Flatpak, Apply on Login will fail with an action-not-registered error. This is the symptom of the v3 distribution gap noted in CLAUDE.md: a future `flatpak-spawn`-based `PrivilegedWriter` (or a portal-mediated approach) would slot into `src/privileged/flatpak/` alongside the kauth one without touching `src/core/` or `LoginSurface`. That work is deferred — for the v0.1.0 native release it isn't on the path.

manuacl and others added 4 commits May 26, 2026 12:26
LoginSurface rejoins the GUI. The card now shows up as the third
surface in the SyncEngine, with Apply routed through KAuth to a
dedicated helper that has the privileges to overwrite
/etc/plasmalogin.conf.

Three new pieces wired up:

  src/helper/plasma-wallpaper-sync-helper — standalone executable
  that runs as root for the duration of one action. Inherits
  KAuth::HelperSupport, exposes a single `writefile(args)` slot
  bound to the action id `dev.manuacl.plasmawallpapersync.writefile`.
  Refuses any path other than `/etc/plasmalogin.conf` as defense
  in depth, then writes atomically via QSaveFile::commit().
  KAUTH_HELPER_MAIN generates the main() with all the D-Bus and
  service-activation plumbing.

  src/privileged/kauth/KAuthPrivilegedWriter — concrete
  PrivilegedWriter. writeAtomically() builds a KAuth::Action with
  the helper id, packs path/contents in a QVariantMap, fires
  execute() and forwards the async job's result through
  writeSucceeded / writeFailed. Lives outside src/core/ because it
  links KF6::AuthCore (which is on the core/ layering guard's deny
  list — the seam is preserved).

  data/dev.manuacl.plasmawallpapersync.policy — polkit policy. Action
  defaults: allow_active=auth_admin so the user sees a sudo prompt
  the first time per session. allow_inactive=no keeps a logged-out
  user from racing into the action.

The shell instantiates KAuthPrivilegedWriter once at startup and
hands a pointer to LoginSurface. SyncEngine.addSurface picks the
new surface up; Main.qml's Repeater over `syncEngine.surfaceIds`
now renders three cards automatically — no QML change needed.

CMake: kauth_install_helper_files + kauth_install_actions handle
the D-Bus service file, helper binary placement, and polkit policy
installation in one shot.

Live testing note: the Flatpak SDK install of this build CANNOT
run KAuth end-to-end — the helper, polkit policy and D-Bus service
file all land under /app/ inside the sandbox, invisible to the
host's polkit daemon and session bus. The UI surfaces Login,
Apply will fail with an action-not-registered error. End-to-end
KAuth validation requires a native install (system make-install
or distro package). Architecturally everything is in place; this
is exactly the v0.1.0 native-release shape called out in CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symptom: the Login card showed "(no wallpaper recorded)" on a
Flatpak install even though the user's /etc/plasmalogin.conf has a
valid wallpaper entry.

Root cause: /etc/ inside a Flatpak sandbox is the runtime's /etc/,
not the host's. With --filesystem=host:ro the host's /etc/ is
bind-mounted at /run/host/etc/. LoginSurface was hardcoded to read
from /etc/plasmalogin.conf, which under Flatpak resolves to the
runtime's empty /etc.

LoginSurface now distinguishes two paths:

  - readPath:  where currentImagePath() reads from. Defaults to
               /run/host/etc/plasmalogin.conf under Flatpak (detected
               via the /.flatpak-info sentinel) and to
               /etc/plasmalogin.conf on a native install.

  - writePath: what the writer is told to write to. Always
               /etc/plasmalogin.conf — that's the canonical host path
               the privileged helper validates against (the helper
               runs on the host, where /etc/ IS the right place).

Three constructors:
  - LoginSurface(writer): production defaults from defaultReadPath()
    and defaultWritePath().
  - LoginSurface(writer, configPath): both paths the same — used by
    every existing tst_LoginSurface case, no migration needed.
  - LoginSurface(writer, readPath, writePath): explicit, for the
    Flatpak case and the new readPathAndWritePathCanDiffer test.

Signal filter in wireWriterSignals now keys on m_writePath (what the
writer actually emits), not on a single m_configPath.

8/8 LoginSurface tests pass (10 if you count init/cleanup), including
the new readPathAndWritePathCanDiffer regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small follow-ups discovered during the native end-to-end test
on a real Bazzite + Plasma 6.10 host:

1. WallpaperLibrary.h docstring used `path/*.<ext>` notation inside
   a /* … */ block comment, which gcc -Wcomment warns about (the
   "/*" sequence reads as a nested comment opening). Switched to
   prose + `<file>.<ext>` notation; also updated the description
   to mention the one-level-subdir scan we added later.

2. tst_LoginSurface::readPathAndWritePathCanDiffer used
   "/etc/plasmalogin.conf" as the write target. Under Flatpak that
   path is redirected into the per-app sandbox dir so the
   FakePrivilegedWriter could create it; outside a sandbox (toolbox
   build sharing the host filesystem) the same test tried to write
   to the real /etc/ and was rejected by the kernel. Per-test temp
   path makes the test environment-independent.

3. PlasmaReloader hard-coded `org.kde.plasmashell` as the D-Bus
   service to talk to, which made
   tst_PlasmaReloader::desktopWithoutPlasmaShellEmitsFailure pass
   only when no plasmashell is reachable (Flatpak sandbox, CI
   container) and fail when one is (toolbox build alongside a live
   Plasma session). Added a test-only constructor accepting a
   service name; the test now targets a deliberately-fake bus name
   so the failure path is exercised regardless of the host's state.

4. dev/install-host-helper.sh was generating /etc/dbus-1/system.d/
   *.conf by splicing the upstream conf with sed, which ended up
   producing nested <busconfig> elements and a dbus ReloadConfig
   that refused to apply. Rewrote with inline templates and added
   verification probes (D-Bus ReloadConfig + pkaction + service
   activation introspection) so future runs surface failures at the
   moment of install. Also preferred the build-native/ tree as the
   helper source when present.

End-to-end manual flow validated on the host: native binary built in
a fedora-toolbox dispatches KAuth, polkit-kde-authentication-agent
prompts for the user's password, systemd starts the helper via
D-Bus activation, the helper writes /etc/plasmalogin.conf atomically,
the GUI shows "Applied to: desktop, lockscreen, login" in green.

6/6 test suites pass both in CI (Fedora 42 container, no plasmashell)
and in a local toolbox (Fedora 42, plasmashell on session bus).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
reuse 5.x parses any line containing "SPDX-License-Identifier:" — even
inside markdown body text — as a license declaration. The CLAUDE.md
bullet documenting our header convention literally writes the string,
which reuse then tries to parse and rejects as an empty expression
once the line wraps before the license name.

Wrapping the bullet in <!-- REUSE-IgnoreStart --> / <!-- REUSE-IgnoreEnd -->
markers tells reuse to skip the block. The doc text still reads
normally for humans; reuse stops trying to interpret it as a real
declaration.

CI didn't catch this earlier because the GitHub action runs an older
reuse where the strictness was lower; reuse 5.x (Fedora 42 toolbox)
flags it. Same fix applies regardless of which version CI ends up on.

(Also incidentally fixed an obsolete "Manu" attribution in the same
bullet → "Manuel Chamorro".)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@manuacl manuacl merged commit 8edf8da into main May 26, 2026
@manuacl manuacl deleted the feat/kauth-helper branch May 26, 2026 11:39
manuacl added a commit that referenced this pull request May 26, 2026
PR #6 wired the helper into the privileged-write seam but the visible
desktop wallpaper still didn't update on Apply. Root cause: nothing
was telling Plasma to re-read the appletsrc — PlasmaReloader existed
and was tested, but never instantiated by the shell.

Wiring it up turned out to be the easy part. The harder part was the
reload script itself: every variant that didn't pass the actual image
through Plasma silently no-op'd on Plasma 6.10. Documented inline in
PlasmaReloader.cpp, the three rejected approaches were:

  - `d.wallpaperPlugin = d.wallpaperPlugin` — same-value setter is
    short-circuited
  - `d.reloadConfig()` — recognized by the scripting API but the
    wallpaper plugin doesn't re-render in response
  - toggle `wallpaperPlugin = 'org.kde.color'` then back to
    `'org.kde.image'` — both setters happen in one script eval,
    Plasma batches them as a net-zero change and skips the reload

The fix matches what Plasma System Settings does internally on its
Wallpaper page: the script writes the Image entry through Plasma's
own writeConfig, which goes through the wallpaper plugin's setter
path and emits the repaint signal the renderer actually listens for.
Our prior KConfig write to disk stays — it's still authoritative
across restarts; this call is the live-update bridge that pokes
Plasma without waiting for the next session.

API:
  - notifyDesktopChanged() → notifyDesktopChanged(QString imagePath)
    Caller passes the path that just landed in the config so the
    script can substitute it. Single-quoted JS string literal with
    \\ and \' escapes for safety.
  - PlasmaReloader.h documents the path-on-call rationale.
  - Shell's connection to surfaceApplySucceeded looks up the surface
    by id, asks it for currentImagePath(), passes that through.

tst_PlasmaReloader updated for the new signature.

Note: lockscreen + login stay as no-ops on purpose — kscreenlocker
re-reads on the next lock event, plasmalogin only at boot. Live
reload is meaningless for them.

End-to-end validated: Apply Desktop on a native install now updates
the live wallpaper without a re-login.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant