mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-12 06:28:52 +00:00
auth-bundle-1 Phase 0: extract internal/auth/ from middleware package
Bundle 1 / Phase 0: pure refactor splitting auth surface out of internal/api/middleware so Bundle 2 (OIDC + sessions) and the broader RBAC primitive (roles, permissions, scoped grants) have a clean home. Moved to internal/auth/: NamedAPIKey, HashAPIKey, AuthConfig, NewAuthWithNamedKeys, NewAuth, UserKey, AdminKey, GetUser, IsAdmin. Added testfixtures.go (WithActor / WithAdmin / WithActorAdmin) so handler tests don't construct context manually. Stayed in internal/api/middleware/: RequestID, Logging, NewLogging, Recovery, RateLimitConfig, NewRateLimiter (now imports auth.GetUser for per-user keying per audit Category C), CORSConfig, NewCORS, ContentType, CORS, GetRequestID, responseWriter, Chain, audit middleware (now imports auth.GetUser). Updated 22 caller files across cmd/, internal/api/handler/, internal/api/middleware/, internal/mcp/. Existing m008_admin_gate_test.go now scans for auth.IsAdmin( substring; Phase 3 will further evolve to track auth.RequirePermission. Behavior unchanged: all handler / middleware / service / connector / cmd / mcp tests pass with no test-logic edits, only import-path renames. Phase 0 exit criteria: internal/auth/ exists with 6 files; middleware.go went 575 -> 422 lines (auth-related ~150 lines moved out); grep -rE 'middleware\.(GetUser|IsAdmin|UserKey|AdminKey|NamedAPIKey|HashAPIKey|NewAuth)' returns 0 hits; context.WithValue(.*middleware.UserKey/AdminKey) returns 0 hits; go vet ./... clean; go test -short ./... green across all packages tested. Branch: dev/auth-bundle-1. Per cowork/auth-bundle-1-prompt.md, do not merge to master without (1) make verify green, (2) >= 2 external testers confirm, (3) >= 90% coverage on internal/auth/ in .github/coverage-thresholds.yml.
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewAuth_MultiKeyAcceptsBothKeys(t *testing.T) {
|
||||
cfg := AuthConfig{
|
||||
Type: "api-key",
|
||||
Secret: "key-one,key-two",
|
||||
}
|
||||
|
||||
mw := NewAuth(cfg)
|
||||
handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
// First key should work
|
||||
req1 := httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil)
|
||||
req1.Header.Set("Authorization", "Bearer key-one")
|
||||
rr1 := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr1, req1)
|
||||
if rr1.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 for first key, got %d", rr1.Code)
|
||||
}
|
||||
|
||||
// Second key should work
|
||||
req2 := httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil)
|
||||
req2.Header.Set("Authorization", "Bearer key-two")
|
||||
rr2 := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr2, req2)
|
||||
if rr2.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 for second key, got %d", rr2.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAuth_MultiKeyRejectsInvalidKey(t *testing.T) {
|
||||
cfg := AuthConfig{
|
||||
Type: "api-key",
|
||||
Secret: "key-one,key-two",
|
||||
}
|
||||
|
||||
mw := NewAuth(cfg)
|
||||
handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
// Invalid key should be rejected
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil)
|
||||
req.Header.Set("Authorization", "Bearer wrong-key")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401 for invalid key, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAuth_MultiKeyWithSpaces(t *testing.T) {
|
||||
// Keys with leading/trailing spaces should be trimmed
|
||||
cfg := AuthConfig{
|
||||
Type: "api-key",
|
||||
Secret: " key-one , key-two ",
|
||||
}
|
||||
|
||||
mw := NewAuth(cfg)
|
||||
handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil)
|
||||
req.Header.Set("Authorization", "Bearer key-one")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 for trimmed key, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAuth_SingleKeyStillWorks(t *testing.T) {
|
||||
cfg := AuthConfig{
|
||||
Type: "api-key",
|
||||
Secret: "my-single-key",
|
||||
}
|
||||
|
||||
mw := NewAuth(cfg)
|
||||
handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil)
|
||||
req.Header.Set("Authorization", "Bearer my-single-key")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 for single key, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAuth_NoneMode(t *testing.T) {
|
||||
cfg := AuthConfig{
|
||||
Type: "none",
|
||||
Secret: "",
|
||||
}
|
||||
|
||||
mw := NewAuth(cfg)
|
||||
handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
// No auth header needed in none mode
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 in none mode, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAuth_MissingAuthHeader(t *testing.T) {
|
||||
cfg := AuthConfig{
|
||||
Type: "api-key",
|
||||
Secret: "test-key",
|
||||
}
|
||||
|
||||
mw := NewAuth(cfg)
|
||||
handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401 for missing auth, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAuth_InvalidBearerFormat(t *testing.T) {
|
||||
cfg := AuthConfig{
|
||||
Type: "api-key",
|
||||
Secret: "test-key",
|
||||
}
|
||||
|
||||
mw := NewAuth(cfg)
|
||||
handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil)
|
||||
req.Header.Set("Authorization", "Basic dGVzdDp0ZXN0")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401 for non-Bearer auth, got %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAuth_RemovedKeyIsRejected(t *testing.T) {
|
||||
// Simulate key rotation: only key-two is configured (key-one was removed)
|
||||
cfg := AuthConfig{
|
||||
Type: "api-key",
|
||||
Secret: "key-two",
|
||||
}
|
||||
|
||||
mw := NewAuth(cfg)
|
||||
handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
// Old key should be rejected
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil)
|
||||
req.Header.Set("Authorization", "Bearer key-one")
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401 for removed key, got %d", rr.Code)
|
||||
}
|
||||
|
||||
// New key should work
|
||||
req2 := httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil)
|
||||
req2.Header.Set("Authorization", "Bearer key-two")
|
||||
rr2 := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr2, req2)
|
||||
if rr2.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 for current key, got %d", rr2.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user