Files
auth/docs/adr/index.md

70 lines
4.6 KiB
Markdown
Raw Permalink Normal View History

feat(auth): initial implementation — authmw and rbac (v1.0.0) Introduces code.nochebuena.dev/einherjar/auth — the provider-agnostic HTTP authentication and authorization layer of the Einherjar framework. Absorbs two micro-lib packages (httpauth, rbac) as sub-packages, replacing the Identity-only context model with a SecurityBag-native design and adding a composable enrichment chain. authmw: - BagEnricher function type — enriches the request-scoped SecurityBag after the base Identity is built; registered via WithBagEnricher; multiple enrichers run in order, each receiving the bag from the previous - IdentityEnricher interface — application-layer contract for loading user data from uid+claims - EnrichmentMiddleware — builds SecurityBag from uid+claims, runs enricher chain, stores via security.SetBagInContext; 401 on missing uid, 500 on enricher error; routes all errors through httputil.Error - AuthzMiddleware — per-route permission gate; 401 on missing identity, 403 on provider error (fail-closed) or insufficient permissions - EnrichOpt type + WithTenantHeader (reads TenantID from header, implemented as a BagEnricher) + WithBagEnricher (registers custom enrichers for hardware IDs, grant codes, or any bag attribute) - SetTokenData / GetClaims — integration contract for auth-jwt / auth-firebase rbac: - NewClaimsPermissionProvider — reads flat JWT claim bitmasks from context; wildcard "*" fallback; handles int64/float64/json.Number; zero DB calls - NewCachedPermissionProvider — TTL cache wrapping any PermissionProvider; default key "rbac:{uid}:{resource}" or "rbac:{tenantID}:{uid}:{resource}"; TenantID sourced from SecurityBag automatically; accepts ...CachedOpt - CachedOpt type + WithCacheKey — overrides the key function for extra dimensions (hardware IDs, grant codes read from bag attributes) - NewChainPermissionProvider — tries providers in order; first non-zero wins; errors short-circuit; typical pattern: claims → cached DB fallback - Cache interface — pluggable backend satisfied by cache-valkey via duck typing Compliance test (package auth_test) enforces CT-6 (≤1 exported TypeSpec/file), compile-time interface satisfaction, and behavioural coverage across the full middleware and provider surface: enrichment success/failure, tenant header, custom BagEnricher, bag-in-context, authz allowed/denied/error, claims hit/wildcard/missing/float64, cached hit/miss/error/tenant-key/custom-key, chain first-non-zero/fallthrough/error. Depends on contracts v1.0.0, core v1.0.0, web v1.0.0. - identifiable.go: package-level Module variable (observability.Identifiable) for version identification — auth is middleware-only; not registered with the launcher
2026-05-29 16:11:21 +00:00
# Architecture Decision Records — `einherjar/auth`
## ADR-001: No root factory
**Status:** Accepted
**Context:** Previous starters (`web`) expose a root `New(logger, ...Config)` factory that pre-wires a complete, opinionated stack for the common case. The same pattern was evaluated for `auth`.
**Decision:** `auth` exposes no root factory. The module is composition-only.
**Rationale:** `web.New` works because the right middleware stack is always the same (Recover → RequestID → RequestLogger → CORS). Auth has no equivalent universal default:
- The auth provider is application-specific (JWT vs Firebase vs custom)
- The `IdentityEnricher` is always application code
- `AuthzMiddleware` is per-route, not global
- The `PermissionProvider` chain depends on the application's permission model
A root factory would either be a thin wrapper (adds no value) or force premature architectural choices on the caller.
---
## ADR-002: Logger added to both middleware functions
**Status:** Accepted
**Context:** micro-lib's `EnrichmentMiddleware` and `AuthzMiddleware` were logger-free — errors were written as HTTP responses with no log emission.
**Decision:** Both middleware functions accept `logging.Logger` and route all errors through `httputil.Error(logger, w, r, err)`.
**Rationale:**
- An `IdentityEnricher` returning a 500 is a server-side failure. It must be logged — silent 500s are invisible in production.
- `httputil.Error` is the single error-processing point for the framework. Using it in auth keeps error response shape, log levels, and field enrichment identical to application handlers.
- 401 (missing token) and 403 (permission denied) log at Warn — client errors, not operator alerts.
- Log level routing is already handled by `httputil.Error`; no per-middleware logic needed.
---
## ADR-003: auth depends on web for httputil.Error
**Status:** Accepted
**Context:** auth error responses could be written independently (custom JSON write, like micro-lib's `WriteJSONError`) to avoid a dependency on `web`.
**Decision:** `auth` depends on `code.nochebuena.dev/einherjar/web` solely for `httputil.Error`.
**Rationale:**
- Consistent error shape across the entire API surface — auth errors and handler errors are indistinguishable to clients.
- `httputil.Error` handles log level routing, error code extraction, and platform code inclusion. Duplicating this in `auth` would create two implementations that drift over time.
- The dependency is one-directional: `web` does not depend on `auth`. The dependency graph remains acyclic.
**Trade-off accepted:** `auth` cannot be used without `web`. This is acceptable — `auth` is an HTTP middleware layer and `web` is the HTTP layer. Separating them was never a goal.
---
## ADR-004: BagEnricher chain over arbitrary context injection
**Status:** Accepted
**Context:** The original design used `WithTenantHeader` as the sole enrichment option, storing additional attributes via arbitrary `context.WithValue` calls. This made enrichment opaque — permission providers had no structured way to access request attributes like hardware IDs or grant codes without knowing the exact context key used elsewhere.
**Decision:** Enrichment is modeled as a chain of `BagEnricher` functions that transform a `security.SecurityBag`. `EnrichmentMiddleware` creates the bag from the base Identity, runs all registered `BagEnricher` functions in order, and stores the final bag via `security.SetBagInContext`. Permission providers retrieve the bag once with `security.BagFromContext(ctx)` and have access to all enriched attributes via `bag.Get(key)`.
**Rationale:**
- Single structured object per request instead of scattered `context.WithValue` entries; easier to reason about what's in the security context at any point in the chain.
- Permission providers access any bag attribute in one call rather than knowing which context key to look up.
- `WithCacheKey` on `CachedPermissionProvider` receives the full bag — no new parameters needed to include extra dimensions (hardware IDs, grant codes) in the cache key.
- `WithTenantHeader` is now a thin wrapper over `WithBagEnricher`, so the same extension point covers all enrichment use cases.
- Framework-owned attributes remain typed fields on `Identity`. Only application-defined attributes go into the bag's string-keyed map. Callers define their own key constants to maintain compile-time safety at the type-assertion call site.
**Trade-off accepted:** Bag attributes are `any` — type assertions are required at read sites. This is inherent to a string-keyed heterogeneous map. The contract is: the package that writes an attribute owns the constant and documents the type; callers that read it perform the assertion.