#!/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 # The control plane is owned by the MANAGER user in BOTH modes. root:root was # never the intended model — it only ever showed up as an artifact of # un-de-sudo'd commands (sudo creating files as root instead of libreportal). # 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 and the live config file; never empty. local owner="${sudo_user_name:-libreportal}" local ddir="${docker_dir:-/docker}" local cdir="${containers_dir:-$ddir/containers/}" 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}" # Read the rootless docker install user AUTHORITATIVELY from config — NOT the # lowercase $docker_install_user global, which check_install_type.sh sets to # the MANAGER user in rooted mode, so during a rooted->rootless switch it's # stale (=manager) and would mis-own the WebUI dir. 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}') appusr="${appusr:-${CFG_DOCKER_INSTALL_USER:-dockerinstall}}" [[ -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" # LibrePortal's OWN WebUI container dir is regenerable, so flip it to the # mode's container owner so the WebUI survives a switch (safe to recurse — # one UID, no per-app uid to clobber). Third-party app data is left untouched. reconcileWebuiDirOwnership "$mode" isSuccessful "Reconciled ownership for $mode" } # 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: # rooted -> the manager ($sudo_user_name) # rootless -> the docker install user, read authoritatively from config (the # $docker_install_user global is the manager in rooted and so goes # stale across a switch). # Recurses (one regenerable UID, no per-app uid to clobber). Must run as root # (runSystem stays root in both modes). 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 cfgdir="${configs_dir:-${docker_dir:-/docker}/configs/}" local webui_dir="${cdir}libreportal" if [[ ! -d "$webui_dir" ]]; then isNotice "reconcileWebuiDirOwnership: WebUI dir '$webui_dir' not found — skipped" return 0 fi local app_owner="${sudo_user_name:-libreportal}" if [[ "$mode" == "rootless" ]]; then 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}') app_owner="${appusr:-${CFG_DOCKER_INSTALL_USER:-dockerinstall}}" fi runSystem chown -R "$app_owner:$app_owner" "$webui_dir" isSuccessful "Reconciled WebUI dir ($webui_dir) -> $app_owner" } 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 chmod +x "$containers_dir" > /dev/null 2>&1) if [ "$silent_flag" == "loud" ]; then checkSuccess "Updating $containers_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 # Install user related local result=$(runSystem chown $docker_install_user:$docker_install_user "$containers_dir" > /dev/null 2>&1) if [ "$silent_flag" == "loud" ]; then checkSuccess "Updating $containers_dir with $docker_install_user ownership" fi }