#!/bin/sh
# fs-ensure - Ensure a file or directory exists with the correct ownership and permissions.
#
# Usage: fs-ensure [-p] [-R] <dir|file> <path> <class>
#
# Options:
#   -p     Create missing parent directories (owned by root:root 0755)
#   -R     (dir only) Apply ownership recursively to existing contents; the
#          mode is still applied to the top-level directory only
#
# Arguments:
#   type   - Resource type: "dir" or "file"
#   path   - Absolute path to create/verify (must be under the active CSL_WS)
#   class  - Permission class (see below)
#
# Workspace/runtime identity is loaded from:
#   /etc/csl/csl.env
#   /etc/csl/runtime.env
# Static policy is loaded from:
#   /etc/csl/fs-ensure.conf
#
# Examples:
#   fs-ensure dir  /var/lib/csl/instances/prod/config                    control_dir
#   fs-ensure dir  /var/lib/csl/instances/prod/logs                      runtime_dir
#   fs-ensure -p   dir /var/lib/csl/instances/prod/installer/releases    installer_dir
#   fs-ensure file /var/lib/csl/instances/prod/.env                      runtime_file

set -eu

CONFIG="/etc/csl/fs-ensure.conf"
COMMON_ENV="/etc/csl/csl.env"
RUNTIME_ENV="/etc/csl/runtime.env"

err() { echo "fs-ensure: $*" >&2; exit 1; }

[ -r "$COMMON_ENV" ] || err "missing config $COMMON_ENV"
[ -r "$RUNTIME_ENV" ] || err "missing config $RUNTIME_ENV"
[ -r "$CONFIG" ] || err "missing config $CONFIG"

. "$COMMON_ENV"
. "$RUNTIME_ENV"

: "${CSL_HOME:=/var/lib/csl}"
: "${CSL_INSTANCE:=prod}"
: "${CSL_WS:=${CSL_HOME}/instances/${CSL_INSTANCE}}"
: "${CSL_RUNTIME_USER:?missing CSL_RUNTIME_USER in $RUNTIME_ENV}"
: "${CSL_RUNTIME_GROUP:?missing CSL_RUNTIME_GROUP in $RUNTIME_ENV}"

BASE_DIR="$CSL_WS"
BASE_DIR_REGEX="$(printf '%s\n' "$BASE_DIR" | awk '{ gsub(/[][(){}.^$*+?|\\]/, "\\\\&"); gsub(/\//, "\\/"); print }')"
CONTROL_USER="csl"
CONTROL_GROUP="csl"
RUNTIME_USER="$CSL_RUNTIME_USER"
RUNTIME_GROUP="$CSL_RUNTIME_GROUP"
IPC_GROUP="${CSL_SOCKET_GROUP:-cslipc}"

. "$CONFIG"

# Parse optional flags
CREATE_PARENTS=0
RECURSIVE=0
while true; do
  case "${1:-}" in
    -p) CREATE_PARENTS=1; shift ;;
    -R) RECURSIVE=1; shift ;;
    *) break ;;
  esac
done

[ $# -eq 3 ] || err "Usage: fs-ensure [-p] [-R] <dir|file> <path> <class>"

TYPE="$1"
TARGET="$2"
CLASS="$3"

case "$TYPE" in
  dir|file) ;;
  *) err "invalid type" ;;
esac

[ "$RECURSIVE" = "1" ] && [ "$TYPE" != "dir" ] && err "-R is only valid for dirs"

