# rbac Foundational identity and permission types for role-based access control. ## Purpose `rbac` defines the `Identity` value type (authenticated principal), the `PermissionMask` bit-set type (resolved capabilities), and the `PermissionProvider` interface (DB-backed permission resolution). Every other module that needs to carry or inspect an authenticated identity imports this package; it has no dependencies of its own. ## Tier & Dependencies **Tier:** 0 **Imports:** `context` (stdlib only) **Must NOT import:** `logz`, `xerrors`, `launcher`, or any other micro-lib module. ## Key Design Decisions - Permissions are bit positions (0–62) packed into an `int64` mask for O(1) checks and compact storage. See `docs/adr/ADR-001-bitset-permissions.md`. - `Identity` is a value type — copied on every enrichment, never a pointer — to prevent nil bugs and accidental mutation from concurrent middleware. See `docs/adr/ADR-002-identity-value-type.md`. - `rbac` owns the context key for `Identity` (`authContextKey{}`). Any package that needs an identity imports only `rbac`, not an HTTP package. See `docs/adr/ADR-003-identity-context-ownership.md`. - `TenantID` is an optional enrichment field on `Identity`. Multi-tenancy is not modelled in the core; `PermissionProvider` implementations retrieve the tenant from `rbac.FromContext(ctx)` when needed. ## Patterns **Identity lifecycle:** ```go // Step 1: create from auth token (in httpauth middleware) id := rbac.NewIdentity(uid, displayName, email) // Step 2: optionally enrich with tenant (in tenant middleware) id = id.WithTenant(tenantID) // Store in context ctx = rbac.SetInContext(ctx, id) // Retrieve anywhere downstream id, ok := rbac.FromContext(ctx) if !ok { // no authenticated user in context } ``` **Defining application permissions:** ```go // In your domain package — define once, use everywhere const ( PermRead rbac.Permission = 0 PermWrite rbac.Permission = 1 PermDelete rbac.Permission = 2 ) ``` **Checking permissions:** ```go mask, err := provider.ResolveMask(ctx, id.UID, "orders") if err != nil { ... } if !mask.Has(PermWrite) { return xerrors.New(xerrors.ErrPermissionDenied, "write access required") } ``` **Building masks in tests:** ```go mask := rbac.PermissionMask(0).Grant(PermRead).Grant(PermWrite) ``` **In-memory PermissionProvider for tests:** ```go type staticProvider struct{ mask rbac.PermissionMask } func (p *staticProvider) ResolveMask(_ context.Context, _, _ string) (rbac.PermissionMask, error) { return p.mask, nil } ``` ## What to Avoid - Do not store `*Identity` in context — always store the value type. The type assertion in `FromContext` relies on `Identity` being stored as a value. - Do not define permission constants in this package. Domain permissions belong in the domain package that owns the resource. - Do not add role-string logic here. The bit-set model deliberately avoids the role-to-permission mapping table problem. - Do not call `rbac.FromContext` from within `rbac` itself — the context helpers are for consumers, not for internal use. - Permissions 63 and above are silently ignored by `Has` and `Grant`. Keep constants in the range 0–62. ## Testing Notes - `compliance_test.go` enforces at compile time that `Identity.WithTenant` returns `Identity` (not `*Identity`) and that `PermissionMask` exposes `Has` and `Grant` with the correct typed signatures. - `identity_test.go` covers `NewIdentity`, `WithTenant` immutability, `SetInContext`/`FromContext` round-trips, and the zero-value absent case. - `permission_test.go` covers `Has`, `Grant`, and boundary conditions (negative positions, position >= 63). - No external dependencies — run with plain `go test`.