From 1dc915f64206e5e4db235e8fe3250eafabc81968 Mon Sep 17 00:00:00 2001 From: librelad Date: Sun, 24 May 2026 01:16:49 +0100 Subject: [PATCH] =?UTF-8?q?feat(switcher):=20reconcileDockerOwnership=20?= =?UTF-8?q?=E2=80=94=20safe=20owner-only=20control-plane=20reconcile=20on?= =?UTF-8?q?=20mode=20switch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mode switches change /docker ownership expectations, but the switcher only ever fixed the socket — never file ownership — so a rooted<->rootless swap left the control plane owned for the wrong mode (CLI + de-sudo helpers then can't access it). Add reconcileDockerOwnership (single source of truth): swaps ONLY the owner of LibrePortal's control plane (configs/logs/scripts/DB + /docker top) to the mode owner (root rooted / manager rootless). It never resets mode bits (only adds o+x on /docker for traversal and o+r on the DB for the WebUI), and never touches /docker/containers/** app data, backups/, or ssl/ssh keys. Wired into both switch branches between container-retag and app-start. App data is deliberately NOT chowned: container UIDs re-map across modes (rootless subuid offset), so a chown can't carry e.g. Postgres data across — that's a backup->switch->restore operation. Switcher now warns to back up stateful apps before switching and restore after. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- .../docker/type_switcher/swap_docker_type.sh | 9 ++++ .../permission/libreportal_folders.sh | 44 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/scripts/docker/type_switcher/swap_docker_type.sh b/scripts/docker/type_switcher/swap_docker_type.sh index 2a9ed13..d34d654 100755 --- a/scripts/docker/type_switcher/swap_docker_type.sh +++ b/scripts/docker/type_switcher/swap_docker_type.sh @@ -23,6 +23,13 @@ dockerSwitcherSwap() if [[ $CFG_DOCKER_INSTALL_TYPE != $docker_type ]]; then isHeader "Docker Root/Rootless Switcher" + # Switching modes re-maps container UIDs (rootless shifts them by the + # subuid base), so a stateful app's existing data won't line up in the + # new mode. LibrePortal's own control plane is reconciled automatically + # (reconcileDockerOwnership), but app data is not touched — the safe way + # to carry stateful apps across is backup-before / restore-after. + isNotice "Switching Docker mode re-maps container ownership. Back up any stateful apps (databases etc.) BEFORE switching and restore them AFTER — app data is intentionally left untouched here." + if [[ $CFG_DOCKER_INSTALL_TYPE == "rooted" ]]; then if [[ $flag != "cli" ]]; then isNotice "The current Docker Setup Type is currently : ${RED}$docker_type${NC}" @@ -53,6 +60,7 @@ dockerSwitcherSwap() fi dockerServiceStart root; dockerSwitcherUpdateContainersToDockerType; + reconcileDockerOwnership "$CFG_DOCKER_INSTALL_TYPE"; dockerStartAllApps; databaseOptionInsert "docker_type" $CFG_DOCKER_INSTALL_TYPE; fi @@ -84,6 +92,7 @@ dockerSwitcherSwap() fi dockerServiceStart rootless; dockerSwitcherUpdateContainersToDockerType; + reconcileDockerOwnership "$CFG_DOCKER_INSTALL_TYPE"; dockerStartAllApps; databaseOptionInsert "docker_type" $CFG_DOCKER_INSTALL_TYPE; fi diff --git a/scripts/function/permission/libreportal_folders.sh b/scripts/function/permission/libreportal_folders.sh index 800d597..64d4a5a 100755 --- a/scripts/function/permission/libreportal_folders.sh +++ b/scripts/function/permission/libreportal_folders.sh @@ -1,5 +1,49 @@ #!/bin/bash +# Reconcile ONLY the LibrePortal control plane ownership for the current Docker +# mode, so the CLI and the de-sudo helpers (runFileOp/runInstallOp) keep working +# after a rooted<->rootless switch: +# rooted -> root:root (CLI operates via sudo) +# rootless -> $sudo_user_name (the manager/runtime user owns its own files) +# +# Scope is DELIBERATELY narrow — the de-sudo-critical, LibrePortal-owned files +# only: configs/, logs/, install scripts, the apps DB, and the /docker top level +# (kept o+x so the docker user can still traverse to its container dirs). +# +# It does NOT touch /docker/containers/** (per-app data, written by per-app +# container UIDs — uid-mapped through subuids in rootless), nor backups/ (owned +# by the backup-engine user), nor ssl/ssh (key material). A blanket chown there +# would break permission-strict apps (Postgres/MySQL refuse a wrong-owned data +# dir; Grafana/Nextcloud run as fixed UIDs) AND can't survive the rootless +# subuid offset anyway. Moving a stateful app across modes is a backup->switch +# ->restore operation, not a chown. Must run as ROOT, apps stopped. Idempotent. +reconcileDockerOwnership() +{ + local mode="${1:-$CFG_DOCKER_INSTALL_TYPE}" + [[ -d "$docker_dir" ]] || return 0 + + local owner="root" + [[ "$mode" == "rootless" ]] && owner="$sudo_user_name" + + # Swap ONLY the owner on our own control-plane files; never reset mode bits + # (so nothing that validates its permissions gets surprised). The only two + # bits we *add* (never remove) are structural and on our own dirs, not app + # files: o+x on /docker so the docker user can still traverse to its + # container dirs, and o+r on the DB so the WebUI container can read it. + runSystem chown "$owner:$owner" "$docker_dir" + runSystem chmod o+x "$docker_dir" + + # LibrePortal-owned control plane (NOT containers/ backups/ ssl/ ssh) — owner + # only, modes preserved. + local p + for p in "$configs_dir" "$logs_dir" "$script_dir" "$docker_dir/$db_file"; do + [[ -e "$p" ]] && runSystem chown -R "$owner:$owner" "$p" + done + [[ -f "$docker_dir/$db_file" ]] && runSystem chmod o+r "$docker_dir/$db_file" + + isSuccessful "Reconciled LibrePortal control-plane ownership for $mode ($owner)" +} + fixFolderPermissions() { local silent_flag="$1"