# Validate base path
case "$TARGET" in
  "$BASE_DIR"/*) ;;
  *) err "path outside BASE_DIR" ;;
esac

# Reject traversal
case "$TARGET" in
  *"/../"*|*"//"*|*"./"*) err "invalid path" ;;
esac

# Resolve class using eval (POSIX replacement for bash ${!var})
eval "OWNER=\${CLASS_${CLASS}_OWNER:-}"
eval "GROUP=\${CLASS_${CLASS}_GROUP:-}"
eval "MODE=\${CLASS_${CLASS}_MODE:-}"

# Container-identity classes declare the uid:gid the service runs as INSIDE the
# container; map them to the host ids docker uses for that identity:
#   rootless: container id N (N>=1) -> subid_base+(N-1); id 0 -> runtime user/group
#   rootful : the literal ids (no user namespace)
eval "C_UID=\${CLASS_${CLASS}_CONTAINER_UID:-}"
eval "C_GID=\${CLASS_${CLASS}_CONTAINER_GID:-}"

subid_base() {
  awk -F: -v u="$2" '$1 == u { print $2; exit }' "$1"
}

if [ -n "$C_UID" ] || [ -n "$C_GID" ]; then
  [ -n "$C_UID" ] && [ -n "$C_GID" ] || err "class $CLASS: CONTAINER_UID and CONTAINER_GID must both be set"
  case "${CSL_DOCKER_MODE:-rootful}" in
    rootless)
      UB="$(subid_base /etc/subuid "$RUNTIME_USER")"
      GB="$(subid_base /etc/subgid "$RUNTIME_USER")"
      [ -n "$UB" ] || err "no /etc/subuid range for $RUNTIME_USER"
      [ -n "$GB" ] || err "no /etc/subgid range for $RUNTIME_USER"
      if [ "$C_UID" -eq 0 ]; then OWNER="$(id -u "$RUNTIME_USER")"; else OWNER=$((UB + C_UID - 1)); fi
      if [ "$C_GID" -eq 0 ]; then GROUP="$(getent group "$RUNTIME_GROUP" | cut -d: -f3)"; else GROUP=$((GB + C_GID - 1)); fi
      ;;
    *)
      OWNER="$C_UID"
      GROUP="$C_GID"
      ;;
  esac
fi

[ -n "$OWNER" ] || err "unknown class $CLASS (no OWNER)"
[ -n "$GROUP" ] || err "unknown class $CLASS (no GROUP)"
[ -n "$MODE"  ] || err "unknown class $CLASS (no MODE)"

# Check allowed patterns
match_allowed() {
  _type="$1"
  _target="$2"
  _TYPE="$(echo "$_type" | tr '[:lower:]' '[:upper:]')"
  _i=1

  while true; do
    _var="ALLOW_${_TYPE}_PATTERN_${_i}"
    eval "_pattern=\${$_var:-}"
    [ -z "$_pattern" ] && break

    if echo "$_target" | grep -qE "$_pattern"; then
      return 0
    fi
    _i=$((_i + 1))
  done

  return 1
}

match_builtin_allowed() {
  _type="$1"
  _target="$2"

  case "$_type:$_target" in
    file:"$BASE_DIR"/config/*|file:"$BASE_DIR"/runtime_config/*|file:"$BASE_DIR"/data/*)
      return 0
      ;;
  esac

  return 1
}

match_allowed "$TYPE" "$TARGET" || match_builtin_allowed "$TYPE" "$TARGET" || err "path not allowed: $TARGET"

# Reject symlinks
if [ -L "$TARGET" ]; then
  err "symlink not allowed: $TARGET"
fi

# Handle parent directories
PARENT="$(dirname "$TARGET")"
if [ ! -d "$PARENT" ]; then
  if [ "$CREATE_PARENTS" = "1" ]; then
    # Create parents with safe default permissions (root:root 0755)
    # Only create directories that are within BASE_DIR
    case "$PARENT" in
      "$BASE_DIR"/*) ;;
      *) err "parent path outside BASE_DIR: $PARENT" ;;
    esac
    mkdir -p "$PARENT"
    # Note: parents are created as root:root 0755 by default.
    # Use explicit fs-ensure calls per directory for custom ownership.
  else
    err "parent does not exist: $PARENT (use -p to create)"
  fi
fi

# Apply
if [ "$TYPE" = "dir" ]; then
# Create the directory, then explicitly enforce owner/group/mode.
#
# We intentionally do not use `install -d -o/-g/-m` because under a setgid
# parent directory the inherited setgid bit may be preserved. An explicit
# chown/chmod guarantees the final state exactly matches the configured policy.
  install -d "$TARGET"
  if [ "$RECURSIVE" = "1" ]; then
    # Ownership recursively (upgrade/self-heal of pre-existing contents); the
    # mode below is applied to the top-level dir only — interior file modes
    # belong to the service that created them.
    chown -R "$OWNER:$GROUP" "$TARGET"
  else
    chown "$OWNER:$GROUP" "$TARGET"
  fi
  chmod "$MODE" "$TARGET"
else
  if [ ! -e "$TARGET" ]; then
    install -o "$OWNER" -g "$GROUP" -m "$MODE" /dev/null "$TARGET"
  else
    chown "$OWNER:$GROUP" "$TARGET"
    chmod "$MODE" "$TARGET"
  fi
fi

# Verify (normalize modes to strip leading zeros before comparing).
# Numeric owners/groups (container-identity classes) are compared numerically:
# mapped subids have no passwd/group entry, so stat %U/%G would print UNKNOWN.
case "$OWNER" in
  *[!0-9]*) A_OWNER="$(stat -c '%U' "$TARGET")" ;;
  *)        A_OWNER="$(stat -c '%u' "$TARGET")" ;;
esac
case "$GROUP" in
  *[!0-9]*) A_GROUP="$(stat -c '%G' "$TARGET")" ;;
  *)        A_GROUP="$(stat -c '%g' "$TARGET")" ;;
esac
A_MODE="$(stat -c '%a' "$TARGET")"

A_MODE_NORM="$(echo "$A_MODE" | sed 's/^0*//')"
MODE_NORM="$(echo "$MODE"   | sed 's/^0*//')"

[ "$A_OWNER"     = "$OWNER"     ] || err "owner mismatch (got $A_OWNER, want $OWNER)"
[ "$A_GROUP"     = "$GROUP"     ] || err "group mismatch (got $A_GROUP, want $GROUP)"
[ "$A_MODE_NORM" = "$MODE_NORM" ] || err "mode mismatch (got $A_MODE, want $MODE)"
