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:
shankar0123
2026-05-09 15:51:31 +00:00
parent 71ebccb8ba
commit 99a012e3be
32 changed files with 397 additions and 283 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ import (
"net/http"
"time"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
"github.com/certctl-io/certctl/internal/domain"
"github.com/certctl-io/certctl/internal/repository"
)
@@ -74,7 +74,7 @@ func (h AdminCRLCacheHandler) ListCache(w http.ResponseWriter, r *http.Request)
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !middleware.IsAdmin(r.Context()) {
if !auth.IsAdmin(r.Context()) {
Error(w, http.StatusForbidden, "Admin access required")
return
}
+6 -5
View File
@@ -10,6 +10,7 @@ import (
"testing"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
)
// fakeAdminCRLCacheService is the test stub for the
@@ -67,7 +68,7 @@ func TestAdminCRLCache_AdminExplicitFalse_Returns403(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crl/cache", nil)
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, false)
ctx = context.WithValue(ctx, auth.AdminKey{}, false)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@@ -99,8 +100,8 @@ func TestAdminCRLCache_AdminPermitted_ForwardsActor(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crl/cache", nil)
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, true)
ctx = context.WithValue(ctx, middleware.UserKey{}, "ops-admin")
ctx = context.WithValue(ctx, auth.AdminKey{}, true)
ctx = context.WithValue(ctx, auth.UserKey{}, "ops-admin")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@@ -131,7 +132,7 @@ func TestAdminCRLCache_RejectsNonGetMethod(t *testing.T) {
h := NewAdminCRLCacheHandler(&fakeAdminCRLCacheService{})
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/crl/cache", nil)
ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
ctx := context.WithValue(context.Background(), auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@@ -150,7 +151,7 @@ func TestAdminCRLCache_PropagatesServiceError(t *testing.T) {
h := NewAdminCRLCacheHandler(svc)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/crl/cache", nil)
ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
ctx := context.WithValue(context.Background(), auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
+3 -3
View File
@@ -7,7 +7,7 @@ import (
"net/http"
"time"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
"github.com/certctl-io/certctl/internal/service"
)
@@ -76,7 +76,7 @@ func (h AdminESTHandler) Profiles(w http.ResponseWriter, r *http.Request) {
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !middleware.IsAdmin(r.Context()) {
if !auth.IsAdmin(r.Context()) {
Error(w, http.StatusForbidden, "Admin access required")
return
}
@@ -104,7 +104,7 @@ func (h AdminESTHandler) ReloadTrust(w http.ResponseWriter, r *http.Request) {
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !middleware.IsAdmin(r.Context()) {
if !auth.IsAdmin(r.Context()) {
Error(w, http.StatusForbidden, "Admin access required")
return
}
+10 -9
View File
@@ -11,6 +11,7 @@ import (
"time"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
"github.com/certctl-io/certctl/internal/service"
)
@@ -65,7 +66,7 @@ func TestAdminEST_Profiles_AdminExplicitFalse_Returns403(t *testing.T) {
h := NewAdminESTHandler(svc)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/est/profiles", nil)
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, false)
ctx = context.WithValue(ctx, auth.AdminKey{}, false)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.Profiles(w, req)
@@ -86,7 +87,7 @@ func TestAdminEST_Profiles_AdminTrue_Returns200(t *testing.T) {
h := NewAdminESTHandler(svc)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/est/profiles", nil)
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, true)
ctx = context.WithValue(ctx, auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.Profiles(w, req)
@@ -121,7 +122,7 @@ func TestAdminEST_Profiles_NilRowsSerializedAsEmptyArray(t *testing.T) {
h := NewAdminESTHandler(svc)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/est/profiles", nil)
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, true)
ctx = context.WithValue(ctx, auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.Profiles(w, req)
@@ -157,7 +158,7 @@ func TestAdminEST_ReloadTrust_AdminExplicitFalse_Returns403(t *testing.T) {
strings.NewReader(`{"path_id":"corp"}`))
req.ContentLength = int64(len(`{"path_id":"corp"}`))
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, false)
ctx = context.WithValue(ctx, auth.AdminKey{}, false)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.ReloadTrust(w, req)
@@ -177,7 +178,7 @@ func TestAdminEST_ReloadTrust_HappyPath(t *testing.T) {
strings.NewReader(body))
req.ContentLength = int64(len(body))
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, true)
ctx = context.WithValue(ctx, auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.ReloadTrust(w, req)
@@ -197,7 +198,7 @@ func TestAdminEST_ReloadTrust_UnknownPathID_Returns404(t *testing.T) {
strings.NewReader(body))
req.ContentLength = int64(len(body))
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, true)
ctx = context.WithValue(ctx, auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.ReloadTrust(w, req)
@@ -214,7 +215,7 @@ func TestAdminEST_ReloadTrust_MTLSDisabled_Returns409(t *testing.T) {
strings.NewReader(body))
req.ContentLength = int64(len(body))
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, true)
ctx = context.WithValue(ctx, auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.ReloadTrust(w, req)
@@ -231,7 +232,7 @@ func TestAdminEST_ReloadTrust_ParseError_Returns500(t *testing.T) {
strings.NewReader(body))
req.ContentLength = int64(len(body))
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, true)
ctx = context.WithValue(ctx, auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.ReloadTrust(w, req)
@@ -248,7 +249,7 @@ func TestAdminEST_ReloadTrust_MalformedJSON_Returns400(t *testing.T) {
strings.NewReader(body))
req.ContentLength = int64(len(body))
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, true)
ctx = context.WithValue(ctx, auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.ReloadTrust(w, req)
+4 -4
View File
@@ -7,7 +7,7 @@ import (
"net/http"
"time"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
"github.com/certctl-io/certctl/internal/service"
)
@@ -90,7 +90,7 @@ func (h AdminSCEPIntuneHandler) Profiles(w http.ResponseWriter, r *http.Request)
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !middleware.IsAdmin(r.Context()) {
if !auth.IsAdmin(r.Context()) {
Error(w, http.StatusForbidden, "Admin access required")
return
}
@@ -118,7 +118,7 @@ func (h AdminSCEPIntuneHandler) Stats(w http.ResponseWriter, r *http.Request) {
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !middleware.IsAdmin(r.Context()) {
if !auth.IsAdmin(r.Context()) {
Error(w, http.StatusForbidden, "Admin access required")
return
}
@@ -146,7 +146,7 @@ func (h AdminSCEPIntuneHandler) ReloadTrust(w http.ResponseWriter, r *http.Reque
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !middleware.IsAdmin(r.Context()) {
if !auth.IsAdmin(r.Context()) {
Error(w, http.StatusForbidden, "Admin access required")
return
}
+20 -19
View File
@@ -11,6 +11,7 @@ import (
"time"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
"github.com/certctl-io/certctl/internal/service"
)
@@ -81,7 +82,7 @@ func TestAdminSCEPIntune_AdminExplicitFalse_Returns403(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/scep/intune/stats", nil)
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, false)
ctx = context.WithValue(ctx, auth.AdminKey{}, false)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@@ -106,8 +107,8 @@ func TestAdminSCEPIntune_AdminPermitted_ForwardsActor(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/scep/intune/stats", nil)
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, true)
ctx = context.WithValue(ctx, middleware.UserKey{}, "ops-admin")
ctx = context.WithValue(ctx, auth.AdminKey{}, true)
ctx = context.WithValue(ctx, auth.UserKey{}, "ops-admin")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@@ -160,7 +161,7 @@ func TestAdminSCEPIntuneReload_AdminExplicitFalse_Returns403(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/scep/intune/reload-trust",
strings.NewReader(`{"path_id":"corp"}`))
req.ContentLength = int64(len(`{"path_id":"corp"}`))
ctx := context.WithValue(context.Background(), middleware.AdminKey{}, false)
ctx := context.WithValue(context.Background(), auth.AdminKey{}, false)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@@ -181,8 +182,8 @@ func TestAdminSCEPIntuneReload_AdminPermitted_ForwardsActor(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/scep/intune/reload-trust",
strings.NewReader(body))
req.ContentLength = int64(len(body))
ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
ctx = context.WithValue(ctx, middleware.UserKey{}, "ops-admin")
ctx := context.WithValue(context.Background(), auth.AdminKey{}, true)
ctx = context.WithValue(ctx, auth.UserKey{}, "ops-admin")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@@ -211,7 +212,7 @@ func TestAdminSCEPIntuneReload_AdminPermitted_ForwardsActor(t *testing.T) {
func TestAdminSCEPIntuneStats_RejectsNonGetMethod(t *testing.T) {
h := NewAdminSCEPIntuneHandler(&fakeAdminSCEPIntuneService{})
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/scep/intune/stats", nil)
ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
ctx := context.WithValue(context.Background(), auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.Stats(w, req)
@@ -223,7 +224,7 @@ func TestAdminSCEPIntuneStats_RejectsNonGetMethod(t *testing.T) {
func TestAdminSCEPIntuneReload_RejectsNonPostMethod(t *testing.T) {
h := NewAdminSCEPIntuneHandler(&fakeAdminSCEPIntuneService{})
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/scep/intune/reload-trust", nil)
ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
ctx := context.WithValue(context.Background(), auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.ReloadTrust(w, req)
@@ -236,7 +237,7 @@ func TestAdminSCEPIntuneStats_PropagatesServiceError(t *testing.T) {
svc := &fakeAdminSCEPIntuneService{statsErr: errors.New("registry walk failed")}
h := NewAdminSCEPIntuneHandler(svc)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/scep/intune/stats", nil)
ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
ctx := context.WithValue(context.Background(), auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.Stats(w, req)
@@ -251,7 +252,7 @@ func TestAdminSCEPIntuneReload_ProfileNotFound_Returns404(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/scep/intune/reload-trust",
strings.NewReader(`{"path_id":"nonexistent"}`))
req.ContentLength = int64(len(`{"path_id":"nonexistent"}`))
ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
ctx := context.WithValue(context.Background(), auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.ReloadTrust(w, req)
@@ -266,7 +267,7 @@ func TestAdminSCEPIntuneReload_IntuneDisabled_Returns409(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/scep/intune/reload-trust",
strings.NewReader(`{"path_id":"iot"}`))
req.ContentLength = int64(len(`{"path_id":"iot"}`))
ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
ctx := context.WithValue(context.Background(), auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.ReloadTrust(w, req)
@@ -281,7 +282,7 @@ func TestAdminSCEPIntuneReload_BadReloadPropagates500(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/scep/intune/reload-trust",
strings.NewReader(`{"path_id":"corp"}`))
req.ContentLength = int64(len(`{"path_id":"corp"}`))
ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
ctx := context.WithValue(context.Background(), auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.ReloadTrust(w, req)
@@ -294,7 +295,7 @@ func TestAdminSCEPIntuneReload_EmptyBodyTargetsLegacyRoot(t *testing.T) {
svc := &fakeAdminSCEPIntuneService{}
h := NewAdminSCEPIntuneHandler(svc)
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/scep/intune/reload-trust", nil)
ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
ctx := context.WithValue(context.Background(), auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.ReloadTrust(w, req)
@@ -312,7 +313,7 @@ func TestAdminSCEPIntuneReload_RejectsMalformedJSON(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/scep/intune/reload-trust",
strings.NewReader(bad))
req.ContentLength = int64(len(bad))
ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
ctx := context.WithValue(context.Background(), auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.ReloadTrust(w, req)
@@ -379,7 +380,7 @@ func TestAdminSCEPProfiles_AdminExplicitFalse_Returns403(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/scep/profiles", nil)
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, false)
ctx = context.WithValue(ctx, auth.AdminKey{}, false)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@@ -417,8 +418,8 @@ func TestAdminSCEPProfiles_AdminPermitted_ForwardsActor(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/scep/profiles", nil)
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, true)
ctx = context.WithValue(ctx, middleware.UserKey{}, "ops-admin")
ctx = context.WithValue(ctx, auth.AdminKey{}, true)
ctx = context.WithValue(ctx, auth.UserKey{}, "ops-admin")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@@ -461,7 +462,7 @@ func TestAdminSCEPProfiles_AdminPermitted_ForwardsActor(t *testing.T) {
func TestAdminSCEPProfiles_RejectsNonGetMethod(t *testing.T) {
h := NewAdminSCEPIntuneHandler(&fakeAdminSCEPIntuneService{})
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/scep/profiles", nil)
ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
ctx := context.WithValue(context.Background(), auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.Profiles(w, req)
@@ -474,7 +475,7 @@ func TestAdminSCEPProfiles_PropagatesServiceError(t *testing.T) {
svc := &fakeAdminSCEPIntuneService{profilesErr: errors.New("registry walk failed")}
h := NewAdminSCEPIntuneHandler(svc)
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/scep/profiles", nil)
ctx := context.WithValue(context.Background(), middleware.AdminKey{}, true)
ctx := context.WithValue(context.Background(), auth.AdminKey{}, true)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.Profiles(w, req)
+3 -2
View File
@@ -8,6 +8,7 @@ import (
"strconv"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
"github.com/certctl-io/certctl/internal/domain"
"github.com/certctl-io/certctl/internal/repository"
"github.com/certctl-io/certctl/internal/service"
@@ -111,7 +112,7 @@ func (h ApprovalHandler) GetApproval(w http.ResponseWriter, r *http.Request) {
// Approve transitions a pending approval request to approved + transitions
// the linked Job from AwaitingApproval to Pending. RBAC: the authenticated
// actor extracted via middleware.UserKey must NOT equal the request's
// actor extracted via auth.UserKey must NOT equal the request's
// RequestedBy — the service-layer check enforces this and the handler
// surfaces it as HTTP 403.
//
@@ -153,7 +154,7 @@ func (h ApprovalHandler) decision(w http.ResponseWriter, r *http.Request, action
// Extract authenticated actor. The auth middleware sets UserKey to the
// API-key NamedAPIKey.Name (or empty for unauthenticated). RBAC at the
// service layer requires a non-empty actor.
actor, _ := r.Context().Value(middleware.UserKey{}).(string)
actor, _ := r.Context().Value(auth.UserKey{}).(string)
if actor == "" {
ErrorWithRequestID(w, http.StatusUnauthorized,
"authentication required to approve / reject", requestID)
+2 -2
View File
@@ -10,7 +10,7 @@ import (
"sync"
"testing"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
"github.com/certctl-io/certctl/internal/domain"
"github.com/certctl-io/certctl/internal/repository"
"github.com/certctl-io/certctl/internal/service"
@@ -117,7 +117,7 @@ func reqWithActor(t *testing.T, method, target string, body string, actor string
}
req.Header.Set("Content-Type", "application/json")
if actor != "" {
req = req.WithContext(context.WithValue(req.Context(), middleware.UserKey{}, actor))
req = req.WithContext(context.WithValue(req.Context(), auth.UserKey{}, actor))
}
if pathID != "" {
req.SetPathValue("id", pathID)
@@ -172,7 +172,7 @@ func authenticatedContext(actor string) context.Context {
type userKey struct{}
// The middleware UserKey is a private type in the middleware package, so
// in this handler test we can't construct one directly. Bulk-renew and
// bulk-reassign read the actor through the same middleware.GetUser path
// bulk-reassign read the actor through the same auth.GetUser path
// that bulk-revoke does — adminContext() in the existing test suite is
// the canonical helper. Reuse it (delivers both UserKey and AdminKey).
_ = userKey{}
@@ -11,6 +11,7 @@ import (
"testing"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
"github.com/certctl-io/certctl/internal/domain"
)
@@ -30,7 +31,7 @@ func (m *mockBulkRenewalService) BulkRenew(ctx context.Context, criteria domain.
// bulk-renew is NOT admin-gated, any authenticated caller can use it.
func authedContext() context.Context {
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id-renew")
ctx = context.WithValue(ctx, middleware.UserKey{}, "alice")
ctx = context.WithValue(ctx, auth.UserKey{}, "alice")
return ctx
}
@@ -126,7 +127,7 @@ func TestBulkRenew_Handler_ActorAttribution(t *testing.T) {
h.BulkRenew(w, req)
if capturedActor != "alice" {
t.Errorf("actor not threaded from middleware.UserKey: got %q, want 'alice'", capturedActor)
t.Errorf("actor not threaded from auth.UserKey: got %q, want 'alice'", capturedActor)
}
}
+3 -2
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
"github.com/certctl-io/certctl/internal/domain"
)
@@ -53,7 +54,7 @@ func (h BulkRevocationHandler) BulkRevoke(w http.ResponseWriter, r *http.Request
// M-003: admin-only gate. Non-admin callers are rejected before any
// criteria/body processing to avoid leaking validation behavior to
// unauthorized actors.
if !middleware.IsAdmin(r.Context()) {
if !auth.IsAdmin(r.Context()) {
ErrorWithRequestID(w, http.StatusForbidden,
"Bulk revocation requires admin privileges",
requestID)
@@ -127,7 +128,7 @@ func (h BulkRevocationHandler) BulkRevokeEST(w http.ResponseWriter, r *http.Requ
return
}
requestID := middleware.GetRequestID(r.Context())
if !middleware.IsAdmin(r.Context()) {
if !auth.IsAdmin(r.Context()) {
ErrorWithRequestID(w, http.StatusForbidden,
"EST bulk revocation requires admin privileges", requestID)
return
@@ -11,6 +11,7 @@ import (
"testing"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
"github.com/certctl-io/certctl/internal/domain"
)
@@ -31,7 +32,7 @@ func (m *mockBulkRevocationService) BulkRevoke(ctx context.Context, criteria dom
// M-003: bulk revocation handler requires admin context to reach the service.
func adminContext() context.Context {
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id-bulk")
ctx = context.WithValue(ctx, middleware.AdminKey{}, true)
ctx = context.WithValue(ctx, auth.AdminKey{}, true)
return ctx
}
@@ -243,7 +244,7 @@ func TestBulkRevoke_AdminExplicitFalse_Returns403(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, false)
ctx = context.WithValue(ctx, auth.AdminKey{}, false)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@@ -273,8 +274,8 @@ func TestBulkRevoke_AdminPermitted_ForwardsActor(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
ctx := context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id")
ctx = context.WithValue(ctx, middleware.AdminKey{}, true)
ctx = context.WithValue(ctx, middleware.UserKey{}, "ops-admin")
ctx = context.WithValue(ctx, auth.AdminKey{}, true)
ctx = context.WithValue(ctx, auth.UserKey{}, "ops-admin")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
+3 -3
View File
@@ -6,7 +6,7 @@ import (
"net/http"
"time"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
)
// HealthHandler handles health and readiness check endpoints.
@@ -152,8 +152,8 @@ func (h HealthHandler) AuthInfo(w http.ResponseWriter, r *http.Request) {
func (h HealthHandler) AuthCheck(w http.ResponseWriter, r *http.Request) {
response := map[string]interface{}{
"status": "authenticated",
"user": middleware.GetUser(r.Context()),
"admin": middleware.IsAdmin(r.Context()),
"user": auth.GetUser(r.Context()),
"admin": auth.IsAdmin(r.Context()),
}
JSON(w, http.StatusOK, response)
}
+5 -5
View File
@@ -9,7 +9,7 @@ import (
"testing"
"time"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
_ "github.com/lib/pq" // Bundle-5 / H-006: postgres driver for /ready DB-probe regression test
)
@@ -238,8 +238,8 @@ func TestAuthCheck_AdminCaller_ReportsAdminTrue(t *testing.T) {
handler := NewHealthHandler("api-key", nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/check", nil)
ctx := context.WithValue(req.Context(), middleware.AdminKey{}, true)
ctx = context.WithValue(ctx, middleware.UserKey{}, "ops-admin")
ctx := context.WithValue(req.Context(), auth.AdminKey{}, true)
ctx = context.WithValue(ctx, auth.UserKey{}, "ops-admin")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@@ -276,8 +276,8 @@ func TestAuthCheck_NonAdminCaller_ReportsAdminFalse(t *testing.T) {
handler := NewHealthHandler("api-key", nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/check", nil)
ctx := context.WithValue(req.Context(), middleware.AdminKey{}, false)
ctx = context.WithValue(ctx, middleware.UserKey{}, "alice")
ctx := context.WithValue(req.Context(), auth.AdminKey{}, false)
ctx = context.WithValue(ctx, auth.UserKey{}, "alice")
req = req.WithContext(ctx)
w := httptest.NewRecorder()
+8 -7
View File
@@ -9,6 +9,7 @@ import (
"time"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
"github.com/certctl-io/certctl/internal/crypto/signer"
"github.com/certctl-io/certctl/internal/domain"
"github.com/certctl-io/certctl/internal/service"
@@ -36,7 +37,7 @@ type IntermediateCAServicer interface {
// All routes are pinned at /api/v1/issuers/{id}/intermediates and
// /api/v1/intermediates/{id}.
//
// Admin gate: every method calls middleware.IsAdmin first and surfaces
// Admin gate: every method calls auth.IsAdmin first and surfaces
// HTTP 403 for non-admin Bearer callers (M-003 admin-gating pattern,
// matches AdminCRLCacheHandler / AdminESTHandler / AdminSCEPIntuneHandler).
// CA hierarchy management is a high-blast-radius surface — adding a
@@ -111,7 +112,7 @@ func (h IntermediateCAHandler) Create(w http.ResponseWriter, r *http.Request) {
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !middleware.IsAdmin(r.Context()) {
if !auth.IsAdmin(r.Context()) {
Error(w, http.StatusForbidden, "Admin access required")
return
}
@@ -122,7 +123,7 @@ func (h IntermediateCAHandler) Create(w http.ResponseWriter, r *http.Request) {
ErrorWithRequestID(w, http.StatusBadRequest, "issuer id required", requestID)
return
}
actor, _ := r.Context().Value(middleware.UserKey{}).(string)
actor, _ := r.Context().Value(auth.UserKey{}).(string)
if actor == "" {
ErrorWithRequestID(w, http.StatusUnauthorized,
"authentication required", requestID)
@@ -211,7 +212,7 @@ func (h IntermediateCAHandler) List(w http.ResponseWriter, r *http.Request) {
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !middleware.IsAdmin(r.Context()) {
if !auth.IsAdmin(r.Context()) {
Error(w, http.StatusForbidden, "Admin access required")
return
}
@@ -237,7 +238,7 @@ func (h IntermediateCAHandler) Get(w http.ResponseWriter, r *http.Request) {
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !middleware.IsAdmin(r.Context()) {
if !auth.IsAdmin(r.Context()) {
Error(w, http.StatusForbidden, "Admin access required")
return
}
@@ -270,7 +271,7 @@ func (h IntermediateCAHandler) Retire(w http.ResponseWriter, r *http.Request) {
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
return
}
if !middleware.IsAdmin(r.Context()) {
if !auth.IsAdmin(r.Context()) {
Error(w, http.StatusForbidden, "Admin access required")
return
}
@@ -281,7 +282,7 @@ func (h IntermediateCAHandler) Retire(w http.ResponseWriter, r *http.Request) {
ErrorWithRequestID(w, http.StatusBadRequest, "id required", requestID)
return
}
actor, _ := r.Context().Value(middleware.UserKey{}).(string)
actor, _ := r.Context().Value(auth.UserKey{}).(string)
if actor == "" {
ErrorWithRequestID(w, http.StatusUnauthorized,
"authentication required", requestID)
+5 -5
View File
@@ -16,7 +16,7 @@ import (
"testing"
"time"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
"github.com/certctl-io/certctl/internal/domain"
"github.com/certctl-io/certctl/internal/service"
)
@@ -80,8 +80,8 @@ func (m *mockIntermediateCAService) LoadHierarchy(ctx context.Context, issuerID
// authenticated user — the standard "admin caller" shape for these
// tests.
func withAdmin(actor string, admin bool) context.Context {
ctx := context.WithValue(context.Background(), middleware.UserKey{}, actor)
ctx = context.WithValue(ctx, middleware.AdminKey{}, admin)
ctx := context.WithValue(context.Background(), auth.UserKey{}, actor)
ctx = context.WithValue(ctx, auth.AdminKey{}, admin)
return ctx
}
@@ -177,8 +177,8 @@ func TestIntermediateCA_Handler_AdminExplicitFalse_Returns403(t *testing.T) {
bytes.NewReader([]byte(`{"name":"r"}`)))
req.SetPathValue("id", "iss-1")
// AdminKey explicitly set to false — distinct from missing key.
ctx := context.WithValue(context.Background(), middleware.UserKey{}, "alice")
ctx = context.WithValue(ctx, middleware.AdminKey{}, false)
ctx := context.WithValue(context.Background(), auth.UserKey{}, "alice")
ctx = context.WithValue(ctx, auth.AdminKey{}, false)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
h.Create(w, req)
+8 -8
View File
@@ -14,19 +14,19 @@ import (
//
// The audit's request is "Admin-gated operation role-gate test coverage
// needs verification". Verified-already-clean recon: only one handler
// in internal/api/handler/ calls middleware.IsAdmin to gate access:
// in internal/api/handler/ calls auth.IsAdmin to gate access:
// bulk_revocation.go — which has 3 dedicated tests
// (NonAdmin_Returns403, AdminExplicitFalse_Returns403,
// AdminPermitted_ForwardsActor) covering all three branches.
//
// This test enforces the invariant going forward by walking every
// .go file in this package, finding every middleware.IsAdmin call
// .go file in this package, finding every auth.IsAdmin call
// site, and asserting the file appears in AdminGatedHandlers below.
// Adding a new middleware.IsAdmin call without updating the constant
// Adding a new auth.IsAdmin call without updating the constant
// AND adding a parallel test triplet fails CI.
// AdminGatedHandlers is the documented allowlist of handler files that
// gate access on middleware.IsAdmin. Every entry MUST have:
// gate access on auth.IsAdmin. Every entry MUST have:
// - a non-admin-rejection test ("_NonAdmin_Returns403")
// - an explicit-false-admin-rejection test ("_AdminExplicitFalse_Returns403")
// - an admin-allowed actor-attribution test ("_AdminPermitted_ForwardsActor")
@@ -43,7 +43,7 @@ var AdminGatedHandlers = map[string]string{
}
// InformationalIsAdminCallers is the documented allowlist of files that
// call middleware.IsAdmin without using the result to gate access. The
// call auth.IsAdmin without using the result to gate access. The
// only legitimate use of an informational call is reporting the flag to
// a downstream consumer (e.g. health.go::AuthCheck reports admin to the
// GUI so it can hide admin-only buttons).
@@ -64,7 +64,7 @@ func TestM008_AdminGatedHandlers_PinExpectedSet(t *testing.T) {
if !slicesEqual008(actual, expected) {
t.Errorf(
"middleware.IsAdmin call sites changed:\n"+
"auth.IsAdmin call sites changed:\n"+
" actual: %v\n"+
" expected: %v\n"+
"\n"+
@@ -143,10 +143,10 @@ func scanIsAdminCallers(dir string) ([]string, error) {
if parseErr != nil {
continue
}
// Substring-match middleware.IsAdmin — cheap and sufficient
// Substring-match auth.IsAdmin — cheap and sufficient
// because the import path is fixed and there's no aliasing
// shenanigans elsewhere in this package.
if strings.Contains(string(body), "middleware.IsAdmin(") {
if strings.Contains(string(body), "auth.IsAdmin(") {
out = append(out, name)
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ import (
"strings"
"time"
"github.com/certctl-io/certctl/internal/api/middleware"
"github.com/certctl-io/certctl/internal/auth"
)
// resolveActor extracts the authenticated named-key identity from the request
@@ -23,7 +23,7 @@ import (
// or "api" — always go through this helper so the named-key identity flows to
// services and the audit trail.
func resolveActor(ctx context.Context) string {
if user := middleware.GetUser(ctx); user != "" {
if user := auth.GetUser(ctx); user != "" {
return user
}
return "api"