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>
84 lines
6.3 KiB
Markdown
84 lines
6.3 KiB
Markdown
# Changelog
|
|
|
|
All notable changes to this module will be documented in this file.
|
|
|
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
|
## [1.0.2] — 2026-05-18
|
|
|
|
### Added
|
|
|
|
- `ClaimsPermissionProvider.ResolveMask` now 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: `ADMIN` role carries `{"*": MaxInt64}` — one JWT entry grants access
|
|
to every resource without per-resource enumeration.
|
|
- `ClaimsPermissionProvider.ResolveMask` now handles `json.Number` mask values in
|
|
addition to the existing `int64` and `float64` paths. `json.Number` is produced by
|
|
`jwt.WithJSONNumber()` (used in `httpauth-jwt`); it preserves `math.MaxInt64` exactly
|
|
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 all `httpauth-*` provider
|
|
packages so the error format is enforced in one place.
|
|
|
|
### Fixed
|
|
|
|
- `AuthzMiddleware` and `EnrichmentMiddleware` now return JSON error bodies instead of
|
|
plain-text `http.Error` responses. Error codes are stable gRPC-aligned names from
|
|
`xerrors`: `UNAUTHENTICATED` (401), `PERMISSION_DENIED` (403), `INTERNAL` (500).
|
|
Previously `AuthzMiddleware` also used the wrong code `UNAUTHORIZED` for 401 responses
|
|
(correct value is `UNAUTHENTICATED`).
|
|
|
|
### Dependencies
|
|
|
|
- Added `code.nochebuena.dev/go/xerrors v1.0.1` (for code constants in error responses).
|
|
|
|
[1.0.2]: https://code.nochebuena.dev/go/httpauth/releases/tag/v1.0.2
|
|
|
|
## [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/rbac` bumped 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.
|
|
|
|
[1.0.0]: https://code.nochebuena.dev/go/httpauth/releases/tag/v1.0.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 package
|
|
- `IdentityEnricher` interface — application-implemented; receives `uid string` and `claims map[string]any`, returns `rbac.Identity`; called by `EnrichmentMiddleware` on every authenticated request
|
|
- `EnrichOpt` functional option type for configuring `EnrichmentMiddleware`
|
|
- `WithTenantHeader(header string) EnrichOpt` — reads a tenant ID from the named request header and attaches it to the identity via `rbac.Identity.WithTenant`; absent header leaves `TenantID` as an empty string with no error
|
|
- `EnrichmentMiddleware(enricher IdentityEnricher, opts ...EnrichOpt) func(http.Handler) http.Handler` — reads uid and claims stored by any upstream AuthMiddleware via `SetTokenData`, calls `enricher.Enrich`, and stores the resulting `rbac.Identity` in context via `rbac.SetInContext`; returns 401 if no uid is present; returns 500 if the enricher fails
|
|
- `AuthzMiddleware(provider rbac.PermissionProvider, resource string, required rbac.Permission) func(http.Handler) http.Handler` — reads `rbac.Identity` from context via `rbac.FromContext`, resolves the permission mask via the provided `rbac.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; uses `rbac.PermissionProvider` directly without redefining it
|
|
- `Cache` interface — abstracts the caching backend for permission masks; `Get(ctx, key) (int64, bool, error)` and `Set(ctx, key, value, ttl) error`; implementations are typically backed by Valkey or Redis
|
|
- `NewCachedPermissionProvider(inner rbac.PermissionProvider, cache Cache, ttl time.Duration) rbac.PermissionProvider` — wraps any `rbac.PermissionProvider` with a TTL-based cache layer; cache key format is `rbac:{uid}:{resource}`; on cache miss falls through to inner and populates the cache; on cache error falls through silently — never fails due to cache unavailability
|
|
- `NewClaimsPermissionProvider(claimsKey string) rbac.PermissionProvider` — 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 decodes numbers as float64); returns 0 without error if the claim is absent
|
|
|
|
### Design Notes
|
|
|
|
- `AuthzMiddleware` uses `rbac.PermissionProvider` directly rather than redefining a local interface; `rbac` is the single source of truth for this contract
|
|
- `EnrichmentMiddleware` and `AuthzMiddleware` are provider-agnostic — they depend only on `SetTokenData` having been called upstream; any `AuthMiddleware` that calls `SetTokenData` (Firebase, JWT, API key, etc.) is compatible without changes to the enrichment or authorization layer
|
|
- Two `rbac.PermissionProvider` implementations ship with this module for the two common architectures: `ClaimsPermissionProvider` for simple applications that embed permissions in the JWT (no per-request DB or network call), and `CachedPermissionProvider` for applications where the permission set is too large to embed or needs to be independently revocable
|
|
- `CachedPermissionProvider` uses TTL-based expiry exclusively; explicit invalidation is left to callers who can interact with the `Cache` directly using the `rbac:{uid}:{resource}` key format
|
|
|
|
[0.1.0]: https://code.nochebuena.dev/go/httpauth/releases/tag/v0.1.0
|