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 +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +27 -9
- data/lib/action_ip_filter/configuration.rb +3 -3
- data/lib/action_ip_filter/ip_filterable.rb +3 -3
- data/lib/action_ip_filter/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af3d4fbb598b8802bc90a29a75a0f6f3d38cd77757a04c8bef538fe34172ba14
|
|
4
|
+
data.tar.gz: 13e2e5ed9b621b70c27f11046149643d7237fe977766e29722f9f293c5ebe46d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 `
|
|
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
|
-
|
|
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 `
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = ->
|
|
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` | `->
|
|
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: ^(
|
|
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 #: ^(
|
|
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 = ->
|
|
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
|
|
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
|
|
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
|
|
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)
|