@@ -316,4 +316,96 @@ defmodule BojRest.RouterTest do
316316 assert { :ok , _ } = Jason . decode ( conn . resp_body )
317317 end
318318 end
319+
320+ # ── Trust-level enforcement (Phase 9 auth) ──────────────────────────────────
321+
322+ # airtable-mcp has auth.method=bearer_token → requires authenticated trust
323+ @ keyed_cart "airtable-mcp"
324+ @ public_cart "boj-health"
325+
326+ test "POST /invoke on keyed cartridge from loopback without header → allowed (loopback bypass)" do
327+ conn =
328+ conn ( :post , "/cartridge/#{ @ keyed_cart } /invoke" , Jason . encode! ( % { tool: "airtable_list_bases" } ) )
329+ |> put_req_header ( "content-type" , "application/json" )
330+ |> BojRest.Router . call ( @ opts )
331+
332+ assert conn . status in [ 200 , 500 ]
333+ body = Jason . decode! ( conn . resp_body )
334+ refute body [ "error" ] == "forbidden"
335+ end
336+
337+ test "POST /invoke on public cartridge from non-loopback without header → allowed" do
338+ conn =
339+ conn ( :post , "/cartridge/#{ @ public_cart } /invoke" , Jason . encode! ( % { tool: "boj_health_status" } ) )
340+ |> put_req_header ( "content-type" , "application/json" )
341+ |> Map . put ( :remote_ip , { 1 , 2 , 3 , 4 } )
342+ |> BojRest.Router . call ( @ opts )
343+
344+ assert conn . status in [ 200 , 500 ]
345+ body = Jason . decode! ( conn . resp_body )
346+ refute body [ "error" ] == "forbidden"
347+ end
348+
349+ test "POST /invoke on keyed cartridge from non-loopback without header → 403 forbidden" do
350+ conn =
351+ conn ( :post , "/cartridge/#{ @ keyed_cart } /invoke" , Jason . encode! ( % { tool: "airtable_list_bases" } ) )
352+ |> put_req_header ( "content-type" , "application/json" )
353+ |> Map . put ( :remote_ip , { 1 , 2 , 3 , 4 } )
354+ |> BojRest.Router . call ( @ opts )
355+
356+ assert conn . status == 403
357+ body = Jason . decode! ( conn . resp_body )
358+ assert body [ "error" ] == "forbidden"
359+ assert body [ "detail" ] == "insufficient-trust"
360+ assert body [ "required" ] == "authenticated"
361+ end
362+
363+ test "POST /invoke on keyed cartridge with X-Trust-Level: authenticated → allowed" do
364+ conn =
365+ conn ( :post , "/cartridge/#{ @ keyed_cart } /invoke" , Jason . encode! ( % { tool: "airtable_list_bases" } ) )
366+ |> put_req_header ( "content-type" , "application/json" )
367+ |> put_req_header ( "x-trust-level" , "authenticated" )
368+ |> Map . put ( :remote_ip , { 1 , 2 , 3 , 4 } )
369+ |> BojRest.Router . call ( @ opts )
370+
371+ assert conn . status in [ 200 , 500 ]
372+ body = Jason . decode! ( conn . resp_body )
373+ refute body [ "error" ] == "forbidden"
374+ end
375+
376+ test "POST /invoke on keyed cartridge with X-Trust-Level: internal → allowed" do
377+ conn =
378+ conn ( :post , "/cartridge/#{ @ keyed_cart } /invoke" , Jason . encode! ( % { tool: "airtable_list_bases" } ) )
379+ |> put_req_header ( "content-type" , "application/json" )
380+ |> put_req_header ( "x-trust-level" , "internal" )
381+ |> Map . put ( :remote_ip , { 1 , 2 , 3 , 4 } )
382+ |> BojRest.Router . call ( @ opts )
383+
384+ assert conn . status in [ 200 , 500 ]
385+ body = Jason . decode! ( conn . resp_body )
386+ refute body [ "error" ] == "forbidden"
387+ end
388+
389+ test "POST /invoke on keyed cartridge with X-Trust-Level: public → 403 forbidden" do
390+ conn =
391+ conn ( :post , "/cartridge/#{ @ keyed_cart } /invoke" , Jason . encode! ( % { tool: "airtable_list_bases" } ) )
392+ |> put_req_header ( "content-type" , "application/json" )
393+ |> put_req_header ( "x-trust-level" , "public" )
394+ |> Map . put ( :remote_ip , { 1 , 2 , 3 , 4 } )
395+ |> BojRest.Router . call ( @ opts )
396+
397+ assert conn . status == 403
398+ body = Jason . decode! ( conn . resp_body )
399+ assert body [ "error" ] == "forbidden"
400+ end
401+
402+ test "POST /invoke with X-Node-Identity header does not crash" do
403+ conn =
404+ conn ( :post , "/cartridge/#{ @ public_cart } /invoke" , Jason . encode! ( % { tool: "boj_health_status" } ) )
405+ |> put_req_header ( "content-type" , "application/json" )
406+ |> put_req_header ( "x-node-identity" , "peer-node-abc123" )
407+ |> BojRest.Router . call ( @ opts )
408+
409+ assert conn . status in [ 200 , 500 ]
410+ end
319411end
0 commit comments