indulgence 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -9,8 +9,8 @@ things:
9
9
  * Filtered a search of objects based on the those permissions
10
10
 
11
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.
12
+ cover most of the use cases I could think of. So permissions were sub-divided
13
+ into the 'abilities': create, read, update, and delete.
14
14
 
15
15
  The other requirement was that the permission for an object could be defined
16
16
  succinctly within a single file.
@@ -45,12 +45,10 @@ placing it in app/permissions/thing_permission.rb
45
45
  == Users and Roles
46
46
 
47
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.
48
+ (e.g. User). The default behaviour assumes that the entity object has a :role
49
+ method that returns the role object, and that the role object has a :name method.
50
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:
51
+ So typically, these objects could look like this:
54
52
 
55
53
  class User < ActiveRecord::Base
56
54
  belongs_to :role
@@ -68,7 +66,7 @@ typically, these objects could look like this:
68
66
  :role_id => role.id
69
67
  )
70
68
 
71
- == indulge?
69
+ == Compare single item: indulge?
72
70
 
73
71
  Simple true/false permission can be determined using the :indulge? method:
74
72
 
@@ -79,7 +77,7 @@ Simple true/false permission can be determined using the :indulge? method:
79
77
  thing.indulge?(user, :update) == false
80
78
  thing.indulge?(user, :delete) == false
81
79
 
82
- == indulgence
80
+ == Filter many: indulgence
83
81
 
84
82
  The :indulgence method is used as a where filter:
85
83
 
@@ -128,8 +126,8 @@ _default_ rather than being defined in _emperor_, as it is already set to *all*.
128
126
 
129
127
  abilities is a hash of hashes. The lowest level, associates action names with
130
128
  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:
129
+ object hashes. In this simple case, construction is perhaps clearer if the
130
+ abilities method above was written like this:
133
131
 
134
132
  def abilities
135
133
  {
@@ -179,21 +177,27 @@ permissions. This can be done by adding this method to ThingPermission:
179
177
  def things_they_wrote
180
178
  define_ability(
181
179
  :name => :things_they_wrote,
182
- :truth => lambda {|thing| thing.author_id == entity.id},
183
- :where_clause => {:author_id => entity.id}
180
+ :compare_single => lambda {|thing, user| thing.author_id == user.id},
181
+ :filter_many => lambda {|things, user| things.where(:author_id => user.id)}
184
182
  )
185
183
  end
186
184
 
187
185
  This will create an Ability object with the following methods:
188
186
 
189
- [name] Allows abilities of the same kind to be matched
190
- [truth] Used by :indulge?
191
- [where_clause] Used by :indulgence
187
+ [name] Allows abilities of the same kind to be matched
188
+ [compare_single] Used by :indulge?
189
+ [filter_many] Used by :indulgence
192
190
 
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.
191
+ ==== Compare single
195
192
 
196
- ==== The where clause
193
+ _compare_single_ should be a lambda that is passed the object being tested and
194
+ the entity it is to be tested against. The lambda returns _true_ if permission
195
+ should be given. Otherwise _false_ should be returned.
196
+
197
+ Alternatively, instead of a lambda, any object can be used that returns _true_
198
+ or _false_ when it _call_ method is passed the object and the entity.
199
+
200
+ ==== Filter many
197
201
 
198
202
  In this example:
199
203
 
@@ -201,14 +205,6 @@ In this example:
201
205
 
202
206
  if the :read ability is matched to *things_they_wrote*
203
207
 
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
208
  Once *things_they_wrote* has been defined, we can use it to define a new set
213
209
  of abilities:
214
210
 
@@ -297,8 +293,8 @@ The method names _indulgence_ and _indulge?_ may not suit your application. If
297
293
  you wish to use alternative names, they can be aliased like this:
298
294
 
299
295
  acts_as_indulgent(
300
- :truth_method => :permit?,
301
- :where_method => :permitted
296
+ :compare_single_method => :permit?,
297
+ :filter_many_method => :permitted
302
298
  )
303
299
 
304
300
  With this used to define indulgence in Thing, we can do this:
@@ -11,8 +11,8 @@ module ActiveRecord
11
11
  include Indulgence::Indulgent::InstanceMethods
12
12
  extend Indulgence::Indulgent::ClassMethods
13
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]
14
+ alias_method args[:compare_single_method], :indulge? if args[:compare_single_method]
15
+ singleton_class.send(:alias_method, args[:filter_many_method], :indulgence) if args[:filter_many_method]
16
16
  self.indulgent_permission_class = args[:using]
