#!/bin/bash
# bpm-restic-user — Per-Host restic-SFTP-User-Lifecycle (Sprint 21,
# überarbeitet in 21.1 für Bind-Mount-Architektur).
#
# Erlaubt dem BPM-Webdienst (www-data), per sudo einen dedizierten
# System-User für einen Backup-Host anzulegen, dessen SSH-Chroot
# lokal liegt (echtes ext4-Filesystem mit POSIX-Permissions, das
# OpenSSH-Chroot stellen kann) und dessen Repo-Verzeichnis via
# `mount --bind` aufs Storage-Target gemappt wird.
#
# Warum nicht direkt aufs Storage: SSHD-ChrootDirectory verlangt
# zwingend root:root + nicht-world-writable für den Chroot-Pfad UND
# alle Eltern. CIFS-Mounts mit `uid=33,gid=33,forceuid,forcegid` haben
# alles als www-data — ChrootDirectory + authorized_keys-Permissions
# scheitern (Symptom: „bad ownership or modes" → Permission denied).
#
# Aufrufe (alle als root via sudo):
#   bpm-restic-user create <host_id> <repo_real_path> <pubkey_b64>
#   bpm-restic-user destroy <host_id>
#   bpm-restic-user create-vma <config_id> <repo_real_path> <pubkey_b64>
#   bpm-restic-user destroy-vma <config_id>
#
# - <host_id> / <config_id> sind nicht-negative Integer.
# - <repo_real_path> = tatsächlicher Repo-Pfad auf dem Storage-Mount,
#   muss unter /mnt/ liegen (Defense in Depth).
# - <pubkey_b64> ist die base64-kodierte authorized_keys-Zeile (mit
#   allen restricting options davor — zusammengebaut im ResticService).
#
# Chroot-Layout:
#   /var/lib/backup-manager/chroots/bpm-restic-<id>/           root:root 0755
#     ├─ .ssh/authorized_keys                                   <user>:<user> 0600
#     └─ repo/                                                  bind-mount → <repo_real_path>

set -euo pipefail

USERNAME_PREFIX="bpm-restic"
# Sprint 21.1: NICHT unter /var/lib/backup-manager/ (das ist 0770
# silvester:www-data — bpm-restic-User ist nicht in der Gruppe, könnte
# das Eltern-Verzeichnis nicht mal traversieren, sshd sieht
# authorized_keys nicht). Eigener System-Pfad, root:root 0755.
CHROOT_BASE="/var/lib/bpm-restic-chroots"
# Sprint 22.9: gemeinsame Gruppe aller per-Host-restic-User (sudo-runas-
# Ziel für www-data) + Per-User-restic-Cache-Basis.
RESTIC_GROUP="bpm-restic"
CACHE_BASE="/var/cache/bpm-restic"

die() { printf '[bpm-restic-user] FAIL: %s\n' "$*" >&2; exit 1; }

require_root() {
    [ "$(id -u)" -eq 0 ] || die "muss als root laufen"
}

valid_id() {
    [[ "$1" =~ ^[0-9]+$ ]] || die "id muss eine nicht-negative Ganzzahl sein, war: $1"
}

