Files
rbac/CLAUDE.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

3.7 KiB
Raw Blame History

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 (062) 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:

// 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:

// In your domain package — define once, use everywhere
const (
    PermRead   rbac.Permission = 0
    PermWrite  rbac.Permission = 1
    PermDelete rbac.Permission = 2
)

Checking permissions:

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:

mask := rbac.PermissionMask(0).Grant(PermRead).Grant(PermWrite)

In-memory PermissionProvider for tests:

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 062.

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.