#!/bin/bash
# Run a command in a user namespace whose id maps let it restore file ownership.
#
#   restic-userns-exec restic restore <id> --target …
#
# Restore runs as the backup user, which has no CAP_CHOWN, so it cannot put back
# the uid a file had when it was backed up. A user namespace solves that — but
# only if every id involved is mapped, and LibrePortal needs three mappings at
# once while `unshare` accepts exactly ONE range per option.
#
# That limit is why the previous one-line prefix silently did not work:
#
#     unshare --map-root-user --map-users=SUB:SUB:N --map-groups=SUB:SUB:N
#
# mapped root and the subuid range, but never the backup user's own GID. App
# data is written as <container-uid>:<backup-user>, so the group half of every
# chown referred to an unmapped id, lchown returned EINVAL, and the file kept
# the restoring user's ownership. Grafana's database came back 1002:1002 instead
# of 231543:1002 and the app died with "attempt to write a readonly database" —
# under a restore that reported success.
#
# newuidmap/newgidmap can write multi-range maps, so the maps become:
#
#     uid:  0      <- caller      (inner root: without this, capabilities are
#                                  dropped at exec and chown is not permitted)
#           SUB..  <- SUB..       (identity, so restic can ask for the stored
#                                  container uid by its real number)
#     gid:  caller <- caller      (identity: the group half of app-data chowns)
#           SUB..  <- SUB..       (identity)
#
# The caller's own UID is deliberately NOT identity-mapped: it is spent on inner
# root. A file stored as <caller>:<caller> therefore fails its chown — and lands
# owned by the caller anyway, because that is who inner root is outside. So the
# one case this cannot map is the one case that needs no mapping.
#
# Anything unexpected — no subuid range, no newuidmap, a namespace that will not
# start — falls back to running the command plainly, which is what happened
# before this existed.
set -u

(( $# )) || { echo "restic-userns-exec: no command given" >&2; exit 2; }

TMPDIR_MADE=""
_plain() { [[ -n "$TMPDIR_MADE" ]] && rm -rf "$TMPDIR_MADE"; exec "$@"; }

for _bin in unshare newuidmap newgidmap; do
    command -v "$_bin" >/dev/null 2>&1 || _plain "$@"
done

_usr=$(id -un 2>/dev/null) || _plain "$@"
_u=$(id -u); _g=$(id -g)

_uline=$(grep "^${_usr}:" /etc/subuid 2>/dev/null | head -1)
_gline=$(grep "^${_usr}:" /etc/subgid 2>/dev/null | head -1)
[[ -n "$_uline" && -n "$_gline" ]] || _plain "$@"

_ustart="${_uline#*:}"; _ustart="${_ustart%%:*}"; _ucount="${_uline##*:}"
_gstart="${_gline#*:}"; _gstart="${_gstart%%:*}"; _gcount="${_gline##*:}"
[[ "$_ustart" =~ ^[0-9]+$ && "$_ucount" =~ ^[0-9]+$ ]] || _plain "$@"
[[ "$_gstart" =~ ^[0-9]+$ && "$_gcount" =~ ^[0-9]+$ ]] || _plain "$@"
(( _ucount > 0 && _gcount > 0 )) || _plain "$@"

# The caller's own id has to sit outside its sub-range, or the two entries would
# overlap on the outer side and the kernel rejects the whole map.
(( _u < _ustart || _u >= _ustart + _ucount )) || _plain "$@"
(( _g < _gstart || _g >= _gstart + _gcount )) || _plain "$@"

TMPDIR_MADE=$(mktemp -d "${TMPDIR:-/tmp}/lp-userns.XXXXXX") || { TMPDIR_MADE=""; _plain "$@"; }
_fifo="$TMPDIR_MADE/gate"
mkfifo -m 600 "$_fifo" 2>/dev/null || _plain "$@"

# Opened here and inherited there, rather than opened by path in the child:
# until the map is written the child's uid is unmapped, so to the filesystem it
# is nobody and could not open its own gate.
exec 9<>"$_fifo"

unshare --user bash -c 'read -r _ <&9; exec "$@"' _ "$@" &
_child=$!

# The namespace exists before unshare execs, but only just — retry briefly
# rather than assume. A write that succeeds cannot be repeated, so stop there.
_mapped=0
for _ in $(seq 1 100); do
    kill -0 "$_child" 2>/dev/null || break
    if newuidmap "$_child" 0 "$_u" 1 "$_ustart" "$_ustart" "$_ucount" 2>/dev/null; then
        _mapped=1; break
    fi
    sleep 0.02
done

if (( _mapped )); then
    newgidmap "$_child" "$_g" "$_g" 1 "$_gstart" "$_gstart" "$_gcount" 2>/dev/null || _mapped=0
fi

if (( ! _mapped )); then
    kill "$_child" 2>/dev/null
    wait "$_child" 2>/dev/null
    _plain "$@"
fi

printf 'go\n' >&9
wait "$_child"
_rc=$?
rm -rf "$TMPDIR_MADE"
exit "$_rc"
