Files
httpauth/claims_provider.go
Rene Nochebuena 601e6a5144 fix(httpauth): wildcard resource fallback, json.Number precision, xerrors JSON error bodies (#1)
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.

Reviewed-on: #1
Co-authored-by: Rene Nochebuena Guerrero <rene@nochebuena.dev>
Co-committed-by: Rene Nochebuena Guerrero <rene@nochebuena.dev>
2026-05-18 14:15:12 -06:00

50 lines
1.4 KiB
Go

package httpauth
import (
"context"
"encoding/json"
"code.nochebuena.dev/go/rbac"
)
type claimsPermissionProvider struct {
claimsKey string
}
// NewClaimsPermissionProvider returns an rbac.PermissionProvider that reads
// pre-computed permission masks from JWT claims stored in the request context
// by SetTokenData. Expects claims[claimsKey] to be a map[string]any where each
// key is a resource name and the value is the bitmask as int64 or float64
// (JSON unmarshaling decodes numbers as float64).
// Returns 0 without error if the claim is absent — callers treat 0 as no access.
func NewClaimsPermissionProvider(claimsKey string) rbac.PermissionProvider {
return &claimsPermissionProvider{claimsKey: claimsKey}
}
func (p *claimsPermissionProvider) ResolveMask(ctx context.Context, _, resource string) (rbac.PermissionMask, error) {
claims, ok := getClaims(ctx)
if !ok {
return 0, nil
}
masks, ok := claims[p.claimsKey].(map[string]any)
if !ok {
return 0, nil
}
// Check specific resource first, then wildcard fallback.
for _, key := range []string{resource, "*"} {
switch v := masks[key].(type) {
case int64:
return rbac.PermissionMask(v), nil
case float64:
return rbac.PermissionMask(int64(v)), nil
case json.Number:
n, err := v.Int64()
if err != nil {
return 0, nil
}
return rbac.PermissionMask(n), nil
}
}
return 0, nil
}