#!/bin/bash
# uhhm-corp-setup: enrol this machine as a uhhm corp device.
# Idempotent; run as root (via sudo, so the invoking user is known). Steps:
#   1. join the headscale VPN (vpn.uhhm.no) via tailscale
#   2. ensure the [uhhm] pacman repo + its signing key (shipped keyring)
#   3. wire kanidm into NSS and PAM, start kanidm-unixd
#   4. FS-Cache: start cachefilesd where the kernel supports it, else drop the
#      `fsc` option from the mount units (kernels without cachefiles fail it)
#   5. create the invoking user's machine-local toolchain dirs in /var/local
#   6. (--with-nfs-home) automount /home from ergo over the VPN. A local /home
#      (btrfs subvolume etc.) blocks this; --take-over-home comments it out of
#      /etc/fstab (data untouched) and asks for a reboot.
#   7. (--with-basement) mount the basement archive from kasse (on demand)
#
# --undo reverses 3, 6 and 7 (PAM, NSS, automounts) so the package can be
# removed without leaving a login stack that points at kanidm.
set -euo pipefail

[ "$(id -u)" -eq 0 ] || { echo "run as root" >&2; exit 1; }

PAM=/etc/pam.d/system-auth
# 0.2.12: the automount units are gone. /home is a static, on-demand
# home.mount that logind pulls in per session (see files/home.mount).
STALE_AUTOMOUNTS=(home.automount home-bl-basement.automount)

undo() {
    echo "==> mounts: stopping the NFS home and archive (if mounted)"
    systemctl disable "${STALE_AUTOMOUNTS[@]}" 2>/dev/null || true   # pre-0.2.12
    systemctl stop home-bl-basement.mount 2>/dev/null || true
    echo "    /home stays mounted while sessions are using it; it is gone after a reboot"

    echo "==> PAM: removing pam_kanidm from $PAM"
    if grep -q pam_kanidm "$PAM"; then
        cp "$PAM" "$PAM.pre-uhhm-undo"
        sed -i '/pam_kanidm\.so/d' "$PAM"
        echo "    removed (backup at $PAM.pre-uhhm-undo)"
    fi

    echo "==> NSS: removing kanidm from /etc/nsswitch.conf"
    sed -i -E '/^(passwd|group):/s/ kanidm\b//' /etc/nsswitch.conf

    # fsc_dropin is defined further down, so clean these up directly.
    rm -f /etc/systemd/system/home.mount.d/10-no-fsc.conf \
          /etc/systemd/system/home-bl-basement.mount.d/10-no-fsc.conf
    rmdir /etc/systemd/system/home.mount.d /etc/systemd/system/home-bl-basement.mount.d 2>/dev/null || true

    echo "==> kanidm-unixd: stopping"
    systemctl disable --now kanidm-unixd-tasks kanidm-unixd 2>/dev/null || true

    if [ -e /etc/fstab.pre-uhhm-corp ]; then
        echo "==> NOTE: --take-over-home commented out your local /home in /etc/fstab."
        echo "    Restore it with: cp /etc/fstab.pre-uhhm-corp /etc/fstab (then reboot)"
    fi
    echo "==> done. Left in place: tailscale (\`tailscale logout\` to leave the VPN)"
    echo "    and the [uhhm] repo in /etc/pacman.conf."
}

WITH_NFS_HOME=0
WITH_BASEMENT=0
TAKE_OVER_HOME=0
HOME_PENDING=0
for arg in "$@"; do
    case "$arg" in
        --with-nfs-home) WITH_NFS_HOME=1 ;;
        --take-over-home) TAKE_OVER_HOME=1 ;;   # comment out a local /home in fstab
        --with-basement) WITH_BASEMENT=1 ;;   # kasse archive; only once kasse is on the tailnet
        --undo) undo; exit 0 ;;
        *) echo "unknown option: $arg (use --with-nfs-home, --take-over-home," >&2
           echo "   --with-basement or --undo)" >&2; exit 1 ;;
    esac
done

