graph_attack 1.1.0 → 2.1.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 +66 -12
- data/.ruby-version +1 -0
- data/.travis.yml +4 -2
- data/CHANGELOG.md +23 -0
- data/Gemfile +2 -0
- data/README.md +49 -38
- 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 +60 -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 +50 -14
- data/lib/graph_attack/metadata.rb +0 -4
- data/lib/graph_attack/rate_limiter.rb +0 -95
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: de17498105231eb4dd5b5135cf8e7683680cafc409a66acf6fddbaa6f6735b36
|
4
|
+
data.tar.gz: 60308e8ccff6fb80b4b5013975f9ea4903cb037b16997030635ace4557211f17
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41706b8ea7768bf2d3220c6803f91c29a20640b177c8c443cf64a3462310dd939d4dfa92ffd2fd9f874bd6f256e50031ffbf893f5f4a58846fd7299191e12169
|
7
|
+
data.tar.gz: 873abb6b86cb16f3575cff878cd2be3d0c47723b472b2949a6f4c0f9c6164996fa3b72142ce3e74a71952c5da89eac400a346a43607f174fa2e28862c5dd8d57
|
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,26 @@ 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
|
-
#
|
30
|
-
|
31
|
-
|
37
|
+
# Limit line length.
|
38
|
+
Layout/LineLength:
|
39
|
+
Max: 80
|
40
|
+
Exclude:
|
41
|
+
- bin/rake
|
42
|
+
- bin/rubocop
|
32
43
|
|
33
44
|
# Allow ASCII comments (e.g "…").
|
34
45
|
Style/AsciiComments:
|
35
46
|
Enabled: false
|
36
47
|
|
37
48
|
# Do not comment the class we create, since the name should be self explanatory.
|
38
|
-
Documentation:
|
49
|
+
Style/Documentation:
|
39
50
|
Enabled: false
|
40
51
|
|
41
52
|
# Do not verify the length of the blocks in specs.
|
@@ -43,15 +54,58 @@ Metrics/BlockLength:
|
|
43
54
|
Exclude:
|
44
55
|
- spec/**/*
|
45
56
|
|
46
|
-
# Allow indenting multiline chained operations.
|
47
|
-
Layout/MultilineMethodCallIndentation:
|
48
|
-
EnforcedStyle: indented
|
49
|
-
|
50
57
|
# Prefer `== 0`, `< 0`, `> 0` to `zero?`, `negative?` or `positive?`,
|
51
58
|
# since they don't exist before Ruby 2.3 or Rails 5 and can be ambiguous.
|
52
59
|
Style/NumericPredicate:
|
53
60
|
EnforcedStyle: comparison
|
54
61
|
|
55
|
-
# Allow
|
56
|
-
|
57
|
-
|
62
|
+
# Allow more expectations per example (default 1).
|
63
|
+
RSpec/MultipleExpectations:
|
64
|
+
Max: 5
|
65
|
+
|
66
|
+
# Allow more group nesting (default 3)
|
67
|
+
RSpec/NestedGroups:
|
68
|
+
Max: 5
|
69
|
+
|
70
|
+
# Allow longer examples (default 5)
|
71
|
+
RSpec/ExampleLength:
|
72
|
+
Max: 8
|
73
|
+
|
74
|
+
Layout/EmptyLinesAroundAttributeAccessor:
|
75
|
+
Enabled: true
|
76
|
+
|
77
|
+
Layout/SpaceAroundMethodCallOperator:
|
78
|
+
Enabled: true
|
79
|
+
|
80
|
+
Lint/DeprecatedOpenSSLConstant:
|
81
|
+
Enabled: true
|
82
|
+
|
83
|
+
Lint/MixedRegexpCaptureTypes:
|
84
|
+
Enabled: true
|
85
|
+
|
86
|
+
Lint/RaiseException:
|
87
|
+
Enabled: true
|
88
|
+
|
89
|
+
Lint/StructNewOverride:
|
90
|
+
Enabled: true
|
91
|
+
|
92
|
+
Style/ExponentialNotation:
|
93
|
+
Enabled: true
|
94
|
+
|
95
|
+
Style/HashEachMethods:
|
96
|
+
Enabled: true
|
97
|
+
|
98
|
+
Style/HashTransformKeys:
|
99
|
+
Enabled: true
|
100
|
+
|
101
|
+
Style/HashTransformValues:
|
102
|
+
Enabled: true
|
103
|
+
|
104
|
+
Style/RedundantRegexpCharacterClass:
|
105
|
+
Enabled: true
|
106
|
+
|
107
|
+
Style/RedundantRegexpEscape:
|
108
|
+
Enabled: true
|
109
|
+
|
110
|
+
Style/SlicingWithRange:
|
111
|
+
Enabled: true
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.7.5
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,29 @@
|
|
1
1
|
unreleased
|
2
2
|
----------
|
3
3
|
|
4
|
+
v2.1.0
|
5
|
+
------
|
6
|
+
|
7
|
+
Feature:
|
8
|
+
- Add support to custom rate limited context key with the `on:` option.
|
9
|
+
|
10
|
+
v2.0.0
|
11
|
+
------
|
12
|
+
|
13
|
+
Breaking changes:
|
14
|
+
- Drop support for GraphQL legacy schema, please use GraphQL::Ruby's class-based
|
15
|
+
syntax exclusively.
|
16
|
+
|
17
|
+
Feature:
|
18
|
+
- Support Ruby 3.
|
19
|
+
|
20
|
+
v1.2.0
|
21
|
+
------
|
22
|
+
|
23
|
+
Feature:
|
24
|
+
- New GraphAttack::RateLimit extension to be used in GraphQL::Ruby's class-based
|
25
|
+
syntax.
|
26
|
+
|
4
27
|
v1.1.0
|
5
28
|
------
|
6
29
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,26 +1,24 @@
|
|
1
1
|
# GraphAttack
|
2
2
|
|
3
|
-
[](https://app.travis-ci.com/sunny/graph_attack)
|
4
4
|
|
5
5
|
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
|
@@ -72,19 +60,34 @@ end
|
|
72
60
|
|
73
61
|
## Configuration
|
74
62
|
|
75
|
-
|
63
|
+
### Custom context key
|
64
|
+
|
65
|
+
If you want to throttle using a different value than the IP address, you can
|
66
|
+
choose which context key you want to use with the `on` option. E.g.:
|
67
|
+
|
68
|
+
```rb
|
69
|
+
extension GraphAttack::RateLimit,
|
70
|
+
threshold: 15,
|
71
|
+
interval: 60,
|
72
|
+
on: :client_id
|
73
|
+
```
|
74
|
+
|
75
|
+
### Custom Redis client
|
76
|
+
|
77
|
+
Use a custom Redis client instead of the default with the `redis_client` option:
|
76
78
|
|
77
79
|
```rb
|
78
|
-
|
79
|
-
|
80
|
-
|
80
|
+
extension GraphAttack::RateLimit,
|
81
|
+
threshold: 15,
|
82
|
+
interval: 60,
|
83
|
+
redis_client: Redis.new(url: "…")
|
81
84
|
```
|
82
85
|
|
83
86
|
## Development
|
84
87
|
|
85
88
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
86
|
-
`rake` to run the tests and the linter. You can also run `bin/console` for
|
87
|
-
interactive prompt that will allow you to experiment.
|
89
|
+
`bin/rake` to run the tests and the linter. You can also run `bin/console` for
|
90
|
+
an interactive prompt that will allow you to experiment.
|
88
91
|
|
89
92
|
## Versionning
|
90
93
|
|
@@ -93,10 +96,18 @@ see the tags on this repository.
|
|
93
96
|
|
94
97
|
## Releasing
|
95
98
|
|
96
|
-
To release a new version, update the version number in `version.rb
|
97
|
-
|
98
|
-
|
99
|
-
|
99
|
+
To release a new version, update the version number in `version.rb` and in the
|
100
|
+
`CHANGELOG.md`. Update the `README.md` if there are missing segments, make sure
|
101
|
+
tests and linting are pristine by calling `bundle && bin/rake`, then create a
|
102
|
+
commit for this version, for example with:
|
103
|
+
|
104
|
+
```sh
|
105
|
+
git add .
|
106
|
+
git commit -m v`ruby -rbundler/setup -rgraph_attack/version -e "puts GraphAttack::VERSION"`
|
107
|
+
```
|
108
|
+
|
109
|
+
You can then run `bin/rake release`, which will assign a git tag, push using
|
110
|
+
git, and push the gem to [rubygems.org](https://rubygems.org).
|
100
111
|
|
101
112
|
## Contributing
|
102
113
|
|
@@ -109,18 +120,18 @@ the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
109
120
|
|
110
121
|
Everyone interacting in the GraphAttack project’s codebases, issue trackers,
|
111
122
|
chat rooms and mailing lists is expected to follow the
|
112
|
-
[code of conduct](https://github.com/sunny/graph_attack/blob/
|
123
|
+
[code of conduct](https://github.com/sunny/graph_attack/blob/main/CODE_OF_CONDUCT.md).
|
113
124
|
|
114
125
|
## License
|
115
126
|
|
116
127
|
This project is licensed under the MIT License - see the
|
117
|
-
[LICENSE.md](https://github.com/sunny/graph_attack/blob/
|
128
|
+
[LICENSE.md](https://github.com/sunny/graph_attack/blob/main/LICENSE.md)
|
118
129
|
file for details.
|
119
130
|
|
120
131
|
## Authors
|
121
132
|
|
122
|
-
-
|
123
|
-
-
|
133
|
+
- [Fanny Cheung](https://github.com/Ynote) — [ynote.hk](https://ynote.hk)
|
134
|
+
- [Sunny Ripert](https://github.com/sunny) — [sunfox.org](https://sunfox.org)
|
124
135
|
|
125
136
|
## Acknowledgments
|
126
137
|
|
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', '~> 0
|
47
|
+
spec.add_development_dependency 'rubocop', '~> 1.33.0'
|
48
|
+
|
49
|
+
# RSpec extension for RuboCop.
|
50
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 2.12.1'
|
51
|
+
|
52
|
+
# Rake extension for RuboCop
|
53
|
+
spec.add_development_dependency 'rubocop-rake', '~> 0.6.0'
|
42
54
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphAttack
|
4
|
+
class RateLimit < GraphQL::Schema::FieldExtension
|
5
|
+
def resolve(object:, arguments:, **_rest)
|
6
|
+
rate_limited_field = object.context[rate_limited_key]
|
7
|
+
unless rate_limited_field
|
8
|
+
raise GraphAttack::Error,
|
9
|
+
"Missing :#{rate_limited_key} value on the GraphQL context"
|
10
|
+
end
|
11
|
+
|
12
|
+
if calls_exceeded_on_query?(rate_limited_field)
|
13
|
+
return RateLimited.new('Query rate limit exceeded')
|
14
|
+
end
|
15
|
+
|
16
|
+
yield(object, arguments)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def key
|
22
|
+
on = "-#{options[:on]}" if options[:on]
|
23
|
+
"graphql-query-#{field.name}#{on}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def calls_exceeded_on_query?(rate_limited_field)
|
27
|
+
rate_limit = Ratelimit.new(rate_limited_field, redis: redis_client)
|
28
|
+
rate_limit.add(key)
|
29
|
+
rate_limit.exceeded?(
|
30
|
+
key,
|
31
|
+
threshold: threshold,
|
32
|
+
interval: interval,
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def threshold
|
37
|
+
options[:threshold] ||
|
38
|
+
raise(
|
39
|
+
GraphAttack::Error,
|
40
|
+
'Missing "threshold:" option on the GraphAttack::RateLimit extension',
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def interval
|
45
|
+
options[:interval] ||
|
46
|
+
raise(
|
47
|
+
GraphAttack::Error,
|
48
|
+
'Missing "interval:" option on the GraphAttack::RateLimit extension',
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def redis_client
|
53
|
+
options[:redis_client] || Redis.current
|
54
|
+
end
|
55
|
+
|
56
|
+
def rate_limited_key
|
57
|
+
options[:on] || :ip
|
58
|
+
end
|
59
|
+
end
|
60
|
+
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.1.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-08-07 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.33.0
|
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.33.0
|
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.12.1
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - "~>"
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 2.12.1
|
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.6.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.6.0
|
112
140
|
description: GraphQL analyser for blocking & throttling
|
113
141
|
email:
|
114
142
|
- fanny@ynote.hk
|
@@ -118,9 +146,11 @@ 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"
|
125
155
|
- CHANGELOG.md
|
126
156
|
- CODE_OF_CONDUCT.md
|
@@ -129,15 +159,19 @@ files:
|
|
129
159
|
- README.md
|
130
160
|
- Rakefile
|
131
161
|
- bin/console
|
162
|
+
- bin/rake
|
163
|
+
- bin/rubocop
|
132
164
|
- bin/setup
|
133
165
|
- graph_attack.gemspec
|
134
166
|
- lib/graph_attack.rb
|
135
|
-
- lib/graph_attack/
|
136
|
-
- lib/graph_attack/
|
167
|
+
- lib/graph_attack/error.rb
|
168
|
+
- lib/graph_attack/rate_limit.rb
|
169
|
+
- lib/graph_attack/rate_limited.rb
|
137
170
|
- lib/graph_attack/version.rb
|
138
171
|
homepage: https://github.com/sunny/graph_attack
|
139
172
|
licenses: []
|
140
|
-
metadata:
|
173
|
+
metadata:
|
174
|
+
rubygems_mfa_required: 'true'
|
141
175
|
post_install_message:
|
142
176
|
rdoc_options: []
|
143
177
|
require_paths:
|
@@ -146,15 +180,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
146
180
|
requirements:
|
147
181
|
- - ">="
|
148
182
|
- !ruby/object:Gem::Version
|
149
|
-
version:
|
183
|
+
version: 2.5.7
|
184
|
+
- - "<"
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
version: '3.2'
|
150
187
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
188
|
requirements:
|
152
189
|
- - ">="
|
153
190
|
- !ruby/object:Gem::Version
|
154
191
|
version: '0'
|
155
192
|
requirements: []
|
156
|
-
|
157
|
-
rubygems_version: 2.6.11
|
193
|
+
rubygems_version: 3.1.6
|
158
194
|
signing_key:
|
159
195
|
specification_version: 4
|
160
196
|
summary: GraphQL analyser for blocking & throttling
|
@@ -1,95 +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 initialize(redis_client: Redis.new)
|
13
|
-
@redis_client = redis_client
|
14
|
-
end
|
15
|
-
|
16
|
-
def initial_value(query)
|
17
|
-
{
|
18
|
-
ip: query.context[:ip],
|
19
|
-
query_rate_limits: [],
|
20
|
-
}
|
21
|
-
end
|
22
|
-
|
23
|
-
def call(memo, visit_type, irep_node)
|
24
|
-
if rate_limited_node?(visit_type, irep_node)
|
25
|
-
data = rate_limit_data(irep_node)
|
26
|
-
|
27
|
-
memo[:query_rate_limits].push(data)
|
28
|
-
|
29
|
-
increment_rate_limit(memo[:ip], data[:key])
|
30
|
-
end
|
31
|
-
|
32
|
-
memo
|
33
|
-
end
|
34
|
-
|
35
|
-
def final_value(memo)
|
36
|
-
handle_exceeded_calls_on_queries(memo)
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
attr_reader :redis_client
|
42
|
-
|
43
|
-
def increment_rate_limit(ip, key)
|
44
|
-
raise Error, 'Missing :ip value on the GraphQL context' unless ip
|
45
|
-
|
46
|
-
rate_limit(ip).add(key)
|
47
|
-
end
|
48
|
-
|
49
|
-
def rate_limit_data(node)
|
50
|
-
data = node.definition.metadata[:rate_limit]
|
51
|
-
|
52
|
-
data.merge(
|
53
|
-
key: "graphql-query-#{node.name}",
|
54
|
-
query_name: node.name,
|
55
|
-
)
|
56
|
-
end
|
57
|
-
|
58
|
-
def handle_exceeded_calls_on_queries(memo)
|
59
|
-
rate_limited_queries = memo[:query_rate_limits].map do |limit_data|
|
60
|
-
next unless calls_exceeded_on_query?(memo[:ip], limit_data)
|
61
|
-
|
62
|
-
limit_data[:query_name]
|
63
|
-
end.compact
|
64
|
-
|
65
|
-
return unless rate_limited_queries.any?
|
66
|
-
|
67
|
-
queries = rate_limited_queries.join(', ')
|
68
|
-
RateLimited.new("Query rate limit exceeded on #{queries}")
|
69
|
-
end
|
70
|
-
|
71
|
-
def calls_exceeded_on_query?(ip, query_limit_data)
|
72
|
-
rate_limit(ip).exceeded?(
|
73
|
-
query_limit_data[:key],
|
74
|
-
threshold: query_limit_data[:threshold],
|
75
|
-
interval: query_limit_data[:interval],
|
76
|
-
)
|
77
|
-
end
|
78
|
-
|
79
|
-
def rate_limit(ip)
|
80
|
-
@rate_limit ||= {}
|
81
|
-
@rate_limit[ip] ||= Ratelimit.new(ip, redis: redis_client)
|
82
|
-
end
|
83
|
-
|
84
|
-
def rate_limited_node?(visit_type, node)
|
85
|
-
query_field_node?(node) &&
|
86
|
-
visit_type == :enter &&
|
87
|
-
node.definition.metadata[:rate_limit]
|
88
|
-
end
|
89
|
-
|
90
|
-
def query_field_node?(node)
|
91
|
-
node.owner_type.name == 'Query' &&
|
92
|
-
node.ast_node.is_a?(GraphQL::Language::Nodes::Field)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|