fix(install): devolver la propiedad del árbol al usuario del servicio

Instalando con sudo, el agente quedaba sin poder leer ni escribir NADA de lo
suyo, y tanto el panel como su sonda lo reportaban como éxito.

El bootstrap ejecuta el binario como root, y ese arranque crea todo el árbol bajo
PREFIX: config/, config/data/app.db, config/logs/, Entrada/, Procesados/,
Fallados/ y Temp/. La siembra escribe config/.env en 0600, también de root. Pero
el unit se registra con User=$SUDO_USER, así que el agente arranca como una
cuenta común que no puede leer su configuración (load_dotenv sin try/except),
ni guardar jobs en su base, ni mover ZIPs entre las carpetas de trabajo.
install.sh no hacía chown en ninguna parte.

Reproducido en un contenedor antes de arreglarlo: .env root:root 600, app.db y
Entrada/ de root, las tres operaciones del agente fallando — y `test -f` de la
sonda del panel dando ok, o sea verde justo en el caso roto.

SERVICE_USER se resuelve ahora al principio (antes se calculaba dentro del case
de --service, después del bootstrap y de la siembra), conservando la misma
cadena de precedencia. El chown va después de ambos pasos, solo cuando el script
corre como root y el servicio no es root, y solo del usuario —no del grupo—;
`chown` no toca los modos, así que el 0600 del .env sobrevive.

Con dos cuidados que no son opcionales:

- Guarda contra un PREFIX de sistema: un `chown -R` sobre / o /opt sería
  catastrófico. Se rechazan las rutas de sistema y las de un solo componente,
  avisando en vez de abortar una instalación ya hecha.
- Si el chown falla se reporta como ERROR con el comando de arreglo, no se traga.
  Es el mismo criterio que el repo ya aplica al icacls de Windows: no se asume
  que un comando de endurecimiento tuvo éxito.

Probado: arreglo, guarda de /opt, usuario inexistente, y sin sudo (no intenta
nada). Windows no necesita el arreglo simétrico y queda documentado por qué: la
tarea corre como SYSTEM con RunLevel Highest y install.ps1 no restringe ninguna
ACL, así que hereda de su carpeta padre y puede leer todo lo del Administrador.
This commit is contained in:
2026-07-30 16:16:28 -06:00
parent f02cd1f4c3
commit 874fe0c46f
2 changed files with 56 additions and 1 deletions

View File

@@ -66,6 +66,10 @@ UNIT_NAME="cloudrestoreas.service"
# línea de cron del vigilante contiene esta ruta, así que el `sh -c` que la ejecuta hace
# match consigo mismo, el vigilante cree que el agente corre y no lo rearranca nunca.
PGREP_PAT="^$PREFIX/$BIN_NAME"
# Usuario del servicio. Se resuelve AQUÍ y no dentro del case de --service porque el chown de
# propiedad tiene que correr antes, justo después del bootstrap. Precedencia: la variable
# explícita, luego quien invocó el sudo, luego el usuario efectivo.
SERVICE_USER="${SERVICE_USER:-${SUDO_USER:-$(id -un)}}"
# Localizar el binario: dist/CloudRestoreAS, dist/CloudRestoreAS-linux, o junto al script.
SRC=""
@@ -269,9 +273,51 @@ if [[ -n "$PANEL_ENV_FILE" ]]; then
rm -f "$PANEL_ENV_FILE"
fi
# --- Propiedad del árbol -------------------------------------------------------
# Corriendo con sudo, el bootstrap crea config/, config/data/app.db, config/logs/, Entrada/,
# Procesados/, Fallados/ y Temp/ como ROOT, y la siembra escribe config/.env en 0600 de root.
# Pero el unit se registra con User=$SERVICE_USER, así que el agente arrancaría sin poder leer su
# propia configuración ni escribir su base local: falla en bucle, y la sonda del panel lo pinta
# verde porque comprueba existencia y no lectura. Devolver la propiedad es lo que cierra eso.
#
# Solo el usuario, no el grupo: basta para que el agente escriba y evita sorpresas con el grupo
# primario del destino. Y `chown` no toca los modos, así que el 0600 del .env sobrevive.
chown_tree_is_safe() {
local path="$1"
# Un chown -R sobre una ruta de sistema sería catastrófico. El panel ya valida la ruta, pero
# este script también se corre a mano.
case "$path" in
/|/opt|/usr|/etc|/var|/home|/srv|/root|/bin|/sbin|/lib|/lib64|/tmp) return 1 ;;
esac
# Al menos dos componentes: /algo/otro.
case "${path#/}" in
*/*) return 0 ;;
*) return 1 ;;
esac
}
if [[ "$MODE" != "update-in-place" && "$(id -u)" -eq 0 && "$SERVICE_USER" != "root" ]]; then
if ! id -u "$SERVICE_USER" >/dev/null 2>&1; then
echo "AVISO: el usuario '$SERVICE_USER' no existe en este servidor; no se cambia la" >&2
echo " propiedad de $PREFIX. Revisa SERVICE_USER." >&2
elif ! chown_tree_is_safe "$PREFIX"; then
echo "AVISO: $PREFIX es una ruta de sistema; no se hace chown -R sobre ella." >&2
echo " Ajústala a mano si el servicio corre como '$SERVICE_USER'." >&2
elif chown -R "$SERVICE_USER" "$PREFIX"; then
echo "Propiedad de $PREFIX asignada a $SERVICE_USER."
else
# No se asume que un comando de endurecimiento tuvo éxito: si falla, el agente no arranca
# y es mejor decirlo ahora que dejar un servicio reiniciándose en silencio.
echo "ERROR: no se pudo asignar la propiedad de $PREFIX a $SERVICE_USER." >&2
echo " El servicio corre como ese usuario y no podría leer config/.env ni escribir" >&2
echo " su base local. Corrígelo antes de arrancarlo:" >&2
echo " chown -R $SERVICE_USER $PREFIX" >&2
exit 1
fi
fi
case "$MODE" in
service)
SERVICE_USER="${SERVICE_USER:-${SUDO_USER:-$(id -un)}}"
UNIT_SRC="$SCRIPT_DIR/packaging/linux/$UNIT_NAME"
UNIT_DST="/etc/systemd/system/$UNIT_NAME"
if [[ ! -w "$(dirname "$UNIT_DST")" ]]; then