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/
96 lines
2.4 KiB
Go
96 lines
2.4 KiB
Go
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")
|
|
}
|
|
}
|