action_ip_filter 0.1.1 → 0.3.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
2
  SHA256:
3
- metadata.gz: eaf87646a87d44c3eb43e5d9c7f08151068ae3428405b46c034ae933adfad7ca
4
- data.tar.gz: b1d4c318d9fdba5163bac3b7ebeb8f9969718b37b526515a5861a5f509fabce1
3
+ metadata.gz: af3d4fbb598b8802bc90a29a75a0f6f3d38cd77757a04c8bef538fe34172ba14
4
+ data.tar.gz: 13e2e5ed9b621b70c27f11046149643d7237fe977766e29722f9f293c5ebe46d
5
5
  SHA512:
6
- metadata.gz: b666b53a5c8bd924a8489bb3b540bfe75add34d6d3e938fa5be54d0dace72d35723d6f08789b0c45944fbd1e9d3998c439963ae1b9f3c2d9e860f3e03e391669
7
- data.tar.gz: 5f3dbd4b22bad8e02f595e47417be2564efac1386bf819077301a6020626994227725cd4f5c05b7a738b3085d3092f2c1debae349b73edaca7070a7f94dfa95d
6
+ metadata.gz: 578ca535fedf30d89e7681b286f2d68518cef4a001802d053cd49cc1a42cf9e9f85d7628230b4a28ddf7c54cc6225a158ce4f7f161d8f09c0fb55833adafb154
7
+ data.tar.gz: 1ba8977aa885e48794147c6c09c1d4c4bbd71f35974371cfb5ef8c75ea47efbf179480fea9f1c6259ea57ac7fdb0fc58e562969a230815bec857aa109da97551
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2025-11-29
4
+
5
+ ### Breaking Changes
6
+
7
+ #### Change `ip_resolver` to use controller context instead of request parameter
8
+
9
+ Usage has changed. The `request` parameter in the Proc was previously required, but it is no longer needed. You can now access controller methods (`request`, `params`, etc.) directly instead of receiving `request` as an argument.
10
+
11
+ Before:
12
+
13
+ ```
14
+ config.ip_resolver = ->(request) {
15
+ request.headers["X-Forwarded-For"]&.split(",")&.first&.strip || request.remote_ip
16
+ }
17
+ ```
18
+
19
+ After:
20
+
21
+ ```
22
+ config.ip_resolver = -> {
23
+ request.headers["X-Forwarded-For"]&.split(",")&.first&.strip || request.remote_ip
24
+ }
25
+ ```
26
+
27
+ ## [0.2.0] - 2025-11-28
28
+
29
+ ### Breaking Changes
30
+
31
+ - Change the interface: `s/restrict_ip/filter_ip/g`
32
+
3
33
  ## [0.1.1] - 2025-11-28
4
34
 
5
35
  ### Maintenance
data/README.md CHANGED
@@ -18,6 +18,24 @@ Unlike Rack middleware solutions (e.g., `rack-attack`), action_ip_filter operate
18
18
  - Minimal overhead (no processing for unrestricted endpoints)
19
19
  - Simple, declarative configuration per controller
20
20
 
21
+ ### Why not Rails native `rate_limit`?
22
+
23
+ Rails 8.0 introduced `rate_limit`, which could be adapted for IP filtering:
24
+
25
+ ```ruby
26
+ # a slightly tricky approach involving rate_limit
27
+ rate_limit to: 0, within: 0.second, only: :create, unless: -> {
28
+ allowed_ips.include?(request.remote_ip)
29
+ }
30
+ ```
31
+
32
+ However, this approach has drawbacks:
33
+ - Semantic mismatch: `rate_limit` is designed for rate limiting, not access control
34
+ - Requires cache store: `rate_limit` needs a cache backend for counting, unnecessary for simple IP allowlists
35
+ - Inverted logic: You're "rate limiting everyone except allowed IPs" rather than "allowing specific IPs"
36
+
37
+ action_ip_filter provides a purpose-built, declarative API for IP-based access control.
38
+
21
39
  ## Installation
22
40
 
23
41
  Add to your Gemfile:
@@ -36,13 +54,13 @@ bundle install
36
54
 
37
55
  ### Basic Usage
38
56
 
39
- Include the concern and use `restrict_ip` to protect specific actions:
57
+ Include the concern and use `filter_ip` to protect specific actions:
40
58
 
41
59
  ```ruby
42
60
  class AdminController < ApplicationController
43
61
  include ActionIpFilter::IpFilterable
44
62
 
45
- restrict_ip :index, :show, allowed_ips: %w[192.0.2.0/24 198.51.100.1]
63
+ filter_ip :index, :show, allowed_ips: %w[192.0.2.0/24 198.51.100.1]
46
64
 
47
65
  def index
48
66
  # Only accessible from 192.0.2.0/24 or 198.51.100.1
@@ -60,14 +78,14 @@ end
60
78
 
61
79
  ### Restrict All Actions
62
80
 
63
- Use `restrict_ip_for_all` to protect all actions with optional exceptions:
81
+ Use `filter_ip_for_all` to protect all actions with optional exceptions:
64
82
 
65
83
  ```ruby
66
84
  class WebhooksController < ApplicationController
67
85
  include ActionIpFilter::IpFilterable
68
86
 
69
- restrict_ip_for_all allowed_ips: ENV["WEBHOOK_ALLOWED_IPS"].to_s.split(","),
70
- except: [:health_check]
87
+ filter_ip_for_all allowed_ips: ENV["WEBHOOK_ALLOWED_IPS"].to_s.split(","),
88
+ except: [:health_check]
71
89
 
