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>
69 lines
2.0 KiB
Go
69 lines
2.0 KiB
Go
package httpauth
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"code.nochebuena.dev/go/rbac"
|
|
"code.nochebuena.dev/go/xerrors"
|
|
)
|
|
|
|
// IdentityEnricher builds an rbac.Identity from verified token data.
|
|
// The application provides the implementation — typically reads from a user store.
|
|
type IdentityEnricher interface {
|
|
Enrich(ctx context.Context, uid string, claims map[string]any) (rbac.Identity, error)
|
|
}
|
|
|
|
// EnrichOpt configures EnrichmentMiddleware.
|
|
type EnrichOpt func(*enrichConfig)
|
|
|
|
type enrichConfig struct {
|
|
tenantHeader string
|
|
}
|
|
|
|
// WithTenantHeader configures the request header from which TenantID is read.
|
|
// If the header is absent on a request, TenantID remains "" — no error is returned.
|
|
func WithTenantHeader(header string) EnrichOpt {
|
|
return func(c *enrichConfig) {
|
|
c.tenantHeader = header
|
|
}
|
|
}
|
|
|
|
// EnrichmentMiddleware reads uid + claims injected 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 (SetTokenData was not called upstream).
|
|
// Returns 500 if the enricher fails.
|
|
func EnrichmentMiddleware(enricher IdentityEnricher, opts ...EnrichOpt) func(http.Handler) http.Handler {
|
|
cfg := &enrichConfig{}
|
|
for _, o := range opts {
|
|
o(cfg)
|
|
}
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
uid, ok := getUID(r.Context())
|
|
if !ok {
|
|
WriteJSONError(w, http.StatusUnauthorized, string(xerrors.ErrUnauthorized), "unauthorized")
|
|
return
|
|
}
|
|
|
|
claims, _ := getClaims(r.Context())
|
|
|
|
identity, err := enricher.Enrich(r.Context(), uid, claims)
|
|
if err != nil {
|
|
WriteJSONError(w, http.StatusInternalServerError, string(xerrors.ErrInternal), "internal server error")
|
|
return
|
|
}
|
|
|
|
if cfg.tenantHeader != "" {
|
|
if tenantID := r.Header.Get(cfg.tenantHeader); tenantID != "" {
|
|
identity = identity.WithTenant(tenantID)
|
|
}
|
|
}
|
|
|
|
ctx := rbac.SetInContext(r.Context(), identity)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
}
|