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 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