package authz import ( "net/http" "net/http/httptest" "testing" "time" ) func TestSurfaceCapabilities(t *testing.T) { if Telegram.CanEmit("TaskCreated") || Ntfy.CanEmit("ApprovalRequested") { t.Fatal("notify surface emitted an event") } if !TUI.CanEmit("TaskCreated") { t.Fatal("control surface cannot emit") } if !MCP.CanEmit("ApprovalRequested") || MCP.CanEmit("TaskCreated") { t.Fatal("mcp gate is wrong") } } // TestSystemSurfaceDowngradedByHTTPMiddleware guards half of B8: System // means "the plane itself, in-process" and is always FullControl with no // token gate, since no deployment configures a token for a surface no HTTP // caller is meant to use. HTTP() must never let a request pass through // treated as System, or a caller declaring X-Orchestra-Surface: system gets // an unconditional, unauthenticated bypass of the token check below it. // (Downstream handlers must independently avoid re-deriving System from the // raw header themselves — see cmd/orchestra/main.go's `surface` closure, // which this package cannot test directly.) func TestSystemSurfaceDowngradedByHTTPMiddleware(t *testing.T) { tokens := map[Surface]string{System: "should-never-be-checked"} h := HTTP(tokens, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) req := httptest.NewRequest(http.MethodPost, "/v1/tasks/t1/complete", nil) req.Header.Set("X-Orchestra-Surface", "system") rec := httptest.NewRecorder() h.ServeHTTP(rec, req) // System's token check is intentionally skipped by HTTP() (it downgrades // to Web before the token comparison), so the request reaching the // handler at all is expected here — the guard that matters is that // nothing downstream can observe "system" as the resolved surface. This // test documents the middleware's half of the fix; main.go's `surface` // closure carries the other half. if rec.Code != http.StatusOK { t.Fatalf("unexpected status %d", rec.Code) } } // B18: the web UI is a full control plane. A session cookie must be an // alternative *presentation* of the Web token, never a widening of it. func TestWebSessionCookieGatesControlPathsOnly(t *testing.T) { // An empty legacy Web bearer-token slot must not open the browser surface: // providing Sessions means the caller needs a session cookie. tokens := map[Surface]string{} sessions := &Sessions{} h := HTTPWithSessions(tokens, sessions, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) do := func(method, path string, c *http.Cookie, surface string) int { req := httptest.NewRequest(method, path, nil) if c != nil { req.AddCookie(c) } if surface != "" { req.Header.Set("X-Orchestra-Surface", surface) } rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec.Code } if got := do(http.MethodPost, "/v1/ui/tasks", nil, ""); got != http.StatusUnauthorized { t.Fatalf("uncredentialed control path = %d, want 401", got) } // The SPA shell and the login endpoint must stay reachable, or no // browser could ever obtain a cookie in the first place. if got := do(http.MethodGet, "/index.html", nil, ""); got != http.StatusOK { t.Fatalf("SPA shell = %d, want 200", got) } if got := do(http.MethodPost, SessionPath, nil, ""); got != http.StatusOK { t.Fatalf("login endpoint = %d, want 200", got) } v, err := sessions.Issue() if err != nil { t.Fatal(err) } if got := do(http.MethodPost, "/v1/ui/tasks", &http.Cookie{Name: SessionCookie, Value: v}, ""); got != http.StatusOK { t.Fatalf("session-cookie control path = %d, want 200", got) } if got := do(http.MethodPost, "/v1/ui/tasks", &http.Cookie{Name: SessionCookie, Value: "forged"}, ""); got != http.StatusUnauthorized { t.Fatalf("forged cookie = %d, want 401", got) } // A cookie must not authenticate a non-browser surface. tokens[TUI] = "tui-secret" if got := do(http.MethodPost, "/v1/tasks", &http.Cookie{Name: SessionCookie, Value: v}, "tui"); got != http.StatusUnauthorized { t.Fatalf("cookie on TUI surface = %d, want 401", got) } } func TestFederationRequestsUseTheirOwnCredentials(t *testing.T) { tokens := map[Surface]string{Web: "web-secret"} h := HTTPWithSessions(tokens, nil, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) })) for _, tc := range []struct { name string method string path string worker string want int }{ {name: "registration reaches admission handler", method: http.MethodPost, path: "/v1/federation/workers", want: http.StatusNoContent}, {name: "worker request reaches worker handler", method: http.MethodGet, path: "/v1/federation/events", worker: "workpc-opencode", want: http.StatusNoContent}, {name: "worker task reconciliation reaches worker handler", method: http.MethodGet, path: "/v1/tasks", worker: "workpc-opencode", want: http.StatusNoContent}, {name: "worker artifact read reaches worker handler", method: http.MethodGet, path: "/v1/artifacts/ref", worker: "workpc-opencode", want: http.StatusNoContent}, {name: "worker artifact upload reaches worker handler", method: http.MethodPost, path: "/v1/artifacts", worker: "workpc-opencode", want: http.StatusNoContent}, {name: "worker intent read reaches worker handler", method: http.MethodGet, path: "/v1/tasks/06G3YR34117MAYT6KEAC9RJHD0/intent", worker: "workpc-opencode", want: http.StatusNoContent}, {name: "unnamed intent read remains web gated", method: http.MethodGet, path: "/v1/tasks/06G3YR34117MAYT6KEAC9RJHD0/intent", want: http.StatusUnauthorized}, {name: "unnamed worker request remains web gated", method: http.MethodGet, path: "/v1/federation/events", want: http.StatusUnauthorized}, {name: "worker list remains web gated", method: http.MethodGet, path: "/v1/federation/workers", want: http.StatusUnauthorized}, } { t.Run(tc.name, func(t *testing.T) { r := httptest.NewRequest(tc.method, tc.path, nil) if tc.worker != "" { r.Header.Set("X-Orchestra-Worker", tc.worker) } w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != tc.want { t.Fatalf("status = %d, want %d", w.Code, tc.want) } }) } } func TestSessionExpires(t *testing.T) { s := &Sessions{TTL: time.Millisecond} v, err := s.Issue() if err != nil { t.Fatal(err) } time.Sleep(5 * time.Millisecond) if s.Valid(v) { t.Fatal("expired session accepted") } if s.Valid("") { t.Fatal("empty session accepted") } } func TestSessionRevoke(t *testing.T) { s := &Sessions{} v, err := s.Issue() if err != nil { t.Fatal(err) } if !s.Valid(v) { t.Fatal("fresh session must be valid") } s.Revoke(v) if s.Valid(v) { t.Fatal("revoked session must not be valid") } } func TestSessionTracksAndRevokesOperator(t *testing.T) { s := &Sessions{} one, err := s.IssueFor("kami") if err != nil { t.Fatal(err) } two, err := s.IssueFor("other") if err != nil { t.Fatal(err) } if username, ok := s.Username(one); !ok || username != "kami" { t.Fatalf("username=%q ok=%v", username, ok) } s.RevokeUser("KAMI") if s.Valid(one) { t.Fatal("operator session survived credential change") } if !s.Valid(two) { t.Fatal("another operator's session was revoked") } } // The agent boundary: an agent may perform work and request lifecycle changes, // never perform one. Both halves are proven here — the bus refuses the event // types Orchestra owns, and the middleware refuses their endpoints — because // an agent that reaches a handler with a valid token would otherwise be // indistinguishable from the browser operator. func TestAgentSurfaceCannotMutateLifecycle(t *testing.T) { for _, typ := range []string{ "TaskLeased", "TaskReleased", "TaskCompleted", "TaskBlocked", "WorkPhaseChanged", "ReviewRecorded", "TaskSubmitted", "TaskChangesRequested", "HumanDecisionRecorded", "ApprovalGranted", } { if Agent.CanEmit(typ) { t.Errorf("agent surface emitted %s", typ) } if err := AuthorizeEvent(Agent, typ); err == nil { t.Errorf("AuthorizeEvent(agent, %s) allowed", typ) } } if !Agent.CanEmit("ApprovalRequested") { t.Fatal("agent surface cannot ask") } if !Agent.CanRead() { t.Fatal("agent surface cannot read") } } func TestAgentSurfaceReachesOnlyRequestEndpoints(t *testing.T) { tokens := map[Surface]string{Agent: "agent-secret"} h := HTTPWithSessions(tokens, &Sessions{}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) })) do := func(method, path, auth string) int { r := httptest.NewRequest(method, path, nil) r.Header.Set("X-Orchestra-Surface", "agent") if auth != "" { r.Header.Set("Authorization", auth) } w := httptest.NewRecorder() h.ServeHTTP(w, r) return w.Code } const bearer = "Bearer agent-secret" for _, tc := range []struct { path string want int }{ // May ask. {"/v1/tasks/t1/decision-request", http.StatusNoContent}, {"/v1/tasks/t1/deferred", http.StatusNoContent}, {"/v1/tasks/t1/approval", http.StatusNoContent}, // May not act. {"/v1/tasks/t1/phase", http.StatusForbidden}, {"/v1/tasks/t1/review", http.StatusForbidden}, {"/v1/tasks/t1/submission", http.StatusForbidden}, {"/v1/tasks/t1/complete", http.StatusForbidden}, {"/v1/tasks/t1/lease", http.StatusForbidden}, {"/v1/tasks/t1/release", http.StatusForbidden}, {"/v1/tasks/t1/approval/grant", http.StatusForbidden}, {"/v1/standup/apply", http.StatusForbidden}, {"/v1/artifacts", http.StatusForbidden}, } { if got := do(http.MethodPost, tc.path, bearer); got != tc.want { t.Errorf("POST %s = %d, want %d", tc.path, got, tc.want) } } // The token still gates the surface: no credential, no request endpoint. if got := do(http.MethodPost, "/v1/tasks/t1/decision-request", ""); got != http.StatusUnauthorized { t.Errorf("uncredentialed agent = %d, want 401", got) } // A session cookie must not authenticate an agent, and an agent token must // not authenticate the browser surface. if got := do(http.MethodGet, "/v1/tasks", "Bearer wrong"); got != http.StatusUnauthorized { t.Errorf("wrong agent token = %d, want 401", got) } } // The harness turn endpoint authenticates its own bearer token in the handler. // Before this exemption it defaulted to the session-gated Web surface, so every // harness call returned 401 in any deployment with web credentials configured. func TestHarnessTurnBypassesSurfaceGate(t *testing.T) { h := HTTPWithSessions(map[Surface]string{}, &Sessions{}, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) })) w := httptest.NewRecorder() h.ServeHTTP(w, httptest.NewRequest(http.MethodPost, HarnessTurnPath, nil)) if w.Code != http.StatusNoContent { t.Fatalf("harness turn = %d, want 204", w.Code) } } // An unset surface token means no check, so a full-control surface without a // credential is an open control plane. Startup must refuse, not warn. func TestRequireCredentialsRefusesUncredentialedFullControl(t *testing.T) { if err := RequireCredentials(map[Surface]string{}); err == nil { t.Fatal("an uncredentialed TUI surface was accepted") } if err := RequireCredentials(map[Surface]string{TUI: " "}); err == nil { t.Fatal("a blank TUI token was accepted") } if err := RequireCredentials(map[Surface]string{TUI: "tui-secret"}); err != nil { t.Fatalf("a credentialed deployment was refused: %v", err) } // Gated and notify-only surfaces are bounded by capability, so an unset // token there is a deployment choice rather than an open control plane. if err := RequireCredentials(map[Surface]string{TUI: "tui-secret", MCP: "", Agent: ""}); err != nil { t.Fatalf("gated surfaces must not block startup: %v", err) } }