forked from molecule-ai/molecule-core
test(security): route-specific #684 regression — three vulnerable admin routes
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 <noreply@anthropic.com>
This commit is contained in:
parent
00ef832e33
commit
7aeaf3c07c
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user