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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user