Skip to content

Commit a2b4e19

Browse files
Merge pull request #707 from Tharsanan1/cel-expression-based-ratelimit
Add CEL expression support for key and cost extraction
2 parents 9d3a8ca + 0233ad5 commit a2b4e19

55 files changed

Lines changed: 1695 additions & 84 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

common/go.sum

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE
1313
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1414
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1515
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
16+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1617
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
1718
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
1819
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
@@ -37,10 +38,13 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
3738
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
3839
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
3940
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
41+
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
4042
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
4143
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
4244
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
45+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
4346
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
47+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
4448
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
4549
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
4650
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -54,11 +58,13 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
5458
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
5559
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5660
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
61+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5762
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
5863
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
5964
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
6065
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
6166
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
67+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
6268
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
6369
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
6470
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -98,6 +104,7 @@ google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBN
98104
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
99105
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
100106
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
107+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
101108
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
102109
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
103110
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

gateway/gateway-controller/default-policies/advanced-ratelimit-v0.1.0.yaml

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,29 @@ parameters:
100100
- apiversion: Use API version from context
101101
- routename: Use route name from metadata (default)
102102
- constant: Use a static string value (requires 'key' field)
103-
enum: ["header", "metadata", "ip", "apiname", "apiversion", "routename", "constant"]
103+
- cel: Use CEL expression to extract key (requires 'expression' field)
104+
enum: ["header", "metadata", "ip", "apiname", "apiversion", "routename", "cel", "constant"]
104105
key:
105106
type: string
106107
description: Header name or metadata key (required for header/metadata types)
107108
minLength: 1
108109
maxLength: 256
110+
expression:
111+
type: string
112+
description: |
113+
CEL expression for key extraction (required for cel type).
114+
The expression must return a string value.
115+
Available variables:
116+
- request.Headers: map[string][]string of request headers
117+
- request.Path: string, the request path
118+
- request.Method: string, the HTTP method
119+
- request.Metadata: map[string]any, shared metadata
120+
- api.Name: string, API name
121+
- api.Version: string, API version
122+
- api.Context: string, API context path
123+
Example: 'request.Headers["x-user-id"][0] + ":" + api.Name'
124+
minLength: 1
125+
maxLength: 1024
109126

110127
costExtraction:
111128
type: object
@@ -147,8 +164,11 @@ parameters:
147164
- response_header: Extract from response header (post-response)
148165
- response_metadata: Extract from response metadata (post-response)
149166
- response_body: Extract from JSON response body using jsonPath (post-response)
167+
- request_cel: Use CEL expression to extract cost from request context (pre-request)
168+
- response_cel: Use CEL expression to extract cost from response context (post-response)
150169
enum: ["request_header", "request_metadata", "request_body",
151-
"response_header", "response_metadata", "response_body"]
170+
"response_header", "response_metadata", "response_body",
171+
"request_cel", "response_cel"]
152172

153173
key:
154174
type: string
@@ -167,6 +187,29 @@ parameters:
167187
minLength: 1
168188
maxLength: 512
169189

190+
expression:
191+
type: string
192+
description: |
193+
CEL expression for cost extraction (required for request_cel/response_cel types).
194+
The expression must return a numeric value (int or double).
195+
Available variables for request_cel:
196+
- request.Headers: map[string][]string of request headers
197+
- request.Body: request body bytes (if buffered)
198+
- request.Path: string, the request path
199+
- request.Method: string, the HTTP method
200+
- request.Metadata: map[string]any, shared metadata
201+
Available variables for response_cel:
202+
- response.Headers: map[string][]string of response headers
203+
- response.Body: response body bytes (if buffered)
204+
- response.Status: int, response status code
205+
- request.Headers: map[string][]string of original request headers
206+
- request.Path: string, the original request path
207+
- request.Method: string, the original HTTP method
208+
- request.Metadata: map[string]any, shared metadata
209+
Example: 'int(request.Headers["x-token-count"][0])'
210+
minLength: 1
211+
maxLength: 1024
212+
170213
multiplier:
171214
type: number
172215
description: |
@@ -210,12 +253,29 @@ parameters:
210253
- apiversion: Use API version from context
211254
- routename: Use route name from metadata (default)
212255
- constant: Use a static string value (requires 'key' field)
213-
enum: ["header", "metadata", "ip", "apiname", "apiversion", "routename", "constant"]
256+
- cel: Use CEL expression to extract key (requires 'expression' field)
257+
enum: ["header", "metadata", "ip", "apiname", "apiversion", "routename", "cel", "constant"]
214258
key:
215259
type: string
216260
description: Header name or metadata key (required for header/metadata types)
217261
minLength: 1
218262
maxLength: 256
263+
expression:
264+
type: string
265+
description: |
266+
CEL expression for key extraction (required for cel type).
267+
The expression must return a string value.
268+
Available variables:
269+
- request.Headers: map[string][]string of request headers
270+
- request.Path: string, the request path
271+
- request.Method: string, the HTTP method
272+
- request.Metadata: map[string]any, shared metadata
273+
- api.Name: string, API name
274+
- api.Version: string, API version
275+
- api.Context: string, API context path
276+
Example: 'request.Headers["x-user-id"][0] + ":" + api.Name'
277+
minLength: 1
278+
maxLength: 1024
219279

