graph_attack 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +2 -1
- data/CHANGELOG.md +10 -0
- data/README.md +12 -57
- data/graph_attack.gemspec +4 -1
- data/lib/graph_attack/version.rb +1 -1
- data/lib/graph_attack.rb +2 -7
- metadata +5 -6
- data/lib/graph_attack/metadata.rb +0 -6
- data/lib/graph_attack/rate_limiter.rb +0 -98
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 71a5e6c0ce41ca59713a49108f5ebcc3a579aefcd1e95273c495a5ae1e4fd6f6
|
4
|
+
data.tar.gz: 5430e07ebf58ac5b9addbe8b397f6fd832fa56091112136f8ce972cd2e1fe0aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cef61dfd8f249877fcdbd6ae1962b6678efd280ef415989d6abe22ebb7e8db68dd164ce47ecce610548ac13352471044741c0bf6918ca050dc2193a5178ab5af
|
7
|
+
data.tar.gz: fdf832b9e228ccbf8aa66777f2f9334a7c32a25a5435e6017e0ac8073c7c636cd21dc06f0416a8abbf9f063ff30f64f209d55d9979e2eeea27fd7dff4f1ffe84
|
data/.rubocop.yml
CHANGED
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.7.
|
1
|
+
2.7.5
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -6,36 +6,19 @@ GraphQL analyser for blocking & throttling.
|
|
6
6
|
|
7
7
|
## Usage
|
8
8
|
|
9
|
-
This gem adds a method to limit access to your GraphQL fields by IP:
|
9
|
+
This gem adds a method to limit access to your GraphQL fields by IP address:
|
10
10
|
|
11
11
|
```rb
|
12
12
|
class QueryType < GraphQL::Schema::Object
|
13
13
|
field :some_expensive_field, String, null: false do
|
14
|
-
extension
|
14
|
+
extension GraphAttack::RateLimit, threshold: 15, interval: 60
|
15
15
|
end
|
16
16
|
|
17
17
|
# …
|
18
18
|
end
|
19
19
|
```
|
20
20
|
|
21
|
-
|
22
|
-
<summary>If using GraphQL::Ruby's legacy schema definition</summary>
|
23
|
-
|
24
|
-
```rb
|
25
|
-
QueryType = GraphQL::ObjectType.define do
|
26
|
-
name 'Query'
|
27
|
-
|
28
|
-
field :someExpensiveField do
|
29
|
-
rate_limit threshold: 15, interval: 60
|
30
|
-
|
31
|
-
# …
|
32
|
-
end
|
33
|
-
end
|
34
|
-
```
|
35
|
-
|
36
|
-
</details>
|
37
|
-
|
38
|
-
This would allow only 15 calls per minute by the same IP.
|
21
|
+
This would allow only 15 calls per minute by the same IP address.
|
39
22
|
|
40
23
|
## Requirements
|
41
24
|
|
@@ -44,11 +27,11 @@ of [Redis](https://redis.io/).
|
|
44
27
|
|
45
28
|
## Installation
|
46
29
|
|
47
|
-
Add these lines to your application
|
30
|
+
Add these lines to your application’s `Gemfile`:
|
48
31
|
|
49
32
|
```ruby
|
50
33
|
# GraphQL analyser for blocking & throttling by IP.
|
51
|
-
gem
|
34
|
+
gem "graph_attack"
|
52
35
|
```
|
53
36
|
|
54
37
|
And then execute:
|
@@ -57,22 +40,7 @@ And then execute:
|
|
57
40
|
$ bundle
|
58
41
|
```
|
59
42
|
|
60
|
-
|
61
|
-
<summary>If using GraphQL::Ruby's legacy schema definition</summary>
|
62
|
-
|
63
|
-
Add the query analyser to your schema:
|
64
|
-
|
65
|
-
```rb
|
66
|
-
ApplicationSchema = GraphQL::Schema.define do
|
67
|
-
query_analyzer GraphAttack::RateLimiter.new
|
68
|
-
|
69
|
-
# …
|
70
|
-
end
|
71
|
-
```
|
72
|
-
|
73
|
-
</details>
|
74
|
-
|
75
|
-
Finally, make sure you add the current user's IP address as `ip:` to the
|
43
|
+
Finally, make sure you add the current user’s IP address as `ip:` to the
|
76
44
|
GraphQL context. E.g.:
|
77
45
|
|
78
46
|
```rb
|
@@ -96,26 +64,13 @@ Use a custom Redis client instead of the default:
|
|
96
64
|
|
97
65
|
```rb
|
98
66
|
field :some_expensive_field, String, null: false do
|
99
|
-
extension
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
redis_client: Redis.new(url: "…"),
|
104
|
-
)
|
67
|
+
extension GraphAttack::RateLimit,
|
68
|
+
threshold: 15,
|
69
|
+
interval: 60,
|
70
|
+
redis_client: Redis.new(url: "…")
|
105
71
|
end
|
106
72
|
```
|
107
73
|
|
108
|
-
<details>
|
109
|
-
<summary>If using GraphQL::Ruby's legacy schema definition</summary>
|
110
|
-
|
111
|
-
```rb
|
112
|
-
query_analyzer GraphAttack::RateLimiter.new(
|
113
|
-
redis_client: Redis.new(url: "…")
|
114
|
-
)
|
115
|
-
```
|
116
|
-
|
117
|
-
</details>
|
118
|
-
|
119
74
|
## Development
|
120
75
|
|
121
76
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
@@ -130,8 +85,8 @@ see the tags on this repository.
|
|
130
85
|
## Releasing
|
131
86
|
|
132
87
|
To release a new version, update the version number in `version.rb`, commit,
|
133
|
-
and then run `
|
134
|
-
|
88
|
+
and then run `bin/rake release`, which will create a git tag for the version,
|
89
|
+
push git commits and tags, and push the gem to
|
135
90
|
[rubygems.org](https://rubygems.org).
|
136
91
|
|
137
92
|
## Contributing
|
data/graph_attack.gemspec
CHANGED
@@ -14,13 +14,16 @@ Gem::Specification.new do |spec|
|
|
14
14
|
spec.description = 'GraphQL analyser for blocking & throttling'
|
15
15
|
spec.homepage = 'https://github.com/sunny/graph_attack'
|
16
16
|
|
17
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
18
|
+
|
17
19
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
20
|
f.match(%r{^(test|spec|features)/})
|
19
21
|
end
|
20
22
|
spec.bindir = 'exe'
|
21
23
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
24
|
spec.require_paths = ['lib']
|
23
|
-
|
25
|
+
|
26
|
+
spec.required_ruby_version = ['>= 2.5.7', '< 3.2']
|
24
27
|
|
25
28
|
# This gem is an analyser for the GraphQL ruby gem.
|
26
29
|
spec.add_dependency 'graphql', '>= 1.7.9'
|
data/lib/graph_attack/version.rb
CHANGED
data/lib/graph_attack.rb
CHANGED
@@ -1,17 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'graphql'
|
4
3
|
require 'ratelimit'
|
5
|
-
|
4
|
+
require 'graphql'
|
6
5
|
require 'graphql/tracing'
|
7
6
|
|
8
7
|
require 'graph_attack/version'
|
9
8
|
|
10
9
|
# Class-based schema
|
11
|
-
require 'graph_attack/rate_limit'
|
12
10
|
require 'graph_attack/error'
|
11
|
+
require 'graph_attack/rate_limit'
|
13
12
|
require 'graph_attack/rate_limited'
|
14
|
-
|
15
|
-
# Legacy schema
|
16
|
-
require 'graph_attack/rate_limiter'
|
17
|
-
require 'graph_attack/metadata'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graph_attack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Fanny Cheung
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2022-05-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: graphql
|
@@ -165,14 +165,13 @@ files:
|
|
165
165
|
- graph_attack.gemspec
|
166
166
|
- lib/graph_attack.rb
|
167
167
|
- lib/graph_attack/error.rb
|
168
|
-
- lib/graph_attack/metadata.rb
|
169
168
|
- lib/graph_attack/rate_limit.rb
|
170
169
|
- lib/graph_attack/rate_limited.rb
|
171
|
-
- lib/graph_attack/rate_limiter.rb
|
172
170
|
- lib/graph_attack/version.rb
|
173
171
|
homepage: https://github.com/sunny/graph_attack
|
174
172
|
licenses: []
|
175
|
-
metadata:
|
173
|
+
metadata:
|
174
|
+
rubygems_mfa_required: 'true'
|
176
175
|
post_install_message:
|
177
176
|
rdoc_options: []
|
178
177
|
require_paths:
|
@@ -184,7 +183,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
184
183
|
version: 2.5.7
|
185
184
|
- - "<"
|
186
185
|
- !ruby/object:Gem::Version
|
187
|
-
version: '2
|
186
|
+
version: '3.2'
|
188
187
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
189
188
|
requirements:
|
190
189
|
- - ">="
|
@@ -1,98 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module GraphAttack
|
4
|
-
# Query analyser you can add to your GraphQL schema to limit calls by IP.
|
5
|
-
#
|
6
|
-
# ApplicationSchema = GraphQL::Schema.define do
|
7
|
-
# query_analyzer GraphAttack::RateLimiter.new
|
8
|
-
# end
|
9
|
-
#
|
10
|
-
class RateLimiter
|
11
|
-
class Error < StandardError; end
|
12
|
-
|
13
|
-
class RateLimited < GraphQL::AnalysisError; end
|
14
|
-
|
15
|
-
def initialize(redis_client: Redis.new)
|
16
|
-
@redis_client = redis_client
|
17
|
-
end
|
18
|
-
|
19
|
-
def initial_value(query)
|
20
|
-
{
|
21
|
-
ip: query.context[:ip],
|
22
|
-
query_rate_limits: [],
|
23
|
-
}
|
24
|
-
end
|
25
|
-
|
26
|
-
def call(memo, visit_type, irep_node)
|
27
|
-
if rate_limited_node?(visit_type, irep_node)
|
28
|
-
data = rate_limit_data(irep_node)
|
29
|
-
|
30
|
-
memo[:query_rate_limits].push(data)
|
31
|
-
|
32
|
-
increment_rate_limit(memo[:ip], data[:key])
|
33
|
-
end
|
34
|
-
|
35
|
-
memo
|
36
|
-
end
|
37
|
-
|
38
|
-
def final_value(memo)
|
39
|
-
handle_exceeded_calls_on_queries(memo)
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
attr_reader :redis_client
|
45
|
-
|
46
|
-
def increment_rate_limit(ip, key)
|
47
|
-
raise Error, 'Missing :ip value on the GraphQL context' unless ip
|
48
|
-
|
49
|
-
rate_limit(ip).add(key)
|
50
|
-
end
|
51
|
-
|
52
|
-
def rate_limit_data(node)
|
53
|
-
data = node.definition.metadata[:rate_limit]
|
54
|
-
|
55
|
-
data.merge(
|
56
|
-
key: "graphql-query-#{node.name}",
|
57
|
-
query_name: node.name,
|
58
|
-
)
|
59
|
-
end
|
60
|
-
|
61
|
-
def handle_exceeded_calls_on_queries(memo)
|
62
|
-
rate_limited_queries = memo[:query_rate_limits].map do |limit_data|
|
63
|
-
next unless calls_exceeded_on_query?(memo[:ip], limit_data)
|
64
|
-
|
65
|
-
limit_data[:query_name]
|
66
|
-
end.compact
|
67
|
-
|
68
|
-
return unless rate_limited_queries.any?
|
69
|
-
|
70
|
-
queries = rate_limited_queries.join(', ')
|
71
|
-
RateLimited.new("Query rate limit exceeded on #{queries}")
|
72
|
-
end
|
73
|
-
|
74
|
-
def calls_exceeded_on_query?(ip, query_limit_data)
|
75
|
-
rate_limit(ip).exceeded?(
|
76
|
-
query_limit_data[:key],
|
77
|
-
threshold: query_limit_data[:threshold],
|
78
|
-
interval: query_limit_data[:interval],
|
79
|
-
)
|
80
|
-
end
|
81
|
-
|
82
|
-
def rate_limit(ip)
|
83
|
-
@rate_limit ||= {}
|
84
|
-
@rate_limit[ip] ||= Ratelimit.new(ip, redis: redis_client)
|
85
|
-
end
|
86
|
-
|
87
|
-
def rate_limited_node?(visit_type, node)
|
88
|
-
query_field_node?(node) &&
|
89
|
-
visit_type == :enter &&
|
90
|
-
node.definition.metadata[:rate_limit]
|
91
|
-
end
|
92
|
-
|
93
|
-
def query_field_node?(node)
|
94
|
-
node.owner_type.name == 'Query' &&
|
95
|
-
node.ast_node.is_a?(GraphQL::Language::Nodes::Field)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|