diff --git a/scripts/function/permission/before_start.sh b/scripts/function/permission/before_start.sh index 5995ba6..7389059 100755 --- a/scripts/function/permission/before_start.sh +++ b/scripts/function/permission/before_start.sh @@ -12,9 +12,12 @@ fixPermissionsBeforeStart() fixAppFolderPermissions; changeRootOwnedFile $docker_dir/$db_file $sudo_user_name - # App Specific - if [[ $app_name != "" ]]; then - changeRootOwnedFilesAndFolders $containers_dir$app_name $docker_install_user + # The regenerable WebUI dir is reconciled to the mode's container owner via + # the shared helper (same code path as install + switch). Third-party app + # data ownership is established at install/restore time, not blanket-chowned + # here — a wrong-owner chown would break permission-strict apps. + if [[ "$app_name" == "libreportal" ]]; then + reconcileWebuiDirOwnership fi # Traefik diff --git a/scripts/function/permission/libreportal_folders.sh b/scripts/function/permission/libreportal_folders.sh index ac145aa..3dff82a 100755 --- a/scripts/function/permission/libreportal_folders.sh +++ b/scripts/function/permission/libreportal_folders.sh @@ -1,47 +1,56 @@ #!/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) +# 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, 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. +# 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 - # 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. + # user). Fall back to absolute defaults; 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 @@ -57,48 +66,53 @@ reconcileDockerOwnership() 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. + # 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: -# 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. +# 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 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" + 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" @@ -109,19 +123,10 @@ fixFolderPermissions() 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 + reconcileContainersTopOwnership } diff --git a/scripts/function/permission/ownership/root_files_folders.sh b/scripts/function/permission/ownership/root_files_folders.sh deleted file mode 100755 index 25ff166..0000000 --- a/scripts/function/permission/ownership/root_files_folders.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -changeRootOwnedFilesAndFolders() -{ - local dir_to_change="$1" - local user_name="$2" - - # Check if the directory exists - if [ ! -d "$dir_to_change" ]; then - isError "Install directory '$dir_to_change' does not exist." - fi - - # Start the result command in the background - (sudo find "$dir_to_change" -type f -user root -exec sudo chown "$user_name:$user_name" {} \; ) & - - local start_time=$(date +%s) - local time_threshold=5 - - # Check periodically if the result command is still running - while ps -p $! > /dev/null; do - local current_time=$(date +%s) - local elapsed_time=$((current_time - start_time)) - if [ "$elapsed_time" -ge "$time_threshold" ]; then - # Display the message - isNotice "Updating ownership of $dir_to_change" - isNotice "This may take a while depending on the size and amount of files..." - break - fi - sleep 1 - done - isSuccessful "Find files owned by root and change ownership" - - local result=$(sudo find "$dir_to_change" -type d -user root -exec sudo chown "$user_name:$user_name" {} \;) - checkSuccess "Find directories owned by root and change ownership" - - isSuccessful "Updated ownership of root-owned files and directories." -} diff --git a/scripts/source/files/arrays/files_function.sh b/scripts/source/files/arrays/files_function.sh index c578690..91f6ed1 100755 --- a/scripts/source/files/arrays/files_function.sh +++ b/scripts/source/files/arrays/files_function.sh @@ -27,7 +27,6 @@ function_scripts=( "function/permission/libreportal_folders.sh" "function/permission/ownership/file.sh" "function/permission/ownership/folder_group.sh" - "function/permission/ownership/root_files_folders.sh" "function/permission/ownership/root_file.sh" "function/run/create_successful_run_file.sh" "function/run/reinstall_libreportal.sh"