/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>
114 lines
4.4 KiB
Elixir
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
|