fix(installer): keep user_created sticky across re-runs #8

Merged
whitlocktech merged 1 commits from fix/service-user-created-sticky into edge 2026-08-05 09:30:14 +00:00
Member

What & why

Found by finally running the installer on a real systemd host — a privileged Debian 12 container with systemd as PID 1, installing into /usr/bin, /etc and /var/lib for real. Phase 2's service registration had never actually been executed before (it needs elevation on Windows and a Linux host for systemd), so this is the first run that exercised it end to end.

service::prepare answers "did this run create the service account", which is false on every run after the first — by then the account exists. Recording that verbatim made the field describe the run rather than the state, with two consequences:

  1. install.json changed on an otherwise-identical second run, breaking the Phase 1 promise that a re-run with nothing new to do writes nothing.
  2. uninstall silently left behind the account this tool created. It removes only an account the record says it created, so after any second install the flag was false and the user survived the uninstall forever.

Reproduced before the fix, on a host where the installer had unambiguously created the account:

· left the runicgateway account alone — this installer did not create it
uid=996(runicgateway) gid=996(runicgateway) groups=996(runicgateway)

The record now inherits true from a prior record naming the same account. Matching on the name matters: inheriting across a rename would authorize deleting a user this installer never made.

This is invisible on Windows, where the SCM's virtual account is never created by us and disappears with the service — which is why three phases of Windows smoke runs never showed it. Same family as the two bugs Phase 1's live run caught (install.json recording the run's verb, and the Bridge.cfg comparison): a record describing the run instead of the state.

How it was tested

fmt / clippy -D warnings / 138 tests on both platforms, plus a new unit test covering the inherit / don't-inherit / different-account cases.

On the real systemd host, after the fix, from a clean slate (userdel, state dirs removed):

Step Result
install runicgateway-link.service active, enabled, running as runicgateway; unit carries both UOLINK_CONFIG and UOLINK_DB_PATH; sidecar.toml is 600 runicgateway:root; db in /var/lib/runicgateway owned by the service user; /health answers protocol 3; record says user_created: true
install again install.json unchanged, byte-identical, still user_created: true
doctor Service runicgateway-link.service active, enabled as runicgateway — first exercise of service::observe on systemd; the database row correctly shows /var/lib/… rather than the path the binary would pick alone
uninstall service stopped, disabled and its unit removed; removed the runicgateway service user; binary removed; sidecar.toml + database kept; nothing listening on 8080; report written; all 24 overlay files still in the ServUO tree

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code (Opus 5). I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a Co-Authored-By trailer.

License

  • I agree that my contribution is licensed under this project's license (GNU GPL v3.0 or later), and I have the right to contribute it.
## What & why Found by finally running the installer on a **real systemd host** — a privileged Debian 12 container with systemd as PID 1, installing into `/usr/bin`, `/etc` and `/var/lib` for real. Phase 2's service registration had never actually been executed before (it needs elevation on Windows and a Linux host for systemd), so this is the first run that exercised it end to end. `service::prepare` answers *"did **this run** create the service account"*, which is `false` on every run after the first — by then the account exists. Recording that verbatim made the field describe the run rather than the state, with two consequences: 1. **`install.json` changed on an otherwise-identical second run**, breaking the Phase 1 promise that a re-run with nothing new to do writes nothing. 2. **`uninstall` silently left behind the account this tool created.** It removes only an account the record says it created, so after any second `install` the flag was `false` and the user survived the uninstall forever. Reproduced before the fix, on a host where the installer had unambiguously created the account: ``` · left the runicgateway account alone — this installer did not create it uid=996(runicgateway) gid=996(runicgateway) groups=996(runicgateway) ``` The record now inherits `true` from a prior record naming the **same** account. Matching on the name matters: inheriting across a rename would authorize deleting a user this installer never made. This is invisible on Windows, where the SCM's virtual account is never created by us and disappears with the service — which is why three phases of Windows smoke runs never showed it. Same family as the two bugs Phase 1's live run caught (`install.json` recording the run's verb, and the `Bridge.cfg` comparison): a record describing *the run* instead of *the state*. ## How it was tested `fmt` / `clippy -D warnings` / 138 tests on both platforms, plus a new unit test covering the inherit / don't-inherit / different-account cases. On the real systemd host, after the fix, from a clean slate (`userdel`, state dirs removed): | Step | Result | |---|---| | `install` | `runicgateway-link.service active, enabled`, running as `runicgateway`; unit carries both `UOLINK_CONFIG` and `UOLINK_DB_PATH`; `sidecar.toml` is `600 runicgateway:root`; db in `/var/lib/runicgateway` owned by the service user; `/health` answers protocol 3; record says `user_created: true` | | `install` again | `install.json unchanged`, **byte-identical**, still `user_created: true` | | `doctor` | `Service runicgateway-link.service active, enabled as runicgateway` — first exercise of `service::observe` on systemd; the database row correctly shows `/var/lib/…` rather than the path the binary would pick alone | | `uninstall` | service stopped, disabled and its unit removed; **`removed the runicgateway service user`**; binary removed; `sidecar.toml` + database kept; nothing listening on 8080; report written; **all 24 overlay files still in the ServUO tree** | ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code (Opus 5)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it.
wtclaude added 1 commit 2026-08-05 09:01:33 +00:00
fix(installer): keep user_created sticky across re-runs
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m2s
060b8815cf
Found on the first real systemd host this installer has ever run on: a
container with systemd as PID 1, installing into /usr/bin, /etc and
/var/lib for real.

`service::prepare` answers "did THIS run create the service account",
which is false on every run after the first — by then the account exists.
Recording that verbatim made the field describe the run rather than the
state, with two consequences:

- `install.json` changed on an otherwise-identical second run, breaking
  the Phase 1 promise that a re-run with nothing new to do writes
  nothing.
- `uninstall` removes only an account it created, so after any second
  `install` it silently left behind the very user this tool had added.
  Reproduced before the fix: "left the runicgateway account alone — this
  installer did not create it", on a host where it plainly had.

The record now inherits `true` from a prior record naming the same
account, and only that account: inheriting across a rename would
authorize deleting a user this installer never made.

Not visible on Windows, where the SCM's virtual account is never created
by us and goes with the service — which is why three phases of Windows
smoke runs never showed it.

Verified after the fix on the same host: fresh install records
user_created true, an identical second run leaves install.json
byte-identical, and uninstall then removes the account, the unit, the
service and the binary — leaving sidecar.toml, the database and all 24
overlay files in the ServUO tree exactly where they were.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit 14f65d50a0 into edge 2026-08-05 09:30:14 +00:00
whitlocktech deleted branch fix/service-user-created-sticky 2026-08-05 09:30:15 +00:00
Sign in to join this conversation.
No description provided.