Require auth for fabric session websocket

This commit is contained in:
2026-05-16 00:19:38 +03:00
parent 8a972ea68f
commit be31798d7c
3 changed files with 238 additions and 4 deletions
@@ -101,7 +101,7 @@ func TestFabricSessionWebSocketPingPongAndEvents(t *testing.T) {
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/mesh/v1/fabric/session/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
conn, _, err := websocket.DefaultDialer.Dial(wsURL, fabricSessionTestHeaders("rap_fsn_smoke"))
if err != nil {
t.Fatalf("dial fabric session websocket: %v", err)
}
@@ -112,7 +112,7 @@ func TestFabricSessionWebSocketPingPongAndEvents(t *testing.T) {
if pong.Type != fabricproto.FramePong || pong.Sequence != 17 || string(pong.Payload) != "probe" {
t.Fatalf("pong = %+v", pong)
}
if len(events) < 2 || events[0].Event != "fabric_session_websocket_opened" || events[1].SessionEvent != fabricproto.SessionEventPing {
if len(events) < 2 || events[0].Event != "fabric_session_websocket_opened" || events[0].AcceptedBy != "legacy_unsigned" || events[1].SessionEvent != fabricproto.SessionEventPing {
t.Fatalf("events = %+v", events)
}
}
@@ -125,7 +125,7 @@ func TestFabricSessionWebSocketOpenStreamDataAck(t *testing.T) {
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/mesh/v1/fabric/session/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
conn, _, err := websocket.DefaultDialer.Dial(wsURL, fabricSessionTestHeaders("rap_fsn_smoke"))
if err != nil {
t.Fatalf("dial fabric session websocket: %v", err)
}
@@ -149,6 +149,89 @@ func TestFabricSessionWebSocketOpenStreamDataAck(t *testing.T) {
}
}
func TestFabricSessionWebSocketRequiresToken(t *testing.T) {
server := httptest.NewServer(Server{
Local: PeerIdentity{ClusterID: "cluster-1", NodeID: "node-a"},
FabricSessionEnabled: true,
}.Handler())
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/mesh/v1/fabric/session/ws"
_, resp, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err == nil {
t.Fatal("dial fabric session without token unexpectedly succeeded")
}
if resp == nil || resp.StatusCode != http.StatusUnauthorized {
t.Fatalf("status = %v err=%v, want 401", resp, err)
}
}
func TestFabricSessionWebSocketRequiresSignedAuthorityWhenConfigured(t *testing.T) {
publicKey, _, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatalf("generate key: %v", err)
}
server := httptest.NewServer(Server{
Local: PeerIdentity{ClusterID: "cluster-1", NodeID: "node-a"},
FabricSessionEnabled: true,
ClusterAuthorityPublicKey: base64.StdEncoding.EncodeToString(publicKey),
}.Handler())
defer server.Close()
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/mesh/v1/fabric/session/ws"
_, resp, err := websocket.DefaultDialer.Dial(wsURL, fabricSessionTestHeaders("rap_fsn_unsigned"))
if err == nil {
t.Fatal("dial unsigned fabric session unexpectedly succeeded")
}
if resp == nil || resp.StatusCode != http.StatusForbidden {
t.Fatalf("status = %v err=%v, want 403", resp, err)
}
}
func TestFabricSessionWebSocketAcceptsSignedAuthority(t *testing.T) {
publicKey, privateKey, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatalf("generate key: %v", err)
}
token := "rap_fsn_signedtest"
var events []FabricSessionEventLogEntry
server := httptest.NewServer(Server{
Local: PeerIdentity{ClusterID: "cluster-1", NodeID: "node-a"},
FabricSessionEnabled: true,
ClusterAuthorityPublicKey: base64.StdEncoding.EncodeToString(publicKey),
FabricSessionLogger: func(entry FabricSessionEventLogEntry) {
events = append(events, entry)
},
}.Handler())
defer server.Close()
headers := signedFabricSessionHeaders(t, token, publicKey, privateKey, fabricSessionAuthorityPayload{
SchemaVersion: "rap.fabric_session_authority.v1",
ClusterID: "cluster-1",
SessionID: "session-1",
SourceNodeID: "phone-1",
SelectedEntryNodeID: "node-a",
TokenHash: fabricSessionTokenHash(token),
IssuedAt: time.Now().UTC().Add(-time.Minute),
ExpiresAt: time.Now().UTC().Add(time.Minute),
})
wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/mesh/v1/fabric/session/ws"
conn, _, err := websocket.DefaultDialer.Dial(wsURL, headers)
if err != nil {
t.Fatalf("dial signed fabric session websocket: %v", err)
}
defer conn.Close()
writeMeshFabricFrame(t, conn, fabricproto.Frame{Type: fabricproto.FramePing, Sequence: 23})
pong := readMeshFabricFrame(t, conn)
if pong.Type != fabricproto.FramePong || pong.Sequence != 23 {
t.Fatalf("pong = %+v", pong)
}
if len(events) < 2 || events[0].AcceptedBy != "signed" || events[0].SessionID != "session-1" {
t.Fatalf("events = %+v", events)
}
}
func TestMeshForwardingGateEnabledStillHasNoProductionRuntime(t *testing.T) {
local := PeerIdentity{ClusterID: "cluster-1", NodeID: "node-b"}
server := httptest.NewServer(Server{
@@ -182,6 +265,38 @@ func writeMeshFabricFrame(t *testing.T, conn *websocket.Conn, frame fabricproto.
}
}
func fabricSessionTestHeaders(token string) http.Header {
headers := http.Header{}
headers.Set("X-RAP-Fabric-Session-Token", token)
return headers
}
func signedFabricSessionHeaders(t *testing.T, token string, publicKey ed25519.PublicKey, privateKey ed25519.PrivateKey, payload fabricSessionAuthorityPayload) http.Header {
t.Helper()
headers := fabricSessionTestHeaders(token)
rawPayload, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal fabric session authority payload: %v", err)
}
canonical, err := authority.CanonicalJSON(rawPayload)
if err != nil {
t.Fatalf("canonical fabric session authority payload: %v", err)
}
signature := authority.Signature{
SchemaVersion: authority.SignatureSchemaVersion,
Algorithm: authority.AlgorithmEd25519,
KeyFingerprint: authority.Fingerprint(publicKey),
Signature: base64.StdEncoding.EncodeToString(ed25519.Sign(privateKey, canonical)),
}
rawSignature, err := json.Marshal(signature)
if err != nil {
t.Fatalf("marshal fabric session authority signature: %v", err)
}
headers.Set("X-RAP-Fabric-Session-Authority-Payload", base64.StdEncoding.EncodeToString(rawPayload))
headers.Set("X-RAP-Fabric-Session-Authority-Signature", base64.StdEncoding.EncodeToString(rawSignature))
return headers
}
func readMeshFabricFrame(t *testing.T, conn *websocket.Conn) fabricproto.Frame {
t.Helper()
if err := conn.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {