#!/usr/bin/env bash set -uo pipefail # CASAN Plan-16 SEC-23 (MT-01) — tenant-partitioned state store + cross-tenant guard. # # Harness state (audit chains, control-plane settings, telemetry, logs, kill-switch) # used to live in SHARED global files, so a run for tenant A could read/modify # tenant B's state directly — bypassing RBAC (which only guarded the API, not the # files). This resolves every state path under a per-tenant root and refuses any # access that escapes the caller's own tenant partition (reusing the SEC-28 realpath # guard). Deny-by-default; secure-by-default in prod (missing tenant = fail-closed). # # Tenant id: CASAN_TENANT_ID (allowlist [A-Za-z0-9_-]+, no path traversal). Unset → # 'default' in dev, but REFUSED under CASAN_PROFILE=prod. # # Usage: # tenant-store.sh id # print resolved tenant id # tenant-store.sh root # print this tenant's state root (0700) # tenant-store.sh init # create tenant root with 0700 perms # tenant-store.sh resolve # print tenant-scoped path for a state file # tenant-store.sh guard # exit 0 if path is inside own tenant root, else DENY # Env: CASAN_TENANT_STATE_ROOT (default .specify/state/tenants) # Exit: 0 ok · 3 denied (reason on stderr) · 64 usage. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/casan-paths.sh" PROJECT_ROOT="$CASAN_APP_ROOT" STATE_ROOT="${CASAN_TENANT_STATE_ROOT:-$CASAN_STATE_ROOT/state/tenants}" die() { echo "TENANT_DENIED reason=$1" >&2; exit 3; } resolve_tenant_id() { local t="${CASAN_TENANT_ID:-}" if [[ -z "$t" ]]; then [[ "${CASAN_PROFILE:-}" == "prod" ]] && die "tenant_id_required_in_prod" t="default" fi [[ "$t" =~ ^[A-Za-z0-9_-]+$ ]] || die "tenant_id_invalid(${t})" printf '%s' "$t" } tenant_root() { local t; t="$(resolve_tenant_id)" || exit 3 printf '%s/%s' "$STATE_ROOT" "$t" } ensure_root() { local root="$1" mkdir -p "$root" || die "mkdir_failed(${root})" chmod 700 "$STATE_ROOT" 2>/dev/null || true chmod 700 "$root" 2>/dev/null || true } CMD="${1:-}"; shift || true case "$CMD" in id) resolve_tenant_id; echo ;; root) tenant_root; echo ;; init) root="$(tenant_root)" || exit 3 ensure_root "$root" echo "$root" ;; resolve) name="${1:-}"; [[ -n "$name" ]] || die "logical_name_required" case "$name" in /*|*..*) die "logical_name_invalid(${name})" ;; esac root="$(tenant_root)" || exit 3 ensure_root "$root" mkdir -p "$(dirname "$root/$name")" 2>/dev/null || true printf '%s/%s\n' "$root" "$name" ;; guard) target="${1:-}"; [[ -n "$target" ]] || die "target_required" root="$(tenant_root)" || exit 3 ensure_root "$root" if bash "$SCRIPT_DIR/path-guard.sh" "$target" "$root" >/dev/null 2>&1; then echo "TENANT_OK path within own tenant root" exit 0 fi die "cross_tenant_access(target=${target})" ;; *) echo "Usage: tenant-store.sh {id|root|init|resolve |guard }" >&2 exit 64 ;; esac