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/
3.7 KiB
3.7 KiB
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
int64mask for O(1) checks and compact storage. Seedocs/adr/ADR-001-bitset-permissions.md. Identityis a value type — copied on every enrichment, never a pointer — to prevent nil bugs and accidental mutation from concurrent middleware. Seedocs/adr/ADR-002-identity-value-type.md.rbacowns the context key forIdentity(authContextKey{}). Any package that needs an identity imports onlyrbac, not an HTTP package. Seedocs/adr/ADR-003-identity-context-ownership.md.TenantIDis an optional enrichment field onIdentity. Multi-tenancy is not modelled in the core;PermissionProviderimplementations retrieve the tenant fromrbac.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
*Identityin context — always store the value type. The type assertion inFromContextrelies onIdentitybeing 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.FromContextfrom withinrbacitself — the context helpers are for consumers, not for internal use. - Permissions 63 and above are silently ignored by
HasandGrant. Keep constants in the range 0–62.
Testing Notes
compliance_test.goenforces at compile time thatIdentity.WithTenantreturnsIdentity(not*Identity) and thatPermissionMaskexposesHasandGrantwith the correct typed signatures.identity_test.gocoversNewIdentity,WithTenantimmutability,SetInContext/FromContextround-trips, and the zero-value absent case.permission_test.gocoversHas,Grant, and boundary conditions (negative positions, position >= 63).- No external dependencies — run with plain
go test.