indulgence 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +315 -0
- data/Rakefile +13 -0
- data/lib/active_record/acts/indulgent.rb +42 -0
- data/lib/indulgence.rb +7 -0
- data/lib/indulgence/ability.rb +19 -0
- data/lib/indulgence/indulgent.rb +27 -0
- data/lib/indulgence/permission.rb +104 -0
- data/lib/indulgence/version.rb +10 -0
- data/test/db/config.yml +17 -0
- data/test/db/migrate/20130408085511_create_users.rb +11 -0
- data/test/db/migrate/20130408103015_create_roles.rb +8 -0
- data/test/db/migrate/20130408132217_create_things.rb +7 -0
- data/test/db/schema.rb +36 -0
- data/test/db/test.sqlite3.db +0 -0
- data/test/lib/role.rb +7 -0
- data/test/lib/thing.rb +19 -0
- data/test/lib/thing_permission.rb +46 -0
- data/test/lib/user.rb +9 -0
- data/test/test_helper.rb +9 -0
- data/test/units/indulgence/ability_test.rb +36 -0
- data/test/units/indulgence/permission_test.rb +15 -0
- data/test/units/role_test.rb +16 -0
- data/test/units/thing_permission_test.rb +78 -0
- data/test/units/thing_test.rb +98 -0
- data/test/units/user_test.rb +21 -0
- metadata +120 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 Rob Nichols
|
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.rdoc
ADDED
@@ -0,0 +1,315 @@
|
|
1
|
+
= Indulgence
|
2
|
+
|
3
|
+
Yet another permissions gem.
|
4
|
+
|
5
|
+
In creating Indulgence I wanted a role based permissions tool that did two main
|
6
|
+
things:
|
7
|
+
|
8
|
+
* Determine what permission a user had to do something to an object
|
9
|
+
* Filtered a search of objects based on the those permissions
|
10
|
+
|
11
|
+
It was apparent to me that if 'something' was one of the CRUD actions, it would
|
12
|
+
cover all the use cases I could think of. So permissions were sub-divided into
|
13
|
+
the 'abilities': create, read, update, and delete.
|
14
|
+
|
15
|
+
The other requirement was that the permission for an object could be defined
|
16
|
+
succinctly within a single file.
|
17
|
+
|
18
|
+
== Defining indulgent permissions
|
19
|
+
|
20
|
+
Indulgence is added to a class via acts_as_indulgent:
|
21
|
+
|
22
|
+
class Thing < ActiveRecord::Base
|
23
|
+
acts_as_indulgent
|
24
|
+
end
|
25
|
+
|
26
|
+
Used in this way, permissions need to be defined in an Indulgence::Permission
|
27
|
+
object called ThingPermission, with an instance method :default
|
28
|
+
|
29
|
+
class ThingPermission < Indulgence::Permission
|
30
|
+
|
31
|
+
def default
|
32
|
+
{
|
33
|
+
create: none,
|
34
|
+
read: all,
|
35
|
+
update: none,
|
36
|
+
delete: none
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
This needs to be available to the Thing class. For example, in a rails app, by
|
43
|
+
placing it in app/permissions/thing_permission.rb
|
44
|
+
|
45
|
+
== Users and Roles
|
46
|
+
|
47
|
+
Indulgence assumes that permissions will be tested against an entity object
|
48
|
+
(e.g. User), that has a role object associated with it, and that each role can
|
49
|
+
be uniquely identified by a name method.
|
50
|
+
|
51
|
+
The default behaviour assumes that the entity object has a :role method that
|
52
|
+
returns the role object, and that the role object has a :name method. So
|
53
|
+
typically, these objects could look like this:
|
54
|
+
|
55
|
+
class User < ActiveRecord::Base
|
56
|
+
belongs_to :role
|
57
|
+
end
|
58
|
+
|
59
|
+
class Role < ActiveRecord::Base
|
60
|
+
has_many :users
|
61
|
+
validates :name, :uniqueness => true
|
62
|
+
end
|
63
|
+
|
64
|
+
role = Role.create(:name => 'pleb')
|
65
|
+
user = User.create(
|
66
|
+
:first_name => 'Joe',
|
67
|
+
:last_name => 'Blogs',
|
68
|
+
:role_id => role.id
|
69
|
+
)
|
70
|
+
|
71
|
+
== indulge?
|
72
|
+
|
73
|
+
Simple true/false permission can be determined using the :indulge? method:
|
74
|
+
|
75
|
+
thing = Thing.first
|
76
|
+
|
77
|
+
thing.indulge?(user, :create) == false
|
78
|
+
thing.indulge?(user, :read) == true
|
79
|
+
thing.indulge?(user, :update) == false
|
80
|
+
thing.indulge?(user, :delete) == false
|
81
|
+
|
82
|
+
== indulgence
|
83
|
+
|
84
|
+
The :indulgence method is used as a where filter:
|
85
|
+
|
86
|
+
Thing.indulgence(user, :create) --> raises ActiveRecord::RecordNotFound
|
87
|
+
Thing.indulgence(user, :read) == Thing.all
|
88
|
+
Thing.indulgence(user, :update) --> raises ActiveRecord::RecordNotFound
|
89
|
+
Thing.indulgence(user, :delete) --> raises ActiveRecord::RecordNotFound
|
90
|
+
|
91
|
+
So to find all the blue things that the user has permission to read:
|
92
|
+
|
93
|
+
Thing.indulgence(user, :read).where(:colour => 'blue')
|
94
|
+
|
95
|
+
== Customisation
|
96
|
+
|
97
|
+
=== Adding other roles
|
98
|
+
|
99
|
+
Up until now, all users get the same permissions irrespective of role. Let's
|
100
|
+
give Emperors the right to see and do anything by first creating an emperor
|
101
|
+
|
102
|
+
emperor = Role.create(:name => 'emperor')
|
103
|
+
caesar = User.create(
|
104
|
+
:first_name => 'Julius',
|
105
|
+
:last_name => 'Ceasar',
|
106
|
+
:role_id => emporor.id
|
107
|
+
)
|
108
|
+
|
109
|
+
And then defining what they can do by adding these two methods to ThingPermission:
|
110
|
+
|
111
|
+
def abilities
|
112
|
+
{
|
113
|
+
emperor: default.merge(emperor)
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
def emperor
|
118
|
+
{
|
119
|
+
create: all,
|
120
|
+
update: all,
|
121
|
+
delete: all
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
This uses a merger of the default abilities so that only the variant abilities
|
126
|
+
need to be defined in the emperor method. That is, _read_ is inherited from
|
127
|
+
_default_ rather than being defined in _emperor_, as it is already set to *all*.
|
128
|
+
|
129
|
+
abilities is a hash of hashes. The lowest level, associates action names with
|
130
|
+
ability objects. The top level associates role names to the lower level ability
|
131
|
+
object hashes. The construction is perhaps clearer in the abilities above was
|
132
|
+
written like this:
|
133
|
+
|
134
|
+
def abilities
|
135
|
+
{
|
136
|
+
emperor: {
|
137
|
+
create: all,
|
138
|
+
read: default[:read],
|
139
|
+
update: all,
|
140
|
+
delete: all
|
141
|
+
}
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
With this done:
|
146
|
+
|
147
|
+
thing.indulge?(caesar, :create) == true
|
148
|
+
thing.indulge?(caesar, :read) == true
|
149
|
+
thing.indulge?(caesar, :update) == true
|
150
|
+
thing.indulge?(caesar, :delete) == true
|
151
|
+
|
152
|
+
Thing.indulgence(caesar, :create) == Thing.all
|
153
|
+
Thing.indulgence(caesar, :read) == Thing.all
|
154
|
+
Thing.indulgence(caesar, :update) == Thing.all
|
155
|
+
Thing.indulgence(caesar, :delete) == Thing.all
|
156
|
+
|
157
|
+
=== Adding abilities
|
158
|
+
|
159
|
+
Indulgence has two built in abilities. These are *all* and *none*. These two
|
160
|
+
have provided all the functionality described above, but in most real cases
|
161
|
+
some more fine tuned ability setting will be needed.
|
162
|
+
|
163
|
+
Let's create an author role, and give authors the ability to create and update
|
164
|
+
their own things.
|
165
|
+
|
166
|
+
author = Role.create(:name => :author)
|
167
|
+
|
168
|
+
Next we need to give author's ownership of things. So we add an :author_id
|
169
|
+
attribute to Thing, and a matching :author method:
|
170
|
+
|
171
|
+
class Thing < ActiveRecord::Base
|
172
|
+
acts_as_indulgent
|
173
|
+
belongs_to :author, :class_name => 'User'
|
174
|
+
end
|
175
|
+
|
176
|
+
Then we need to create an Ability that uses this relationship to determine
|
177
|
+
permissions. This can be done by adding this method to ThingPermission:
|
178
|
+
|
179
|
+
def things_they_wrote
|
180
|
+
define_ability(
|
181
|
+
:name => :things_they_wrote,
|
182
|
+
:truth => lambda {|thing| thing.author_id == entity.id},
|
183
|
+
:where_clause => {:author_id => entity.id}
|
184
|
+
)
|
185
|
+
end
|
186
|
+
|
187
|
+
This will create an Ability object with the following methods:
|
188
|
+
|
189
|
+
[name] Allows abilities of the same kind to be matched
|
190
|
+
[truth] Used by :indulge?
|
191
|
+
[where_clause] Used by :indulgence
|
192
|
+
|
193
|
+
Note that Indulgence::Permission#entity returns the entity object passed to the
|
194
|
+
instance on creation. In this example, that will be the User.
|
195
|
+
|
196
|
+
==== The where clause
|
197
|
+
|
198
|
+
In this example:
|
199
|
+
|
200
|
+
Thing.indulgence(user, :read) == Thing.where(:author_id => user.id)
|
201
|
+
|
202
|
+
if the :read ability is matched to *things_they_wrote*
|
203
|
+
|
204
|
+
==== truth
|
205
|
+
|
206
|
+
In *all* :truth is simply _true_, and in *none* :truth is _false_.
|
207
|
+
|
208
|
+
For more complex abilities :truth should be a lambda that is passed the object
|
209
|
+
being tested, and returns _true_ if permission should be given. Otherwise _false_
|
210
|
+
should be returned.
|
211
|
+
|
212
|
+
Once *things_they_wrote* has been defined, we can use it to define a new set
|
213
|
+
of abilities:
|
214
|
+
|
215
|
+
def abilities
|
216
|
+
{
|
217
|
+
emperor: default.merge(emperor),
|
218
|
+
author: default.merge(author)
|
219
|
+
}
|
220
|
+
end
|
221
|
+
|
222
|
+
def author
|
223
|
+
{
|
224
|
+
create: things_they_wrote,
|
225
|
+
update: things_they_wrote
|
226
|
+
}
|
227
|
+
end
|
228
|
+
|
229
|
+
With that done:
|
230
|
+
|
231
|
+
cicero = User.create(
|
232
|
+
:first_name => 'Marcus',
|
233
|
+
:last_name => 'Cicero',
|
234
|
+
:role_id => author.id
|
235
|
+
)
|
236
|
+
|
237
|
+
thing.author = cicero
|
238
|
+
thing.save
|
239
|
+
|
240
|
+
thing.indulge?(cicero, :create) == true
|
241
|
+
thing.indulge?(cicero, :read) == true
|
242
|
+
thing.indulge?(cicero, :update) == true
|
243
|
+
thing.indulge?(cicero, :delete) == false
|
244
|
+
|
245
|
+
Thing.indulgence(cicero, :create) == [thing]
|
246
|
+
Thing.indulgence(cicero, :read) == Thing.all
|
247
|
+
Thing.indulgence(cicero, :update) == [thing]
|
248
|
+
Thing.indulgence(cicero, :delete) --> raises ActiveRecord::RecordNotFound
|
249
|
+
|
250
|
+
=== Alternative Permission Class
|
251
|
+
|
252
|
+
As stated above, the default behaviour is for a Thing class to expect its
|
253
|
+
permissions to be defined in ThingPermission. However, you can define an
|
254
|
+
alternative class name:
|
255
|
+
|
256
|
+
acts_as_indulgent :using => PermissionsForThing
|
257
|
+
|
258
|
+
=== Alternative Entity behaviour
|
259
|
+
|
260
|
+
Consider this example:
|
261
|
+
|
262
|
+
class User < ActiveRecord::Base
|
263
|
+
belongs_to :position
|
264
|
+
end
|
265
|
+
|
266
|
+
class Position < ActiveRecord::Base
|
267
|
+
has_many :users
|
268
|
+
validates :title, :uniqueness => true
|
269
|
+
end
|
270
|
+
|
271
|
+
There are two ways of dealing with this.
|
272
|
+
|
273
|
+
If only ThingPermission is affected, the attributes that stores the _role_method_
|
274
|
+
and _role_name_method_ could be overwritten:
|
275
|
+
|
276
|
+
class ThingPermission < Indulgence::Permission
|
277
|
+
def self.role_method
|
278
|
+
:position
|
279
|
+
end
|
280
|
+
|
281
|
+
def self.role_name_method
|
282
|
+
:title
|
283
|
+
end
|
284
|
+
|
285
|
+
.....
|
286
|
+
end
|
287
|
+
|
288
|
+
Alternatively if all permissions were to be altered the super class Permission
|
289
|
+
could be updated (for example in a rails initializer):
|
290
|
+
|
291
|
+
Indulgence::Permission.role_method = :position
|
292
|
+
Indulgence::Permission.role_name_method = :title
|
293
|
+
|
294
|
+
=== Alternative method names
|
295
|
+
|
296
|
+
The method names _indulgence_ and _indulge?_ may not suit your application. If
|
297
|
+
you wish to use alternative names, they can be aliased like this:
|
298
|
+
|
299
|
+
acts_as_indulgent(
|
300
|
+
:truth_method => :permit?,
|
301
|
+
:where_method => :permitted
|
302
|
+
)
|
303
|
+
|
304
|
+
With this used to define indulgence in Thing, we can do this:
|
305
|
+
|
306
|
+
thing.permit?(cicero, :update) == true
|
307
|
+
thing.permit?(cicero, :delete) == false
|
308
|
+
|
309
|
+
Thing.permitted(cicero, :create) == [thing]
|
310
|
+
Thing.permitted(cicero, :read) == Thing.all
|
311
|
+
|
312
|
+
== Examples
|
313
|
+
|
314
|
+
For some examples, have a look at the tests. In particular, look at the object
|
315
|
+
definitions in test/lib.
|
data/Rakefile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Acts
|
3
|
+
module Indulgent
|
4
|
+
def self.included(base)
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
def acts_as_indulgent(args = {})
|
11
|
+
include Indulgence::Indulgent::InstanceMethods
|
12
|
+
extend Indulgence::Indulgent::ClassMethods
|
13
|
+
|
14
|
+
alias_method args[:truth_method], :indulge? if args[:truth_method]
|
15
|
+
singleton_class.send(:alias_method, args[:where_method], :indulgence) if args[:where_method]
|
16
|
+
self.indulgent_permission_class = args[:using]
|
17
|
+
end
|
18
|
+
|
19
|
+
def indulgent_permission_class
|
20
|
+
@indulgent_permission_class
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def default_indulgence_permission_class
|
25
|
+
class_name = to_s
|
26
|
+
name = class_name + "Permission"
|
27
|
+
require name.underscore
|
28
|
+
if const_defined? name
|
29
|
+
class_eval name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def indulgent_permission_class=(klass = nil)
|
34
|
+
@indulgent_permission_class = klass || default_indulgence_permission_class
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
ActiveRecord::Base.send(:include, ActiveRecord::Acts::Indulgent)
|
data/lib/indulgence.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
module Indulgence
|
3
|
+
class Ability
|
4
|
+
attr_reader :name, :truth, :where_clause
|
5
|
+
|
6
|
+
def initialize(args = {})
|
7
|
+
@name = args[:name]
|
8
|
+
@truth = args[:truth]
|
9
|
+
@where_clause = args[:where_clause]
|
10
|
+
end
|
11
|
+
|
12
|
+
def ==(another_ability)
|
13
|
+
[:name, :truth, :where_clause].each do |method|
|
14
|
+
return false if send(method) != another_ability.send(method)
|
15
|
+
end
|
16
|
+
return true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Indulgence
|
2
|
+
module Indulgent
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
def indulgence(entity, ability)
|
6
|
+
permission = indulgent_permission_class.new(entity, ability)
|
7
|
+
raise_not_found if permission.ability == Permission.none or permission.ability.blank?
|
8
|
+
where(permission.where)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
def raise_not_found
|
13
|
+
raise ActiveRecord::RecordNotFound.new('Unable to find the item(s) you were looking for')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module InstanceMethods
|
18
|
+
|
19
|
+
def indulge?(entity, ability)
|
20
|
+
permission = self.class.indulgent_permission_class.new(entity, ability)
|
21
|
+
return permission.indulge? self
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require_relative 'ability'
|
2
|
+
|
3
|
+
module Indulgence
|
4
|
+
class Permission
|
5
|
+
attr_reader :entity, :ability
|
6
|
+
|
7
|
+
def initialize(entity, ability)
|
8
|
+
self
|
9
|
+
@entity = entity
|
10
|
+
@ability = abilities_for_role[ability]
|
11
|
+
end
|
12
|
+
|
13
|
+
def abilities
|
14
|
+
{}
|
15
|
+
end
|
16
|
+
|
17
|
+
def default
|
18
|
+
raise 'There must always be a default'
|
19
|
+
end
|
20
|
+
|
21
|
+
def where
|
22
|
+
ability.where_clause
|
23
|
+
end
|
24
|
+
|
25
|
+
def indulge?(thing)
|
26
|
+
ability.truth.respond_to?(:call) ? ability.truth.call(thing) : ability.truth
|
27
|
+
end
|
28
|
+
|
29
|
+
@@role_method = :role
|
30
|
+
|
31
|
+
def self.role_method=(name)
|
32
|
+
@@role_method = name.to_sym
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.role_method
|
36
|
+
@@role_method
|
37
|
+
end
|
38
|
+
|
39
|
+
@@role_name_method = :name
|
40
|
+
|
41
|
+
def self.role_name_method=(name)
|
42
|
+
@@role_name_method = name.to_sym
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.role_name_method
|
46
|
+
@@role_name_method
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.none
|
50
|
+
@none ||= define_ability(
|
51
|
+
:name => :none,
|
52
|
+
:truth => false
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.all
|
57
|
+
@all ||= define_ability(
|
58
|
+
:name => :all,
|
59
|
+
:truth => true
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.define_ability(args)
|
64
|
+
Ability.new args
|
65
|
+
end
|
66
|
+
|
67
|
+
def define_ability(args)
|
68
|
+
self.class.define_ability(args)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Ensure passing an unknown key behaves as one would expect for a hash
|
72
|
+
def [](key)
|
73
|
+
return {}[key] unless keys.include? key
|
74
|
+
super
|
75
|
+
end
|
76
|
+
|
77
|
+
def role_name
|
78
|
+
@role_name ||= entity_role_name
|
79
|
+
end
|
80
|
+
|
81
|
+
def abilities_for_role
|
82
|
+
abilities[role_name] || default
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
def entity_role_name
|
87
|
+
role = entity.send(self.class.role_method)
|
88
|
+
name_method = self.class.role_name_method
|
89
|
+
if role and abilities.keys.include?(role.send(name_method).to_sym)
|
90
|
+
role.send(name_method).to_sym
|
91
|
+
else
|
92
|
+
:default
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def all
|
97
|
+
self.class.all
|
98
|
+
end
|
99
|
+
|
100
|
+
def none
|
101
|
+
self.class.none
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/test/db/config.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
test: &test
|
2
|
+
adapter: sqlite3
|
3
|
+
database: test/db/test.sqlite3.db
|
4
|
+
pool: 5
|
5
|
+
timeout: 5000
|
6
|
+
|
7
|
+
develop:
|
8
|
+
adapter: sqlite3
|
9
|
+
database: test/db/develop.sqlite3.db
|
10
|
+
pool: 5
|
11
|
+
timeout: 5000
|
12
|
+
|
13
|
+
production:
|
14
|
+
adapter: sqlite3
|
15
|
+
database: test/db/production.sqlite3.db
|
16
|
+
pool: 5
|
17
|
+
timeout: 5000
|
data/test/db/schema.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# This file is auto-generated from the current state of the database. Instead
|
3
|
+
# of editing this file, please use the migrations feature of Active Record to
|
4
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
5
|
+
#
|
6
|
+
# Note that this schema.rb definition is the authoritative source for your
|
7
|
+
# database schema. If you need to create the application database on another
|
8
|
+
# system, you should be using db:schema:load, not running all the migrations
|
9
|
+
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
10
|
+
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
11
|
+
#
|
12
|
+
# It's strongly recommended to check this file into your version control system.
|
13
|
+
|
14
|
+
ActiveRecord::Schema.define(:version => 20130408132217) do
|
15
|
+
|
16
|
+
create_table "roles", :force => true do |t|
|
17
|
+
t.string "name"
|
18
|
+
t.datetime "created_at", :null => false
|
19
|
+
t.datetime "updated_at", :null => false
|
20
|
+
end
|
21
|
+
|
22
|
+
create_table "things", :force => true do |t|
|
23
|
+
t.string "name"
|
24
|
+
t.integer "owner_id"
|
25
|
+
t.datetime "created_at", :null => false
|
26
|
+
t.datetime "updated_at", :null => false
|
27
|
+
end
|
28
|
+
|
29
|
+
create_table "users", :force => true do |t|
|
30
|
+
t.string "name"
|
31
|
+
t.integer "role_id"
|
32
|
+
t.datetime "created_at", :null => false
|
33
|
+
t.datetime "updated_at", :null => false
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
Binary file
|
data/test/lib/role.rb
ADDED
data/test/lib/thing.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
class Thing < ActiveRecord::Base
|
3
|
+
|
4
|
+
belongs_to :owner, :class_name => 'User'
|
5
|
+
|
6
|
+
acts_as_indulgent(
|
7
|
+
:truth_method => :permit?,
|
8
|
+
:where_method => :permitted
|
9
|
+
)
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class OtherThing < Thing
|
14
|
+
|
15
|
+
acts_as_indulgent :using => ThingPermission
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'indulgence'
|
2
|
+
|
3
|
+
class ThingPermission < Indulgence::Permission
|
4
|
+
|
5
|
+
def abilities
|
6
|
+
{
|
7
|
+
god: default.merge(god),
|
8
|
+
demigod: default.merge(demigod)
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def default
|
14
|
+
{
|
15
|
+
create: none,
|
16
|
+
read: all,
|
17
|
+
update: none,
|
18
|
+
delete: none
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def god
|
23
|
+
{
|
24
|
+
create: all,
|
25
|
+
update: all,
|
26
|
+
delete: all
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def demigod
|
31
|
+
{
|
32
|
+
create: things_they_own,
|
33
|
+
update: things_they_own,
|
34
|
+
delete: things_they_own
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def things_they_own
|
39
|
+
@things_they_own ||= define_ability(
|
40
|
+
:name => :things_they_own,
|
41
|
+
:truth => lambda {|thing| thing.owner_id == entity.id},
|
42
|
+
:where_clause => {:owner_id => entity.id}
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
data/test/lib/user.rb
ADDED
data/test/test_helper.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
ENV["RAILS_ENV"] = "test"
|
2
|
+
$:.unshift File.join(File.dirname(__FILE__),'units')
|
3
|
+
$:.unshift File.join(File.dirname(__FILE__),'lib')
|
4
|
+
$:.unshift File.join(File.dirname(__FILE__),'..','lib','indulgence')
|
5
|
+
|
6
|
+
require 'test/unit'
|
7
|
+
|
8
|
+
require 'active_record'
|
9
|
+
ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => "test/db/test.sqlite3.db"
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative '../../test_helper'
|
2
|
+
require 'ability'
|
3
|
+
|
4
|
+
module Indulgence
|
5
|
+
class AbilityTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def test_none
|
8
|
+
none = Ability.new(
|
9
|
+
:name => :none,
|
10
|
+
:truth => false
|
11
|
+
)
|
12
|
+
assert_equal(:none, none.name)
|
13
|
+
assert_equal(false, none.truth)
|
14
|
+
assert_equal(nil, none.where_clause)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_all
|
18
|
+
all = Ability.new(
|
19
|
+
:name => :all,
|
20
|
+
:truth => true
|
21
|
+
)
|
22
|
+
assert_equal(:all, all.name)
|
23
|
+
assert_equal(true, all.truth)
|
24
|
+
assert_equal(nil, all.where_clause)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_equality
|
28
|
+
attributes = {:name => :same, :true => true}
|
29
|
+
ability = Ability.new(attributes)
|
30
|
+
other_ability = Ability.new(attributes)
|
31
|
+
assert(ability == other_ability, "#{ability} == #{other_ability} should return true")
|
32
|
+
assert_equal(ability, other_ability)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative '../../test_helper'
|
2
|
+
require 'user'
|
3
|
+
|
4
|
+
module Indulgence
|
5
|
+
class PermissionTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def test_creation_fails_as_methods_undefined_in_parent_class
|
8
|
+
assert_raise RuntimeError do
|
9
|
+
Permission.new(User.create(:name => 'Whisp'), :read)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require 'user'
|
3
|
+
require 'role'
|
4
|
+
|
5
|
+
class RoleTest < Test::Unit::TestCase
|
6
|
+
def teardown
|
7
|
+
User.delete_all
|
8
|
+
Role.delete_all
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_users
|
12
|
+
role = Role.create(:name => 'god')
|
13
|
+
user = User.create(:name => 'Harry', :role_id => role.id)
|
14
|
+
assert_equal([user], role.users)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require 'user'
|
3
|
+
require 'role'
|
4
|
+
require 'thing_permission'
|
5
|
+
|
6
|
+
class ThingPermissionTest < Test::Unit::TestCase
|
7
|
+
|
8
|
+
Permission = Indulgence::Permission
|
9
|
+
|
10
|
+
def setup
|
11
|
+
god = Role.create(:name => 'god')
|
12
|
+
mortal = Role.create(:name => 'mortal')
|
13
|
+
@user = User.create(:name => 'Trevor', :role_id => mortal.id)
|
14
|
+
@super_user = User.create(:name => 'Jane', :role_id => god.id)
|
15
|
+
@original_role_name = Permission.role_method
|
16
|
+
@original_role_name_method = Permission.role_name_method
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_super_user_permissions
|
20
|
+
assert_equal Permission.all, ThingPermission.new(@super_user, :create).ability
|
21
|
+
assert_equal Permission.all, ThingPermission.new(@super_user, :read).ability
|
22
|
+
assert_equal Permission.all, ThingPermission.new(@super_user, :update).ability
|
23
|
+
assert_equal Permission.all, ThingPermission.new(@super_user, :delete).ability
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_default_permissions
|
27
|
+
assert_equal Permission.none, ThingPermission.new(@user, :create).ability
|
28
|
+
assert_equal Permission.all, ThingPermission.new(@user, :read).ability
|
29
|
+
assert_equal Permission.none, ThingPermission.new(@user, :update).ability
|
30
|
+
assert_equal Permission.none, ThingPermission.new(@user, :delete).ability
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_role_name
|
34
|
+
assert_equal @super_user.role.name.to_sym, ThingPermission.new(@super_user, :read).role_name
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_role_name_when_not_defined_in_permissions
|
38
|
+
assert_equal :default, ThingPermission.new(@user, :read).role_name
|
39
|
+
end
|
40
|
+
|
41
|
+
def test_setting_unknown_role_method_causes_error
|
42
|
+
Permission.role_method = :something_else
|
43
|
+
assert_raise NoMethodError do
|
44
|
+
ThingPermission.new(@super_user, :read).role_name
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_setting_role_method
|
49
|
+
Permission.role_method = :position
|
50
|
+
test_super_user_permissions
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_setting_unknown_role_name_method_causes_error
|
54
|
+
Permission.role_name_method = :something_else
|
55
|
+
assert_raise NoMethodError do
|
56
|
+
ThingPermission.new(@super_user, :read).role_name
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_setting_role_name_method
|
61
|
+
Permission.role_name_method = :title
|
62
|
+
test_super_user_permissions
|
63
|
+
end
|
64
|
+
|
65
|
+
def teardown
|
66
|
+
User.delete_all
|
67
|
+
Role.delete_all
|
68
|
+
Thing.delete_all
|
69
|
+
Permission.role_method = @original_role_name
|
70
|
+
Permission.role_name_method = @original_role_name_method
|
71
|
+
end
|
72
|
+
|
73
|
+
def make_things
|
74
|
+
@users_thing = Thing.create(:one, :owner_id => @user.id)
|
75
|
+
@super_users_thing = Thing.create(:two, :owner_id => @super_user.id)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require 'user'
|
3
|
+
require 'thing'
|
4
|
+
|
5
|
+
class ThingTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def setup
|
8
|
+
@god = Role.create(:name => 'god')
|
9
|
+
@demigod = Role.create(:name => 'demigod')
|
10
|
+
@owner = User.create(:name => 'Mary')
|
11
|
+
@thing = Thing.create(:name => 'Stuff', :owner_id => @owner.id)
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def test_user
|
16
|
+
assert_equal(@owner, @thing.owner)
|
17
|
+
assert_equal([@thing], @owner.things)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_indulge
|
21
|
+
make_second_thing
|
22
|
+
assert_equal(true, @thing.indulge?(@owner, :read))
|
23
|
+
assert_equal(false, @thing.indulge?(@owner, :delete))
|
24
|
+
assert_equal(false, @other_thing.indulge?(@owner, :delete))
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_indulge_by_god
|
28
|
+
make_second_thing
|
29
|
+
@owner.update_attribute(:role_id, @god.id)
|
30
|
+
assert_equal(true, @thing.indulge?(@owner, :read))
|
31
|
+
assert_equal(true, @thing.indulge?(@owner, :delete))
|
32
|
+
assert_equal(true, @other_thing.indulge?(@owner, :delete))
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_indulge_by_demigod
|
36
|
+
make_second_thing
|
37
|
+
@owner.update_attribute(:role_id, @demigod.id)
|
38
|
+
assert_equal(true, @thing.indulge?(@owner, :read))
|
39
|
+
assert_equal(true, @thing.indulge?(@owner, :delete))
|
40
|
+
assert_equal(false, @other_thing.indulge?(@owner, :delete))
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_indulge_other_thing
|
44
|
+
other_thing = OtherThing.create(:name => 'Other Stuff', :owner_id => @owner.id)
|
45
|
+
assert_equal(true, other_thing.indulge?(@owner, :read))
|
46
|
+
assert_equal(false, other_thing.indulge?(@owner, :delete))
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_indulgence
|
50
|
+
make_second_thing
|
51
|
+
@owner.update_attribute(:role_id, @demigod.id)
|
52
|
+
assert_equal(Thing.order('id'), Thing.indulgence(@owner, :read).order('id'))
|
53
|
+
assert_equal(Thing.order('id'), Thing.indulgence(@user, :read).order('id'))
|
54
|
+
assert_equal([@thing], Thing.indulgence(@owner, :delete))
|
55
|
+
assert_raise ActiveRecord::RecordNotFound do
|
56
|
+
Thing.indulgence(@user, :delete)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_find
|
61
|
+
make_second_thing
|
62
|
+
@owner.update_attribute(:role_id, @demigod.id)
|
63
|
+
assert_equal(@thing, Thing.indulgence(@owner, :delete).find(@thing.id))
|
64
|
+
assert_raise ActiveRecord::RecordNotFound do
|
65
|
+
assert_equal(@thing, Thing.indulgence(@user, :delete).find(@thing.id))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_truth_method
|
70
|
+
make_second_thing
|
71
|
+
assert_equal(true, @thing.permit?(@owner, :read))
|
72
|
+
assert_equal(false, @thing.permit?(@owner, :delete))
|
73
|
+
assert_equal(false, @other_thing.permit?(@owner, :delete))
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_where_method
|
77
|
+
make_second_thing
|
78
|
+
@owner.update_attribute(:role_id, @demigod.id)
|
79
|
+
assert_equal(Thing.order('id'), Thing.permitted(@owner, :read).order('id'))
|
80
|
+
assert_equal(Thing.order('id'), Thing.permitted(@user, :read).order('id'))
|
81
|
+
assert_equal([@thing], Thing.permitted(@owner, :delete))
|
82
|
+
assert_raise ActiveRecord::RecordNotFound do
|
83
|
+
Thing.permitted(@user, :delete)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def make_second_thing
|
88
|
+
@user = User.create(:name => 'Clive')
|
89
|
+
@other_thing = Thing.create(:name => 'Debris', :owner_id => @user.id)
|
90
|
+
end
|
91
|
+
|
92
|
+
def teardown
|
93
|
+
Role.delete_all
|
94
|
+
User.delete_all
|
95
|
+
Thing.delete_all
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require 'user'
|
3
|
+
require 'role'
|
4
|
+
|
5
|
+
class UserTest < Test::Unit::TestCase
|
6
|
+
def teardown
|
7
|
+
User.delete_all
|
8
|
+
Role.delete_all
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_create
|
12
|
+
user = User.create(:name => 'Bob')
|
13
|
+
assert_equal(user, User.find_by_name('Bob'))
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_role
|
17
|
+
role = Role.create(:name => 'god')
|
18
|
+
user = User.create(:name => 'Harry', :role_id => role.id)
|
19
|
+
assert_equal(role, user.role)
|
20
|
+
end
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: indulgence
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Rob Nichols
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-10 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: sqlite3
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: Packages permission functionality into a set of permission objects.
|
47
|
+
email:
|
48
|
+
- rob@undervale.co.uk
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- lib/active_record/acts/indulgent.rb
|
54
|
+
- lib/indulgence/version.rb
|
55
|
+
- lib/indulgence/indulgent.rb
|
56
|
+
- lib/indulgence/ability.rb
|
57
|
+
- lib/indulgence/permission.rb
|
58
|
+
- lib/indulgence.rb
|
59
|
+
- MIT-LICENSE
|
60
|
+
- Rakefile
|
61
|
+
- README.rdoc
|
62
|
+
- test/units/role_test.rb
|
63
|
+
- test/units/indulgence/permission_test.rb
|
64
|
+
- test/units/indulgence/ability_test.rb
|
65
|
+
- test/units/user_test.rb
|
66
|
+
- test/units/thing_permission_test.rb
|
67
|
+
- test/units/thing_test.rb
|
68
|
+
- test/lib/role.rb
|
69
|
+
- test/lib/user.rb
|
70
|
+
- test/lib/thing.rb
|
71
|
+
- test/lib/thing_permission.rb
|
72
|
+
- test/db/migrate/20130408103015_create_roles.rb
|
73
|
+
- test/db/migrate/20130408085511_create_users.rb
|
74
|
+
- test/db/migrate/20130408132217_create_things.rb
|
75
|
+
- test/db/config.yml
|
76
|
+
- test/db/test.sqlite3.db
|
77
|
+
- test/db/schema.rb
|
78
|
+
- test/test_helper.rb
|
79
|
+
homepage: https://github.com/reggieb/indulgence
|
80
|
+
licenses: []
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ! '>='
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ! '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
requirements: []
|
98
|
+
rubyforge_project:
|
99
|
+
rubygems_version: 1.8.24
|
100
|
+
signing_key:
|
101
|
+
specification_version: 3
|
102
|
+
summary: Yet another permissions gem
|
103
|
+
test_files:
|
104
|
+
- test/units/role_test.rb
|
105
|
+
- test/units/indulgence/permission_test.rb
|
106
|
+
- test/units/indulgence/ability_test.rb
|
107
|
+
- test/units/user_test.rb
|
108
|
+
- test/units/thing_permission_test.rb
|
109
|
+
- test/units/thing_test.rb
|
110
|
+
- test/lib/role.rb
|
111
|
+
- test/lib/user.rb
|
112
|
+
- test/lib/thing.rb
|
113
|
+
- test/lib/thing_permission.rb
|
114
|
+
- test/db/migrate/20130408103015_create_roles.rb
|
115
|
+
- test/db/migrate/20130408085511_create_users.rb
|
116
|
+
- test/db/migrate/20130408132217_create_things.rb
|
117
|
+
- test/db/config.yml
|
118
|
+
- test/db/test.sqlite3.db
|
119
|
+
- test/db/schema.rb
|
120
|
+
- test/test_helper.rb
|