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 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
+