From 7aeaf3c07cf36cea2cf98ffadf726f911497ded3 Mon Sep 17 00:00:00 2001 From: Molecule AI QA Engineer Date: Fri, 17 Apr 2026 15:25:41 +0000 Subject: [PATCH] =?UTF-8?q?test(security):=20route-specific=20#684=20regre?= =?UTF-8?q?ssion=20=E2=80=94=20three=20vulnerable=20admin=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The BE's tests (AdminTokenSet_*, FailOpen_*) validated the core AdminAuth contract on /admin/secrets. These table-driven additions pin the same contract on the three routes explicitly named in the #684 security report, each with three scenarios: workspace token rejected, correct ADMIN_TOKEN accepted, no bearer rejected. Routes covered: GET /admin/liveness GET /admin/github-installation-token GET /approvals/pending When ADMIN_TOKEN is set (tier 2), ValidateAnyToken is never called — the env-var comparison short-circuits before any DB lookup. The mock sets only HasAnyLiveTokenGlobal and nothing else; an extra DB expectation would itself be a test bug (calling it proves the middleware regressed to tier 3). All 18 TestAdminAuth_684* tests pass. Full go test ./... is green across all 15 platform packages. Co-Authored-By: Claude Sonnet 4.6 --- .../middleware/wsauth_middleware_test.go | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/platform/internal/middleware/wsauth_middleware_test.go b/platform/internal/middleware/wsauth_middleware_test.go index 740c574a..e5b157ed 100644 --- a/platform/internal/middleware/wsauth_middleware_test.go +++ b/platform/internal/middleware/wsauth_middleware_test.go @@ -1170,3 +1170,168 @@ func TestAdminAuth_684_FailOpen_AdminTokenSet_NoGlobalTokens(t *testing.T) { t.Errorf("unmet sqlmock expectations: %v", err) } } + +// ── Issue #684 route-specific regression ───────────────────────────────────── +// The tests above validate the core AdminAuth middleware contract. These +// table-driven tests pin the same contract for the three specific routes named +// in the #684 security report: /admin/liveness, /admin/github-installation-token, +// and /approvals/pending. Coverage: workspace-token rejected, correct ADMIN_TOKEN +// accepted, no-bearer rejected (with and without ADMIN_TOKEN configured). + +// TestAdminAuth_684_SpecificRoutes_WorkspaceTokenRejected — a workspace bearer +// must be rejected on each vulnerable route when ADMIN_TOKEN is set (tier 2). +// The workspace token value intentionally differs from ADMIN_TOKEN. +func TestAdminAuth_684_SpecificRoutes_WorkspaceTokenRejected(t *testing.T) { + routes := []struct { + method string + path string + }{ + {http.MethodGet, "/admin/liveness"}, + {http.MethodGet, "/admin/github-installation-token"}, + {http.MethodGet, "/approvals/pending"}, + } + + for _, rt := range routes { + rt := rt + t.Run(rt.path, func(t *testing.T) { + mockDB, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("sqlmock.New: %v", err) + } + defer mockDB.Close() + + const adminSecret = "correct-admin-secret-not-a-workspace-token" + t.Setenv("ADMIN_TOKEN", adminSecret) + + mock.ExpectQuery(hasAnyLiveTokenGlobalQuery). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + + // With ADMIN_TOKEN set, ValidateAnyToken is never called — the env-var + // check short-circuits. No DB token lookup expectation is set here. + + r := gin.New() + r.Handle(rt.method, rt.path, AdminAuth(mockDB), func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"ok": true}) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest(rt.method, rt.path, nil) + // Workspace-scoped token — valid for a workspace, but ≠ ADMIN_TOKEN. + req.Header.Set("Authorization", "Bearer workspace-agent-bearer-not-admin") + r.ServeHTTP(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("#684 %s %s: workspace token should be rejected, got %d: %s", + rt.method, rt.path, w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet sqlmock expectations: %v", err) + } + }) + } +} + +// TestAdminAuth_684_SpecificRoutes_CorrectAdminTokenAccepted — the exact +// ADMIN_TOKEN value must grant access on each vulnerable route. No DB token +// lookup occurs — the env-var comparison is constant-time only. +func TestAdminAuth_684_SpecificRoutes_CorrectAdminTokenAccepted(t *testing.T) { + routes := []struct { + method string + path string + }{ + {http.MethodGet, "/admin/liveness"}, + {http.MethodGet, "/admin/github-installation-token"}, + {http.MethodGet, "/approvals/pending"}, + } + + for _, rt := range routes { + rt := rt + t.Run(rt.path, func(t *testing.T) { + mockDB, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("sqlmock.New: %v", err) + } + defer mockDB.Close() + + const adminSecret = "correct-admin-secret-not-a-workspace-token" + t.Setenv("ADMIN_TOKEN", adminSecret) + + mock.ExpectQuery(hasAnyLiveTokenGlobalQuery). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + + // No DB token lookup — ADMIN_TOKEN match triggers c.Next() directly. + + r := gin.New() + r.Handle(rt.method, rt.path, AdminAuth(mockDB), func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"ok": true}) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest(rt.method, rt.path, nil) + req.Header.Set("Authorization", "Bearer "+adminSecret) + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("#684 %s %s: correct ADMIN_TOKEN should pass, got %d: %s", + rt.method, rt.path, w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet sqlmock expectations: %v", err) + } + }) + } +} + +// TestAdminAuth_684_SpecificRoutes_NoBearer_Returns401 — no bearer returns +// 401 on each vulnerable route, both with and without ADMIN_TOKEN set. +func TestAdminAuth_684_SpecificRoutes_NoBearer_Returns401(t *testing.T) { + routes := []struct { + method string + path string + adminToken string // empty = ADMIN_TOKEN not configured (tier-3 fallback) + }{ + // ADMIN_TOKEN configured — explicit rejection before any DB lookup. + {http.MethodGet, "/admin/liveness", "some-admin-secret"}, + {http.MethodGet, "/admin/github-installation-token", "some-admin-secret"}, + {http.MethodGet, "/approvals/pending", "some-admin-secret"}, + // ADMIN_TOKEN absent — tier-3 fallback, still rejects missing bearer. + {http.MethodGet, "/admin/liveness", ""}, + {http.MethodGet, "/admin/github-installation-token", ""}, + {http.MethodGet, "/approvals/pending", ""}, + } + + for _, rt := range routes { + rt := rt + label := rt.path + "/ADMIN_TOKEN=" + rt.adminToken + t.Run(label, func(t *testing.T) { + mockDB, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("sqlmock.New: %v", err) + } + defer mockDB.Close() + + t.Setenv("ADMIN_TOKEN", rt.adminToken) + + mock.ExpectQuery(hasAnyLiveTokenGlobalQuery). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) + + r := gin.New() + r.Handle(rt.method, rt.path, AdminAuth(mockDB), func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"ok": true}) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest(rt.method, rt.path, nil) + // No Authorization header — must be rejected unconditionally. + r.ServeHTTP(w, req) + + if w.Code != http.StatusUnauthorized { + t.Errorf("#684 no-bearer %s %s: expected 401, got %d: %s", + rt.method, rt.path, w.Code, w.Body.String()) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet sqlmock expectations: %v", err) + } + }) + } +}