feat(auth): add RequireAdmin middleware for /api/admin/* routes
Replaces the old X-API-Token-based RequireAdmin in middleware.go with a
context-aware RequireAdmin() that runs after RequireUser, checks
user.IsAdmin, and returns 403 {"error":"not_authorized"} for non-admins
or 500 {"error":"internal_error"} if RequireUser was bypassed. Updates
server.go to mount RequireUser then RequireAdmin on the /api/admin group.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
)
|
||||
|
||||
// injectUser returns a copy of r with the given user stored under userCtxKey.
|
||||
// This mirrors what RequireUser does at runtime.
|
||||
func injectUser(r *http.Request, u dbq.User) *http.Request {
|
||||
ctx := context.WithValue(r.Context(), userCtxKey, u)
|
||||
return r.WithContext(ctx)
|
||||
}
|
||||
|
||||
func TestRequireAdmin_AdminPasses(t *testing.T) {
|
||||
called := false
|
||||
stub := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
h := RequireAdmin()(stub)
|
||||
|
||||
req := injectUser(
|
||||
httptest.NewRequest(http.MethodGet, "/api/admin/test", nil),
|
||||
dbq.User{IsAdmin: true},
|
||||
)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want 200", w.Code)
|
||||
}
|
||||
if !called {
|
||||
t.Error("stub handler was not called for admin user")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_NonAdminReturns403(t *testing.T) {
|
||||
stub := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("stub handler must not be called for non-admin user")
|
||||
})
|
||||
|
||||
h := RequireAdmin()(stub)
|
||||
|
||||
req := injectUser(
|
||||
httptest.NewRequest(http.MethodGet, "/api/admin/test", nil),
|
||||
dbq.User{IsAdmin: false},
|
||||
)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("status = %d, want 403", w.Code)
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, "not_authorized") {
|
||||
t.Errorf("body %q does not contain %q", body, "not_authorized")
|
||||
}
|
||||
ct := w.Header().Get("Content-Type")
|
||||
if !strings.HasPrefix(ct, "application/json") {
|
||||
t.Errorf("Content-Type = %q, want application/json", ct)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_NoUserContextReturns500(t *testing.T) {
|
||||
stub := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("stub handler must not be called when no user is in context")
|
||||
})
|
||||
|
||||
h := RequireAdmin()(stub)
|
||||
|
||||
// Do NOT inject a user — simulate RequireUser being bypassed.
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/admin/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("status = %d, want 500", w.Code)
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, "internal_error") {
|
||||
t.Errorf("body %q does not contain %q", body, "internal_error")
|
||||
}
|
||||
ct := w.Header().Get("Content-Type")
|
||||
if !strings.HasPrefix(ct, "application/json") {
|
||||
t.Errorf("Content-Type = %q, want application/json", ct)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user