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/
This commit is contained in:
113
CLAUDE.md
Normal file
113
CLAUDE.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user