220280
onRateLimitExceeded:
221281
type: object

gateway/gateway-controller/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515
github.com/knadh/koanf/v2 v2.3.0
1616
github.com/mattn/go-sqlite3 v1.14.32
1717
github.com/oapi-codegen/runtime v1.1.2
18+
github.com/prometheus/client_golang v1.23.2
1819
github.com/stretchr/testify v1.11.1
1920
github.com/wso2/api-platform/common v0.0.0
2021
github.com/wso2/api-platform/sdk v0.0.0
@@ -71,7 +72,6 @@ require (
7172
github.com/perimeterx/marshmallow v1.1.5 // indirect
7273
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
7374
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
74-
github.com/prometheus/client_golang v1.23.2 // indirect
7575
github.com/prometheus/client_model v0.6.2 // indirect
7676
github.com/prometheus/common v0.66.1 // indirect
7777
github.com/prometheus/procfs v0.16.1 // indirect

gateway/gateway-controller/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
8484
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
8585
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
8686
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
87+
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
88+
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
8789
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
8890
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
8991
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
@@ -100,6 +102,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
100102
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
101103
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
102104
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
105+
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
106+
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
103107
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
104108
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
105109
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=

gateway/it/features/ratelimit.feature

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1394,3 +1394,164 @@ Feature: Rate Limiting
13941394
# Group B: 6th request should fail
13951395
When I send a GET request to "http://localhost:8080/ratelimit-constant/v1.0/group-b-1"
13961396
Then the response status code should be 429
1397+
1398+
Scenario: CEL expression based key extraction for per-user rate limiting
1399+
Given I authenticate using basic auth as "admin"
1400+
When I deploy this API configuration:
1401+
"""
1402+
apiVersion: gateway.api-platform.wso2.com/v1alpha1
1403+
kind: RestApi
1404+
metadata:
1405+
name: ratelimit-cel-key-api
1406+
spec:
1407+
displayName: RateLimit CEL Key API
1408+
version: v1.0
1409+
context: /ratelimit-cel-key/$version
1410+
upstream:
1411+
main:
1412+
url: http://sample-backend:9080/api/v1
1413+
operations:
1414+
- method: GET
1415+
path: /resource
1416+
policies:
1417+
- name: advanced-ratelimit
1418+
version: v0.1.0
1419+
params:
1420+
quotas:
1421+
- name: per-user-cel
1422+
limits:
1423+
- limit: 3
1424+
duration: "1h"
1425+
keyExtraction:
1426+
- type: cel
1427+
expression: 'request.Headers["x-user-id"][0]'
1428+
"""
1429+
Then the response should be successful
1430+
And I wait for the endpoint "http://localhost:8080/ratelimit-cel-key/v1.0/resource" to be ready
1431+
1432+
# User-A sends 3 requests using CEL key extraction - should succeed
1433+
When I send 3 GET requests to "http://localhost:8080/ratelimit-cel-key/v1.0/resource" with header "X-User-ID" value "cel-user-A"
1434+
Then the response status code should be 200
1435+
1436+
# User-A's 4th request should be rate limited
1437+
When I send a GET request to "http://localhost:8080/ratelimit-cel-key/v1.0/resource" with header "X-User-ID" value "cel-user-A"
1438+
Then the response status code should be 429
1439+
1440+
# User-B should have separate quota (different CEL-extracted key)
1441+
When I send 3 GET requests to "http://localhost:8080/ratelimit-cel-key/v1.0/resource" with header "X-User-ID" value "cel-user-B"
1442+
Then the response status code should be 200
1443+
1444+
# User-B's 4th request should be rate limited
1445+
When I send a GET request to "http://localhost:8080/ratelimit-cel-key/v1.0/resource" with header "X-User-ID" value "cel-user-B"
1446+
Then the response status code should be 429
1447+
1448+
Scenario: CEL expression based composite key extraction
1449+
Given I authenticate using basic auth as "admin"
1450+
When I deploy this API configuration:
1451+
"""
1452+
apiVersion: gateway.api-platform.wso2.com/v1alpha1
1453+
kind: RestApi
1454+
metadata:
1455+
name: ratelimit-cel-composite-key-api
1456+
spec:
1457+
displayName: RateLimit CEL Composite Key API
1458+
version: v1.0
1459+
context: /ratelimit-cel-composite-key/$version
1460+
upstream:
1461+
main:
1462+
url: http://sample-backend:9080/api/v1
1463+
operations:
1464+
- method: GET
1465+
path: /resource
1466+
policies:
1467+
- name: advanced-ratelimit
1468+
version: v0.1.0
1469+
params:
1470+
quotas:
1471+
- name: per-user-per-api-cel
1472+
limits:
1473+
- limit: 3
1474+
duration: "1h"
1475+
keyExtraction:
1476+
- type: cel
1477+
expression: 'api.Name + ":" + request.Headers["x-user-id"][0]'
1478+
"""
1479+
Then the response should be successful
1480+
And I wait for the endpoint "http://localhost:8080/ratelimit-cel-composite-key/v1.0/resource" to be ready
1481+
1482+
# User-A sends 3 requests - composite key includes API name + user ID
1483+
When I send 3 GET requests to "http://localhost:8080/ratelimit-cel-composite-key/v1.0/resource" with header "X-User-ID" value "composite-user-A"
1484+
Then the response status code should be 200
1485+
1486+
# User-A's 4th request should be rate limited
1487+
When I send a GET request to "http://localhost:8080/ratelimit-cel-composite-key/v1.0/resource" with header "X-User-ID" value "composite-user-A"
1488+
Then the response status code should be 429
1489+
1490+
# User-B should have separate composite key and full quota
1491+
When I send 3 GET requests to "http://localhost:8080/ratelimit-cel-composite-key/v1.0/resource" with header "X-User-ID" value "composite-user-B"
1492+
Then the response status code should be 200
1493+
1494+
Scenario: CEL expression based cost extraction from request header
1495+
Given I authenticate using basic auth as "admin"
1496+
When I deploy this API configuration:
1497+
"""
1498+
apiVersion: gateway.api-platform.wso2.com/v1alpha1
1499+
kind: RestApi
1500+
metadata:
1501+
name: ratelimit-cel-cost-api
1502+
spec:
1503+
displayName: RateLimit CEL Cost API
1504+
version: v1.0
1505+
context: /ratelimit-cel-cost/$version
1506+
upstream:
1507+
main:
1508+
url: http://sample-backend:9080/api/v1
1509+
operations:
1510+
- method: GET
1511+
path: /health
1512+
- method: POST
1513+
path: /resource
1514+
policies:
1515+
- name: advanced-ratelimit
1516+
version: v0.1.0
1517+
params:
1518+
quotas:
1519+
- name: token-quota-cel
1520+
limits:
1521+
- limit: 100
1522+
duration: "1h"
1523+
costExtraction:
1524+
enabled: true
1525+
sources:
1526+
- type: request_cel
1527+
expression: 'int(request.Headers["x-token-cost"][0])'
1528+
default: 1
1529+
"""
1530+
Then the response should be successful
1531+
And I wait for the endpoint "http://localhost:8080/ratelimit-cel-cost/v1.0/health" to be ready
1532+
1533+
# Send a request with X-Token-Cost=40 header, CEL extracts as integer
1534+
When I send a POST request to "http://localhost:8080/ratelimit-cel-cost/v1.0/resource" with header "X-Token-Cost" value "40" with body:
1535+
"""
1536+
{}
1537+
"""
1538+
Then the response status code should be 200
1539+
# After first request: 100 - 40 = 60 remaining
1540+
And the response header "X-RateLimit-Remaining" should be "60"
1541+
1542+
# Send another request with X-Token-Cost=40
1543+
When I send a POST request to "http://localhost:8080/ratelimit-cel-cost/v1.0/resource" with header "X-Token-Cost" value "40" with body:
1544+
"""
1545+
{}
1546+
"""
1547+
Then the response status code should be 200
1548+
# After second request: 60 - 40 = 20 remaining
1549+
And the response header "X-RateLimit-Remaining" should be "20"
1550+
1551+
# Send a request with X-Token-Cost=30 - should be rate limited (need 30, only 20 remaining)
1552+
When I send a POST request to "http://localhost:8080/ratelimit-cel-cost/v1.0/resource" with header "X-Token-Cost" value "30" with body:
1553+
"""
1554+
{}
1555+
"""
1556+
Then the response status code should be 429
1557+
And the response body should contain "Rate limit exceeded"

0 commit comments

Comments
 (0)