70 lines
4.6 KiB
Markdown
70 lines
4.6 KiB
Markdown
|
|
# 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.
|