Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions gateway/gateway-controller/cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ func derivePolicyFromAPIConfig(cfg *models.StoredConfig, routerConfig *config.Ro
apiPolicies := make(map[string]policyenginev1.PolicyInstance)
if apiData.Policies != nil {
for _, p := range *apiData.Policies {
apiPolicies[p.Name] = convertAPIPolicyToModel(p)
apiPolicies[p.Name] = convertAPIPolicyToModel(p, "api")
}
}

Expand All @@ -453,7 +453,7 @@ func derivePolicyFromAPIConfig(cfg *models.StoredConfig, routerConfig *config.Ro
addedNames := make(map[string]struct{})

for _, opPolicy := range *op.Policies {
finalPolicies = append(finalPolicies, convertAPIPolicyToModel(opPolicy))
finalPolicies = append(finalPolicies, convertAPIPolicyToModel(opPolicy, "route"))
addedNames[opPolicy.Name] = struct{}{}
}

Expand Down Expand Up @@ -529,13 +529,19 @@ func derivePolicyFromAPIConfig(cfg *models.StoredConfig, routerConfig *config.Ro
}

// convertAPIPolicyToModel converts generated api.Policy to policyenginev1.PolicyInstance
func convertAPIPolicyToModel(p api.Policy) policyenginev1.PolicyInstance {
func convertAPIPolicyToModel(p api.Policy, attachedTo string) policyenginev1.PolicyInstance {
paramsMap := make(map[string]interface{})
if p.Params != nil {
for k, v := range *p.Params {
paramsMap[k] = v
}
}

// Add attachedTo metadata to parameters
if attachedTo != "" {
paramsMap["attachedTo"] = attachedTo
}

return policyenginev1.PolicyInstance{
Name: p.Name,
Version: p.Version,
Expand Down
14 changes: 10 additions & 4 deletions gateway/gateway-controller/pkg/api/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1607,7 +1607,7 @@ func (s *APIServer) buildStoredPolicyFromAPI(cfg *models.StoredConfig) *models.S
apiPolicies := make(map[string]policyenginev1.PolicyInstance) // name -> policy
if cfg.GetPolicies() != nil {
for _, p := range *cfg.GetPolicies() {
apiPolicies[p.Name] = convertAPIPolicy(p)
apiPolicies[p.Name] = convertAPIPolicy(p, "api")
}
}

Expand All @@ -1630,7 +1630,7 @@ func (s *APIServer) buildStoredPolicyFromAPI(cfg *models.StoredConfig) *models.S
addedNames := make(map[string]struct{})

for _, opPolicy := range *ch.Policies {
finalPolicies = append(finalPolicies, convertAPIPolicy(opPolicy))
finalPolicies = append(finalPolicies, convertAPIPolicy(opPolicy, "route"))
addedNames[opPolicy.Name] = struct{}{}
}

Expand Down Expand Up @@ -1675,7 +1675,7 @@ func (s *APIServer) buildStoredPolicyFromAPI(cfg *models.StoredConfig) *models.S
addedNames := make(map[string]struct{})

for _, opPolicy := range *op.Policies {
finalPolicies = append(finalPolicies, convertAPIPolicy(opPolicy))
finalPolicies = append(finalPolicies, convertAPIPolicy(opPolicy, "route"))
addedNames[opPolicy.Name] = struct{}{}
}

Expand Down Expand Up @@ -1753,13 +1753,19 @@ func (s *APIServer) buildStoredPolicyFromAPI(cfg *models.StoredConfig) *models.S
}

// convertAPIPolicy converts generated api.Policy to policyenginev1.PolicyInstance
func convertAPIPolicy(p api.Policy) policyenginev1.PolicyInstance {
func convertAPIPolicy(p api.Policy, attachedTo string) policyenginev1.PolicyInstance {
paramsMap := make(map[string]interface{})
if p.Params != nil {
for k, v := range *p.Params {
paramsMap[k] = v
}
}

// Add attachedTo metadata to parameters
if attachedTo != "" {
paramsMap["attachedTo"] = attachedTo
}

return policyenginev1.PolicyInstance{
Name: p.Name,
Version: p.Version,
Expand Down
100 changes: 93 additions & 7 deletions gateway/it/features/basic-ratelimit.feature
Original file line number Diff line number Diff line change
Expand Up @@ -156,20 +156,27 @@ Feature: Basic Rate Limiting
upstream:
main:
url: http://sample-backend:9080/api/v1
policies:
- name: basic-ratelimit
version: v0.1.0
params:
limits:
- limit: 3
duration: "1h"
operations:
- method: GET
path: /health
- method: GET
path: /route1
policies:
- name: basic-ratelimit
version: v0.1.0
params:
limits:
- limit: 3
duration: "1h"
- method: GET
path: /route2
policies:
- name: basic-ratelimit
version: v0.1.0
params:
limits:
- limit: 3
duration: "1h"
"""
Then the response should be successful
And I wait for the endpoint "http://localhost:8080/basic-ratelimit-per-route/v1.0/health" to be ready
Expand Down Expand Up @@ -230,3 +237,82 @@ Feature: Basic Rate Limiting
When I send a GET request to "http://localhost:8080/basic-ratelimit-retry/v1.0/resource"
Then the response status code should be 429
And the response header "Retry-After" should exist

Scenario: Rate limit scope based on policy attachment level
Given I authenticate using basic auth as "admin"
When I deploy this API configuration:
"""
apiVersion: gateway.api-platform.wso2.com/v1alpha1
kind: RestApi
metadata:
name: basic-ratelimit-scope-api
spec:
displayName: Basic RateLimit Scope API
version: v1.0
context: /basic-ratelimit-scope/$version
upstream:
main:
url: http://sample-backend:9080/api/v1
policies:
- name: basic-ratelimit
version: v0.1.0
params:
limits:
- limit: 5
duration: "1h"
operations:
- method: GET
path: /health
policies:
- name: basic-ratelimit
version: v0.1.0
params:
limits:
- limit: 100
duration: "1h"
- method: GET
path: /resource-a
- method: GET
path: /resource-b
policies:
- name: basic-ratelimit
version: v0.1.0
params:
limits:
- limit: 3
duration: "1h"
- method: GET
path: /resource-c
"""
Then the response should be successful
And I wait for the endpoint "http://localhost:8080/basic-ratelimit-scope/v1.0/health" to be ready

# Resource B has its own route-level policy (Limit: 3)
# Send 3 requests to B -> Should succeed
When I send 3 GET requests to "http://localhost:8080/basic-ratelimit-scope/v1.0/resource-b"
Then the response status code should be 200

# 4th request to B -> Should fail (Limit 3 exhausted)
When I send a GET request to "http://localhost:8080/basic-ratelimit-scope/v1.0/resource-b"
Then the response status code should be 429

# Resource A and C fall back to API-level policy (Limit: 5, Shared)
# Send 2 requests to A -> Should succeed
When I send 2 GET requests to "http://localhost:8080/basic-ratelimit-scope/v1.0/resource-a"
Then the response status code should be 200

# Send 2 requests to C -> Should succeed (Total 4/5)
When I send 2 GET requests to "http://localhost:8080/basic-ratelimit-scope/v1.0/resource-c"
Then the response status code should be 200

# Send 1 request to A -> Should succeed (Total 5/5)
When I send a GET request to "http://localhost:8080/basic-ratelimit-scope/v1.0/resource-a"
Then the response status code should be 200

# Send 1 request to C -> Should fail (Total 6/5, Limit 5 exhausted)
When I send a GET request to "http://localhost:8080/basic-ratelimit-scope/v1.0/resource-c"
Then the response status code should be 429

# Verify B is still rate limited (independent of A/C bucket)
When I send a GET request to "http://localhost:8080/basic-ratelimit-scope/v1.0/resource-b"
Then the response status code should be 429
1 change: 0 additions & 1 deletion gateway/it/features/ratelimit.feature
Original file line number Diff line number Diff line change
Expand Up @@ -1304,4 +1304,3 @@ Feature: Rate Limiting
# Double-check API-A is still rate limited (not affected by API-B usage)
When I send a GET request to "http://localhost:8080/ratelimit-apiname-isolation-a/v1.0/resource"
Then the response status code should be 429

11 changes: 8 additions & 3 deletions gateway/policies/basic-ratelimit/v0.1.0/basic_ratelimit.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func GetPolicy(
params map[string]interface{},
) (policy.Policy, error) {
// Transform simple limits to full ratelimit config
rlParams := transformToRatelimitParams(params)
rlParams := transformToRatelimitParams(params, metadata)

// Create the delegate ratelimit policy
delegate, err := ratelimit.GetPolicy(metadata, rlParams)
Expand All @@ -52,17 +52,22 @@ func GetPolicy(
// transformToRatelimitParams converts the simple limits array to a full ratelimit
// quota configuration with routename key extraction, and passes through system
// parameters (algorithm, backend, redis, memory).
func transformToRatelimitParams(params map[string]interface{}) map[string]interface{} {
func transformToRatelimitParams(params map[string]interface{}, metadata policy.PolicyMetadata) map[string]interface{} {
limits, _ := params["limits"].([]interface{})

keyExtractorType := "routename"
if metadata.AttachedTo == "api" {
keyExtractorType = "apiname"
}

rlParams := map[string]interface{}{
"quotas": []interface{}{
map[string]interface{}{
"name": "default",
"limits": limits,
"keyExtraction": []interface{}{
map[string]interface{}{
"type": "routename",
"type": keyExtractorType,
},
},
},
Expand Down
7 changes: 7 additions & 0 deletions gateway/policy-engine/internal/xdsclient/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ func (h *ResourceHandler) buildPolicyChain(routeKey string, config *policyengine
APIVersion: apiMetadata.Version,
}

// Check if attachedTo is present in parameters and set it in metadata
if val, ok := policyConfig.Parameters["attachedTo"]; ok {
if attachedTo, ok := val.(string); ok {
metadata.AttachedTo = attachedTo
}
}

// Create instance using factory with metadata and params
// CreateInstance returns the policy and merged params (initParams + runtime params)
impl, mergedParams, err := h.registry.CreateInstance(policyConfig.Name, policyConfig.Version, metadata, policyConfig.Parameters)
Expand Down
3 changes: 3 additions & 0 deletions sdk/gateway/policy/v1alpha/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ type PolicyMetadata struct {

// APIVersion is the version of the API this policy belongs to
APIVersion string

// AttachedTo indicates where the policy is attached (e.g., "api", "route")
AttachedTo string
}

// Policy is the base interface that all policies must implement
Expand Down
Loading