#!/usr/bin/env bash set -uo pipefail # CASAN Plan-16 SEC-23 Phase 1 (MT-01) — tenant-partitioned state store + guard. # # Proves: # * 23.1 two tenants resolve to DIFFERENT partitioned paths; an invalid/traversal # tenant id or logical name is rejected, # * 23.2 cross-tenant guard: tenant A cannot access tenant B's path (DENY), but its # own path is allowed, # * 23.3 the tenant root is created 0700 (owner-only), # * 23.12 secure-by-default: prod with no CASAN_TENANT_ID fails CLOSED; dev falls # back to a 'default' tenant; prod WITH a tenant is fine. # # Deterministic; hermetic (temp tenant-state root); no model/network. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh" PROJECT_ROOT="$CASAN_APP_ROOT" TS="$CASAN_HARNESS_ROOT/scripts/bash/tenant-store.sh" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT export CASAN_TENANT_STATE_ROOT="$WORK/tenants" PASS=0; FAIL=0 pass() { echo "PASS: $1"; PASS=$((PASS + 1)); } fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); } rc_of() { set +e; "$@" >/dev/null 2>&1; echo $?; set -e 2>/dev/null || true; } echo "===== Plan-16 SEC-23 Phase 1: tenant-store + cross-tenant guard =====" # --- 23.1 partitioned paths + id/name validation --- pA="$(CASAN_TENANT_ID=alpha bash "$TS" resolve audit/chain.jsonl 2>/dev/null)" pB="$(CASAN_TENANT_ID=beta bash "$TS" resolve audit/chain.jsonl 2>/dev/null)" { [[ "$pA" != "$pB" && "$pA" == *"/alpha/audit/chain.jsonl" && "$pB" == *"/beta/audit/chain.jsonl" ]]; } \ && pass "23.1 two tenants resolve to different partitioned paths" \ || fail "23.1 tenant partitioning wrong (A=$pA B=$pB)" [[ "$(rc_of env CASAN_TENANT_ID=../evil bash "$TS" resolve x)" -eq 3 ]] \ && pass "23.1 traversal tenant id rejected" || fail "23.1 traversal id accepted" [[ "$(rc_of env CASAN_TENANT_ID='a b' bash "$TS" resolve x)" -eq 3 ]] \ && pass "23.1 invalid-charset tenant id rejected" || fail "23.1 bad-charset id accepted" [[ "$(rc_of env CASAN_TENANT_ID=alpha bash "$TS" resolve ../../escape)" -eq 3 ]] \ && pass "23.1 traversal logical-name rejected" || fail "23.1 traversal name accepted" # --- 23.3 tenant root 0700 --- CASAN_TENANT_ID=alpha bash "$TS" init >/dev/null 2>&1 mode="$(stat -c '%a' "$WORK/tenants/alpha" 2>/dev/null || stat -f '%Lp' "$WORK/tenants/alpha" 2>/dev/null)" [[ "$mode" == "700" ]] && pass "23.3 tenant root created 0700 (owner-only)" || fail "23.3 tenant root mode=$mode (want 700)" # --- 23.2 cross-tenant guard --- betaPath="$(CASAN_TENANT_ID=beta bash "$TS" resolve audit/chain.jsonl 2>/dev/null)" [[ "$(rc_of env CASAN_TENANT_ID=alpha bash "$TS" guard "$betaPath")" -eq 3 ]] \ && pass "23.2 tenant A denied access to tenant B's path" || fail "23.2 cross-tenant access allowed" alphaPath="$(CASAN_TENANT_ID=alpha bash "$TS" resolve audit/chain.jsonl 2>/dev/null)" [[ "$(rc_of env CASAN_TENANT_ID=alpha bash "$TS" guard "$alphaPath")" -eq 0 ]] \ && pass "23.2 tenant A allowed access to its own path" || fail "23.2 own path wrongly denied" # --- 23.12 secure-by-default --- [[ "$(rc_of env CASAN_PROFILE=prod bash "$TS" id)" -eq 3 ]] \ && pass "23.12 prod without CASAN_TENANT_ID fails CLOSED" || fail "23.12 prod ran without tenant" out="$(bash "$TS" id 2>/dev/null)" [[ "$out" == "default" ]] && pass "23.12 dev falls back to 'default' tenant" || fail "23.12 dev id=$out" [[ "$(rc_of env CASAN_PROFILE=prod CASAN_TENANT_ID=alpha bash "$TS" id)" -eq 0 ]] \ && pass "23.12 prod WITH tenant is accepted" || fail "23.12 prod with tenant wrongly refused" echo "" echo "===== SEC-23 Phase 1 SUMMARY: PASS=$PASS FAIL=$FAIL =====" [[ "$FAIL" -eq 0 ]] || exit 1