Files
orca/internal/acl/acl_test.go
T
Jon Chery 1fb82f09b2 fix(P06): ACL rewrite to OIDC claims (REQ-145, REQ-122, F1)
---ci---
project: orca
phase: 6
milestone: v0.12
status: execute
---/ci---

Add KindOidc to ACL: OIDCClaims struct, OidcIdentity, OidcGroupIdentity,
CheckOidc (checks user sub + group: prefix entries). KindToken now
always denies (R-021: no Orca-issued tokens). Existing acl.json entries
with KindToken are inert (P07 removes, P22 migrates). acl.json file
mode tightened to 0600. Deny-by-default enforced. 4 new OIDC ACL tests
+ deprecation test. Existing tests migrated to KindOidc. All pass.
2026-08-07 11:03:42 +00:00

296 lines
8.7 KiB
Go

package acl
import (
"fmt"
"sync"
"testing"
)
func TestGrantAndCheck(t *testing.T) {
a := NewACL()
id := Identity{Kind: KindOidc, 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: KindOidc, 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: KindOidc, 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: KindOidc, 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: KindOidc, 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: KindOidc, 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: KindOidc, 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: KindOidc, 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: KindOidc, 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: KindOidc, 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: KindOidc, 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
}
// --- REQ-145 / F1 ACL OIDC rewrite tests ---
// TestACLOidcUserGrant verifies an OIDC user (by sub) can be granted
// and checked.
func TestACLOidcUserGrant(t *testing.T) {
a := NewACL()
claims := OIDCClaims{Subject: "user-1", Groups: []string{"devs"}}
a.Grant(OidcIdentity(claims), "prod", PermWrite|PermRead)
if !a.CheckOidc(claims, "prod", PermWrite) {
t.Error("CheckOidc should allow write")
}
if !a.CheckOidc(claims, "prod", PermRead) {
t.Error("CheckOidc should allow read (explicit)")
}
if a.CheckOidc(claims, "prod", PermAdmin) {
t.Error("CheckOidc should deny admin")
}
if a.CheckOidc(claims, "other", PermRead) {
t.Error("CheckOidc should deny on wrong ns")
}
}
// TestACLOidcGroupGrant verifies group-based grants work.
func TestACLOidcGroupGrant(t *testing.T) {
a := NewACL()
a.Grant(OidcGroupIdentity("orca-admins"), "prod", PermAdmin)
claims := OIDCClaims{Subject: "user-2", Groups: []string{"orca-admins"}}
if !a.CheckOidc(claims, "prod", PermAdmin) {
t.Error("admin group should have admin")
}
if !a.CheckOidc(claims, "prod", PermWrite) {
t.Error("admin implies write")
}
claimsNoGroup := OIDCClaims{Subject: "user-3", Groups: []string{"devs"}}
if a.CheckOidc(claimsNoGroup, "prod", PermRead) {
t.Error("non-admin group should deny")
}
}
// TestACLOidcDenyByDefault verifies an ungranted OIDC user is denied.
func TestACLOidcDenyByDefault(t *testing.T) {
a := NewACL()
claims := OIDCClaims{Subject: "nobody"}
if a.CheckOidc(claims, "prod", PermRead) {
t.Error("ungranted user should deny")
}
}
// TestACLTokenDeprecated verifies KindToken always denies (R-021).
func TestACLTokenDeprecated(t *testing.T) {
a := NewACL()
// Even if an old acl.json has a KindToken entry, Check returns false.
a.Grant(Identity{Kind: KindToken, ID: "old-token-123"}, "prod", PermAdmin)
if a.Check(Identity{Kind: KindToken, ID: "old-token-123"}, "prod", PermRead) {
t.Error("KindToken should always deny (R-021)")
}
if a.Check(Identity{Kind: KindToken, ID: "old-token-123"}, "prod", PermAdmin) {
t.Error("KindToken should always deny even admin (R-021)")
}
}