echo "==> VPN: joining headscale at vpn.uhhm.no"
systemctl enable --now tailscaled
WANT="https://vpn.uhhm.no"
# Check the actual control plane, not just "tailscaled is running": a machine
# already logged into commercial Tailscale passes `tailscale status` yet is
# NOT on our VPN. Switching control planes needs --force-reauth.
HAVE=$(tailscale debug prefs 2>/dev/null | jq -r '.ControlURL // empty' 2>/dev/null || true)
if [ "$HAVE" != "$WANT" ]; then
    echo "    control plane is '${HAVE:-none}', moving to $WANT"
    # HEADSCALE_AUTHKEY (preauth key) makes this non-interactive; otherwise
    # it prints a Kanidm OIDC URL to complete in a browser. The key goes via
    # a private file, not argv, so it never shows up in `ps`.
    authargs=()
    if [ -n "${HEADSCALE_AUTHKEY:-}" ]; then
        keyfile=$(mktemp)
        trap 'rm -f "$keyfile"' EXIT
        printf '%s' "$HEADSCALE_AUTHKEY" >"$keyfile"
        authargs=(--auth-key "file:$keyfile")
    else
        # --qr also renders the login URL as a QR code: scan it with a phone
        # that is already signed into Kanidm and the node registers itself via
        # OIDC. No `headscale nodes register` on the server.
        authargs=(--qr)
    fi
    tailscale up --login-server="$WANT" --accept-dns --accept-routes --force-reauth \
        "${authargs[@]}"
else
    echo "    already on $WANT ($(tailscale ip -4 2>/dev/null | head -1))"
fi
# A failed join must NOT abort the run: NSS/PAM/kanidm-unixd are what make
# logins work, and leaving them unconfigured (especially right after --undo)
# locks the user out of their own machine. Only the NFS home needs the tailnet.
VPN_OK=1
if ! tailscale status >/dev/null 2>&1; then
    VPN_OK=0
    echo "    WARN: VPN join did not complete; continuing with identity setup." >&2
    echo "          Join later with: tailscale up --login-server=$WANT --qr" >&2
fi

echo "==> pacman: ensure the [uhhm] package registry + its signing key"
if ! grep -q '^\[uhhm\]' /etc/pacman.conf; then
    cat >> /etc/pacman.conf <<'PAC'

[uhhm]
SigLevel = Required DatabaseOptional
Server = https://project.uhhm.no/api/packages/bl/arch/$repo/$arch
PAC
    echo "    added [uhhm] repo"
fi
# The key ships in this package (/usr/share/pacman/keyrings/uhhm*), so no
# trust-on-first-use download; the install script also populates it.
pacman-key --populate uhhm >/dev/null \
    && echo "    uhhm registry key trusted" \
    || echo "    WARN: pacman-key --populate uhhm failed (is the keyring initialised?)"

echo "==> NSS: adding kanidm to /etc/nsswitch.conf"
for db in passwd group; do
    if ! grep -qE "^${db}:.*\bkanidm\b" /etc/nsswitch.conf; then
        sed -i "s/^${db}:.*/& kanidm/" /etc/nsswitch.conf
        echo "    ${db}: + kanidm"
    fi
done

echo "==> PAM: wiring pam_kanidm into system-auth"
if ! grep -q pam_kanidm "$PAM"; then
    cp "$PAM" "$PAM.pre-uhhm-corp"
    sed -i \
        -e '/^auth.*pam_unix.so/i auth       sufficient                  pam_kanidm.so ignore_unknown_user' \
        -e '/^account.*pam_unix.so/i account    [default=bad success=ok user_unknown=ignore] pam_kanidm.so' \
        -e '/^password.*pam_unix.so/i password   sufficient                  pam_kanidm.so ignore_unknown_user' \
        -e '/^session.*pam_unix.so/i session    optional                    pam_kanidm.so' \
        "$PAM"
    echo "    patched (backup at $PAM.pre-uhhm-corp)"
else
    echo "    already wired"
fi
# system-auth belongs to pambase; an upgrade that changes it lands as .pacnew
# WITHOUT our lines. Merging it naively drops kanidm from the login stack.
if [ -e "$PAM.pacnew" ]; then
    echo "    WARN: $PAM.pacnew exists. After merging it, re-run uhhm-corp-setup"
    echo "          to re-add the pam_kanidm lines."
fi

echo "==> kanidm-unixd"
systemctl enable --now kanidm-unixd kanidm-unixd-tasks

