action_ip_filter 0.3.0 → 0.4.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: af3d4fbb598b8802bc90a29a75a0f6f3d38cd77757a04c8bef538fe34172ba14
4
- data.tar.gz: 13e2e5ed9b621b70c27f11046149643d7237fe977766e29722f9f293c5ebe46d
3
+ metadata.gz: dd87d049abbc429bef20f3c4d684efd1f94a97cf9758603e6a404ecc97c487c0
4
+ data.tar.gz: 1460f97edeb6ddd39c71190ec07d866c3bc473818ab2ae852624a034027b1d7d
5
5
  SHA512:
6
- metadata.gz: 578ca535fedf30d89e7681b286f2d68518cef4a001802d053cd49cc1a42cf9e9f85d7628230b4a28ddf7c54cc6225a158ce4f7f161d8f09c0fb55833adafb154
7
- data.tar.gz: 1ba8977aa885e48794147c6c09c1d4c4bbd71f35974371cfb5ef8c75ea47efbf179480fea9f1c6259ea57ac7fdb0fc58e562969a230815bec857aa109da97551
6
+ metadata.gz: 7e279fadffc67defc41e2447f985780fc0332022a1bce62de27b065f9e715bbf18d173f8c87aa72780d2b8b91b743d45130608235f0b6d7c42cdfa24b699518e
7
+ data.tar.gz: e2918f5170516b232838a516e0ddf7c80f81cd5ef7c9359a1c07820e9ec96d1a8d91f9579824df7b535b24064596cd0da2e1d046db63146a4d3c524eeea9c0cb
data/CHANGELOG.md CHANGED
@@ -1,10 +1,38 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2025-12-01
4
+
5
+ ### Breaking Changes
6
+
7
+ #### Change both the behavior and the interface of `filter_ip` [#5](https://github.com/smartbank-inc/action_ip_filter/pull/5)
8
+
9
+ Previously, `filter_ip` accepted a variable number of action names along with an `allowed_ips:` parameter. It now instead accepts a variable number of allowed IP addresses.
10
+
11
+ When neither `only:` nor `except:` is specified, the filter is applied to all actions in the controller. To restric filtering to specific actions, `only:` or `except:` must be used—similar to how `before_action` is scoped.
12
+
13
+ **Example:**
14
+
15
+ Before:
16
+
17
+ ```ruby
18
+ filter_ip :index, :show, allowed_ips: %w[192.0.2.0/24 198.51.100.1]
19
+ ```
20
+
21
+ After:
22
+
23
+ ```ruby
24
+ filter_ip "192.0.2.0/24", "198.51.100.1", only: [:index, :show]
25
+ ```
26
+
27
+ ## [0.3.1] - 2025-12-01
28
+
29
+ - Simplify IpMatcher#match? by removing unnecessary branching [#3](https://github.com/smartbank-inc/action_ip_filter/pull/3)
30
+
3
31
  ## [0.3.0] - 2025-11-29
4
32
 
5
33
  ### Breaking Changes
6
34
 
7
- #### Change `ip_resolver` to use controller context instead of request parameter
35
+ #### Change `ip_resolver` to use controller context instead of request parameter [#1](https://github.com/smartbank-inc/action_ip_filter/pull/1)
8
36
 
9
37
  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
38
 
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # action_ip_filter
1
+ # action_ip_filter [![CI](https://github.com/smartbank-inc/action_ip_filter/actions/workflows/ci.yml/badge.svg)](https://github.com/smartbank-inc/action_ip_filter/actions/workflows/ci.yml) [![Gem Version](https://img.shields.io/gem/v/action_ip_filter)](https://rubygems.org/gems/action_ip_filter)
2
2
 
3
3
  A lightweight gem that provides IP address restrictions for Rails controllers at the action level.
4
4
 
@@ -54,13 +54,13 @@ bundle install
54
54
 
55
55
  ### Basic Usage
56
56
 
57
- Include the concern and use `filter_ip` to protect specific actions:
57
+ Include the concern and use `filter_ip` to protect all actions when neither `:only` nor `:except` is specified:
58
58
 
59
59
  ```ruby
60
60
  class AdminController < ApplicationController
61
61
  include ActionIpFilter::IpFilterable
62
62
 
63
- filter_ip :index, :show, allowed_ips: %w[192.0.2.0/24 198.51.100.1]
63
+ filter_ip "192.0.2.0/24", "198.51.100.1"
64
64
 
65
65
  def index
66
66
  # Only accessible from 192.0.2.0/24 or 198.51.100.1
@@ -71,27 +71,50 @@ class AdminController < ApplicationController
71
71
  end
72
72
 
73
73
  def public_action
74
- # Not restricted
74
+ # Also restricted
75
75
  end
76
76
  end
77
77
  ```
