graph_attack 1.0.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 +5 -5
- data/.circleci/config.yml +6 -9
- data/.github/dependabot.yml +9 -0
- data/.rubocop.yml +121 -13
- data/.ruby-version +1 -0
- data/.travis.yml +4 -2
- data/CHANGELOG.md +30 -0
- data/Gemfile +2 -0
- data/README.md +29 -28
- data/Rakefile +3 -3
- data/bin/console +1 -0
- data/bin/rake +29 -0
- data/bin/rubocop +29 -0
- data/graph_attack.gemspec +15 -3
- data/lib/graph_attack/error.rb +5 -0
- data/lib/graph_attack/rate_limit.rb +50 -0
- data/lib/graph_attack/rate_limited.rb +5 -0
- data/lib/graph_attack/version.rb +3 -1
- data/lib/graph_attack.rb +8 -3
- metadata +51 -14
- data/lib/graph_attack/metadata.rb +0 -4
- data/lib/graph_attack/rate_limiter.rb +0 -89
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
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/.circleci/config.yml
CHANGED
@@ -6,7 +6,7 @@ version: 2
|
|
6
6
|
jobs:
|
7
7
|
build:
|
8
8
|
docker:
|
9
|
-
- image: circleci/ruby:2.
|
9
|
+
- image: circleci/ruby:2.7.3
|
10
10
|
- image: redis
|
11
11
|
|
12
12
|
working_directory: ~/repo
|
@@ -17,19 +17,16 @@ jobs:
|
|
17
17
|
# Download and cache dependencies
|
18
18
|
- restore_cache:
|
19
19
|
keys:
|
20
|
-
-
|
21
|
-
|
22
|
-
- v1-dependencies-
|
20
|
+
- v2-dependencies-{{ checksum "graph_attack.gemspec" }}
|
21
|
+
- v2-dependencies-
|
23
22
|
|
24
|
-
- run:
|
25
|
-
|
26
|
-
command: |
|
27
|
-
bundle install --jobs=4 --retry=3 --path vendor/bundle
|
23
|
+
- run: gem install bundler:2.0.2
|
24
|
+
- run: bundle install --jobs=4 --retry=3 --path vendor/bundle
|
28
25
|
|
29
26
|
- save_cache:
|
30
27
|
paths:
|
31
28
|
- ./vendor/bundle
|
32
|
-
key:
|
29
|
+
key: v2-dependencies-{{ checksum "graph_attack.gemspec" }}
|
33
30
|
|
34
31
|
# Run tests!
|
35
32
|
- run:
|
data/.rubocop.yml
CHANGED
@@ -1,6 +1,11 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-rspec
|
3
|
+
- rubocop-rake
|
4
|
+
|
1
5
|
AllCops:
|
2
|
-
TargetRubyVersion: 2.
|
6
|
+
TargetRubyVersion: 2.7
|
3
7
|
DisplayCopNames: true
|
8
|
+
NewCops: enable
|
4
9
|
|
5
10
|
# Do not sort gems in Gemfile, since we are grouping them by functionality.
|
6
11
|
Bundler/OrderedGems:
|
@@ -22,20 +27,19 @@ Style/TrailingCommaInHashLiteral:
|
|
22
27
|
Layout/MultilineMethodCallIndentation:
|
23
28
|
EnforcedStyle: indented
|
24
29
|
|
30
|
+
Gemspec/RequiredRubyVersion:
|
31
|
+
Enabled: false
|
32
|
+
|
25
33
|
# Limit method length (default is 10).
|
26
34
|
Metrics/MethodLength:
|
27
35
|
Max: 15
|
28
36
|
|
29
|
-
# Do not require `# frozen_string_literal: true` at the top of every file.
|
30
|
-
FrozenStringLiteralComment:
|
31
|
-
Enabled: false
|
32
|
-
|
33
37
|
# Allow ASCII comments (e.g "…").
|
34
38
|
Style/AsciiComments:
|
35
39
|
Enabled: false
|
36
40
|
|
37
41
|
# Do not comment the class we create, since the name should be self explanatory.
|
38
|
-
Documentation:
|
42
|
+
Style/Documentation:
|
39
43
|
Enabled: false
|
40
44
|
|
41
45
|
# Do not verify the length of the blocks in specs.
|
@@ -43,15 +47,119 @@ Metrics/BlockLength:
|
|
43
47
|
Exclude:
|
44
48
|
- spec/**/*
|
45
49
|
|
46
|
-
# Allow indenting multiline chained operations.
|
47
|
-
Layout/MultilineMethodCallIndentation:
|
48
|
-
EnforcedStyle: indented
|
49
|
-
|
50
50
|
# Prefer `== 0`, `< 0`, `> 0` to `zero?`, `negative?` or `positive?`,
|
51
51
|
# since they don't exist before Ruby 2.3 or Rails 5 and can be ambiguous.
|
52
52
|
Style/NumericPredicate:
|
53
53
|
EnforcedStyle: comparison
|
54
54
|
|
55
|
-
# Allow
|
56
|
-
|
57
|
-
|
55
|
+
# Allow more expectations per example (default 1).
|
56
|
+
RSpec/MultipleExpectations:
|
57
|
+
Max: 5
|
58
|
+
|
59
|
+
# Allow more group nesting (default 3)
|
60
|
+
RSpec/NestedGroups:
|
61
|
+
Max: 5
|
62
|
+
|
63
|
+
# Allow longer examples (default 5)
|
64
|
+
RSpec/ExampleLength:
|
65
|
+
Max: 8
|
66
|
+
|
67
|
+
Layout/EmptyLinesAroundAttributeAccessor:
|
68
|
+
Enabled: true
|
69
|
+
|
70
|
+
Layout/SpaceAroundMethodCallOperator:
|
71
|
+
Enabled: true
|
72
|
+
|
73
|
+
Lint/DeprecatedOpenSSLConstant:
|
74
|
+
Enabled: true
|
75
|
+
|
76
|
+
Lint/MixedRegexpCaptureTypes:
|
77
|
+
Enabled: true
|
78
|
+
|
79
|
+
Lint/RaiseException:
|
80
|
+
Enabled: true
|
81
|
+
|
82
|
+
Lint/StructNewOverride:
|
83
|
+
Enabled: true
|
84
|
+
|
85
|
+
Style/ExponentialNotation:
|
86
|
+
Enabled: true
|
87
|
+
|
88
|
+
Style/HashEachMethods:
|
89
|
+
Enabled: true
|
90
|
+
|
91
|
+
Style/HashTransformKeys:
|
92
|
+
Enabled: true
|
93
|
+
|
94
|
+
Style/HashTransformValues:
|
95
|
+
Enabled: true
|
96
|
+
|
97
|
+
Style/RedundantRegexpCharacterClass:
|
98
|
+
Enabled: true
|
99
|
+
|
100
|
+
Style/RedundantRegexpEscape:
|
101
|
+
Enabled: true
|
102
|
+
|
103
|
+
Style/SlicingWithRange:
|
104
|
+
Enabled: true
|
105
|
+
|
106
|
+
Gemspec/DateAssignment: # (new in 1.10)
|
107
|
+
Enabled: true
|
108
|
+
Layout/SpaceBeforeBrackets: # (new in 1.7)
|
109
|
+
Enabled: true
|
110
|
+
Lint/AmbiguousAssignment: # (new in 1.7)
|
111
|
+
Enabled: true
|
112
|
+
Lint/DeprecatedConstants: # (new in 1.8)
|
113
|
+
Enabled: true
|
114
|
+
Lint/DuplicateBranch: # (new in 1.3)
|
115
|
+
Enabled: true
|
116
|
+
Lint/DuplicateRegexpCharacterClassElement: # (new in 1.1)
|
117
|
+
Enabled: true
|
118
|
+
Lint/EmptyBlock: # (new in 1.1)
|
119
|
+
Enabled: true
|
120
|
+
Lint/EmptyClass: # (new in 1.3)
|
121
|
+
Enabled: true
|
122
|
+
Lint/LambdaWithoutLiteralBlock: # (new in 1.8)
|
123
|
+
Enabled: true
|
124
|
+
Lint/NoReturnInBeginEndBlocks: # (new in 1.2)
|
125
|
+
Enabled: true
|
126
|
+
Lint/NumberedParameterAssignment: # (new in 1.9)
|
127
|
+
Enabled: true
|
128
|
+
Lint/OrAssignmentToConstant: # (new in 1.9)
|
129
|
+
Enabled: true
|
130
|
+
Lint/RedundantDirGlobSort: # (new in 1.8)
|
131
|
+
Enabled: true
|
132
|
+
Lint/SymbolConversion: # (new in 1.9)
|
133
|
+
Enabled: true
|
134
|
+
Lint/ToEnumArguments: # (new in 1.1)
|
135
|
+
Enabled: true
|
136
|
+
Lint/TripleQuotes: # (new in 1.9)
|
137
|
+
Enabled: true
|
138
|
+
Lint/UnexpectedBlockArity: # (new in 1.5)
|
139
|
+
Enabled: true
|
140
|
+
Lint/UnmodifiedReduceAccumulator: # (new in 1.1)
|
141
|
+
Enabled: true
|
142
|
+
Style/ArgumentsForwarding: # (new in 1.1)
|
143
|
+
Enabled: true
|
144
|
+
Style/CollectionCompact: # (new in 1.2)
|
145
|
+
Enabled: true
|
146
|
+
Style/DocumentDynamicEvalDefinition: # (new in 1.1)
|
147
|
+
Enabled: true
|
148
|
+
Style/EndlessMethod: # (new in 1.8)
|
149
|
+
Enabled: true
|
150
|
+
Style/HashConversion: # (new in 1.10)
|
151
|
+
Enabled: true
|
152
|
+
Style/HashExcept: # (new in 1.7)
|
153
|
+
Enabled: true
|
154
|
+
Style/IfWithBooleanLiteralBranches: # (new in 1.9)
|
155
|
+
Enabled: true
|
156
|
+
Style/NegatedIfElseCondition: # (new in 1.2)
|
157
|
+
Enabled: true
|
158
|
+
Style/NilLambda: # (new in 1.3)
|
159
|
+
Enabled: true
|
160
|
+
Style/RedundantArgument: # (new in 1.4)
|
161
|
+
Enabled: true
|
162
|
+
Style/StringChars: # (new in 1.12)
|
163
|
+
Enabled: true
|
164
|
+
Style/SwapValues: # (new in 1.1)
|
165
|
+
Enabled: true
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.7.5
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
unreleased
|
2
|
+
----------
|
3
|
+
|
4
|
+
v2.0.0
|
5
|
+
------
|
6
|
+
|
7
|
+
Breaking changes:
|
8
|
+
- Drop support for GraphQL legacy schema, please use GraphQL::Ruby's class-based
|
9
|
+
syntax exclusively.
|
10
|
+
|
11
|
+
Feature:
|
12
|
+
- Support Ruby 3.
|
13
|
+
|
14
|
+
v1.2.0
|
15
|
+
------
|
16
|
+
|
17
|
+
Feature:
|
18
|
+
- New GraphAttack::RateLimit extension to be used in GraphQL::Ruby's class-based
|
19
|
+
syntax.
|
20
|
+
|
21
|
+
v1.1.0
|
22
|
+
------
|
23
|
+
|
24
|
+
Feature:
|
25
|
+
- Add `redis_client` option to provide a custom Redis client.
|
26
|
+
|
27
|
+
v1.0.0
|
28
|
+
------
|
29
|
+
|
30
|
+
First release!
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -6,21 +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
|
-
QueryType
|
13
|
-
|
14
|
-
|
15
|
-
field :someExpensiveField do
|
16
|
-
rate_limit threshold: 15, interval: 60
|
17
|
-
|
18
|
-
# …
|
12
|
+
class QueryType < GraphQL::Schema::Object
|
13
|
+
field :some_expensive_field, String, null: false do
|
14
|
+
extension GraphAttack::RateLimit, threshold: 15, interval: 60
|
19
15
|
end
|
16
|
+
|
17
|
+
# …
|
20
18
|
end
|
21
19
|
```
|
22
20
|
|
23
|
-
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.
|
24
22
|
|
25
23
|
## Requirements
|
26
24
|
|
@@ -29,11 +27,11 @@ of [Redis](https://redis.io/).
|
|
29
27
|
|
30
28
|
## Installation
|
31
29
|
|
32
|
-
Add these lines to your application
|
30
|
+
Add these lines to your application’s `Gemfile`:
|
33
31
|
|
34
32
|
```ruby
|
35
33
|
# GraphQL analyser for blocking & throttling by IP.
|
36
|
-
gem
|
34
|
+
gem "graph_attack"
|
37
35
|
```
|
38
36
|
|
39
37
|
And then execute:
|
@@ -42,18 +40,8 @@ And then execute:
|
|
42
40
|
$ bundle
|
43
41
|
```
|
44
42
|
|
45
|
-
|
46
|
-
|
47
|
-
```rb
|
48
|
-
ApplicationSchema = GraphQL::Schema.define do
|
49
|
-
query_analyzer GraphAttack::RateLimiter.new
|
50
|
-
|
51
|
-
# …
|
52
|
-
end
|
53
|
-
```
|
54
|
-
|
55
|
-
Finally, make sure you add the current user's IP address as `ip:` to the
|
56
|
-
GraphQL context:
|
43
|
+
Finally, make sure you add the current user’s IP address as `ip:` to the
|
44
|
+
GraphQL context. E.g.:
|
57
45
|
|
58
46
|
```rb
|
59
47
|
class GraphqlController < ApplicationController
|
@@ -70,6 +58,19 @@ class GraphqlController < ApplicationController
|
|
70
58
|
end
|
71
59
|
```
|
72
60
|
|
61
|
+
## Configuration
|
62
|
+
|
63
|
+
Use a custom Redis client instead of the default:
|
64
|
+
|
65
|
+
```rb
|
66
|
+
field :some_expensive_field, String, null: false do
|
67
|
+
extension GraphAttack::RateLimit,
|
68
|
+
threshold: 15,
|
69
|
+
interval: 60,
|
70
|
+
redis_client: Redis.new(url: "…")
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
73
74
|
## Development
|
74
75
|
|
75
76
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
@@ -83,9 +84,9 @@ see the tags on this repository.
|
|
83
84
|
|
84
85
|
## Releasing
|
85
86
|
|
86
|
-
To release a new version, update the version number in `version.rb`,
|
87
|
-
run `
|
88
|
-
push git commits and tags, and push the
|
87
|
+
To release a new version, update the version number in `version.rb`, commit,
|
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
|
89
90
|
[rubygems.org](https://rubygems.org).
|
90
91
|
|
91
92
|
## Contributing
|
@@ -99,12 +100,12 @@ the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
99
100
|
|
100
101
|
Everyone interacting in the GraphAttack project’s codebases, issue trackers,
|
101
102
|
chat rooms and mailing lists is expected to follow the
|
102
|
-
[code of conduct](https://github.com/sunny/graph_attack/blob/
|
103
|
+
[code of conduct](https://github.com/sunny/graph_attack/blob/main/CODE_OF_CONDUCT.md).
|
103
104
|
|
104
105
|
## License
|
105
106
|
|
106
107
|
This project is licensed under the MIT License - see the
|
107
|
-
[LICENSE.md](https://github.com/sunny/graph_attack/blob/
|
108
|
+
[LICENSE.md](https://github.com/sunny/graph_attack/blob/main/LICENSE.md)
|
108
109
|
file for details.
|
109
110
|
|
110
111
|
## Authors
|
data/Rakefile
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Bundler
|
2
4
|
require 'bundler/gem_tasks'
|
3
|
-
require 'rspec/core/rake_task'
|
4
5
|
|
5
|
-
#
|
6
|
+
# RSpec
|
6
7
|
require 'rspec/core/rake_task'
|
7
8
|
RSpec::Core::RakeTask.new(:spec)
|
8
9
|
|
9
|
-
task default: :spec
|
10
10
|
# Rubocop
|
11
11
|
require 'rubocop/rake_task'
|
12
12
|
RuboCop::RakeTask.new(:rubocop)
|
data/bin/console
CHANGED
data/bin/rake
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rake' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'pathname'
|
12
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path('bundle', __dir__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'rubygems'
|
27
|
+
require 'bundler/setup'
|
28
|
+
|
29
|
+
load Gem.bin_path('rake', 'rake')
|
data/bin/rubocop
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rubocop' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'pathname'
|
12
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
13
|
+
Pathname.new(__FILE__).realpath)
|
14
|
+
|
15
|
+
bundle_binstub = File.expand_path('bundle', __dir__)
|
16
|
+
|
17
|
+
if File.file?(bundle_binstub)
|
18
|
+
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
|
19
|
+
load(bundle_binstub)
|
20
|
+
else
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'rubygems'
|
27
|
+
require 'bundler/setup'
|
28
|
+
|
29
|
+
load Gem.bin_path('rubocop', 'rubocop')
|
data/graph_attack.gemspec
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
lib = File.expand_path('lib', __dir__)
|
2
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
5
|
require 'graph_attack/version'
|
@@ -12,6 +14,8 @@ Gem::Specification.new do |spec|
|
|
12
14
|
spec.description = 'GraphQL analyser for blocking & throttling'
|
13
15
|
spec.homepage = 'https://github.com/sunny/graph_attack'
|
14
16
|
|
17
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
18
|
+
|
15
19
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
20
|
f.match(%r{^(test|spec|features)/})
|
17
21
|
end
|
@@ -19,6 +23,8 @@ Gem::Specification.new do |spec|
|
|
19
23
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
24
|
spec.require_paths = ['lib']
|
21
25
|
|
26
|
+
spec.required_ruby_version = ['>= 2.5.7', '< 3.2']
|
27
|
+
|
22
28
|
# This gem is an analyser for the GraphQL ruby gem.
|
23
29
|
spec.add_dependency 'graphql', '>= 1.7.9'
|
24
30
|
|
@@ -26,10 +32,10 @@ Gem::Specification.new do |spec|
|
|
26
32
|
spec.add_dependency 'ratelimit', '>= 1.0.3'
|
27
33
|
|
28
34
|
# Loads local dependencies.
|
29
|
-
spec.add_development_dependency 'bundler', '~>
|
35
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
30
36
|
|
31
37
|
# Development tasks runner.
|
32
|
-
spec.add_development_dependency 'rake', '~>
|
38
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
33
39
|
|
34
40
|
# Testing framework.
|
35
41
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
@@ -38,5 +44,11 @@ Gem::Specification.new do |spec|
|
|
38
44
|
spec.add_development_dependency 'rspec_junit_formatter', '~> 0.3'
|
39
45
|
|
40
46
|
# Ruby code linter.
|
41
|
-
spec.add_development_dependency 'rubocop', '~>
|
47
|
+
spec.add_development_dependency 'rubocop', '~> 1.1'
|
48
|
+
|
49
|
+
# RSpec extension for RuboCop.
|
50
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 2.2'
|
51
|
+
|
52
|
+
# Rake extension for RuboCop
|
53
|
+
spec.add_development_dependency 'rubocop-rake'
|
42
54
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphAttack
|
4
|
+
class RateLimit < GraphQL::Schema::FieldExtension
|
5
|
+
def resolve(object:, arguments:, **_rest)
|
6
|
+
ip = object.context[:ip]
|
7
|
+
raise GraphAttack::Error, 'Missing :ip value on the GraphQL context' unless ip
|
8
|
+
|
9
|
+
return RateLimited.new('Query rate limit exceeded') if calls_exceeded_on_query?(ip)
|
10
|
+
|
11
|
+
yield(object, arguments)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def key
|
17
|
+
"graphql-query-#{field.name}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def calls_exceeded_on_query?(ip)
|
21
|
+
rate_limit = Ratelimit.new(ip, redis: redis_client)
|
22
|
+
rate_limit.add(key)
|
23
|
+
rate_limit.exceeded?(
|
24
|
+
key,
|
25
|
+
threshold: threshold,
|
26
|
+
interval: interval,
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def threshold
|
31
|
+
options[:threshold] ||
|
32
|
+
raise(
|
33
|
+
GraphAttack::Error,
|
34
|
+
'Missing "threshold:" option on the GraphAttack::RateLimit extension',
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def interval
|
39
|
+
options[:interval] ||
|
40
|
+
raise(
|
41
|
+
GraphAttack::Error,
|
42
|
+
'Missing "interval:" option on the GraphAttack::RateLimit extension',
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def redis_client
|
47
|
+
options[:redis_client] || Redis.current
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/graph_attack/version.rb
CHANGED
data/lib/graph_attack.rb
CHANGED
@@ -1,7 +1,12 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'ratelimit'
|
4
|
+
require 'graphql'
|
3
5
|
require 'graphql/tracing'
|
4
6
|
|
5
7
|
require 'graph_attack/version'
|
6
|
-
|
7
|
-
|
8
|
+
|
9
|
+
# Class-based schema
|
10
|
+
require 'graph_attack/error'
|
11
|
+
require 'graph_attack/rate_limit'
|
12
|
+
require 'graph_attack/rate_limited'
|
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
|
@@ -45,28 +45,28 @@ dependencies:
|
|
45
45
|
requirements:
|
46
46
|
- - "~>"
|
47
47
|
- !ruby/object:Gem::Version
|
48
|
-
version: '
|
48
|
+
version: '2.0'
|
49
49
|
type: :development
|
50
50
|
prerelease: false
|
51
51
|
version_requirements: !ruby/object:Gem::Requirement
|
52
52
|
requirements:
|
53
53
|
- - "~>"
|
54
54
|
- !ruby/object:Gem::Version
|
55
|
-
version: '
|
55
|
+
version: '2.0'
|
56
56
|
- !ruby/object:Gem::Dependency
|
57
57
|
name: rake
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
60
|
- - "~>"
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: '
|
62
|
+
version: '13.0'
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
67
|
- - "~>"
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: '
|
69
|
+
version: '13.0'
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
71
|
name: rspec
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
@@ -101,14 +101,42 @@ dependencies:
|
|
101
101
|
requirements:
|
102
102
|
- - "~>"
|
103
103
|
- !ruby/object:Gem::Version
|
104
|
-
version: '
|
104
|
+
version: '1.1'
|
105
105
|
type: :development
|
106
106
|
prerelease: false
|
107
107
|
version_requirements: !ruby/object:Gem::Requirement
|
108
108
|
requirements:
|
109
109
|
- - "~>"
|
110
110
|
- !ruby/object:Gem::Version
|
111
|
-
version: '
|
111
|
+
version: '1.1'
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: rubocop-rspec
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - "~>"
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '2.2'
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - "~>"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '2.2'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: rubocop-rake
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
type: :development
|
134
|
+
prerelease: false
|
135
|
+
version_requirements: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
112
140
|
description: GraphQL analyser for blocking & throttling
|
113
141
|
email:
|
114
142
|
- fanny@ynote.hk
|
@@ -118,25 +146,32 @@ extensions: []
|
|
118
146
|
extra_rdoc_files: []
|
119
147
|
files:
|
120
148
|
- ".circleci/config.yml"
|
149
|
+
- ".github/dependabot.yml"
|
121
150
|
- ".gitignore"
|
122
151
|
- ".rspec"
|
123
152
|
- ".rubocop.yml"
|
153
|
+
- ".ruby-version"
|
124
154
|
- ".travis.yml"
|
155
|
+
- CHANGELOG.md
|
125
156
|
- CODE_OF_CONDUCT.md
|
126
157
|
- Gemfile
|
127
158
|
- LICENSE.md
|
128
159
|
- README.md
|
129
160
|
- Rakefile
|
130
161
|
- bin/console
|
162
|
+
- bin/rake
|
163
|
+
- bin/rubocop
|
131
164
|
- bin/setup
|
132
165
|
- graph_attack.gemspec
|
133
166
|
- lib/graph_attack.rb
|
134
|
-
- lib/graph_attack/
|
135
|
-
- lib/graph_attack/
|
167
|
+
- lib/graph_attack/error.rb
|
168
|
+
- lib/graph_attack/rate_limit.rb
|
169
|
+
- lib/graph_attack/rate_limited.rb
|
136
170
|
- lib/graph_attack/version.rb
|
137
171
|
homepage: https://github.com/sunny/graph_attack
|
138
172
|
licenses: []
|
139
|
-
metadata:
|
173
|
+
metadata:
|
174
|
+
rubygems_mfa_required: 'true'
|
140
175
|
post_install_message:
|
141
176
|
rdoc_options: []
|
142
177
|
require_paths:
|
@@ -145,15 +180,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
145
180
|
requirements:
|
146
181
|
- - ">="
|
147
182
|
- !ruby/object:Gem::Version
|
148
|
-
version:
|
183
|
+
version: 2.5.7
|
184
|
+
- - "<"
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '3.2'
|
149
187
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
150
188
|
requirements:
|
151
189
|
- - ">="
|
152
190
|
- !ruby/object:Gem::Version
|
153
191
|
version: '0'
|
154
192
|
requirements: []
|
155
|
-
|
156
|
-
rubygems_version: 2.6.11
|
193
|
+
rubygems_version: 3.1.6
|
157
194
|
signing_key:
|
158
195
|
specification_version: 4
|
159
196
|
summary: GraphQL analyser for blocking & throttling
|
@@ -1,89 +0,0 @@
|
|
1
|
-
module GraphAttack
|
2
|
-
# Query analyser you can add to your GraphQL schema to limit calls by IP.
|
3
|
-
#
|
4
|
-
# ApplicationSchema = GraphQL::Schema.define do
|
5
|
-
# query_analyzer GraphAttack::RateLimiter.new
|
6
|
-
# end
|
7
|
-
#
|
8
|
-
class RateLimiter
|
9
|
-
class Error < StandardError; end
|
10
|
-
class RateLimited < GraphQL::AnalysisError; end
|
11
|
-
|
12
|
-
def initial_value(query)
|
13
|
-
{
|
14
|
-
ip: query.context[:ip],
|
15
|
-
query_rate_limits: [],
|
16
|
-
}
|
17
|
-
end
|
18
|
-
|
19
|
-
def call(memo, visit_type, irep_node)
|
20
|
-
if rate_limited_node?(visit_type, irep_node)
|
21
|
-
data = rate_limit_data(irep_node)
|
22
|
-
|
23
|
-
memo[:query_rate_limits].push(data)
|
24
|
-
|
25
|
-
increment_rate_limit(memo[:ip], data[:key])
|
26
|
-
end
|
27
|
-
|
28
|
-
memo
|
29
|
-
end
|
30
|
-
|
31
|
-
def final_value(memo)
|
32
|
-
handle_exceeded_calls_on_queries(memo)
|
33
|
-
end
|
34
|
-
|
35
|
-
private
|
36
|
-
|
37
|
-
def increment_rate_limit(ip, key)
|
38
|
-
raise Error, 'Missing :ip value on the GraphQL context' unless ip
|
39
|
-
|
40
|
-
rate_limit(ip).add(key)
|
41
|
-
end
|
42
|
-
|
43
|
-
def rate_limit_data(node)
|
44
|
-
data = node.definition.metadata[:rate_limit]
|
45
|
-
|
46
|
-
data.merge(
|
47
|
-
key: "graphql-query-#{node.name}",
|
48
|
-
query_name: node.name,
|
49
|
-
)
|
50
|
-
end
|
51
|
-
|
52
|
-
def handle_exceeded_calls_on_queries(memo)
|
53
|
-
rate_limited_queries = memo[:query_rate_limits].map do |limit_data|
|
54
|
-
next unless calls_exceeded_on_query?(memo[:ip], limit_data)
|
55
|
-
|
56
|
-
limit_data[:query_name]
|
57
|
-
end.compact
|
58
|
-
|
59
|
-
return unless rate_limited_queries.any?
|
60
|
-
|
61
|
-
queries = rate_limited_queries.join(', ')
|
62
|
-
RateLimited.new("Query rate limit exceeded on #{queries}")
|
63
|
-
end
|
64
|
-
|
65
|
-
def calls_exceeded_on_query?(ip, query_limit_data)
|
66
|
-
rate_limit(ip).exceeded?(
|
67
|
-
query_limit_data[:key],
|
68
|
-
threshold: query_limit_data[:threshold],
|
69
|
-
interval: query_limit_data[:interval],
|
70
|
-
)
|
71
|
-
end
|
72
|
-
|
73
|
-
def rate_limit(ip)
|
74
|
-
@rate_limit ||= {}
|
75
|
-
@rate_limit[ip] ||= Ratelimit.new(ip)
|
76
|
-
end
|
77
|
-
|
78
|
-
def rate_limited_node?(visit_type, node)
|
79
|
-
query_field_node?(node) &&
|
80
|
-
visit_type == :enter &&
|
81
|
-
node.definition.metadata[:rate_limit]
|
82
|
-
end
|
83
|
-
|
84
|
-
def query_field_node?(node)
|
85
|
-
node.owner_type.name == 'Query' &&
|
86
|
-
node.ast_node.is_a?(GraphQL::Language::Nodes::Field)
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|