Files
rbac/docs/adr/ADR-002-identity-value-type.md
Rene Nochebuena 0864f031a1 feat(rbac): initial stable release v0.9.0
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/
2026-03-18 13:25:43 -06:00

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:

  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.