-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathphase_c_seam_test.exs
More file actions
165 lines (135 loc) · 5.7 KB
/
Copy pathphase_c_seam_test.exs
File metadata and controls
165 lines (135 loc) · 5.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# SPDX-License-Identifier: PMPL-1.0-or-later
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# Phase C seam tests — BoJ-side enforcement of the http-capability-gateway
# contract §3 (trust-header security invariant).
#
# Companion to http-capability-gateway#11 (gateway-side X-Trust-Level
# strip + re-emit) — together they form the §3 defence-in-depth pair:
#
# gateway-side: strips client-supplied X-Trust-Level, re-emits from
# the gateway's resolved trust decision (mTLS-derived
# from Phase B onward)
# BoJ-side: ignores X-Trust-Level from any non-loopback caller
# (enforced by BojRest.TrustPolicy.satisfies?/3)
#
# All 9 tests here are live. The 4 non-loopback negative tests
# previously documented an unfixed defect (with `@tag skip:`); the
# defect was fixed by adding a `satisfies?(_, _, false), do: false`
# clause and those tests are now enforced.
#
# Refs hyperpolymath/standards#98 (Phase C).
# Refs hyperpolymath/standards#91 (HCG tier-2 channel).
defmodule BojRest.PhaseCSeamTest do
use ExUnit.Case, async: true
import Plug.Test
import Plug.Conn
@opts BojRest.Router.init([])
@keyed_cart "airtable-mcp"
@public_cart "boj-health"
# ── §3 invariant 3 (positive control) — loopback HONOURS X-Trust-Level ────
describe "loopback callers (gateway-equivalent path)" do
test "X-Trust-Level: internal from 127.0.0.1 → allowed on :authenticated cartridge" do
conn = invoke(@keyed_cart, "airtable_list_bases",
remote_ip: {127, 0, 0, 1},
trust_level: "internal"
)
assert conn.status in [200, 500]
refute body(conn)["error"] == "forbidden"
end
test "X-Trust-Level: internal from ::1 → allowed on :authenticated cartridge" do
conn = invoke(@keyed_cart, "airtable_list_bases",
remote_ip: {0, 0, 0, 0, 0, 0, 0, 1},
trust_level: "internal"
)
assert conn.status in [200, 500]
refute body(conn)["error"] == "forbidden"
end
end
# ── Non-loopback to :public cartridge — header is irrelevant, still passes ─
describe ":public cartridge — non-loopback caller (header irrelevant)" do
test "X-Trust-Level: internal from non-loopback → :public cartridge still allowed" do
conn = invoke(@public_cart, "boj_health_status",
remote_ip: {1, 2, 3, 4},
trust_level: "internal"
)
assert conn.status in [200, 500]
refute body(conn)["error"] == "forbidden"
end
end
# ── §3 invariant 3 — non-loopback X-Trust-Level MUST be IGNORED ───────────
describe "non-loopback callers (header ignored per §3)" do
test "X-Trust-Level: internal from 1.2.3.4 → 403 on :authenticated cartridge" do
conn = invoke(@keyed_cart, "airtable_list_bases",
remote_ip: {1, 2, 3, 4},
trust_level: "internal"
)
assert conn.status == 403
assert body(conn)["error"] == "forbidden"
assert body(conn)["detail"] == "insufficient-trust"
assert body(conn)["required"] == "authenticated"
end
test "X-Trust-Level: authenticated from 1.2.3.4 → 403 on :authenticated cartridge" do
conn = invoke(@keyed_cart, "airtable_list_bases",
remote_ip: {1, 2, 3, 4},
trust_level: "authenticated"
)
assert conn.status == 403
assert body(conn)["error"] == "forbidden"
end
test "X-Trust-Level: internal from IPv6 non-loopback → 403" do
# IPv6 documentation prefix 2001:db8::/32 — never routable, never loopback.
conn = invoke(@keyed_cart, "airtable_list_bases",
remote_ip: {0x2001, 0xdb8, 0, 0, 0, 0, 0, 1},
trust_level: "internal"
)
assert conn.status == 403
end
test "SSE: X-Trust-Level: internal from 1.2.3.4 → 403 on :authenticated cartridge" do
conn = invoke_sse(@keyed_cart, "airtable_list_bases",
remote_ip: {1, 2, 3, 4},
trust_level: "internal"
)
assert conn.status == 403
assert body(conn)["error"] == "forbidden"
end
end
# ── TrustPolicy.satisfies?/3 function-level parity with the §3 contract ───
describe "TrustPolicy.satisfies?/3 function-level — live tests" do
test "is_local=true accepts every claim (loopback bypass; defended by §4)" do
for trust <- [nil, "public", "authenticated", "internal"] do
assert BojRest.TrustPolicy.satisfies?(:authenticated, trust, true)
end
end
test "is_local=false rejects every non-:public exposure regardless of header" do
for trust <- [nil, "public", "authenticated", "internal", "garbage"] do
refute BojRest.TrustPolicy.satisfies?(:authenticated, trust, false),
"satisfies?(:authenticated, #{inspect(trust)}, false) leaked through"
end
end
end
# ── helpers ────────────────────────────────────────────────────────────────
defp invoke(cart, tool, opts) do
do_invoke("/cartridge/#{cart}/invoke", tool, opts)
end
defp invoke_sse(cart, tool, opts) do
do_invoke("/cartridge/#{cart}/sse", tool, opts)
end
defp do_invoke(path, tool, opts) do
base =
conn(:post, path, Jason.encode!(%{tool: tool}))
|> put_req_header("content-type", "application/json")
base =
case Keyword.get(opts, :trust_level) do
nil -> base
v -> put_req_header(base, "x-trust-level", v)
end
base =
case Keyword.get(opts, :remote_ip) do
nil -> base
ip -> Map.put(base, :remote_ip, ip)
end
BojRest.Router.call(base, @opts)
end
defp body(conn), do: Jason.decode!(conn.resp_body)
end