33c2b4a78b
internal/acl/acl.go: Identity, Permission, ACLEntry, ACL with Grant/Revoke/Check/List; SpiffeNamespace extraction; deny-by-default. internal/cli/acl.go: orca acl grant/revoke/list/check CLI; state at cluster/acl.json. Tests: grant/revoke/deny/ns-isolation/concurrent. ---ci--- project: orca phase: 02 milestone: v0.11 status: execute ---/ci---
235 lines
6.7 KiB
Go
235 lines
6.7 KiB
Go
package acl
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func TestGrantAndCheck(t *testing.T) {
|
|
a := NewACL()
|
|
id := Identity{Kind: KindToken, ID: "tok-A", Namespace: "test"}
|
|
a.Grant(id, "test", PermRead)
|
|
if !a.Check(id, "test", PermRead) {
|
|
t.Errorf("Check(Read) = false, want true after Grant(Read)")
|
|
}
|
|
if a.Check(id, "test", PermWrite) {
|
|
t.Errorf("Check(Write) = true, want false (only Read granted)")
|
|
}
|
|
}
|
|
|
|
func TestRevoke(t *testing.T) {
|
|
a := NewACL()
|
|
id := Identity{Kind: KindToken, ID: "tok-A", Namespace: "test"}
|
|
a.Grant(id, "test", PermRead)
|
|
a.Revoke(id, "test")
|
|
if a.Check(id, "test", PermRead) {
|
|
t.Errorf("Check(Read) = true after Revoke, want false")
|
|
}
|
|
if got := a.List(); len(got) != 0 {
|
|
t.Errorf("List() len = %d after Revoke, want 0", len(got))
|
|
}
|
|
}
|
|
|
|
func TestRevokeNonExistentNoOp(t *testing.T) {
|
|
a := NewACL()
|
|
id := Identity{Kind: KindToken, ID: "tok-A", Namespace: "test"}
|
|
a.Revoke(id, "ghost")
|
|
if got := a.List(); len(got) != 0 {
|
|
t.Errorf("List() len = %d after no-op Revoke, want 0", len(got))
|
|
}
|
|
}
|
|
|
|
func TestDenyByDefault(t *testing.T) {
|
|
a := NewACL()
|
|
id := Identity{Kind: KindToken, ID: "tok-A", Namespace: "test"}
|
|
if a.Check(id, "test", PermRead) {
|
|
t.Errorf("Check on un-granted identity = true, want false (deny-by-default)")
|
|
}
|
|
if a.Check(id, "test", PermWrite) {
|
|
t.Errorf("Check Write on un-granted identity = true, want false")
|
|
}
|
|
if a.Check(id, "test", PermAdmin) {
|
|
t.Errorf("Check Admin on un-granted identity = true, want false")
|
|
}
|
|
}
|
|
|
|
func TestNamespaceIsolation(t *testing.T) {
|
|
a := NewACL()
|
|
id := Identity{Kind: KindToken, ID: "tok-A", Namespace: "ns-A"}
|
|
a.Grant(id, "ns-A", PermRead)
|
|
if !a.Check(id, "ns-A", PermRead) {
|
|
t.Errorf("Check on ns-A = false, want true")
|
|
}
|
|
if a.Check(id, "ns-B", PermRead) {
|
|
t.Errorf("Check on ns-B = true, want false (namespace isolation)")
|
|
}
|
|
}
|
|
|
|
func TestGrantReplacesPermissions(t *testing.T) {
|
|
a := NewACL()
|
|
id := Identity{Kind: KindToken, ID: "tok-A", Namespace: "test"}
|
|
a.Grant(id, "test", PermRead)
|
|
a.Grant(id, "test", PermWrite)
|
|
if a.Check(id, "test", PermRead) {
|
|
t.Errorf("Check(Read) = true after re-grant with Write-only, want false")
|
|
}
|
|
if !a.Check(id, "test", PermWrite) {
|
|
t.Errorf("Check(Write) = false after re-grant, want true")
|
|
}
|
|
if got := a.List(); len(got) != 1 {
|
|
t.Errorf("List() len = %d, want 1 (grant replaces, not appends)", len(got))
|
|
}
|
|
}
|
|
|
|
func TestSpiffeNamespace(t *testing.T) {
|
|
got, err := SpiffeNamespace("spiffe://orca.local/ns/myapp/sa/svc1/alloc-123")
|
|
if err != nil {
|
|
t.Fatalf("SpiffeNamespace: %v", err)
|
|
}
|
|
if got != "myapp" {
|
|
t.Errorf("SpiffeNamespace = %q, want %q", got, "myapp")
|
|
}
|
|
}
|
|
|
|
func TestSpiffeNamespace_OtherTrustDomain(t *testing.T) {
|
|
got, err := SpiffeNamespace("spiffe://example.com/ns/prod/sa/api/0")
|
|
if err != nil {
|
|
t.Fatalf("SpiffeNamespace: %v", err)
|
|
}
|
|
if got != "prod" {
|
|
t.Errorf("SpiffeNamespace = %q, want %q", got, "prod")
|
|
}
|
|
}
|
|
|
|
func TestSpiffeNamespace_Malformed(t *testing.T) {
|
|
cases := []string{
|
|
"https://orca.local/ns/prod/sa/api/0",
|
|
"spiffe://orca.local/ns/prod/api/0",
|
|
"spiffe://orca.local/ns/prod/sa/api",
|
|
"spiffe://orca.local/ns//sa/api/0",
|
|
":::not-a-uri",
|
|
}
|
|
for _, c := range cases {
|
|
if _, err := SpiffeNamespace(c); err == nil {
|
|
t.Errorf("SpiffeNamespace(%q): expected error, got nil", c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPermissionsDistinct(t *testing.T) {
|
|
if PermRead == PermWrite || PermRead == PermAdmin || PermWrite == PermAdmin {
|
|
t.Errorf("permission flags collide: read=%d write=%d admin=%d", PermRead, PermWrite, PermAdmin)
|
|
}
|
|
a := NewACL()
|
|
id := Identity{Kind: KindToken, ID: "tok-A", Namespace: "test"}
|
|
a.Grant(id, "test", PermRead|PermWrite)
|
|
if !a.Check(id, "test", PermRead) {
|
|
t.Errorf("Check(Read) for read+write grant = false, want true")
|
|
}
|
|
if !a.Check(id, "test", PermWrite) {
|
|
t.Errorf("Check(Write) for read+write grant = false, want true")
|
|
}
|
|
if a.Check(id, "test", PermAdmin) {
|
|
t.Errorf("Check(Admin) for read+write grant = true, want false")
|
|
}
|
|
}
|
|
|
|
func TestAdminImpliesReadAndWrite(t *testing.T) {
|
|
a := NewACL()
|
|
id := Identity{Kind: KindToken, ID: "tok-A", Namespace: "test"}
|
|
a.Grant(id, "test", PermAdmin)
|
|
if !a.Check(id, "test", PermAdmin) {
|
|
t.Errorf("Check(Admin) = false, want true")
|
|
}
|
|
if !a.Check(id, "test", PermRead) {
|
|
t.Errorf("Check(Read) for admin grant = false, want true (admin implies read)")
|
|
}
|
|
if !a.Check(id, "test", PermWrite) {
|
|
t.Errorf("Check(Write) for admin grant = false, want true (admin implies write)")
|
|
}
|
|
}
|
|
|
|
func TestConcurrentAccess(t *testing.T) {
|
|
a := NewACL()
|
|
id := Identity{Kind: KindToken, ID: "tok-concurrent", Namespace: "ns"}
|
|
const n = 200
|
|
var wg sync.WaitGroup
|
|
wg.Add(n * 3)
|
|
for i := 0; i < n; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
a.Grant(id, "ns", PermRead|PermWrite)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
a.Check(id, "ns", PermRead)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
a.List()
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
if !a.Check(id, "ns", PermRead) {
|
|
t.Errorf("Check(Read) after concurrent grants = false, want true")
|
|
}
|
|
if got := a.List(); len(got) != 1 {
|
|
t.Errorf("List() len = %d, want 1 (concurrent grants replace, not append)", len(got))
|
|
}
|
|
}
|
|
|
|
func TestListIsCopy(t *testing.T) {
|
|
a := NewACL()
|
|
id := Identity{Kind: KindToken, ID: "tok-A", Namespace: "test"}
|
|
a.Grant(id, "test", PermRead)
|
|
lst := a.List()
|
|
lst[0].Permissions = PermAdmin
|
|
if a.Check(id, "test", PermAdmin) {
|
|
t.Errorf("mutating List() result leaked into ACL: %v", a.List())
|
|
}
|
|
}
|
|
|
|
func TestSpiffeIdentityGrant(t *testing.T) {
|
|
a := NewACL()
|
|
uri := "spiffe://orca.local/ns/myapp/sa/svc1/alloc-123"
|
|
ns, err := SpiffeNamespace(uri)
|
|
if err != nil {
|
|
t.Fatalf("SpiffeNamespace: %v", err)
|
|
}
|
|
id := Identity{Kind: KindSpiffe, ID: uri, Namespace: ns}
|
|
a.Grant(id, ns, PermRead|PermWrite)
|
|
if !a.Check(id, ns, PermRead) || !a.Check(id, ns, PermWrite) {
|
|
t.Errorf("spiffe identity check failed for ns=%s", ns)
|
|
}
|
|
}
|
|
|
|
func TestTokenAndSpiffeIdentitiesIndependent(t *testing.T) {
|
|
a := NewACL()
|
|
uri := "spiffe://orca.local/ns/prod/sa/api/0"
|
|
spiffeID := Identity{Kind: KindSpiffe, ID: uri, Namespace: "prod"}
|
|
tokenID := Identity{Kind: KindToken, ID: "operator-1", Namespace: "prod"}
|
|
a.Grant(spiffeID, "prod", PermRead)
|
|
if a.Check(tokenID, "prod", PermRead) {
|
|
t.Errorf("token identity matched spiffe grant (kind isolation broken)")
|
|
}
|
|
if !a.Check(spiffeID, "prod", PermRead) {
|
|
t.Errorf("spiffe identity check failed")
|
|
}
|
|
if got := a.List(); len(got) != 1 {
|
|
t.Errorf("List() len = %d, want 1", len(got))
|
|
}
|
|
}
|
|
|
|
func TestAllPermissionsConstant(t *testing.T) {
|
|
if AllPermissions != PermRead|PermWrite|PermAdmin {
|
|
t.Errorf("AllPermissions = %d, want %d", AllPermissions, PermRead|PermWrite|PermAdmin)
|
|
}
|
|
}
|
|
|
|
func ExampleSpiffeNamespace() {
|
|
ns, _ := SpiffeNamespace("spiffe://orca.local/ns/myapp/sa/svc1/alloc-123")
|
|
fmt.Println(ns)
|
|
// Output: myapp
|
|
}
|