17
17
  end
18
18
 
@@ -1,19 +1,41 @@
1
1
 
2
2
  module Indulgence
3
3
  class Ability
4
- attr_reader :name, :truth, :where_clause
4
+ attr_reader :name, :compare_single, :filter_many
5
5
 
6
6
  def initialize(args = {})
7
7
  @name = args[:name]
8
- @truth = args[:truth]
9
- @where_clause = args[:where_clause]
8
+ @compare_single = args[:compare_single]
9
+ @filter_many = args[:filter_many]
10
+ valid?
10
11
  end
11
12
 
12
13
  def ==(another_ability)
13
- [:name, :truth, :where_clause].each do |method|
14
+ [:name, :compare_single, :filter_many].each do |method|
14
15
  return false if send(method) != another_ability.send(method)
15
16
  end
16
17
  return true
17
18
  end
19
+
20
+ def valid?
21
+ must_be_name
22
+ must_respond_to_call :compare_single
23
+ must_respond_to_call :filter_many
24
+ end
25
+
26
+ private
27
+ def must_be_name
28
+ unless name
29
+ raise AbilityConfigurationError, "A name is required for each ability"
30
+ end
31
+ end
32
+
33
+ def must_respond_to_call(method)
34
+ unless send(method).respond_to? :call
35
+ raise AbilityConfigurationError, "ability.#{method} must respond to call"
36
+ end
37
+ end
38
+
39
+
18
40
  end
19
41
  end
@@ -0,0 +1,5 @@
1
+ module Indulgence
2
+ class AbilityConfigurationError < StandardError; end
3
+ class NotFoundError < StandardError; end
4
+ class AbilityNotFound < StandardError; end
5
+ end
@@ -4,8 +4,9 @@ module Indulgence
4
4
  module ClassMethods
5
5
  def indulgence(entity, ability)
6
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)
7
+ permission.filter_many(self)
8
+ rescue Indulgence::NotFoundError, Indulgence::AbilityNotFound
9
+ raise_not_found
9
10
  end
10
11
 
11
12
  private
@@ -18,7 +19,7 @@ module Indulgence
18
19
 
19
20
  def indulge?(entity, ability)
20
21
  permission = self.class.indulgent_permission_class.new(entity, ability)
21
- return permission.indulge? self
22
+ return permission.compare_single self
22
23
  end
23
24
 
24
25
  end
@@ -1,5 +1,3 @@
1
- require_relative 'ability'
2
-
3
1
  module Indulgence
4
2
  class Permission
5
3
  attr_reader :entity, :ability
@@ -8,6 +6,7 @@ module Indulgence
8
6
  self
9
7
  @entity = entity
10
8
  @ability = abilities_for_role[ability]
9
+ raise AbilityNotFound, "Unable to find an ability called #{ability}" unless @ability
11
10
  end
12
11
 
13
12
  def abilities
@@ -18,12 +17,14 @@ module Indulgence
18
17
  raise 'There must always be a default'
19
18
  end
20
19
 
21
- def where
22
- ability.where_clause
20
+ def filter_many(things)
21
+ check_method_can_be_called(:filter_many)
22
+ ability.filter_many.call things, entity
23
23
  end
24
24
 
25
- def indulge?(thing)
26
- ability.truth.respond_to?(:call) ? ability.truth.call(thing) : ability.truth
25
+ def compare_single(thing)
26
+ check_method_can_be_called(:compare_single)
27
+ ability.compare_single.call thing, entity
27
28
  end
28
29
 
29
30
  @@role_method = :role
@@ -47,21 +48,24 @@ module Indulgence
47
48
  end
48
49
 
49
50
  def self.none
