rate_limiter 0.0.6 → 0.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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE.txt +20 -0
  4. data/README.md +18 -11
  5. data/lib/generators/rate_limiter/install_generator.rb +22 -0
  6. data/lib/generators/rate_limiter/templates/rate_limiter.rb +6 -0
  7. data/lib/rate_limiter.rb +51 -51
  8. data/lib/rate_limiter/config.rb +18 -3
  9. data/lib/rate_limiter/frameworks/rails.rb +3 -0
  10. data/lib/rate_limiter/frameworks/rails/controller.rb +69 -0
  11. data/lib/rate_limiter/model.rb +30 -48
  12. data/lib/rate_limiter/model_config.rb +34 -0
  13. data/lib/rate_limiter/request.rb +112 -0
  14. data/lib/rate_limiter/throttle.rb +79 -0
  15. data/lib/rate_limiter/validator.rb +29 -0
  16. data/lib/rate_limiter/version.rb +3 -1
  17. data/rate_limiter.gemspec +24 -18
  18. metadata +98 -183
  19. data/.gitignore +0 -11
  20. data/.rspec +0 -3
  21. data/.rvmrc +0 -80
  22. data/Gemfile +0 -3
  23. data/Gemfile.lock +0 -150
  24. data/Rakefile +0 -6
  25. data/lib/rate_limiter/controller.rb +0 -36
  26. data/spec/dummy/Rakefile +0 -7
  27. data/spec/dummy/app/assets/images/rails.png +0 -0
  28. data/spec/dummy/app/assets/javascripts/application.js +0 -15
  29. data/spec/dummy/app/assets/stylesheets/application.css +0 -13
  30. data/spec/dummy/app/controllers/application_controller.rb +0 -7
  31. data/spec/dummy/app/controllers/messages_controller.rb +0 -75
  32. data/spec/dummy/app/helpers/application_helper.rb +0 -2
  33. data/spec/dummy/app/mailers/.gitkeep +0 -0
  34. data/spec/dummy/app/models/message.rb +0 -5
  35. data/spec/dummy/app/views/layouts/application.html.erb +0 -15
  36. data/spec/dummy/app/views/messages/_form.html.erb +0 -22
  37. data/spec/dummy/app/views/messages/_message.html.erb +0 -8
  38. data/spec/dummy/app/views/messages/edit.html.erb +0 -3
  39. data/spec/dummy/app/views/messages/index.html.erb +0 -6
  40. data/spec/dummy/app/views/messages/new.html.erb +0 -3
  41. data/spec/dummy/app/views/messages/show.html.erb +0 -4
  42. data/spec/dummy/config.ru +0 -4
  43. data/spec/dummy/config/application.rb +0 -70
  44. data/spec/dummy/config/boot.rb +0 -11
  45. data/spec/dummy/config/database.yml +0 -25
  46. data/spec/dummy/config/environment.rb +0 -5
  47. data/spec/dummy/config/environments/development.rb +0 -37
  48. data/spec/dummy/config/environments/production.rb +0 -67
  49. data/spec/dummy/config/environments/test.rb +0 -37
  50. data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -7
  51. data/spec/dummy/config/initializers/inflections.rb +0 -15
  52. data/spec/dummy/config/initializers/mime_types.rb +0 -5
  53. data/spec/dummy/config/initializers/secret_token.rb +0 -7
  54. data/spec/dummy/config/initializers/session_store.rb +0 -8
  55. data/spec/dummy/config/initializers/wrap_parameters.rb +0 -14
  56. data/spec/dummy/config/locales/en.yml +0 -5
  57. data/spec/dummy/config/routes.rb +0 -5
  58. data/spec/dummy/db/migrate/20121213101512_create_messages.rb +0 -21
  59. data/spec/dummy/db/schema.rb +0 -33
  60. data/spec/dummy/db/seeds.rb +0 -7
  61. data/spec/dummy/lib/assets/.gitkeep +0 -0
  62. data/spec/dummy/lib/tasks/.gitkeep +0 -0
  63. data/spec/dummy/log/.gitkeep +0 -0
  64. data/spec/dummy/public/404.html +0 -26
  65. data/spec/dummy/public/422.html +0 -26
  66. data/spec/dummy/public/500.html +0 -25
  67. data/spec/dummy/public/favicon.ico +0 -0
  68. data/spec/dummy/script/rails +0 -6
  69. data/spec/rate_limiter/model_spec.rb +0 -20
  70. data/spec/rate_limiter_spec.rb +0 -10
  71. data/spec/spec_helper.rb +0 -30
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 59402eb858231d2bb8f7478a18c82a49a30268a5d9d7c4842f383a67d9e43ce8
4
+ data.tar.gz: ee860589d525fb08ab112d60065fc5d44aa76ef2a8f1df32c5edb0f334d0f95a
5
+ SHA512:
6
+ metadata.gz: fced90a375bdc2787e8a45da94e054379556a747bd38bdca81b46d7055332e267382fd2ef52e9151d0f7e627b61f7d3404b47354c2e42885a875fab93f964a29
7
+ data.tar.gz: 3deedcb193a1472ef5bdc47da33f995d848c43616165512836ff898d57f6eb82851dd3fcf53be210cc34970528c7944c8c45d387203b155b733cd30836906ff1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## v0.1.0
2
+
3
+ * Updated for Rails 5 (and 6).
4
+ * Use `validate` instead of `before_create`.
5
+ * Use minitest for tests.
6
+
1
7
  ## v0.0.6
