# `rbac` > Identity and role-based access control primitives for the request lifecycle. **Module:** `code.nochebuena.dev/go/rbac` **Tier:** 0 — zero external dependencies, stdlib only **Go:** 1.25+ **Dependencies:** none --- ## Overview `rbac` provides the foundational types that flow through every authenticated request: [`Identity`](#identity) (the authenticated principal), [`Permission`](#permissions) and [`PermissionMask`](#permissions) (typed bit-mask access control), and [`PermissionProvider`](#permissionprovider) (the interface authorization backends implement). These types are Tier 0 — every module that needs to read or carry identity data imports this package. Higher-level modules (`httpauth`, `httpserver`) build on top of it without this package knowing about them. This package does **not** verify tokens, issue JWTs, query a database, or enforce permissions on HTTP routes. Token verification belongs to [`firebase`](../firebase) or any `httpauth.TokenVerifier` implementation. Route enforcement belongs to [`httpauth`](../httpauth). ## Installation ```sh go get code.nochebuena.dev/go/rbac ``` ## Quick start ```go import "code.nochebuena.dev/go/rbac" // 1. Construct an identity from token claims (e.g. in auth middleware) id := rbac.NewIdentity(uid, displayName, email) // 2. Optionally enrich with a tenant ID (e.g. in enrichment middleware) id = id.WithTenant(tenantID) // 3. Store it in the request context ctx = rbac.SetInContext(ctx, id) // 4. Retrieve it anywhere downstream (handler, service, repository) id, ok := rbac.FromContext(ctx) if !ok { // no authenticated identity in context } ``` ## Usage ### Identity `Identity` is a **value type** — it is always copied, never a pointer. This eliminates nil-check burden and prevents accidental mutation of a shared context value. ```go type Identity struct { UID string TenantID string DisplayName string Email string } ``` **Construction follows a two-step pattern** that mirrors the middleware pipeline: ```go // Step 1 — auth middleware: populate from token claims id := rbac.NewIdentity(uid, displayName, email) // id.TenantID == "" at this point // Step 2 — enrichment middleware: attach tenant (opt-in, returns a new value) id = id.WithTenant(tenantID) // original id is unchanged — WithTenant does not mutate ``` `WithTenant` is safe to call from concurrent middleware because it returns a new value instead of mutating the receiver. ### Context helpers ```go // Store — typically called in auth middleware ctx = rbac.SetInContext(ctx, id) // Retrieve — typically called in handlers or services id, ok := rbac.FromContext(ctx) if !ok { // unauthenticated request } ``` `FromContext` returns the **zero-value `Identity` and `false`** when no identity is present — no nil pointer to check. ### Permissions Define your application's permissions as typed constants: ```go // In your application code const ( Read rbac.Permission = 0 Write rbac.Permission = 1 Delete rbac.Permission = 2 Admin rbac.Permission = 3 ) ``` `Permission` is a bit position (0–62). `PermissionMask` is the resolved bit-mask for a user on a resource. Check permissions with `Has`: ```go mask, err := provider.ResolveMask(ctx, id.UID, "orders") if err != nil { ... } if !mask.Has(Read) { return xerrors.New(xerrors.ErrPermissionDenied, "cannot read orders") } ``` Build masks for tests or in-memory implementations with `Grant`: ```go mask := rbac.PermissionMask(0).Grant(Read).Grant(Write) mask.Has(Read) // true mask.Has(Delete) // false ``` `Grant` returns a new mask — it does not mutate the receiver. Valid bit positions are 0–62. `Has` and `Grant` both return the unmodified value for out-of-range inputs rather than panicking. ### PermissionProvider `PermissionProvider` is the interface that authorization backends implement. `httpauth.AuthzMiddleware` calls it — no knowledge of the concrete implementation is required. ```go type PermissionProvider interface { ResolveMask(ctx context.Context, uid, resource string) (PermissionMask, error) } ``` **Multi-tenant implementations** retrieve `TenantID` from the context rather than requiring it as a parameter: ```go func (p *myProvider) ResolveMask(ctx context.Context, uid, resource string) (rbac.PermissionMask, error) { id, _ := rbac.FromContext(ctx) return p.db.QueryMask(ctx, id.TenantID, uid, resource) } ``` **Single-tenant implementations** ignore `TenantID` entirely — the interface signature does not force them to handle it. **In-memory implementation for tests:** ```go type staticProvider struct{ mask rbac.PermissionMask } func (p *staticProvider) ResolveMask(_ context.Context, _, _ string) (rbac.PermissionMask, error) { return p.mask, nil } // Usage in tests provider := &staticProvider{mask: rbac.PermissionMask(0).Grant(Read)} ``` ## Design decisions **Value type for `Identity`** — the original `authz` package used `*Identity` everywhere. Pointer semantics forced every caller to check for nil and opened the door to accidental mutation of a context value shared across the request. Value semantics are safer and simpler at zero extra cost. **No JSON tags on `Identity`** — `Identity` is a runtime value threaded through context, not a data transfer object. If an application needs to serialize identity data (e.g. in an audit log), it maps to its own DTO with the fields it actually needs. **`WithTenant` instead of a mutable setter** — middleware pipelines are concurrent. Returning a new value from `WithTenant` makes the enrichment step safe without synchronization. **`tenantID` removed from `PermissionProvider.ResolveMask`** — it is already in the context via `Identity`. Passing it again as an explicit parameter forces every call site to extract and re-pass data that is already available, and leaks multi-tenancy concerns into the interface signature. Single-tenant implementations never needed it; multi-tenant implementations read it from context. **`Permission` and `PermissionMask` as distinct types** — the original `HasPermission(mask, bit int64)` had two `int64` parameters in the same position, making it trivial to swap them by accident. Distinct types make that a compile-time error. ## Ecosystem ``` Tier 0: rbac ← you are here ↑ Tier 4: httpauth (builds auth + enrichment + authz middleware on top of rbac) ``` `rbac.Identity` flows from `httpauth` into every downstream handler and service via `rbac.FromContext(ctx)`. No module between `rbac` and the handler needs to import `httpauth`. ## License MIT