Skip to content

Commit 60c4463

Browse files
committed
platform api revamp
1 parent 87f8c56 commit 60c4463

5 files changed

Lines changed: 186 additions & 64 deletions

File tree

event-gateway/gateway-runtime/internal/connectors/receiver/websub/delivery.go

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,20 +144,19 @@ func (d *Deliverer) doDeliver(ctx context.Context, callbackURL, secret string, m
144144
// hopByHopHeaders contains the RFC 2616 hop-by-hop headers that must never be
145145
// forwarded to a downstream subscriber. Keys are lower-cased for comparison.
146146
var hopByHopHeaders = map[string]bool{
147-
"connection": true,
148-
"keep-alive": true,
149-
"proxy-authenticate": true,
150-
"proxy-authorization": true,
151-
"te": true,
152-
"trailers": true,
153-
"transfer-encoding": true,
154-
"upgrade": true,
155-
"host": true,
147+
"connection": true,
148+
"keep-alive": true,
149+
"te": true,
150+
"trailer": true,
151+
"transfer-encoding": true,
152+
"upgrade": true,
153+
"host": true,
156154
}
157155

158156
// internalHeaderPrefixes lists lower-cased prefixes used by the gateway for
159157
// internal metadata that must not be leaked to external subscribers.
160158
var internalHeaderPrefixes = []string{
159+
"proxy-",
161160
"x-internal-",
162161
}
163162

event-gateway/gateway-runtime/internal/connectors/receiver/websub/handler.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ func (h *HubHandler) handleSubscribe(w http.ResponseWriter, r *http.Request) {
106106
return
107107
}
108108
if shortCircuited {
109-
http.Error(w, "forbidden by policy", http.StatusForbidden)
109+
writePolicyResponse(w, nil, http.StatusForbidden, "forbidden by policy")
110110
return
111111
}
112112

@@ -210,7 +210,7 @@ func (h *HubHandler) handleUnsubscribe(w http.ResponseWriter, r *http.Request) {
210210
return
211211
}
212212
if shortCircuited {
213-
http.Error(w, "forbidden by policy", http.StatusForbidden)
213+
writePolicyResponse(w, nil, http.StatusForbidden, "forbidden by policy")
214214
return
215215
}
216216

@@ -342,7 +342,7 @@ func (h *WebhookReceiverHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
342342
}
343343
if shortCircuited {
344344
slog.Info("Inbound request rejected by policy", "channel", channelName, "binding", h.bindingName)
345-
http.Error(w, "forbidden by policy", http.StatusForbidden)
345+
writePolicyResponse(w, processed, http.StatusForbidden, "forbidden by policy")
346346
return
347347
}
348348

@@ -355,6 +355,33 @@ func (h *WebhookReceiverHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
355355
w.WriteHeader(http.StatusAccepted)
356356
}
357357

358+
// writePolicyResponse writes the HTTP response from a short-circuited policy execution.
359+
// If msg is non-nil and carries a status code in Metadata["status_code"], that status is used
360+
// along with any headers and body from the message. Otherwise the fallback status and body are used.
361+
func writePolicyResponse(w http.ResponseWriter, msg *connectors.Message, fallbackStatus int, fallbackBody string) {
362+
if msg != nil {
363+
statusCode := fallbackStatus
364+
if sc, ok := msg.Metadata["status_code"]; ok {
365+
if code, ok := sc.(int); ok && code > 0 {
366+
statusCode = code
367+
}
368+
}
369+
for k, vals := range msg.Headers {
370+
for _, v := range vals {
371+
w.Header().Add(k, v)
372+
}
373+
}
374+
if len(msg.Value) > 0 {
375+
w.WriteHeader(statusCode)
376+
_, _ = w.Write(msg.Value)
377+
return
378+
}
379+
w.WriteHeader(statusCode)
380+
return
381+
}
382+
http.Error(w, fallbackBody, fallbackStatus)
383+
}
384+
358385
func (h *WebhookReceiverHandler) publishToBrokerDriver(ctx context.Context, kafkaTopic string, msg *connectors.Message) error {
359386
if err := h.brokerDriver.Publish(ctx, kafkaTopic, msg); err != nil {
360387
return fmt.Errorf("failed to publish to broker-driver: %w", err)

event-gateway/gateway-runtime/internal/hub/hub.go

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,20 @@ type ChannelChainKeySet struct {
3838

3939
// ChannelBinding holds the runtime state for a registered channel.
4040
type ChannelBinding struct {
41-
APIID string
42-
Name string
43-
Mode string // "websub" or "protocol-mediation"
44-
Context string
45-
Version string
46-
Vhost string
47-
SubscribeChainKey string
48-
InboundChainKey string
49-
OutboundChainKey string
50-
BrokerDriverTopic string
51-
Ordering string // "ordered" or "unordered"
52-
Channels map[string]string // channel-name → Kafka topic (WebSubApi only)
53-
ChannelChainKeys map[string]ChannelChainKeySet // channel-name → per-channel chain keys
41+
APIID string
42+
Name string
43+
Mode string // "websub" or "protocol-mediation"
44+
Context string
45+
Version string
46+
Vhost string
47+
SubscribeChainKey string
48+
InboundChainKey string
49+
OutboundChainKey string
50+
BrokerDriverTopic string
51+
Ordering string // "ordered" or "unordered"
52+
Channels map[string]string // channel-name → Kafka topic (WebSubApi only)
53+
KafkaTopicToChannel map[string]string // Kafka topic → channel-name (reverse of Channels, cached)
54+
ChannelChainKeys map[string]ChannelChainKeySet // channel-name → per-channel chain keys
5455
}
5556

5657
// Hub is the central message router. It holds the policy engine reference and
@@ -70,9 +71,16 @@ func NewHub(eng *engine.Engine) *Hub {
7071
}
7172

7273
// RegisterBinding adds a channel binding to the hub.
74+
// It automatically builds the KafkaTopicToChannel reverse map from b.Channels.
7375
func (h *Hub) RegisterBinding(b ChannelBinding) {
7476
h.mu.Lock()
7577
defer h.mu.Unlock()
78+
if len(b.Channels) > 0 {
79+
b.KafkaTopicToChannel = make(map[string]string, len(b.Channels))
80+
for channelName, topic := range b.Channels {
81+
b.KafkaTopicToChannel[topic] = channelName
82+
}
83+
}
7684
h.bindings[b.Name] = &b
7785
}
7886

@@ -138,7 +146,7 @@ func (h *Hub) ProcessSubscribe(ctx context.Context, bindingName string, msg *con
138146
}
139147
if result.ShortCircuited {
140148
logShortCircuit("Subscribe request short-circuited by hub policy", bindingName, binding.SubscribeChainKey, result.ImmediateResponse)
141-
return nil, true, nil
149+
return immediateResponseToMessage(result.ImmediateResponse), true, nil
142150
}
143151
if err := ApplyRequestHeaderResult(result, msg); err != nil {
144152
return nil, false, fmt.Errorf("failed to apply subscribe header result: %w", err)
@@ -158,7 +166,7 @@ func (h *Hub) ProcessSubscribe(ctx context.Context, bindingName string, msg *con
158166
}
159167
if result.ShortCircuited {
160168
logShortCircuit("Subscribe request short-circuited by channel policy", bindingName, keys.SubscribeChainKey, result.ImmediateResponse)
161-
return nil, true, nil
169+
return immediateResponseToMessage(result.ImmediateResponse), true, nil
162170
}
163171
if err := ApplyRequestHeaderResult(result, msg); err != nil {
164172
return nil, false, fmt.Errorf("failed to apply subscribe channel header result: %w", err)
@@ -190,7 +198,7 @@ func (h *Hub) ProcessInbound(ctx context.Context, bindingName string, msg *conne
190198
}
191199
if result.ShortCircuited {
192200
logShortCircuit("Inbound message short-circuited by hub policy", bindingName, binding.InboundChainKey, result.ImmediateResponse)
193-
return nil, true, nil
201+
return immediateResponseToMessage(result.ImmediateResponse), true, nil
194202
}
195203
if err := ApplyRequestHeaderResult(result, msg); err != nil {
196204
return nil, false, fmt.Errorf("failed to apply inbound header result: %w", err)
@@ -202,7 +210,7 @@ func (h *Hub) ProcessInbound(ctx context.Context, bindingName string, msg *conne
202210
return nil, false, fmt.Errorf("inbound body policy execution failed: %w", err)
203211
}
204212
if bodyResult.ShortCircuited {
205-
return nil, true, nil
213+
return immediateResponseToMessage(bodyResult.ImmediateResponse), true, nil
206214
}
207215
if err := ApplyRequestBodyResult(bodyResult, msg); err != nil {
208216
return nil, false, fmt.Errorf("failed to apply inbound body result: %w", err)
@@ -223,7 +231,7 @@ func (h *Hub) ProcessInbound(ctx context.Context, bindingName string, msg *conne
223231
}
224232
if result.ShortCircuited {
225233
logShortCircuit("Inbound message short-circuited by channel policy", bindingName, keys.InboundChainKey, result.ImmediateResponse)
226-
return nil, true, nil
234+
return immediateResponseToMessage(result.ImmediateResponse), true, nil
227235
}
228236
if err := ApplyRequestHeaderResult(result, msg); err != nil {
229237
return nil, false, fmt.Errorf("failed to apply inbound channel header result: %w", err)
@@ -235,7 +243,7 @@ func (h *Hub) ProcessInbound(ctx context.Context, bindingName string, msg *conne
235243
return nil, false, fmt.Errorf("inbound channel body policy execution failed: %w", err)
236244
}
237245
if bodyResult.ShortCircuited {
238-
return nil, true, nil
246+
return immediateResponseToMessage(bodyResult.ImmediateResponse), true, nil
239247
}
240248
if err := ApplyRequestBodyResult(bodyResult, msg); err != nil {
241249
return nil, false, fmt.Errorf("failed to apply inbound channel body result: %w", err)
@@ -292,7 +300,7 @@ func (h *Hub) ProcessOutbound(ctx context.Context, bindingName string, msg *conn
292300
// Apply channel-level outbound chain if present.
293301
// msg.Topic here is the Kafka topic; resolve back to channel name.
294302
if msg.Topic != "" && len(binding.ChannelChainKeys) > 0 {
295-
channelName := resolveChannelName(binding.Channels, msg.Topic)
303+
channelName := resolveChannelName(binding.KafkaTopicToChannel, binding.Channels, msg.Topic)
296304
if channelName != "" {
297305
if keys, ok := binding.ChannelChainKeys[channelName]; ok && keys.OutboundChainKey != "" {
298306
chain := h.engine.GetChain(keys.OutboundChainKey)
@@ -330,9 +338,13 @@ func (h *Hub) ProcessOutbound(ctx context.Context, bindingName string, msg *conn
330338
return msg, false, nil
331339
}
332340

333-
// resolveChannelName reverse-maps a Kafka topic name to the channel name
334-
// using the binding's channel-name → Kafka-topic map.
335-
func resolveChannelName(channels map[string]string, kafkaTopic string) string {
341+
// resolveChannelName reverse-maps a Kafka topic name to the channel name.
342+
// It first checks the pre-built kafkaTopicToChannel reverse map for an O(1) lookup,
343+
// and falls back to an O(n) scan over channels only if the cached map is absent.
344+
func resolveChannelName(kafkaTopicToChannel map[string]string, channels map[string]string, kafkaTopic string) string {
345+
if len(kafkaTopicToChannel) > 0 {
346+
return kafkaTopicToChannel[kafkaTopic]
347+
}
336348
for channelName, topic := range channels {
337349
if topic == kafkaTopic {
338350
return channelName
@@ -341,6 +353,28 @@ func resolveChannelName(channels map[string]string, kafkaTopic string) string {
341353
return ""
342354
}
343355

356+
// immediateResponseToMessage encodes an ImmediateResponseResult from the policy engine
357+
// into a connectors.Message so callers can write the policy-provided HTTP response.
358+
// The status code is stored in Metadata["status_code"] (int), body in Value,
359+
// and response headers (single-value) in Headers ([]string slice per key).
360+
// Returns nil when resp is nil.
361+
func immediateResponseToMessage(resp *engine.ImmediateResponseResult) *connectors.Message {
362+
if resp == nil {
363+
return nil
364+
}
365+
headers := make(map[string][]string, len(resp.Headers))
366+
for k, v := range resp.Headers {
367+
headers[k] = []string{v}
368+
}
369+
return &connectors.Message{
370+
Value: resp.Body,
371+
Headers: headers,
372+
Metadata: map[string]interface{}{
373+
"status_code": resp.StatusCode,
374+
},
375+
}
376+
}
377+
344378
// logShortCircuit keeps Info logs to metadata only; ImmediateResponse.Body is
345379
// user-visible content and must not contain sensitive information.
346380
func logShortCircuit(message, bindingName, chainKey string, resp *engine.ImmediateResponseResult) {

platform-api/src/internal/model/deployment.go

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ type DeploymentContent struct {
6161
type DeploymentStatus string
6262

6363
const (
64-
DeploymentStatusDeployed DeploymentStatus = "DEPLOYED"
65-
DeploymentStatusUndeployed DeploymentStatus = "UNDEPLOYED"
64+
DeploymentStatusDeployed DeploymentStatus = "DEPLOYED"
65+
DeploymentStatusUndeployed DeploymentStatus = "UNDEPLOYED"
6666
DeploymentStatusDeploying DeploymentStatus = "DEPLOYING"
6767
DeploymentStatusUndeploying DeploymentStatus = "UNDEPLOYING"
6868
DeploymentStatusFailed DeploymentStatus = "FAILED"
69-
DeploymentStatusArchived DeploymentStatus = "ARCHIVED" // Derived state: exists in history but not in status table
69+
DeploymentStatusArchived DeploymentStatus = "ARCHIVED" // Derived state: exists in history but not in status table
7070
)
7171

7272
// DeploymentInfo is a lightweight representation of a deployment
@@ -121,12 +121,13 @@ type WebSubAPIDeploymentYAML struct {
121121

122122
// WebSubAPIDeploymentSpec represents the spec section of the WebSub API deployment YAML
123123
type WebSubAPIDeploymentSpec struct {
124-
DisplayName string `yaml:"displayName"`
125-
Version string `yaml:"version"`
126-
Context string `yaml:"context"`
127-
Vhosts *WebSubAPIDeploymentVhosts `yaml:"vhosts,omitempty"`
128-
Channels []WebSubAPIDeploymentChannel `yaml:"channels,omitempty"`
129-
Policies []Policy `yaml:"policies,omitempty"`
124+
DisplayName string `yaml:"displayName"`
125+
Version string `yaml:"version"`
126+
Context string `yaml:"context"`
127+
Vhosts *WebSubAPIDeploymentVhosts `yaml:"vhosts,omitempty"`
128+
Hub WebSubHub `json:"hub" yaml:"hub"`
129+
Receiver *WebSubReceiver `json:"receiver,omitempty" yaml:"receiver,omitempty"`
130+
Delivery *WebSubDelivery `json:"delivery,omitempty" yaml:"delivery,omitempty"`
130131
}
131132

132133
// WebSubAPIDeploymentVhosts represents vhost configuration in the WebSub API deployment YAML
@@ -140,3 +141,34 @@ type WebSubAPIDeploymentChannel struct {
140141
Name string `yaml:"name"`
141142
Method string `yaml:"method"`
142143
}
144+
145+
// WebSubDelivery Delivery configuration for the WebSub API - handles outbound event delivery to subscribers.
146+
type WebSubDelivery struct {
147+
// Policies List of policies applied when delivering events to subscriber callback URLs (e.g., hmac-signature-validation)
148+
Policies *[]Policy `json:"policies,omitempty" yaml:"policies,omitempty"`
149+
}
150+
151+
// WebSubHub Hub configuration for the WebSub API - handles subscriber management and event fan-out.
152+
type WebSubHub struct {
153+
// Channels List of topic channels available for subscription
154+
Channels []HubChannel `json:"channels" yaml:"channels"`
155+
156+
// Policies List of policies applied at the hub level (e.g., api-key-auth for subscribers)
157+
Policies *[]Policy `json:"policies,omitempty" yaml:"policies,omitempty"`
158+
}
159+
160+
// HubChannel A subscribable topic channel within the WebSub hub.
161+
type HubChannel struct {
162+
// Name Channel name or topic identifier relative to API context.
163+
Name string `json:"name" yaml:"name"`
164+
// Method The method by which the channel is identified (e.g., "SUB" for subscription-based, "PUB" for publish-only)
165+
Method string `json:"method" yaml:"method"`
166+
// Policies List of policies applied only to this channel (e.g., rbac)
167+
Policies *[]Policy `json:"policies,omitempty" yaml:"policies,omitempty"`
168+
}
169+
170+
// WebSubReceiver Receiver configuration for the WebSub API - handles inbound event publishing from publishers.
171+
type WebSubReceiver struct {
172+
// Policies List of policies applied to inbound webhook requests (e.g., hmac-signature-validation)
173+
Policies *[]Policy `json:"policies,omitempty" yaml:"policies,omitempty"`
174+
}

0 commit comments

Comments
 (0)