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.
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.
- aperturectl values.yaml
# 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
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:
- Parse the query
- Check if the mutation query is
createTodo
- Verify the JWT token with a secret key
secret
(only for demonstration purposes) - Decode the JWT token and extract the
userID
from the claims - Assign the value of
userID
to the exported variableuserID
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.