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>
6.3 KiB
6.3 KiB
Changelog
All notable changes to this module will be documented in this file.
The format is based on Keep a Changelog, and this module adheres to Semantic Versioning.
1.0.2 — 2026-05-18
Added
ClaimsPermissionProvider.ResolveMasknow supports a wildcard resource key"*"as a fallback when the requested resource is not present in the JWT masks claim. Specific resource key takes precedence; wildcard is consulted only when the exact key is absent. Primary use case:ADMINrole carries{"*": MaxInt64}— one JWT entry grants access to every resource without per-resource enumeration.ClaimsPermissionProvider.ResolveMasknow handlesjson.Numbermask values in addition to the existingint64andfloat64paths.json.Numberis produced byjwt.WithJSONNumber()(used inhttpauth-jwt); it preservesmath.MaxInt64exactly where float64 would overflow.WriteJSONError(w http.ResponseWriter, status int, code, message string)— exported helper that writes a structured JSON error body (Content-Type: application/json,{"code":"...","message":"..."}). Intended for use by allhttpauth-*provider packages so the error format is enforced in one place.
Fixed
AuthzMiddlewareandEnrichmentMiddlewarenow return JSON error bodies instead of plain-texthttp.Errorresponses. Error codes are stable gRPC-aligned names fromxerrors:UNAUTHENTICATED(401),PERMISSION_DENIED(403),INTERNAL(500). PreviouslyAuthzMiddlewarealso used the wrong codeUNAUTHORIZEDfor 401 responses (correct value isUNAUTHENTICATED).
Dependencies
- Added
code.nochebuena.dev/go/xerrors v1.0.1(for code constants in error responses).
1.0.0 — 2026-05-08
Added
NewChainPermissionProvider(providers ...rbac.PermissionProvider) rbac.PermissionProvider— tries each provider in order and returns the first non-zero mask; propagates errors immediately without consulting subsequent providers; primary use case is a JWT claims fast-path (ClaimsPermissionProvider) chained with a DB-backed fallback (CachedPermissionProvider)
Changed
- Dependency
code.nochebuena.dev/go/rbacbumped from v0.9.0 to v1.0.0
Unchanged
SetTokenData, EnrichmentMiddleware, AuthzMiddleware, IdentityEnricher,
WithTenantHeader, Cache, NewClaimsPermissionProvider, and
NewCachedPermissionProvider are API-compatible with v0.1.0.
0.1.0 - 2026-05-08
Added
SetTokenData(ctx, uid, claims) context.Context— injects verified uid and raw claims into the request context; called by provider-specific AuthMiddleware implementations (e.g.httpauth-firebase,httpauth-jwt) after token verification; downstream middleware reads these values via unexported helpers in the same packageIdentityEnricherinterface — application-implemented; receivesuid stringandclaims map[string]any, returnsrbac.Identity; called byEnrichmentMiddlewareon every authenticated requestEnrichOptfunctional option type for configuringEnrichmentMiddlewareWithTenantHeader(header string) EnrichOpt— reads a tenant ID from the named request header and attaches it to the identity viarbac.Identity.WithTenant; absent header leavesTenantIDas an empty string with no errorEnrichmentMiddleware(enricher IdentityEnricher, opts ...EnrichOpt) func(http.Handler) http.Handler— reads uid and claims stored by any upstream AuthMiddleware viaSetTokenData, callsenricher.Enrich, and stores the resultingrbac.Identityin context viarbac.SetInContext; returns 401 if no uid is present; returns 500 if the enricher failsAuthzMiddleware(provider rbac.PermissionProvider, resource string, required rbac.Permission) func(http.Handler) http.Handler— readsrbac.Identityfrom context viarbac.FromContext, resolves the permission mask via the providedrbac.PermissionProvider, and gates the request against the required permission bit; returns 401 if no identity is in context; returns 403 if the permission check fails or the provider returns an error; usesrbac.PermissionProviderdirectly without redefining itCacheinterface — abstracts the caching backend for permission masks;Get(ctx, key) (int64, bool, error)andSet(ctx, key, value, ttl) error; implementations are typically backed by Valkey or RedisNewCachedPermissionProvider(inner rbac.PermissionProvider, cache Cache, ttl time.Duration) rbac.PermissionProvider— wraps anyrbac.PermissionProviderwith a TTL-based cache layer; cache key format isrbac:{uid}:{resource}; on cache miss falls through to inner and populates the cache; on cache error falls through silently — never fails due to cache unavailabilityNewClaimsPermissionProvider(claimsKey string) rbac.PermissionProvider— reads pre-computed permission masks from JWT claims stored in the request context bySetTokenData; expectsclaims[claimsKey]to be amap[string]anywhere each key is a resource name and the value is the bitmask asint64orfloat64(JSON decodes numbers as float64); returns 0 without error if the claim is absent
Design Notes
AuthzMiddlewareusesrbac.PermissionProviderdirectly rather than redefining a local interface;rbacis the single source of truth for this contractEnrichmentMiddlewareandAuthzMiddlewareare provider-agnostic — they depend only onSetTokenDatahaving been called upstream; anyAuthMiddlewarethat callsSetTokenData(Firebase, JWT, API key, etc.) is compatible without changes to the enrichment or authorization layer- Two
rbac.PermissionProviderimplementations ship with this module for the two common architectures:ClaimsPermissionProviderfor simple applications that embed permissions in the JWT (no per-request DB or network call), andCachedPermissionProviderfor applications where the permission set is too large to embed or needs to be independently revocable CachedPermissionProvideruses TTL-based expiry exclusively; explicit invalidation is left to callers who can interact with theCachedirectly using therbac:{uid}:{resource}key format