Files
rbac/CLAUDE.md

114 lines
3.7 KiB
Markdown
Raw Normal View 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:**
```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 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`.