fix(httpauth): wildcard resource fallback, json.Number precision, xerrors JSON error bodies
ClaimsPermissionProvider.ResolveMask now falls back to the "*" resource key when the
exact resource is absent in the JWT masks claim. Specific resource takes precedence;
wildcard is the fallback. Enables ADMIN wildcard masks ({"*": MaxInt64}) to pass every
endpoint guard without per-resource entries.
Add json.Number handling alongside existing int64/float64 paths. json.Number is
produced by jwt.WithJSONNumber() (httpauth-jwt) and preserves math.MaxInt64 exactly —
float64 would round to 2^63 and overflow on cast back to int64.
Export WriteJSONError(w, status, code, message) — shared JSON error helper for all
httpauth-* provider packages. Ensures a consistent {"code":"...","message":"..."} body
and Content-Type: application/json across the full middleware stack.
AuthzMiddleware and EnrichmentMiddleware now use WriteJSONError with xerrors code
constants (UNAUTHENTICATED, PERMISSION_DENIED, INTERNAL) instead of http.Error which
writes text/plain. AuthzMiddleware also fixes a wrong code string: UNAUTHORIZED →
UNAUTHENTICATED (stable gRPC-aligned name, matching xerrors.ErrUnauthorized).
Add xerrors v1.0.1 as a direct dependency for the code constants.
This commit is contained in:
110
httpauth_test.go
110
httpauth_test.go
@@ -2,7 +2,9 @@ package httpauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -348,3 +350,111 @@ func TestChainPermissionProvider_ErrorPropagates(t *testing.T) {
|
||||
t.Error("expected error from first provider, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// --- ClaimsPermissionProvider wildcard + json.Number ---
|
||||
|
||||
func TestClaimsPermissionProvider_WildcardFallback(t *testing.T) {
|
||||
p := NewClaimsPermissionProvider("permisos")
|
||||
ctx := SetTokenData(context.Background(), "uid1", map[string]any{
|
||||
"permisos": map[string]any{"*": int64(515)},
|
||||
})
|
||||
mask, err := p.ResolveMask(ctx, "uid1", "cualquier_recurso")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if mask != 515 {
|
||||
t.Errorf("want 515 from wildcard, got %d", mask)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimsPermissionProvider_SpecificBeatsWildcard(t *testing.T) {
|
||||
p := NewClaimsPermissionProvider("permisos")
|
||||
ctx := SetTokenData(context.Background(), "uid1", map[string]any{
|
||||
"permisos": map[string]any{
|
||||
"system": int64(42),
|
||||
"*": int64(999),
|
||||
},
|
||||
})
|
||||
mask, err := p.ResolveMask(ctx, "uid1", "system")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if mask != 42 {
|
||||
t.Errorf("want 42 from specific key, got %d (wildcard must not override specific)", mask)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaimsPermissionProvider_JSONNumber(t *testing.T) {
|
||||
p := NewClaimsPermissionProvider("permisos")
|
||||
ctx := SetTokenData(context.Background(), "uid1", map[string]any{
|
||||
"permisos": map[string]any{"system": json.Number("9223372036854775807")},
|
||||
})
|
||||
mask, err := p.ResolveMask(ctx, "uid1", "system")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if int64(mask) != math.MaxInt64 {
|
||||
t.Errorf("want MaxInt64 (%d), got %d", int64(math.MaxInt64), int64(mask))
|
||||
}
|
||||
}
|
||||
|
||||
// --- JSON error body assertions ---
|
||||
|
||||
func assertJSONError(t *testing.T, rec *httptest.ResponseRecorder, wantStatus int, wantCode string) {
|
||||
t.Helper()
|
||||
if rec.Code != wantStatus {
|
||||
t.Errorf("want status %d, got %d", wantStatus, rec.Code)
|
||||
}
|
||||
if ct := rec.Header().Get("Content-Type"); ct != "application/json" {
|
||||
t.Errorf("want Content-Type application/json, got %q", ct)
|
||||
}
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("response body is not valid JSON: %v", err)
|
||||
}
|
||||
if body["code"] != wantCode {
|
||||
t.Errorf("want code %q, got %q", wantCode, body["code"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthzMiddleware_ForbiddenJSON(t *testing.T) {
|
||||
mp := &mockProvider{mask: rbac.PermissionMask(0)}
|
||||
h := AuthzMiddleware(mp, "resource", testPerm)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
ctx := rbac.SetInContext(req.Context(), rbac.NewIdentity("uid1", "", ""))
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req.WithContext(ctx))
|
||||
assertJSONError(t, rec, http.StatusForbidden, "PERMISSION_DENIED")
|
||||
}
|
||||
|
||||
func TestAuthzMiddleware_UnauthorizedJSON(t *testing.T) {
|
||||
mp := &mockProvider{}
|
||||
h := AuthzMiddleware(mp, "resource", testPerm)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
assertJSONError(t, rec, http.StatusUnauthorized, "UNAUTHENTICATED")
|
||||
}
|
||||
|
||||
func TestEnrichmentMiddleware_NoUIDJSON(t *testing.T) {
|
||||
h := EnrichmentMiddleware(&mockEnricher{})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
assertJSONError(t, rec, http.StatusUnauthorized, "UNAUTHENTICATED")
|
||||
}
|
||||
|
||||
func TestEnrichmentMiddleware_EnricherErrorJSON(t *testing.T) {
|
||||
me := &mockEnricher{err: errors.New("db error")}
|
||||
inner := EnrichmentMiddleware(me)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
h := injectTokenData("uid1", nil, inner)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
assertJSONError(t, rec, http.StatusInternalServerError, "INTERNAL")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user