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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 86f3e5cffd6d44474777d8922c3c4082d21143dd
4
- data.tar.gz: 2a0ad09a08d65b22c3b97752b1ce0428cc2040e9
2
+ SHA256:
3
+ metadata.gz: 71a5e6c0ce41ca59713a49108f5ebcc3a579aefcd1e95273c495a5ae1e4fd6f6
4
+ data.tar.gz: 5430e07ebf58ac5b9addbe8b397f6fd832fa56091112136f8ce972cd2e1fe0aa
5
5
  SHA512:
6
- metadata.gz: 830ea8dbf7fad402e69a27bc1e05189af0488fb241580e12a65bb69eca19591552411f4868cd657fd656d989a868686564c5791b0f250bded67e26ae17389d92
7
- data.tar.gz: b7566c50d48645f92ad17bb2f5210346042c178bc10cbe91ea9d58514161570e364d898275b3dadee215588201372d2c38e78177318c10554b4760e523c91bfd
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.4.1-node-browsers
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
- - v1-dependencies-{{ checksum "graph_attack.gemspec" }}
21
- # fallback to using the latest cache if no exact match is found
22
- - v1-dependencies-
20
+ - v2-dependencies-{{ checksum "graph_attack.gemspec" }}
21
+ - v2-dependencies-
23
22
 
24
- - run:
25
- name: install dependencies
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: v1-dependencies-{{ checksum "graph_attack.gemspec" }}
29
+ key: v2-dependencies-{{ checksum "graph_attack.gemspec" }}
33
30
 
34
31
  # Run tests!
35
32
  - run:
@@ -0,0 +1,9 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: bundler
4
+ directory: "/"
5
+ schedule:
6
+ interval: daily
7
+ time: "04:00"
8
+ open-pull-requests-limit: 3
9
+ rebase-strategy: disabled
data/.rubocop.yml CHANGED
@@ -1,6 +1,11 @@
1
+ require:
2
+ - rubocop-rspec
3
+ - rubocop-rake
4
+
1
5
  AllCops:
2
- TargetRubyVersion: 2.3
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 shorter argument names like `ip`.
56
- Naming/UncommunicativeMethodParamName:
57
- MinNameLength: 2
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
@@ -1,5 +1,7 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.4.1
5
- before_install: gem install bundler -v 1.16.1
4
+ - 2.7.5
5
+ - 3.1.2
6
+ services:
7
+ - redis-server
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
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 = GraphQL::ObjectType.define do
13
- name 'Query'
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's `Gemfile`:
30
+ Add these lines to your applications `Gemfile`:
33
31
 
34
32
  ```ruby
35
33
  # GraphQL analyser for blocking & throttling by IP.
36
- gem 'graph_attack'
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
- Add the query analyser to your schema:
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`, and then
87
- run `bundle exec rake release`, which will create a git tag for the version,
88
- push git commits and tags, and push the `.gem` file to
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/master/CODE_OF_CONDUCT.md).
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/master/LICENSE.md)
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
- # Rspec
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
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bundler/setup'
4
5
  require 'graph_attack'
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', '~> 1.15'
35
+ spec.add_development_dependency 'bundler', '~> 2.0'
30
36
 
31
37
  # Development tasks runner.
32
- spec.add_development_dependency 'rake', '~> 10.0'
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.55'
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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphAttack
4
+ class Error < StandardError; end
5
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphAttack
4
+ class RateLimited < GraphQL::AnalysisError; end
5
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GraphAttack
2
- VERSION = '1.0.0'.freeze
4
+ VERSION = '2.0.0'
3
5
  end
data/lib/graph_attack.rb CHANGED
@@ -1,7 +1,12 @@
1
- require 'graphql'
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
- require 'graph_attack/rate_limiter'
7
- require 'graph_attack/metadata'
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: 1.0.0
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: 2018-09-07 00:00:00.000000000 Z
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: '1.15'
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: '1.15'
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: '10.0'
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: '10.0'
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: '0.55'
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: '0.55'
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/metadata.rb
135
- - lib/graph_attack/rate_limiter.rb
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: '0'
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
- rubyforge_project:
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,4 +0,0 @@
1
- # Add custom field metadata
2
- GraphQL::Field.accepts_definitions(
3
- rate_limit: GraphQL::Define.assign_metadata_key(:rate_limit),
4
- )
@@ -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