Files
bikoret/vav_pipeline/test/tablet45_gate_test.exs
T
Scott KarpandClaude Fable 5.1 79eb34840f Tablet 45: the courtyard gate checks the key, not the shape of the header
/courtyard/* and /chatzer/* admitted any `Authorization: Bearer <x>`.
Every test sent `Bearer test-token`; none sent a wrong one. Every
admitted request spends the day's manna.

VavPipeline.BigdeiKodesh.Gate: keys are a file (Construction §4.6 —
never env, never config, never git), one `tier key` per line, tiers
courtyard < chatzer (the inner ring opens the outer). Constant-time
compare against every line; no file / empty / unreadable → nobody
passes, GATE_NO_KEYS in the log, the caller told only "missing or
invalid". 403 FORBIDDEN for an outer key at an inner gate. Read on
every request so a rotated key is live in ~1 min without a rollout.

Deploy: Secret shorashim-api-keys projected at
/etc/shorashim/secrets/api_keys (vav-pipeline only, 0440),
API_KEYS_FILE in the configmap, template + issue/rotate recipe in
secrets.yaml and KOHEN.md "Issuing a key". Spec: tamid §1.1 (gate,
403), vayakhel §4.6. Doorpost 91d09d8d… → 42b38626….
Tests: tablet45_gate_test (parse, tiers, wrong/near/empty/no key, no
file) — Elixir 527/0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 15:16:33 +00:00

114 lines
4.4 KiB
Elixir

defmodule VavPipeline.Tablet45GateTest do
@moduledoc """
Tablet 45 (2026-10-06) — the courtyard gate opened for any hand.
`/courtyard/*` and `/chatzer/*` checked that the Authorization header
had the SHAPE `Bearer <something>`, and nothing else. Every test of it
sent `Bearer test-token` and passed; none sent `Bearer wrong`.
These tests send the wrong key, no key, a key for the outer ring at
the inner gate, and a key when there is no keys file at all.
"""
use ExUnit.Case, async: false
import Plug.Test
import Plug.Conn
alias VavPipeline.BigdeiKodesh.{Gate, Router}
@opts Router.init([])
describe "Gate.parse/1" do
test "reads tier and key, skips comments, blanks and unknown tiers" do
assert Gate.parse("""
# keys
courtyard abc
chatzer\tdef # trailing words are ignored
kodesh ghi
courtyard
""") == [courtyard: "abc", chatzer: "def"]
end
end
describe "Gate.admit/2" do
test "the right key for the ring" do
assert {:ok, :courtyard} = Gate.admit(with_key("test-token"), :courtyard)
assert {:ok, :chatzer} = Gate.admit(with_key("chatzer-test-token"), :chatzer)
end
test "the inner ring's key opens the outer gate; not the reverse" do
assert {:ok, :chatzer} = Gate.admit(with_key("chatzer-test-token"), :courtyard)
assert {:error, :tier_too_low} = Gate.admit(with_key("test-token"), :chatzer)
end
test "a wrong key, a near key, an empty key, no header" do
assert {:error, :unknown_key} = Gate.admit(with_key("wrong"), :courtyard)
assert {:error, :unknown_key} = Gate.admit(with_key("test-token2"), :courtyard)
assert {:error, :unknown_key} = Gate.admit(with_key("test-toke"), :courtyard)
assert {:error, :no_header} = Gate.admit(with_key(""), :courtyard)
assert {:error, :no_header} = Gate.admit(conn(:post, "/courtyard/offering"), :courtyard)
basic = conn(:post, "/courtyard/offering") |> put_req_header("authorization", "Basic dGVzdA==")
assert {:error, :no_header} = Gate.admit(basic, :courtyard)
end
test "no keys file, an empty one, a missing one — nobody passes" do
original = Application.get_env(:vav_pipeline, :api_keys_file)
on_exit(fn -> Application.put_env(:vav_pipeline, :api_keys_file, original) end)
Application.put_env(:vav_pipeline, :api_keys_file, nil)
assert {:error, :unknown_key} = Gate.admit(with_key("test-token"), :courtyard)
empty = Path.join(System.tmp_dir!(), "tablet45_empty_#{System.unique_integer([:positive])}")
File.write!(empty, "# nothing here\n\n")
Application.put_env(:vav_pipeline, :api_keys_file, empty)
assert {:error, :unknown_key} = Gate.admit(with_key("test-token"), :courtyard)
File.rm!(empty)
Application.put_env(:vav_pipeline, :api_keys_file, "/nonexistent/api_keys")
assert {:error, :unknown_key} = Gate.admit(with_key("test-token"), :courtyard)
end
end
describe "at the router" do
test "a wrong key is 401 at every gated door, and says nothing about the keys" do
for {path, body} <- [
{"/courtyard/offering", %{offering_type: "shelamim", content: "x"}},
{"/courtyard/bedikah", %{output: "x", model: "gpt"}},
{"/chatzer/sanhedrin", %{case: "x"}}
] do
conn = post(path, body, "Bearer wrong")
assert conn.status == 401, path
decoded = Jason.decode!(conn.resp_body)
assert decoded["error"]["code"] == "UNAUTHORIZED"
refute conn.resp_body =~ "test-token"
end
end
test "a courtyard key at the chatzer is 403, not 401" do
conn = post("/chatzer/sanhedrin", %{case: "x"}, "Bearer test-token")
assert conn.status == 403
assert Jason.decode!(conn.resp_body)["error"]["code"] == "FORBIDDEN"
end
test "a chatzer key is good in the courtyard" do
conn = post("/courtyard/offering", %{offering_type: "shelamim"}, "Bearer chatzer-test-token")
# Past the gate: the next check (no content) answers, not the gate.
assert conn.status == 422
end
end
defp with_key(key) do
conn(:post, "/courtyard/offering") |> put_req_header("authorization", "Bearer " <> key)
end
defp post(path, body, auth) do
conn(:post, path, Jason.encode!(body))
|> put_req_header("content-type", "application/json")
|> put_req_header("authorization", auth)
|> Router.call(@opts)
end
end