rack_attack_admin 0.1.1 → 0.1.2

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
2
  SHA256:
3
- metadata.gz: 9ecd6ce1a1b78254993c7d6b6e72b23c7e0aeea62268282666ff3f9c55177f35
4
- data.tar.gz: fb1449910735e43bff061886f9d362c0358b8f37b8de2332701864b3d123f859
3
+ metadata.gz: 56573b4e1953af195a40c77e4ff7a43dc0092ab28752985ff3f21d92ae4b27ce
4
+ data.tar.gz: 2c224cb40e27bbabac363ba02ed2b37259595d1c4342be2ab2884167d85cb5c3
5
5
  SHA512:
6
- metadata.gz: d5cb6070210f2b5ba29f3d2c219d9ec3574df58e3e56f0f87294cc60be6c20f719b94379079e3736093a4411584c171445ef2f435e0958d519fee76808ff84c1
7
- data.tar.gz: eb24534ef311d712d43647d0804f04ddf193d8f3b623e54bafa80be491e353d83897a05138add8106e0a859f9be8eecc7885d42c0e1318f34c70fde94fe2ff11
6
+ metadata.gz: 6fd53b0f1e13e09370292b1e3121c1135096d0f4e4f211d11be87dcf588fa4e3a0fadcf8b5f2fab4f666d1600ebf35566022f3d5430833da801d76cde1816ae0
7
+ data.tar.gz: ca371334c028fcf6e1eb632ca0f8371e58e9cd02c3cf1126e522a430babc30cc953c5e67606d67ee0f73b25f167e7887d005f351cfdc892f9cf41bf8249524b0
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.2 (2019-06-17)
4
+
5
+ - Various fixes and improvements to the dashboard templates
6
+ - Make it work also when Rack::Attack.cache.store is set to an instance of ActiveSupport::Cache::RedisCacheStore (from rails)
7
+ - Let current_request respond to .html as well as .json
8
+
9
+ ## 0.1.1 (2019-04-02)
10
+
11
+ Initial release
data/Readme.md CHANGED
@@ -1,4 +1,8 @@
1
- # Rack::Attack Admin Dashboard
1
+ # [Rack::Attack](https://github.com/kickstarter/rack-attack) Admin Dashboard [![Gem Version](https://badge.fury.io/rb/rack_attack_admin.svg)](https://badge.fury.io/rb/rack_attack_admin)
2
+
3
+ Lets you see the current state of all throttles and bans. Delete existing keys/bans. Manually add bans.
4
+
5
+ ![Screenshot](Screenshot.png)
2
6
 
3
7
  Inspired by: https://www.backerkit.com/blog/building-a-rackattack-dashboard/
4
8
 
@@ -10,11 +14,6 @@ Add this line to your application's `Gemfile`:
10
14
  gem 'rack_attack_admin'
11
15
  ```
12
16
 
13
- And then execute:
14
-
15
- $ bundle
16
-
17
-
18
17
  Add this line to your application's `config/routes.rb`:
19
18
 
20
19
  ```ruby
@@ -23,13 +22,97 @@ mount RackAttackAdmin::Engine, at: '/admin/rack_attack'
23
22
 
24
23
  ## Usage
25
24
 
26
- TODO: Write usage instructions here
25
+ Go to http://localhost:3000/admin/rack_attack in your browser!
26
+
27
+ You can also use the provided read-only command-line command, `rake rack_attack_admin:watch` instead of the web interface:
28
+ ```
29
+ +-------------------------------+--------------------+
30
+ | Banned IP | Previous (5 s ago) |
31
+ +-------------------------------+--------------------+
32
+ | allow2ban:ban:login-127.0.0.1 | |
33
+ +-------------------------------+--------------------+
34
+ +----------------------------------------------------------------------------------+---------------+--------------------+
35
+ | Key | Current Count | Previous (5 s ago) |
36
+ +----------------------------------------------------------------------------------+---------------+--------------------+
37
+ | ('allow2ban:count'):login-127.0.0.1 | 2 | 1 |
38
+ | throttle('logins/ip'):127.0.0.1 | 1 | |
39
+ | throttle('logins/email'):me@example.com | 1 | |
40
+ | throttle('req/ip'):127.0.0.1 | 2 | 1 |
41
+ +----------------------------------------------------------------------------------+---------------+--------------------+
42
+ ```
43
+
27
44
 