78
78
 
79
- ### Restrict All Actions
79
+ #### With modifiers
80
+
81
+ ```ruby
82
+ class AdminController < ApplicationController
83
+ include ActionIpFilter::IpFilterable
84
+
85
+ filter_ip "192.0.2.0/24", "198.51.100.1", only: [:index, :show]
86
+
87
+ def index
88
+ # Only accessible from 192.0.2.0/24 or 198.51.100.1
89
+ end
90
+
91
+ def show
92
+ # Also restricted
93
+ end
94
+
95
+ def public_action
96
+ # Not restricted
97
+ end
98
+ end
99
+ ```
80
100
 
81
- Use `filter_ip_for_all` to protect all actions with optional exceptions:
101
+ The above example is functionally equivalent to the following:
82
102
 
83
103
  ```ruby
84
- class WebhooksController < ApplicationController
104
+ class AdminController < ApplicationController
85
105
  include ActionIpFilter::IpFilterable
86
106
 
87
- filter_ip_for_all allowed_ips: ENV["WEBHOOK_ALLOWED_IPS"].to_s.split(","),
88
- except: [:health_check]
107
+ filter_ip "192.0.2.0/24", "198.51.100.1", except: [:public_action]
108
+
109
+ def index
110
+ # Only accessible from 192.0.2.0/24 or 198.51.100.1
111
+ end
89
112
 
90
- def stripe
91
- # Restricted
113
+ def show
114
+ # Also restricted
92
115
  end
93
116
 
94
- def health_check
117
+ def public_action
95
118
  # Not restricted
96
119
  end
97
120
  end
@@ -105,8 +128,19 @@ Pass a Proc for dynamic IP resolution:
105
128
  class SecureController < ApplicationController
106
129
  include ActionIpFilter::IpFilterable
107
130
 
108
- filter_ip :sensitive_action,
109
- allowed_ips: -> { Rails.application.credentials.dig(:allowed_ips) || [] }
131
+ filter_ip -> { Rails.application.credentials.dig(:allowed_ips) || [] }
132
+ end
133
+ ```
134
+
135
+ The Proc must return a value of type `Array[String]`.
136
+
137
+ You can also combine static IPs with dynamic resolution:
138
+
139
+ ```ruby
140
+ class SecureController < ApplicationController
141
+ include ActionIpFilter::IpFilterable
142
+
143
+ filter_ip "192.0.2.0/24", -> { Rails.application.credentials.dig(:allowed_ips) || [] }
110
144
  end
111
145
  ```
112
146
 
@@ -118,8 +152,7 @@ Customize the response when access is denied. The block is executed via `instanc
118
152
  class ApiController < ApplicationController
119
153
  include ActionIpFilter::IpFilterable
120
154
 
121
- filter_ip :create,
122
- allowed_ips: %w[192.0.2.0/24],
155
+ filter_ip "192.0.2.0/24",
123
156
  on_denied: -> { render json: { error: "Access denied from #{request.remote_ip}" }, status: :forbidden }
124
157
  end
125
158
  ```
@@ -6,80 +6,46 @@ module ActionIpFilter
6
6
  module IpFilterable
7
7
  extend ActiveSupport::Concern
8
8
 
9
- included do
10
- class_attribute :action_ip_restrictions, default: {}
11
- end
12
-
13
9
  # @rbs!
14
- # type restriction_options = { allowed_ips: Array[String] | ^() -> Array[String], on_denied: (^() -> void)? }
15
- # def action_ip_restrictions: () -> Hash[Symbol, untyped]
16
- # def action_ip_restrictions=: (Hash[Symbol, untyped]) -> void
17
-
18
- # @rbs!
19
- # def request: () -> ActionDispatch::Request
20
10
  # def action_name: () -> String
21
- # def instance_exec: (*untyped) { () -> void } -> void
11
+ # def instance_exec: [T] (*untyped) { () -> T } -> T
22
12
  # def before_action: (*untyped, **untyped) -> void
23
13
 
24
14
  class_methods do
