1fb82f09b2
---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.
296 lines
8.7 KiB
Go
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)")
|
|
}
|
|
}
|