28
- ## Development
45
+ ## Fail2Ban/Allow2Ban
29
46
 
30
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
47
+ In order to allow your Fail2Ban/Allow2Ban rules to be introspected by this app, you must define them
48
+ slightly differently than the upstream [Rack::Attack
49
+ documentation](https://github.com/kickstarter/rack-attack#fail2ban) tells you
50
+ to define them:
51
+
52
+ If you have an Allow2Ban filter in a blocklist like this:
53
+
54
+ ```ruby
55
+ blocklist('login:allow2ban') do |req|
56
+ Rack::Attack::Allow2Ban.filter("login-#{req.ip}", maxretry: 5, findtime: 1.minute, bantime: 10.minutes) do
57
+ # The count for the IP is incremented if this return value is truthy.
58
+ is_login.(req)
59
+ end
60
+ end
61
+ ```
62
+
63
+ , you can change it to this equivalent definition:
64
+
65
+ ```ruby
66
+ blocklist('login:allow2ban') do |req|
67
+ def_allow2ban('login', limit: 5, period: 1.minute, bantime: 10.minutes)
68
+ allow2ban( 'login', req.ip) do
69
+ is_login.(req)
70
+ end
71
+ end
72
+ ```
31
73
 
32
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
74
+ `def_fail2ban`/`def_allow2ban` save your configuration in a hash (by name, _without_ discriminator in it), much the same as
75
+ `throttle` does.
76
+
77
+ `fail2ban`/`allow2ban` methods are simply wrappers for `Rack::Attack::Allow2Ban.filter` that look up and use the options from the definition (that matches the given name).
78
+
79
+ This has the following advantages:
80
+ - It actually stores your Fail2Ban rule so that we can look it up later
81
+ - It provides an API and options that are more similar to and consistent with regular `throttle`
82
+ commands:
83
+ ```ruby
84
+ # Compare:
85
+ def_allow2ban('login', limit: 5, period: 1.minute, bantime: …)
86
+ allow2ban('login', discriminator) do
87
+ # Return truthy value to increment counter
88
+ end
89
+ # allow2ban returns true if counter reaches limit
90
+
91
+ throttle('logins/email', limit: 5, period: 1.minute) do |req|
92
+ discriminator.(req)
93
+ end
94
+ ```
95
+ - You can use the familiar `limit` and `period` options instead of `maxretry` and `findtime`
96
+ options, respectively.
97
+ - You don't have to interpolate the discriminator into a string like you do with the standard
98
+ `Fail2Ban.filter` syntax.
99
+
100
+
101
+ This is completely optional. If you choose not to define them this way, it will still show your
102
+ fail2ban counter keys and value; it just won't be able to find the matching rule, and therefore
103
+ won't be able to show what the limit or time bucket is for that counter key. So instead of showing
104
+ the limit for `allow2ban('login')` like it shows in the screenshot above, it will fall back to
105
+ just showing what little it can show about that key:
106
+
107
+ ![Screenshot](Screenshot-not_find_allow2ban_rule.png)
108
+
109
+ ## Redis cache store compatibility
110
+
111
+ This has been tested with `Rack::Attack.cache.store` set to an instance of:
112
+ - `Redis::Store` from the fantastic [redis-store](https://github.com/redis-store/redis-store) gem.
113
+ (Which is used by the `ActiveSupport::Cache::RedisStore` (from redis-activesupport/redis-rails
114
+ gems))
115
+ - `ActiveSupport::Cache::RedisCacheStore` (provided by Rails 5.2+)
33
116
 
34
117
  ## Contributing
35
118
 
Binary file
@@ -13,7 +13,21 @@ module RackAttackAdmin
13
13
 
14
14
  helper_method \
15
15
  def is_redis?
16
- Rack::Attack.cache.respond.store.respond_to? :ttl
16
+ Rack::Attack.cache.respond.store.to_s.match?(/Redis/)
17
+ end
18
+
19
+ helper_method \
20
+ def redis
21
+ return unless is_redis?
22
+ store = Rack::Attack.cache.store
23
+ store = store.redis if store.respond_to?(:redis)
24
+ store = store.data if store.respond_to?(:data)
25
+ store
26
+ end
27
+
28
+ helper_method \
29
+ def has_ttl?
30
+ !!redis
17
31
  end
18
32
 
19
33
  helper_method \
@@ -5,14 +5,18 @@ module RackAttackAdmin
5
5
  # Web version of lib/tasks/rack_attack_admin_tasks.rake
6
6
  def index
7
7
  @default_banned_ip = Rack::Attack::BannedIp.new(bantime: '60 m')
8
- @banned_ip_keys = Rack::Attack::Fail2Ban.banned_ip_keys
9
8
  @counters_h = Rack::Attack.counters_h.
10
9
  without(*Rack::Attack::BannedIps.keys)
11
10
  render
12
11
  end
13
12
 
14
13
  def current_request
15
- render json: current_request_rack_attack_stats
14
+ respond_to do |format|
15
+ format.json do
16
+ render json: current_request_rack_attack_stats
17
+ end
18
+ format.html
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -1,3 +1,4 @@
1
+ - expires_column = local_assigns.key?(:expires_column) ? local_assigns[:expires_column] : has_ttl?
1
2
  %tr
2
3
  %td= key
3
4
 
@@ -6,9 +7,10 @@
6
7
  = ip
7
8
  (#{link_to 'Look up', "http://ip-lookup.net/?ip=#{ip}", target:'_blank'})
8
9
 
9
- - if is_redis?
10
+ - if expires_column
10
11
  %td
11
- - interval = Rack::Attack.cache.store.ttl("#{Rack::Attack.cache.prefix}:#{key}")
12
+ - if has_ttl?
13
+ - interval = redis.ttl("#{Rack::Attack.prefix_with_namespace}:#{key}")
12
14
  - if interval
13
15
  =# distance_of_time_in_words(interval)
14
16
  in #{ActiveSupport::Duration.build(interval)&.human_str}
@@ -0,0 +1,10 @@
1
+ %h1 Current Request
2
+ %table.table.table-striped
3
+ %thead
4
+ %tr
5
+ %th Key
6
+ %th Value
7
+ - current_request_rack_attack_stats.each do |key, value|
8
+ %tr
9
+ %td= key
10
+ %td= value
@@ -4,14 +4,14 @@
4
4
  %tr
5
5
  %th Key
6
6
  %th Banned IP
7
- - if is_redis?
8
- %th Expires
7
+ %th Expires
9
8
  %th Actions
10
9
 
11
10
  = render partial: 'rack_attack_admin/banned_ips/banned_ip',
12
11
  collection: Rack::Attack::BannedIps.keys, as: 'key',
13
12
  locals: { _:nil,
14
13
  full_key_prefix: Rack::Attack::BannedIps.full_key_prefix,
14
+ expires_column: true,
15
15
  }
16
16
 
17
17
  = form_for @default_banned_ip, url: [rack_attack_admin, :banned_ips] do |f|
@@ -27,18 +27,17 @@
27
27
  %tr
28
28
  %th Key
29
29
  %th Banned IP
30
- - if is_redis?
30
+ - if has_ttl?
31
31
  %th Expires
32
32
  %th Actions
33
33
 
34
- -#
35
- = render partial: 'admin/rack_attack/banned_ips/banned_ip',
36
- collection: @banned_ip_keys, as: 'key',
34
+ = render partial: 'rack_attack_admin/banned_ips/banned_ip',
35
+ collection: Rack::Attack::Fail2Ban.banned_ip_keys, as: 'key',
37
36
  locals: { _:nil,
38
37
  full_key_prefix: Rack::Attack::Fail2Ban.full_key_prefix,
39
38
  }
40
39
 
41
- %h1 Throttle/Fail2Ban Count Keys
40
+ %h1 Throttle/Fail2Ban Counters
42
41
  %table.table.table-striped.mb-0
43
42
  %thead
44
43
  %tr
@@ -52,11 +51,25 @@
52
51
  (Time bucket)
53
52
  %th Actions
54
53
  - @counters_h.each do |key, value|
54
+ :ruby
55
+ parsed = Rack::Attack.parse_key(key)
56
+ value = value.to_i
57
+ name = Rack::Attack.humanize_key(key).sub(":#{parsed[:discriminator]}", '')
58
+
59
+ # We can get expires_in from redis or directly from the matched throttle rule
60
+ interval =
61
+ if has_ttl?
62
+ redis.ttl("#{Rack::Attack.prefix_with_namespace}:#{key}")
63
+ elsif parsed and time_range = parsed[:time_range]
64
+ (time_range.end - Time.now)
65
+ end
66
+ if parsed
67
+ time_range = parsed[:time_range]
68
+ end
69
+ if time_range
70
+ human_duration = time_range.duration&.human_str
71
+ end
55
72
  %tr
56
- :ruby
57
- parsed = Rack::Attack.parse_key(key)
58
- value = value.to_i
59
- name = Rack::Attack.humanize_key(key).sub(":#{parsed[:discriminator]}", '')
60
73
  %td
61
74
  = name
62
75
  -# if parsed and rule = parsed[:rule]
@@ -65,52 +78,33 @@
65
78
  %td= parsed[:discriminator]
66
79
 
67
80
  - limit = parsed && (rule = parsed[:rule]) && rule.limit.to_i
68
- - over_limit = value >= limit
81
+ - over_limit = limit && value >= limit
69
82
  %td{class: ('over_limit' if over_limit), style: ('color: red' if over_limit)}
70
83
  = value
84
+ <br/>
71
85
  - if limit
72
- = "/#{limit}"
86
+ Limit:
87
+ = "#{limit}"
88
+ = "/#{human_duration}" if human_duration
89
+ <br/>
90
+ - if time_range
91
+ %small
92
+ (#{(limit / (time_range.duration/60.0)).round(0)}/min)
73
93
 
74
94
  %td
75
- -# We can get expires_in from redis or directly from the mached throttle rule
76
- :ruby
77
- interval =
78
- if is_redis?
79
- Rack::Attack.cache.store.ttl("#{Rack::Attack.cache.prefix}:#{key}")
80
- elsif parsed and time_range = parsed[:time_range]
81
- (time_range.end - Time.now)
82
- end
83
95
  - if interval
84
- in #{ActiveSupport::Duration.build(interval)&.human_str}<br/>
96
+ - if interval < 0
97
+ expired<br/>
98
+ - elsif interval
99
+ in #{ActiveSupport::Duration.build(interval)&.human_str}<br/>
85
100
 
86
- - if parsed and time_range = parsed[:time_range]
87
- %small
88
- (#{ time_range.begin.to_s(:time_with_s)}
89
- %span><
90
- \-
91
- #{time_range.end .to_s(:time_with_s)}
92
- \= #{time_range.duration&.human_str})
101
+ - if time_range
102
+ %small
103
+ (#{ time_range.begin.to_s(:time_with_s)}
104
+ %span><
105
+ \-
106
+ #{time_range.end .to_s(:time_with_s)}
107
+ \= #{human_duration})
93
108
 
94
109
  %td= link_to 'Delete', rack_attack_admin.key_path(key), method: :delete, class: 'btn'
95
110
  .current_time.mb-2 (Current time: #{Time.now.to_s(:time_with_s)})
96
-
97
- %h1 Flags
98
- %table.table.table-striped
99
- %thead
100
- %tr
101
- %th Key
102
- %th Value
103
- %tr
104
- %td cookies[:skip_safelist]
105
- %td= cookies[:skip_safelist]
106
-
107
- %h1 Current Request
108
- %table.table.table-striped
109
- %thead
110
- %tr
111
- %th Key
112
- %th Value
113
- - current_request_rack_attack_stats.each do |key, value|
114
- %tr
115
- %td= key
116
- %td= value
@@ -6,8 +6,65 @@ class Rack::Attack
6
6
  class << self
7
7
  extend Memoist
8
8
 
9
+ def all_keys
10
+ store, namespace = cache_store_and_namespace_to_strip
11
+ keys = store.keys
12
+ if namespace
13
+ keys.map {|key| key.to_s.sub(/^#{namespace}:/, '') }
14
+ else
15
+ keys
16
+ end
17
+ end
18
+
19
+ # The same as cache.prefix but prefixed with "{namespace}:" if namespace option is set and needs
20
+ # to be stripped from keys returned from store.keys.
21
+ # Like cache.prefix, this does not include the trailing ':'.
22
+ def prefix_with_namespace_to_strip
23
+ prefix = cache.prefix
24
+ store, namespace = cache_store_and_namespace_to_strip
25
+ if namespace
26
+ prefix = "#{namespace}:#{prefix}"
27
+ end
28
+ prefix
29
+ end
30
+
31
+ # The same as cache.prefix but prefixed with "{namespace}:" if namespace option is set.
32
+ # Needed when passing a key directly to a Redis command, like Redis#ttl, since Redis class
33
+ # doesn't know about namespacing.
34
+ def prefix_with_namespace
35
+ prefix = cache.prefix
36
+ if namespace = cache_namespace
37
+ prefix = "#{namespace}:#{prefix}"
38
+ end
39
+ prefix
40
+ end
41
+
42
+ def cache_namespace
43
+ cache.store&.options&.[](:namespace)
44
+ end
45
+
46
+ # Returns an array of [cache_store, namespace_to_strip]
47
+ # This will either be [a Redis::Store, nil] or
48
+ # [a Redis , namespace_to_strip]
49
+ def cache_store_and_namespace_to_strip
50
+ store = cache.store
51
+ # Store can be a ActiveSupport::Cache::RedisCacheStore, a Redis::Store object, or a Redis object.
52
+ # If it is a ActiveSupport::Cache::RedisCacheStore, then we need to get the redis object in
53
+ # order to get keys from it.
54
+ store = store.redis if store.respond_to?(:redis)
55
+ if store.respond_to?(:data) # Redis::Store already stripped namespaced
56
+ store = store.data
57
+ [store, nil]
58
+ else
59
+ # Redis object (which is all we have available in the case of a
60
+ # ActiveSupport::Cache::RedisCacheStore) unfortunately returns keys with namespace prefix in
61
+ # each key, so we need to strip this out (Redis::Store does this already; see store.data.keys above)
62
+ [store, cache_namespace]
63
+ end
64
+ end
65
+
9
66
  def prefixed_keys
10
- cache.store.keys.grep(/^rack::attack:/)
67
+ all_keys.grep(/^#{cache.prefix}:/)
11
68
  end
12
69
 
13
70
  # AKA unprefixed_keys
@@ -56,9 +113,6 @@ class Rack::Attack
56
113
  )
57
114
  end
58
115
 
59
- class ParsedKey
60
- end
61
-
62
116
  # Reverse Cache#key_and_expiry:
63
117
  # … "#{prefix}:#{(@last_epoch_time / period).to_i}:#{unprefixed_key}" …
64
118
  # @return [Hash]
@@ -231,7 +285,7 @@ class Rack::Attack
231
285
 
232
286
  class << self
233
287
  def prefixed_keys
234
- cache.store.keys.grep(/^#{cache.prefix}:(allow|fail)2ban:/)
288
+ Rack::Attack.all_keys.grep(/^#{cache.prefix}:(allow|fail)2ban:/)
235
289
  end
236
290
 
237
291
  # AKA unprefixed_keys
@@ -273,7 +327,7 @@ class Rack::Attack
273
327
  class BannedIps
274
328
  class << self
275
329
  def prefixed_keys
276
- cache.store.keys.grep(/^#{full_key_prefix}:/)
330
+ Rack::Attack.all_keys.grep(/^#{full_key_prefix}:/)
277
331
  end
278
332
 
279
333
  # Removes only the Rack::Attack.cache.prefix
@@ -1,5 +1,5 @@
1
1
  module RackAttackAdmin
2
2
  def self.version
3
- "0.1.1"
3
+ "0.1.2"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack_attack_admin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Rick
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-04-02 00:00:00.000000000 Z
11
+ date: 2019-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -164,11 +164,14 @@ files:
164
164
  - License
165
165
  - Rakefile
166
166
  - Readme.md
167
+ - Screenshot-not_find_allow2ban_rule.png
168
+ - Screenshot.png
167
169
  - app/controllers/rack_attack_admin/application_controller.rb
168
170
  - app/controllers/rack_attack_admin/banned_ips_controller.rb
169
171
  - app/controllers/rack_attack_admin/keys_controller.rb
170
172
  - app/controllers/rack_attack_admin/rack_attack_controller.rb
171
173
  - app/views/rack_attack_admin/banned_ips/_banned_ip.html.haml
174
+ - app/views/rack_attack_admin/rack_attack/current_request.html.haml
172
175
  - app/views/rack_attack_admin/rack_attack/index.html.haml
173
176
  - bin/console
174
177
  - bin/setup
@@ -201,7 +204,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
201
204
  - !ruby/object:Gem::Version
202
205
  version: '0'
203
206
  requirements: []
204
- rubygems_version: 3.0.1
207
+ rubygems_version: 3.0.3
205
208
  signing_key:
206
209
  specification_version: 4
207
210
  summary: A Rack::Attack admin dashboard