Files
rbac/docs/adr/ADR-002-identity-value-type.md

53 lines
2.1 KiB
Markdown
Raw Permalink Normal View History

# ADR-002: Identity as a Value Type
**Status:** Accepted
**Date:** 2026-03-18
## Context
`Identity` travels through the request context from authentication middleware to
handler and downstream service calls. When a type travels via `context.WithValue`,
callers retrieve it with a type assertion. If the stored type were a pointer
(`*Identity`), any middleware could mutate the struct fields after the value was
placed in context, causing non-obvious bugs in concurrent or pipelined middleware
chains. Additionally, pointer types retrieved from context require nil checks at
every retrieval site.
## Decision
`Identity` is declared as a plain struct, not a pointer:
```go
type Identity struct {
UID string
TenantID string
DisplayName string
Email string
}
```
`NewIdentity` returns `Identity` (value), not `*Identity`. `SetInContext` stores the
value directly. `FromContext` retrieves it as a value and returns `(Identity, bool)`;
the boolean indicates absence without requiring a nil pointer check.
Enrichment methods return new values rather than mutating the receiver.
`WithTenant(id string) Identity` copies the receiver, sets `TenantID` on the copy,
and returns the copy. The original is unchanged, making it safe to call from
concurrent middleware.
The two-step construction pattern is intentional:
1. `NewIdentity(uid, displayName, email)` — populated from the auth token.
2. `id.WithTenant(tenantID)` — optionally enriched by a tenant-resolution middleware.
## Consequences
- No nil checks are needed at retrieval sites. A zero-value `Identity{}` is returned
when no identity is in context; the `bool` return of `FromContext` distinguishes
"not authenticated" from "authenticated with empty fields".
- Concurrent middleware cannot accidentally mutate an `Identity` already stored in
context — each enrichment step produces a new value.
- Copying `Identity` on every `WithTenant` call is negligible: four string fields,
each a pointer-and-length pair internally.
- The compliance test enforces that `WithTenant` returns `Identity` (not `*Identity`)
at compile time, preventing a future regression to a pointer receiver.