195 lines
7.0 KiB
Go
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
|
|
}
|