50
- @none ||= define_ability(
51
+ Permission.define_ability(
51
52
  :name => :none,
52
- :truth => false
53
+ :compare_single => lambda {|thing, entity| false},
54
+ :filter_many => lambda {|things, entity| raise NotFoundError}
53
55
  )
54
56
  end
55
57
 
56
58
  def self.all
57
- @all ||= define_ability(
59
+ Permission.define_ability(
58
60
  :name => :all,
59
- :truth => true
61
+ :compare_single => lambda {|thing, entity| true},
62
+ :filter_many => lambda {|things, entity| things.where(nil)}
60
63
  )
61
64
  end
62
65
 
63
66
  def self.define_ability(args)
64
- Ability.new args
67
+ raise AbilityConfigurationError, "A name is required for each ability" unless args[:name]
68
+ ability_cache[args[:name].to_sym] ||= Ability.new args
65
69
  end
66
70
 
67
71
  def define_ability(args)
@@ -99,6 +103,14 @@ module Indulgence
99
103
 
100
104
  def none
101
105
  self.class.none
102
- end
106
+ end
107
+
108
+ def self.ability_cache
109
+ @ability_cache ||= {}
110
+ end
111
+
112
+ def check_method_can_be_called(name)
113
+ raise AbilityConfigurationError, "#{name} method must respond to call" unless ability.send(name).respond_to? :call
114
+ end
103
115
  end
104
116
  end
@@ -1,10 +1,28 @@
1
1
  module Indulgence
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
4
4
 
5
5
  # History
6
6
  # =======
7
7
  #
8
+ # 0.0.2 Rebuild with lessons learnt from first usage in host app
9
+ #
10
+ # Adds automatic caching of abilites. Required a reworking of ability
11
+ # lambdas, so that a particular entity id wasn't cached
12
+ #
13
+ # Renamed ability methods truth as compare_single, and where_clause as
14
+ # filter_many as more descriptive.
15
+ #
16
+ # Forced Ability methods #compare_single and #filter_many to use lambdas
17
+ # (or other object that responds to call). The reasons:
18
+ #
19
+ # 1. To remove the special way that the none ability was handled.
20
+ # 2. Previously, if Ability#filter_many was nil everything would be returned,
21
+ # which I think is counter-intuitive.
22
+ # 3. Also if Ability#filter_many was undefined, then everything would
23
+ # be returned, which is just poor design for a permission tool. Now
24
+ # an error is raised.
25
+ #
8
26
  # 0.0.1 Initial build
9
27
  # First alpa version
10
28
  #
data/lib/indulgence.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require_relative 'indulgence/exceptions'
2
+ require_relative 'indulgence/ability'
1
3
  require_relative 'indulgence/permission'
2
4
  require_relative 'indulgence/indulgent'
3
5
  require_relative 'active_record/acts/indulgent'
Binary file
data/test/lib/thing.rb CHANGED
@@ -1,11 +1,14 @@
1
-
1
+ require 'indulgence'
2
+ require 'role'
3
+ require 'user'
4
+ require 'thing_permission'
2
5
  class Thing < ActiveRecord::Base
3
6
 
4
7
  belongs_to :owner, :class_name => 'User'
5
8
 
6
9
  acts_as_indulgent(
7
- :truth_method => :permit?,
8
- :where_method => :permitted
10
+ :compare_single_method => :permit?,
11
+ :filter_many_method => :permitted
9
12
  )
10
13
 
11
14
  end
@@ -36,10 +36,10 @@ class ThingPermission < Indulgence::Permission
36
36
  end
37
37
 
38
38
  def things_they_own
39
- @things_they_own ||= define_ability(
39
+ define_ability(
40
40
  :name => :things_they_own,
41
- :truth => lambda {|thing| thing.owner_id == entity.id},
42
- :where_clause => {:owner_id => entity.id}
41
+ :compare_single => lambda {|thing, user| thing.owner_id == user.id},
42
+ :filter_many => lambda {|things, user| things.where(:owner_id => user.id)}
43
43
  )
44
44
  end
45
45
 
@@ -1,36 +1,45 @@
1
1
  require_relative '../../test_helper'
2
2
  require 'ability'
3
+ require 'exceptions'
3
4
 
4
5
  module Indulgence
5
6
  class AbilityTest < Test::Unit::TestCase
6
7
 
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)
8
+ def setup
9
+ @attributes = {
10
+ name: :foo,
11
+ compare_single: lambda {|thing, entity| true},
12
+ filter_many: lambda {|entity| nil}
13
+ }
25
14
  end
26
15
 
27
16
  def test_equality
28
- attributes = {:name => :same, :true => true}
29
- ability = Ability.new(attributes)
30
- other_ability = Ability.new(attributes)
17
+ ability = Ability.new(@attributes)
18
+ other_ability = Ability.new(@attributes)
31
19
  assert(ability == other_ability, "#{ability} == #{other_ability} should return true")
32
20
  assert_equal(ability, other_ability)
33
21
  end
34
22
 
23
+ def test_name_absence_raises_error
24
+ @attributes.delete(:name)
25
+ assert_initiation_raises_error
26
+ end
27
+
28
+ def test_indulge_must_respond_to_call
29
+ @attributes[:compare_single] = true
30
+ assert_initiation_raises_error
31
+ end
32
+
33
+ def test_indulgence_must_respond_to_call
34
+ @attributes[:filter_many] = true
35
+ assert_initiation_raises_error
36
+ end
37
+
38
+ def assert_initiation_raises_error
39
+ assert_raise AbilityConfigurationError do
40
+ Ability.new(@attributes)
41
+ end
42
+ end
43
+
35
44
  end
36
45
  end
@@ -1,5 +1,7 @@
1
1
  require_relative '../../test_helper'
2
2
  require 'user'
3
+ require 'permission'
4
+ require 'ability'
3
5
 
4
6
  module Indulgence
5
7
  class PermissionTest < Test::Unit::TestCase
@@ -10,6 +12,19 @@ module Indulgence
10
12
  end
11
13
  end
12
14
 
15
+ def test_define_ability_uses_cache_rather_than_duplicates
16
+ args = {
17
+ name: :test_ability,
18
+ compare_single: lambda {|thing, entity| true},
19
+ filter_many: lambda {|entity| nil}
20
+ }
21
+ abilities_at_start = ObjectSpace.each_object(Ability).count
22
+ Permission.define_ability args
23
+ Permission.define_ability args
24
+ Permission.define_ability args
25
+ assert_equal((abilities_at_start + 1), ObjectSpace.each_object(Ability).count)
26
+ assert_equal Ability, Permission.send(:ability_cache)[:test_ability].class
27
+ end
13
28
 
14
29
  end
15
30
  end
@@ -62,6 +62,12 @@ class ThingPermissionTest < Test::Unit::TestCase
62
62
  test_super_user_permissions
63
63
  end
64
64
 
65
+ def test_with_unspecified_ability
66
+ assert_raise Indulgence::AbilityNotFound do
67
+ ThingPermission.new(@user, :unspecified)
68
+ end
69
+ end
70
+
65
71
  def teardown
66
72
  User.delete_all
67
73
  Role.delete_all
@@ -28,10 +28,17 @@ class ThingTest < Test::Unit::TestCase
28
28
  make_second_thing
29
29
  @owner.update_attribute(:role_id, @god.id)
30
30
  assert_equal(true, @thing.indulge?(@owner, :read))
31
+ @thing.indulge?(@owner, :delete)
31
32
  assert_equal(true, @thing.indulge?(@owner, :delete))
32
33
  assert_equal(true, @other_thing.indulge?(@owner, :delete))
33
34
  end
34
35
 
36
+ def test_indulgence_by_god
37
+ make_second_thing
38
+ @owner.update_attribute(:role_id, @god.id)
39
+ assert_equal(Thing.all, Thing.indulgence(@owner, :delete))
40
+ end
41
+
35
42
  def test_indulge_by_demigod
36
43
  make_second_thing
37
44
  @owner.update_attribute(:role_id, @demigod.id)
@@ -57,6 +64,12 @@ class ThingTest < Test::Unit::TestCase
57
64
  end
58
65
  end
59
66
 
67
+ def test_indulgence_with_unspecified_ability
68
+ assert_raise ActiveRecord::RecordNotFound do
69
+ Thing.indulgence(@owner, :unspecified)
70
+ end
71
+ end
72
+
60
73
  def test_find
61
74
  make_second_thing
62
75
  @owner.update_attribute(:role_id, @demigod.id)
@@ -66,14 +79,14 @@ class ThingTest < Test::Unit::TestCase
66
79
  end
67
80
  end
68
81
 
69
- def test_truth_method
82
+ def test_aliased_compare_single_method
70
83
  make_second_thing
71
84
  assert_equal(true, @thing.permit?(@owner, :read))
72
85
  assert_equal(false, @thing.permit?(@owner, :delete))
73
86
  assert_equal(false, @other_thing.permit?(@owner, :delete))
74
87
  end
75
88
 
76
- def test_where_method
89
+ def test_aliased_filter_many_method
77
90
  make_second_thing
78
91
  @owner.update_attribute(:role_id, @demigod.id)
79
92
  assert_equal(Thing.order('id'), Thing.permitted(@owner, :read).order('id'))
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: indulgence
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-10 00:00:00.000000000 Z
12
+ date: 2013-04-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -52,6 +52,7 @@ extra_rdoc_files: []
52
52
  files:
53
53
  - lib/active_record/acts/indulgent.rb
54
54
  - lib/indulgence/version.rb
55
+ - lib/indulgence/exceptions.rb
55
56
  - lib/indulgence/indulgent.rb
56
57
  - lib/indulgence/ability.rb
57
58
  - lib/indulgence/permission.rb