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>
This commit is contained in:
1 parent
996a8efe68
commit
79eb34840f
17 files changed
+372
-34
No files matched your search
@@ -342,7 +342,7 @@ defmodule VavPipeline.BedikahTest do
|
||||
|
||||
defp post_bedikah(body, opts \\ []) do
|
||||
conn = conn(:post, "/courtyard/bedikah", Jason.encode!(body)) |> put_req_header("content-type", "application/json")
|
||||
conn = if Keyword.get(opts, :auth, true), do: put_req_header(conn, "authorization", "Bearer test"), else: conn
|
||||
conn = if Keyword.get(opts, :auth, true), do: put_req_header(conn, "authorization", "Bearer test-token"), else: conn
|
||||
Router.call(conn, @opts)
|
||||
end
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ defmodule VavPipeline.BigdeiKodeshTest do
|
||||
conn =
|
||||
conn(:post, "/chatzer/sanhedrin", Jason.encode!(%{not_case: "wrong field"}))
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> put_req_header("authorization", "Bearer test-token")
|
||||
|> put_req_header("authorization", "Bearer chatzer-test-token")
|
||||
|> Router.call(@opts)
|
||||
|
||||
assert conn.status == 422
|
||||
@@ -306,7 +306,7 @@ defmodule VavPipeline.BigdeiKodeshTest do
|
||||
case: "Is cooking on Shabbat permitted for pikuach nefesh?"
|
||||
}))
|
||||
|> put_req_header("content-type", "application/json")
|
||||
|> put_req_header("authorization", "Bearer test-token")
|
||||
|> put_req_header("authorization", "Bearer chatzer-test-token")
|
||||
|> Router.call(@opts)
|
||||
|
||||
assert conn.status == 200
|
||||
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
# Test keys for the gate (BigdeiKodesh.Gate). tier, whitespace, key.
|
||||
courtyard test-token
|
||||
chatzer chatzer-test-token
|
||||
|
||||
# A malformed line, ignored:
|
||||
kodesh not-a-tier
|
||||
@@ -0,0 +1 @@
|
||||
{"consumed":{"api_calls_external":0,"compute_units":3,"memory_mb":0,"sanhedrin_panels_max":0,"storage_delta_mb":0},"cycle_date":"2026-10-06","written_at":"2026-10-06T15:15:24.042079Z"}
|
||||
@@ -0,0 +1,113 @@
|
||||
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
|
||||
Reference in new issue
Block a user