# The `fsc` mount option needs the kernel's cachefiles backend. Some kernels
# (e.g. linux-asahi) ship without it, and then cachefilesd cannot bind a cache
# AND an fsc mount fails outright — so drop fsc from the mount units there.
# Both directions are idempotent, so a kernel change is picked up on re-run.
fsc_dropin() {   # $1 = unit, $2 = on|off
    local dir f opts
    dir="/etc/systemd/system/$1.d"
    f="$dir/10-no-fsc.conf"
    if [ "$2" = off ]; then
        opts=$(sed -n 's/^Options=//p' "/usr/lib/systemd/system/$1" | sed -e 's/,fsc//' -e 's/fsc,//')
        install -dm755 "$dir"
        printf '# Written by uhhm-corp-setup: this kernel has no cachefiles backend.\n[Mount]\nOptions=%s\n' \
            "$opts" > "$f"
    else
        rm -f "$f"
        rmdir "$dir" 2>/dev/null || true
    fi
}

echo "==> NFS FS-Cache (local disk cache for the NFS mounts)"
modprobe -q cachefiles 2>/dev/null || true
if [ -e /dev/cachefiles ] || [ -e /proc/fs/cachefiles ] || [ -d /sys/module/cachefiles ]; then
    install -dm700 /var/cache/fscache   # cachefilesd refuses to start without it
    if systemctl list-unit-files cachefilesd.service >/dev/null 2>&1 &&
       systemctl enable --now cachefilesd 2>/dev/null; then
        echo "    cachefilesd running; mounts keep the fsc option"
        fsc_dropin home.mount on
        fsc_dropin home-bl-basement.mount on
    else
        echo "    WARN: cachefilesd not installed or failed to start; dropping fsc"
        fsc_dropin home.mount off
        fsc_dropin home-bl-basement.mount off
    fi
else
    echo "    no cachefiles backend in this kernel ($(uname -r)); dropping fsc"
    echo "    from the NFS mounts (they work fine without the local cache)"
    fsc_dropin home.mount off
    fsc_dropin home-bl-basement.mount off
fi
systemctl daemon-reload

# Machine-local, arch-specific toolchains. The shared fish config sets
# CARGO_HOME=/var/local/cargo, RUSTUP_HOME=/var/local/rustup and
# PNPM_HOME=/var/local/pnpm-<arch>, so on a mixed-arch fleet each machine
# keeps its own compiled toolchains outside the NFS home. ~/.local/bin stays
# roamed and is for arch-independent scripts only. These paths are shared by
# design (one toolchain per machine), owned by the user who ran the setup.
echo "==> machine-local toolchain dirs in /var/local"
if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != root ]; then
    install -dm755 -o "$SUDO_USER" -g "$(id -g "$SUDO_USER")" \
        /var/local/cargo /var/local/rustup "/var/local/pnpm-$(uname -m)"
    echo "    owned by $SUDO_USER"
else
    echo "    skipped: run via sudo so the owning user is known"
fi

# A local /home (btrfs subvolume, LVM, whatever) blocks the NFS home twice
# over: home.automount refuses to start when /home is already a mount point,
# and an /etc/fstab entry for /home generates a home.mount in
# /run/systemd/generator that takes priority over our packaged unit. So the
# local mount has to leave fstab before /home can come from ergo. Nothing is
# reformatted or deleted: the local filesystem keeps its data, it just stops
# being mounted at /home.
free_local_home() {
    local src fstype opts
    findmnt -n /home >/dev/null 2>&1 || return 0
    fstype=$(findmnt -no FSTYPE /home)
    src=$(findmnt -no SOURCE /home)
    opts=$(findmnt -no OPTIONS /home)
    case "$fstype" in nfs|nfs4) return 0 ;; esac   # already ours

    # For btrfs, quote the subvolume back so the suggested mount command is
    # the exact one (a bare `mount <dev>` would give the top-level subvolume).
    local mountopt=""
    case "$opts" in
        *subvol=*) mountopt="-o subvol=${opts##*subvol=}"; mountopt="${mountopt%%,*}" ;;
    esac

    if ! awk '$1 !~ /^#/ && $2 == "/home" { found = 1 } END { exit !found }' /etc/fstab; then
        echo "    /home is mounted from $src ($fstype) but not via /etc/fstab;" >&2
        echo "    unmount it (or remove whatever mounts it) and re-run." >&2
        return 1
    fi

    if [ "$TAKE_OVER_HOME" -eq 0 ]; then
        cat >&2 <<EOM
    /home is a local mount: $src ($fstype)
      options: $opts
    The NFS home cannot be enabled while that is in /etc/fstab. Re-run with
    --take-over-home to comment out the /home line (backup: /etc/fstab.pre-uhhm-corp),
    then reboot and run this again. The local data is untouched and stays
    reachable, e.g.:
      mkdir -p /mnt/local-home && mount $mountopt $src /mnt/local-home
