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:
2026-03-18 13:25:43 -06:00
commit 0864f031a1
17 changed files with 940 additions and 0 deletions

95
permission_test.go Normal file
View File

@@ -0,0 +1,95 @@
package rbac
import "testing"
// App-level permission constants used only within this test file.
const (
read Permission = 0
write Permission = 1
del Permission = 2
admin Permission = 62 // highest valid bit
)
func TestPermissionMask_Has(t *testing.T) {
tests := []struct {
name string
mask PermissionMask
perm Permission
want bool
}{
{"bit 0 set", 1, read, true},
{"bit 4 set", 16, Permission(4), true},
{"multi-bit, check bit 0", 17, read, true},
{"multi-bit, check bit 4", 17, Permission(4), true},
{"bit not set", 16, read, false},
{"out of range low", 1, Permission(-1), false},
{"out of range high", 1, Permission(63), false},
{"max valid bit (62)", PermissionMask(1 << 62), admin, true},
{"zero mask", 0, read, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.mask.Has(tt.perm); got != tt.want {
t.Errorf("PermissionMask(%d).Has(%d) = %v, want %v", tt.mask, tt.perm, got, tt.want)
}
})
}
}
func TestPermissionMask_Grant(t *testing.T) {
mask := PermissionMask(0)
mask = mask.Grant(read)
if !mask.Has(read) {
t.Error("Grant(read): read not set")
}
if mask.Has(write) {
t.Error("Grant(read): write must not be set")
}
mask = mask.Grant(write)
if !mask.Has(read) {
t.Error("Grant(write): read must still be set")
}
if !mask.Has(write) {
t.Error("Grant(write): write not set")
}
}
func TestPermissionMask_Grant_Chain(t *testing.T) {
mask := PermissionMask(0).Grant(read).Grant(write).Grant(del)
if !mask.Has(read) {
t.Error("chained Grant: read not set")
}
if !mask.Has(write) {
t.Error("chained Grant: write not set")
}
if !mask.Has(del) {
t.Error("chained Grant: del not set")
}
if mask.Has(admin) {
t.Error("chained Grant: admin must not be set")
}
}
func TestPermissionMask_Grant_OutOfRange(t *testing.T) {
mask := PermissionMask(0)
result := mask.Grant(Permission(-1))
if result != mask {
t.Error("Grant with out-of-range permission must return the original mask unchanged")
}
result = mask.Grant(Permission(63))
if result != mask {
t.Error("Grant with out-of-range permission (63) must return the original mask unchanged")
}
}
func TestPermissionMask_Grant_DoesNotMutateReceiver(t *testing.T) {
original := PermissionMask(0)
_ = original.Grant(read)
if original.Has(read) {
t.Error("Grant mutated the receiver; PermissionMask must be a value type")
}
}