indulgence 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|