25
- # @rbs *actions: Symbol
26
- # @rbs allowed_ips: Array[String] | ^() -> Array[String]
27
- # @rbs on_denied: (^() -> void)?
28
- # @rbs return: void
29
- def filter_ip(*actions, allowed_ips:, on_denied: nil)
30
- actions.flatten.each do |action|
31
- self.action_ip_restrictions = action_ip_restrictions.merge(action.to_sym => {allowed_ips:, on_denied:})
32
- before_action -> { check_ip_restriction(action) }, only: action
33
- end
34
- end
35
-
36
- # @rbs allowed_ips: Array[String] | ^() -> Array[String]
37
- # @rbs except: Array[Symbol]
15
+ # @rbs allowed_ips: String | ^() -> Array[String]
38
16
  # @rbs on_denied: (^() -> void)?
17
+ # @rbs only: Array[Symbol]?
18
+ # @rbs except: Array[Symbol]?
39
19
  # @rbs return: void
40
- def filter_ip_for_all(allowed_ips:, except: [], on_denied: nil)
41
- # note: hyphen is not allowed in method (i.e., action) names, so it's safe to use it as a marker
42
- self.action_ip_restrictions = action_ip_restrictions.merge("all-marker": {allowed_ips:, on_denied:})
43
- before_action :check_ip_restriction_for_all, except:
20
+ def filter_ip(*allowed_ips, on_denied: nil, only: nil, except: nil)
21
+ before_action -> { verify_ip_access(allowed_ips:, on_denied:) }, only:, except:
44
22
  end
45
23
  end
46
24
 
47
25
  private
48
26
 
49
- # @rbs action: Symbol
27
+ # @rbs allowed_ips: Array[String | ^() -> Array[String]]
28
+ # @rbs on_denied: (^() -> void)?
50
29
  # @rbs return: void
51
- def check_ip_restriction(action)
52
- verify_ip_access(action_ip_restrictions[action.to_sym])
53
- end
30
+ def verify_ip_access(allowed_ips:, on_denied:)
31
+ return if ActionIpFilter.test_mode?
54
32
 
55
- # @rbs return: void
56
- def check_ip_restriction_for_all
57
- verify_ip_access(action_ip_restrictions[:"all-marker"])
58
- end
59
-
60
- # @rbs restriction: restriction_options?
61
- # @rbs return: void
62
- def verify_ip_access(restriction)
63
- return if restriction.nil? || ActionIpFilter.test_mode?
33
+ client_ip = instance_exec(&ActionIpFilter.configuration.ip_resolver)
64
34
 
65
- client_ip = instance_exec(&ActionIpFilter.configuration.ip_resolver) #: String?
66
- allowed_ips = resolve_allowed_ips(restriction[:allowed_ips])
35
+ allowed = allowed_ips.any? do |allowed_ip|
36
+ ips = case allowed_ip
37
+ when Proc
38
+ instance_exec(&allowed_ip)
39
+ else
40
+ [allowed_ip]
41
+ end
67
42
 
68
- unless IpMatcher.allowed?(client_ip, allowed_ips)
69
- log_ip_denial(client_ip)
70
- on_denied = restriction[:on_denied] || ActionIpFilter.configuration.on_denied
71
- instance_exec(&on_denied)
43
+ IpMatcher.allowed?(client_ip, ips)
72
44
  end
73
- end
74
45
 
75
- # @rbs allowed_ips: Array[String] | ^() -> Array[String]
76
- # @rbs return: Array[String]
77
- def resolve_allowed_ips(allowed_ips)
78
- case allowed_ips
79
- when Proc
80
- instance_exec(&allowed_ips) #: Array[String]
81
- else
82
- Array(allowed_ips)
46
+ unless allowed
47
+ log_ip_denial(client_ip)
48
+ instance_exec(&on_denied || ActionIpFilter.configuration.on_denied)
83
49
  end
84
50
  end
85
51
 
@@ -33,13 +33,8 @@ module ActionIpFilter
33
33
  # @rbs allowed_ip: String
34
34
  # @rbs return: bool
35
35
  def match?(client_addr, allowed_ip)
36
- if allowed_ip.include?("/")
37
- range = IPAddr.new(allowed_ip)
38
- range.include?(client_addr)
39
- else
40
- allowed_addr = IPAddr.new(allowed_ip)
41
- client_addr == allowed_addr
42
- end
36
+ allowed_addr = IPAddr.new(allowed_ip)
37
+ allowed_addr.include?(client_addr)
43
38
  rescue IPAddr::InvalidAddressError
44
39
  false
45
40
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionIpFilter
4
- VERSION = "0.3.0" #: String
4
+ VERSION = "0.4.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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SmartBank, Inc.