mikldt-authenticates_access 0.0.0
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.
- 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
|