From be8a6252f50722787b364437fc529a306d139e8a Mon Sep 17 00:00:00 2001 From: Tharindu Dharmarathna Date: Wed, 13 May 2026 19:07:27 +0530 Subject: [PATCH 1/2] change the spec to support 0.5.0 and 0.6.0 --- distribution/all-in-one/docker-compose.yaml | 1 + .../api/management-openapi.yaml | 41 +-- .../pkg/api/management/generated.go | 246 +++++++++--------- .../gateway-controller/pkg/policy/builder.go | 26 +- .../pkg/policyxds/event_channel_translator.go | 41 ++- platform-api/src/api/generated.go | 35 +-- platform-api/src/internal/model/deployment.go | 60 ++++- platform-api/src/internal/model/websub_api.go | 73 ++++-- .../src/internal/service/websub_api.go | 72 +++-- .../internal/service/websub_api_deployment.go | 88 ++++++- platform-api/src/resources/openapi.yaml | 36 +-- 11 files changed, 443 insertions(+), 276 deletions(-) diff --git a/distribution/all-in-one/docker-compose.yaml b/distribution/all-in-one/docker-compose.yaml index e91d923a7..9db075741 100644 --- a/distribution/all-in-one/docker-compose.yaml +++ b/distribution/all-in-one/docker-compose.yaml @@ -50,6 +50,7 @@ services: condition: service_healthy platform-api: + image: ghcr.io/wso2/api-platform/platform-api:latest build: context: ../../platform-api dockerfile: Dockerfile diff --git a/gateway/gateway-controller/api/management-openapi.yaml b/gateway/gateway-controller/api/management-openapi.yaml index 635d89729..3dc645643 100644 --- a/gateway/gateway-controller/api/management-openapi.yaml +++ b/gateway/gateway-controller/api/management-openapi.yaml @@ -3761,7 +3761,7 @@ components: description: Custom virtual host/domain for sandbox traffic pattern: '^[a-zA-Z0-9\.\-]+$' example: sandbox-api.example.com - policies: + allChannels: $ref: '#/components/schemas/WebSubAllChannelPolicies' channels: type: object @@ -3779,9 +3779,26 @@ components: WebSubChannel: type: object description: A single channel definition with optional per-channel policy overrides. + properties: + on_subscription: + $ref: '#/components/schemas/WebSubEventPolicies' + on_unsubscription: + $ref: '#/components/schemas/WebSubEventPolicies' + on_message_received: + $ref: '#/components/schemas/WebSubEventPolicies' + on_message_delivery: + $ref: '#/components/schemas/WebSubEventPolicies' + + # WebSubEventPolicies defines policies for a single event type. + WebSubEventPolicies: + type: object + description: Policies for a single event type. properties: policies: - $ref: '#/components/schemas/WebSubChannelPolicies' + type: array + description: List of policies applied for this event type. + items: + $ref: '#/components/schemas/Policy' # WebSubAllChannelPolicies defines policies applied to all channels for each event type. WebSubAllChannelPolicies: @@ -3789,25 +3806,13 @@ components: description: Policies applied to all channels, organized by event type. properties: on_subscription: - type: array - description: Policies applied when a client subscribes to a channel (e.g., api-key-auth) - items: - $ref: '#/components/schemas/Policy' + $ref: '#/components/schemas/WebSubEventPolicies' on_unsubscription: - type: array - description: Policies applied when a client unsubscribes from a channel (e.g., api-key-auth) - items: - $ref: '#/components/schemas/Policy' + $ref: '#/components/schemas/WebSubEventPolicies' on_message_received: - type: array - description: Policies applied when a message is received from the publisher via webhook (e.g., hmac-signature-validation) - items: - $ref: '#/components/schemas/Policy' + $ref: '#/components/schemas/WebSubEventPolicies' on_message_delivery: - type: array - description: Policies applied when delivering a message to a subscriber callback URL (e.g., hmac-sign-messages) - items: - $ref: '#/components/schemas/Policy' + $ref: '#/components/schemas/WebSubEventPolicies' # WebSubChannelPolicies defines per-channel policies organized by event type. WebSubChannelPolicies: diff --git a/gateway/gateway-controller/pkg/api/management/generated.go b/gateway/gateway-controller/pkg/api/management/generated.go index edfc7838c..a7c728937 100644 --- a/gateway/gateway-controller/pkg/api/management/generated.go +++ b/gateway/gateway-controller/pkg/api/management/generated.go @@ -1584,42 +1584,45 @@ type WebSubAPIRequestKind string // WebSubAllChannelPolicies Policies applied to all channels, organized by event type. type WebSubAllChannelPolicies struct { - // OnMessageDelivery Policies applied when delivering a message to a subscriber callback URL (e.g., hmac-sign-messages) - OnMessageDelivery *[]Policy `json:"on_message_delivery,omitempty" yaml:"on_message_delivery,omitempty"` + // OnMessageDelivery Policies for a single event type. + OnMessageDelivery *WebSubEventPolicies `json:"on_message_delivery,omitempty" yaml:"on_message_delivery,omitempty"` - // OnMessageReceived Policies applied when a message is received from the publisher via webhook (e.g., hmac-signature-validation) - OnMessageReceived *[]Policy `json:"on_message_received,omitempty" yaml:"on_message_received,omitempty"` + // OnMessageReceived Policies for a single event type. + OnMessageReceived *WebSubEventPolicies `json:"on_message_received,omitempty" yaml:"on_message_received,omitempty"` - // OnSubscription Policies applied when a client subscribes to a channel (e.g., api-key-auth) - OnSubscription *[]Policy `json:"on_subscription,omitempty" yaml:"on_subscription,omitempty"` + // OnSubscription Policies for a single event type. + OnSubscription *WebSubEventPolicies `json:"on_subscription,omitempty" yaml:"on_subscription,omitempty"` - // OnUnsubscription Policies applied when a client unsubscribes from a channel (e.g., api-key-auth) - OnUnsubscription *[]Policy `json:"on_unsubscription,omitempty" yaml:"on_unsubscription,omitempty"` + // OnUnsubscription Policies for a single event type. + OnUnsubscription *WebSubEventPolicies `json:"on_unsubscription,omitempty" yaml:"on_unsubscription,omitempty"` } // WebSubChannel A single channel definition with optional per-channel policy overrides. type WebSubChannel struct { - // Policies Policies applied to a specific channel, organized by event type. - Policies *WebSubChannelPolicies `json:"policies,omitempty" yaml:"policies,omitempty"` -} + // OnMessageDelivery Policies for a single event type. + OnMessageDelivery *WebSubEventPolicies `json:"on_message_delivery,omitempty" yaml:"on_message_delivery,omitempty"` -// WebSubChannelPolicies Policies applied to a specific channel, organized by event type. -type WebSubChannelPolicies struct { - // OnMessageDelivery Policies applied when delivering a message for this channel - OnMessageDelivery *[]Policy `json:"on_message_delivery,omitempty" yaml:"on_message_delivery,omitempty"` + // OnMessageReceived Policies for a single event type. + OnMessageReceived *WebSubEventPolicies `json:"on_message_received,omitempty" yaml:"on_message_received,omitempty"` - // OnMessageReceived Policies applied when a message is received for this channel - OnMessageReceived *[]Policy `json:"on_message_received,omitempty" yaml:"on_message_received,omitempty"` + // OnSubscription Policies for a single event type. + OnSubscription *WebSubEventPolicies `json:"on_subscription,omitempty" yaml:"on_subscription,omitempty"` - // OnSubscription Policies applied when a client subscribes to this channel (e.g., rbac) - OnSubscription *[]Policy `json:"on_subscription,omitempty" yaml:"on_subscription,omitempty"` + // OnUnsubscription Policies for a single event type. + OnUnsubscription *WebSubEventPolicies `json:"on_unsubscription,omitempty" yaml:"on_unsubscription,omitempty"` +} - // OnUnsubscription Policies applied when a client unsubscribes from this channel - OnUnsubscription *[]Policy `json:"on_unsubscription,omitempty" yaml:"on_unsubscription,omitempty"` +// WebSubEventPolicies Policies for a single event type. +type WebSubEventPolicies struct { + // Policies List of policies applied for this event type. + Policies *[]Policy `json:"policies,omitempty" yaml:"policies,omitempty"` } // WebhookAPIData defines model for WebhookAPIData. type WebhookAPIData struct { + // AllChannels Policies applied to all channels, organized by event type. + AllChannels *WebSubAllChannelPolicies `json:"allChannels,omitempty" yaml:"allChannels,omitempty"` + // Channels Per-channel configuration keyed by channel name. Each key is a channel name and defines policies applied only to that channel. Channels *map[string]WebSubChannel `json:"channels,omitempty" yaml:"channels,omitempty"` @@ -1632,9 +1635,6 @@ type WebhookAPIData struct { // DisplayName Human-readable API name (must be URL-friendly - only letters, numbers, spaces, hyphens, underscores, and dots allowed) DisplayName string `json:"displayName" yaml:"displayName"` - // Policies Policies applied to all channels, organized by event type. - Policies *WebSubAllChannelPolicies `json:"policies,omitempty" yaml:"policies,omitempty"` - // Version Semantic version of the API Version string `json:"version" yaml:"version"` @@ -4629,106 +4629,104 @@ var swaggerSpec = []string{ "qawuWrgqhYuTw+tmMLiVv9abl1/Q4CoezHSaMPlkc54wn7Ygp6bsDNOdR0fxwEb3bPDFA0wjGAS86od+", "9Ojq5q2quHNseYTEKHewKPNCFPv+r4m4sqFpx6Iy3Zefi3rn0Z/jATjnr1nLO6lmmJ1nHFMrcGmD2Sl/", "iWX+qxyBShcz07O+xkvLWvkFDUZh+PWk1230HJQci++fCt7rldZs6eVrtTC30PeBYtqWqhwkjuFzHuMT", - "2i7YBmHwq9Riv7rI9+4RntToj3vp8n3uBytVKBxUGZoZIAwc6PvMDBRmpbApRmPo2MS7C2z51fOLxbT0", - "gWDkIM+YLGoeSEo9r6EtPhY2Lq9MEg98j4wQBvceBA9i8QtjgTTGyE7NjmaGpAe56g/H8T1ekEEtAxHL", - "ItlDkS5dY5v52M1QGwfPojf5fKBcjMXSXG5pSQk0IJPKSlJ0pe5mzi+LELbVS7J6TlI5qSiGen2m6VZd", - "HiCmj2RGLEm0hBrnCyJK4uJLUlYRKhZBYoOir9OmJAkPoLOiUt/kVJZIhq6/izvdie1XXtivtpAWi/71", - "NGTIuvNf0UTIl3rMDMk2OIfOiD3jNbAyz8RJQh4LJlUV3CBVn7VNNbpKL81o/DbQBwTpiG+kzXIdaOlt", - "oOtzFyhPgprxho4W/+ormsgjo+tyWQejetmHZvPFIxZ+B0iN2EvRkp/nek1R8+T5F2sadL/YYAf32nUP", - "ZEde0JCU/hP950oyyMIeU5uTrQkeTaqOKpYvXICZvY2hZIna/b5dskAEBu4gfJyZNPmdkS75zH4+ffmK", - "lmwSjftKde5/UJhtbEDl4vPSvsmmkOeoTTbOqDy1hP2aUjqiNBKBGS8YhqpeDxSxYxnV+OXq0x4XcbVN", - "D67FdSj5fa7zq2v+HptgHhKUlWtz15qo/P9iu7IQo9hClVWTLUN1xgseb+SQLZg1DZJ02kcirsPvjos8", - "69jab3fa+7IqD5+ZHYcxNg+JiKm6Q9S0bacOJjBHW7DT9YcroH8MnBhjFFCGbiF003qP2ksi6bXdD65H", - "iKDs5wzXk5tWmCEsah//fH3du8psF8qovTwGnNRD6rpy2+hUH1Fa7YePbq/TSQoxifQhLSFn53cisIkk", - "dbCrgE7rJ5M8yFnIvJuVmeynlnXYIDl806CKiG7ABBb6qtYED+MLiYnHY8g8FUGotshOdi4pvCNMLrWh", - "awzIxPHR5lLFXFQbhz4v8GRBd8yzrmQFJYStW65QTNfv3ER8kwuCAD3keQxs9c4vgNhH21YJEkpQePlS", - "/WWPKEZ0JwEcew70/Qk31sKYnw1jxpvKhFCtFDhK0KMN2GqpglRvQ3dSY/m0kK5GnnVs2ey/t+fvuh/B", - "6fnldfen7unJ9Tn/tR9cdLtn/3t9enry9Ze7k4fu25O77j9P3n/o3Lz7fnz5nv5+cdJ5d3r1x7ur7mD/", - "7F/nb08fbk4uzm8eT/88+efbu4+f+0G73e4HvLXzj2eGHtJY7Xhii/W2HREHnJX/xSQlmy1ZGOd7GgU5", - "3F2EHFaxv86zcSQ5Qx5rGsa+z/2ng+UKJI+YZZhWbhKvIjZkJNPJCESDuPDUyuqkHYxYtyJOZAKMC55P", - "xJw97N3dIVGfl1MaDgWU6VomCWwOPR+RCRFn8HJQUgCBS5QDgWcrlnyVr2TnVdtM1ekWQ5Klm6/OrpJS", - "rhkOrjxFUOMwX8uiIYX+2wk1RcvEUUp+e4iaW0lUTk0kPe3t7R4eHRnzDPJ2W5W8asPPC+zKSUnCjpIJ", - "m9SgBungBRX5SvnIXKqY/Q5gFmSUEGR15wgGIranYijP0Zui46ze1O4yOf5SOAd6ppw+nVQaAjm0TObF", - "YQf9eNDp2GjvaGAf7LoHNvxh9419cPDmzeHhwUFHZNwwP01VmFP7lq6V1026vss7LbeNirnIbp55GFWJ", - "Gka4kFO2YLCYUYgTooo692B5IqwTFIQUDMM4cFcSSEyS2wyA+P44uUfcpmgc+ZXOH/cJPny4SC4nB8k3", - "AKM7j1CEU29PAkIr2QzyJ0zXincG4urattFvE5ew8x6uE6KmgMZPvGVebl1+Un5H7qcIBSddBQt/xAhP", - "UlzIRhqWBQhO4czcvrEWwYw6XF/SWhsIhqmvs5tQ7uia2WW1Xd4SmlORYy+oaQJqnmYRvjKf98RVZrWR", - "hoKne6Jxu9z/EEdL1MlsDvyiQAJ6ZD/ywKNKx5DHuTOdFUVSnKs1ccasDnC9xTT0lDqUmYNTOxM49htq", - "eKmeqlHMDEJkZAJ1NchquKzZ4zZp5odMQd0WlB0tUbGHwdD3HArsVDT5xhyBY3mPHPQxgu5EnKJaTTAS", - "QlcFBk3iUbkxUNuvCEogq+BilDgIZnyp1PnyIATPTHL08xDqnkANNg2+Aw+Ge2vgHSSE1rP/zetgNLqX", - "YfnPQM6yfQAzaevhDQSLR4WW2Q14h2i5uA8mwKMEdM+Kcv4OmSz7t5OuO7egq93ZsqlYSWGf3TBo2OiZ", - "RUop9HyyEcwagsnEolwm3Ibdh9i4Y8ZrJMMgPR5uJijroZu2uly4cI0sQlELFdK/kG/SWQ3fxBhfXHHf", - "ZINrU3b76qHKIv2RGWKS84YiWyrjrAVkUlELCFu4BUIMePLY1HDlDGHKzBxOCVUmk/nMmGWrJjnaeah8", - "wp2p+/T153ct535Hgr+WNJtVDjkS0sOsc5GQS3eNszdNa5mi5t6Tb9LOpyaczr02jBELKYNicmRGnnGN", - "5GcvF9DeMwW0MwI+a4Q6UyR/AfVr60W11yiYXRrDbjh1qyyMXYhepwAoo9e8Lk4IGIdg6MiMeulsyovw", - "WtqNBkk2YFK5owW0y8Y5mkO+3upC82wOeY1g9+KD3MYLhRqzJktarzZNvAD835OLD0zx/fPq00eVjPRC", - "IfKcnE+hXYXHxaEQeVv1JlY+LVaeYEE+Vh64SS7+OsfNnw19Bqt03uD4HDHxmp530eXOzUGqCPllqcJu", - "sKOcfbnCwfASsucIja9GRHz1AuHrGP9uQLpniHbXDnLPENx+DZI7pz5fhKVTQ+5WILS9ZhFtHsjWy8A2", - "60vME9OeOZS9buL4F3A9bmTQODfDLxLyng1EVjfcvcG1uSPaC/MUdmTdlinRbFVggL1pytCbinm5oPRJ", - "r/uedVoP+EQJYhPoZYpQK+LW3zAR01P34KZamI18VdsN6i4zfc5MzNyAFeGEAYnHlQHJdyhAOI0LSILm", - "Eq5CgFDwTyPSdafIZA8lgWttaoi54VPWmIFR1uZSE3jzRJQLjOK1dczaXQl4e5mA6JYbi06EIIZ8Z5I/", - "EvsOzILYXv0IaAXSNYu8UyyenW8w8t4jvkVdGTG9RPfhV26bSdLb4FPgIID5724LeBQ4MABBCPwwuGNO", - "qawWQUN96ye5Kp6YDvGytpqH8OVAdWGjmM2pViaHrzc31dgoMzQl6d3qPgAjPelKrZiNxtbNqQ24kmM2", - "gFsDcOX9hGzaVtu0LMDDUmzK6sCUokTsVauCKfJ+5IBQJCsQxDS0pYXHdEgYoBrhqlcJTYbUz8VD06Ks", - "2+x1SE3YtvkWl5r+Obtlu1JBMLnO6wOxG/N23uDdStq2OxgpL768Us1l8k4mCPmcsETa5Ou3a5MJfh0K", - "JFm6hkMk5nZXXJngkG7CJK/Pak/w7iVA+9GrWdSEvfhCxwc4jbMeHnicgGzq/8sdHHicvMypgcfJSh4Z", - "WIkDA2xNXttpASXLM5wVeJy8+EGBR29Nat5IGMrh8ONk4ScEHifm4wEM4uqfDUgTvvPQnZ4ZyJ4PmOE4", - "wONkoWcBcmzaZDZOadNl9sXjZHWOABTEt4rqTfL/vMn/j5NXmPnPRbYxMMuZlLNn/z9OZkz9f5w8N12R", - "t5A/YW+rB+tR+SYhd6Ykf645XjbDv4yEF/IaHyfrltvfrPzWyvB/nNRK73+cNJHbv+rSOY92btxcmSZg", - "L5rHv/IypSXxC9aO8zzZsL0/Wxa/sDRrp/CviUJ81T5CLl0/cYuWmas/E0RssvTXDrWqAGPRJv3z0/Rr", - "gJoW+Z00kKD/OJmenb9W1sV6ZeWvhRVQIyX/+cLVVDJ+DRHKxuaev9ctZGhqDv66WAyb3PtN7v2zQGyT", - "mdR44n2j+Fppu6xswn0zSL1YRH5eiv3jZJNfvwHVFFRfTXJ909bhy6TVvyYAMifSLxKANln0myz6VQPS", - "jaHabAr9C1mpzafO1wgi5PPmX5d5WpYpv44aYpMmv0mTf9XG95Qc+cZReexE9bLjL057vcaT40Ms86bN", - "eyNpn/Wz4i9Oe9ms+GI9/QvxVk/H4uZz4lNClpsTn/ZbnhOP7hGe0BFr63XmxS86M/3QlJk+dqLejMnp", - "ksNfMDldk7GVzk3PYIFCwESMF5earlYon5leshOlXl9QlriRX5oxhKY0vdTdnRKxKLJQsjqb+1Drpnmn", - "MvOKUr01sWsMG3Lm0QyZ3glX1k301sh/1tVq6ZiT207b/azhkap+mw1Ot0NWOAfcTHW9VPBkNV4sE7ya", - "gmX7RQk165EHvhDZrs4CT2aoOglcvfas20vzkrsu8jqP+m7cPJkibC+TFL4m8sV4PcPobsOGdc0c8ISG", - "eingC1GVIlC/VNH7i/kGnRf0DTb3kb4GvKqAjqatfowItWHkTQmJXiJCT3rdJQZEVY/1w6EnvW55IPQS", - "QX4ano/mpNddXDCUkbHcMCjrsTwAisXIbd/jJS5e522izbpkSh5qxTUlo5oimTWDqQsLeCYytNLhTk3S", - "FbSxnzhbLyzWKTutGepUa7wYa0a23oz9UmhsqdHMRBiKPKFmfBO+rBu+ZLP1igKXqRA1JeYZA6Z20DKR", - "/bohy5TwZ7lhEm7MsUpdS/NclTWJVpbRXS9eqVbixcKVlQQs2ztRxKxJsLJ5ea4KVSZSWx2olG89K045", - "DLES2PUR03pauQHLolqMXiYOuR6Sw/hY52K3WYu3ZhBSUVAvBtms7jMHHxcsVK/QYO8s02DfxBRfAfaU", - "A8FC7fG5a0vUhin2/WwFJaaBVFJVQp6I5xS9CjtgTYpMrI82ryox8XzRemZtiTIRAtey0oNHAAT7e/Zg", - "QhHAMHCT84YocEJXhPhH6BG6yPHG0G+BCKOh94hcEZb4DUZe9OtvbXBDUCJA79FE1JedgDDQxUpCNQJe", - "4IRjBkDqALVojY48ws9jl8TgZjqnMk3GTVUv1t0q2RTA2BTAeE0AW1VfolFwrTBbVrCsRKM4KMh7ERSc", - "rejENLI21Sc2iLbyiFYAiUYNxGWXl2gMiFYOckTE40UgZ1NvYlNvYrnQySZobU4Nl+IZsxHT8/+uALbl", - "m4iN1XSodN4jjO69MCbKi1fGAQwYa0U+dJSLLiamAR+/opDE63HMZy808ap0xKbixKbixGszuMuKTDQe", - "QCDIwYiW73Ncql0FmESMoe8DQkPMuEx83QaXiMY4IPIHDSdFlDSMaT9gaAQdGvOx89c4oovIM0FOjD06", - "AVGMo5AgInZbi5smV5LgBUqd6KLufoOcg2T/xSR7u8vjr5uArXuIvT+RC+z8NWoJdK10ai1J1lhxulz1", - "+oxevvdwxViXSBNDMiIKHDyJ+I1kFDCDSRgs8mn3DIxjQnnoi5sD7X7AHksvlGifx4SZRJQbOx4blnrG", - "Jj+5EXaAhiFGIEKYeISiwEEmbheBRDHyBaXwisYXcBypsuGGovDSfhH1P0TknBOY8NNVIocisi7OKggT", - "W6TLf5YnGI6tO2moMusn8iEdhnjcfiDhXtsJxzv3u9CPRnDXallfvYAtTrIsY0ShCymfEXUaA1I4gATZ", - "ESTkIcRc2kiEnCIz9kJC7zC6+tcHMIZeANSnIPm0lTnccWydqTd6euNJgqGciBNqHVt7nb03dmfX7hxe", - "73aO9zvHnc6/mVnnGmlsWdLXLP/2ia/dMzhArLFgbOETmbBCfLoauyFvYer22mDsES7gIQaetHGGHvJd", - "ssIw/1Jp4BI8003S7tlK5n4DW8doYZhWbekQJfnP0E2a5TU1/7uH8BiygfqqOgFTXnJ2k1xwJc9McXlE", - "7JGPIHblJ3wZ+kHAnEAnvEd4AsbIGcHAI2Oh6xLdw771XDSOQrYiwBYt8CtZQRAGNl87FNB+IGnA0vY7", - "6ByY1JhIvNXUWNFqM4q/KbcZbAUhkLyyvdIydzCjAgtCaguHJKvC5FyEiHCfhU++rsSS/HRLrkbW50r9", - "nFRJsL5+lc5PfTyfOjtX1f2viqwnGpZJeoxRWZp4E2LeqvapiLz/loNPKtQZ2zOxMeVruo3ZD0zGpTNi", - "hoQ0MQdIZKwwCUVuG3SF+6ZeJnwWAA37gWyfg4nouwUgOOx05MzxeJ1oRsXouJPqOUDyoEn43yFaKfkz", - "SIg6MFFm4kn/C/qv0cZLhmSRONrHZN/B+/S/1s/0U6zvViBI6khr4rE+bvVS41nrArqo2sDSokzN4G6d", - "mH4hVpXGxGVNSfbXxyzgMAklEd+p6J5pYhnh0G27gzaT8HYGEzwRZM+gFv8t24ABUJ4aytqr2GInma0c", - "3WQXxi6nTiik5J+ZiEc/SEMeTowxMxkrQh8tgAI48OUF/+EYUqY/vDvBuf2AhqwfhEVKqhvjtEg7aYNP", - "vquF2ziYMn8CDnwE7j0o4y66HjTpJDHyv2ZcZValK/VCqdJNbrbYRFVmVa27xweHLxBVWYmEgqlRFcFO", - "GyW/Tkp+WhRFJUE0F0GJBwldDF6CGsd19G8A/wbAe+j5XIfUObRzpTXQ430ucicq11ntPanCKFd3w8dA", - "6+IrqiQRvULvgI4gBS4aegEigO/B+t7Yo8JZhxw0AeU7m0OZf6S3QcrOgeSXclGWR64bVQjmRU5A5Imp", - "BLnCQqg9nRdUTi8WP1/tkw0FoWn4MGYR2He+sT+6NSulFIW6bs0Ug5TmXEmDRyZIe2ae/oEhEF4YhoyJ", - "L90C+bgepT0WyZcVRT74/osoIcHzYwz8V1394+W4rrMiWP9SFTg+rvxZ3RJu4rGj5VfhKNJSrx7HUjl8", - "8VZV4RDB08pKlorgbCTL7Isu0ZSZ4p5mXq1bpvak120BbTKnFqi9yhA0U5Xa7hnY0oqmds9YX+Jqxe2S", - "Iqkw8rgEVyavmz9MhjRfAxXlWU9Or7ufz62W1f2Y/PXy/POn9+dniyjSWle253Hu18SvX4ZLL6dywBWW", - "NgH8pHLtuixFZ30JjvrKOOm1Vctf2TcHdlZrrFNBU5Jl7IVpup1v+j/n8tvncdlrmZVZyhbstr+Ux54h", - "Ilg/930VPPf6Tvvy+a7zsvj/Uv76GrG1wXlfEb99dpd9Kfy9WBvrxVz22uz8Up76GsmU0W1v2I55QAMS", - "D2pcLvMLGlzFg+VeL5P2Wd91F99U3zPzC4J0hLD27uJumtHoWe6FM1rH5ffOPIiZsNE9Y9/NzTPN3zyT", - "8PAq3j2jCdhKH5HNAIGCP43BF3YDTdJxzTto0tVejI5P2m8mwdLQ3FKjMZpwFDkknfvNbTR1QzWaTLyi", - "S2l0qWpO+nPmT/2raVLGrBu10QfwrAI82qhLb6lROj0d2xpcUWMkut79NOlyvNgNNVNIWLaPk5KzJrGw", - "xQh45U016RxVB76S9xq5rSYd1JpIbV3t3YgVMk20XiYKty7SxPg6y9Vu09ZyzfhbSkW94NuC1KP5IptF", - "CtorNfg7yzb4N7fZvApEqoKGhZvy899qo9FT54xMMqQmrrfJIpjxlpt1NxvW5IIbbSXW+Y6bbJD7eSL3", - "3LtuygVrTa+70UW/Wck3ldhdYzNmc+3N5tqb58Huy0RUt9xYdCKEMMR8xtijtFTD9ppdzLMgjVBpg63g", - "FT2Lwe5lYPRsl/IYKdrcxrMB2lTg1+ZmiQI6LNrGXfZ1Pa8flJIaOksEpc19PZv7elYOXDcG7XNvFVoN", - "a7a524SmhEdW60Khv4L5nKzrq9BWm5uDNjcHvW7nwHyP0KI0BOtc3uPDMY9/dhLTkXX85ZaJsqDVBIgf", - "Qgf6QO5g8Y5bVox969gaURod7+z47IVRSOjxUeeowxTPzjihcue+0z6yijh2FjpfEd55Hw8QDnjl/DT1", - "Ot+BrFVps+XDoe8jXNHTbTJthcpilzdnaTF9seWgjiWQFA5NJxWK9Jsauzjt9XD46CGttYvTHmA/Tqqb", - "Ew+VV3b94Qo4CDPF4/BSsKz1n6+ve1cgjgjFCI7BPcLisUi7l92dpl/NTv+HDxeMVlGk9RqNI581kxF4", - "bWTmt5/Xaa2+5u3icTKt/WmrZGo8vfVKtmUomfh0+/T/AwAA///E4aBU9fcBAA==", + "2i7YBmHwq9Riv7rI9+4RntQY/lU84ByZkPjU0lvCyEGezNacryU9uDN/K3Hw7HbKtbFcJQP3qswVuRya", + "S5Kz3SOEbfWSrLCSVNfZLFVzS5V9sVycRGkTuXpVUjNHOaXEU822+9wSSaYR6+BUjKgk+ELqzbIBkJ5a", + "mn4sL342vW0lQ4XCaD1NMrIuz1c0EaimHjNl2wbn0BmxZ7xOUOaZOG3F42WkqsoVpOqztqmOUenFAo3f", + "mPiAIB3xzYZZrkwsvTFxfe5L5IkiM95i0OJffUUTeaxuXS40YFQv+2Bh/oD9Qu5JmPn6QFHT4fkXBxrA", + "WGwggnutnD3ZkQXok9Jmov/ckXNZuGBqc7I1wV9JVUXFroUL/rLV5kumt93v2yWTS2DgDsLHmUmT3xnp", + "ks/s59OXr9jHJtEYN69T317hrbEBlWvMS5cmQW/PUZsIXMvwrXP2a0rpiNJIOJ5eMAxVPRIoYmPSa/vl", + "6tMeF0+1DQmuxXUP+Tj++dU1f49NMA95yMqcuWsbVH5zsV1ZaE5sEcmqsJah+twFj6dwuBXMmjqBnfaR", + "8Fv53ViRZx1b++1Oe19WHeEzs+MwxuYun5iqO0RN2xIq8Zo5EoKdrj9cAf1j4MQYo4AyZAqhm9az014S", + "SX3tfnA9QgRlP2eYnNwkcY+wrO368/V17yqzHSKjkvKYY1LvpetKC+tUH1FazYSPbq/TSQrNiPQILeFg", + "53cisIkkdX6rrBStn0xyFGchs+GXmeynlnXYIDk8KFpFRDdgAgt9dZaehymFxMTjMWQOgyBUW2QnO5cU", + "3hEml9rQNQZk4vhoc6mCMR3ZOPR5ARsLumOeVSIrxCBs3fKyo6brRW4iHsSHIEAPeR4DW73zCyD2CbbV", + "BrASFF6eUX/ZI4oR3UkAx54DfX/CDa0w5mdfmOGldnpVKwWOEvRoA7aSC9ffhu6kxvJpISuNPOvYstl/", + "b8/fdT+C0/PL6+5P3dOT63P+az+46HbP/vf69PTk6y93Jw/dtyd33X+evP/QuXn3/fjyPf394qTz7vTq", + "j3dX3cH+2b/O354+3JxcnN88nv558s+3dx8/94N2u90PeGvnH88MPaSxqPHEFuttOyLOMSv/i0lKgslZ", + "GOcx24Ic7i5CDqvYX+fZOJKcIY9tDGPf5y7UwXIFkm9EZJhWboKtIjZkJNPJCESDuPDUyuqkHYxYt8Kx", + "NgHGBc+XYI4a9u7ukKg/yikNhwLKdC3D/QZ+ZMbzEZkQccYoByUFELhEORB4tmLJVzFKdpa0zSKdbjEk", + "WZr26uwqKVWZ4eDKLOkah5VaFg0p9N9OqCl8IY6K8dsR1NxKonJqIulpb2/38OjIuI+at9uq5FUbfl5g", + "V05KEnaUTNikBjVIBy8Yx1fKR+ZSrOx3ALMgo4QgqztHMLjjalPFP56jN0XHWb2p3dVw/KVwzu1MOX06", + "qTQEcmiZneXDDvrxoNOx0d7RwD7YdQ9s+MPuG/vg4M2bw8ODg47IKGB+mqqgpfZlXCuvm3R9l3dabhsV", + "c5G9OfMwqjaijXAhp2zBYDGjECdEFXXuwfJEWCcoCCkYhnHgriSQmCS3GQDx/XFyT7JN0TjyK50/7hN8", + "+HCRXL4Mkm8ARnceoQin3p4EhFaykeFPmK4V7wzE1Zxto98mLpnmPVwnRE0BjZ94y7yctPyk/A5Qcde5", + "goU/YoQnKS5kIw3LAgSncCZo33jWekYdri9prT0Ew9TX2VAod3TN7LLaLm8JzanIsRfUNAE1T7MIX5nP", + "e+Iqs9pIQ8HTPdG4Xe5diNR5dfKUA784AI4e2Y888Ki2m+Vx1UxnRZEU5wZNnDGrA1xvMQ09pQ5l5mDI", + "zgSO/YYaXqqnahQzgxAZmUBdfbAaLmv2OEGaUCdT7LYFZUdLVOxhMPQ9hwI7FU2+qUbgWN6TBX2MoDsR", + "p0RWE4yE0FWBQZN4VG4M1PYrghLIKrgYJQ6CGV8qdb5M9I7ige85er63ugdNg02D78CD4d4aeAcJofXs", + "f/M6GI3uZVj+M5CzbB/ATNp6eAPB4lGhZXYD3iFaLu6DCfAoAd2zopy/QybL/u2k684t6Gp3tmwqVlLY", + "ZzcMGjZ6ZpFSCj2fbASzhmAysSiXCbdh9yE27pjxGrAwSI+/mgnKeuimrS4XLlwji1DUQoX0L+SbdFbD", + "NzHGF1fcN9ng2pTdvnqoskh/ZIaY5LyhyJbKOGsBmVTUAsIWboEQA548NjVcOUOYMjOHU0KVyWQ+M2bZ", + "qkmOdt4jn3Bn6j59/fldy7nfkeCvJbxmlUOOhPSw3lwk5FJV4+xNulqWp7n35Ju086nJonOvDWPEQsqg", + "mByZkWdcI/nZywW090wB7YyAzxqhzhQBX0B9znpR7TUKZpfGsBtO3SoLYxei1ykAyug1r/sRAsYhGDoy", + "G146m/Kir5ZWsT3JBkwqE7SAdpkyR3PI11td2JzN/64R7F58kNt4YUpj1mRJ69WmiReA/3ty8YEpvn9e", + "ffqokpFeKESek/MptKvwuDjQIW/j3cTKp8XKEyzIx8oDN8nFX+e4+bOhz2CVzhscnyMmXtPzLrrcuTlI", + "FSG/DFLYDXaUsy9XOBheQvYcofHViIivXiB8HePfDUj3DNHu2kHuGYLbr0Fy59Tni7B0asjdCoS21yyi", + "zQPZepnLZn2JeWLaM4ey100c/wKux40MGudm+EVC3rOByOqGuze4NndEe2Gewo4sMzklmq2KA7A3TRl6", + "UzEvF5Q+6XXfs07rAZ8osWoCvUyRXUXc+hsmYnrqHtxUC7ORr2q7Qd3VpM+ZiZkbsCKcMCDxuDIg+Q4F", + "CKdxAUnQXMJVCBAK/mlEuu4UmeyhJHCtTQ0xN3zKGjMwytpcagJvnohygVG8to5ZuysBby8TEN1yY9GJ", + "EMSQ70zyR2LfgVkQ26sfAa1AumaRd4rFs/MNRt57xLeoKyOml+g+/MptM0l6G3wKHAQw/91tAY8CBwYg", + "CIEfBnfMKZXVImiob/0kV2ET0yFe1lbzEL4cqC5sFLM51crk8PXmphobZYamJL1b1Ts30pOu1IrZaGzd", + "nNqAKzlmA7g1AFfev8ambbVNywI8LMWmrA5MKUrEXrUqmCLvfw0IRbICQUxDW1p4TIeEAaoRrnqV0GRI", + "/Vw8NC3Kus1e99KEbZtvcanpn7NbtisVBJPrvD4QuzFv5w3eraRtu4OR8uLLK9VcJu9kgpDPCUukTb5+", + "uzaZ4NehQJKlazhEYm53xZUJDukmTPL6rPYE714CtB+9mkVN2IsvdHyA0zjr4YHHCcim/r/cwYHHycuc", + "GnicrOSRgZU4MMDW5LWdFlCyPMNZgcfJix8UePTWpOaNhKEcDj9OFn5C4HFiPh7AIK7+2YA04TsP3emZ", + "gez5gBmOAzxOFnoWIMemTWbjlDZdZl88TlbnCEBBfKuo3iT/z5v8/zh5hZn/XGQbA7OcSTl79v/jZMbU", + "/8fJc9MVeQv5E/a2erAelW8ScmdK8uea42Uz/MtIeCGv8XGybrn9zcpvrQz/x0mt9P7HSRO5/asunfNo", + "58bNlWkC9qJ5/CsvU1oSv2DtOM+TDdv7s2XxC0uzdgr/mijEV+0j5NL1E7dombn6M0HEJkt/7VCrCjAW", + "bdI/P02/Bqhpkd9JAwn6j5Pp2flrZV2sV1b+WlgBNVLyny9cTSXj1xChbGzu+XvdQoam5uCvi8Wwyb3f", + "5N4/C8Q2mUmNJ943iq+VtsvKJtw3g9SLReTnpdg/Tjb59RtQTUH11STXN20dvkxa/WsCIHMi/SIBaJNF", + "v8miXzUg3RiqzabQv5CV2nzqfI0gQj5v/nWZp2WZ8uuoITZp8ps0+VdtfE/JkW8clcdOVC87/uK012s8", + "OT7EMm/avDeS9lk/K/7itJfNii/W078Qb/V0LG4+Jz4lZLk58Wm/5Tnx6B7hCR2xtl5nXvyiM9MPTZnp", + "YyfqzZicLjn8BZPTNRlb6dz0DBYoBEzEeHGp6WqF8pnpJTtR6vUFZYkb+aUZQ2hK00vd3SkRiyILJauz", + "uQ+1bpp3KjOvKNVbE7vGsCFnHs2Q6Z1wZd1Eb438Z12tlo45ue203c8aHqnqt9ngdDtkhXPAzVTXSwVP", + "VuPFMsGrKVi2X5RQsx554AuR7eos8GSGqpPA1WvPur00L7nrIq/zqO/GzZMpwvYySeFrIl+M1zOM7jZs", + "WNfMAU9oqJcCvhBVKQL1SxW9v5hv0HlB32BzH+lrwKsK6Gja6seIUBtG3pSQ6CUi9KTXXWJAVPVYPxx6", + "0uuWB0IvEeSn4floTnrdxQVDGRnLDYOyHssDoFiM3PY9XuLidd4m2qxLpuShVlxTMqopklkzmLqwgGci", + "Qysd7tQkXUEb+4mz9cJinbLTmqFOtcaLsWZk683YL4XGlhrNTIShyBNqxjfhy7rhSzZbryhwmQpRU2Ke", + "MWBqBy0T2a8bskwJf5YbJuHGHKvUtTTPVVmTaGUZ3fXilWolXixcWUnAsr0TRcyaBCubl+eqUGUitdWB", + "SvnWs+KUwxArgV0fMa2nlRuwLKrF6GXikOshOYyPdS52m7V4awYhFQX1YpDN6j5z8HHBQvUKDfbOMg32", + "TUzxFWBPORAs1B6fu7ZEbZhi389WUGIaSCVVJeSJeE7Rq7AD1qTIxPpo86oSE88XrWfWligTIXAtKz14", + "BECwv2cPJhQBDAM3OW+IAid0RYh/hB6hixxvDP0WiDAaeo/IFWGJ32DkRb/+1gY3BCUC9B5NRH3ZCQgD", + "XawkVCPgBU44ZgCkDlCL1ujII/w8dkkMbqZzKtNk3FT1Yt2tkk0BjE0BjNcEsFX1JRoF1wqzZQXLSjSK", + "g4K8F0HB2YpOTCNrU31ig2grj2gFkGjUQFx2eYnGgGjlIEdEPF4Ecjb1Jjb1JpYLnWyC1ubUcCmeMRsx", + "Pf/vCmBbvonYWE2HSuc9wujeC2OivHhlHMCAsVbkQ0e56GJiGvDxKwpJvB7HfPZCE69KR2wqTmwqTrw2", + "g7usyETjAQSCHIxo+T7HpdpVgEnEGPo+IDTEjMvE121wiWiMAyJ/0HBSREnDmPYDhkbQoTEfO3+NI7qI", + "PBPkxNijExDFOAoJImK3tbhpciUJXqDUiS7q7jfIOUj2X0yyt7s8/roJ2LqH2PsTucDOX6OWQNdKp9aS", + "ZI0Vp8tVr8/o5XsPV4x1iTQxJCOiwMGTiN9IRgEzmITBIp92z8A4JpSHvrg50O4H7LH0Qon2eUyYSUS5", + "seOxYalnbPKTG2EHaBhiBCKEiUcoChxk4nYRSBQjX1AKr2h8AceRKhtuKAov7RdR/0NEzjmBCT9dJXIo", + "IuvirIIwsUW6/Gd5guHYupOGKrN+Ih/SYYjH7QcS7rWdcLxzvwv9aAR3rZb11QvY4iTLMkYUupDyGVGn", + "MSCFA0iQHUFCHkLMpY1EyCkyYy8k9A6jq399AGPoBUB9CpJPW5nDHcfWmXqjpzeeJBjKiTih1rG119l7", + "Y3d27c7h9W7neL9z3On8m5l1rpHGliV9zfJvn/jaPYMDxBoLxhY+kQkrxKersRvyFqZurw3GHuECHmLg", + "SRtn6CHfJSsM8y+VBi7BM90k7Z6tZO43sHWMFoZp1ZYOUZL/DN2kWV5T8797CI8hG6ivqhMw5SVnN8kF", + "V/LMFJdHxB75CGJXfsKXoR8EzAl0wnuEJ2CMnBEMPDIWui7RPexbz0XjKGQrAmzRAr+SFQRhYPO1QwHt", + "B5IGLG2/g86BSY2JxFtNjRWtNqP4m3KbwVYQAskr2ystcwczKrAgpLZwSLIqTM5FiAj3Wfjk60osyU+3", + "5Gpkfa7Uz0mVBOvrV+n81MfzqbNzVd3/qsh6omGZpMcYlaWJNyHmrWqfisj7bzn4pEKdsT0TG1O+ptuY", + "/cBkXDojZkhIE3OARMYKk1DktkFXuG/qZcJnAdCwH8j2OZiIvlsAgsNOR84cj9eJZlSMjjupngMkD5qE", + "/x2ilZI/g4SoAxNlJp70v6D/Gm28ZEgWiaN9TPYdvE//a/1MP8X6bgWCpI60Jh7r41YvNZ61LqCLqg0s", + "LcrUDO7WiekXYlVpTFzWlGR/fcwCDpNQEvGdiu6ZJpYRDt22O2gzCW9nMMETQfYMavHfsg0YAOWpoay9", + "ii12ktnK0U12Yexy6oRCSv6ZiXj0gzTk4cQYM5OxIvTRAiiAA19e8B+OIWX6w7sTnNsPaMj6QVikpLox", + "Tou0kzb45LtauI2DKfMn4MBH4N6DMu6i60GTThIj/2vGVWZVulIvlCrd5GaLTVRlVtW6e3xw+AJRlZVI", + "KJgaVRHstFHy66Tkp0VRVBJEcxGUeJDQxeAlqHFcR/8G8G8AvIeez3VInUM7V1oDPd7nIneicp3V3pMq", + "jHJ1N3wMtC6+okoS0Sv0DugIUuCioRcgAvgerO+NPSqcdchBE1C+szmU+Ud6G6TsHEh+KRdleeS6UYVg", + "XuQERJ6YSpArLITa03lB5fRi8fPVPtlQEJqGD2MWgX3nG/ujW7NSSlGo69ZMMUhpzpU0eGSCtGfm6R8Y", + "AuGFYciY+NItkI/rUdpjkXxZUeSD77+IEhI8P8bAf9XVP16O6zorgvUvVYHj48qf1S3hJh47Wn4VjiIt", + "9epxLJXDF29VFQ4RPK2sZKkIzkayzL7oEk2ZKe5p5tW6ZWpPet0W0CZzaoHaqwxBM1Wp7Z6BLa1oaveM", + "9SWuVtwuKZIKI49LcGXyuvnDZEjzNVBRnvXk9Lr7+dxqWd2PyV8vzz9/en9+togirXVlex7nfk38+mW4", + "9HIqB1xhaRPATyrXrstSdNaX4KivjJNeW7X8lX1zYGe1xjoVNCVZxl6Yptv5pv9zLr99Hpe9llmZpWzB", + "bvtLeewZIoL1c99XwXOv77Qvn+86L4v/L+WvrxFbG5z3FfHbZ3fZl8Lfi7WxXsxlr83OL+Wpr5FMGd32", + "hu2YBzQg8aDG5TK/oMFVPFju9TJpn/Vdd/FN9T0zvyBIRwhr7y7uphmNnuVeOKN1XH7vzIOYCRvdM/bd", + "3DzT/M0zCQ+v4t0zmoCt9BHZDBAo+NMYfGE30CQd17yDJl3txej4pP1mEiwNzS01GqMJR5FD0rnf3EZT", + "N1SjycQrupRGl6rmpD9n/tS/miZlzLpRG30AzyrAo4269JYapdPTsa3BFTVGouvdT5Mux4vdUDOFhGX7", + "OCk5axILW4yAV95Uk85RdeArea+R22rSQa2J1NbV3o1YIdNE62WicOsiTYyvs1ztNm0t14y/pVTUC74t", + "SD2aL7JZpKC9UoO/s2yDf3ObzatApCpoWLgpP/+tNho9dc7IJENq4nqbLIIZb7lZd7NhTS640VZine+4", + "yQa5nydyz73rplyw1vS6G130m5V8U4ndNTZjNtfebK69eR7svkxEdcuNRSdCCEPMZ4w9Sks1bK/ZxTwL", + "0giVNtgKXtGzGOxeBkbPdimPkaLNbTwboE0Ffm1uliigw6Jt3GVf1/P6QSmpobNEUNrc17O5r2flwHVj", + "0D73VqHVsGabu01oSnhktS4U+iuYz8m6vgpttbk5aHNz0Ot2Dsz3CC1KQ7DO5T0+HPP4ZycxHVnHX26Z", + "KAtaTYD4IXSgD+QOFu+4ZcXYt46tEaXR8c6Oz14YhYQeH3WOOkzx7IwTKnfuO+0jq4hjZ6HzFeGd9/EA", + "4YBXzk9Tr/MdyFqVNls+HPo+whU93SbTVqgsdnlzlhbTF1sO6lgCSeHQdFKhSL+psYvTXg+Hjx7SWrs4", + "7QH246S6OfFQeWXXH66AgzBTPA4vBcta//n6uncF4ohQjOAY3CMsHou0e9ndafrV7PR/+HDBaBVFWq/R", + "OPJZMxmB10Zmfvt5ndbqa94uHifT2p+2SqbG01uvZFuGkolPt0//PwAA///yE4cZ1fQBAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/gateway/gateway-controller/pkg/policy/builder.go b/gateway/gateway-controller/pkg/policy/builder.go index 40287a540..3cf3ce738 100644 --- a/gateway/gateway-controller/pkg/policy/builder.go +++ b/gateway/gateway-controller/pkg/policy/builder.go @@ -68,17 +68,13 @@ func DerivePolicyFromAPIConfig(cfg *models.StoredConfig, routerConfig *config.Ro channels = *apiData.Channels } for chName, ch := range channels { - var chPolicies api.WebSubChannelPolicies - if ch.Policies != nil { - chPolicies = *ch.Policies - } var finalPolicies []policyenginev1.PolicyInstance - // Policy execution order: policies (on_subscription) -> per-channel policies + // Policy execution order: allChannels (on_subscription) -> per-channel policies // Start with API-level subscription policies - if apiData.Policies != nil && apiData.Policies.OnSubscription != nil { - finalPolicies = make([]policyenginev1.PolicyInstance, 0, len(*apiData.Policies.OnSubscription)) - for _, p := range *apiData.Policies.OnSubscription { + if apiData.AllChannels != nil && apiData.AllChannels.OnSubscription != nil && apiData.AllChannels.OnSubscription.Policies != nil { + finalPolicies = make([]policyenginev1.PolicyInstance, 0, len(*apiData.AllChannels.OnSubscription.Policies)) + for _, p := range *apiData.AllChannels.OnSubscription.Policies { resolved, err := config.ResolvePolicyVersion(policyDefinitions, latestVersions, p.Name, p.Version) if err != nil { slog.Error("Failed to resolve policy version for all-channel subscription policy", "policy_name", p.Name, "error", err) @@ -89,8 +85,8 @@ func DerivePolicyFromAPIConfig(cfg *models.StoredConfig, routerConfig *config.Ro } // Append channel-level on_subscription policies - if chPolicies.OnSubscription != nil && len(*chPolicies.OnSubscription) > 0 { - for _, opPolicy := range *chPolicies.OnSubscription { + if ch.OnSubscription != nil && ch.OnSubscription.Policies != nil && len(*ch.OnSubscription.Policies) > 0 { + for _, opPolicy := range *ch.OnSubscription.Policies { resolved, err := config.ResolvePolicyVersion(policyDefinitions, latestVersions, opPolicy.Name, opPolicy.Version) if err != nil { slog.Error("Failed to resolve policy version for channel-level policy", "policy_name", opPolicy.Name, "channel_name", chName, "error", err) @@ -111,9 +107,9 @@ func DerivePolicyFromAPIConfig(cfg *models.StoredConfig, routerConfig *config.Ro // Build UNSUB (unsubscription) policy chain for this channel var unsubPolicies []policyenginev1.PolicyInstance - if apiData.Policies != nil && apiData.Policies.OnUnsubscription != nil { - unsubPolicies = make([]policyenginev1.PolicyInstance, 0, len(*apiData.Policies.OnUnsubscription)) - for _, p := range *apiData.Policies.OnUnsubscription { + if apiData.AllChannels != nil && apiData.AllChannels.OnUnsubscription != nil && apiData.AllChannels.OnUnsubscription.Policies != nil { + unsubPolicies = make([]policyenginev1.PolicyInstance, 0, len(*apiData.AllChannels.OnUnsubscription.Policies)) + for _, p := range *apiData.AllChannels.OnUnsubscription.Policies { resolved, err := config.ResolvePolicyVersion(policyDefinitions, latestVersions, p.Name, p.Version) if err != nil { slog.Error("Failed to resolve policy version for all-channel unsubscription policy", "policy_name", p.Name, "error", err) @@ -122,8 +118,8 @@ func DerivePolicyFromAPIConfig(cfg *models.StoredConfig, routerConfig *config.Ro unsubPolicies = append(unsubPolicies, ConvertAPIPolicyToModel(p, policyv1alpha.LevelAPI, versionutil.MajorVersion(resolved))) } } - if chPolicies.OnUnsubscription != nil && len(*chPolicies.OnUnsubscription) > 0 { - for _, opPolicy := range *chPolicies.OnUnsubscription { + if ch.OnUnsubscription != nil && ch.OnUnsubscription.Policies != nil && len(*ch.OnUnsubscription.Policies) > 0 { + for _, opPolicy := range *ch.OnUnsubscription.Policies { resolved, err := config.ResolvePolicyVersion(policyDefinitions, latestVersions, opPolicy.Name, opPolicy.Version) if err != nil { slog.Error("Failed to resolve policy version for channel-level unsubscription policy", "policy_name", opPolicy.Name, "channel_name", chName, "error", err) diff --git a/gateway/gateway-controller/pkg/policyxds/event_channel_translator.go b/gateway/gateway-controller/pkg/policyxds/event_channel_translator.go index 832c12309..6e2fbf41f 100644 --- a/gateway/gateway-controller/pkg/policyxds/event_channel_translator.go +++ b/gateway/gateway-controller/pkg/policyxds/event_channel_translator.go @@ -81,17 +81,26 @@ func (t *Translator) buildEventChannelResource(uuid string, webSubCfg *api.WebSu sort.Strings(sortedKeys) for _, chName := range sortedKeys { ch := channels[chName] - var chPolicies api.WebSubChannelPolicies - if ch.Policies != nil { - chPolicies = *ch.Policies + var subPolicies, unsubPolicies, inboundPolicies, outboundPolicies []interface{} + if ch.OnSubscription != nil { + subPolicies = buildPolicyList(ch.OnSubscription.Policies) + } + if ch.OnUnsubscription != nil { + unsubPolicies = buildPolicyList(ch.OnUnsubscription.Policies) + } + if ch.OnMessageReceived != nil { + inboundPolicies = buildPolicyList(ch.OnMessageReceived.Policies) + } + if ch.OnMessageDelivery != nil { + outboundPolicies = buildPolicyList(ch.OnMessageDelivery.Policies) } chEntry := map[string]interface{}{ "name": chName, "policies": map[string]interface{}{ - "subscribe": buildPolicyList(chPolicies.OnSubscription), - "unsubscribe": buildPolicyList(chPolicies.OnUnsubscription), - "inbound": buildPolicyList(chPolicies.OnMessageReceived), - "outbound": buildPolicyList(chPolicies.OnMessageDelivery), + "subscribe": subPolicies, + "unsubscribe": unsubPolicies, + "inbound": inboundPolicies, + "outbound": outboundPolicies, }, } channelEntries = append(channelEntries, chEntry) @@ -102,11 +111,19 @@ func (t *Translator) buildEventChannelResource(uuid string, webSubCfg *api.WebSu unsubscribePolicies := []interface{}{} inboundPolicies := []interface{}{} outboundPolicies := []interface{}{} - if spec.Policies != nil { - subscribePolicies = buildPolicyList(spec.Policies.OnSubscription) - unsubscribePolicies = buildPolicyList(spec.Policies.OnUnsubscription) - inboundPolicies = buildPolicyList(spec.Policies.OnMessageReceived) - outboundPolicies = buildPolicyList(spec.Policies.OnMessageDelivery) + if spec.AllChannels != nil { + if spec.AllChannels.OnSubscription != nil { + subscribePolicies = buildPolicyList(spec.AllChannels.OnSubscription.Policies) + } + if spec.AllChannels.OnUnsubscription != nil { + unsubscribePolicies = buildPolicyList(spec.AllChannels.OnUnsubscription.Policies) + } + if spec.AllChannels.OnMessageReceived != nil { + inboundPolicies = buildPolicyList(spec.AllChannels.OnMessageReceived.Policies) + } + if spec.AllChannels.OnMessageDelivery != nil { + outboundPolicies = buildPolicyList(spec.AllChannels.OnMessageDelivery.Policies) + } } data := map[string]interface{}{ diff --git a/platform-api/src/api/generated.go b/platform-api/src/api/generated.go index f8af9492d..21679af35 100644 --- a/platform-api/src/api/generated.go +++ b/platform-api/src/api/generated.go @@ -3084,8 +3084,8 @@ type WebSubAPI struct { // Name Human-readable name for the WebSub API Name string `binding:"required" json:"name" yaml:"name"` - // Policies Policies applied to all channels, organized by event type. - Policies *WebSubAllChannelPolicies `json:"policies,omitempty" yaml:"policies,omitempty"` + // AllChannels Policies applied to all channels, organized by event type. + AllChannels *WebSubAllChannelPolicies `json:"allChannels,omitempty" yaml:"allChannels,omitempty"` // ProjectId UUID of the project this API belongs to ProjectId string `binding:"required" json:"projectId" yaml:"projectId"` @@ -3132,42 +3132,45 @@ type WebSubAPIListResponse struct { Pagination Pagination `json:"pagination" yaml:"pagination"` } +// WebSubEventPolicies Policies for a single event type. +type WebSubEventPolicies struct { + // Policies List of policies applied for this event type. + Policies *[]Policy `json:"policies,omitempty" yaml:"policies,omitempty"` +} + // WebSubAllChannelPolicies Policies applied to all channels, organized by event type. type WebSubAllChannelPolicies struct { // OnMessageDelivery Policies applied when delivering a message to a subscriber callback URL (e.g., hmac-sign-messages) - OnMessageDelivery *[]Policy `json:"on_message_delivery,omitempty" yaml:"on_message_delivery,omitempty"` + OnMessageDelivery *WebSubEventPolicies `json:"on_message_delivery,omitempty" yaml:"on_message_delivery,omitempty"` // OnMessageReceived Policies applied when a message is received from the publisher via webhook (e.g., hmac-signature-validation) - OnMessageReceived *[]Policy `json:"on_message_received,omitempty" yaml:"on_message_received,omitempty"` + OnMessageReceived *WebSubEventPolicies `json:"on_message_received,omitempty" yaml:"on_message_received,omitempty"` // OnSubscription Policies applied when a client subscribes to a channel (e.g., api-key-auth) - OnSubscription *[]Policy `json:"on_subscription,omitempty" yaml:"on_subscription,omitempty"` + OnSubscription *WebSubEventPolicies `json:"on_subscription,omitempty" yaml:"on_subscription,omitempty"` // OnUnsubscription Policies applied when a client unsubscribes from a channel - OnUnsubscription *[]Policy `json:"on_unsubscription,omitempty" yaml:"on_unsubscription,omitempty"` + OnUnsubscription *WebSubEventPolicies `json:"on_unsubscription,omitempty" yaml:"on_unsubscription,omitempty"` } // WebSubChannel A single channel definition with optional per-channel policy overrides. type WebSubChannel struct { - // Policies Policies applied to a specific channel, organized by event type. - Policies *WebSubChannelPolicies `json:"policies,omitempty" yaml:"policies,omitempty"` -} - -// WebSubChannelPolicies Policies applied to a specific channel, organized by event type. -type WebSubChannelPolicies struct { // OnMessageDelivery Policies applied when delivering a message for this channel - OnMessageDelivery *[]Policy `json:"on_message_delivery,omitempty" yaml:"on_message_delivery,omitempty"` + OnMessageDelivery *WebSubEventPolicies `json:"on_message_delivery,omitempty" yaml:"on_message_delivery,omitempty"` // OnMessageReceived Policies applied when a message is received for this channel - OnMessageReceived *[]Policy `json:"on_message_received,omitempty" yaml:"on_message_received,omitempty"` + OnMessageReceived *WebSubEventPolicies `json:"on_message_received,omitempty" yaml:"on_message_received,omitempty"` // OnSubscription Policies applied when a client subscribes to this channel (e.g., rbac) - OnSubscription *[]Policy `json:"on_subscription,omitempty" yaml:"on_subscription,omitempty"` + OnSubscription *WebSubEventPolicies `json:"on_subscription,omitempty" yaml:"on_subscription,omitempty"` // OnUnsubscription Policies applied when a client unsubscribes from this channel - OnUnsubscription *[]Policy `json:"on_unsubscription,omitempty" yaml:"on_unsubscription,omitempty"` + OnUnsubscription *WebSubEventPolicies `json:"on_unsubscription,omitempty" yaml:"on_unsubscription,omitempty"` } +// WebSubChannelPolicies Policies applied to a specific channel, organized by event type. +type WebSubChannelPolicies = WebSubAllChannelPolicies + // ArtifactTypeQ defines model for ArtifactType-Q. type ArtifactTypeQ string diff --git a/platform-api/src/internal/model/deployment.go b/platform-api/src/internal/model/deployment.go index c1030f6dd..adc067cec 100644 --- a/platform-api/src/internal/model/deployment.go +++ b/platform-api/src/internal/model/deployment.go @@ -121,12 +121,16 @@ type WebSubAPIDeploymentYAML struct { // WebSubAPIDeploymentSpec represents the spec section of the WebSub API deployment YAML type WebSubAPIDeploymentSpec struct { - DisplayName string `yaml:"displayName"` - Version string `yaml:"version"` - Context string `yaml:"context"` - Vhosts *WebSubAPIDeploymentVhosts `yaml:"vhosts,omitempty"` - Policies *WebSubDeployAllChannelPolicies `yaml:"policies,omitempty"` - Channels map[string]WebSubDeployChannel `yaml:"channels,omitempty"` + DisplayName string `yaml:"displayName"` + Version string `yaml:"version"` + Context string `yaml:"context"` + Vhosts *WebSubAPIDeploymentVhosts `yaml:"vhosts,omitempty"` + AllChannels *WebSubDeployAllChannelPolicies `json:"allChannels,omitempty"` + Receiver *WebSubDeployReceiver `yaml:"receiver,omitempty"` + Hub *WebSubDeployHub `yaml:"hub,omitempty"` + Delivery *WebSubDeployDelivery `yaml:"delivery,omitempty"` + Channels map[string]WebSubDeployChannel `yaml:"channels,omitempty"` + DeploymentState string `yaml:"deploymentState,omitempty"` } // WebSubAPIDeploymentVhosts represents vhost configuration in the WebSub API deployment YAML @@ -135,23 +139,51 @@ type WebSubAPIDeploymentVhosts struct { Sandbox *string `yaml:"sandbox,omitempty"` } +// WebSubDeployEventPolicies wraps a list of policies for a single event type, +// matching the gateway controller's WebSubEventPolicies schema. +type WebSubDeployEventPolicies struct { + Policies *[]Policy `yaml:"policies,omitempty"` +} + // WebSubDeployAllChannelPolicies represents policies for all channels in the deployment YAML, organized by event type. type WebSubDeployAllChannelPolicies struct { - OnSubscription *[]Policy `yaml:"on_subscription,omitempty"` - OnUnsubscription *[]Policy `yaml:"on_unsubscription,omitempty"` - OnMessageReceived *[]Policy `yaml:"on_message_received,omitempty"` - OnMessageDelivery *[]Policy `yaml:"on_message_delivery,omitempty"` + OnSubscription *WebSubDeployEventPolicies `yaml:"on_subscription,omitempty"` + OnUnsubscription *WebSubDeployEventPolicies `yaml:"on_unsubscription,omitempty"` + OnMessageReceived *WebSubDeployEventPolicies `yaml:"on_message_received,omitempty"` + OnMessageDelivery *WebSubDeployEventPolicies `yaml:"on_message_delivery,omitempty"` } // WebSubDeployChannelPolicies represents per-channel policies in the deployment YAML, organized by event type. type WebSubDeployChannelPolicies struct { - OnSubscription *[]Policy `yaml:"on_subscription,omitempty"` - OnUnsubscription *[]Policy `yaml:"on_unsubscription,omitempty"` - OnMessageReceived *[]Policy `yaml:"on_message_received,omitempty"` - OnMessageDelivery *[]Policy `yaml:"on_message_delivery,omitempty"` + OnSubscription *WebSubDeployEventPolicies `yaml:"on_subscription,omitempty"` + OnUnsubscription *WebSubDeployEventPolicies `yaml:"on_unsubscription,omitempty"` + OnMessageReceived *WebSubDeployEventPolicies `yaml:"on_message_received,omitempty"` + OnMessageDelivery *WebSubDeployEventPolicies `yaml:"on_message_delivery,omitempty"` } // WebSubDeployChannel represents a single channel entry in the deployment YAML. type WebSubDeployChannel struct { Policies *WebSubDeployChannelPolicies `yaml:"policies,omitempty"` } + +// WebSubDeployReceiver represents the receiver section in the deployment YAML. +type WebSubDeployReceiver struct { + Policies []Policy `yaml:"policies"` +} + +// WebSubDeployHub represents the hub section in the deployment YAML. +type WebSubDeployHub struct { + Policies []Policy `yaml:"policies"` + Channels []WebSubDeployHubChannel `yaml:"channels,omitempty"` +} + +// WebSubDeployHubChannel represents a channel entry under the hub section in the deployment YAML. +type WebSubDeployHubChannel struct { + Name string `yaml:"name"` + Policies []Policy `yaml:"policies"` +} + +// WebSubDeployDelivery represents the delivery section in the deployment YAML. +type WebSubDeployDelivery struct { + Policies []Policy `yaml:"policies"` +} diff --git a/platform-api/src/internal/model/websub_api.go b/platform-api/src/internal/model/websub_api.go index 0d1361ab0..cf48c55b4 100644 --- a/platform-api/src/internal/model/websub_api.go +++ b/platform-api/src/internal/model/websub_api.go @@ -23,18 +23,18 @@ import "time" // WebSubAPI represents a WebSub API entity in the platform type WebSubAPI struct { - UUID string `json:"uuid" db:"-"` - Handle string `json:"id" db:"-"` - OrganizationUUID string `json:"organizationId" db:"-"` - ProjectUUID string `json:"projectId" db:"-"` - Name string `json:"name" db:"-"` - Description string `json:"description,omitempty" db:"-"` - CreatedBy string `json:"createdBy,omitempty" db:"-"` - Version string `json:"version" db:"-"` - LifeCycleStatus string `json:"lifeCycleStatus" db:"-"` - Transport []string `json:"transport,omitempty" db:"-"` - CreatedAt time.Time `json:"createdAt" db:"-"` - UpdatedAt time.Time `json:"updatedAt" db:"-"` + UUID string `json:"uuid" db:"-"` + Handle string `json:"id" db:"-"` + OrganizationUUID string `json:"organizationId" db:"-"` + ProjectUUID string `json:"projectId" db:"-"` + Name string `json:"name" db:"-"` + Description string `json:"description,omitempty" db:"-"` + CreatedBy string `json:"createdBy,omitempty" db:"-"` + Version string `json:"version" db:"-"` + LifeCycleStatus string `json:"lifeCycleStatus" db:"-"` + Transport []string `json:"transport,omitempty" db:"-"` + CreatedAt time.Time `json:"createdAt" db:"-"` + UpdatedAt time.Time `json:"updatedAt" db:"-"` Configuration WebSubAPIConfiguration `json:"configuration" db:"-"` } @@ -45,27 +45,52 @@ type WebSubAPIConfiguration struct { Context *string `json:"context,omitempty"` Channels map[string]WebSubChannel `json:"channels,omitempty"` Upstream UpstreamConfig `json:"upstream,omitempty"` - Policies *WebSubAllChannelPolicies `json:"policies,omitempty"` + AllChannels *WebSubAllChannelPolicies `json:"allChannels,omitempty"` SubscriptionPlans []string `json:"subscriptionPlans,omitempty"` } +// WebSubEventPolicies holds policies for a single event type. +type WebSubEventPolicies struct { + Policies []Policy `json:"policies,omitempty"` +} + // WebSubAllChannelPolicies holds policies applied to all channels, organized by event type. type WebSubAllChannelPolicies struct { - OnSubscription []Policy `json:"on_subscription,omitempty"` - OnUnsubscription []Policy `json:"on_unsubscription,omitempty"` - OnMessageReceived []Policy `json:"on_message_received,omitempty"` - OnMessageDelivery []Policy `json:"on_message_delivery,omitempty"` + OnSubscription *WebSubEventPolicies `json:"on_subscription,omitempty"` + OnUnsubscription *WebSubEventPolicies `json:"on_unsubscription,omitempty"` + OnMessageReceived *WebSubEventPolicies `json:"on_message_received,omitempty"` + OnMessageDelivery *WebSubEventPolicies `json:"on_message_delivery,omitempty"` } // WebSubChannelPolicies holds policies applied to a specific channel, organized by event type. -type WebSubChannelPolicies struct { - OnSubscription []Policy `json:"on_subscription,omitempty"` - OnUnsubscription []Policy `json:"on_unsubscription,omitempty"` - OnMessageReceived []Policy `json:"on_message_received,omitempty"` - OnMessageDelivery []Policy `json:"on_message_delivery,omitempty"` -} +type WebSubChannelPolicies = WebSubAllChannelPolicies // WebSubChannel represents a single channel with optional per-channel policy overrides. type WebSubChannel struct { - Policies *WebSubChannelPolicies `json:"policies,omitempty"` + OnSubscription *WebSubEventPolicies `json:"on_subscription,omitempty"` + OnUnsubscription *WebSubEventPolicies `json:"on_unsubscription,omitempty"` + OnMessageReceived *WebSubEventPolicies `json:"on_message_received,omitempty"` + OnMessageDelivery *WebSubEventPolicies `json:"on_message_delivery,omitempty"` +} + +// WebSubReceiver represents the receiver section of a WebSub API configuration. +type WebSubReceiver struct { + Policies []Policy `json:"policies,omitempty"` +} + +// WebSubHub represents the hub section of a WebSub API configuration. +type WebSubHub struct { + Policies []Policy `json:"policies,omitempty"` + Channels []WebSubHubChannel `json:"channels,omitempty"` +} + +// WebSubHubChannel represents a channel entry under the hub section. +type WebSubHubChannel struct { + Name string `json:"name"` + Policies []Policy `json:"policies,omitempty"` +} + +// WebSubDelivery represents the delivery section of a WebSub API configuration. +type WebSubDelivery struct { + Policies []Policy `json:"policies,omitempty"` } diff --git a/platform-api/src/internal/service/websub_api.go b/platform-api/src/internal/service/websub_api.go index 60a38b8ba..6872b1a6f 100644 --- a/platform-api/src/internal/service/websub_api.go +++ b/platform-api/src/internal/service/websub_api.go @@ -144,7 +144,7 @@ func (s *WebSubAPIService) Create(orgUUID, createdBy string, req *api.WebSubAPI) Context: req.Context, Channels: mapWebSubChannelsAPIToModel(req.Channels), Upstream: *mapUpstreamAPIToModel(req.Upstream), - Policies: mapWebSubAllChannelPoliciesAPIToModel(req.Policies), + AllChannels: mapWebSubAllChannelPoliciesAPIToModel(req.AllChannels), SubscriptionPlans: subscriptionPlans, }, } @@ -259,7 +259,7 @@ func (s *WebSubAPIService) Update(orgUUID, handle string, req *api.WebSubAPI) (* Context: req.Context, Channels: mapWebSubChannelsAPIToModel(req.Channels), Upstream: *mapUpstreamAPIToModel(req.Upstream), - Policies: mapWebSubAllChannelPoliciesAPIToModel(req.Policies), + AllChannels: mapWebSubAllChannelPoliciesAPIToModel(req.AllChannels), SubscriptionPlans: subscriptionPlans, } @@ -415,7 +415,7 @@ func mapWebSubAPIModelToAPI(m *model.WebSubAPI, apiUtil *utils.APIUtil) *api.Web Context: m.Configuration.Context, Upstream: mapUpstreamModelToAPI(&m.Configuration.Upstream), Channels: mapWebSubChannelsModelToAPI(m.Configuration.Channels), - Policies: mapWebSubAllChannelPoliciesModelToAPI(m.Configuration.Policies), + AllChannels: mapWebSubAllChannelPoliciesModelToAPI(m.Configuration.AllChannels), SubscriptionPlans: subscriptionPlans, CreatedAt: utils.TimePtr(m.CreatedAt), UpdatedAt: utils.TimePtr(m.UpdatedAt), @@ -432,22 +432,35 @@ func mapWebSubChannelsAPIToModel(in *map[string]api.WebSubChannel) map[string]mo out := make(map[string]model.WebSubChannel, len(*in)) for name, ch := range *in { out[name] = model.WebSubChannel{ - Policies: mapWebSubChannelPoliciesAPIToModel(ch.Policies), + OnSubscription: mapEventPoliciesAPIToModel(ch.OnSubscription), + OnUnsubscription: mapEventPoliciesAPIToModel(ch.OnUnsubscription), + OnMessageReceived: mapEventPoliciesAPIToModel(ch.OnMessageReceived), + OnMessageDelivery: mapEventPoliciesAPIToModel(ch.OnMessageDelivery), } } return out } -// mapWebSubChannelPoliciesAPIToModel converts API per-channel policies to model. +// mapEventPoliciesAPIToModel converts API event policies to model. +func mapEventPoliciesAPIToModel(in *api.WebSubEventPolicies) *model.WebSubEventPolicies { + if in == nil { + return nil + } + return &model.WebSubEventPolicies{ + Policies: mapAPIPolicySliceToModel(in.Policies), + } +} + +// mapWebSubChannelPoliciesAPIToModel is kept for compatibility. func mapWebSubChannelPoliciesAPIToModel(in *api.WebSubChannelPolicies) *model.WebSubChannelPolicies { if in == nil { return nil } return &model.WebSubChannelPolicies{ - OnSubscription: mapAPIPolicySliceToModel(in.OnSubscription), - OnUnsubscription: mapAPIPolicySliceToModel(in.OnUnsubscription), - OnMessageReceived: mapAPIPolicySliceToModel(in.OnMessageReceived), - OnMessageDelivery: mapAPIPolicySliceToModel(in.OnMessageDelivery), + OnSubscription: mapEventPoliciesAPIToModel(in.OnSubscription), + OnUnsubscription: mapEventPoliciesAPIToModel(in.OnUnsubscription), + OnMessageReceived: mapEventPoliciesAPIToModel(in.OnMessageReceived), + OnMessageDelivery: mapEventPoliciesAPIToModel(in.OnMessageDelivery), } } @@ -457,10 +470,10 @@ func mapWebSubAllChannelPoliciesAPIToModel(in *api.WebSubAllChannelPolicies) *mo return nil } return &model.WebSubAllChannelPolicies{ - OnSubscription: mapAPIPolicySliceToModel(in.OnSubscription), - OnUnsubscription: mapAPIPolicySliceToModel(in.OnUnsubscription), - OnMessageReceived: mapAPIPolicySliceToModel(in.OnMessageReceived), - OnMessageDelivery: mapAPIPolicySliceToModel(in.OnMessageDelivery), + OnSubscription: mapEventPoliciesAPIToModel(in.OnSubscription), + OnUnsubscription: mapEventPoliciesAPIToModel(in.OnUnsubscription), + OnMessageReceived: mapEventPoliciesAPIToModel(in.OnMessageReceived), + OnMessageDelivery: mapEventPoliciesAPIToModel(in.OnMessageDelivery), } } @@ -472,22 +485,35 @@ func mapWebSubChannelsModelToAPI(in map[string]model.WebSubChannel) *map[string] out := make(map[string]api.WebSubChannel, len(in)) for name, ch := range in { out[name] = api.WebSubChannel{ - Policies: mapWebSubChannelPoliciesModelToAPI(ch.Policies), + OnSubscription: mapEventPoliciesModelToAPI(ch.OnSubscription), + OnUnsubscription: mapEventPoliciesModelToAPI(ch.OnUnsubscription), + OnMessageReceived: mapEventPoliciesModelToAPI(ch.OnMessageReceived), + OnMessageDelivery: mapEventPoliciesModelToAPI(ch.OnMessageDelivery), } } return &out } -// mapWebSubChannelPoliciesModelToAPI converts model per-channel policies to API. +// mapEventPoliciesModelToAPI converts model event policies to API. +func mapEventPoliciesModelToAPI(in *model.WebSubEventPolicies) *api.WebSubEventPolicies { + if in == nil { + return nil + } + return &api.WebSubEventPolicies{ + Policies: mapModelPolicySliceToAPI(in.Policies), + } +} + +// mapWebSubChannelPoliciesModelToAPI is kept for compatibility. func mapWebSubChannelPoliciesModelToAPI(in *model.WebSubChannelPolicies) *api.WebSubChannelPolicies { if in == nil { return nil } return &api.WebSubChannelPolicies{ - OnSubscription: mapModelPolicySliceToAPI(in.OnSubscription), - OnUnsubscription: mapModelPolicySliceToAPI(in.OnUnsubscription), - OnMessageReceived: mapModelPolicySliceToAPI(in.OnMessageReceived), - OnMessageDelivery: mapModelPolicySliceToAPI(in.OnMessageDelivery), + OnSubscription: mapEventPoliciesModelToAPI(in.OnSubscription), + OnUnsubscription: mapEventPoliciesModelToAPI(in.OnUnsubscription), + OnMessageReceived: mapEventPoliciesModelToAPI(in.OnMessageReceived), + OnMessageDelivery: mapEventPoliciesModelToAPI(in.OnMessageDelivery), } } @@ -497,10 +523,10 @@ func mapWebSubAllChannelPoliciesModelToAPI(in *model.WebSubAllChannelPolicies) * return nil } return &api.WebSubAllChannelPolicies{ - OnSubscription: mapModelPolicySliceToAPI(in.OnSubscription), - OnUnsubscription: mapModelPolicySliceToAPI(in.OnUnsubscription), - OnMessageReceived: mapModelPolicySliceToAPI(in.OnMessageReceived), - OnMessageDelivery: mapModelPolicySliceToAPI(in.OnMessageDelivery), + OnSubscription: mapEventPoliciesModelToAPI(in.OnSubscription), + OnUnsubscription: mapEventPoliciesModelToAPI(in.OnUnsubscription), + OnMessageReceived: mapEventPoliciesModelToAPI(in.OnMessageReceived), + OnMessageDelivery: mapEventPoliciesModelToAPI(in.OnMessageDelivery), } } diff --git a/platform-api/src/internal/service/websub_api_deployment.go b/platform-api/src/internal/service/websub_api_deployment.go index 71dfc98ce..2067f48d7 100644 --- a/platform-api/src/internal/service/websub_api_deployment.go +++ b/platform-api/src/internal/service/websub_api_deployment.go @@ -598,8 +598,12 @@ func buildWebSubAPIDeploymentYAML(websubAPI *model.WebSubAPI) *model.WebSubAPIDe Vhosts: &model.WebSubAPIDeploymentVhosts{ Main: constants.VhostGatewayDefault, }, - Policies: buildWebSubAllChannelPolicies(websubAPI.Configuration.Policies), - Channels: buildWebSubDeployChannels(websubAPI.Configuration.Channels), + AllChannels: buildWebSubAllChannelPolicies(websubAPI.Configuration.AllChannels), + Receiver: buildWebSubDeployReceiver(websubAPI.Configuration.AllChannels), + Hub: buildWebSubDeployHub(websubAPI.Configuration.AllChannels, &websubAPI.Configuration.Channels), + Delivery: buildWebSubDeployDelivery(websubAPI.Configuration.AllChannels), + Channels: buildWebSubDeployChannels(websubAPI.Configuration.Channels), + DeploymentState: "deployed", }, } @@ -618,10 +622,10 @@ func buildWebSubAllChannelPolicies(p *model.WebSubAllChannelPolicies) *model.Web return nil } return &model.WebSubDeployAllChannelPolicies{ - OnSubscription: generatePolicyList(p.OnSubscription), - OnUnsubscription: generatePolicyList(p.OnUnsubscription), - OnMessageReceived: generatePolicyList(p.OnMessageReceived), - OnMessageDelivery: generatePolicyList(p.OnMessageDelivery), + OnSubscription: generateEventPolicyList(p.OnSubscription), + OnUnsubscription: generateEventPolicyList(p.OnUnsubscription), + OnMessageReceived: generateEventPolicyList(p.OnMessageReceived), + OnMessageDelivery: generateEventPolicyList(p.OnMessageDelivery), } } @@ -633,25 +637,36 @@ func buildWebSubDeployChannels(channels map[string]model.WebSubChannel) map[stri result := make(map[string]model.WebSubDeployChannel, len(channels)) for name, ch := range channels { result[name] = model.WebSubDeployChannel{ - Policies: buildWebSubDeployChannelPolicies(ch.Policies), + Policies: buildWebSubDeployChannelPoliciesFromChannel(ch), } } return result } -// buildWebSubDeployChannelPolicies converts model channel policies to deployment channel policies. -func buildWebSubDeployChannelPolicies(p *model.WebSubChannelPolicies) *model.WebSubDeployChannelPolicies { - if p == nil { +// buildWebSubDeployChannelPoliciesFromChannel converts a model channel to deployment channel policies. +func buildWebSubDeployChannelPoliciesFromChannel(ch model.WebSubChannel) *model.WebSubDeployChannelPolicies { + if ch.OnSubscription == nil && ch.OnUnsubscription == nil && ch.OnMessageReceived == nil && ch.OnMessageDelivery == nil { return nil } return &model.WebSubDeployChannelPolicies{ - OnSubscription: generatePolicyList(p.OnSubscription), - OnUnsubscription: generatePolicyList(p.OnUnsubscription), - OnMessageReceived: generatePolicyList(p.OnMessageReceived), - OnMessageDelivery: generatePolicyList(p.OnMessageDelivery), + OnSubscription: generateEventPolicyList(ch.OnSubscription), + OnUnsubscription: generateEventPolicyList(ch.OnUnsubscription), + OnMessageReceived: generateEventPolicyList(ch.OnMessageReceived), + OnMessageDelivery: generateEventPolicyList(ch.OnMessageDelivery), } } +func generateEventPolicyList(ep *model.WebSubEventPolicies) *model.WebSubDeployEventPolicies { + if ep == nil { + return nil + } + policies := generatePolicyList(ep.Policies) + if policies == nil { + return &model.WebSubDeployEventPolicies{Policies: &[]model.Policy{}} + } + return &model.WebSubDeployEventPolicies{Policies: policies} +} + func generatePolicyList(policyConfigs []model.Policy) *[]model.Policy { if len(policyConfigs) == 0 { return nil @@ -666,3 +681,48 @@ func generatePolicyList(policyConfigs []model.Policy) *[]model.Policy { } return &policies } + +// buildWebSubDeployReceiver builds the receiver section for the deployment YAML. +func buildWebSubDeployReceiver(p *model.WebSubAllChannelPolicies) *model.WebSubDeployReceiver { + policies := []model.Policy{} + if p != nil && p.OnMessageReceived != nil && len(p.OnMessageReceived.Policies) > 0 { + policies = append(policies, p.OnMessageReceived.Policies...) + } + return &model.WebSubDeployReceiver{Policies: policies} +} + +// buildWebSubDeployHub builds the hub section for the deployment YAML. +func buildWebSubDeployHub(policies *model.WebSubAllChannelPolicies, channels *map[string]model.WebSubChannel) *model.WebSubDeployHub { + hub := &model.WebSubDeployHub{ + Policies: []model.Policy{}, + } + allPolicies := []model.Policy{} + if policies != nil && policies.OnSubscription != nil && len(policies.OnSubscription.Policies) > 0 { + allPolicies = append(allPolicies, policies.OnSubscription.Policies...) + } + if channels != nil && len(*channels) > 0 { + channelPolicies := []model.WebSubDeployHubChannel{} + for name, ch := range *channels { + chPolicy := []model.Policy{} + if ch.OnSubscription != nil && len(ch.OnSubscription.Policies) > 0 { + chPolicy = append(chPolicy, ch.OnSubscription.Policies...) + } + channelPolicies = append(channelPolicies, model.WebSubDeployHubChannel{ + Name: name, + Policies: chPolicy, + }) + } + hub.Channels = channelPolicies + } + hub.Policies = allPolicies + return hub +} + +// buildWebSubDeployDelivery builds the delivery section for the deployment YAML. +func buildWebSubDeployDelivery(d *model.WebSubAllChannelPolicies) *model.WebSubDeployDelivery { + policies := []model.Policy{} + if d != nil && d.OnMessageDelivery != nil && len(d.OnMessageDelivery.Policies) > 0 { + policies = d.OnMessageDelivery.Policies + } + return &model.WebSubDeployDelivery{Policies: policies} +} diff --git a/platform-api/src/resources/openapi.yaml b/platform-api/src/resources/openapi.yaml index eba397bf2..3fa57b2dd 100644 --- a/platform-api/src/resources/openapi.yaml +++ b/platform-api/src/resources/openapi.yaml @@ -9044,7 +9044,7 @@ components: example: /my-websub-api upstream: $ref: '#/components/schemas/Upstream' - policies: + allChannels: $ref: '#/components/schemas/WebSubAllChannelPolicies' channels: type: object @@ -9151,23 +9151,21 @@ components: description: Policies applied to all channels, organized by event type. properties: on_subscription: - type: array - description: Policies applied when a client subscribes to a channel (e.g., api-key-auth) - items: - $ref: '#/components/schemas/Policy' + $ref: '#/components/schemas/WebSubEventPolicies' on_unsubscription: - type: array - description: Policies applied when a client unsubscribes from a channel - items: - $ref: '#/components/schemas/Policy' + $ref: '#/components/schemas/WebSubEventPolicies' on_message_received: - type: array - description: Policies applied when a message is received from the publisher via webhook (e.g., hmac-signature-validation) - items: - $ref: '#/components/schemas/Policy' + $ref: '#/components/schemas/WebSubEventPolicies' on_message_delivery: + $ref: '#/components/schemas/WebSubEventPolicies' + + WebSubEventPolicies: + type: object + description: Policies for a single event type. + properties: + policies: type: array - description: Policies applied when delivering a message to a subscriber callback URL (e.g., hmac-sign-messages) + description: List of policies applied for this event type. items: $ref: '#/components/schemas/Policy' @@ -9200,8 +9198,14 @@ components: type: object description: A single channel definition with optional per-channel policy overrides. properties: - policies: - $ref: '#/components/schemas/WebSubChannelPolicies' + on_subscription: + $ref: '#/components/schemas/WebSubEventPolicies' + on_unsubscription: + $ref: '#/components/schemas/WebSubEventPolicies' + on_message_received: + $ref: '#/components/schemas/WebSubEventPolicies' + on_message_delivery: + $ref: '#/components/schemas/WebSubEventPolicies' responses: Unauthorized: From 020841930bb12918d02f69d8f8564e185cdfdc52 Mon Sep 17 00:00:00 2001 From: Tharindu Dharmarathna Date: Thu, 14 May 2026 00:18:08 +0530 Subject: [PATCH 2/2] include unit tests --- platform-api/src/api/generated.go | 25 +- platform-api/src/internal/model/deployment.go | 12 +- .../src/internal/service/websub_api.go | 118 +++++- .../internal/service/websub_api_deployment.go | 18 +- .../websub_api_deployment_yaml_test.go | 396 ++++++++++++++++++ .../src/internal/service/websub_api_test.go | 388 +++++++++++++++++ platform-api/src/resources/openapi.yaml | 14 +- 7 files changed, 909 insertions(+), 62 deletions(-) create mode 100644 platform-api/src/internal/service/websub_api_deployment_yaml_test.go create mode 100644 platform-api/src/internal/service/websub_api_test.go diff --git a/platform-api/src/api/generated.go b/platform-api/src/api/generated.go index 21679af35..b93960cd5 100644 --- a/platform-api/src/api/generated.go +++ b/platform-api/src/api/generated.go @@ -3084,8 +3084,8 @@ type WebSubAPI struct { // Name Human-readable name for the WebSub API Name string `binding:"required" json:"name" yaml:"name"` - // AllChannels Policies applied to all channels, organized by event type. - AllChannels *WebSubAllChannelPolicies `json:"allChannels,omitempty" yaml:"allChannels,omitempty"` + // Policies Policies applied to all channels, organized by event type (flat arrays). + Policies *WebSubChannelPolicies `json:"policies,omitempty" yaml:"policies,omitempty"` // ProjectId UUID of the project this API belongs to ProjectId string `binding:"required" json:"projectId" yaml:"projectId"` @@ -3155,22 +3155,25 @@ type WebSubAllChannelPolicies struct { // WebSubChannel A single channel definition with optional per-channel policy overrides. type WebSubChannel struct { + // Policies Per-channel policy overrides organized by event type. + Policies *WebSubChannelPolicies `json:"policies,omitempty" yaml:"policies,omitempty"` +} + +// WebSubChannelPolicies Policies applied to a specific channel, organized by event type (flat arrays). +type WebSubChannelPolicies struct { // OnMessageDelivery Policies applied when delivering a message for this channel - OnMessageDelivery *WebSubEventPolicies `json:"on_message_delivery,omitempty" yaml:"on_message_delivery,omitempty"` + OnMessageDelivery *[]Policy `json:"on_message_delivery,omitempty" yaml:"on_message_delivery,omitempty"` // OnMessageReceived Policies applied when a message is received for this channel - OnMessageReceived *WebSubEventPolicies `json:"on_message_received,omitempty" yaml:"on_message_received,omitempty"` + OnMessageReceived *[]Policy `json:"on_message_received,omitempty" yaml:"on_message_received,omitempty"` - // OnSubscription Policies applied when a client subscribes to this channel (e.g., rbac) - OnSubscription *WebSubEventPolicies `json:"on_subscription,omitempty" yaml:"on_subscription,omitempty"` + // OnSubscription Policies applied when a client subscribes to a channel + OnSubscription *[]Policy `json:"on_subscription,omitempty" yaml:"on_subscription,omitempty"` - // OnUnsubscription Policies applied when a client unsubscribes from this channel - OnUnsubscription *WebSubEventPolicies `json:"on_unsubscription,omitempty" yaml:"on_unsubscription,omitempty"` + // OnUnsubscription Policies applied when a client unsubscribes from a channel + OnUnsubscription *[]Policy `json:"on_unsubscription,omitempty" yaml:"on_unsubscription,omitempty"` } -// WebSubChannelPolicies Policies applied to a specific channel, organized by event type. -type WebSubChannelPolicies = WebSubAllChannelPolicies - // ArtifactTypeQ defines model for ArtifactType-Q. type ArtifactTypeQ string diff --git a/platform-api/src/internal/model/deployment.go b/platform-api/src/internal/model/deployment.go index adc067cec..e16684340 100644 --- a/platform-api/src/internal/model/deployment.go +++ b/platform-api/src/internal/model/deployment.go @@ -125,7 +125,7 @@ type WebSubAPIDeploymentSpec struct { Version string `yaml:"version"` Context string `yaml:"context"` Vhosts *WebSubAPIDeploymentVhosts `yaml:"vhosts,omitempty"` - AllChannels *WebSubDeployAllChannelPolicies `json:"allChannels,omitempty"` + AllChannels *WebSubDeployAllChannelPolicies `yaml:"allChannels,omitempty"` Receiver *WebSubDeployReceiver `yaml:"receiver,omitempty"` Hub *WebSubDeployHub `yaml:"hub,omitempty"` Delivery *WebSubDeployDelivery `yaml:"delivery,omitempty"` @@ -153,19 +153,15 @@ type WebSubDeployAllChannelPolicies struct { OnMessageDelivery *WebSubDeployEventPolicies `yaml:"on_message_delivery,omitempty"` } -// WebSubDeployChannelPolicies represents per-channel policies in the deployment YAML, organized by event type. -type WebSubDeployChannelPolicies struct { +// WebSubDeployChannel represents a single channel entry in the deployment YAML. +// Event policies are at the top level to match the gateway-controller's WebSubChannel schema. +type WebSubDeployChannel struct { OnSubscription *WebSubDeployEventPolicies `yaml:"on_subscription,omitempty"` OnUnsubscription *WebSubDeployEventPolicies `yaml:"on_unsubscription,omitempty"` OnMessageReceived *WebSubDeployEventPolicies `yaml:"on_message_received,omitempty"` OnMessageDelivery *WebSubDeployEventPolicies `yaml:"on_message_delivery,omitempty"` } -// WebSubDeployChannel represents a single channel entry in the deployment YAML. -type WebSubDeployChannel struct { - Policies *WebSubDeployChannelPolicies `yaml:"policies,omitempty"` -} - // WebSubDeployReceiver represents the receiver section in the deployment YAML. type WebSubDeployReceiver struct { Policies []Policy `yaml:"policies"` diff --git a/platform-api/src/internal/service/websub_api.go b/platform-api/src/internal/service/websub_api.go index 6872b1a6f..ae6a2eefa 100644 --- a/platform-api/src/internal/service/websub_api.go +++ b/platform-api/src/internal/service/websub_api.go @@ -144,7 +144,7 @@ func (s *WebSubAPIService) Create(orgUUID, createdBy string, req *api.WebSubAPI) Context: req.Context, Channels: mapWebSubChannelsAPIToModel(req.Channels), Upstream: *mapUpstreamAPIToModel(req.Upstream), - AllChannels: mapWebSubAllChannelPoliciesAPIToModel(req.AllChannels), + AllChannels: mapWebSubPoliciesAPIToAllChannels(req.Policies), SubscriptionPlans: subscriptionPlans, }, } @@ -259,7 +259,7 @@ func (s *WebSubAPIService) Update(orgUUID, handle string, req *api.WebSubAPI) (* Context: req.Context, Channels: mapWebSubChannelsAPIToModel(req.Channels), Upstream: *mapUpstreamAPIToModel(req.Upstream), - AllChannels: mapWebSubAllChannelPoliciesAPIToModel(req.AllChannels), + AllChannels: mapWebSubPoliciesAPIToAllChannels(req.Policies), SubscriptionPlans: subscriptionPlans, } @@ -415,7 +415,7 @@ func mapWebSubAPIModelToAPI(m *model.WebSubAPI, apiUtil *utils.APIUtil) *api.Web Context: m.Configuration.Context, Upstream: mapUpstreamModelToAPI(&m.Configuration.Upstream), Channels: mapWebSubChannelsModelToAPI(m.Configuration.Channels), - AllChannels: mapWebSubAllChannelPoliciesModelToAPI(m.Configuration.AllChannels), + Policies: mapAllChannelsModelToWebSubPolicies(m.Configuration.AllChannels), SubscriptionPlans: subscriptionPlans, CreatedAt: utils.TimePtr(m.CreatedAt), UpdatedAt: utils.TimePtr(m.UpdatedAt), @@ -431,11 +431,15 @@ func mapWebSubChannelsAPIToModel(in *map[string]api.WebSubChannel) map[string]mo } out := make(map[string]model.WebSubChannel, len(*in)) for name, ch := range *in { + var p *api.WebSubChannelPolicies + if ch.Policies != nil { + p = ch.Policies + } out[name] = model.WebSubChannel{ - OnSubscription: mapEventPoliciesAPIToModel(ch.OnSubscription), - OnUnsubscription: mapEventPoliciesAPIToModel(ch.OnUnsubscription), - OnMessageReceived: mapEventPoliciesAPIToModel(ch.OnMessageReceived), - OnMessageDelivery: mapEventPoliciesAPIToModel(ch.OnMessageDelivery), + OnSubscription: policySlicePtrToEventPolicies(policySlicePtrFromChannelPolicies(p, "on_subscription")), + OnUnsubscription: policySlicePtrToEventPolicies(policySlicePtrFromChannelPolicies(p, "on_unsubscription")), + OnMessageReceived: policySlicePtrToEventPolicies(policySlicePtrFromChannelPolicies(p, "on_message_received")), + OnMessageDelivery: policySlicePtrToEventPolicies(policySlicePtrFromChannelPolicies(p, "on_message_delivery")), } } return out @@ -457,10 +461,10 @@ func mapWebSubChannelPoliciesAPIToModel(in *api.WebSubChannelPolicies) *model.We return nil } return &model.WebSubChannelPolicies{ - OnSubscription: mapEventPoliciesAPIToModel(in.OnSubscription), - OnUnsubscription: mapEventPoliciesAPIToModel(in.OnUnsubscription), - OnMessageReceived: mapEventPoliciesAPIToModel(in.OnMessageReceived), - OnMessageDelivery: mapEventPoliciesAPIToModel(in.OnMessageDelivery), + OnSubscription: policySlicePtrToEventPolicies(in.OnSubscription), + OnUnsubscription: policySlicePtrToEventPolicies(in.OnUnsubscription), + OnMessageReceived: policySlicePtrToEventPolicies(in.OnMessageReceived), + OnMessageDelivery: policySlicePtrToEventPolicies(in.OnMessageDelivery), } } @@ -477,6 +481,80 @@ func mapWebSubAllChannelPoliciesAPIToModel(in *api.WebSubAllChannelPolicies) *mo } } +// mapWebSubPoliciesAPIToAllChannels converts flat WebSubChannelPolicies (from API) to model.WebSubAllChannelPolicies (for storage). +func mapWebSubPoliciesAPIToAllChannels(in *api.WebSubChannelPolicies) *model.WebSubAllChannelPolicies { + if in == nil { + return nil + } + return &model.WebSubAllChannelPolicies{ + OnSubscription: policySlicePtrToEventPolicies(in.OnSubscription), + OnUnsubscription: policySlicePtrToEventPolicies(in.OnUnsubscription), + OnMessageReceived: policySlicePtrToEventPolicies(in.OnMessageReceived), + OnMessageDelivery: policySlicePtrToEventPolicies(in.OnMessageDelivery), + } +} + +// mapAllChannelsModelToWebSubPolicies converts stored model.WebSubAllChannelPolicies to flat WebSubChannelPolicies (for API response). +func mapAllChannelsModelToWebSubPolicies(in *model.WebSubAllChannelPolicies) *api.WebSubChannelPolicies { + if in == nil { + return nil + } + return &api.WebSubChannelPolicies{ + OnSubscription: eventPoliciesToPolicySlicePtr(in.OnSubscription), + OnUnsubscription: eventPoliciesToPolicySlicePtr(in.OnUnsubscription), + OnMessageReceived: eventPoliciesToPolicySlicePtr(in.OnMessageReceived), + OnMessageDelivery: eventPoliciesToPolicySlicePtr(in.OnMessageDelivery), + } +} + +// policySlicePtrToEventPolicies wraps a flat policy slice pointer into a model.WebSubEventPolicies. +func policySlicePtrToEventPolicies(in *[]api.Policy) *model.WebSubEventPolicies { + if in == nil { + return nil + } + policies := make([]model.Policy, 0, len(*in)) + for _, p := range *in { + policy := model.Policy{ + Name: p.Name, + Version: p.Version, + } + if p.ExecutionCondition != nil { + policy.ExecutionCondition = p.ExecutionCondition + } + if p.Params != nil { + policy.Params = p.Params + } + policies = append(policies, policy) + } + return &model.WebSubEventPolicies{Policies: policies} +} + +// eventPoliciesToPolicySlicePtr converts a model.WebSubEventPolicies to a flat policy slice pointer. +func eventPoliciesToPolicySlicePtr(in *model.WebSubEventPolicies) *[]api.Policy { + if in == nil || len(in.Policies) == 0 { + return nil + } + return mapModelPolicySliceToAPI(in.Policies) +} + +// policySlicePtrFromChannelPolicies extracts the policy slice for a given event type from WebSubChannelPolicies. +func policySlicePtrFromChannelPolicies(p *api.WebSubChannelPolicies, event string) *[]api.Policy { + if p == nil { + return nil + } + switch event { + case "on_subscription": + return p.OnSubscription + case "on_unsubscription": + return p.OnUnsubscription + case "on_message_received": + return p.OnMessageReceived + case "on_message_delivery": + return p.OnMessageDelivery + } + return nil +} + // mapWebSubChannelsModelToAPI converts the model channel map to the API channel map. func mapWebSubChannelsModelToAPI(in map[string]model.WebSubChannel) *map[string]api.WebSubChannel { if len(in) == 0 { @@ -485,10 +563,12 @@ func mapWebSubChannelsModelToAPI(in map[string]model.WebSubChannel) *map[string] out := make(map[string]api.WebSubChannel, len(in)) for name, ch := range in { out[name] = api.WebSubChannel{ - OnSubscription: mapEventPoliciesModelToAPI(ch.OnSubscription), - OnUnsubscription: mapEventPoliciesModelToAPI(ch.OnUnsubscription), - OnMessageReceived: mapEventPoliciesModelToAPI(ch.OnMessageReceived), - OnMessageDelivery: mapEventPoliciesModelToAPI(ch.OnMessageDelivery), + Policies: &api.WebSubChannelPolicies{ + OnSubscription: eventPoliciesToPolicySlicePtr(ch.OnSubscription), + OnUnsubscription: eventPoliciesToPolicySlicePtr(ch.OnUnsubscription), + OnMessageReceived: eventPoliciesToPolicySlicePtr(ch.OnMessageReceived), + OnMessageDelivery: eventPoliciesToPolicySlicePtr(ch.OnMessageDelivery), + }, } } return &out @@ -510,10 +590,10 @@ func mapWebSubChannelPoliciesModelToAPI(in *model.WebSubChannelPolicies) *api.We return nil } return &api.WebSubChannelPolicies{ - OnSubscription: mapEventPoliciesModelToAPI(in.OnSubscription), - OnUnsubscription: mapEventPoliciesModelToAPI(in.OnUnsubscription), - OnMessageReceived: mapEventPoliciesModelToAPI(in.OnMessageReceived), - OnMessageDelivery: mapEventPoliciesModelToAPI(in.OnMessageDelivery), + OnSubscription: eventPoliciesToPolicySlicePtr(in.OnSubscription), + OnUnsubscription: eventPoliciesToPolicySlicePtr(in.OnUnsubscription), + OnMessageReceived: eventPoliciesToPolicySlicePtr(in.OnMessageReceived), + OnMessageDelivery: eventPoliciesToPolicySlicePtr(in.OnMessageDelivery), } } diff --git a/platform-api/src/internal/service/websub_api_deployment.go b/platform-api/src/internal/service/websub_api_deployment.go index 2067f48d7..c0a369a68 100644 --- a/platform-api/src/internal/service/websub_api_deployment.go +++ b/platform-api/src/internal/service/websub_api_deployment.go @@ -637,25 +637,15 @@ func buildWebSubDeployChannels(channels map[string]model.WebSubChannel) map[stri result := make(map[string]model.WebSubDeployChannel, len(channels)) for name, ch := range channels { result[name] = model.WebSubDeployChannel{ - Policies: buildWebSubDeployChannelPoliciesFromChannel(ch), + OnSubscription: generateEventPolicyList(ch.OnSubscription), + OnUnsubscription: generateEventPolicyList(ch.OnUnsubscription), + OnMessageReceived: generateEventPolicyList(ch.OnMessageReceived), + OnMessageDelivery: generateEventPolicyList(ch.OnMessageDelivery), } } return result } -// buildWebSubDeployChannelPoliciesFromChannel converts a model channel to deployment channel policies. -func buildWebSubDeployChannelPoliciesFromChannel(ch model.WebSubChannel) *model.WebSubDeployChannelPolicies { - if ch.OnSubscription == nil && ch.OnUnsubscription == nil && ch.OnMessageReceived == nil && ch.OnMessageDelivery == nil { - return nil - } - return &model.WebSubDeployChannelPolicies{ - OnSubscription: generateEventPolicyList(ch.OnSubscription), - OnUnsubscription: generateEventPolicyList(ch.OnUnsubscription), - OnMessageReceived: generateEventPolicyList(ch.OnMessageReceived), - OnMessageDelivery: generateEventPolicyList(ch.OnMessageDelivery), - } -} - func generateEventPolicyList(ep *model.WebSubEventPolicies) *model.WebSubDeployEventPolicies { if ep == nil { return nil diff --git a/platform-api/src/internal/service/websub_api_deployment_yaml_test.go b/platform-api/src/internal/service/websub_api_deployment_yaml_test.go new file mode 100644 index 000000000..698689977 --- /dev/null +++ b/platform-api/src/internal/service/websub_api_deployment_yaml_test.go @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +package service + +import ( + "strings" + "testing" + + "platform-api/src/internal/model" + + "gopkg.in/yaml.v3" +) + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +func makePolicy(name, version string) model.Policy { + return model.Policy{Name: name, Version: version} +} + +func makeEventPolicies(policies ...model.Policy) *model.WebSubEventPolicies { + return &model.WebSubEventPolicies{Policies: policies} +} + +func ptrMap(m map[string]interface{}) *map[string]interface{} { return &m } + +// buildTestWebSubAPI builds a minimal WebSubAPI with both global (allChannels) +// and per-channel policies, matching the user's reported request body. +func buildTestWebSubAPI() *model.WebSubAPI { + return &model.WebSubAPI{ + Handle: "my-websub-api1", + Name: "echo api1", + Version: "v1.0", + ProjectUUID: "019e2158-6d48-7730-8af3-a5b484c9ee4c", + Configuration: model.WebSubAPIConfiguration{ + Context: strPtr("/repos1"), + AllChannels: &model.WebSubAllChannelPolicies{ + OnSubscription: makeEventPolicies(model.Policy{ + Name: "api-key-auth", + Version: "v1", + Params: ptrMap(map[string]interface{}{"in": "header", "key": "X-API-Key"}), + }), + OnMessageReceived: makeEventPolicies(model.Policy{ + Name: "basic-auth", + Version: "v1", + Params: ptrMap(map[string]interface{}{"username": "admin", "password": "admin123"}), + }), + OnMessageDelivery: makeEventPolicies(model.Policy{ + Name: "set-headers", + Version: "v1", + Params: ptrMap(map[string]interface{}{ + "request": map[string]interface{}{ + "headers": []interface{}{ + map[string]interface{}{"name": "level", "value": "global"}, + }, + }, + }), + }), + }, + Channels: map[string]model.WebSubChannel{ + "issues": { + OnMessageDelivery: makeEventPolicies(model.Policy{ + Name: "set-headers", + Version: "v1", + Params: ptrMap(map[string]interface{}{ + "request": map[string]interface{}{ + "headers": []interface{}{ + map[string]interface{}{"name": "level", "value": "local"}, + map[string]interface{}{"name": "channel", "value": "issues"}, + }, + }, + }), + }), + }, + }, + }, + } +} + +// ─── Tests ─────────────────────────────────────────────────────────────────── + +// TestBuildWebSubAPIDeploymentYAML_AllChannelsPresentInYAML is the regression test +// for the bug where AllChannels had a `json` struct tag instead of `yaml`, causing +// it to be silently omitted from the marshaled YAML sent to the gateway controller. +func TestBuildWebSubAPIDeploymentYAML_AllChannelsPresentInYAML(t *testing.T) { + websubAPI := buildTestWebSubAPI() + d := buildWebSubAPIDeploymentYAML(websubAPI) + + raw, err := yaml.Marshal(d) + if err != nil { + t.Fatalf("yaml.Marshal failed: %v", err) + } + yamlStr := string(raw) + + if !strings.Contains(yamlStr, "allChannels") { + t.Errorf("allChannels missing from marshaled YAML; this means the struct tag was wrong (json instead of yaml).\nFull YAML:\n%s", yamlStr) + } +} + +// TestBuildWebSubAPIDeploymentYAML_AllChannelsPoliciesPresent verifies that global +// (all-channel) policies from the API configuration appear in the deployment YAML. +func TestBuildWebSubAPIDeploymentYAML_AllChannelsPoliciesPresent(t *testing.T) { + websubAPI := buildTestWebSubAPI() + d := buildWebSubAPIDeploymentYAML(websubAPI) + + raw, err := yaml.Marshal(d) + if err != nil { + t.Fatalf("yaml.Marshal failed: %v", err) + } + yamlStr := string(raw) + + cases := []struct { + desc string + want string + }{ + {"on_subscription present", "on_subscription"}, + {"on_message_received present", "on_message_received"}, + {"on_message_delivery present", "on_message_delivery"}, + {"api-key-auth policy present", "api-key-auth"}, + {"basic-auth policy present", "basic-auth"}, + } + for _, tc := range cases { + if !strings.Contains(yamlStr, tc.want) { + t.Errorf("%s: %q not found in YAML.\nFull YAML:\n%s", tc.desc, tc.want, yamlStr) + } + } +} + +// TestBuildWebSubAPIDeploymentYAML_ChannelPoliciesPresentInYAML verifies that +// per-channel policy overrides appear in the marshaled YAML, not just the struct. +func TestBuildWebSubAPIDeploymentYAML_ChannelPoliciesPresentInYAML(t *testing.T) { + websubAPI := buildTestWebSubAPI() + d := buildWebSubAPIDeploymentYAML(websubAPI) + + raw, err := yaml.Marshal(d) + if err != nil { + t.Fatalf("yaml.Marshal failed: %v", err) + } + yamlStr := string(raw) + + if !strings.Contains(yamlStr, "channels") { + t.Errorf("channels section missing from marshaled YAML.\nFull YAML:\n%s", yamlStr) + } + if !strings.Contains(yamlStr, "issues") { + t.Errorf("'issues' channel missing from marshaled YAML.\nFull YAML:\n%s", yamlStr) + } +} + +// TestBuildWebSubAPIDeploymentYAML_ChannelPoliciesNotWrapped verifies that per-channel +// event policies appear DIRECTLY on the channel (no extra "policies:" wrapper key). +// The gateway-controller's WebSubChannel schema has on_subscription/on_message_delivery +// at the top level of the channel, not nested under a "policies:" key. +func TestBuildWebSubAPIDeploymentYAML_ChannelPoliciesNotWrapped(t *testing.T) { + websubAPI := buildTestWebSubAPI() + d := buildWebSubAPIDeploymentYAML(websubAPI) + + raw, err := yaml.Marshal(d) + if err != nil { + t.Fatalf("yaml.Marshal failed: %v", err) + } + + // Unmarshal into a generic map to inspect the channel structure precisely. + var parsed map[string]interface{} + if err := yaml.Unmarshal(raw, &parsed); err != nil { + t.Fatalf("yaml.Unmarshal failed: %v", err) + } + + spec, _ := parsed["spec"].(map[string]interface{}) + channels, _ := spec["channels"].(map[string]interface{}) + issuesCh, ok := channels["issues"].(map[string]interface{}) + if !ok { + t.Fatalf("'issues' channel not found or wrong type in parsed YAML") + } + + // The "on_message_delivery" key must exist directly on the channel, NOT under a "policies" key. + if _, hasDirect := issuesCh["on_message_delivery"]; !hasDirect { + t.Errorf("on_message_delivery should be a direct key of the 'issues' channel entry, not wrapped under 'policies'.\nChannel map keys: %v", keysOf(issuesCh)) + } + if _, hasWrapper := issuesCh["policies"]; hasWrapper { + t.Errorf("unexpected 'policies' wrapper key found inside 'issues' channel; gateway-controller expects event policies at the top level of each channel") + } +} + +// keysOf returns the keys of a map[string]interface{} for test error messages. +func keysOf(m map[string]interface{}) []string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + return keys +} // TestBuildWebSubAPIDeploymentYAML_ChannelDeliveryPolicyParams verifies the +// per-channel on_message_delivery set-headers policy and its params are in the YAML. +// The "local" value appears only in the channel-level policy; if channels are dropped +// from YAML output, this test will catch it. +func TestBuildWebSubAPIDeploymentYAML_ChannelDeliveryPolicyParams(t *testing.T) { + websubAPI := buildTestWebSubAPI() + d := buildWebSubAPIDeploymentYAML(websubAPI) + + raw, err := yaml.Marshal(d) + if err != nil { + t.Fatalf("yaml.Marshal failed: %v", err) + } + yamlStr := string(raw) + + if !strings.Contains(yamlStr, "local") { + t.Errorf("channel-level policy param value 'local' missing from YAML; channel policies may have been dropped.\nFull YAML:\n%s", yamlStr) + } +} + +// TestBuildWebSubAPIDeploymentYAML_AllChannelsStruct verifies the in-memory struct +// has AllChannels populated with the expected policies before marshaling. +func TestBuildWebSubAPIDeploymentYAML_AllChannelsStruct(t *testing.T) { + websubAPI := buildTestWebSubAPI() + d := buildWebSubAPIDeploymentYAML(websubAPI) + + if d.Spec.AllChannels == nil { + t.Fatal("Spec.AllChannels should not be nil") + } + + ac := d.Spec.AllChannels + if ac.OnSubscription == nil || ac.OnSubscription.Policies == nil || len(*ac.OnSubscription.Policies) != 1 { + t.Errorf("expected 1 OnSubscription policy in AllChannels, got %v", ac.OnSubscription) + } + if (*ac.OnSubscription.Policies)[0].Name != "api-key-auth" { + t.Errorf("expected 'api-key-auth', got %q", (*ac.OnSubscription.Policies)[0].Name) + } + if ac.OnMessageReceived == nil || ac.OnMessageReceived.Policies == nil || len(*ac.OnMessageReceived.Policies) != 1 { + t.Errorf("expected 1 OnMessageReceived policy in AllChannels, got %v", ac.OnMessageReceived) + } + if (*ac.OnMessageReceived.Policies)[0].Name != "basic-auth" { + t.Errorf("expected 'basic-auth', got %q", (*ac.OnMessageReceived.Policies)[0].Name) + } + if ac.OnMessageDelivery == nil || ac.OnMessageDelivery.Policies == nil || len(*ac.OnMessageDelivery.Policies) != 1 { + t.Errorf("expected 1 OnMessageDelivery policy in AllChannels, got %v", ac.OnMessageDelivery) + } +} + +// TestBuildWebSubAPIDeploymentYAML_ChannelsStruct verifies the in-memory struct +// has per-channel policies populated correctly. +func TestBuildWebSubAPIDeploymentYAML_ChannelsStruct(t *testing.T) { + websubAPI := buildTestWebSubAPI() + d := buildWebSubAPIDeploymentYAML(websubAPI) + + if len(d.Spec.Channels) != 1 { + t.Fatalf("expected 1 channel in Spec.Channels, got %d", len(d.Spec.Channels)) + } + ch, ok := d.Spec.Channels["issues"] + if !ok { + t.Fatal("'issues' channel not found in Spec.Channels") + } + if ch.OnMessageDelivery == nil || ch.OnMessageDelivery.Policies == nil { + t.Fatal("'issues' channel OnMessageDelivery should not be nil") + } + if len(*ch.OnMessageDelivery.Policies) != 1 { + t.Errorf("expected 1 OnMessageDelivery policy for 'issues' channel, got %d", len(*ch.OnMessageDelivery.Policies)) + } + if (*ch.OnMessageDelivery.Policies)[0].Name != "set-headers" { + t.Errorf("expected 'set-headers' in 'issues' channel, got %q", (*ch.OnMessageDelivery.Policies)[0].Name) + } +} + +// TestBuildWebSubAPIDeploymentYAML_NoChannels verifies correct YAML output +// when no per-channel policies are defined. +func TestBuildWebSubAPIDeploymentYAML_NoChannels(t *testing.T) { + websubAPI := &model.WebSubAPI{ + Handle: "simple-api", + Name: "Simple API", + Version: "v1.0", + Configuration: model.WebSubAPIConfiguration{ + Context: strPtr("/simple"), + AllChannels: &model.WebSubAllChannelPolicies{ + OnSubscription: makeEventPolicies(makePolicy("api-key-auth", "v1")), + }, + }, + } + d := buildWebSubAPIDeploymentYAML(websubAPI) + + raw, err := yaml.Marshal(d) + if err != nil { + t.Fatalf("yaml.Marshal failed: %v", err) + } + yamlStr := string(raw) + + if !strings.Contains(yamlStr, "allChannels") { + t.Errorf("allChannels missing from marshaled YAML when no per-channel policies exist.\nFull YAML:\n%s", yamlStr) + } + if !strings.Contains(yamlStr, "api-key-auth") { + t.Errorf("api-key-auth policy missing from marshaled YAML.\nFull YAML:\n%s", yamlStr) + } +} + +// TestBuildWebSubAPIDeploymentYAML_NilAllChannels verifies no panic and no allChannels +// key in YAML when AllChannels is nil. +func TestBuildWebSubAPIDeploymentYAML_NilAllChannels(t *testing.T) { + websubAPI := &model.WebSubAPI{ + Handle: "bare-api", + Name: "Bare API", + Version: "v1.0", + Configuration: model.WebSubAPIConfiguration{ + Context: strPtr("/bare"), + AllChannels: nil, + }, + } + d := buildWebSubAPIDeploymentYAML(websubAPI) + + raw, err := yaml.Marshal(d) + if err != nil { + t.Fatalf("yaml.Marshal failed: %v", err) + } + yamlStr := string(raw) + + if strings.Contains(yamlStr, "allChannels") { + t.Errorf("allChannels should not appear in YAML when nil.\nFull YAML:\n%s", yamlStr) + } +} + +// TestBuildWebSubAPIDeploymentYAML_Context verifies that the context is correctly +// set in the deployment struct, defaulting to "/" when nil or empty. +func TestBuildWebSubAPIDeploymentYAML_Context(t *testing.T) { + t.Run("explicit context", func(t *testing.T) { + websubAPI := buildTestWebSubAPI() + d := buildWebSubAPIDeploymentYAML(websubAPI) + if d.Spec.Context != "/repos1" { + t.Errorf("expected context '/repos1', got %q", d.Spec.Context) + } + }) + + t.Run("nil context defaults to /", func(t *testing.T) { + websubAPI := &model.WebSubAPI{ + Handle: "ctx-api", + Name: "ctx api", + Version: "v1.0", + Configuration: model.WebSubAPIConfiguration{Context: nil}, + } + d := buildWebSubAPIDeploymentYAML(websubAPI) + if d.Spec.Context != "/" { + t.Errorf("expected default context '/', got %q", d.Spec.Context) + } + }) + + t.Run("empty context defaults to /", func(t *testing.T) { + empty := "" + websubAPI := &model.WebSubAPI{ + Handle: "ctx-api2", + Name: "ctx api2", + Version: "v1.0", + Configuration: model.WebSubAPIConfiguration{Context: &empty}, + } + d := buildWebSubAPIDeploymentYAML(websubAPI) + if d.Spec.Context != "/" { + t.Errorf("expected default context '/', got %q", d.Spec.Context) + } + }) +} + +// TestBuildWebSubAPIDeploymentYAML_ProjectLabelSet verifies the projectId label +// is set in metadata when ProjectUUID is non-empty. +func TestBuildWebSubAPIDeploymentYAML_ProjectLabelSet(t *testing.T) { + websubAPI := buildTestWebSubAPI() + d := buildWebSubAPIDeploymentYAML(websubAPI) + + if d.Metadata.Labels == nil { + t.Fatal("Metadata.Labels should not be nil when ProjectUUID is set") + } + if d.Metadata.Labels["projectId"] != websubAPI.ProjectUUID { + t.Errorf("expected projectId label %q, got %q", websubAPI.ProjectUUID, d.Metadata.Labels["projectId"]) + } +} + +// TestBuildWebSubAPIDeploymentYAML_ProjectLabelAbsent verifies no labels are set +// when ProjectUUID is empty. +func TestBuildWebSubAPIDeploymentYAML_ProjectLabelAbsent(t *testing.T) { + websubAPI := buildTestWebSubAPI() + websubAPI.ProjectUUID = "" + d := buildWebSubAPIDeploymentYAML(websubAPI) + + if d.Metadata.Labels != nil { + t.Errorf("expected nil Metadata.Labels when ProjectUUID is empty, got %v", d.Metadata.Labels) + } +} diff --git a/platform-api/src/internal/service/websub_api_test.go b/platform-api/src/internal/service/websub_api_test.go new file mode 100644 index 000000000..a4277a77c --- /dev/null +++ b/platform-api/src/internal/service/websub_api_test.go @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +package service + +import ( + "testing" + "time" + + "platform-api/src/api" + "platform-api/src/internal/model" + "platform-api/src/internal/repository" + "platform-api/src/internal/utils" +) + +// ─── Mock Repository ───────────────────────────────────────────────────────── + +type mockWebSubAPIRepository struct { + repository.WebSubAPIRepository + store map[string]*model.WebSubAPI + exists bool + count int +} + +func newMockWebSubAPIRepository() *mockWebSubAPIRepository { + return &mockWebSubAPIRepository{store: make(map[string]*model.WebSubAPI)} +} + +func (m *mockWebSubAPIRepository) Create(a *model.WebSubAPI) error { + a.UUID = "test-uuid" + a.CreatedAt = time.Now() + a.UpdatedAt = time.Now() + m.store[a.Handle] = a + return nil +} +func (m *mockWebSubAPIRepository) GetByHandle(handle, _ string) (*model.WebSubAPI, error) { + a, ok := m.store[handle] + if !ok { + return nil, nil + } + return a, nil +} +func (m *mockWebSubAPIRepository) Update(a *model.WebSubAPI) error { + a.UpdatedAt = time.Now() + m.store[a.Handle] = a + return nil +} +func (m *mockWebSubAPIRepository) Delete(handle, _ string) error { + delete(m.store, handle) + return nil +} +func (m *mockWebSubAPIRepository) Exists(handle, _ string) (bool, error) { return m.exists, nil } +func (m *mockWebSubAPIRepository) Count(_ string) (int, error) { return m.count, nil } +func (m *mockWebSubAPIRepository) CountByProject(_, _ string) (int, error) { + return m.count, nil +} +func (m *mockWebSubAPIRepository) List(_, _ string, _, _ int) ([]*model.WebSubAPI, error) { + result := make([]*model.WebSubAPI, 0, len(m.store)) + for _, v := range m.store { + result = append(result, v) + } + return result, nil +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +func policySlice(name, version string) *[]api.Policy { + return &[]api.Policy{{Name: name, Version: version}} +} + +func buildCreateRequest() *api.WebSubAPI { + handle := "repo-watcher-v1-0" + ctx := "/repos" + return &api.WebSubAPI{ + Id: &handle, + Name: "repo-watcher", + Version: "v1.0", + ProjectId: "project-uuid", + Context: &ctx, + Upstream: api.Upstream{}, + Policies: &api.WebSubChannelPolicies{ + OnSubscription: policySlice("api-key-auth", "v1"), + OnUnsubscription: &[]api.Policy{}, + OnMessageReceived: policySlice("websub-hmac-auth", "v1"), + OnMessageDelivery: &[]api.Policy{}, + }, + Channels: &map[string]api.WebSubChannel{ + "issues": { + Policies: &api.WebSubChannelPolicies{ + OnSubscription: &[]api.Policy{}, + OnUnsubscription: &[]api.Policy{}, + OnMessageReceived: &[]api.Policy{}, + OnMessageDelivery: &[]api.Policy{}, + }, + }, + }, + } +} + +func buildService(repo *mockWebSubAPIRepository) *WebSubAPIService { + return &WebSubAPIService{ + repo: repo, + apiUtil: &utils.APIUtil{}, + } +} + +// ─── Tests ─────────────────────────────────────────────────────────────────── + +// TestWebSubAPI_PoliciesStoredAsAllChannels verifies that the flat "policies" +// sent by the client are converted to AllChannels in the stored model. +func TestWebSubAPI_PoliciesStoredAsAllChannels(t *testing.T) { + repo := newMockWebSubAPIRepository() + svc := buildService(repo) + + req := buildCreateRequest() + _, err := svc.Create("org-uuid", "alice", req) + if err != nil { + t.Fatalf("Create failed: %v", err) + } + + stored := repo.store["repo-watcher-v1-0"] + if stored == nil { + t.Fatal("API not found in store after Create") + } + + if stored.Configuration.AllChannels == nil { + t.Fatal("AllChannels should not be nil after storing flat policies") + } + + ac := stored.Configuration.AllChannels + if ac.OnSubscription == nil || len(ac.OnSubscription.Policies) != 1 { + t.Errorf("expected 1 OnSubscription policy, got %v", ac.OnSubscription) + } + if ac.OnSubscription.Policies[0].Name != "api-key-auth" { + t.Errorf("expected policy name 'api-key-auth', got %q", ac.OnSubscription.Policies[0].Name) + } + if ac.OnMessageReceived == nil || len(ac.OnMessageReceived.Policies) != 1 { + t.Errorf("expected 1 OnMessageReceived policy, got %v", ac.OnMessageReceived) + } + if ac.OnMessageReceived.Policies[0].Name != "websub-hmac-auth" { + t.Errorf("expected policy name 'websub-hmac-auth', got %q", ac.OnMessageReceived.Policies[0].Name) + } +} + +// TestWebSubAPI_GetReturnsFlatPolicies verifies that the stored AllChannels +// is returned as flat "policies" in the API response. +func TestWebSubAPI_GetReturnsFlatPolicies(t *testing.T) { + repo := newMockWebSubAPIRepository() + svc := buildService(repo) + + req := buildCreateRequest() + _, err := svc.Create("org-uuid", "alice", req) + if err != nil { + t.Fatalf("Create failed: %v", err) + } + + resp, err := svc.Get("org-uuid", "repo-watcher-v1-0") + if err != nil { + t.Fatalf("Get failed: %v", err) + } + if resp.Policies == nil { + t.Fatal("Get response should have Policies (not AllChannels)") + } + + p := resp.Policies + if p.OnSubscription == nil || len(*p.OnSubscription) != 1 { + t.Errorf("expected 1 OnSubscription policy in response, got %v", p.OnSubscription) + } + if (*p.OnSubscription)[0].Name != "api-key-auth" { + t.Errorf("expected policy name 'api-key-auth', got %q", (*p.OnSubscription)[0].Name) + } + if p.OnMessageReceived == nil || len(*p.OnMessageReceived) != 1 { + t.Errorf("expected 1 OnMessageReceived policy in response, got %v", p.OnMessageReceived) + } + if (*p.OnMessageReceived)[0].Name != "websub-hmac-auth" { + t.Errorf("expected policy name 'websub-hmac-auth', got %q", (*p.OnMessageReceived)[0].Name) + } +} + +// TestWebSubAPI_ChannelPoliciesStoredAndReturned verifies channel-level policies +// are stored and returned correctly with the new structure. +func TestWebSubAPI_ChannelPoliciesStoredAndReturned(t *testing.T) { + repo := newMockWebSubAPIRepository() + svc := buildService(repo) + + req := buildCreateRequest() + _, err := svc.Create("org-uuid", "alice", req) + if err != nil { + t.Fatalf("Create failed: %v", err) + } + + // Verify stored model channel structure + stored := repo.store["repo-watcher-v1-0"] + if len(stored.Configuration.Channels) != 1 { + t.Fatalf("expected 1 channel, got %d", len(stored.Configuration.Channels)) + } + ch, ok := stored.Configuration.Channels["issues"] + if !ok { + t.Fatal("'issues' channel not found in stored model") + } + // Empty slices (&[]api.Policy{}) are non-nil, so policySlicePtrToEventPolicies + // returns a non-nil *WebSubEventPolicies with an empty Policies slice. + if ch.OnSubscription == nil || len(ch.OnSubscription.Policies) != 0 { + t.Errorf("expected ch.OnSubscription non-nil with empty policies, got %v", ch.OnSubscription) + } + if ch.OnUnsubscription == nil || len(ch.OnUnsubscription.Policies) != 0 { + t.Errorf("expected ch.OnUnsubscription non-nil with empty policies, got %v", ch.OnUnsubscription) + } + if ch.OnMessageReceived == nil || len(ch.OnMessageReceived.Policies) != 0 { + t.Errorf("expected ch.OnMessageReceived non-nil with empty policies, got %v", ch.OnMessageReceived) + } + if ch.OnMessageDelivery == nil || len(ch.OnMessageDelivery.Policies) != 0 { + t.Errorf("expected ch.OnMessageDelivery non-nil with empty policies, got %v", ch.OnMessageDelivery) + } + + // Verify API response channel structure + resp, err := svc.Get("org-uuid", "repo-watcher-v1-0") + if err != nil { + t.Fatalf("Get failed: %v", err) + } + if resp.Channels == nil { + t.Fatal("response Channels should not be nil") + } + _, ok = (*resp.Channels)["issues"] + if !ok { + t.Fatal("'issues' channel not found in response") + } +} + +// TestWebSubAPI_UpdatePolicies verifies that updating the API replaces AllChannels +// with new values from the incoming flat policies. +func TestWebSubAPI_UpdatePolicies(t *testing.T) { + repo := newMockWebSubAPIRepository() + svc := buildService(repo) + + req := buildCreateRequest() + _, err := svc.Create("org-uuid", "alice", req) + if err != nil { + t.Fatalf("Create failed: %v", err) + } + + // Update with different policies + updateReq := buildCreateRequest() + updateReq.Policies = &api.WebSubChannelPolicies{ + OnSubscription: policySlice("jwt-auth", "v1"), + OnUnsubscription: nil, + OnMessageReceived: nil, + OnMessageDelivery: nil, + } + + _, err = svc.Update("org-uuid", "repo-watcher-v1-0", updateReq) + if err != nil { + t.Fatalf("Update failed: %v", err) + } + + resp, err := svc.Get("org-uuid", "repo-watcher-v1-0") + if err != nil { + t.Fatalf("Get after Update failed: %v", err) + } + if resp.Policies == nil { + t.Fatal("Policies should not be nil after update") + } + if resp.Policies.OnSubscription == nil || len(*resp.Policies.OnSubscription) != 1 { + t.Errorf("expected 1 OnSubscription policy after update, got %v", resp.Policies.OnSubscription) + } + if (*resp.Policies.OnSubscription)[0].Name != "jwt-auth" { + t.Errorf("expected updated policy name 'jwt-auth', got %q", (*resp.Policies.OnSubscription)[0].Name) + } + if resp.Policies.OnMessageReceived != nil { + t.Errorf("expected OnMessageReceived to be nil after update, got %v", resp.Policies.OnMessageReceived) + } +} + +// TestWebSubAPI_Delete verifies that Delete removes the API. +func TestWebSubAPI_Delete(t *testing.T) { + repo := newMockWebSubAPIRepository() + svc := buildService(repo) + + req := buildCreateRequest() + _, err := svc.Create("org-uuid", "alice", req) + if err != nil { + t.Fatalf("Create failed: %v", err) + } + + err = svc.Delete("org-uuid", "repo-watcher-v1-0") + if err != nil { + t.Fatalf("Delete failed: %v", err) + } + + if _, ok := repo.store["repo-watcher-v1-0"]; ok { + t.Error("API should not exist in store after Delete") + } +} + +// TestWebSubAPI_List verifies that List returns the correct number of items. +func TestWebSubAPI_List(t *testing.T) { + repo := newMockWebSubAPIRepository() + svc := buildService(repo) + + req := buildCreateRequest() + _, err := svc.Create("org-uuid", "alice", req) + if err != nil { + t.Fatalf("Create failed: %v", err) + } + + listResp, err := svc.List("org-uuid", "project-uuid", 20, 0) + if err != nil { + t.Fatalf("List failed: %v", err) + } + if listResp.Count != 1 { + t.Errorf("expected 1 item in list, got %d", listResp.Count) + } +} + +// TestWebSubAPI_MapPoliciesAPIToAllChannels tests the low-level mapping helper. +func TestWebSubAPI_MapPoliciesAPIToAllChannels(t *testing.T) { + in := &api.WebSubChannelPolicies{ + OnSubscription: policySlice("api-key-auth", "v1"), + OnMessageReceived: policySlice("websub-hmac-auth", "v1"), + } + out := mapWebSubPoliciesAPIToAllChannels(in) + if out == nil { + t.Fatal("expected non-nil AllChannels") + } + if out.OnSubscription == nil || len(out.OnSubscription.Policies) != 1 { + t.Errorf("OnSubscription: expected 1 policy, got %v", out.OnSubscription) + } + if out.OnSubscription.Policies[0].Name != "api-key-auth" { + t.Errorf("expected 'api-key-auth', got %q", out.OnSubscription.Policies[0].Name) + } + if out.OnUnsubscription != nil { + t.Errorf("expected nil OnUnsubscription, got %v", out.OnUnsubscription) + } +} + +// TestWebSubAPI_MapAllChannelsModelToWebSubPolicies tests the reverse mapping helper. +func TestWebSubAPI_MapAllChannelsModelToWebSubPolicies(t *testing.T) { + in := &model.WebSubAllChannelPolicies{ + OnSubscription: &model.WebSubEventPolicies{ + Policies: []model.Policy{{Name: "api-key-auth", Version: "v1"}}, + }, + OnMessageReceived: &model.WebSubEventPolicies{ + Policies: []model.Policy{{Name: "websub-hmac-auth", Version: "v1"}}, + }, + } + out := mapAllChannelsModelToWebSubPolicies(in) + if out == nil { + t.Fatal("expected non-nil WebSubChannelPolicies") + } + if out.OnSubscription == nil || len(*out.OnSubscription) != 1 { + t.Errorf("expected 1 OnSubscription policy, got %v", out.OnSubscription) + } + if (*out.OnSubscription)[0].Name != "api-key-auth" { + t.Errorf("expected 'api-key-auth', got %q", (*out.OnSubscription)[0].Name) + } + if out.OnMessageReceived == nil || len(*out.OnMessageReceived) != 1 { + t.Errorf("expected 1 OnMessageReceived policy, got %v", out.OnMessageReceived) + } + if out.OnUnsubscription != nil { + t.Errorf("expected nil OnUnsubscription, got %v", out.OnUnsubscription) + } +} + +// TestWebSubAPI_NilPoliciesHandled ensures nil policies are handled gracefully. +func TestWebSubAPI_NilPoliciesHandled(t *testing.T) { + if got := mapWebSubPoliciesAPIToAllChannels(nil); got != nil { + t.Errorf("expected nil for nil input, got %v", got) + } + if got := mapAllChannelsModelToWebSubPolicies(nil); got != nil { + t.Errorf("expected nil for nil input, got %v", got) + } +} diff --git a/platform-api/src/resources/openapi.yaml b/platform-api/src/resources/openapi.yaml index 3fa57b2dd..a98d690b9 100644 --- a/platform-api/src/resources/openapi.yaml +++ b/platform-api/src/resources/openapi.yaml @@ -9044,8 +9044,8 @@ components: example: /my-websub-api upstream: $ref: '#/components/schemas/Upstream' - allChannels: - $ref: '#/components/schemas/WebSubAllChannelPolicies' + policies: + $ref: '#/components/schemas/WebSubChannelPolicies' channels: type: object description: Per-channel configuration keyed by channel name. Each key is a channel name and defines policies applied only to that channel. @@ -9198,14 +9198,8 @@ components: type: object description: A single channel definition with optional per-channel policy overrides. properties: - on_subscription: - $ref: '#/components/schemas/WebSubEventPolicies' - on_unsubscription: - $ref: '#/components/schemas/WebSubEventPolicies' - on_message_received: - $ref: '#/components/schemas/WebSubEventPolicies' - on_message_delivery: - $ref: '#/components/schemas/WebSubEventPolicies' + policies: + $ref: '#/components/schemas/WebSubChannelPolicies' responses: Unauthorized: