#!/bin/bash
#===============================================================================
# felhom-pbs-apply — the ONLY storage-entry path the felhom-agent sudoers permits for the PBS DR
# tier (slice 2; SPIKE-pbs-tier-provisioning-2026-07-10 §2b). The guarded-mkfs shape: the agent's
# in-process validation is the PRIMARY gate (descriptor-field charset + equality checks BEFORE
# exec); this wrapper is the minimal, auditable second gate as root.
#
# THE SET-ONLY LAW (spike §4, data-loss class): `pvesm` entry deletion DESTROYS the client
# encryption key file (<id>.enc = K) — un-decryptable backups. This wrapper therefore contains
# NO deletion path of any kind, and a re-apply is `pvesm set`-only. Grep-assertable; do not add
# a "cleanup" verb here, ever. Deprovision is a deliberate future operator op, not this tool.
#
# SECRET DISCIPLINE (spike §2b): sudo logs its full argv to auth.log → the PBS token secret
# arrives on STDIN, never as an argument to this wrapper. Inside, it is passed to pvesm's
# --password (root-local, transient ps exposure — the accepted spike posture); it is never
# echoed, never written anywhere except by pvesm itself (the 0600 .pw store).
#
# Ops (non-secret args on argv):
#   create    <id> <server> <datastore> <namespace> <token-id> <fingerprint> <secret-dir>
#             secret on stdin (required). Creates the pbs entry with --encryption-key autogen
#             (K born at /etc/pve/priv/storage/<id>.enc), then places .pw/.enc copies in
#             <secret-dir> when it differs (the §4b WARN-fix dir; escrow-create's PBSEncKeyPath
#             must find K there — spike §4 flag).
#   reconcile <id> <server> <namespace> <token-id> <fingerprint> <secret-dir>
#             secret on stdin (optional; empty = no credential change). `pvesm set` ONLY:
#             server/fingerprint (+ --password when a secret is fed, e.g. after a hub re-issue).
#             NOTE datastore is deliberately NOT settable, and namespace/token-id are accepted
#             for validation parity but NOT applied — tenancy identity is adopt-only (the
#             demo's live entry must never be repointed at a different namespace).
#   grant     <id>
#             The Part-0-evidenced dual-grant: FelhomAgentStore on /storage/<id> to the agent
#             user AND token (privsep intersection). Datastore.Audit reads ride the base role.
#===============================================================================
set -euo pipefail

die() { echo "felhom-pbs-apply: REFUSED: $*" >&2; exit 1; }

op="${1:-}"; id="${2:-}"
[[ -n "$op" && -n "$id" ]] || die "usage: felhom-pbs-apply <create|reconcile|grant> <storage-id> ..."

# Storage id: PVE grammar, conservative. Also the ACL path component — no slashes possible.
[[ "$id" =~ ^[A-Za-z][A-Za-z0-9_.-]{0,27}$ ]] || die "bad storage id ($id)"

STORECFG=/etc/pve/storage.cfg
PRIVDIR=/etc/pve/priv/storage

entry_exists() { grep -Eq "^pbs: ${id}\$" "$STORECFG"; }

val_server()    { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9.-]{0,62}$ ]] || die "bad server ($1)"; }
val_datastore() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,31}$ ]] || die "bad datastore ($1)"; }
val_ns()        { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]{0,31}$ ]] || die "bad namespace ($1)"; }
val_tok()       { [[ "$1" =~ ^[A-Za-z0-9_.-]+@[A-Za-z0-9]+![A-Za-z0-9_.-]+$ ]] || die "bad token id ($1)"; }
val_fp()        { [[ "$1" =~ ^([A-Fa-f0-9]{2}:){31}[A-Fa-f0-9]{2}$ ]] || die "bad fingerprint"; }
val_sdir() {
  case "$1" in
    /etc/pve/priv/storage) : ;;
    /var/lib/felhom-agent/*) [[ "$1" != *..* ]] || die "secret-dir traversal" ;;
    *) die "secret-dir must be $PRIVDIR or under /var/lib/felhom-agent ($1)" ;;
  esac
}

read_secret() { # → SECRET (may be empty when optional)
  local s
  s=$(head -c 256 || true)
  s="${s%$'\n'}"; s="${s%$'\r'}"
  printf '%s' "$s"
}

place_copies() { # secret-dir — the §4b WARN-fix placement (non-root agent can't read /etc/pve/priv)
  local sdir="$1"
  [[ "$sdir" == "$PRIVDIR" ]] && return 0
  install -d -o felhom-agent -g felhom-agent -m 0700 "$sdir"
  [[ -f "$PRIVDIR/$id.pw" ]] && install -o felhom-agent -g felhom-agent -m 0600 "$PRIVDIR/$id.pw" "$sdir/$id.pw"
  # K's copy: escrow-create stats PBSEncKeyPath(<secret-dir>/<id>.enc) — the spike §4 flag.
  [[ -f "$PRIVDIR/$id.enc" ]] && install -o root -g felhom-agent -m 0640 "$PRIVDIR/$id.enc" "$sdir/$id.enc"
  return 0
}

case "$op" in
create)
  [[ $# -eq 8 ]] || die "create needs 7 args: <id> <server> <datastore> <namespace> <token-id> <fingerprint> <secret-dir>"
  server="$3"; datastore="$4"; ns="$5"; tok="$6"; fp="$7"; sdir="$8"
  val_server "$server"; val_datastore "$datastore"; val_ns "$ns"; val_tok "$tok"; val_fp "$fp"; val_sdir "$sdir"
  entry_exists && die "entry $id already exists (reconcile is the re-apply path — set-only law)"
  SECRET=$(read_secret)
  [[ -n "$SECRET" ]] || die "create requires the token secret on stdin"
  pvesm add pbs "$id" \
    --server "$server" --datastore "$datastore" --namespace "$ns" \
    --username "$tok" --password "$SECRET" --fingerprint "$fp" \
    --content backup --encryption-key autogen >&2
  [[ -f "$PRIVDIR/$id.enc" ]] || die "pvesm add succeeded but K ($PRIVDIR/$id.enc) was not born"
  place_copies "$sdir"
  echo "felhom-pbs-apply: created $id (K born; encryption-key autogen)" >&2
  ;;
reconcile)
  [[ $# -eq 7 ]] || die "reconcile needs 6 args: <id> <server> <namespace> <token-id> <fingerprint> <secret-dir>"
  server="$3"; ns="$4"; tok="$5"; fp="$6"; sdir="$7"
  val_server "$server"; val_ns "$ns"; val_tok "$tok"; val_fp "$fp"; val_sdir "$sdir"
  entry_exists || die "entry $id does not exist (create is the fresh path)"
  SECRET=$(read_secret)
  args=(--server "$server" --fingerprint "$fp")
  [[ -n "$SECRET" ]] && args+=(--password "$SECRET")
  pvesm set "$id" "${args[@]}" >&2
  place_copies "$sdir"
  echo "felhom-pbs-apply: reconciled $id (set-only; tenancy identity untouched)" >&2
  ;;
grant)
  [[ $# -eq 2 ]] || die "grant takes only <id>"
  pveum acl modify "/storage/$id" --users felhom-agent@pve --roles FelhomAgentStore >&2
  pveum acl modify "/storage/$id" --tokens 'felhom-agent@pve!agent' --roles FelhomAgentStore >&2
  echo "felhom-pbs-apply: granted FelhomAgentStore on /storage/$id (user + token)" >&2
  ;;
*)
  die "unknown op ($op)"
  ;;
esac
