// Package daemon — acl_test.go verifies the ACL enforcement wiring // (P04, v0.13; C-44/C-45). It exercises the aclPolicy.Check path // with constructed mTLS peer certificates (SPIFFE SVID + OIDC CN) // and asserts deny-by-default + log-only mode semantics. package daemon import ( "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/json" "encoding/pem" "log/slog" "math/big" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strings" "testing" "time" "git.cloudinit.dev/coreci/orca/internal/acl" "git.cloudinit.dev/coreci/orca/internal/paths" "git.cloudinit.dev/coreci/orca/internal/store" ) // aclStateJSON mirrors the on-disk acl.json shape. type aclStateJSON struct { Entries []acl.ACLEntry `json:"entries"` } // mustMarshal marshals v or fails the test. func mustMarshal(t *testing.T, v any) []byte { t.Helper() b, err := json.MarshalIndent(v, "", " ") if err != nil { t.Fatalf("marshal: %v", err) } return b } // writeACLFile writes the given entries to paths.ACLPath() under a // fresh $ORCA_HOME so NewACLPolicy picks them up. func writeACLFile(t *testing.T, entries []acl.ACLEntry) { t.Helper() home := t.TempDir() t.Setenv("ORCA_HOME", home) if err := os.MkdirAll(paths.ClusterDir(), 0o755); err != nil { t.Fatalf("mkdir cluster dir: %v", err) } data := mustMarshal(t, aclStateJSON{Entries: entries}) if err := os.WriteFile(paths.ACLPath(), data, 0o600); err != nil { t.Fatalf("write acl: %v", err) } } // buildSelfSignedCert builds an in-memory self-signed x509 cert with // the given SPIFFE URI SAN and CommonName. The ACL layer only inspects // URIs + CommonName, not the signature chain (chain verification is // the mTLS handshake's job). func buildSelfSignedCert(t *testing.T, spiffeURI, commonName string) *x509.Certificate { t.Helper() key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("rsa key: %v", err) } tmpl := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: commonName}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), DNSNames: []string{"localhost"}, } if spiffeURI != "" { u, err := url.Parse(spiffeURI) if err != nil { t.Fatalf("parse spiffe uri: %v", err) } tmpl.URIs = []*url.URL{u} } der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) if err != nil { t.Fatalf("create cert: %v", err) } cert, err := x509.ParseCertificate(der) if err != nil { t.Fatalf("parse cert: %v", err) } // Round-trip through PEM so the cert is realistic. _ = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) return cert } // makePeerCert is a shorthand for buildSelfSignedCert. func makePeerCert(t *testing.T, spiffeURI, commonName string) *x509.Certificate { return buildSelfSignedCert(t, spiffeURI, commonName) } // reqWithPeerCert builds an *http.Request whose r.TLS.PeerCertificates // is populated with the given cert, simulating an mTLS handshake. func reqWithPeerCert(cert *x509.Certificate) *http.Request { r := httptest.NewRequest(http.MethodGet, "/v1/jobs", nil) r.TLS = &tls.ConnectionState{ PeerCertificates: []*x509.Certificate{cert}, } return r } // newTestServer builds a daemon Server with a temp DB and the given // ACL enforce mode. Used by the handler-level tests. func newACLTestServer(t *testing.T, enforce bool) *Server { t.Helper() db, err := store.Open(filepath.Join(t.TempDir(), "test.db")) if err != nil { t.Fatalf("open db: %v", err) } t.Cleanup(func() { db.Close() }) s := NewServer(Options{ DB: db, Log: slog.New(slog.NewTextHandler(os.Stderr, nil)), Addr: ":0", ACLEnforce: enforce, }) return s } // --- aclPolicy unit tests --- // TestACLPolicyDenyByDefault verifies that an authenticated request // with no matching ACL entry is denied in enforce mode. func TestACLPolicyDenyByDefault(t *testing.T) { writeACLFile(t, nil) // empty ACL p := NewACLPolicy(true, slog.New(slog.NewTextHandler(os.Stderr, nil))) cert := makePeerCert(t, "spiffe://orca.local/ns/myapp/sa/svc1/alloc-1", "") r := reqWithPeerCert(cert) _, ok := p.Check(r, "_defaults", acl.PermRead) if ok { t.Fatal("expected deny (no ACL entry), got allow") } } // TestACLPolicyAllowWithEntry verifies that an authenticated request // with a matching ACL entry is allowed. func TestACLPolicyAllowWithEntry(t *testing.T) { id := acl.Identity{Kind: acl.KindSpiffe, ID: "spiffe://orca.local/ns/_defaults/sa/orca/alloc-1", Namespace: "_defaults"} a := acl.NewACL() a.Grant(id, "_defaults", acl.PermRead|acl.PermWrite) writeACLFile(t, a.List()) p := NewACLPolicy(true, slog.New(slog.NewTextHandler(os.Stderr, nil))) cert := makePeerCert(t, id.ID, "") r := reqWithPeerCert(cert) gotID, ok := p.Check(r, "_defaults", acl.PermRead) if !ok { t.Fatal("expected allow (matching entry), got deny") } if gotID.ID != id.ID { t.Errorf("identity ID = %q, want %q", gotID.ID, id.ID) } } // TestACLPolicyUnauthenticatedEnforce verifies that a request with no // peer cert is denied in enforce mode. func TestACLPolicyUnauthenticatedEnforce(t *testing.T) { writeACLFile(t, nil) p := NewACLPolicy(true, slog.New(slog.NewTextHandler(os.Stderr, nil))) r := httptest.NewRequest(http.MethodGet, "/v1/jobs", nil) _, ok := p.Check(r, "_defaults", acl.PermRead) if ok { t.Fatal("expected deny for unauthenticated in enforce mode, got allow") } } // TestACLPolicyLogOnlyAllowsDenials (C-45) verifies that in log-only // mode (enforce=false), denials are logged but the request proceeds // (allow=true). This is the staged-rollout semantics. func TestACLPolicyLogOnlyAllowsDenials(t *testing.T) { writeACLFile(t, nil) // empty ACL → all denials p := NewACLPolicy(false, slog.New(slog.NewTextHandler(os.Stderr, nil))) // Unauthenticated in log-only mode → logged but allowed. r := httptest.NewRequest(http.MethodGet, "/v1/jobs", nil) _, ok := p.Check(r, "_defaults", acl.PermRead) if !ok { t.Fatal("expected allow in log-only mode (unauthenticated), got deny") } // Authenticated-but-no-entry in log-only mode → logged but allowed. cert := makePeerCert(t, "spiffe://orca.local/ns/myapp/sa/svc1/alloc-1", "") r2 := reqWithPeerCert(cert) _, ok = p.Check(r2, "_defaults", acl.PermRead) if !ok { t.Fatal("expected allow in log-only mode (no entry), got deny") } } // TestACLPolicyOIDCCNIdentity verifies that a cert with no SPIFFE URI // but a CommonName is treated as an OIDC identity. func TestACLPolicyOIDCCNIdentity(t *testing.T) { id := acl.Identity{Kind: acl.KindOidc, ID: "operator@example.com"} a := acl.NewACL() a.Grant(id, "_defaults", acl.PermRead) writeACLFile(t, a.List()) p := NewACLPolicy(true, slog.New(slog.NewTextHandler(os.Stderr, nil))) cert := makePeerCert(t, "", "operator@example.com") r := reqWithPeerCert(cert) gotID, ok := p.Check(r, "_defaults", acl.PermRead) if !ok { t.Fatal("expected allow for OIDC CN identity, got deny") } if gotID.Kind != acl.KindOidc || gotID.ID != "operator@example.com" { t.Errorf("identity = %+v, want oidc:operator@example.com", gotID) } } // --- Handler-level tests (T10) --- // TestACLJobsHandlerEnforceDeniesUnauthenticated verifies the wired // jobs handler denies an unauthenticated request in enforce mode. func TestACLJobsHandlerEnforceDeniesUnauthenticated(t *testing.T) { writeACLFile(t, nil) srv := newACLTestServer(t, true) rec := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/v1/jobs", nil) srv.handleJobsCollection(rec, r) if rec.Code != http.StatusForbidden { t.Errorf("unauthenticated /v1/jobs (enforce): %d, want 403", rec.Code) } if !strings.Contains(rec.Body.String(), "access denied") { t.Errorf("body should contain 'access denied': %s", rec.Body.String()) } } // TestACLJobsHandlerLogOnlyAllowsUnauthenticated (C-45) verifies the // wired jobs handler allows an unauthenticated request in log-only // mode (the denial is logged but the request proceeds). func TestACLJobsHandlerLogOnlyAllowsUnauthenticated(t *testing.T) { writeACLFile(t, nil) srv := newACLTestServer(t, false) rec := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/v1/jobs", nil) srv.handleJobsCollection(rec, r) if rec.Code == http.StatusForbidden { t.Errorf("unauthenticated /v1/jobs (log-only): %d, want non-403", rec.Code) } } // TestACLJobsHandlerAllowsAuthenticatedWithEntry verifies the wired // jobs handler allows an authenticated request with a matching ACL // entry in enforce mode. func TestACLJobsHandlerAllowsAuthenticatedWithEntry(t *testing.T) { id := acl.Identity{Kind: acl.KindSpiffe, ID: "spiffe://orca.local/ns/_defaults/sa/orca/alloc-1", Namespace: "_defaults"} a := acl.NewACL() a.Grant(id, "_defaults", acl.PermRead) writeACLFile(t, a.List()) srv := newACLTestServer(t, true) cert := makePeerCert(t, id.ID, "") rec := httptest.NewRecorder() r := reqWithPeerCert(cert) srv.handleJobsCollection(rec, r) if rec.Code == http.StatusForbidden { t.Errorf("authenticated /v1/jobs (matching entry): %d, want non-403", rec.Code) } } // TestACLNodesHandlerEnforceDeniesUnauthenticated verifies the nodes // handler denies an unauthenticated request in enforce mode. func TestACLNodesHandlerEnforceDeniesUnauthenticated(t *testing.T) { writeACLFile(t, nil) srv := newACLTestServer(t, true) rec := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/v1/nodes", nil) srv.handleNodesCollection(rec, r) if rec.Code != http.StatusForbidden { t.Errorf("unauthenticated /v1/nodes (enforce): %d, want 403", rec.Code) } } // TestACLTasksHandlerEnforceDeniesUnauthenticated verifies the tasks // handler denies an unauthenticated request in enforce mode. func TestACLTasksHandlerEnforceDeniesUnauthenticated(t *testing.T) { writeACLFile(t, nil) srv := newACLTestServer(t, true) rec := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/v1/tasks", nil) srv.handleTasksCollection(rec, r) if rec.Code != http.StatusForbidden { t.Errorf("unauthenticated /v1/tasks (enforce): %d, want 403", rec.Code) } }