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:
2026-05-18 14:14:04 -06:00
parent 9438983f32
commit fe6db9bef2
7 changed files with 183 additions and 11 deletions

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"code.nochebuena.dev/go/rbac"
"code.nochebuena.dev/go/xerrors"
)
// IdentityEnricher builds an rbac.Identity from verified token data.
@@ -42,7 +43,7 @@ func EnrichmentMiddleware(enricher IdentityEnricher, opts ...EnrichOpt) func(htt
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uid, ok := getUID(r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
WriteJSONError(w, http.StatusUnauthorized, string(xerrors.ErrUnauthorized), "unauthorized")
return
}
@@ -50,7 +51,7 @@ func EnrichmentMiddleware(enricher IdentityEnricher, opts ...EnrichOpt) func(htt
identity, err := enricher.Enrich(r.Context(), uid, claims)
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
WriteJSONError(w, http.StatusInternalServerError, string(xerrors.ErrInternal), "internal server error")
return
}