280 lines
14 KiB
Bash
Executable File
280 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
|
|
verify="$root/scripts/verify-dwh-auth-docs.sh"
|
|
temp_root=
|
|
|
|
report_pass() {
|
|
printf 'case=%s status=PASS\n' "$1"
|
|
}
|
|
|
|
report_fail() {
|
|
printf 'case=%s status=FAIL\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
cleanup() {
|
|
if [[ "$temp_root" == /tmp/thothii-dwh-auth-docs.* && -d "$temp_root" ]]; then
|
|
rm -rf -- "$temp_root"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
[[ -x "$verify" ]] || report_fail verifier_missing
|
|
|
|
temp_root=$(mktemp -d /tmp/thothii-dwh-auth-docs.XXXXXXXX) || report_fail fixture_root
|
|
fixture_root="$temp_root/fixture"
|
|
mkdir -p "$fixture_root/docs/install" "$fixture_root/docs/operations" \
|
|
"$fixture_root/docs/testing/evidence" "$fixture_root/scripts" "$fixture_root/deploy/psd"
|
|
|
|
for relative in \
|
|
docs/install/dwh-auth-server.md \
|
|
docs/install/dwh-auth-client-enrollment.md \
|
|
docs/install/dwh-auth-tls.md \
|
|
docs/operations/psd-dwh-auth-rollout.md \
|
|
docs/testing/dwh-auth-manual-acceptance.md \
|
|
docs/testing/evidence/psd-dwh-auth-rollout-report-template.md \
|
|
docs/install/local-workspace-registry.md \
|
|
docs/install/server-workspace-registry.md \
|
|
docs/install/psd-workspace-setup.md \
|
|
docs/guida-utente.md \
|
|
docs/index.md \
|
|
mkdocs.yml; do
|
|
mkdir -p "$fixture_root/$(dirname "$relative")"
|
|
cp "$root/$relative" "$fixture_root/$relative"
|
|
done
|
|
cp -a "$root/docs/." "$fixture_root/docs/"
|
|
cp "$root/deploy/psd/workspace-bindings.env.example" "$fixture_root/deploy/psd/workspace-bindings.env.example"
|
|
|
|
"$verify" --root "$fixture_root" || report_fail positive_source
|
|
report_pass positive_source
|
|
|
|
expect_rejected() {
|
|
local name=$1 target=$2 addition=$3
|
|
local case_root="$temp_root/$name"
|
|
cp -a "$fixture_root" "$case_root"
|
|
printf '\n%s\n' "$addition" >>"$case_root/$target"
|
|
if "$verify" --root "$case_root" >/dev/null 2>&1; then
|
|
report_fail "$name"
|
|
fi
|
|
report_pass "$name"
|
|
}
|
|
|
|
exercise_header_file_curl() {
|
|
local v1_header="$temp_root/synthetic-v1.header"
|
|
local legacy_header="$temp_root/synthetic-legacy.header"
|
|
command -v curl >/dev/null 2>&1 || report_fail header_file_curl_command
|
|
printf '%s\n' 'X-API-Key: synthetic-v1' >"$v1_header"
|
|
printf '%s\n' 'X-API-Key: legacy opaque value /:[]' >"$legacy_header"
|
|
chmod 0600 "$v1_header" "$legacy_header"
|
|
if ! python3 - "$v1_header" "$legacy_header" <<'PY'
|
|
import http.server
|
|
import pathlib
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
|
|
accepted = {"synthetic-v1", "legacy opaque value /:[]"}
|
|
|
|
class Handler(http.server.BaseHTTPRequestHandler):
|
|
def log_message(self, _format, *_args):
|
|
pass
|
|
|
|
def do_GET(self):
|
|
self.send_response(204 if self.headers.get("X-API-Key") in accepted else 401)
|
|
self.end_headers()
|
|
|
|
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
for header_file in sys.argv[1:]:
|
|
if stat.S_IMODE(pathlib.Path(header_file).stat().st_mode) != 0o600:
|
|
raise SystemExit(1)
|
|
result = subprocess.run(
|
|
["curl", "--silent", "--show-error", "--output", "/dev/null", "--write-out", "%{http_code}", "--header", f"@{header_file}", f"http://127.0.0.1:{server.server_port}/rpc/ping"],
|
|
check=False,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL,
|
|
text=True,
|
|
)
|
|
if result.returncode != 0 or result.stdout != "204":
|
|
raise SystemExit(1)
|
|
finally:
|
|
server.shutdown()
|
|
thread.join()
|
|
PY
|
|
then
|
|
report_fail header_file_curl_synthetic
|
|
fi
|
|
report_pass header_file_curl_synthetic
|
|
}
|
|
|
|
# Build synthetic only-in-fixture text at runtime: it is never a provisioned credential.
|
|
fake_key="thtdwh_v1.$(printf 'A%.0s' {1..16}).$(printf 'A%.0s' {1..43})"
|
|
fake_digest="$(printf 'A%.0s' {1..43})"
|
|
|
|
expect_rejected credential_literal docs/install/dwh-auth-client-enrollment.md "$fake_key"
|
|
expect_rejected opaque_legacy_header_literal docs/install/dwh-auth-client-enrollment.md "curl --header 'X-API-Key: opaque-legacy-fixture' https://example.invalid/dwh/rpc/ping"
|
|
expect_rejected credential_digest_literal docs/testing/evidence/psd-dwh-auth-rollout-report-template.md "\"secret_sha256\": \"$fake_digest\""
|
|
expect_rejected curl_insecure docs/install/dwh-auth-tls.md 'curl -k https://example.invalid/dwh/rpc/ping'
|
|
expect_rejected tls_disabled docs/install/dwh-auth-tls.md 'verify_tls=false'
|
|
expect_rejected secret_in_environment docs/install/dwh-auth-client-enrollment.md "export THT_WS_PSD_CLINICAL_DWH_API_KEY=$fake_key"
|
|
expect_rejected secret_in_argv docs/install/dwh-auth-client-enrollment.md "curl -H 'X-API-Key: $fake_key' https://example.invalid/dwh/rpc/ping"
|
|
expect_rejected curl_config_artifact docs/install/dwh-auth-server.md 'curl --config /root/dwh-auth-provision/request.conf https://example.invalid/dwh/rpc/ping'
|
|
expect_rejected world_readable_secret docs/install/dwh-auth-server.md 'chmod 0644 /root/dwh-auth-provision/client.key'
|
|
expect_rejected sudo_raw_nginx_capture docs/operations/psd-dwh-auth-rollout.md 'sudo nginx -T > /tmp/nginx-full.conf'
|
|
expect_rejected raw_diff_capture docs/operations/psd-dwh-auth-rollout.md 'sudo diff -u /etc/nginx/sites-available/policlinicosandonato /root/backup.conf'
|
|
expect_rejected git_raw_diff_capture docs/operations/psd-dwh-auth-rollout.md 'git diff --no-index /root/old.conf /root/new.conf'
|
|
expect_rejected raw_nginx_capture docs/operations/psd-dwh-auth-rollout.md 'nginx -T > /tmp/nginx-full.conf'
|
|
expect_rejected compose_coupling docs/install/dwh-auth-server.md 'docker compose up dwh-auth'
|
|
|
|
expect_replacement_rejected() {
|
|
local name=$1 target=$2 needle=$3 replacement=$4
|
|
local case_root="$temp_root/$name"
|
|
cp -a "$fixture_root" "$case_root"
|
|
[[ $(grep -Foc -- "$needle" "$case_root/$target") -eq 1 ]] || report_fail "${name}_fixture"
|
|
sed -i "s~$needle~$replacement~" "$case_root/$target"
|
|
if "$verify" --root "$case_root" >/dev/null 2>&1; then report_fail "$name"; fi
|
|
report_pass "$name"
|
|
}
|
|
|
|
|
|
expect_global_replacement_rejected() {
|
|
local name=$1 target=$2 needle=$3 replacement=$4
|
|
local case_root="$temp_root/$name"
|
|
cp -a "$fixture_root" "$case_root"
|
|
[[ $(grep -Foc -- "$needle" "$case_root/$target") -gt 0 ]] || report_fail "${name}_fixture"
|
|
sed -i "s~$needle~$replacement~g" "$case_root/$target"
|
|
if "$verify" --root "$case_root" >/dev/null 2>&1; then report_fail "$name"; fi
|
|
report_pass "$name"
|
|
}
|
|
|
|
exercise_header_file_curl
|
|
|
|
exercise_journal_scanner() {
|
|
local journal_source="$fixture_root/docs/install/dwh-auth-server.md"
|
|
local journal_code
|
|
journal_code=$(python3 - "$journal_source" <<'PY2'
|
|
import pathlib
|
|
import sys
|
|
|
|
source = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")
|
|
start = source.index("if sudo python3 -c '") + len("if sudo python3 -c '")
|
|
end = source.index("' \"$since\" \"$v1_key_file\" \"$legacy_key_file\"; then", start)
|
|
print(source[start:end])
|
|
PY2
|
|
) || report_fail journal_extract
|
|
[[ "$journal_code" == *"process.stdout.read1("* ]] || report_fail journal_chunk_reader
|
|
[[ "$journal_code" != *"enumerate(process.stdout"* ]] || report_fail journal_unbounded_reader
|
|
|
|
local fake_bin="$temp_root/fake-bin"
|
|
local fake_journalctl="$fake_bin/journalctl"
|
|
local data_file="$temp_root/journal.data"
|
|
local pid_file="$temp_root/journal.pid"
|
|
local stdout_file="$temp_root/journal.stdout"
|
|
local stderr_file="$temp_root/journal.stderr"
|
|
mkdir -p "$fake_bin"
|
|
python3 - "$fake_journalctl" <<'PY2'
|
|
import pathlib
|
|
import sys
|
|
|
|
path = pathlib.Path(sys.argv[1])
|
|
path.write_text("""#!/usr/bin/env python3
|
|
import os
|
|
import pathlib
|
|
import sys
|
|
import time
|
|
|
|
pathlib.Path(os.environ["JOURNAL_PID_FILE"]).write_text(str(os.getpid()))
|
|
data = pathlib.Path(os.environ["JOURNAL_DATA_FILE"]).read_bytes()
|
|
for offset in range(0, len(data), 997):
|
|
os.write(sys.stdout.fileno(), data[offset:offset + 997])
|
|
if os.environ.get("JOURNAL_HOLD") == "1":
|
|
time.sleep(30)
|
|
raise SystemExit(int(os.environ.get("JOURNAL_EXIT", "0")))
|
|
""")
|
|
path.chmod(0o755)
|
|
PY2
|
|
|
|
local key_file="$temp_root/synthetic-real.key"
|
|
local legacy_file="$temp_root/synthetic-legacy.key"
|
|
printf '%s' 'REAL_SECRET_SYNTHETIC' >"$key_file"
|
|
printf '%s' 'LEGACY_SECRET_SYNTHETIC' >"$legacy_file"
|
|
chmod 0600 "$key_file" "$legacy_file"
|
|
|
|
run_journal_case() {
|
|
local name=$1 expected=$2 data=$3 exit_code=$4 hold=$5
|
|
printf '%s' "$data" >"$data_file"
|
|
: >"$pid_file" "$stdout_file" "$stderr_file"
|
|
if PATH="$fake_bin:$PATH" JOURNAL_DATA_FILE="$data_file" JOURNAL_PID_FILE="$pid_file" JOURNAL_EXIT="$exit_code" JOURNAL_HOLD="$hold" \
|
|
python3 -c "$journal_code" synthetic-since "$key_file" "$legacy_file" >"$stdout_file" 2>"$stderr_file"; then
|
|
actual=0
|
|
else
|
|
actual=$?
|
|
fi
|
|
[[ "$actual" -eq "$expected" ]] || report_fail "journal_${name}_status"
|
|
[[ ! -s "$stdout_file" && ! -s "$stderr_file" ]] || report_fail "journal_${name}_output"
|
|
if [[ -s "$pid_file" ]]; then
|
|
pid=$(cat "$pid_file")
|
|
[[ ! -e "/proc/$pid" ]] || report_fail "journal_${name}_reaped"
|
|
fi
|
|
report_pass "journal_${name}"
|
|
}
|
|
|
|
run_journal_case clean 0 $'INFO clean\n' 0 0
|
|
run_journal_case oversized_line 2 "$(printf 'A%.0s' {1..1048577})" 0 0
|
|
run_journal_case too_many_lines 2 "$(printf 'x\n%.0s' {1..10001})" 0 0
|
|
run_journal_case child_failure 2 $'INFO child failure\n' 7 0
|
|
run_journal_case actual_key_across_chunk 1 "$(printf 'A%.0s' {1..65530})REAL_SECRET_SYNTHETIC" 0 1
|
|
}
|
|
|
|
exercise_journal_scanner
|
|
|
|
expect_replacement_rejected missing_exact_gui_label docs/install/dwh-auth-client-enrollment.md "Validate workspace source" "Validate workspace"
|
|
expect_replacement_rejected server_transport_contradiction docs/operations/psd-dwh-auth-rollout.md 'server PSD: `postgres_direct` read-only' 'server PSD: `rest_api` read-only'
|
|
expect_replacement_rejected missing_mac_local_marker deploy/psd/workspace-bindings.env.example "Mac/local/remota" "server PSD"
|
|
expect_replacement_rejected missing_private_ca docs/install/psd-workspace-setup.md "TLS_CA_FILE" "TLS_CA_REMOVED"
|
|
expect_global_replacement_rejected missing_socket_path docs/install/dwh-auth-server.md "/run/dwh-auth/verify.sock" "/run/dwh-auth/other.sock"
|
|
expect_replacement_rejected rest_transport_flag deploy/psd/workspace-bindings.env.example "THT_WS_PSD_CLINICAL_DWH_TRANSPORT=rest_api" "THT_WS_PSD_CLINICAL_DWH_TRANSPORT=postgres_direct"
|
|
|
|
expect_replacement_rejected wrong_key_output_path docs/install/dwh-auth-server.md "key_output=/root/dwh-auth-provision/psd-mac-primary.key" "key_output=/root/dwh-auth-provision/psd-mac-primary-primary.key"
|
|
expect_replacement_rejected wrong_legacy_key_id docs/install/dwh-auth-server.md "legacy_key_id=legacy-shared" "legacy_key_id=legacy-public-key-id"
|
|
expect_global_replacement_rejected missing_header_file_transport docs/install/dwh-auth-server.md '--header "@' '--header "X-API-Key: '
|
|
expect_global_replacement_rejected missing_legacy_header_file docs/install/dwh-auth-server.md "legacy_header_file" "retired_auth_header"
|
|
expect_global_replacement_rejected missing_safe_restore docs/install/dwh-auth-server.md "registry_staging" "registry_overlay"
|
|
expect_replacement_rejected unsafe_restore_overlay docs/install/dwh-auth-server.md "sudo tar --acls --xattrs -C \"\$registry_staging\" -xf \"\$registry_backup\"" "sudo tar --acls --xattrs -C /var/lib -xf \"\$registry_backup\""
|
|
expect_replacement_rejected missing_guarded_registry_publish docs/install/dwh-auth-server.md "if ! sudo mv -T -- \"\$registry_root\" \"\$registry_previous\"; then" "sudo mv -T -- \"\$registry_root\" \"\$registry_previous\""
|
|
expect_replacement_rejected missing_candidate_check_guard docs/install/dwh-auth-server.md "if ! sudo /usr/local/sbin/dwh-auth --registry-root \"\$registry_candidate\" check; then" "sudo /usr/local/sbin/dwh-auth --registry-root \"\$registry_candidate\" check"
|
|
expect_replacement_rejected unsafe_registry_publish_target docs/install/dwh-auth-server.md "if ! sudo mv -T -- \"\$registry_root\" \"\$registry_previous\"; then" "if ! sudo mv \"\$registry_root\" \"\$registry_previous\"; then"
|
|
expect_replacement_rejected missing_key_file_byte_guard docs/install/dwh-auth-server.md "key_file_bytes=PASS" "key_file_bytes=SKIPPED"
|
|
expect_replacement_rejected missing_failed_registry_publish_target docs/install/dwh-auth-server.md "if ! sudo mv -T -- \"\$registry_root\" \"\$registry_staging/failed-dwh-auth\"; then" "if ! sudo mv \"\$registry_root\" \"\$registry_staging/failed-dwh-auth\"; then"
|
|
expect_global_replacement_rejected missing_rollback_registry_publish_target docs/install/dwh-auth-server.md "if ! sudo mv -T -- \"\$registry_previous\" \"\$registry_root\"; then" "if ! sudo mv \"\$registry_previous\" \"\$registry_root\"; then"
|
|
expect_global_replacement_rejected missing_exact_header_bytes docs/install/dwh-auth-server.md 'dd if="$1" bs=65536 status=none' 'dd if="$1" bs=1'
|
|
|
|
|
|
expect_replacement_rejected missing_actual_key_journal_scan docs/install/dwh-auth-server.md "journal_actual_key_scan=PASS" "journal_generic_scan=PASS"
|
|
expect_replacement_rejected missing_journal_actual_key_match docs/install/dwh-auth-server.md "any(needle in searchable for needle in needles)" "False"
|
|
expect_replacement_rejected missing_journal_returncode_guard docs/install/dwh-auth-server.md "if process.returncode != 0:" "if process.returncode == 0:"
|
|
journal_contract_source="$fixture_root/docs/install/dwh-auth-server.md"
|
|
if grep -Fq 'result.stdout.splitlines' "$journal_contract_source" \
|
|
|| grep -Fq 'enumerate(process.stdout' "$journal_contract_source" \
|
|
|| ! grep -Fq 'max_journal_bytes = 1_048_576' "$journal_contract_source" \
|
|
|| ! grep -Fq 'max_journal_lines = 10_000' "$journal_contract_source" \
|
|
|| ! grep -Fq 'max_chunk_bytes = 65_536' "$journal_contract_source" \
|
|
|| ! grep -Fq 'process.stdout.read1(' "$journal_contract_source" \
|
|
|| ! grep -Fq 'remaining = max_journal_bytes - bytes_seen' "$journal_contract_source" \
|
|
|| ! grep -Fq 'searchable = carry + chunk' "$journal_contract_source" \
|
|
|| ! grep -Fq 'process.kill()' "$journal_contract_source"; then
|
|
report_fail journal_streaming_limits
|
|
fi
|
|
report_pass journal_streaming_limits
|
|
expect_replacement_rejected missing_socket_legacy_probe docs/install/dwh-auth-server.md "socket_legacy=PASS" "socket_legacy=SKIPPED"
|
|
expect_replacement_rejected missing_https_post_revoke_v1 docs/install/dwh-auth-server.md "https_v1_post_revoke=PASS" "https_v1_post_revoke=SKIPPED"
|
|
expect_replacement_rejected missing_https_post_revoke_legacy docs/install/dwh-auth-server.md "https_legacy_post_revoke=PASS" "https_legacy_post_revoke=SKIPPED"
|
|
expect_global_replacement_rejected rollout_post_revoke_status docs/operations/psd-dwh-auth-rollout.md "v1=2xx post-revoca" "v1=204 post-revoca"
|
|
|
|
report_pass summary
|