rack-restrict_access 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|