diff --git a/CHANGELOG.md b/CHANGELOG.md index 146aee9..59223bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,9 @@ All notable changes to this project will be documented in this file. ### Added -- Shorthand for blocking IP addresses `Rack::Attack.blocklist_ip("1.2.3.4")` +- Shorthand for blocking an IP address `Rack::Attack.blocklist_ip("1.2.3.4")` - Shorthand for blocking IP subnets `Rack::Attack.blocklist_ip("1.2.0.0/16")` +- Shorthand for safelisting an IP address `Rack::Attack.safelist_ip("5.6.7.8")` - Throw helpful error message when using `allow2ban` but cache store is misconfigured ([#315](https://github.com/kickstarter/rack-attack/issues/315)) - Throw helpful error message when using `fail2ban` but cache store is misconfigured ([#315](https://github.com/kickstarter/rack-attack/issues/315)) diff --git a/lib/rack/attack.rb b/lib/rack/attack.rb index 836f7a9..4abca17 100644 --- a/lib/rack/attack.rb +++ b/lib/rack/attack.rb @@ -43,6 +43,12 @@ class Rack::Attack @ip_blocklists << Blocklist.new(nil, ip_blocklist_proc) end + def safelist_ip(ip) + @ip_safelists ||= [] + ip_safelist_proc = lambda { |request| ip == request.ip } + @ip_safelists << Safelist.new(nil, ip_safelist_proc) + end + def blacklist(name, &block) warn "[DEPRECATION] 'Rack::Attack.blacklist' is deprecated. Please use 'blocklist' instead." blocklist(name, &block) @@ -72,9 +78,8 @@ class Rack::Attack end def safelisted?(req) - safelists.any? do |name, safelist| - safelist[req] - end + ip_safelists.any? { |safelist| safelist.match?(req) } || + safelists.any? { |_name, safelist| safelist.match?(req) } end def whitelisted?(req) @@ -115,6 +120,7 @@ class Rack::Attack def clear! @safelists, @blocklists, @throttles, @tracks = {}, {}, {}, {} @ip_blocklists = [] + @ip_safelists = [] end def blacklisted_response=(res) @@ -132,6 +138,10 @@ class Rack::Attack def ip_blocklists @ip_blocklists ||= [] end + + def ip_safelists + @ip_safelists ||= [] + end end # Set defaults diff --git a/spec/acceptance/safelisting_ip_spec.rb b/spec/acceptance/safelisting_ip_spec.rb new file mode 100644 index 0000000..38faf72 --- /dev/null +++ b/spec/acceptance/safelisting_ip_spec.rb @@ -0,0 +1,49 @@ +require_relative "../spec_helper" + +describe "Safelist an IP" do + before do + Rack::Attack.blocklist("admin") do |request| + request.path == "/admin" + end + + Rack::Attack.safelist_ip("5.6.7.8") + end + + it "forbids request if blocklist condition is true and safelist is false" do + get "/admin", {}, "REMOTE_ADDR" => "1.2.3.4" + + assert_equal 403, last_response.status + end + + it "succeeds if blocklist condition is false and safelist is false" do + get "/", {}, "REMOTE_ADDR" => "1.2.3.4" + + assert_equal 200, last_response.status + end + + it "succeeds request if blocklist condition is false and safelist is true" do + get "/", {}, "REMOTE_ADDR" => "5.6.7.8" + + assert_equal 200, last_response.status + end + + it "succeeds request if both blocklist and safelist conditions are true" do + get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8" + + assert_equal 200, last_response.status + end + + it "notifies when the request is safe" do + notification_type = nil + + ActiveSupport::Notifications.subscribe("rack.attack") do |_name, _start, _finish, _id, request| + notification_type = request.env["rack.attack.match_type"] + end + + get "/admin", {}, "REMOTE_ADDR" => "5.6.7.8" + + assert_equal 200, last_response.status + assert_equal :safelist, notification_type + end +end +