test(client): NATS nkey auth end to end
Harness gains newHarnessFull(ctrlMode, natsAuth) wiring the nkey authenticator to the user allowlist; NATS auth and HTTP auth are independent so each plane is tested in isolation. TestNatsNkeyAuth: registered peer connects with nkey and operates (golden); unregistered peer and no-nkey client refused at connect (error paths); peer revoked at runtime refused on its next connection without a restart (edge). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+75
-11
@@ -14,6 +14,7 @@ import (
|
||||
cs "fn-registry/functions/cybersecurity"
|
||||
|
||||
"github.com/enmanuel/unibus/pkg/blobstore"
|
||||
"github.com/enmanuel/unibus/pkg/busauth"
|
||||
"github.com/enmanuel/unibus/pkg/client"
|
||||
"github.com/enmanuel/unibus/pkg/embeddednats"
|
||||
"github.com/enmanuel/unibus/pkg/frame"
|
||||
@@ -42,31 +43,49 @@ func freePort(t *testing.T) int {
|
||||
return l.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
func newHarness(t *testing.T) *testHarness { return newHarnessMode(t, membership.AuthOff) }
|
||||
func newHarness(t *testing.T) *testHarness { return newHarnessFull(t, membership.AuthOff, false) }
|
||||
|
||||
// newHarnessMode is newHarness with an explicit control-plane auth mode, so auth
|
||||
// tests can boot the real server in enforce/soft and exercise it through the
|
||||
// production client (which signs every request).
|
||||
// newHarnessMode is newHarness with an explicit control-plane auth mode and the
|
||||
// NATS data plane left open (no nkey auth), so HTTP-auth tests can use a plain
|
||||
// client.New that does not present an nkey.
|
||||
func newHarnessMode(t *testing.T, mode membership.AuthMode) *testHarness {
|
||||
return newHarnessFull(t, mode, false)
|
||||
}
|
||||
|
||||
// newHarnessFull boots the embedded NATS (optionally with the nkey authenticator
|
||||
// backed by the user allowlist) and the membershipd HTTP server in ctrlMode.
|
||||
// natsAuth and ctrlMode are independent on purpose: an HTTP-enforce test can
|
||||
// keep NATS open, and an nkey test can keep HTTP off, mirroring how the rollout
|
||||
// flags compose. The store is created before NATS so the authenticator can
|
||||
// consult IsAuthorized for live revocation.
|
||||
func newHarnessFull(t *testing.T, ctrlMode membership.AuthMode, natsAuth bool) *testHarness {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
|
||||
ns, err := embeddednats.Start(filepath.Join(dir, "js"), freePort(t))
|
||||
store, err := membership.Open(filepath.Join(dir, "unibus.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("membership store: %v", err)
|
||||
}
|
||||
|
||||
var ns *server.Server
|
||||
if natsAuth {
|
||||
ns, err = embeddednats.StartHostAuth(filepath.Join(dir, "js"), "127.0.0.1", freePort(t),
|
||||
busauth.NewNkeyAuthenticator(store.IsAuthorized))
|
||||
} else {
|
||||
ns, err = embeddednats.Start(filepath.Join(dir, "js"), freePort(t))
|
||||
}
|
||||
if err != nil {
|
||||
store.Close()
|
||||
t.Fatalf("embedded nats: %v", err)
|
||||
}
|
||||
|
||||
store, err := membership.Open(filepath.Join(dir, "unibus.db"))
|
||||
if err != nil {
|
||||
ns.Shutdown()
|
||||
t.Fatalf("membership store: %v", err)
|
||||
}
|
||||
blobs, err := blobstore.New(filepath.Join(dir, "blobs"))
|
||||
if err != nil {
|
||||
ns.Shutdown()
|
||||
store.Close()
|
||||
t.Fatalf("blob store: %v", err)
|
||||
}
|
||||
srv := membership.NewServer(store, blobs, mode)
|
||||
srv := membership.NewServer(store, blobs, ctrlMode)
|
||||
httpts := httptest.NewServer(srv)
|
||||
|
||||
h := &testHarness{natsURL: embeddednats.ClientURL(ns), ctrlURL: httpts.URL, ns: ns, httpts: httpts, store: store}
|
||||
@@ -516,6 +535,51 @@ func TestControlPlaneAuthEnforceE2E(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestNatsNkeyAuth exercises the data-plane authenticator: with NATS nkey auth
|
||||
// on, a registered peer connecting with its nkey is accepted and can publish
|
||||
// (golden); an unregistered peer is refused at connect time (error path); and a
|
||||
// peer revoked while the server runs is refused on its NEXT connection, proving
|
||||
// revocation without a restart (edge).
|
||||
func TestNatsNkeyAuth(t *testing.T) {
|
||||
h := newHarnessFull(t, membership.AuthOff, true) // NATS auth on; HTTP off to isolate the data plane
|
||||
waitHealth(t, h.ctrlURL)
|
||||
|
||||
idA := mustIdentity(t)
|
||||
if err := h.store.AddUser(hex.EncodeToString(idA.SignPub), "alice", membership.RoleMember); err != nil {
|
||||
t.Fatalf("register A: %v", err)
|
||||
}
|
||||
|
||||
// Golden: registered peer connects with its nkey and uses the bus.
|
||||
a, err := client.NewWithOptions(h.natsURL, h.ctrlURL, idA, client.Options{UseNkey: true})
|
||||
if err != nil {
|
||||
t.Fatalf("registered peer should connect with nkey: %v", err)
|
||||
}
|
||||
defer a.Close()
|
||||
if _, err := a.CreateRoom("room.nkey", room.ModeNATS); err != nil {
|
||||
t.Fatalf("registered peer should operate: %v", err)
|
||||
}
|
||||
|
||||
// Error path: an unregistered identity is refused at connect time.
|
||||
idB := mustIdentity(t)
|
||||
if _, err := client.NewWithOptions(h.natsURL, h.ctrlURL, idB, client.Options{UseNkey: true}); err == nil {
|
||||
t.Fatalf("unregistered peer must be refused by the NATS authenticator")
|
||||
}
|
||||
|
||||
// Error path: presenting no nkey to an auth-required server is refused.
|
||||
if _, err := client.NewWithOptions(h.natsURL, h.ctrlURL, idB, client.Options{UseNkey: false}); err == nil {
|
||||
t.Fatalf("a client without an nkey must be refused when the server requires auth")
|
||||
}
|
||||
|
||||
// Edge: revoke A while the server runs; A's NEXT connection is refused even
|
||||
// though an already-open connection (a) is unaffected. No server restart.
|
||||
if err := h.store.RevokeUser(hex.EncodeToString(idA.SignPub)); err != nil {
|
||||
t.Fatalf("revoke A: %v", err)
|
||||
}
|
||||
if _, err := client.NewWithOptions(h.natsURL, h.ctrlURL, idA, client.Options{UseNkey: true}); err == nil {
|
||||
t.Fatalf("revoked peer must be refused on a new connection without a restart")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- test helpers ---------------------------------------------------------
|
||||
|
||||
type collector struct {
|
||||
|
||||
Reference in New Issue
Block a user