Files

195 lines
7.0 KiB
Go

package webingress
import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"net/http"
"testing"
)
func TestFabricRuntimeReceiverVerifiesEnvelopeAndReturnsRuntimeResponse(t *testing.T) {
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
keyID := ed25519EnvelopeKeyID(publicKey)
receiver := FabricRuntimeReceiver{
Config: ReceiverConfig{ServiceType: "global-admin-runtime", Scope: "platform", ServiceClasses: []string{"platform_admin"}},
Keys: StaticEnvelopeKeyResolver{keyID: publicKey},
Handler: recordingRuntimeHandler{response: FabricResponse{
StatusCode: http.StatusCreated,
Headers: http.Header{"X-RAP-Runtime": []string{"ok"}, "Set-Cookie": []string{"blocked"}},
Body: []byte(`{"ok":true}`),
}},
Now: fixedEnvelopeNow,
}
payload := signedReceiverEnvelope(t, privateKey, keyID, FabricServiceChannelEnvelope{
SchemaVersion: FabricServiceChannelEnvelopeSchema,
RequestSchema: "rap.web_ingress.fabric_request.v1",
Method: http.MethodPost,
Path: "/platform-admin/root",
Query: "tab=nodes",
Host: "admin.example.test",
ServiceType: "admin-ingress",
Scope: "platform",
ServiceClass: "platform_admin",
Headers: map[string][]string{"X-Trace-Id": {"trace-1"}},
BodyBase64: base64.StdEncoding.EncodeToString([]byte(`{"hello":"world"}`)),
ObservedAt: "2026-05-17T00:00:00Z",
EnvelopedAt: "2026-05-17T00:00:01Z",
})
responsePayload, err := receiver.Receive(context.Background(), payload)
if err != nil {
t.Fatalf("receive: %v", err)
}
var response struct {
SchemaVersion string `json:"schema_version"`
StatusCode int `json:"status_code"`
Headers map[string][]string `json:"headers"`
BodyBase64 string `json:"body_b64"`
}
if err := json.Unmarshal(responsePayload, &response); err != nil {
t.Fatalf("decode response: %v", err)
}
if response.SchemaVersion != FabricRuntimeResponseSchema ||
response.StatusCode != http.StatusCreated ||
response.Headers["X-Rap-Runtime"][0] != "ok" ||
response.Headers["Set-Cookie"] != nil ||
response.BodyBase64 != "eyJvayI6dHJ1ZX0=" {
t.Fatalf("response = %+v", response)
}
}
func TestFabricRuntimeReceiverRejectsBadSignatureScopeClassAndStaleEnvelope(t *testing.T) {
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
keyID := ed25519EnvelopeKeyID(publicKey)
receiver := FabricRuntimeReceiver{
Config: ReceiverConfig{Scope: "platform", ServiceClasses: []string{"platform_admin"}},
Keys: StaticEnvelopeKeyResolver{keyID: publicKey},
Handler: recordingRuntimeHandler{},
Now: fixedEnvelopeNow,
}
base := FabricServiceChannelEnvelope{
SchemaVersion: FabricServiceChannelEnvelopeSchema,
RequestSchema: "rap.web_ingress.fabric_request.v1",
Method: http.MethodGet,
Path: "/platform-admin/root",
Scope: "platform",
ServiceClass: "platform_admin",
ObservedAt: "2026-05-17T00:00:00Z",
EnvelopedAt: "2026-05-17T00:00:01Z",
}
badSignature := signedReceiverEnvelope(t, privateKey, keyID, base)
badSignature[len(badSignature)-2] = 'x'
if _, err := receiver.Receive(context.Background(), badSignature); !errors.Is(err, ErrFabricEnvelopeSignatureInvalid) {
t.Fatalf("bad signature err = %v", err)
}
wrongScope := base
wrongScope.Scope = "organization"
if _, err := receiver.Receive(context.Background(), signedReceiverEnvelope(t, privateKey, keyID, wrongScope)); !errors.Is(err, ErrFabricEnvelopeUnauthorized) {
t.Fatalf("wrong scope err = %v", err)
}
wrongClass := base
wrongClass.ServiceClass = "cluster_admin"
if _, err := receiver.Receive(context.Background(), signedReceiverEnvelope(t, privateKey, keyID, wrongClass)); !errors.Is(err, ErrFabricEnvelopeUnauthorized) {
t.Fatalf("wrong class err = %v", err)
}
stale := base
stale.EnvelopedAt = "2026-05-16T00:00:00Z"
if _, err := receiver.Receive(context.Background(), signedReceiverEnvelope(t, privateKey, keyID, stale)); !errors.Is(err, ErrFabricEnvelopeUnauthorized) {
t.Fatalf("stale err = %v", err)
}
}
func TestFabricRuntimeReceiverRequiresTrustedKeyAndHandler(t *testing.T) {
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
keyID := ed25519EnvelopeKeyID(publicKey)
payload := signedReceiverEnvelope(t, privateKey, keyID, FabricServiceChannelEnvelope{
SchemaVersion: FabricServiceChannelEnvelopeSchema,
Scope: "platform",
ServiceClass: "platform_admin",
ObservedAt: "2026-05-17T00:00:00Z",
EnvelopedAt: "2026-05-17T00:00:01Z",
})
_, err = (FabricRuntimeReceiver{Keys: StaticEnvelopeKeyResolver{keyID: publicKey}, Now: fixedEnvelopeNow}).Receive(context.Background(), payload)
if !errors.Is(err, ErrFabricEnvelopeRuntimeRequired) {
t.Fatalf("handler err = %v", err)
}
_, err = (FabricRuntimeReceiver{Handler: recordingRuntimeHandler{}, Now: fixedEnvelopeNow}).Receive(context.Background(), payload)
if !errors.Is(err, ErrFabricEnvelopeSignatureInvalid) {
t.Fatalf("key resolver err = %v", err)
}
_, otherPrivateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate other key: %v", err)
}
untrusted := signedReceiverEnvelope(t, otherPrivateKey, "other-key", FabricServiceChannelEnvelope{
SchemaVersion: FabricServiceChannelEnvelopeSchema,
Scope: "platform",
ServiceClass: "platform_admin",
ObservedAt: "2026-05-17T00:00:00Z",
EnvelopedAt: "2026-05-17T00:00:01Z",
})
_, err = (FabricRuntimeReceiver{Keys: StaticEnvelopeKeyResolver{keyID: publicKey}, Handler: recordingRuntimeHandler{}, Now: fixedEnvelopeNow}).Receive(context.Background(), untrusted)
if !errors.Is(err, ErrFabricEnvelopeUnauthorized) {
t.Fatalf("untrusted key err = %v", err)
}
}
func signedReceiverEnvelope(t *testing.T, privateKey ed25519.PrivateKey, keyID string, envelope FabricServiceChannelEnvelope) []byte {
t.Helper()
canonical, err := json.Marshal(envelope)
if err != nil {
t.Fatalf("marshal envelope: %v", err)
}
payload, err := json.Marshal(SignedFabricServiceChannelEnvelope{
SchemaVersion: SignedFabricServiceChannelEnvelopeSchema,
Envelope: envelope,
Signature: FabricEnvelopeSignature{
KeyID: keyID,
Alg: "ed25519",
Signature: base64.StdEncoding.EncodeToString(ed25519.Sign(privateKey, canonical)),
SignedAt: "2026-05-17T00:00:01Z",
},
})
if err != nil {
t.Fatalf("marshal signed envelope: %v", err)
}
return payload
}
type recordingRuntimeHandler struct {
request FabricRequest
response FabricResponse
err error
}
func (h recordingRuntimeHandler) HandleFabricRequest(_ context.Context, request FabricRequest) (FabricResponse, error) {
h.request = request
if h.err != nil {
return FabricResponse{}, h.err
}
if h.response.StatusCode == 0 {
h.response = FabricResponse{StatusCode: http.StatusOK, Body: []byte(`{"ready":true}`)}
}
return h.response, nil
}