#!/bin/sh
# ===========================================================================
# bastion-broker — command-allowlist gate for docker-bastion "broker mode".
#
# Invoked once per session with the client-requested command:
#   • SSH:  the force-command wrapper passes "$SSH_ORIGINAL_COMMAND".
#   • HTTP: the CGI passes the X-Bastion-Command request header.
#
# The requested command is matched — anchored, WHOLE-LINE — against a set of
# extended-regex (ERE) rules. On a match it is word-split (NO shell, so the
# metacharacters ; | & ` $( ) < > are literal arguments, never operators)
# and exec'd, optionally behind a trusted COMMAND_PREFIX. No match → refused.
#
# ---------------------------------------------------------------------------
# Security model
# ---------------------------------------------------------------------------
# The regex rules are the ENTIRE authorization boundary for what a client may
# run. Two properties keep that boundary tight:
#
#   1. Whole-line anchoring (grep -x). A rule must match the request from
#      first character to last; it can never match a substring.
#   2. Shell-free execution. The validated request is split on whitespace and
#      exec'd directly — there is no `sh -c`. A sloppy rule therefore cannot
#      escalate to command injection; the worst case is that an allowed
#      program receives an odd-looking argument.
#
# Still: write rules with restrictive argument classes (e.g. [^[:space:]]+),
# never a bare `.*`. Values that must reach the target program intact cannot
# contain whitespace (word-splitting) — generate passwords/tokens from a
# space-free alphabet (hex, base64url) on the caller side.
#
# Config is read from boot-written FILES, never the environment: sshd does
# not propagate the daemon's env to a ForceCommand session, so anything the
# broker needs at session time must live on disk.
# ===========================================================================
set -u
export HOME=/home/agent
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

ENV_RULES=/etc/bastion/allowed-commands.env      # snapshot of $ALLOWED_COMMANDS (written at boot)
LIVE_RULES=/etc/bastion/allowed-commands.list    # optional live bind-mount (re-read every session)
PREFIX_FILE=/etc/bastion/command-prefix          # optional trusted prefix (written at boot)

# $1 wins (SSH wrapper / HTTP CGI pass it explicitly); fall back to the env
# var sshd sets so the broker also works as a bare ForceCommand target.
REQ="${1:-${SSH_ORIGINAL_COMMAND:-}}"

log() { printf '[broker] %s\n' "$1" >&2; }

refuse() {
    # $1 = reason for the audit log (may include the request),
    # $2 = sanitized message shown to the client.
    log "DENY: $1"
    printf 'bastion: %s\n' "$2" >&2
    exit 126
}

# --- 1) Empty request -----------------------------------------------------
[ -n "$REQ" ] || refuse "<empty request>" "no command supplied (broker mode expects a command)"

# --- 2) Reject anything spanning more than one line -----------------------
# grep matches line-by-line; a multi-line request could slip a benign first
# line past the allowlist while carrying a second, malicious line that the
# shell-free exec would still pass along. Refuse outright.
if [ "$(printf '%s' "$REQ" | tr -dc '\n\r' | wc -c)" -ne 0 ]; then
    refuse "<multiline request>" "command not permitted"
fi

# --- 3) Match against the rule set ---------------------------------------
# Sources are concatenated; comments (#…) and blank lines are dropped and
# each rule is trimmed of surrounding whitespace (so indented YAML block
# lines work). grep -Eqx => ERE, whole-line (implicit ^…$ anchoring), quiet.
collect_rules() {
    for src in "$ENV_RULES" "$LIVE_RULES"; do
        [ -f "$src" ] || continue
        awk '{ sub(/^[[:space:]]+/, ""); sub(/[[:space:]]+$/, "") } NF && $0 !~ /^#/' "$src"
    done
}

matched=0
while IFS= read -r pat; do
    [ -n "$pat" ] || continue
    if printf '%s' "$REQ" | grep -Eqx -- "$pat"; then
        matched=1
        break
    fi
done <<RULES
$(collect_rules)
RULES

[ "$matched" -eq 1 ] || refuse "$REQ" "command not permitted"

# --- 4) Execute -----------------------------------------------------------
log "ALLOW: $REQ"

PREFIX=""
[ -f "$PREFIX_FILE" ] && PREFIX="$(cat "$PREFIX_FILE")"

set -f                  # no pathname expansion when we word-split below
# Intentional, unquoted word-splitting of the trusted prefix + validated
# request. set -f above means * ? [ are NOT globbed; IFS does the splitting.
# shellcheck disable=SC2086
set -- $PREFIX $REQ
[ "$#" -ge 1 ] || refuse "$REQ" "command not permitted"

exec "$@"
