rack-restrict_access 0.0.1
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 +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +177 -0
- data/Rakefile +1 -0
- data/lib/rack/restrict_access.rb +313 -0
- data/lib/rack/restrict_access/version.rb +5 -0
- data/rack-restrict_access.gemspec +29 -0
- data/spec/allow_filter_spec.rb +54 -0
- data/spec/block_filter_spec.rb +82 -0
- data/spec/filter_spec.rb +392 -0
- data/spec/restrict_access_spec.rb +311 -0
- data/spec/restrict_filter_spec.rb +175 -0
- data/spec/spec_helper.rb +2 -0
- metadata +179 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e253b537840c9ba69e36d3d18e155534e6c5bbc7
|
4
|
+
data.tar.gz: dee32c0bc384fba84d18c5d1597b9f23234627bc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e5df594610789f4a7e777a7cc6d902c06852532c857d6fcad35e492b97406b0b7c1e88d9e6d1ae4730a595f600eac7b4dc8d80e595004e0423d2083dd49d0664
|
7
|
+
data.tar.gz: fce72b5f59e0dd3b3526b8331112e82f2203adc697d09ad6d1aadfb53786ef985fb3b2ded766e2148b96a4590433bf75c2ca71b11727e235bc97f7170956d433
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Mainor Claros
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
# Rack::RestrictAccess
|
2
|
+
|
3
|
+
Compares env 'REMOTE_ADDR' and 'PATH_INFO' against user-defined values to _block_ (403), _restrict_ (401 basic auth), or _allow_ access to the rack app.
|
4
|
+
|
5
|
+
Intended for use in simple access control. Should not be considered a security solution.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'rack-restrict_access'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install rack-restrict_access
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Include in application.rb
|
24
|
+
|
25
|
+
```
|
26
|
+
# /config/application.rb
|
27
|
+
|
28
|
+
module ApplicationName
|
29
|
+
class Application < Rails::Application
|
30
|
+
|
31
|
+
...
|
32
|
+
|
33
|
+
config.middleware.use Rack::RestrictAccess do
|
34
|
+
restrict do
|
35
|
+
all_resources!
|
36
|
+
# single/multiple delimited string(s), regexp(s), or array(s) of both
|
37
|
+
credentials ENV['RESTRICTED_ACCESS_CREDENTIALS'], :delimiter => ","
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
# single/multiple delimited string(s), regexp(s), or array(s) of both
|
42
|
+
allow do
|
43
|
+
origin_ips ENV['ALLOWED_IPS'], "0.0.0.0", /192.168.\d{1}.\d.\d{1}$/, :delimiter => ","
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
...
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
### DSL
|
54
|
+
|
55
|
+
There are three kinds of filters: `block`, `restrict`, and `allow`. They apply to `resources` (site paths) and `origin_ips` (REMOTE_ADDR/IP of requester), which the user designates.
|
56
|
+
|
57
|
+
#### `restrict`
|
58
|
+
|
59
|
+
Enforces HTTP Authentication for items passed through a block.
|
60
|
+
|
61
|
+
```
|
62
|
+
# require login the entire site
|
63
|
+
|
64
|
+
restrict do
|
65
|
+
all_resources!
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
```
|
70
|
+
# restrict access to the specific path "/secret-place", and paths starting with "/admin"
|
71
|
+
|
72
|
+
restrict do
|
73
|
+
resources /^\/admin\/.*/, "/secret-place"
|
74
|
+
# also supports array(s) of strings/regexps
|
75
|
+
# use :delimiter => STRING/REGEXP for delimited strings (also applies to strings in arrays)
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
```
|
80
|
+
#restrict access to any part of the site for the follolowing IPs
|
81
|
+
|
82
|
+
restrict do
|
83
|
+
origin_ips "192.168.1.1,192.169.9.9", :delimiter => "," # or Regexps
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
```
|
88
|
+
#or both!
|
89
|
+
|
90
|
+
restrict do
|
91
|
+
# block access to the specific path "/secret-place", and paths starting with "/admin"
|
92
|
+
resources /^\/admin\/*/, "/secret-place"
|
93
|
+
|
94
|
+
# also block access to anything for these IPs:
|
95
|
+
origins "192.168.1.1", "192.169.9.9"
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
##### Credentials
|
100
|
+
|
101
|
+
Designate one or multiple valid username/password combinations. Can be single/multiple delimited strings, hashes (where :username and :password are set to a string or regexp), or arrays of strings
|
102
|
+
|
103
|
+
|
104
|
+
```
|
105
|
+
restrict do
|
106
|
+
all_resources!
|
107
|
+
|
108
|
+
#delimiter options required if dealing with delimited strings:
|
109
|
+
credentials "stewie:coolwhip|brian:novel" :credentials_delimiter => ":", :credential_pair_delimiter => "|"
|
110
|
+
|
111
|
+
#delimiter options default to :credentials => "," and :credential_pair_delimiter => ";" , respectively
|
112
|
+
|
113
|
+
#or single/multiple hashes:
|
114
|
+
credentials :username => "admin", :password => "password123"
|
115
|
+
end
|
116
|
+
```
|
117
|
+
|
118
|
+
#### `block`
|
119
|
+
|
120
|
+
Returns _403 FORBIDDEN_ for items passed through a block. Use same as `restrict`.
|
121
|
+
|
122
|
+
```
|
123
|
+
# block all the things!
|
124
|
+
block do
|
125
|
+
all_resources!
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
```
|
130
|
+
block do
|
131
|
+
# block access to the specific path "/secret-place", and paths starting with "/admin"
|
132
|
+
|
133
|
+
resources /^\/admin\/*/, "/secret-place"
|
134
|
+
|
135
|
+
# also supports array(s) of strings/regexps
|
136
|
+
# use :delimiter => STRING/REGEXP for delimited strings (also applies to strings in arrays)
|
137
|
+
|
138
|
+
# also block access to anything for these IPs:
|
139
|
+
origin_ips "192.168.1.1", "192.169.9.9"
|
140
|
+
end
|
141
|
+
|
142
|
+
```
|
143
|
+
|
144
|
+
You may also designate a custom HTTP status code and response header, body for blocked resources
|
145
|
+
|
146
|
+
```
|
147
|
+
block do
|
148
|
+
all_resources!
|
149
|
+
|
150
|
+
status_code 401 #default is 403
|
151
|
+
|
152
|
+
body ["You shall not pass!"]
|
153
|
+
#body must respond to :each. Default is ["<h1>Forbidden</h1>"]
|
154
|
+
end
|
155
|
+
```
|
156
|
+
|
157
|
+
#### `allow`
|
158
|
+
|
159
|
+
Create exceptions for particular paths/IPs. These exceptions override `block` and `restrict` filters.
|
160
|
+
|
161
|
+
```
|
162
|
+
allow do
|
163
|
+
#make this path open to anyone
|
164
|
+
resources "/index"
|
165
|
+
|
166
|
+
#allow this ip to bypass block/basic auth
|
167
|
+
origin_ips "192.168.0.1"
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
## Contributing
|
172
|
+
|
173
|
+
1. Fork it ( http://github.com/<my-github-username>/rack-restrict_access/fork )
|
174
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
175
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
176
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
177
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,313 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "rack/restrict_access/version"
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
class RestrictAccess
|
6
|
+
def initialize(app, &blk)
|
7
|
+
@app = app
|
8
|
+
@options = {enabled: true, auth: true}
|
9
|
+
|
10
|
+
if block_given?
|
11
|
+
instance_eval(&blk)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def options(options_hash)
|
16
|
+
@options.merge!(options_hash)
|
17
|
+
end
|
18
|
+
|
19
|
+
def app_enabled?
|
20
|
+
@options[:enabled] == true
|
21
|
+
end
|
22
|
+
|
23
|
+
def auth_enabled?
|
24
|
+
@options[:auth] == true
|
25
|
+
end
|
26
|
+
|
27
|
+
def block(&blk)
|
28
|
+
filter = BlockFilter.new
|
29
|
+
block_filters << filter
|
30
|
+
filter.instance_eval(&blk)
|
31
|
+
end
|
32
|
+
|
33
|
+
def restrict(&blk)
|
34
|
+
filter = RestrictFilter.new
|
35
|
+
restrict_filters << filter
|
36
|
+
filter.instance_eval(&blk)
|
37
|
+
end
|
38
|
+
|
39
|
+
def allow(&blk)
|
40
|
+
filter = AllowFilter.new
|
41
|
+
allow_filters << filter
|
42
|
+
filter.instance_eval(&blk)
|
43
|
+
end
|
44
|
+
|
45
|
+
def call(env)
|
46
|
+
return success_response(env) unless app_enabled?
|
47
|
+
request = Rack::Request.new(env)
|
48
|
+
path = request.path
|
49
|
+
origin = request.ip
|
50
|
+
|
51
|
+
exception = allow_filters.detect { |filter| filter.allows_resource?(path) || filter.allows_ip?(origin) }
|
52
|
+
return success_response(env) if exception
|
53
|
+
|
54
|
+
blocker = block_filters.detect { |filter| filter.blocks_resource?(path) || filter.blocks_ip?(origin) }
|
55
|
+
return blocked_response(blocker) if blocker
|
56
|
+
|
57
|
+
if auth_enabled?
|
58
|
+
restrictor = restrict_filters.detect { |filter| filter.restricts_resource?(path) || filter.restricts_ip?(origin) }
|
59
|
+
return restricted_response(env, restrictor) if restrictor && restrictor.credentials_count > 0
|
60
|
+
end
|
61
|
+
|
62
|
+
success_response(env)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def block_filters
|
67
|
+
@block_filters ||= []
|
68
|
+
end
|
69
|
+
|
70
|
+
def restrict_filters
|
71
|
+
@restrict_filters ||= []
|
72
|
+
end
|
73
|
+
|
74
|
+
def allow_filters
|
75
|
+
@allow_filters ||= []
|
76
|
+
end
|
77
|
+
|
78
|
+
def success_response(env)
|
79
|
+
@app.call(env)
|
80
|
+
end
|
81
|
+
|
82
|
+
def blocked_response(block_filter)
|
83
|
+
content_type = {"Content-Type" => "text/html"}
|
84
|
+
body = block_filter.instance_variable_get(:@body)
|
85
|
+
code = block_filter.instance_variable_get(:@status_code)
|
86
|
+
[code, content_type, body]
|
87
|
+
end
|
88
|
+
|
89
|
+
def restricted_response(env, restrict_filter)
|
90
|
+
auth = Rack::Auth::Basic.new(@app) do |uname, pass|
|
91
|
+
restrict_filter.credentials_match?(username: uname, password: pass)
|
92
|
+
end
|
93
|
+
auth.call(env)
|
94
|
+
end
|
95
|
+
|
96
|
+
class Filter
|
97
|
+
attr_reader :filter_type
|
98
|
+
|
99
|
+
def initialize(&blk)
|
100
|
+
@ips = []
|
101
|
+
@resources = []
|
102
|
+
@applies_to_all_resources = false
|
103
|
+
|
104
|
+
if block_given?
|
105
|
+
instance_eval(&blk)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def applies_to_resource?(requested_resource)
|
110
|
+
return true if @applies_to_all_resources
|
111
|
+
raise TypeError, requested_resource unless requested_resource.respond_to? :to_str
|
112
|
+
requested_resource = requested_resource.to_str
|
113
|
+
@resources.any? do |resource|
|
114
|
+
!(requested_resource =~ resource).nil?
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def applies_to_ip?(remote_addr)
|
119
|
+
raise TypeError, remote_addr unless remote_addr.respond_to? :to_str
|
120
|
+
remote_addr = remote_addr.to_str
|
121
|
+
@ips.any? do |ip|
|
122
|
+
!(remote_addr =~ ip).nil?
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def path_to_regexp(path)
|
127
|
+
if path.respond_to? :to_str
|
128
|
+
/^#{Regexp.escape(path)}\/?$/
|
129
|
+
elsif path.respond_to? :match
|
130
|
+
path
|
131
|
+
else
|
132
|
+
raise TypeError, path
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def all_resources!
|
137
|
+
@applies_to_all_resources = true
|
138
|
+
end
|
139
|
+
|
140
|
+
def resources(*paths)
|
141
|
+
options = paths.pop if paths.last.is_a? Hash
|
142
|
+
options ||= {}
|
143
|
+
delimiter = options.fetch(:delimiter, false)
|
144
|
+
|
145
|
+
concat_new_attributes(args: paths, ivar: @resources) do |path|
|
146
|
+
path = path.split(delimiter) if delimiter && path.is_a?(String)
|
147
|
+
if path.is_a? Array
|
148
|
+
path.map! { |pt| path_to_regexp(pt) }
|
149
|
+
else
|
150
|
+
path_to_regexp(path)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def ip_to_regexp(ip)
|
156
|
+
if ip.respond_to? :to_str
|
157
|
+
/^#{Regexp.escape(ip)}$/
|
158
|
+
elsif ip.respond_to? :match
|
159
|
+
ip
|
160
|
+
else
|
161
|
+
raise TypeError, ip
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def origin_ips(*ips)
|
166
|
+
options = ips.pop if ips.last.is_a? Hash
|
167
|
+
options ||= {}
|
168
|
+
delimiter = options.fetch(:delimiter, false)
|
169
|
+
|
170
|
+
concat_new_attributes(args: ips, ivar: @ips) do |ip|
|
171
|
+
ip = ip.split(delimiter) if delimiter && ip.is_a?(String)
|
172
|
+
if ip.is_a? Array
|
173
|
+
ip.map! { |addr| ip_to_regexp(addr) }
|
174
|
+
else
|
175
|
+
ip_to_regexp(ip)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
def concat_new_attributes(options, &blk)
|
182
|
+
args = options.fetch(:args, [])
|
183
|
+
ivar = options.fetch(:ivar, nil)
|
184
|
+
raise ArgumentError, "Missing :ivar option" unless ivar
|
185
|
+
values_to_save = args.flatten.map do |arg|
|
186
|
+
blk.call(arg)
|
187
|
+
end.flatten
|
188
|
+
ivar.concat(values_to_save)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
class AllowFilter < Filter
|
193
|
+
def allows_resource?(resource)
|
194
|
+
applies_to_resource?(resource)
|
195
|
+
end
|
196
|
+
|
197
|
+
def allows_ip?(ip)
|
198
|
+
applies_to_ip?(ip)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
class BlockFilter < Filter
|
203
|
+
def initialize
|
204
|
+
@status_code = 403
|
205
|
+
@body = ["<h1>Forbidden</h1>"]
|
206
|
+
super
|
207
|
+
end
|
208
|
+
|
209
|
+
def body(enumerable)
|
210
|
+
raise ArgumentError, "Body must respond to #each" unless enumerable.respond_to? :each
|
211
|
+
@body = enumerable
|
212
|
+
end
|
213
|
+
|
214
|
+
def status_code(int)
|
215
|
+
@status_code = int.to_i
|
216
|
+
end
|
217
|
+
|
218
|
+
def blocks_resource?(path)
|
219
|
+
applies_to_resource?(path)
|
220
|
+
end
|
221
|
+
|
222
|
+
def blocks_ip?(ip)
|
223
|
+
applies_to_ip?(ip)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
class RestrictFilter < Filter
|
228
|
+
def initialize
|
229
|
+
@credentials = []
|
230
|
+
super
|
231
|
+
end
|
232
|
+
|
233
|
+
def restricts_resource?(path)
|
234
|
+
applies_to_resource?(path)
|
235
|
+
end
|
236
|
+
|
237
|
+
def restricts_ip?(ip)
|
238
|
+
applies_to_ip?(ip)
|
239
|
+
end
|
240
|
+
|
241
|
+
def credentials(*creds)
|
242
|
+
if !creds.first.is_a?(Hash) && creds.last.is_a?(Hash)
|
243
|
+
options = creds.pop
|
244
|
+
end
|
245
|
+
options ||= {}
|
246
|
+
|
247
|
+
concat_new_attributes(args: creds, ivar: @credentials) do |credential_pair|
|
248
|
+
if credential_pair.is_a? Hash
|
249
|
+
creds_from_hash(credential_pair)
|
250
|
+
elsif credential_pair.is_a? String
|
251
|
+
creds_from_string(credential_pair, options)
|
252
|
+
elsif credential_pair.is_a? Array
|
253
|
+
creds_from_array(credential_pair, options)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def credentials_match?(creds_hash)
|
259
|
+
@credentials.any? do |saved_hash|
|
260
|
+
saved_u = saved_hash[:username]
|
261
|
+
saved_p = saved_hash[:password]
|
262
|
+
given_u = creds_hash[:username]
|
263
|
+
given_p = creds_hash[:password]
|
264
|
+
!(given_u =~ saved_u).nil? && !(given_p =~ saved_p).nil?
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def credentials_count
|
269
|
+
@credentials.length
|
270
|
+
end
|
271
|
+
|
272
|
+
private
|
273
|
+
def cred_to_regexp(cred)
|
274
|
+
if cred.respond_to? :to_str
|
275
|
+
/^#{Regexp.escape(cred)}$/
|
276
|
+
elsif cred.respond_to? :match
|
277
|
+
cred
|
278
|
+
else
|
279
|
+
raise TypeError, cred
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def creds_from_hash(hash)
|
284
|
+
{
|
285
|
+
username: cred_to_regexp(hash[:username]),
|
286
|
+
password: cred_to_regexp(hash[:password])
|
287
|
+
}
|
288
|
+
end
|
289
|
+
|
290
|
+
def creds_from_array(array)
|
291
|
+
array.map do |el|
|
292
|
+
creds_from_string(el, options)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def creds_from_string(string, options = {})
|
297
|
+
string = string.to_str
|
298
|
+
credentials_delimiter = options.fetch(:credentials_delimiter, ',')
|
299
|
+
credential_pair_delimiter = options.fetch(:credential_pair_delimiter, ';')
|
300
|
+
creds = string.split(credential_pair_delimiter)
|
301
|
+
creds.map do |str|
|
302
|
+
cred_pair = str.split(credentials_delimiter)
|
303
|
+
{
|
304
|
+
username: cred_to_regexp(cred_pair[0]),
|
305
|
+
password: cred_to_regexp(cred_pair[1])
|
306
|
+
}
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|