Files
Agent-System/internal/server/security_p0_test.go
T
weichen 3145550a59 feat(security): complete P0 release blocking remediation
Unify auth middleware and remove X-User-ID trust, add project_members
authorization, sandbox workspaces via resolver, and admin-only reconcile
by run_id. Introduce goose migrations with startup schema gate, fail closed
when DB unavailable outside explicit ALLOW_IN_MEMORY, bundle web/dist in
app Dockerfile, and remove frontend pseudo-login fallback.
2026-09-13 02:50:22 +00:00

115 lines
3.4 KiB
Go

package server
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/config"
)
func TestP0_AnonymousBusinessAPIReturns401(t *testing.T) {
cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: false}
svc, err := bizdev.NewService(cfg)
if err != nil {
t.Fatalf("NewService: %v", err)
}
srv := NewServer(cfg, svc)
cases := []struct {
method string
path string
body string
}{
{http.MethodPost, "/api/v1/docgov/content", `{"doc_id":"d1","markdown_body":"x"}`},
{http.MethodPost, "/api/v1/docgov/lease/acquire", `{"doc_id":"d1","holder_type":"human","holder_id":"1"}`},
{http.MethodPost, "/api/v1/admin/workspaces/reconcile", `{"run_id":"run_1"}`},
{http.MethodPost, "/api/v1/tasks/runs/run_test/cancel", ""},
{http.MethodGet, "/api/v1/projects", ""},
}
for _, tc := range cases {
var body *bytes.Reader
if tc.body != "" {
body = bytes.NewReader([]byte(tc.body))
} else {
body = bytes.NewReader(nil)
}
req := httptest.NewRequest(tc.method, tc.path, body)
if tc.body != "" {
req.Header.Set("Content-Type", "application/json")
}
rec := httptest.NewRecorder()
srv.Routes().ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("%s %s expected 401 got %d body=%s", tc.method, tc.path, rec.Code, rec.Body.String())
}
}
}
func TestP0_XUserIDCannotAuthenticate(t *testing.T) {
cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: false}
svc, err := bizdev.NewService(cfg)
if err != nil {
t.Fatalf("NewService: %v", err)
}
srv := NewServer(cfg, svc)
req := httptest.NewRequest(http.MethodGet, "/api/v1/projects", nil)
req.Header.Set("X-User-ID", "1")
rec := httptest.NewRecorder()
srv.Routes().ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("X-User-ID must not authenticate, got %d", rec.Code)
}
}
func TestP0_HealthDegradedWhenDBRequiredButUnavailable(t *testing.T) {
cfg := &config.Config{
DataDir: t.TempDir(),
AgentBackend: "stub",
ContextDbHost: "127.0.0.1",
ContextDbPort: 59999,
AllowInMemory: false,
}
svc, err := bizdev.NewService(cfg)
if err == nil {
t.Fatal("expected NewService error when DB unavailable")
}
_ = svc
srv := NewServer(cfg, bizdev.MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"}))
srv.cfg = cfg
req := httptest.NewRequest(http.MethodGet, "/api/v1/health", nil)
rec := httptest.NewRecorder()
srv.handleHealth(rec, req)
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("expected 503 health, got %d %s", rec.Code, rec.Body.String())
}
var resp HealthResponse
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
if resp.Status != "degraded" {
t.Fatalf("expected degraded status, got %q", resp.Status)
}
}
func TestP0_ReconcileRejectsClientWorkDir(t *testing.T) {
cfg := &config.Config{DataDir: t.TempDir(), AgentBackend: "stub", AllowInMemory: false}
svc, err := bizdev.NewService(cfg)
if err != nil {
t.Fatalf("NewService: %v", err)
}
srv := NewServer(cfg, svc)
body := bytes.NewReader([]byte(`{"work_dir":"/tmp/evil"}`))
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/workspaces/reconcile", body)
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
srv.Routes().ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("anonymous reconcile must be 401, got %d", rec.Code)
}
}