graph_attack 1.1.0 → 2.1.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: a15c0b11d8e25c73943da6bf70907df378deb42c
4
- data.tar.gz: 25e5f9f166d1bb010eee56c7e595b126fe05b748
2
+ SHA256:
3
+ metadata.gz: de17498105231eb4dd5b5135cf8e7683680cafc409a66acf6fddbaa6f6735b36
4
+ data.tar.gz: 60308e8ccff6fb80b4b5013975f9ea4903cb037b16997030635ace4557211f17
5
5
  SHA512:
6
- metadata.gz: ef5a10ecbc9cbe51553bce3936cdbee0eb3ff67bfee7b8f423bd91f2d9a7a012d6b615d2c961f16f1e5366f537ec60ba13ad5264d4141bfaca7a5fc3b25db6c4
7
- data.tar.gz: 542014545b679ea08d44c59ec07df312f31be870d02edb88d3d6bb559948ced10ea9d566b09a7aa1e5ccd028642b230b2ebf6097b337d40108a4fbec1cb9c364
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.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,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
- # Do not require `# frozen_string_literal: true` at the top of every file.
30
- FrozenStringLiteralComment:
31
- Enabled: false
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 shorter argument names like `ip`.
56
- Naming/UncommunicativeMethodParamName:
57
- MinNameLength: 2
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
@@ -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 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
@@ -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
@@ -1,26 +1,24 @@
1
1
  # GraphAttack
2
2
 
3
- [![CircleCI](https://circleci.com/gh/sunny/graph_attack.svg?style=svg)](https://circleci.com/gh/sunny/graph_attack)
3
+ [![Build Status](https://app.travis-ci.com/sunny/graph_attack.svg?branch=main)](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 = 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
@@ -72,19 +60,34 @@ end
72
60
 
73
61
  ## Configuration
74
62
 
75
- Use a custom Redis client instead of the default:
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
- query_analyzer GraphAttack::RateLimiter.new(
79
- redis_client: Redis.new(url: "…")
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 an
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`, commit,
97
- and then run `bundle exec rake release`, which will create a git tag for the
98
- version, push git commits and tags, and push the `.gem` file to
99
- [rubygems.org](https://rubygems.org).
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/master/CODE_OF_CONDUCT.md).
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/master/LICENSE.md)
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
- - **Fanny Cheung** - [KissKissBankBank](https://github.com/KissKissBankBank)
123
- - **Sunny Ripert** - [KissKissBankBank](https://github.com/KissKissBankBank)
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
- # 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.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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphAttack
4
+ class Error < StandardError; end
5
+ 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
@@ -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.1.0'.freeze
4
+ VERSION = '2.1.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.1.0
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: 2019-02-18 00:00:00.000000000 Z
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: '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.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: '0.55'
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/metadata.rb
136
- - 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
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: '0'
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
- rubyforge_project:
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,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,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