167 lines
7.1 KiB
Bash
Executable File
167 lines
7.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Hermetic acceptance gate for the server authentication runtime projection.
|
|
set -euo pipefail
|
|
umask 077
|
|
|
|
root="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
forbidden_server_root="/$(printf '%s' srv)/"
|
|
tmp_base="$(cd "${TMPDIR:-/tmp}" && pwd -P)"
|
|
if [[ "$tmp_base/" == "$forbidden_server_root"* ]]; then
|
|
echo "runtime projection gate refuses a server temp root" >&2
|
|
exit 1
|
|
fi
|
|
if [[ -n "${DOCKER_HOST:-}" ]]; then
|
|
echo "runtime projection gate refuses a remote Docker endpoint" >&2
|
|
exit 1
|
|
fi
|
|
docker_context="$(docker context show)"
|
|
docker_endpoint="$(docker context inspect "$docker_context" --format '{{(index .Endpoints "docker").Host}}')"
|
|
if [[ "$docker_endpoint" != unix:///* ]]; then
|
|
echo "runtime projection gate requires a local Unix Docker endpoint" >&2
|
|
exit 1
|
|
fi
|
|
tmp="$(mktemp -d "$tmp_base/thoth-auth-runtime-projection.XXXXXX")"
|
|
chmod 0700 "$tmp"
|
|
mkdir -m 0700 "$tmp/go-mod" "$tmp/go-build"
|
|
|
|
cleanup() {
|
|
local cleanup_status=0
|
|
if [[ -d "$tmp" ]]; then
|
|
docker run --rm --network none -v "$tmp:/cleanup" golang:1.26.5 \
|
|
sh -c 'find /cleanup -mindepth 1 -delete' >/dev/null 2>&1 || cleanup_status=$?
|
|
if ((cleanup_status == 0)); then
|
|
rmdir "$tmp" || cleanup_status=$?
|
|
fi
|
|
fi
|
|
return "$cleanup_status"
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'exit 129' HUP
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
|
|
project_name_flag="--project"-name
|
|
compose_project_var='COMPOSE_PROJECT'_NAME
|
|
network_flag="--net"work
|
|
compose_pattern='docker[[:space:]]+com''pose'
|
|
if rg -n -F -- "$forbidden_server_root" "$0"; then
|
|
echo "runtime projection gate must not reference a server path" >&2
|
|
exit 1
|
|
fi
|
|
if rg -n -- "$project_name_flag|$compose_project_var|${compose_pattern}[^[:cntrl:]]*(up|start)|docker[[:space:]]+network" "$0"; then
|
|
echo "runtime projection gate has a forbidden project, lifecycle, or network attachment" >&2
|
|
exit 1
|
|
fi
|
|
if rg -n -F -- "$network_flag" "$0" | rg -v -F -e "$network_flag none" -e "$network_flag=none"; then
|
|
echo "runtime projection gate permits only an explicitly isolated network option" >&2
|
|
exit 1
|
|
fi
|
|
if rg -n --pcre2 '^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*(?:PASSWORD|SECRET|TOKEN|KEY)[A-Za-z_]*=' "$0"; then
|
|
echo "runtime projection gate declares a credential value" >&2
|
|
exit 1
|
|
fi
|
|
|
|
node24_source="$(
|
|
env -i PATH="$PATH" HOME="$HOME" npm_config_offline=true npm_config_userconfig=/dev/null \
|
|
npx -y -p node@24 node -p 'process.execPath'
|
|
)"
|
|
if [[ "$node24_source" != /* || ! -f "$node24_source" || ! -x "$node24_source" ]]; then
|
|
echo "runtime projection gate requires a cached Node 24 runtime" >&2
|
|
exit 1
|
|
fi
|
|
install -m 0755 "$node24_source" "$tmp/node24"
|
|
|
|
run_case() {
|
|
local name="$1"
|
|
shift
|
|
local output="$tmp/$name.log"
|
|
if ! "$@" >"$output" 2>&1; then
|
|
echo "$name FAIL (test output suppressed and removed by cleanup)" >&2
|
|
return 1
|
|
fi
|
|
printf '%s PASS\n' "$name"
|
|
}
|
|
|
|
go_test() {
|
|
docker run --rm \
|
|
-v "$root:/work:ro" \
|
|
-v "$tmp/go-mod:/go/pkg/mod" \
|
|
-v "$tmp/go-build:/root/.cache/go-build" \
|
|
-w /work/tools/tht golang:1.26.5 go test "$@"
|
|
}
|
|
|
|
go_windows_compile() {
|
|
docker run --rm \
|
|
-v "$root:/work:ro" \
|
|
-v "$tmp/go-mod:/go/pkg/mod" \
|
|
-v "$tmp/go-build:/root/.cache/go-build" \
|
|
-w /work/tools/tht -e GOOS=windows -e GOARCH=amd64 golang:1.26.5 \
|
|
go test -c ./cmd/tht -o /tmp/tht.test.exe
|
|
}
|
|
|
|
go_darwin_compile() {
|
|
docker run --rm \
|
|
-v "$root:/work:ro" \
|
|
-v "$tmp/go-mod:/go/pkg/mod" \
|
|
-v "$tmp/go-build:/root/.cache/go-build" \
|
|
-w /work/tools/tht -e GOOS=darwin -e GOARCH=amd64 golang:1.26.5 \
|
|
go test -c ./cmd/tht -o /tmp/tht.test
|
|
}
|
|
|
|
backend_node24() {
|
|
docker run --rm --network none \
|
|
-e HOME=/tmp \
|
|
-v "$root:/work:ro" \
|
|
-v "$tmp/node24:/opt/node24:ro" \
|
|
-v "$root/backend/vitest.config.ts:/opt/vitest.config.ts:ro" \
|
|
-v "$root/backend/node_modules:/opt/node_modules:ro" \
|
|
-w /work/backend golang:1.26.5 \
|
|
/opt/node24 node_modules/vitest/vitest.mjs run --no-cache \
|
|
--config /opt/vitest.config.ts "$@"
|
|
}
|
|
|
|
backend_in_flight() {
|
|
backend_node24 test/auth-runtime-projection.test.ts \
|
|
-t 'in-flight snapshot authenticates A after selection B and deletion A'
|
|
}
|
|
|
|
backend_direct_file() {
|
|
backend_node24 test/config.test.ts \
|
|
-t 'keeps the direct auth-file provider when the runtime projection environment is absent'
|
|
}
|
|
|
|
mac_windows_direct_file() {
|
|
go_windows_compile
|
|
go_darwin_compile
|
|
backend_direct_file
|
|
}
|
|
|
|
run_case descriptor_requires_server_uid_gid_and_matching_env \
|
|
go_test ./internal/config -run 'TestLoadAcceptsServerRuntimeProjectionAndPlacesAutomaticOverrideBeforeCurrentImage|TestLoadRejectsInvalidRuntimeProjection|TestLoadRejectsRuntimeProjectionEnvironmentWithoutDescriptor' -count=1
|
|
run_case compose_mount_is_core_only_read_only_and_noncanonical \
|
|
bash "$root/scripts/test-auth-runtime-projection-compose.sh"
|
|
run_case local_initial_configure_publishes_equal_ready \
|
|
go_test ./internal/setup ./internal/authconfig -run 'TestRunConfigureOnlyPublishesInitialProjectedServerAuthentication|TestRunProjectedMutationPublishesExactLocalAndOIDCSnapshots/local' -count=1
|
|
run_case oidc_initial_configure_publishes_equal_ready \
|
|
go_test ./internal/authconfig -run 'TestRunProjectedMutationPublishesExactLocalAndOIDCSnapshots/oidc' -count=1
|
|
run_case every_user_mutation_blocks_then_publishes \
|
|
go_test ./internal/authconfig -run 'TestProjectedAuthMutatorsBlockBeforeCanonicalWriteAndPublishOnlyEqualSnapshots' -count=1
|
|
run_case publish_repairs_blocked_from_canonical \
|
|
go_test ./internal/authconfig -run 'TestPublishProjectedCanonicalRepairsBlockedStateFromCanonicalOnly|TestProjectedAuthPublishStatusAndCheckFailClosed' -count=1
|
|
run_case start_and_doctor_fail_closed_for_missing_blocked_tampered_or_divergent \
|
|
go_test ./internal/authprojection ./internal/authconfig ./internal/service ./internal/doctor ./cmd/tht -run 'TestInspectRejectsMissingBlockedMalformedAndTamperedCurrent|TestRequireRuntimeAuthProjectionReadyRejectsMissingBlockedAndDivergentStates|TestStartRefusesProjectedAuthenticationBeforeComposeWhenNotReady|TestRunReportsOneSanitizedRuntimeAuthProjectionFailureBeforeCompose|TestRunUpdateCheckOnlyRefusesProjectedAuthenticationBeforeCompose' -count=1
|
|
run_case backend_authenticates_from_one_immutable_generation_after_previous_gc backend_in_flight
|
|
run_case auth_restore_publishes_candidate \
|
|
go_test ./internal/backup -run 'TestRestoreAuthBearingArchiveBlocksBeforeWritePublishesBeforeRestart' -count=1
|
|
run_case failed_candidate_verification_republishes_checkpoint \
|
|
go_test ./internal/backup -run 'TestRestoreAuthVerificationFailuresRepublishCheckpointBeforeRecoveryRestart' -count=1
|
|
run_case failed_recovery_publish_remains_blocked \
|
|
go_test ./internal/backup -run 'TestRestoreAuthCandidatePublicationFailureRecoversThenStaysBlockedWithoutRestart' -count=1
|
|
run_case non_auth_restore_never_touches_projection \
|
|
go_test ./internal/backup -run 'TestRestoreNonAuthArchiveNeverBeginsProjection' -count=1
|
|
run_case mac_windows_local_regression mac_windows_direct_file
|
|
run_case secret_redaction \
|
|
go_test ./internal/authconfig -run 'TestProjectionCoordinatorErrorsAndLogsNeverContainSyntheticPasswordsOrHashes' -count=1
|
|
|
|
echo "runtime authentication projection acceptance gate passed."
|