@@ -0,0 +1,190 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"vctp/internal/auth"
|
||||
"vctp/internal/secrets"
|
||||
"vctp/internal/settings"
|
||||
)
|
||||
|
||||
func TestProtectedRoutesRequireAuthentication(t *testing.T) {
|
||||
cfg := testRouterSettings(t, false)
|
||||
app := testRouter(t, cfg)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
body string
|
||||
}{
|
||||
{
|
||||
name: "auth me",
|
||||
method: http.MethodGet,
|
||||
path: "/api/auth/me",
|
||||
},
|
||||
{
|
||||
name: "viewer route",
|
||||
method: http.MethodGet,
|
||||
path: "/api/report/snapshot",
|
||||
},
|
||||
{
|
||||
name: "admin route",
|
||||
method: http.MethodPost,
|
||||
path: "/api/encrypt",
|
||||
body: `{"plaintext":"hello"}`,
|
||||
},
|
||||
{
|
||||
name: "legacy route",
|
||||
method: http.MethodPost,
|
||||
path: "/api/import/vm",
|
||||
body: `{`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
|
||||
if tc.body != "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
app.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected status %d, got %d for %s %s", http.StatusUnauthorized, rr.Code, tc.method, tc.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewerCanReadButCannotMutate(t *testing.T) {
|
||||
cfg := testRouterSettings(t, false)
|
||||
app := testRouter(t, cfg)
|
||||
viewerToken := mustIssueToken(t, cfg, "viewer-user", []string{"viewer"})
|
||||
|
||||
readReq := httptest.NewRequest(http.MethodGet, "/api/report/snapshot", nil)
|
||||
readReq.Header.Set("Authorization", "Bearer "+viewerToken)
|
||||
readRR := httptest.NewRecorder()
|
||||
app.ServeHTTP(readRR, readReq)
|
||||
if readRR.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected viewer read to reach handler and return %d, got %d", http.StatusBadRequest, readRR.Code)
|
||||
}
|
||||
|
||||
mutateReq := httptest.NewRequest(http.MethodPost, "/api/encrypt", strings.NewReader(`{"plaintext":"hello"}`))
|
||||
mutateReq.Header.Set("Authorization", "Bearer "+viewerToken)
|
||||
mutateReq.Header.Set("Content-Type", "application/json")
|
||||
mutateRR := httptest.NewRecorder()
|
||||
app.ServeHTTP(mutateRR, mutateReq)
|
||||
if mutateRR.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected viewer mutate to return %d, got %d", http.StatusForbidden, mutateRR.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminCanMutate(t *testing.T) {
|
||||
cfg := testRouterSettings(t, false)
|
||||
app := testRouter(t, cfg)
|
||||
adminToken := mustIssueToken(t, cfg, "admin-user", []string{"admin"})
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/encrypt", strings.NewReader(`{"plaintext":"hello"}`))
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rr := httptest.NewRecorder()
|
||||
app.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected admin mutate to return %d, got %d body=%s", http.StatusOK, rr.Code, rr.Body.String())
|
||||
}
|
||||
|
||||
var payload map[string]string
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if payload["status"] != "OK" {
|
||||
t.Fatalf("expected status OK, got %q", payload["status"])
|
||||
}
|
||||
if strings.TrimSpace(payload["ciphertext"]) == "" {
|
||||
t.Fatal("expected ciphertext in response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyEndpointGatingStillAppliesWithAuthEnabled(t *testing.T) {
|
||||
disabledCfg := testRouterSettings(t, false)
|
||||
disabledApp := testRouter(t, disabledCfg)
|
||||
adminToken := mustIssueToken(t, disabledCfg, "admin-user", []string{"admin"})
|
||||
|
||||
disabledReq := httptest.NewRequest(http.MethodPost, "/api/import/vm", strings.NewReader(`{`))
|
||||
disabledReq.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
disabledReq.Header.Set("Content-Type", "application/json")
|
||||
disabledRR := httptest.NewRecorder()
|
||||
disabledApp.ServeHTTP(disabledRR, disabledReq)
|
||||
if disabledRR.Code != http.StatusGone {
|
||||
t.Fatalf("expected legacy-disabled route to return %d, got %d", http.StatusGone, disabledRR.Code)
|
||||
}
|
||||
|
||||
enabledCfg := testRouterSettings(t, true)
|
||||
enabledApp := testRouter(t, enabledCfg)
|
||||
enabledToken := mustIssueToken(t, enabledCfg, "admin-user", []string{"admin"})
|
||||
|
||||
enabledReq := httptest.NewRequest(http.MethodPost, "/api/import/vm", strings.NewReader(`{`))
|
||||
enabledReq.Header.Set("Authorization", "Bearer "+enabledToken)
|
||||
enabledReq.Header.Set("Content-Type", "application/json")
|
||||
enabledRR := httptest.NewRecorder()
|
||||
enabledApp.ServeHTTP(enabledRR, enabledReq)
|
||||
if enabledRR.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected legacy-enabled route to reach handler and return %d, got %d", http.StatusBadRequest, enabledRR.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func testRouter(t *testing.T, cfg *settings.Settings) http.Handler {
|
||||
t.Helper()
|
||||
logger := testRouterLogger()
|
||||
secretKey := []byte("0123456789abcdef0123456789abcdef")
|
||||
secretSvc := secrets.New(logger, secretKey)
|
||||
return New(logger, nil, "test-build", "test-sha", "go-test", nil, secretSvc, cfg)
|
||||
}
|
||||
|
||||
func testRouterSettings(t *testing.T, enableLegacyAPI bool) *settings.Settings {
|
||||
t.Helper()
|
||||
cfg := &settings.Settings{Values: &settings.SettingsYML{}}
|
||||
cfg.Values.Settings.AuthEnabled = true
|
||||
cfg.Values.Settings.AuthMode = "required"
|
||||
cfg.Values.Settings.AuthJWTSigningKey = base64.StdEncoding.EncodeToString([]byte("router-auth-test-signing-key"))
|
||||
cfg.Values.Settings.AuthTokenLifespanMinutes = 120
|
||||
cfg.Values.Settings.AuthJWTIssuer = "vctp"
|
||||
cfg.Values.Settings.AuthJWTAudience = "vctp-api"
|
||||
cfg.Values.Settings.AuthClockSkewSeconds = 60
|
||||
cfg.Values.Settings.EnableLegacyAPI = enableLegacyAPI
|
||||
cfg.Values.Settings.ReportsDir = t.TempDir()
|
||||
return cfg
|
||||
}
|
||||
|
||||
func mustIssueToken(t *testing.T, cfg *settings.Settings, subject string, roles []string) string {
|
||||
t.Helper()
|
||||
svc, err := auth.NewJWTService(auth.JWTConfig{
|
||||
SigningKeyBase64: cfg.Values.Settings.AuthJWTSigningKey,
|
||||
Issuer: cfg.Values.Settings.AuthJWTIssuer,
|
||||
Audience: cfg.Values.Settings.AuthJWTAudience,
|
||||
TokenLifespan: time.Duration(cfg.Values.Settings.AuthTokenLifespanMinutes) * time.Minute,
|
||||
ClockSkew: time.Duration(cfg.Values.Settings.AuthClockSkewSeconds) * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create jwt service: %v", err)
|
||||
}
|
||||
token, _, err := svc.IssueToken(subject, roles, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to issue token: %v", err)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func testRouterLogger() *slog.Logger {
|
||||
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||
}
|
||||
Reference in New Issue
Block a user