#!/bin/sh
# SC-355 / SC-216 / SC-538: select identity and protect the lifecycle lock.
# shellcheck disable=SC2015 # Every A && B || die chain intentionally fails either check.
set -eu
# Read by updater preflight; this is a fixed capability marker, not an override.
# shellcheck disable=SC2034
SHCP_CONSOLE_IDENTITY_SCHEMA=1
PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH
SHCPD=/usr/sbin/shcpd
RELEASE=/opt/shcp
CONFIG=/etc/shcp/console.env
LOCK=/run/shcp/release-lifecycle.lock

die() { printf 'shcp: %s\n' "$*" >&2; exit 1; }
usage() {
	printf '%s\n' 'Usage: shcp <exact-command-or-alias> [arguments/options]' \
		'       shcp help <exact-command-or-alias> [options]' \
		'       shcp list [options] | --help | --version' \
		'Unprefixed SHCP aliases are accepted. Abbreviations are refused.' \
		'Global options must follow the command. No arguments lists SHCP commands.'
}
case "${1:-}" in
	-h|--help) usage; exit 0 ;;
	-V|--version) [ "$#" -eq 1 ] || { usage >&2; exit 2; } ;;
	-*) usage >&2; exit 2 ;;
esac

# An immutable file in a writable directory is replaceable. Check resolved
# ancestors too; the release symlink itself lives under the trusted /opt parent.
trusted() (
	path=$(readlink -f -- "$1") || exit 1
	[ -n "$path" ] || exit 1
	while :; do
		[ "$(stat -c %u -- "$path")" = 0 ] || exit 1
		mode=$(stat -c %a -- "$path") || exit 1
		[ "$((0$mode & 022))" -eq 0 ] || exit 1
		[ "$path" != / ] || break
		path=${path%/*}
		[ -n "$path" ] || path=/
	done
)
trusted_lock() {
	[ ! -L "$LOCK" ] && [ -f "$LOCK" ] && trusted "$LOCK" &&
		[ "$(stat -c '%u:%g:%a:%h' -- "$LOCK")" = "0:$panel_gid:640:1" ]
}
[ -f "$CONFIG" ] && trusted "$CONFIG" || die "missing or unsafe $CONFIG; reinstall the current installer console configuration"
OS_SETPRIV='' OS_FLOCK=''
# Two-key data, never shell code. Reject duplicates and extras.
while IFS= read -r line || [ -n "$line" ]; do
	case "$line" in
		OS_SETPRIV=/*) [ -z "$OS_SETPRIV" ] || die 'duplicate OS_SETPRIV'; OS_SETPRIV=${line#*=} ;;
		OS_FLOCK=/*) [ -z "$OS_FLOCK" ] || die 'duplicate OS_FLOCK'; OS_FLOCK=${line#*=} ;;
		*) die 'invalid console configuration' ;;
	esac
done < "$CONFIG"
for executable in "$OS_SETPRIV" "$OS_FLOCK" "$SHCPD"; do
	case "$executable" in *[!a-zA-Z0-9_./-]*|'') die 'invalid console executable path' ;; esac
	[ -f "$executable" ] && [ -x "$executable" ] && trusted "$executable" || die "missing or unsafe executable: $executable"
done
# SC-538: read access is sufficient to take an exclusive flock. Only root and
# the panel group may open this inode; world readability permits tenant DoS.
panel_uid=$(id -u shcp) && panel_gid=$(id -g shcp) || die 'shcp account is unavailable'
case "$panel_uid:$panel_gid" in *[!0-9:]*|0:*|*:0|:*|*:) die 'invalid shcp account identity' ;; esac
caller=$(id -u) || die 'cannot determine caller identity'
[ "$caller" = 0 ] || [ "$caller" = "$panel_uid" ] || die 'console must run as root or shcp'
trusted_lock || die "missing or unsafe lifecycle lock: $LOCK; require root:shcp 0640 with one link; rerun installer configuration"
# The installer creates this inode once; never truncate, unlink or recreate it.
# fd 9 survives both execs, retaining the lock until the command exits.
exec 9< "$LOCK"
"$OS_FLOCK" --shared --timeout 30 9 || die 'release lifecycle busy; retry after installation/update completes'
trusted "$(dirname -- "$RELEASE")" && trusted "$RELEASE" || die 'unsafe active release path'
cd -P -- "$RELEASE" || die 'active release is unavailable'
for artifact in bin/command-identity config/console-identities.json bin/console; do
	[ -f "$artifact" ] && trusted "$artifact" || die 'release lacks trusted command identity metadata; upgrade shcp-base first. Recovery: /usr/sbin/shcpd php-cli /opt/shcp/bin/console <exact-command> (root-cache semantics)'
done

resolve() {
	resolved=$("$SHCPD" php-cli bin/command-identity --resolve "$1") || die "unknown or unclassified command: $1 (use an exact name)"
	case "$resolved" in ''|*[!a-zA-Z0-9_:.-]*|-*) die 'invalid command resolver output' ;; esac
	printf '%s\n' "$resolved"
}
if [ "$#" -eq 0 ]; then
	command=list
	set -- list shcp
else
	case "$1" in
		-V|--version) command=list; set -- --version ;;
		list) shift; command=list; set -- list shcp "$@" ;;
		help)
			shift
			if [ "$#" -gt 0 ]; then
				command=$(resolve "$1") || exit 1
				shift
				set -- help "$command" "$@"
			else
				command=help; set -- help
			fi ;;
		*) command=$(resolve "$1") || exit 1; shift; set -- "$command" "$@" ;;
	esac
fi
identity=$("$SHCPD" php-cli bin/command-identity "$command") || die "missing identity for $command"
case "$identity" in
	manual) die "$command requires explicit worker identity; use the configured worker service or /usr/sbin/shcpd php-cli /opt/shcp/bin/console as the required user" ;;
	root) [ "$caller" = 0 ] || die "$command requires root" ;;
	panel)
		if [ "$caller" = 0 ]; then
			exec "$OS_SETPRIV" --reuid="$panel_uid" --regid="$panel_gid" --init-groups -- "$SHCPD" php-cli bin/console "$@"
		fi
		[ "$caller" = "$panel_uid" ] || die "$command must run as root or shcp" ;;
	*) die 'invalid command identity output' ;;
esac
exec "$SHCPD" php-cli bin/console "$@"
