-
Release v1.0.0 Stable
released this
2026-05-29 10:11:48 -06:00 | 0 commits to main since this releasev1.0.0
code.nochebuena.dev/einherjar/auth
Architecture Decisions Resolved
Decision Outcome Root factory Excluded — auth is composition-only; no universal default stack exists (see ADR-001) Logger in middleware Added — routes through httputil.Errorfor consistent log levels and response shape (see ADR-002)web dependency Accepted — auth depends on web solely for httputil.Error(see ADR-003)Enrichment model BagEnricherchain over bag — flexible, multi-attribute enrichment without untyped context scatter (see ADR-004)AuthzMiddleware on provider error Fail-closed → 403 — provider failure denies access Cache key for multi-tenant TenantID auto-included from SecurityBag; WithCacheKeyfor extra dimensionsClaimsPermissionProvider claims format Flat map: claims[key][resource] = mask; wildcard"*"fallbackGetClaims visibility Exported from authmw — used by ClaimsPermissionProvider and custom enrichers
API
Package sentinel
import "code.nochebuena.dev/einherjar/auth" // Module identifies this package to observability systems. // auth is a function-only package — not registered with the launcher as a lifecycle component. // Register Module manually with any version registry if needed. var Module observability.Identifiableauthmwimport "code.nochebuena.dev/einherjar/auth/authmw" // Function type — registered via WithBagEnricher type BagEnricher func(bag security.SecurityBag, r *http.Request) security.SecurityBag // Interface (application implements) type IdentityEnricher interface { Enrich(ctx context.Context, uid string, claims map[string]any) (security.Identity, error) } // Option type type EnrichOpt func(*enrichConfig) func WithTenantHeader(header string) EnrichOpt func WithBagEnricher(fn BagEnricher) EnrichOpt // Integration contract — called by auth-jwt / auth-firebase after token verification func SetTokenData(ctx context.Context, uid string, claims map[string]any) context.Context // Accessor for raw claims stored by SetTokenData; nil if not present func GetClaims(ctx context.Context) map[string]any // Middleware constructors func EnrichmentMiddleware(logger logging.Logger, enricher IdentityEnricher, opts ...EnrichOpt) func(http.Handler) http.Handler func AuthzMiddleware(logger logging.Logger, provider security.PermissionProvider, resource string, required security.Permission) func(http.Handler) http.HandlerEnrichmentMiddleware behaviour:
- Missing uid in context → 401
UNAUTHENTICATED - Enricher error → 500
INTERNAL(logged at Error) - BagEnrichers run in registration order after the base Identity is built
- Bag stored via
security.SetBagInContext— downstream reads viaBagFromContextorFromContext
AuthzMiddleware behaviour:
- No identity in context → 401
UNAUTHENTICATED - Provider error → 403
PERMISSION_DENIED(fail-closed) !mask.Has(required)→ 403PERMISSION_DENIED
rbacimport "code.nochebuena.dev/einherjar/auth/rbac" // Interface (cache-valkey implements via duck typing) type Cache interface { Get(ctx context.Context, key string) (int64, bool, error) Set(ctx context.Context, key string, value int64, ttl time.Duration) error } // Option type type CachedOpt func(*cachedConfig) func WithCacheKey(fn func(security.SecurityBag, string, string) string) CachedOpt // Constructors (all return security.PermissionProvider) func NewClaimsPermissionProvider(claimsKey string) security.PermissionProvider func NewCachedPermissionProvider(inner security.PermissionProvider, cache Cache, ttl time.Duration, opts ...CachedOpt) security.PermissionProvider func NewChainPermissionProvider(providers ...security.PermissionProvider) security.PermissionProviderClaimsPermissionProvider:
- Reads
claims[claimsKey][resource]from context (stored bySetTokenData) - Wildcard fallback:
claims[claimsKey]["*"]when resource is absent - Handles int64, float64, json.Number — zero DB calls
CachedPermissionProvider:
- Default key:
"rbac:{uid}:{resource}"or"rbac:{tenantID}:{uid}:{resource}" - TenantID from
security.BagFromContext(ctx)— automatic WithCacheKeyoverrides for additional dimensions (hardware IDs, grant codes)- Cache miss / error → delegates to inner provider; set errors ignored (best-effort)
ChainPermissionProvider:
- Iterates providers in order; returns first non-zero mask
- Error in any provider short-circuits immediately
Wiring example
enricher := userservice.NewIdentityEnricher(userRepo) // Build permission chain: JWT claims fast-path, then DB with cache. permissions := rbac.NewChainPermissionProvider( rbac.NewClaimsPermissionProvider("perms"), rbac.NewCachedPermissionProvider(dbProvider, valkeyCache, 5*time.Minute), ) // Provider AuthMiddleware (auth-jwt / auth-firebase) runs first. // Then enrichment globally: srv.Use(authmw.EnrichmentMiddleware(logger, enricher, authmw.WithTenantHeader("X-Tenant-ID"), )) // Per-route authorization: const ReadOrders = security.Permission(0) srv.With(authmw.AuthzMiddleware(logger, permissions, "orders", ReadOrders)). Get("/orders", ordersHandler)
Install
go get code.nochebuena.dev/einherjar/auth@v1.0.0
Dependencies
Module Version Role code.nochebuena.dev/einherjar/contractsv1.0.0 SecurityBag, Identity, Permission, PermissionMask, PermissionProvider code.nochebuena.dev/einherjar/corev1.0.0 xerrors, logz code.nochebuena.dev/einherjar/webv1.0.0 httputil.Error (centralized error handler) Downloads
- Missing uid in context → 401