Repository navigation
fix(config): persist geo-filter saves through config symlinks - #422
Conversation
…n write (#339) Review round 2 of the opt-in non-root mode. All red on the previous head: - scripts/test-nonroot-entrypoint.sh (new, meant for CI): the real entrypoint and helper as root in the runtime image's pinned Alpine base with supervisord stubbed. Pins the unchanged off-mode start for unset/empty/0/false x DISABLE_*, the data dir keeping its owner (group write + sticky bit only), "0 of N" on a second start, that the services' user cannot modify, replace or remove .env/config.json/theme.json (host-owned and root-owned originals), that a start refuses such files when the services could have written them (also after the setting is removed), that root-written queue requests are set aside, and exact owner/mode restore from an owners record. - scripts/test-nonroot-helper.sh: -journal files in the list, docs and compose example must not suggest the host owner's IDs, CI wiring. - scripts/test-docker-nonroot.sh (local): new data dir expectations, a held root request, 409 with an in-volume writable config, record/restore rollback, and a case run as the services' user in a real container. - Go: geo-filter save refused in non-root mode even when every path is writable (no longer depends on /app being root-owned, cf. #422); the delete-client-rx CLI refuses root next to a database owned by another user. Adds the clientRxGeteuid seam the test needs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#339) Review round 2 of the opt-in non-root mode. Finding 1: the services no longer own the data directory. It keeps its owner and gets the services' group with group write and the sticky bit, so SQLite can still create its -wal/-shm/-journal files but the services can neither remove nor replace entries they do not own. Before .env is sourced and config.json/theme.json are linked, `nonroot.sh preflight` refuses the start ([nonroot] ERROR, nothing sourced, linked or changed) if one of them is a symlink, hard link or non-regular file, owned by the services' user, writable for their group or everyone, or if that user owns the data dir. The settings are read from .env as data for this check (plain lines only). A root-owned marker with the IDs keeps the check active after the setting is removed, until a rollback. Docs and the compose example now ask for a dedicated UID instead of the host owner's. Finding 2: SaveGeoFilter refuses explicitly in this mode, before touching the filesystem, so the 409 no longer depends on /app being root-owned (and holds with #422's symlink resolution). Finding 3: delete-client-rx refuses to run as root next to a database owned by another user, and writes nothing. At start, queue requests the services did not write are renamed to *.held, reported and never run; queue entries are no longer chowned. Finding 4: scripts/test-nonroot-entrypoint.sh runs in CI (Go Build & Test), plus ShellCheck of the entrypoint. Finding 5: `nonroot.sh owners` records owner and mode of the data dir and every listed path; `restore-owners` applies such a record exactly (paths created while on go to root) and accepts nothing outside the list. `set-owner` stays as the coarse fallback. Both refuse while a file root uses fails the check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review — CS-pve-agent1 PR#422 — head 42aea67Dom: APPROVE med nits Note: despite the task framing, #422 is the first PR for issue #407, not a follow-up closing earlier review nits. I checked each point of the issue's "Proposed work" and acceptance criteria. No author report was posted, so the PR body is the only author claim. Findings
No blocking findings. Issue points, one by one
Tests and mutantsOn the merged tree (
Mutants I ran against the PR's tests:
Live check: a local Go server with a fixture DB, prepared with Scope and policy checks: the diff is two files ( CI: Go Build & Test, Playwright E2E and Docker image all pass. The jobs that were skipped are the expected fork-guarded ones [T]. No flaky test (#256, #267) was involved. Not verified
|
Summary
Relates to #407.
Resolve the selected config path before reading and atomically replacing it. A config symlink remains a symlink, while its persistent target receives the geo-filter update or removal. Existing unique temporary files, permissions, best-effort ownership preservation, fsync, cleanup and wrapped errors remain intact.
Verification
Baseline: master
4a5ec5e231dd97ed7a4392e50e5f409f4dc5fab0.-race: PASS (504.391 s). Unchanged ingestor suite: PASS (128.895 s). Existing JavaScript baseline: 238/238 files.go vet, formatting and diff checks pass.Independent review
A separate reviewer inspected the immutable candidate tree and ran the focused race tests in an isolated archive export: PASS, no actionable findings.
Integration and limitations
The proposed non-root PR #404's create/rename error wrappers are not replaced; its changes are separate from this insertion. Filesystem permissions are checked at the effective target. A genuinely unwritable/read-only target still fails.
A file's read-only mode is not a write-policy guarantee when its parent directory permits atomic replacement. Foreign ownership restoration remains best-effort. This change does not add protection against an external actor retargeting links/directories during a save. An actual Docker bind-mount rehearsal has not been performed.
Test environment: macOS arm64, Go 1.26.3, Node 24.19.0. GitHub CI is separate evidence and pending at creation.
Performance and scope
One bounded filesystem path-resolution operation on an admin-save path, not the packet hot path. No database/schema, workflow, configuration option, customizer or dependency changes. No staging/production access, deployment, merge or upstream writes.