valid_repo_path() {
    local p="$1"
    [[ "$p" == /mnt/* ]] || die "repo_real_path muss unter /mnt/ liegen, war: $p"
    [[ "$p" != *".."* ]] || die "repo_real_path enthält .., abgelehnt"
}

# Anlage + Bind-Mount. Idempotent: bei wiederholtem Aufruf wird der
# Public-Key aktualisiert + bind-mount neu gesetzt falls nicht (mehr) aktiv.
do_create() {
    local kind="$1" id="$2" repo_real_path="$3" pubkey_b64="$4"
    valid_id "$id"
    valid_repo_path "$repo_real_path"

    local username
    case "$kind" in
        host) username="${USERNAME_PREFIX}-${id}" ;;
        vma)  username="${USERNAME_PREFIX}-vma-${id}" ;;
        *)    die "unbekannter kind: $kind" ;;
    esac

    local chroot_dir="${CHROOT_BASE}/${username}"
    local chroot_repo="${chroot_dir}/repo"
    local chroot_ssh="${chroot_dir}/.ssh"

    # CHROOT_BASE liegt auf lokalem ext4 (/var/lib/...) → POSIX-Perms
    # respektiert, root:root möglich.
    install -d -o root -g root -m 0755 "$CHROOT_BASE"

    # Sprint 22.9: gemeinsame Gruppe für alle per-Host-restic-User, damit
    # www-data via `sudo -u <user> restic …` die lokalen Ops (init/forget/
    # snapshots/ls) als der jeweilige User ausführen kann (Repo-Ownership
    # auf echtem POSIX-FS). sudoers erlaubt runas auf %bpm-restic.
    getent group "$RESTIC_GROUP" >/dev/null 2>&1 || groupadd --system "$RESTIC_GROUP"

    # User anlegen (idempotent). Mitgliedschaft in www-data: für CIFS-
    # Schreibzugriff (file_mode/dir_mode, ohne Gruppe nur other-r-x →
    # restic Permission Denied). Mitgliedschaft in bpm-restic: sudo-runas-
    # Ziel. Innerhalb des Chroots bleibt der User auf sein Repo isoliert.
    if ! id -u "$username" >/dev/null 2>&1; then
        useradd --system --no-create-home --home-dir "$chroot_dir" \
                --shell /usr/sbin/nologin --groups "www-data,$RESTIC_GROUP" "$username"
    else
        usermod -aG "www-data,$RESTIC_GROUP" "$username" 2>/dev/null || true
    fi

    # Repo-Real-Verzeichnis auf dem Storage muss existieren (sonst läuft
    # bind-mount auf "nicht vorhanden"). Sprint 22.9: Owner = der per-Host-
    # User (NICHT www-data), damit auf echtem ext4/iSCSI die als dieser User
    # laufenden restic-Ops UND das Remote-SFTP-Backup dasselbe 0700-Repo
    # teilen. Auf CIFS ist chown/chmod No-op (forceuid+noperm) → harmlos.
    install -d -o "$username" -g "$username" -m 0700 "$repo_real_path"

    # Sprint 22.9: Per-User-restic-Cache (XDG_CACHE_HOME). Das passwd-Home
    # zeigt auf den root:root-Chroot (nicht schreibbar) → restic braucht
    # einen eigenen, vom User schreibbaren Cache-Ort.
    install -d -o root -g root -m 0755 "$CACHE_BASE"
    install -d -o "$username" -g "$username" -m 0700 "$CACHE_BASE/$username"

    # Chroot-Layout. SSHD-Anforderung: Chroot UND alle Eltern root:root,
    # nicht-group/other-writable. Symlinks im Chroot-Pfad sind verboten.
    install -d -o root     -g root     -m 0755 "$chroot_dir"
    install -d -o "$username" -g "$username" -m 0700 "$chroot_ssh"

    # Bind-Mount chroot/repo → repo_real_path (auf Storage). WICHTIG: den
    # chroot_repo-Mountpoint NICHT mit `install -d` anfassen, solange dort ein
    # (evtl. veralteter) Bind aktiv ist — bei gelöschtem/gewechseltem Ziel
    # liefert stat/install I/O-Error (set -e → Abbruch). Reihenfolge daher:
    #   1. aktiven Mount lösen, falls er auf ein ANDERES Ziel zeigt
    #      (Storage-Wechsel SMB→iSCSI bzw. neu angelegtes Repo-Verzeichnis).
    #      Vergleich über device:inode (robust ggü. findmnt-Quelldarstellung
    #      CIFS-UNC vs. /dev); stale Mount → leerer stat → != → wird gelöst.
    #   2. Verzeichnis sicherstellen + binden — NUR wenn kein Mount aktiv ist.
    if findmnt -n -M "$chroot_repo" >/dev/null 2>&1 \
       && [ "$(stat -c '%d:%i' "$chroot_repo" 2>/dev/null)" != "$(stat -c '%d:%i' "$repo_real_path" 2>/dev/null)" ]; then
        umount "$chroot_repo" 2>/dev/null || umount -l "$chroot_repo" 2>/dev/null \
            || die "umount (Re-Point) $chroot_repo fehlgeschlagen (busy?). Bitte manuell prüfen."
    fi
    if ! findmnt -n -M "$chroot_repo" >/dev/null 2>&1; then
        install -d -o root -g root -m 0755 "$chroot_repo"
        mount --bind "$repo_real_path" "$chroot_repo"
    fi

    # authorized_keys aktualisieren.
    local ak="${chroot_ssh}/authorized_keys"
    printf '%s\n' "$pubkey_b64" | base64 -d > "$ak"
    chown "$username:$username" "$ak"
    chmod 0600 "$ak"

    # Home-Eintrag in /etc/passwd auf Chroot zeigen.
    usermod --home "$chroot_dir" "$username" >/dev/null 2>&1 || true

    printf 'username=%s\n' "$username"
    printf 'chroot_dir=%s\n' "$chroot_dir"
    printf 'chroot_repo=%s\n' "$chroot_repo"
    printf 'repo_real_path=%s\n' "$repo_real_path"
}

# Entfernt User + Chroot. Der Bind-Mount wird zuerst gelöst, dann das
# Chroot-Verzeichnis (inkl. authorized_keys) entfernt. Das tatsächliche
# Repo-Verzeichnis auf dem Storage bleibt UNANGETASTET — Admin entscheidet
# selbst, ob die Backup-Daten weg sollen.
do_destroy() {
    local kind="$1" id="$2"
    valid_id "$id"

    local username
    case "$kind" in
        host) username="${USERNAME_PREFIX}-${id}" ;;
        vma)  username="${USERNAME_PREFIX}-vma-${id}" ;;
        *)    die "unbekannter kind: $kind" ;;
    esac

    if ! id -u "$username" >/dev/null 2>&1; then
        printf 'noop=user_not_found\n'
        return 0
    fi

    local chroot_dir="${CHROOT_BASE}/${username}"
    local chroot_repo="${chroot_dir}/repo"

    # Bind-Mount lösen — wenn das fehlschlägt (busy), Admin manuell.
    if findmnt -n -M "$chroot_repo" >/dev/null 2>&1; then
        umount "$chroot_repo" 2>/dev/null || die "umount $chroot_repo fehlgeschlagen (busy?). Bitte manuell prüfen."
    fi

    # User droppen (--remove löscht das Home-Verzeichnis nicht, weil
    # --no-create-home oben gesetzt war; aber wir räumen das Chroot
    # selbst weg, da es nur SSH-Auth-Material enthält, keine Daten).
    userdel "$username" 2>/dev/null || true
    [ -d "$chroot_dir" ] && rm -rf "$chroot_dir"
    # Sprint 22.9: Per-User-restic-Cache mit aufräumen.
    [ -d "$CACHE_BASE/$username" ] && rm -rf "$CACHE_BASE/$username"

    printf 'removed_user=%s\n' "$username"
    printf 'preserved_repo_path=%s\n' "(real repo path auf Storage — siehe vorherige create-Calls)"
}

require_root

[ $# -ge 1 ] || die "Aufruf: bpm-restic-user {create|destroy|create-vma|destroy-vma} ..."

cmd="$1"
shift

case "$cmd" in
    create)       do_create host "$@" ;;
    destroy)      do_destroy host "$@" ;;
    create-vma)   do_create vma "$@" ;;
    destroy-vma)  do_destroy vma "$@" ;;
    *)            die "unbekanntes Kommando: $cmd" ;;
esac
