rack_attack_admin 0.1.1 → 0.1.2

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