Skip to main content
Version: 2.6.0

GraphQL Query Rate Limiting

Overview

This policy is an example of how to implement rate limiting for GraphQL queries using the Classifier.

Configuration

The following policy contains a Classifier that extracts the userID claim from a JWT token in the request's authorization header and then rate limit unique users based on the extracted user_id Flow Label; todo-service.prod.svc.cluster.local is selected as the target service for this policy.

tip

Classification rules can be written based on HTTP requests and scheduler priorities can be defined based on Flow Labels by live previewing them first using introspection APIs.

# yaml-language-server: $schema=../../../../../../blueprints/policies/rate-limiting/gen/definitions.json
policy:
policy_name: "graphql-rate-limiting"
rate_limiter:
bucket_capacity: 40
fill_amount: 2
selectors:
- control_point: ingress
service: todo-service.svc.cluster.local
parameters:
label_key: user_id
interval: 1s
resources:
flow_control:
classifiers:
- selectors:
- control_point: ingress
service: todo-service.svc.cluster.local
rego:
labels:
user_id:
telemetry: true
module: |
package graphql_example
import future.keywords.if
query_ast := graphql.parse_query(input.parsed_body.query)
claims := payload if {
io.jwt.verify_hs256(bearer_token, "secret")
[_, payload, _] := io.jwt.decode(bearer_token)
}
bearer_token := t if {
v := input.attributes.request.http.headers.authorization
startswith(v, "Bearer ")
t := substring(v, count("Bearer "), -1)
}
queryIsCreateTodo if {
some operation
walk(query_ast, [_, operation])
operation.Name == "createTodo"
count(operation.SelectionSet) > 0
some selection
walk(operation.SelectionSet, [_, selection])
selection.Name == "createTodo"
}
user_id := u if {
queryIsCreateTodo
u := claims.userID
}

Generated Policy

apiVersion: fluxninja.com/v1alpha1
kind: Policy
metadata:
annotations:
fluxninja.com/blueprint-name: policies/rate-limiting
fluxninja.com/blueprints-uri: local
fluxninja.com/values: '{"policy": {"policy_name": "graphql-rate-limiting", "rate_limiter":
{"bucket_capacity": 40, "fill_amount": 2, "parameters": {"interval": "1s", "label_key":
"user_id"}, "selectors": [{"control_point": "ingress", "service": "todo-service.svc.cluster.local"}]},
"resources": {"flow_control": {"classifiers": [{"rego": {"labels": {"user_id":
{"telemetry": true}}, "module": "package graphql_example\nimport future.keywords.if\nquery_ast
:= graphql.parse_query(input.parsed_body.query)\nclaims := payload if {\n io.jwt.verify_hs256(bearer_token,
\"secret\")\n [_, payload, _] := io.jwt.decode(bearer_token)\n}\nbearer_token
:= t if {\n v := input.attributes.request.http.headers.authorization\n startswith(v,
\"Bearer \")\n t := substring(v, count(\"Bearer \"), -1)\n}\nqueryIsCreateTodo
if {\n some operation\n walk(query_ast, [_, operation])\n operation.Name
== \"createTodo\"\n count(operation.SelectionSet) > 0\n some selection\n walk(operation.SelectionSet,
[_, selection])\n selection.Name == \"createTodo\"\n}\nuser_id := u if {\n queryIsCreateTodo\n u
:= claims.userID\n}\n"}, "selectors": [{"control_point": "ingress", "service":
"todo-service.svc.cluster.local"}]}]}}}}'
labels:
fluxninja.com/validate: "true"
name: graphql-rate-limiting
spec:
circuit:
components:
- flow_control:
rate_limiter:
in_ports:
bucket_capacity:
constant_signal:
value: 40
fill_amount:
constant_signal:
value: 2
parameters:
interval: 1s
label_key: user_id
selectors:
- control_point: ingress
service: todo-service.svc.cluster.local
evaluation_interval: 1s
resources:
flow_control:
classifiers:
- rego:
labels:
user_id:
telemetry: true
module: |
package graphql_example
import future.keywords.if
query_ast := graphql.parse_query(input.parsed_body.query)
claims := payload if {
io.jwt.verify_hs256(bearer_token, "secret")
[_, payload, _] := io.jwt.decode(bearer_token)
}
bearer_token := t if {
v := input.attributes.request.http.headers.authorization
startswith(v, "Bearer ")
t := substring(v, count("Bearer "), -1)
}
queryIsCreateTodo if {
some operation
walk(query_ast, [_, operation])
operation.Name == "createTodo"
count(operation.SelectionSet) > 0
some selection
walk(operation.SelectionSet, [_, selection])
selection.Name == "createTodo"
}
user_id := u if {
queryIsCreateTodo
u := claims.userID
}
selectors:
- control_point: ingress
service: todo-service.svc.cluster.local

info

Circuit Diagram for this policy.

For example, if the mutation query is as follows

mutation createTodo {
createTodo(input: { text: "todo" }) {
user {
id
}
text
done
}
}

Without diving deep into how Rego works, the source section mentioned in this tutorial does the following:

  1. Parse the query
  2. Check if the mutation query is createTodo
  3. Verify the JWT token with a secret key secret (only for demonstration purposes)
  4. Decode the JWT token and extract the userID from the claims
  5. Assign the value of userID to the exported variable userID in Rego source

From there on, the Classifier rule assigns the value of the exported variable userID in Rego source to user_id flow label, effectively creating a label user_id:1. This label is used by the Rate Limiter component in the policy to limit the createTodo mutation query to 2 requests per second with a burst capacity of 40 requests for each userID.

Policy in Action

In this example, the traffic generator is configured to generate 50 requests per second for 2-minutes. When loading the above policy in the playground, you can observe that it accepts no more than 2 requests per second at any given time, and rejects the rest of the requests.

GraphQL Status Rate Limiting