test: control-plane auth middleware + end-to-end enforce

membership/auth_test: golden (signed+registered accepted), error paths
(unregistered 401, replayed nonce 401, clock skew 401, tampered body 401,
missing headers 401), exemptions (healthz, soft allows, off no-op).
client_test: end-to-end with the real client against an enforce server —
registered peer accepted, unregistered rejected, revoked peer denied without
a server restart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 12:31:58 +02:00
parent 567e604fc7
commit 2130eaa44d
2 changed files with 258 additions and 3 deletions
+64 -3
View File
@@ -1,10 +1,12 @@
package client_test
import (
"encoding/hex"
"net"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"sync"
"testing"
"time"
@@ -27,6 +29,7 @@ type testHarness struct {
ctrlURL string
ns *server.Server
httpts *httptest.Server
store *membership.Store
}
func freePort(t *testing.T) int {
@@ -39,7 +42,12 @@ func freePort(t *testing.T) int {
return l.Addr().(*net.TCPAddr).Port
}
func newHarness(t *testing.T) *testHarness {
func newHarness(t *testing.T) *testHarness { return newHarnessMode(t, membership.AuthOff) }
// 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).
func newHarnessMode(t *testing.T, mode membership.AuthMode) *testHarness {
t.Helper()
dir := t.TempDir()
@@ -58,10 +66,10 @@ func newHarness(t *testing.T) *testHarness {
ns.Shutdown()
t.Fatalf("blob store: %v", err)
}
srv := membership.NewServer(store, blobs)
srv := membership.NewServer(store, blobs, mode)
httpts := httptest.NewServer(srv)
h := &testHarness{natsURL: embeddednats.ClientURL(ns), ctrlURL: httpts.URL, ns: ns, httpts: httpts}
h := &testHarness{natsURL: embeddednats.ClientURL(ns), ctrlURL: httpts.URL, ns: ns, httpts: httpts, store: store}
t.Cleanup(func() {
httpts.Close()
store.Close()
@@ -71,6 +79,15 @@ func newHarness(t *testing.T) *testHarness {
return h
}
// registerClient adds a peer's signing identity to the bus allowlist so its
// signed control-plane requests pass under enforce.
func registerClient(t *testing.T, h *testHarness, c *client.Client, handle, role string) {
t.Helper()
if err := h.store.AddUser(hex.EncodeToString(c.Endpoint().SignPub), handle, role); err != nil {
t.Fatalf("register %s: %v", handle, err)
}
}
func waitHealth(t *testing.T, ctrlURL string) {
t.Helper()
deadline := time.Now().Add(3 * time.Second)
@@ -455,6 +472,50 @@ func TestListMyRoomsDiscovery(t *testing.T) {
}
}
// TestControlPlaneAuthEnforceE2E closes the loop end to end with the production
// client against a server in enforce mode: a registered peer's signed requests
// are accepted (golden), and an unregistered peer is rejected with 401 on its
// first control-plane call (error path). This proves the client's real
// signature construction matches the server's verification.
func TestControlPlaneAuthEnforceE2E(t *testing.T) {
h := newHarnessMode(t, membership.AuthEnforce)
waitHealth(t, h.ctrlURL)
a, err := client.New(h.natsURL, h.ctrlURL, mustIdentity(t))
if err != nil {
t.Fatalf("connect A: %v", err)
}
defer a.Close()
registerClient(t, h, a, "alice", membership.RoleAdmin)
// Golden: registered peer's signed request is accepted.
if _, err := a.CreateRoom("room.enforced", room.ModeNATS); err != nil {
t.Fatalf("registered peer should create a room under enforce: %v", err)
}
// Error path: an unregistered peer is rejected on its first control-plane call.
b, err := client.New(h.natsURL, h.ctrlURL, mustIdentity(t))
if err != nil {
t.Fatalf("connect B: %v", err)
}
defer b.Close()
_, err = b.CreateRoom("room.denied", room.ModeNATS)
if err == nil {
t.Fatalf("unregistered peer must be rejected under enforce")
}
if !strings.Contains(err.Error(), "401") && !strings.Contains(strings.ToLower(err.Error()), "unauthorized") {
t.Fatalf("expected a 401/unauthorized error, got %v", err)
}
// Revocation takes effect without restart: revoke A, its next request fails.
if err := h.store.RevokeUser(hex.EncodeToString(a.Endpoint().SignPub)); err != nil {
t.Fatalf("revoke A: %v", err)
}
if _, err := a.CreateRoom("room.after-revoke", room.ModeNATS); err == nil {
t.Fatalf("revoked peer must be rejected without a server restart")
}
}
// ---- test helpers ---------------------------------------------------------
type collector struct {