EOM
        return 1
    fi

    echo "    commenting out the local /home in /etc/fstab (was: $src $fstype)"
    cp /etc/fstab /etc/fstab.pre-uhhm-corp
    awk '$1 !~ /^#/ && $2 == "/home" { print "# uhhm-corp: /home now comes from ergo over NFS"; print "#" $0; next } { print }' \
        /etc/fstab.pre-uhhm-corp > /etc/fstab
    systemctl daemon-reload
    echo "    done (backup at /etc/fstab.pre-uhhm-corp). REBOOT, then re-run"
    echo "    'uhhm-corp-setup --with-nfs-home' to enable the NFS automount."
    echo "    Local home data stays on $src; reach it later with:"
    echo "      mkdir -p /mnt/local-home && mount $mountopt $src /mnt/local-home"
    return 1   # /home is still mounted until the reboot
}

if [ "$WITH_NFS_HOME" -eq 1 ] && [ "$VPN_OK" -eq 0 ]; then
    echo "==> NFS home: skipped, this machine is not on the tailnet yet" >&2
    HOME_PENDING=1
elif [ "$WITH_NFS_HOME" -eq 1 ]; then
    echo "==> NFS home from ergo (shadows local /home!)"
    if free_local_home; then
        systemctl daemon-reload
        # Nothing to enable: logind requires /home/<user> for every session, so
        # home.mount is pulled in at login, after tailscaled is up. Enabling a
        # unit here would put /home in the boot transaction, which is what
        # deadlocked network bring-up before 0.2.9. Mount it now so the current
        # session sees it too.
        systemctl disable "${STALE_AUTOMOUNTS[@]}" 2>/dev/null || true   # pre-0.2.12 leftovers
        systemctl start home.mount
        echo "    mounted now, and on every login from here on (no unit enabled)"
    else
        echo "    NFS home NOT enabled (see above)." >&2
        HOME_PENDING=1
    fi
fi
if [ "$WITH_BASEMENT" -eq 1 ]; then
    echo "==> basement archive from kasse"
    systemctl daemon-reload
    if systemctl start home-bl-basement.mount; then
        echo "    mounted at /home/bl/basement"
    else
        echo "    WARN: could not mount (is kasse on the tailnet?)" >&2
    fi
    echo "    On demand only — it is not enabled, so it never gates boot:"
    echo "      systemctl start home-bl-basement.mount"
fi

echo "==> SSH: kanidm-authorised logins are configured (etc/ssh drop-ins)."
echo "    Each user should now give this machine its own key:  uhhm-ssh-enroll"
echo "    Break-glass: drop a local admin key in /etc/ssh/authorized_keys.d/<user>"
echo "    so the box stays reachable if kanidm or the NFS home are down at boot."
echo
echo "==> IMPORTANT: every corp user needs a Kanidm POSIX password (SEPARATE from"
echo "    their web password+TOTP), or pam_kanidm's account phase refuses ALL logins"
echo "    for that user here (even key-based SSH): 'administrator enforced password"
echo "    change'. Set it with:"
echo "        kanidm login -D <user> && kanidm person posix set-password <user>"
if [ "$HOME_PENDING" -eq 1 ]; then
    echo
    echo "==> NFS home is NOT active. See the note above (a local /home needs"
    echo "    --take-over-home + a reboot; a missing tailnet needs the VPN join),"
    echo "    then re-run with --with-nfs-home."
fi
echo "==> done. Verify with: kanidm login -D <user>; getent passwd <user>"
