mikldt-authenticates_access 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/MIT-LICENSE +20 -0
- data/README +86 -0
- data/Rakefile +39 -0
- data/VERSION +1 -0
- data/init.rb +6 -0
- data/install.rb +1 -0
- data/lib/authenticates_access.rb +440 -0
- data/mikldt-authenticates_access.gemspec +63 -0
- data/tasks/authenticates_access_tasks.rake +4 -0
- data/test/admin_item.rb +3 -0
- data/test/authenticates_access_test.rb +108 -0
- data/test/database.yml +5 -0
- data/test/fixtures/admin_items.yml +3 -0
- data/test/fixtures/owned_items.yml +16 -0
- data/test/fixtures/users.yml +23 -0
- data/test/owned_item.rb +7 -0
- data/test/test_helper.rb +75 -0
- data/test/user.rb +14 -0
- data/uninstall.rb +1 -0
- metadata +78 -0
data/.gitignore
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Andrew H. Armenia
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
AuthenticatesAccess
|
2
|
+
===================
|
3
|
+
|
4
|
+
AuthenticatesAccess can be used to implement model-based authentication and
|
5
|
+
authorization features in your application. It is based around the concept
|
6
|
+
of "accessors", or model objects which are used as tokens to access other
|
7
|
+
model objects. Accessors might be users, groups, or sessions.
|
8
|
+
AuthenticatesAccess allows the use of methods within the accessors or within
|
9
|
+
the accessed objects to determine whether certain actions should be allowed.
|
10
|
+
|
11
|
+
Example
|
12
|
+
=======
|
13
|
+
|
14
|
+
Models need to define the access restrictions which will apply. If the concept
|
15
|
+
of "ownership" is to be used, it is necessary to define which attribute
|
16
|
+
refers to the object's owner. The owner should fill the role of accessor
|
17
|
+
in the application.
|
18
|
+
|
19
|
+
class User < ActiveRecord::Base
|
20
|
+
# user has an is_admin attribute
|
21
|
+
|
22
|
+
# don't let non-admins change the is_admin attribute
|
23
|
+
authenticates_writes_to :is_admin, :with_accessor_method => :is_admin
|
24
|
+
|
25
|
+
# allow users to save their own profile
|
26
|
+
authenticates_saves :with => :allow_owner
|
27
|
+
|
28
|
+
# allow admins to save the profile as well
|
29
|
+
authenticates_saves :with_accessor_method => :is_admin
|
30
|
+
|
31
|
+
# note that ownership doesn't confer all privileges!
|
32
|
+
# has_owner :self means that the accessor's ID will be compared
|
33
|
+
# with this object's own ID for the allow_owner test.
|
34
|
+
has_owner :self
|
35
|
+
|
36
|
+
# also, allow admins to save any user profile
|
37
|
+
authenticates_saves :with_accessor_method => :is_admin
|
38
|
+
end
|
39
|
+
|
40
|
+
class Comment < ActiveRecord::Base
|
41
|
+
belongs_to :user
|
42
|
+
|
43
|
+
# allow users to edit their own comments (but not others)
|
44
|
+
|
45
|
+
# has_owner :user means that user.id will be compared to accessor.id
|
46
|
+
# for the allow_owner test to pass.
|
47
|
+
has_owner :user
|
48
|
+
|
49
|
+
# register the ownership test for any saves
|
50
|
+
authenticates_saves :with => :allow_owner
|
51
|
+
|
52
|
+
# this will also allow admins to edit any comments
|
53
|
+
authenticates_saves :with_accessor_method => :is_admin
|
54
|
+
|
55
|
+
# this makes the creating user the owner of the comment
|
56
|
+
autosets_owner_on_create
|
57
|
+
end
|
58
|
+
|
59
|
+
The application controller should set an accessor to be used:
|
60
|
+
|
61
|
+
class ApplicationController < ActionController::Base
|
62
|
+
before_filter :setup_accessor
|
63
|
+
|
64
|
+
protected
|
65
|
+
|
66
|
+
def setup_accessor
|
67
|
+
ActiveRecord::Base.accessor = logged_in_user
|
68
|
+
end
|
69
|
+
|
70
|
+
def logged_in_user
|
71
|
+
User.find(session[:user_id])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
The views may use methods to determine which attributes may currently
|
76
|
+
be written, or whether the object may be modified at all.
|
77
|
+
|
78
|
+
<% if @user.allowed_to_save(:is_admin) %>
|
79
|
+
<%= f.check_box :is_admin %>
|
80
|
+
<% end %>
|
81
|
+
|
82
|
+
<% if user.allowed_to_save %>
|
83
|
+
<%= link_to 'Edit', edit_user_path(user) %>
|
84
|
+
<% end %>
|
85
|
+
|
86
|
+
Copyright (c) 2009 Andrew H. Armenia, released under the MIT license.
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc 'Test the authenticates_access plugin.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.libs << 'test'
|
12
|
+
t.pattern = 'test/**/*_test.rb'
|
13
|
+
t.verbose = true
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Generate documentation for the authenticates_access plugin.'
|
17
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
18
|
+
rdoc.rdoc_dir = 'rdoc'
|
19
|
+
rdoc.title = 'AuthenticatesAccess'
|
20
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
21
|
+
rdoc.rdoc_files.include('README')
|
22
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
23
|
+
end
|
24
|
+
|
25
|
+
begin
|
26
|
+
require 'jeweler'
|
27
|
+
Jeweler::Tasks.new do |gemspec|
|
28
|
+
gemspec.name = "mikldt-authenticates_access"
|
29
|
+
gemspec.summary = "Model-based Authorization on Rails!"
|
30
|
+
gemspec.description = "Model-based read and write user authorization in Rails"
|
31
|
+
gemspec.email = "mikldt@gmail.com"
|
32
|
+
gemspec.homepage = "http://github.com/mikldt/authenticates_access"
|
33
|
+
gemspec.authors = ["Michael DiTore"]
|
34
|
+
end
|
35
|
+
Jeweler::GemcutterTasks.new
|
36
|
+
rescue LoadError
|
37
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
38
|
+
end
|
39
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
data/init.rb
ADDED
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,440 @@
|
|
1
|
+
# AuthenticatesAccess
|
2
|
+
|
3
|
+
module AuthenticatesAccess
|
4
|
+
class AuthMethod < Struct.new(:type, :name, :options)
|
5
|
+
end
|
6
|
+
|
7
|
+
class AuthMethodList < Array
|
8
|
+
def add_method(options)
|
9
|
+
if options[:with_accessor_method]
|
10
|
+
self << AuthMethod.new( :accessor, options[:with_accessor_method], options )
|
11
|
+
elsif options[:with]
|
12
|
+
self << AuthMethod.new( :model, options[:with], options )
|
13
|
+
else
|
14
|
+
fail "Either :with or :with_accessor_method must be specified"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def check(targets)
|
19
|
+
# start out assuming we have not passed any tests
|
20
|
+
# if one passes this gets set to true
|
21
|
+
passed = false
|
22
|
+
|
23
|
+
self.each do |method|
|
24
|
+
unless targets[method.type].nil?
|
25
|
+
target = targets[method.type]
|
26
|
+
if run_method(target, method.name.to_sym, method.options[:options])
|
27
|
+
passed = true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
passed
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
# Run a method on the object if it's available, otherwise return false.
|
37
|
+
def run_method(object, method, options)
|
38
|
+
if object.nil?
|
39
|
+
false
|
40
|
+
elsif object.respond_to?(method)
|
41
|
+
if options
|
42
|
+
object.send(method, options)
|
43
|
+
else
|
44
|
+
object.send(method)
|
45
|
+
end
|
46
|
+
else
|
47
|
+
false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
module ClassMethods
|
55
|
+
# Set an accessor to be used
|
56
|
+
def accessor=(accessor)
|
57
|
+
@@accessor = accessor
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return the accessor being used by the model classes
|
61
|
+
def accessor
|
62
|
+
@@accessor ||= nil
|
63
|
+
@@accessor
|
64
|
+
end
|
65
|
+
|
66
|
+
# Include the instance methods used to implement authentication
|
67
|
+
def authenticates_access
|
68
|
+
include InstanceMethods
|
69
|
+
end
|
70
|
+
|
71
|
+
# Used to require an authentication test to be passed on the accessor
|
72
|
+
# before the model may be saved or destroyed. If the test fails, an exception
|
73
|
+
# will be thrown. Multiple calls build a chain of tests. If any test
|
74
|
+
# passes, the accessor is considered authenticated. Attribute writes will
|
75
|
+
# also be disallowed if the object may not be saved by the accessor
|
76
|
+
#
|
77
|
+
# examples:
|
78
|
+
#
|
79
|
+
# authenticates_saves :with_accessor_method => :is_admin
|
80
|
+
# will only allow the object to be saved if the accessor's is_admin
|
81
|
+
# method returns true
|
82
|
+
#
|
83
|
+
# authenticates_saves :with => :allow_owner
|
84
|
+
# will only allow the object to be saved if its own allow_owner
|
85
|
+
# method returns true
|
86
|
+
#
|
87
|
+
def authenticates_saves(options={})
|
88
|
+
unless @save_method_list
|
89
|
+
authenticates_access
|
90
|
+
before_save :auth_save_filter
|
91
|
+
before_destroy :auth_save_filter
|
92
|
+
@save_method_list = AuthMethodList.new
|
93
|
+
end
|
94
|
+
|
95
|
+
@save_method_list.add_method(options)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Used to require that an authentication test is passed on the accessor
|
99
|
+
# before data may be read from the model.
|
100
|
+
def authenticates_reads(options={})
|
101
|
+
unless @read_method_list
|
102
|
+
authenticates_access
|
103
|
+
#Sadly, no easy way to block reads at this level
|
104
|
+
@read_method_list = AuthMethodList.new
|
105
|
+
end
|
106
|
+
|
107
|
+
@read_method_list.add_method(options)
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
# Used to specify that a given attribute should only be written to if the
|
112
|
+
# accessor passes a test. The test may be a method of the accessor or
|
113
|
+
# of the object itself, which should return a boolean value.. If the test
|
114
|
+
# fails, the attribute write will be ignored. Multiple calls build up
|
115
|
+
# a chain of tests: if any test in the chain passes, the accessor is
|
116
|
+
# considered authorized.
|
117
|
+
#
|
118
|
+
# Examples:
|
119
|
+
#
|
120
|
+
# authenticates_writes_to :is_admin, :with_accessor_method => :is_admin
|
121
|
+
# would only allow admins to grant or revoke the admin privileges of others
|
122
|
+
#
|
123
|
+
# authenticates_writes_to :title, :with => :allow_owner
|
124
|
+
# would only allow the owner of this object to edit its title
|
125
|
+
#
|
126
|
+
def authenticates_writes_to(attr, options={})
|
127
|
+
authenticates_access
|
128
|
+
@write_validation_map ||= {}
|
129
|
+
@write_validation_map[attr.to_s] ||= AuthMethodList.new
|
130
|
+
@write_validation_map[attr.to_s].add_method(options)
|
131
|
+
end
|
132
|
+
|
133
|
+
# Used to specify that a given attribute may only be read if the
|
134
|
+
# accessor passes a test. Behaves similarly to authenticates_writes_to
|
135
|
+
def authenticates_reads_from(attr, options={})
|
136
|
+
authenticates_access
|
137
|
+
@read_validation_map ||= {}
|
138
|
+
@read_validation_map[attr.to_s] ||= AuthMethodList.new
|
139
|
+
@read_validation_map[attr.to_s].add_method(options)
|
140
|
+
end
|
141
|
+
|
142
|
+
# You might use this one to only allow authenticated users to create objects
|
143
|
+
def authenticates_creation(options={})
|
144
|
+
unless @create_method_list
|
145
|
+
authenticates_access
|
146
|
+
before_create :auth_create_filter
|
147
|
+
@create_method_list = AuthMethodList.new
|
148
|
+
extend CreationControl
|
149
|
+
end
|
150
|
+
|
151
|
+
@create_method_list.add_method(options)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Used to specify that a given attribute represents an accessor that is
|
155
|
+
# an owner of this object. This creates an allow_owner method which
|
156
|
+
# may be used to allow the object's owner to save the object, or edit
|
157
|
+
# certain attributes, as well as an owner_id method which returns the
|
158
|
+
# owner's ID. Currently, accessors are compared by their ID in the database,
|
159
|
+
# so accessors should be of the same class to avoid security holes.
|
160
|
+
# (i.e. if both User and Group are used as accessors, a User with ID 7 can
|
161
|
+
# access objects owned by a Group with ID 7).
|
162
|
+
# This may be fixed in the future.
|
163
|
+
#
|
164
|
+
# Example:
|
165
|
+
# belongs_to :user
|
166
|
+
# has_owner :user
|
167
|
+
# authenticates_saves :with => :allow_owner
|
168
|
+
#
|
169
|
+
# or:
|
170
|
+
#
|
171
|
+
# has_owner
|
172
|
+
# def owner_id
|
173
|
+
# id
|
174
|
+
# end
|
175
|
+
#
|
176
|
+
def has_owner(attr=nil)
|
177
|
+
unless attr.nil?
|
178
|
+
id_accessor = "#{attr.to_s}_id"
|
179
|
+
id_mutator = "#{attr.to_s}_id="
|
180
|
+
if attr == :self
|
181
|
+
# special case the self attribute but don't allow ownership change
|
182
|
+
define_method(:owner_id) do
|
183
|
+
id
|
184
|
+
end
|
185
|
+
else
|
186
|
+
define_method(:owner) do
|
187
|
+
self.send(attr)
|
188
|
+
end
|
189
|
+
define_method(:owner_id) do
|
190
|
+
self.send(id_accessor)
|
191
|
+
end
|
192
|
+
define_method("owner_id=") do |new_value|
|
193
|
+
self.send(id_mutator,new_value)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
include Ownership
|
198
|
+
end
|
199
|
+
|
200
|
+
# If declared, the accessor used to create this object automatically
|
201
|
+
# becomes its owner.
|
202
|
+
#
|
203
|
+
# Examples:
|
204
|
+
#
|
205
|
+
# class Comment < ActiveRecord::Base
|
206
|
+
# belongs_to :member
|
207
|
+
# has_owner :member
|
208
|
+
# autosets_owner_on_create
|
209
|
+
# authenticates_saves :with => :allow_owner
|
210
|
+
# end
|
211
|
+
#
|
212
|
+
def autosets_owner_on_create
|
213
|
+
has_owner # this will do nothing if the user has already set up has_owner :something
|
214
|
+
# the hook runs before validation so we can validate_associated
|
215
|
+
before_validation_on_create :autoset_owner
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
def authenticates_saves_method_list
|
220
|
+
@save_method_list ||= nil
|
221
|
+
@save_method_list
|
222
|
+
end
|
223
|
+
|
224
|
+
def authenticates_reads_method_list
|
225
|
+
@read_method_list ||= nil
|
226
|
+
@read_method_list
|
227
|
+
end
|
228
|
+
|
229
|
+
def write_validations(attr)
|
230
|
+
@write_validation_map ||= nil
|
231
|
+
if @write_validation_map
|
232
|
+
@write_validation_map[attr]
|
233
|
+
else
|
234
|
+
nil
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def read_validations(attr)
|
239
|
+
@read_validation_map ||= nil
|
240
|
+
if @read_validation_map
|
241
|
+
@read_validation_map[attr]
|
242
|
+
else
|
243
|
+
nil
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
248
|
+
|
249
|
+
module InstanceMethods
|
250
|
+
# Shorthand to get at the accessor of interest
|
251
|
+
def accessor
|
252
|
+
self.class.accessor
|
253
|
+
end
|
254
|
+
|
255
|
+
def bypass_auth
|
256
|
+
@bypass_auth = true
|
257
|
+
yield
|
258
|
+
@bypass_auth = false
|
259
|
+
end
|
260
|
+
|
261
|
+
# Auto-set the owner id to the accessor id before save if the object is new
|
262
|
+
def autoset_owner
|
263
|
+
bypass_auth do
|
264
|
+
if accessor
|
265
|
+
self.owner_id ||= accessor.id
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
true # this is very important!
|
270
|
+
end
|
271
|
+
|
272
|
+
|
273
|
+
# before_save/before_destroy hook installed by authenticates_saves
|
274
|
+
def auth_save_filter
|
275
|
+
if not allowed_to_save
|
276
|
+
# An interesting thought: could this throw an HTTP error?
|
277
|
+
false
|
278
|
+
else
|
279
|
+
true
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# before_save/before_destroy hook installed by authenticates_saves
|
284
|
+
def auth_create_filter
|
285
|
+
if not self.class.allowed_to_create
|
286
|
+
false
|
287
|
+
else
|
288
|
+
true
|
289
|
+
end
|
290
|
+
end
|
291
|
+
# Included for completeness, this could be used to filter out accessors
|
292
|
+
# who can't read an object. Sadly, there's no way to install this, yet.
|
293
|
+
def auth_read_filter
|
294
|
+
if not allowed_to_read
|
295
|
+
false
|
296
|
+
else
|
297
|
+
true
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# This method may be used to determine whether the current accessor has
|
302
|
+
# privileges to save the object. Returns true if so, false otherwise.
|
303
|
+
def allowed_to_save
|
304
|
+
method_list = self.class.authenticates_saves_method_list
|
305
|
+
if method_list.nil?
|
306
|
+
# No method list, so it's allowed
|
307
|
+
true
|
308
|
+
elsif method_list.check :accessor => accessor, :model => self
|
309
|
+
# Method list passed, so allowed
|
310
|
+
true
|
311
|
+
else
|
312
|
+
# Method list failed, so denied
|
313
|
+
false
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# This method may be used to determine whether the current accessor has
|
318
|
+
# privileges to read data from the object. Returns true if so, else false.
|
319
|
+
def allowed_to_read
|
320
|
+
method_list = self.class.authenticates_reads_method_list
|
321
|
+
if method_list.nil?
|
322
|
+
# No method list, so it's allowed
|
323
|
+
true
|
324
|
+
elsif method_list.check :accessor => accessor, :model => self
|
325
|
+
# Method list passed, so allowed
|
326
|
+
true
|
327
|
+
else
|
328
|
+
# Method list failed, so denied
|
329
|
+
false
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
# Overload of write_attribute to implement the filtration
|
334
|
+
def write_attribute(name, value)
|
335
|
+
# Simply check if the accessor is allowed to write the field
|
336
|
+
# (if so, go to superclass and do it)
|
337
|
+
@bypass_auth ||= false
|
338
|
+
if allowed_to_write(name) || @bypass_auth
|
339
|
+
super(name, value)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
# Overload of read_attribute to filter data access
|
344
|
+
def read_attribute(name)
|
345
|
+
@bypass_auth ||= false
|
346
|
+
if allowed_to_read_from(name) || @bypass_auth
|
347
|
+
super(name)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# This method may be used to determine if the current accessor may write
|
352
|
+
# to a given attribute. Returns true if so, false otherwise.
|
353
|
+
def allowed_to_write(name)
|
354
|
+
# no point allowing attribute writes if we can't save them?
|
355
|
+
if allowed_to_save
|
356
|
+
name = name.to_s
|
357
|
+
validation_methods = self.class.write_validations(name)
|
358
|
+
if validation_methods.nil?
|
359
|
+
# We haven't registered any filters on this attribute, so allow the write.
|
360
|
+
true
|
361
|
+
elsif validation_methods.check :accessor => accessor, :model => self
|
362
|
+
# One of the authentication methods worked, so allow the write.
|
363
|
+
true
|
364
|
+
else
|
365
|
+
# We had filters but none of them passed. Disallow write.
|
366
|
+
false
|
367
|
+
end
|
368
|
+
else
|
369
|
+
false
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
# This method may be used to determine if the current accessor may read
|
374
|
+
# a given attribute. Returns true if so, false otherwise.
|
375
|
+
def allowed_to_read_from(name)
|
376
|
+
# no point allowing attribute writes if we can't save them?
|
377
|
+
if allowed_to_read
|
378
|
+
name = name.to_s
|
379
|
+
validation_methods = self.class.read_validations(name)
|
380
|
+
if validation_methods.nil?
|
381
|
+
# We haven't registered any filters on this attribute, so allow the write.
|
382
|
+
true
|
383
|
+
elsif validation_methods.check :accessor => accessor, :model => self
|
384
|
+
# One of the authentication methods worked, so allow the write.
|
385
|
+
true
|
386
|
+
else
|
387
|
+
# We had filters but none of them passed. Disallow write.
|
388
|
+
false
|
389
|
+
end
|
390
|
+
else
|
391
|
+
false
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
|
396
|
+
# This method may be used to determine if the current accessor may write
|
397
|
+
# to a given attribute. Returns true if so, false otherwise.
|
398
|
+
# for now, if you can save, you can destroy
|
399
|
+
def allowed_to_destroy
|
400
|
+
allowed_to_save
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
module Ownership
|
405
|
+
# This method implements a simple test: whether the object is owned by
|
406
|
+
# the accessor. See has_owner in ClassMethods. Note that new records,
|
407
|
+
# with no owner, will always pass this test. This allows the
|
408
|
+
# (not-yet-existent) owner (i.e. the creator) to write any attributes
|
409
|
+
# that they would have had access to anyway. Use authenticates_creation
|
410
|
+
# to allow only certain accessors the right to create objects.
|
411
|
+
def allow_owner(options={})
|
412
|
+
if new_record?
|
413
|
+
true
|
414
|
+
elsif accessor.nil?
|
415
|
+
false # maybe this is failing up the works?
|
416
|
+
else
|
417
|
+
# must define an owner_id method for this to work
|
418
|
+
accessor.id == owner_id
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
module CreationControl
|
424
|
+
def allowed_to_create
|
425
|
+
method_list = @create_method_list
|
426
|
+
if method_list.nil?
|
427
|
+
# No method list, so it's allowed
|
428
|
+
true
|
429
|
+
elsif method_list.check :accessor => accessor
|
430
|
+
# Method list passed, so allowed
|
431
|
+
true
|
432
|
+
else
|
433
|
+
# Method list failed, so denied
|
434
|
+
false
|
435
|
+
end
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
end
|
440
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{mikldt-authenticates_access}
|
8
|
+
s.version = "0.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Michael DiTore"]
|
12
|
+
s.date = %q{2010-01-24}
|
13
|
+
s.description = %q{Model-based read and write user authorization in Rails}
|
14
|
+
s.email = %q{mikldt@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"MIT-LICENSE",
|
21
|
+
"README",
|
22
|
+
"Rakefile",
|
23
|
+
"VERSION",
|
24
|
+
"init.rb",
|
25
|
+
"install.rb",
|
26
|
+
"lib/authenticates_access.rb",
|
27
|
+
"mikldt-authenticates_access.gemspec",
|
28
|
+
"tasks/authenticates_access_tasks.rake",
|
29
|
+
"test/admin_item.rb",
|
30
|
+
"test/authenticates_access_test.rb",
|
31
|
+
"test/database.yml",
|
32
|
+
"test/fixtures/admin_items.yml",
|
33
|
+
"test/fixtures/owned_items.yml",
|
34
|
+
"test/fixtures/users.yml",
|
35
|
+
"test/owned_item.rb",
|
36
|
+
"test/test_helper.rb",
|
37
|
+
"test/user.rb",
|
38
|
+
"uninstall.rb"
|
39
|
+
]
|
40
|
+
s.homepage = %q{http://github.com/mikldt/authenticates_access}
|
41
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
42
|
+
s.require_paths = ["lib"]
|
43
|
+
s.rubygems_version = %q{1.3.5}
|
44
|
+
s.summary = %q{Model-based Authorization on Rails!}
|
45
|
+
s.test_files = [
|
46
|
+
"test/authenticates_access_test.rb",
|
47
|
+
"test/user.rb",
|
48
|
+
"test/test_helper.rb",
|
49
|
+
"test/owned_item.rb",
|
50
|
+
"test/admin_item.rb"
|
51
|
+
]
|
52
|
+
|
53
|
+
if s.respond_to? :specification_version then
|
54
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
55
|
+
s.specification_version = 3
|
56
|
+
|
57
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
58
|
+
else
|
59
|
+
end
|
60
|
+
else
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
data/test/admin_item.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class AuthenticatesAccessTest < ActiveSupport::TestCase
|
4
|
+
test "authenticates_saves :with => :allow_owner positive" do
|
5
|
+
ActiveRecord::Base.accessor = users(:user1)
|
6
|
+
item = owned_items(:item3)
|
7
|
+
item.description = "I changed the text"
|
8
|
+
assert item.save
|
9
|
+
end
|
10
|
+
|
11
|
+
test "authenticates_saves :with => :allow_owner negative" do
|
12
|
+
ActiveRecord::Base.accessor = users(:user1)
|
13
|
+
item = owned_items(:item4)
|
14
|
+
item.description = "I changed the text"
|
15
|
+
flunk if item.save
|
16
|
+
end
|
17
|
+
|
18
|
+
test "authenticates_saves :with => :allow_owner on new object" do
|
19
|
+
ActiveRecord::Base.accessor = users(:user1)
|
20
|
+
item = OwnedItem.new(:description => "This should work")
|
21
|
+
assert item.save
|
22
|
+
assert item.user_id == users(:user1).id
|
23
|
+
end
|
24
|
+
|
25
|
+
test "authenticates_creates negative" do
|
26
|
+
ActiveRecord::Base.accessor = users(:user1)
|
27
|
+
user = User.new(
|
28
|
+
:is_admin => true,
|
29
|
+
:name => "cracker",
|
30
|
+
:bio => "I like breaking into things"
|
31
|
+
)
|
32
|
+
flunk if user.save
|
33
|
+
end
|
34
|
+
|
35
|
+
test "authenticates_creates positive" do
|
36
|
+
ActiveRecord::Base.accessor = users(:admin)
|
37
|
+
user = User.new(
|
38
|
+
:is_admin => false,
|
39
|
+
:name => "some legit user",
|
40
|
+
:bio => "This should work"
|
41
|
+
)
|
42
|
+
assert user.save
|
43
|
+
end
|
44
|
+
|
45
|
+
test "authenticates_writes_to negative" do
|
46
|
+
ActiveRecord::Base.accessor = users(:user1)
|
47
|
+
user2 = users(:user2)
|
48
|
+
user2.is_admin = true
|
49
|
+
flunk if user2.is_admin
|
50
|
+
end
|
51
|
+
|
52
|
+
test "authenticates_writes_to positive" do
|
53
|
+
ActiveRecord::Base.accessor = users(:admin)
|
54
|
+
user2 = users(:user2)
|
55
|
+
user2.is_admin = true
|
56
|
+
assert user2.is_admin
|
57
|
+
end
|
58
|
+
|
59
|
+
test "allowed_to_create positive" do
|
60
|
+
ActiveRecord::Base.accessor = users(:admin)
|
61
|
+
assert User.allowed_to_create
|
62
|
+
end
|
63
|
+
|
64
|
+
test "allowed_to_create negative" do
|
65
|
+
ActiveRecord::Base.accessor = users(:user1)
|
66
|
+
flunk if User.allowed_to_create
|
67
|
+
end
|
68
|
+
|
69
|
+
test "allowed_to_write positive" do
|
70
|
+
ActiveRecord::Base.accessor = users(:admin)
|
71
|
+
assert users(:user1).allowed_to_write(:is_admin)
|
72
|
+
assert users(:user1).allowed_to_write(:bio)
|
73
|
+
|
74
|
+
ActiveRecord::Base.accessor = users(:user1)
|
75
|
+
assert users(:user1).allowed_to_write(:bio)
|
76
|
+
|
77
|
+
flunk if users(:user2).allowed_to_write(:bio)
|
78
|
+
end
|
79
|
+
|
80
|
+
test "allowed_to_write negative" do
|
81
|
+
ActiveRecord::Base.accessor = users(:user1)
|
82
|
+
flunk if users(:user1).allowed_to_write(:is_admin)
|
83
|
+
end
|
84
|
+
|
85
|
+
test "allowed_to_save as user1" do
|
86
|
+
ActiveRecord::Base.accessor = users(:user1)
|
87
|
+
assert owned_items(:item3).allowed_to_save
|
88
|
+
flunk if owned_items(:item4).allowed_to_save
|
89
|
+
end
|
90
|
+
|
91
|
+
test "can_create_but_not_change_owner" do
|
92
|
+
ActiveRecord::Base.accessor = users(:user1)
|
93
|
+
flunk if ActiveRecord::Base.accessor.is_admin
|
94
|
+
created = CreatedItem.new(:description => "something")
|
95
|
+
assert created.save
|
96
|
+
assert created.owner_id == users(:user1).id
|
97
|
+
created.owner_id = 1234
|
98
|
+
assert created.owner_id == users(:user1).id
|
99
|
+
end
|
100
|
+
|
101
|
+
test "admin_can_set_owner" do
|
102
|
+
ActiveRecord::Base.accessor = users(:admin)
|
103
|
+
created = CreatedItem.new(:description => "something")
|
104
|
+
created.owner_id = users(:user1).id
|
105
|
+
assert created.save
|
106
|
+
assert created.owner_id == users(:user1).id
|
107
|
+
end
|
108
|
+
end
|
data/test/database.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
item1:
|
2
|
+
user_id: 1
|
3
|
+
description: "I'm owned by the admin"
|
4
|
+
|
5
|
+
item2:
|
6
|
+
user_id: 2
|
7
|
+
description: "I'm owned by the other admin"
|
8
|
+
|
9
|
+
item3:
|
10
|
+
user_id: 3
|
11
|
+
description: "I'm owned by user1"
|
12
|
+
|
13
|
+
item4:
|
14
|
+
user_id: 4
|
15
|
+
description: "I'm owned by user2"
|
16
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
admin:
|
2
|
+
id: 1
|
3
|
+
name: The Administrator
|
4
|
+
is_admin: true
|
5
|
+
bio: I like writing code and doing other things
|
6
|
+
|
7
|
+
admin2:
|
8
|
+
id: 2
|
9
|
+
name: Another Administrator
|
10
|
+
is_admin: true
|
11
|
+
bio: I am also an administrator
|
12
|
+
|
13
|
+
user1:
|
14
|
+
id: 3
|
15
|
+
name: User 1
|
16
|
+
is_admin: false
|
17
|
+
bio: I own some things but not others
|
18
|
+
|
19
|
+
user2:
|
20
|
+
id: 4
|
21
|
+
name: User 2
|
22
|
+
is_admin: false
|
23
|
+
bio: I am like user 1
|
data/test/owned_item.rb
ADDED
data/test/test_helper.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/test_case'
|
4
|
+
|
5
|
+
# workaround for rails being on crack (maybe?)
|
6
|
+
require 'test/unit'
|
7
|
+
require 'test/unit/testcase'
|
8
|
+
require 'active_support/testing/setup_and_teardown'
|
9
|
+
#require 'active_support/testing/assertions'
|
10
|
+
|
11
|
+
# load up the plugin in question
|
12
|
+
require 'active_record'
|
13
|
+
require 'active_record/fixtures'
|
14
|
+
require 'authenticates_access'
|
15
|
+
|
16
|
+
ActiveRecord::Base.class_eval do
|
17
|
+
extend AuthenticatesAccess::ClassMethods
|
18
|
+
end
|
19
|
+
|
20
|
+
root_dir = File.dirname(__FILE__)
|
21
|
+
|
22
|
+
# do cocaine to make ActiveRecord happy
|
23
|
+
ActiveRecord::Base.configurations = YAML::load(File.open(File.join(root_dir, 'database.yml')))
|
24
|
+
|
25
|
+
# bring up some database stuff
|
26
|
+
ActiveRecord::Base.establish_connection({
|
27
|
+
:adapter => 'sqlite3',
|
28
|
+
:dbfile => ':memory:'
|
29
|
+
})
|
30
|
+
|
31
|
+
def build_schema
|
32
|
+
# define a crude schema
|
33
|
+
ActiveRecord::Schema.define do
|
34
|
+
create_table "users", :force => true do |t|
|
35
|
+
t.column "name", :string
|
36
|
+
t.column "is_admin", :boolean
|
37
|
+
t.column "bio", :text
|
38
|
+
end
|
39
|
+
|
40
|
+
create_table "owned_items", :force => true do |t|
|
41
|
+
t.column "user_id", :integer
|
42
|
+
t.column "description", :text
|
43
|
+
end
|
44
|
+
|
45
|
+
create_table "admin_items", :force => true do |t|
|
46
|
+
t.column "description", :text
|
47
|
+
end
|
48
|
+
|
49
|
+
create_table "created_items", :force => true do |t|
|
50
|
+
t.column "user_id", :integer
|
51
|
+
t.column "description", :text
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
class ActiveSupport::TestCase
|
58
|
+
# 2.3.2 seems to need this?
|
59
|
+
begin
|
60
|
+
include ActiveRecord::TestFixtures
|
61
|
+
rescue NameError
|
62
|
+
puts "You appear to be using a pre-2.3 version of Rails. No need to include ActiveRecord::TestFixtures..."
|
63
|
+
end
|
64
|
+
|
65
|
+
self.fixture_path = File.join(File.dirname(__FILE__), "fixtures")
|
66
|
+
|
67
|
+
build_schema
|
68
|
+
|
69
|
+
self.use_transactional_fixtures = true
|
70
|
+
self.use_instantiated_fixtures = false
|
71
|
+
|
72
|
+
# load up fixtures
|
73
|
+
fixtures :all
|
74
|
+
end
|
75
|
+
|
data/test/user.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
class User < ActiveRecord::Base
|
2
|
+
# users own themselves, so to speak
|
3
|
+
has_owner :self
|
4
|
+
|
5
|
+
# only admins can write to the is_admin and can_create fields
|
6
|
+
authenticates_writes_to :is_admin, :with_accessor_method => :is_admin
|
7
|
+
authenticates_writes_to :can_create, :with_accessor_method => :is_admin
|
8
|
+
# users can save their own profiles
|
9
|
+
authenticates_saves :with => :allow_owner
|
10
|
+
# admins can save anyone's profile
|
11
|
+
authenticates_saves :with_accessor_method => :is_admin
|
12
|
+
# admins can create users
|
13
|
+
authenticates_creation :with_accessor_method => :is_admin
|
14
|
+
end
|
data/uninstall.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Uninstall hook code here
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mikldt-authenticates_access
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael DiTore
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-24 00:00:00 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Model-based read and write user authorization in Rails
|
17
|
+
email: mikldt@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
files:
|
25
|
+
- .gitignore
|
26
|
+
- MIT-LICENSE
|
27
|
+
- README
|
28
|
+
- Rakefile
|
29
|
+
- VERSION
|
30
|
+
- init.rb
|
31
|
+
- install.rb
|
32
|
+
- lib/authenticates_access.rb
|
33
|
+
- mikldt-authenticates_access.gemspec
|
34
|
+
- tasks/authenticates_access_tasks.rake
|
35
|
+
- test/admin_item.rb
|
36
|
+
- test/authenticates_access_test.rb
|
37
|
+
- test/database.yml
|
38
|
+
- test/fixtures/admin_items.yml
|
39
|
+
- test/fixtures/owned_items.yml
|
40
|
+
- test/fixtures/users.yml
|
41
|
+
- test/owned_item.rb
|
42
|
+
- test/test_helper.rb
|
43
|
+
- test/user.rb
|
44
|
+
- uninstall.rb
|
45
|
+
has_rdoc: true
|
46
|
+
homepage: http://github.com/mikldt/authenticates_access
|
47
|
+
licenses: []
|
48
|
+
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options:
|
51
|
+
- --charset=UTF-8
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.3.5
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: Model-based Authorization on Rails!
|
73
|
+
test_files:
|
74
|
+
- test/authenticates_access_test.rb
|
75
|
+
- test/user.rb
|
76
|
+
- test/test_helper.rb
|
77
|
+
- test/owned_item.rb
|
78
|
+
- test/admin_item.rb
|