72
90
  def stripe
73
91
  # Restricted
@@ -87,7 +105,7 @@ Pass a Proc for dynamic IP resolution:
87
105
  class SecureController < ApplicationController
88
106
  include ActionIpFilter::IpFilterable
89
107
 
90
- restrict_ip :sensitive_action,
108
+ filter_ip :sensitive_action,
91
109
  allowed_ips: -> { Rails.application.credentials.dig(:allowed_ips) || [] }
92
110
  end
93
111
  ```
@@ -100,7 +118,7 @@ Customize the response when access is denied. The block is executed via `instanc
100
118
  class ApiController < ApplicationController
101
119
  include ActionIpFilter::IpFilterable
102
120
 
103
- restrict_ip :create,
121
+ filter_ip :create,
104
122
  allowed_ips: %w[192.0.2.0/24],
105
123
  on_denied: -> { render json: { error: "Access denied from #{request.remote_ip}" }, status: :forbidden }
106
124
  end
@@ -114,7 +132,7 @@ Configure global settings in an initializer:
114
132
  # config/initializers/action_ip_filter.rb
115
133
  ActionIpFilter.configure do |config|
116
134
  # Custom IP resolver (default: request.remote_ip)
117
- config.ip_resolver = ->(request) {
135
+ config.ip_resolver = -> {
118
136
  request.headers["X-Forwarded-For"]&.split(",")&.first&.strip || request.remote_ip
119
137
  }
120
138
 
@@ -133,7 +151,7 @@ end
133
151
 
134
152
  | Option | Default | Description |
135
153
  |--------|-------------------------------------|----------------------------------------------------|
136
- | `ip_resolver` | `->(request) { request.remote_ip }` | Proc that extracts client IP from request |
154
+ | `ip_resolver` | `-> { request.remote_ip }` | Proc that extracts client IP from request |
137
155
  | `on_denied` | `-> { head :forbidden }` | Handler called when access is denied (returns 403) |
138
156
  | `logger` | `Rails.logger` | Logger instance for denied request logging |
139
157
  | `log_denials` | `true` | Whether to log denied requests as warn level |
@@ -2,19 +2,19 @@
2
2
 
3
3
  module ActionIpFilter
4
4
  class Configuration
5
- # @rbs @ip_resolver: ^(ActionDispatch::Request) -> String?
5
+ # @rbs @ip_resolver: ^() -> String?
6
6
  # @rbs @on_denied: ^() -> void
7
7
  # @rbs @logger: Logger?
8
8
  # @rbs @log_denials: bool
9
9
 
10
- attr_accessor :ip_resolver #: ^(ActionDispatch::Request) -> String?
10
+ attr_accessor :ip_resolver #: ^() -> String?
11
11
  attr_accessor :on_denied #: ^() -> void
12
12
  attr_accessor :logger #: Logger?
13
13
  attr_accessor :log_denials #: bool
14
14
 
15
15
  # @rbs return: void
16
16
  def initialize
17
- @ip_resolver = ->(request) { request.remote_ip }
17
+ @ip_resolver = -> { request.remote_ip } # steep:ignore NoMethod
18
18
  @on_denied = -> { head :forbidden } # steep:ignore NoMethod
19
19
  @logger = nil
20
20
  @log_denials = true
@@ -26,7 +26,7 @@ module ActionIpFilter
26
26
  # @rbs allowed_ips: Array[String] | ^() -> Array[String]
27
27
  # @rbs on_denied: (^() -> void)?
28
28
  # @rbs return: void
29
- def restrict_ip(*actions, allowed_ips:, on_denied: nil)
29
+ def filter_ip(*actions, allowed_ips:, on_denied: nil)
30
30
  actions.flatten.each do |action|
31
31
  self.action_ip_restrictions = action_ip_restrictions.merge(action.to_sym => {allowed_ips:, on_denied:})
32
32
  before_action -> { check_ip_restriction(action) }, only: action
@@ -37,7 +37,7 @@ module ActionIpFilter
37
37
  # @rbs except: Array[Symbol]
38
38
  # @rbs on_denied: (^() -> void)?
39
39
  # @rbs return: void
40
- def restrict_ip_for_all(allowed_ips:, except: [], on_denied: nil)
40
+ def filter_ip_for_all(allowed_ips:, except: [], on_denied: nil)
41
41
  # note: hyphen is not allowed in method (i.e., action) names, so it's safe to use it as a marker
42
42
  self.action_ip_restrictions = action_ip_restrictions.merge("all-marker": {allowed_ips:, on_denied:})
43
43
  before_action :check_ip_restriction_for_all, except:
@@ -62,7 +62,7 @@ module ActionIpFilter
62
62
  def verify_ip_access(restriction)
63
63
  return if restriction.nil? || ActionIpFilter.test_mode?
64
64
 
65
- client_ip = ActionIpFilter.configuration.ip_resolver.call(request)
65
+ client_ip = instance_exec(&ActionIpFilter.configuration.ip_resolver) #: String?
66
66
  allowed_ips = resolve_allowed_ips(restriction[:allowed_ips])
67
67
 
68
68
  unless IpMatcher.allowed?(client_ip, allowed_ips)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionIpFilter
4
- VERSION = "0.1.1" #: String
4
+ VERSION = "0.3.0" #: String
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_ip_filter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SmartBank, Inc.