Files
orchestra/internal/authz/authz_test.go
T
kami 4fbf3ac966 Report queued tasks the scheduling pass never considers
A task in retry backoff was filtered out before the candidate loop, so it
recorded no rejection at all: queued, apparently assignable, and silent. That is
the exact shape that made F5 take a live session to diagnose. It now reports
"retry backoff until <time>".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 23:42:26 +04:00

298 lines
12 KiB
Go

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)
}
}