graph_attack 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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