Foundational identity and permission types for role-based access control — bit-set PermissionMask, immutable Identity value type, and PermissionProvider interface. What's included: - `Identity` value type with NewIdentity / WithTenant constructors and SetInContext / FromContext context helpers - `Permission` (int64 bit position) and `PermissionMask` (int64 bit-set) with O(1) Has and non-mutating Grant - `PermissionProvider` interface for DB-backed ResolveMask(ctx, uid, resource) resolution Tested-via: todo-api POC integration Reviewed-against: docs/adr/
2.1 KiB
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:
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:
NewIdentity(uid, displayName, email)— populated from the auth token.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; theboolreturn ofFromContextdistinguishes "not authenticated" from "authenticated with empty fields". - Concurrent middleware cannot accidentally mutate an
Identityalready stored in context — each enrichment step produces a new value. - Copying
Identityon everyWithTenantcall is negligible: four string fields, each a pointer-and-length pair internally. - The compliance test enforces that
WithTenantreturnsIdentity(not*Identity) at compile time, preventing a future regression to a pointer receiver.