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 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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rack-restrict_access.gemspec
4
+ gemspec
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
+