#!/bin/bash # The user that owns container data for the current Docker mode: # rooted -> the manager ($sudo_user_name) # rootless -> the docker install user # For rootless read it AUTHORITATIVELY from config — the $docker_install_user # global is set to the manager in rooted mode, so it goes stale across a switch. # Single source of truth for "who owns container files", used by the install # permission pass and the switch reconcile alike. dockerContainerOwner() { local mode="${1:-$CFG_DOCKER_INSTALL_TYPE}" if [[ "$mode" == "rootless" ]]; then local cfgdir="${configs_dir:-${docker_dir:-/docker}/configs/}" local appusr appusr=$(grep -h '^CFG_DOCKER_INSTALL_USER=' "$cfgdir/general/general_docker_install" 2>/dev/null | head -1 | cut -d= -f2 | awk '{print $1}') echo "${appusr:-${CFG_DOCKER_INSTALL_USER:-dockerinstall}}" else echo "${sudo_user_name:-libreportal}" fi } # Reconcile the LibrePortal control plane + structural container dirs for the # current Docker mode, so the CLI and the de-sudo helpers (runFileOp/ # runInstallOp) keep working after a rooted<->rootless switch. The control plane # is owned by the MANAGER user in BOTH modes (root:root was never the intended # model — it only ever appeared as an artifact of un-de-sudo'd commands); the # structural container dirs are owned by the mode's container owner (see # dockerContainerOwner). # # Scope is DELIBERATELY narrow — the de-sudo-critical, LibrePortal-owned files # only: configs/, logs/, install scripts, the apps DB, the /docker top level # (kept o+x for traversal), plus the structural containers/ top dir and the # regenerable WebUI dir. It does NOT touch third-party /docker/containers// # data (per-app container UIDs, uid-mapped through subuids in rootless), nor # backups/, nor ssl/ssh (key material) — a blanket chown there would break # permission-strict apps 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 # Robust resolution — these globals aren't always populated in the CLI/switch # context, which previously made ops silently no-op (relative paths / empty # user). Fall back to absolute defaults; never empty. local owner="${sudo_user_name:-libreportal}" local ddir="${docker_dir:-/docker}" local cfgdir="${configs_dir:-$ddir/configs/}" local logdir="${logs_dir:-$ddir/logs/}" local scrdir="${script_dir:-$ddir/install}" local dbpath="$ddir/${db_file:-database.db}" [[ -d "$ddir" ]] || return 0 # Swap ONLY the owner on our own control-plane files; never reset mode bits. # The only two bits we *add* (never remove) are structural: o+x on /docker so # the docker user can traverse to its container dirs, and o+r on the DB so the # WebUI container can read it. runSystem chown "$owner:$owner" "$ddir" runSystem chmod o+x "$ddir" local p for p in "$cfgdir" "$logdir" "$scrdir" "$dbpath"; do [[ -e "$p" ]] && runSystem chown -R "$owner:$owner" "$p" done [[ -f "$dbpath" ]] && runSystem chmod o+r "$dbpath" # Structural container dirs owned by the mode's container owner. reconcileContainersTopOwnership "$mode" reconcileWebuiDirOwnership "$mode" isSuccessful "Reconciled ownership for $mode" } # Own the containers/ top dir (structural, NOT per-app data) as the mode's # container owner and keep it traversable, so the docker user can create + own # its per-app dirs under it. Shared by the install permission pass and the switch # reconcile. Runs as root (runSystem stays root in both modes). reconcileContainersTopOwnership() { local mode="${1:-$CFG_DOCKER_INSTALL_TYPE}" local cdir="${containers_dir:-${docker_dir:-/docker}/containers/}" [[ -d "$cdir" ]] || return 0 local owner owner="$(dockerContainerOwner "$mode")" runSystem chown "$owner:$owner" "$cdir" runSystem chmod o+x "$cdir" } # Chown LibrePortal's own (regenerable) WebUI container dir to the mode's # container owner, so both the WebUI container and the host-side runFileOp # generators — which run as that user — can write it. Recurses (one regenerable # UID, no per-app uid to clobber). Must run as root. Shared by the switch # reconcile and the fresh-install WebUI setup so a fresh install gets the same # ownership a switch does — otherwise rootless generators hit "Permission # denied" on a manager-owned frontend/data tree. reconcileWebuiDirOwnership() { local mode="${1:-$CFG_DOCKER_INSTALL_TYPE}" local cdir="${containers_dir:-${docker_dir:-/docker}/containers/}" local webui_dir="${cdir}libreportal" if [[ ! -d "$webui_dir" ]]; then isNotice "reconcileWebuiDirOwnership: WebUI dir '$webui_dir' not found — skipped" return 0 fi local owner owner="$(dockerContainerOwner "$mode")" runSystem chown -R "$owner:$owner" "$webui_dir" isSuccessful "Reconciled WebUI dir ($webui_dir) -> $owner" } # Traversal (+x) bits only. Ownership of the structural container dirs is handled # by the shared reconcile helpers above (single source of truth), so install and # switch converge on the same state. fixFolderPermissions() { local silent_flag="$1" local app_name="$2" local result=$(runSystem chmod +x "$docker_dir" > /dev/null 2>&1) if [ "$silent_flag" == "loud" ]; then checkSuccess "Updating $docker_dir with execute permissions." fi local result=$(runSystem find "$script_dir" "$ssl_dir" "$ssh_dir" "$backup_dir" "$restore_dir" "$migrate_dir" -maxdepth 2 -type d -exec chmod +x {} \;) if [ "$silent_flag" == "loud" ]; then checkSuccess "Adding execute permissions for $docker_install_user user" fi reconcileContainersTopOwnership }