2
8
 
3
9
  * Updated dependencies to allow for Rails 4.
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012-2019 Sean Eshbaugh
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -1,10 +1,15 @@
1
1
  # Rate Limiter
2
2
 
3
- A gem that limits the rate at which ActiveRecord model instances can be created.
3
+ [![Gem Version](https://badge.fury.io/rb/rate_limiter.svg)](https://badge.fury.io/rb/rate_limiter)
4
+ [![Travis](https://travis-ci.com/seaneshbaugh/rate_limiter.svg?branch=master)](https://travis-ci.org/seaneshbaugh/rate_limiter)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/287d36cd30f34a818738/test_coverage)](https://codeclimate.com/github/seaneshbaugh/rate_limiter/test_coverage)
6
+ [![Maintainability](https://api.codeclimate.com/v1/badges/287d36cd30f34a818738/maintainability)](https://codeclimate.com/github/seaneshbaugh/rate_limiter/maintainability)
4
7
 
5
- ## Rails Version
8
+ Limit the rate at which ActiveRecord model instances can be created.
6
9
 
7
- This gem has only been tested on Rails 3.2. There is no reason that I am aware of that would prevent it from working on all versions of Rails 3 (and Rails 4 when it is released).
10
+ ## Compatibility
11
+
12
+ This gem is compatible with Ruby 2.5 or greater and Rails 5 or greater. For Ruby versions older than 2.5 or for Rails 3 and 4 use [version 0.0.6](https://rubygems.org/gems/rate_limiter/versions/0.0.6).
8
13
 
9
14
  ## Installation
10
15
 
@@ -14,19 +19,19 @@ Add the gem to your project's Gemfile:
14
19
 
15
20
  ## Basic Usage
16
21
 
17
- In the models you want to rate limit simply call the `rate_limit` method inside the model.
22
+ In the models you want to rate limit simply call the `rate_limit` class method inside the model.
18
23
 
19
24
  ```ruby
20
- class ProductReview < ActiveRecord::Base
25
+ class ProductReview < ApplicationRecord
21
26
  rate_limit
22
27
  end
23
28
  ```
24
29
 
25
- By default this will rate limit creation of instances using the `ip_address` attribute with an interval of one minute. This is kind of a bold assumption (that may change in future versions) since there's a good chance you don't have an `ip_address` attribute on your model. If that's the case then you can do the following:
30
+ By default this will rate limit creation of instances using the `ip_address` attribute with an interval of one minute. This is a pretty big assumption that may change in future versions. To use a different attribute do the following:
26
31
 
27
32
  ```ruby
28
- class ProductReview < ActiveRecord::Base
29
- rate_limit :on => :username
33
+ class ProductReview < ApplicationRecord
34
+ rate_limit on: :username
30
35
  end
31
36
  ```
32
37
 
@@ -36,10 +41,14 @@ Because you may want to increase or decrease the interval between creating insta
36
41
 
37
42
  ```ruby
38
43
  class ProductReview < ActiveRecord::Base
39
- rate_limit :interval => 3.hours
44
+ rate_limit interval: 3.hours
40
45
  end
41
46
  ```
42
47
 
48
+ ## Credit Where Credit Is Due
49
+
50
+ Large portions of this gem are copied almost verbatim from the excellent [paper_trail](https://github.com/paper-trail-gem/paper_trail) gem; in particular the overall structure and all of the stuff that handles whether or not rate limiting is active.
51
+
43
52
  ## Contributing
44
53
 
45
54
  If you feel like you can add something useful to rate_limiter then don't hesitate to contribute! To make sure your fix/feature has a high chance of being included, please do the following:
@@ -61,5 +70,3 @@ Some things that will increase the chance that your pull request is accepted, ta
61
70
  * Use Rails idioms and helpers
62
71
  * Include tests that fail without your code, and pass with it
63
72
  * Update the documentation, guides, or whatever is affected by your contribution
64
-
65
- Yes, I am well aware of the irony of asking for tests when there are effectively none right now. This gem is a work in progress.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+
5
+ module RateLimiter
6
+ module Generators
7
+ # Rails generator for installing the default RateLimiter initializer.
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ source_root File.expand_path('./templates', __dir__)
10
+
11
+ desc 'Creates a RateLimiter initializer and copies a default locale file to your application.'
12
+
13
+ def copy_initializer
14
+ template('rate_limiter.rb', 'config/initializers/rate_limiter.rb')
15
+ end
16
+
17
+ def copy_locale
18
+ copy_file('../../../../config/locales/en.yml', 'config/locales/rate_limiter.en.yml')
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configure the default behavior of RateLimiter here.
4
+ RateLimiter.config do |config|
5
+ # config.timestamp_field = :created_at
6
+ end
data/lib/rate_limiter.rb CHANGED
@@ -1,63 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/all'
4
+ require 'active_record'
5
+
1
6
  require 'rate_limiter/config'
2
- require 'rate_limiter/controller'
3
7
  require 'rate_limiter/model'
8
+ require 'rate_limiter/request'
4
9
 
10
+ # Extend your ActiveRecord models with the ability to limit the rate at which
11
+ # they are saved.
5
12
  module RateLimiter
6
- def self.enabled=(value)
7
- RateLimiter.config.enabled = value
8
- end
9
-
10
- def self.enabled?
11
- !!RateLimiter.config.enabled
12
- end
13
-
14
- def self.enabled_for_controller=(value)
15
- rate_limiter_store[:request_enabled_for_controller] = value
16
- end
17
-
18
- def self.enabled_for_controller?
19
- !!rate_limiter_store[:request_enabled_for_controller]
20
- end
21
-
22
- def self.timestamp_field=(field_name)
23
- RateLimiter.config.timestamp_field = field_name
24
- end
25
-
26
- def self.timestamp_field
27
- RateLimiter.config.timestamp_field
28
- end
29
-
30
- def self.source=(value)
31
- rate_limiter_store[:source] = value
32
- end
33
-
34
- def self.source
35
- rate_limiter_store[:source]
36
- end
37
-
38
- def self.controller_info=(value)
39
- rate_limiter_store[:controller_info] = value
40
- end
41
-
42
- def self.controller_info
43
- rate_limiter_store[:controller_info]
44
- end
45
-
46
- private
47
-
48
- def self.rate_limiter_store
49
- Thread.current[:rate_limiter] ||= { :request_enabled_for_controller => true }
50
- end
51
-
52
- def self.config
53
- @@config ||= RateLimiter::Config.instance
13
+ class << self
14
+ # Return the RateLimiter singleton configuration object. This is for all
15
+ # threads.
16
+ def config
17
+ @config ||= Config.instance
18
+ yield @config if block_given?
19
+ @config
20
+ end
21
+ alias configure config
22
+
23
+ # Switches RateLimiter on or off, for all threads.
24
+ def enabled=(value)
25
+ config.enabled = value
26
+ end
27
+
28
+ # Returns `true` if RateLimiter is on, `false if it is off. This is for all
29
+ # threads.
30
+ def enabled?
31
+ config.enabled
32
+ end
33
+
34
+ # Gets the options local to the current request.
35
+ #
36
+ # If given a block the options passed in are set, the block is executed,
37
+ # previous options are restored, and the return value of the block is
38
+ # returned.
39
+ def request(options = nil, &block)
40
+ if options.nil? && !block_given?
41
+ Request
42
+ else
43
+ Request.with(options, &block)
44
+ end
45
+ end
54
46
  end
55
47
  end
56
48
 
49
+ # See https://guides.rubyonrails.org/engines.html#what-are-on-load-hooks-questionmark
50
+ # for more information on `on_load` hooks.
57
51
  ActiveSupport.on_load(:active_record) do
58
52
  include RateLimiter::Model
59
53
  end
60
54
 
61
- ActiveSupport.on_load(:action_controller) do
62
- include RateLimiter::Controller
55
+ # Load Rails controller extensions if RateLimiter is being used in a Rails
56
+ # application.
57
+ if defined?(::Rails)
58
+ if defined?(::Rails.application)
59
+ require 'rate_limiter/frameworks/rails'
60
+ else
61
+ Kernel.warn('RateLimiter has been loaded before Rails.')
62
+ end
63
63
  end
@@ -1,13 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'singleton'
2
4
 
3
5
  module RateLimiter
6
+ # Global configuration that affects all threads. Thread-specific configuration
7
+ # can be found in `/lib/rate_limiter.rb` and in
8
+ # `/lib/rate_limite/frameworks/rails/controller.rb`.
4
9
  class Config
5
10
  include Singleton
6
- attr_accessor :enabled, :timestamp_field
11
+
12
+ attr_accessor :rate_limit_defaults
7
13
 
8
14
  def initialize
9
- @enabled = true
10
- @timestamp_field = :created_at
15
+ @mutex = Mutex.new
16
+ @enabled = true
17
+ @rate_limit_defaults = {}
18
+ end
19
+
20
+ def enabled
21
+ @mutex.synchronize { !!@enabled }
22
+ end
23
+
24
+ def enabled=(enable)
25
+ @mutex.synchronize { @enabled = enable }
11
26
  end
12
27
  end
13
28
  end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rate_limiter/frameworks/rails/controller'
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RateLimiter
4
+ module Rails
5
+ # Extensions for Rails controllers. Allows for rate limiting to be turned on
6
+ # or off without disabling it on the model.
7
+ module Controller
8
+ def self.included(base)
9
+ base.before_action(
10
+ :set_rate_limiter_enabled_for_controller,
11
+ :set_rate_limiter_source
12
+ )
13
+ end
14
+
15
+ protected
16
+
17
+ # Get the user to use for the source for the current request. By default this will
18
+ # attempt to return the value of `current_user` since that is what Devise uses. If
19
+ # that assumption is incorrect this method can be overridden to return the correct
20
+ # user or ID (or nothing at all).
21
+ #
22
+ # ```
23
+ # def user_for_rate_limiter
24
+ # logged_in_user.id
25
+ # end
26
+ # ```
27
+ def user_for_rate_limiter
28
+ return nil unless respond_to?(:current_user)
29
+
30
+ current_user
31
+ end
32
+
33
+ # Returns `true` or `false` depending on whether rate imiting should be
34
+ # active for the current request for all models.
35
+ #
36
+ # Override this method in your controller to turn rate limiting on or off.
37
+ #
38
+ # ```
39
+ # def rate_limiter_enabled_for_controller
40
+ # # It is recommended that you always call `super` here unless simply
41
+ # # returning `false`.
42
+ # super && !user_for_rate_limiter.has_role?(:admin)
43
+ # end
44
+ # ```
45
+ def rate_limiter_enabled_for_controller
46
+ RateLimiter.enabled?
47
+ end
48
+
49
+ private
50
+
51
+ # Tells RateLimiter whether rate limiting should be enabled for the
52
+ # current request.
53
+ def set_rate_limiter_enabled_for_controller
54
+ RateLimiter.request.enabled = rate_limiter_enabled_for_controller
55
+ end
56
+
57
+ # Set the request store's source.
58
+ def set_rate_limiter_source
59
+ RateLimiter.request.source = user_for_rate_limiter if rate_limiter_enabled_for_controller
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ if defined?(::ActionController)
66
+ ::ActiveSupport.on_load(:action_controller) do
67
+ include RateLimiter::Rails::Controller
68
+ end
69
+ end
@@ -1,66 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rate_limiter/model_config'
4
+ require 'rate_limiter/throttle'
5
+
1
6
  module RateLimiter
7
+ # Mixin module for ActiveRecord models.
2
8
  module Model
3
9
  def self.included(base)
4
- base.send :extend, ClassMethods
10
+ base.send(:extend, ClassMethods)
5
11
  end
6
12
 
13
+ # Class methods available to models after RateLimiter has been loaded.
7
14
  module ClassMethods
15
+ # Tell the model to limit creation of records based on an attribute for a
16
+ # given interval of time.
17
+ #
18
+ # Options:
19
+ #
20
+ # - :on - The attribute to limit on. Defaults to `:ip_address`. Set to an
21
+ # array to limit on multiple attributes (e.g. `:ip_address` or `:user_id`.
22
+ # - :interval - The amount of time that must have elapses since the last
23
+ # record that has the same value as the attribute indicated by the `:on`
24
+ # option in seconds. Defaults to 1 minute.
25
+ # - :if, :unless - Procs that specify the conditions for when record
26
+ # creation rate limiting should occur.
8
27
  def rate_limit(options = {})
9
- send :include, InstanceMethods
10
-
11
- class_attribute :rate_limit_on
12
- self.rate_limit_on = options[:on] || :ip_address
13
-
14
- class_attribute :rate_limit_interval
15
- self.rate_limit_interval = options[:interval] || 1.minute
16
-
17
- class_attribute :rate_limit_if_condition
18
- self.rate_limit_if_condition = options[:if]
19
-
20
- class_attribute :rate_limit_unless_condition
21
- self.rate_limit_unless_condition = options[:unless]
22
-
23
- class_attribute :rate_limit_enabled_for_model
24
- self.rate_limit_enabled_for_model = true
25
-
26
- self.before_create :check_rate_limit
28
+ defaults = RateLimiter.config.rate_limit_defaults
29
+ rate_limiter.setup(defaults.merge(options))
27
30
  end
28
31
 
29
- def rate_limit_off
30
- self.rate_limit_enabled_for_model = false
31
- end
32
-
33
- def rate_limit_on
34
- self.rate_limit_enabled_for_model = true
32
+ def rate_limiter
33
+ ModelConfig.new(self)
35
34
  end
36
35
  end
37
36
 
37
+ # Instance methods available to models after RateLimiter has been initialized by
38
+ # calling `rate_limit`.
38
39
  module InstanceMethods
39
- def check_rate_limit
40
- if switched_on? && rate_limit?
41
- klass = self.class
42
-
43
- others = klass.where("#{klass.rate_limit_on.to_s} = ? AND #{RateLimiter.config.timestamp_field.to_s} >= ?", self.send(klass.rate_limit_on), Time.now - klass.rate_limit_interval)
44
-
45
- if others.present?
46
- # TODO: Come up with a better error message.
47
- self.errors.add(:base, "You cannot create a new #{klass.name.downcase} yet.")
48
-
49
- false
50
- else
51
- true
52
- end
53
- else
54
- true
55
- end
56
- end
57
-
58
- def switched_on?
59
- RateLimiter.enabled? && RateLimiter.enabled_for_controller? && self.class.rate_limit_enabled_for_model
40
+ def rate_limit_exceeded?
41
+ throttle.exceeded?
60
42
  end
61
43
 
62
- def rate_limit?
63
- (rate_limit_if_condition.blank? || rate_limit_if_condition.call(self)) && !rate_limit_unless_condition.try(:call, self)
44
+ def throttle
45
+ Throttle.new(self, self.class.rate_limiter_options)
64
46
  end
65
47
  end
66
48
  end