mongoid_ability 0.0.11 → 0.1.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -13
  3. data/lib/mongoid_ability.rb +8 -2
  4. data/lib/mongoid_ability/ability.rb +32 -30
  5. data/lib/mongoid_ability/accessible_query_builder.rb +61 -41
  6. data/lib/mongoid_ability/lock.rb +16 -29
  7. data/lib/mongoid_ability/owner.rb +6 -22
  8. data/lib/mongoid_ability/resolve_default_locks.rb +15 -0
  9. data/lib/mongoid_ability/resolve_inherited_locks.rb +32 -0
  10. data/lib/mongoid_ability/resolve_locks.rb +34 -0
  11. data/lib/mongoid_ability/resolve_owner_locks.rb +25 -0
  12. data/lib/mongoid_ability/subject.rb +28 -32
  13. data/lib/mongoid_ability/version.rb +1 -1
  14. data/test/mongoid_ability/ability_role_test.rb +11 -5
  15. data/test/mongoid_ability/ability_test.rb +67 -91
  16. data/test/mongoid_ability/accessible_query_builder_test.rb +9 -5
  17. data/test/mongoid_ability/can_options_test.rb +17 -0
  18. data/test/mongoid_ability/lock_test.rb +51 -70
  19. data/test/mongoid_ability/owner_locks_test.rb +42 -0
  20. data/test/mongoid_ability/owner_test.rb +12 -39
  21. data/test/mongoid_ability/resolve_default_locks_test.rb +27 -0
  22. data/test/mongoid_ability/resolve_inherited_locks_test.rb +49 -0
  23. data/test/mongoid_ability/resolve_locks_test.rb +25 -0
  24. data/test/mongoid_ability/resolve_owner_locks_test.rb +50 -0
  25. data/test/mongoid_ability/subject_accessible_by_test.rb +135 -0
  26. data/test/mongoid_ability/subject_test.rb +20 -201
  27. data/test/support/test_classes.rb +136 -61
  28. data/test/test_helper.rb +3 -2
  29. metadata +20 -5
  30. data/lib/mongoid_ability/ability_resolver.rb +0 -42
  31. data/test/mongoid_ability/ability_resolver_test.rb +0 -78
@@ -1,50 +1,35 @@
1
1
  module MongoidAbility
2
2
  module Owner
3
-
4
3
  def self.included base
5
4
  base.extend ClassMethods
6
5
  base.class_eval do
7
6
  delegate :can?, :cannot?, to: :ability
8
-
9
7
  before_save :cleanup_locks
10
8
  end
11
9
  end
12
10
 
13
- # =====================================================================
11
+ # ---------------------------------------------------------------------
14
12
 
15
13
  module ClassMethods
16
- # override if needed
17
- # return for example :my_locks
18
14
  def locks_relation_name
19
- @locks_relation_name ||= relations.detect{ |name, meta| meta.class_name == lock_class_name }.first.to_sym
15
+ :locks
20
16
  end
21
17
 
22
- # override if your relation is named differently
23
- def roles_relation_name
18
+ def inherit_from_relation_name
24
19
  :roles
25
20
  end
26
-
27
- # override if needed
28
- # return for example 'MyLock'
29
- def lock_class_name
30
- lock_classes = ObjectSpace.each_object(Class).select{ |cls| cls < MongoidAbility::Lock }
31
- lock_superclasses = lock_classes.reject{ |cls| lock_classes.any?{ |c| cls < c } }
32
- @lock_class_name ||= lock_superclasses.first.name
33
- end
34
21
  end
35
22
 
36
- # =====================================================================
23
+ # ---------------------------------------------------------------------
37
24
 
38
25
  def locks_relation
39
26
  self.send(self.class.locks_relation_name)
40
27
  end
41
28
 
42
- def roles_relation
43
- self.send(self.class.roles_relation_name)
29
+ def inherit_from_relation
30
+ self.send(self.class.inherit_from_relation_name)
44
31
  end
45
32
 
46
- # ---------------------------------------------------------------------
47
-
48
33
  def ability
49
34
  @ability ||= MongoidAbility::Ability.new(self)
50
35
  end
@@ -56,6 +41,5 @@ module MongoidAbility
56
41
  lock.destroy if locks_relation.where(action: lock.action, subject_type: lock.subject_type, subject_id: lock.subject_id).any?(&:closed?)
57
42
  end
58
43
  end
59
-
60
44
  end
61
45
  end
@@ -0,0 +1,15 @@
1
+ module MongoidAbility
2
+ class ResolveDefaultLocks < ResolveLocks
3
+
4
+ def call
5
+ return false if default_locks.any?{ |l| l.closed?(options) }
6
+ return true if default_locks.any?{ |l| l.open?(options) }
7
+ end
8
+
9
+ private # =============================================================
10
+
11
+ def default_locks
12
+ subject_class.default_locks.select{ |l| l.action.to_s == action.to_s }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ module MongoidAbility
2
+ class ResolveInheritedLocks < ResolveLocks
3
+
4
+ def call
5
+ uo = user_outcome
6
+ return uo if uo != nil
7
+
8
+ if owner.respond_to?(owner.class.inherit_from_relation_name) && owner.inherit_from_relation != nil
9
+ io = owner.inherit_from_relation.collect { |inherited_owner| inherited_owner_outcome(inherited_owner) }.compact
10
+ return io.any?{ |o| o == true } unless io.empty?
11
+ end
12
+
13
+ default_outcome
14
+ end
15
+
16
+ private # =============================================================
17
+
18
+ def user_outcome
19
+ @user_outcome ||= ResolveOwnerLocks.call(owner, action, subject_class, subject, options)
20
+ end
21
+
22
+ def inherited_owner_outcome inherited_owner
23
+ @inherited_owner_outcome ||= {}
24
+ @inherited_owner_outcome[inherited_owner] ||= ResolveOwnerLocks.call(inherited_owner, action, subject_class, subject, options)
25
+ end
26
+
27
+ def default_outcome
28
+ @default_outcome ||= ResolveDefaultLocks.call(nil, action, subject_class, nil, options)
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ module MongoidAbility
2
+ class ResolveLocks < Struct.new(:owner, :action, :subject_type, :subject, :options)
3
+
4
+ attr_reader(
5
+ :subject_class,
6
+ :subject_id
7
+ )
8
+
9
+ def self.call(*args)
10
+ new(*args).call
11
+ end
12
+
13
+ # =====================================================================
14
+
15
+ def initialize(*args)
16
+ super(*args)
17
+
18
+ @subject_class = subject_type.to_s.constantize
19
+ @subject_id = subject.id if subject.present?
20
+
21
+ raise StandardError, "#{subject_type} class does not have default locks" unless @subject_class.respond_to?(:default_locks)
22
+ raise StandardError, "#{subject_type} class does not have default lock for :#{action} action" unless @subject_class.self_and_ancestors_with_default_locks.any? do |cls|
23
+ cls.default_locks.any?{ |l| l.action == action }
24
+ end
25
+ end
26
+
27
+ # =====================================================================
28
+
29
+ def call
30
+ raise NotImplementedError
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ module MongoidAbility
2
+ class ResolveOwnerLocks < ResolveLocks
3
+
4
+ def call
5
+ locks_for_subject_type = owner.locks_relation.for_action(action).for_subject_type(subject_type)
6
+
7
+ return unless locks_for_subject_type.exists?
8
+
9
+ # return outcome if owner defines lock for id
10
+ if subject.present?
11
+ id_locks = locks_for_subject_type.id_locks.for_subject_id(subject_id)
12
+ return false if id_locks.any?{ |l| l.closed?(options) }
13
+ return true if id_locks.any?{ |l| l.open?(options) }
14
+ end
15
+
16
+ # return outcome if owner defines lock for subject_type
17
+ class_locks = locks_for_subject_type.class_locks
18
+ return false if class_locks.class_locks.any?{ |l| l.closed?(options) }
19
+ return true if class_locks.class_locks.any?{ |l| l.open?(options) }
20
+
21
+ nil
22
+ end
23
+
24
+ end
25
+ end
@@ -3,58 +3,54 @@ module MongoidAbility
3
3
 
4
4
  def self.included base
5
5
  base.extend ClassMethods
6
- base.class_eval do
7
- end
8
6
  end
9
7
 
10
- # =====================================================================
8
+ # ---------------------------------------------------------------------
11
9
 
12
10
  module ClassMethods
13
11
  def default_locks
14
- @default_locks ||= []
12
+ @default_locks ||= DefaultLocksExtension.new
15
13
  end
16
14
 
17
- def default_locks_with_inherited
18
- return default_locks unless superclass.respond_to?(:default_locks_with_inherited)
19
- superclass.default_locks_with_inherited.concat(default_locks)
15
+ def default_locks= locks
16
+ @default_locks = DefaultLocksExtension.new(locks)
20
17
  end
21
18
 
22
- def default_locks= val
23
- @default_locks = val
19
+ def default_lock lock_cls, action, outcome, attrs={}
20
+ default_locks << lock_cls.new( { subject_type: self.to_s, action: action, outcome: outcome }.merge(attrs))
24
21
  end
25
22
 
26
- def default_lock action, outcome
27
- if existing_lock = default_locks.detect{ |l| l.action.to_s == action.to_s }
28
- existing_lock.outcome = outcome
29
- else
30
- default_locks << lock_class_name.constantize.new(subject_type: self, action: action, outcome: outcome)
31
- end
23
+ def self_and_ancestors_with_default_locks
24
+ ancestors.select { |a| a.is_a?(Class) && a.respond_to?(:default_locks) }
32
25
  end
33
26
 
34
- # ---------------------------------------------------------------------
27
+ def ancestors_with_default_locks
28
+ self_and_ancestors_with_default_locks - [self]
29
+ end
35
30
 
36
- # override if needed
37
- # return for example 'MyLock'
38
- def lock_class_name
39
- lock_classes = ObjectSpace.each_object(Class).select{ |cls| cls < MongoidAbility::Lock }
40
- lock_superclasses = lock_classes.reject{ |cls| lock_classes.any?{ |c| cls < c } }
41
- @lock_class_name ||= lock_superclasses.first.name
31
+ def accessible_by ability, action=:read, options={}
32
+ AccessibleQueryBuilder.call(self, ability, action, options)
42
33
  end
34
+ end
43
35
 
44
- # ---------------------------------------------------------------------
36
+ # ---------------------------------------------------------------------
45
37
 
46
- def self_and_ancestors_with_default_locks
47
- self.ancestors.select{ |a| a.is_a?(Class) && a.respond_to?(:default_locks) }
48
- end
38
+ require 'forwardable'
39
+ class DefaultLocksExtension
40
+ extend Forwardable
41
+ def_delegators :@default_locks, :any?, :collect, :delete, :detect, :first, :map, :push, :select
49
42
 
50
- def ancestors_with_default_locks
51
- self_and_ancestors_with_default_locks - [self]
52
- end
43
+ attr_reader :default_locks
53
44
 
54
- # ---------------------------------------------------------------------
45
+ def initialize default_locks=[]
46
+ @default_locks = default_locks
47
+ end
55
48
 
56
- def accessible_by ability, action=:read
57
- AccessibleQueryBuilder.call(self, ability, action)
49
+ def << lock
50
+ if existing_lock = self.detect{ |l| l.action.to_s == lock.action.to_s }
51
+ @default_locks.delete(existing_lock)
52
+ end
53
+ @default_locks.push lock
58
54
  end
59
55
  end
60
56
 
@@ -1,3 +1,3 @@
1
1
  module MongoidAbility
2
- VERSION = "0.0.11"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -3,22 +3,28 @@ require "test_helper"
3
3
  module MongoidAbility
4
4
  describe 'ability on Role' do
5
5
 
6
- let(:read_lock) { TestLock.new(subject_type: TestAbilitySubject.to_s, action: :read, outcome: false) }
7
- let(:role) { TestRole.new(test_locks: [read_lock]) }
6
+ let(:read_lock) { MyLock.new(subject_type: MySubject, action: :read, outcome: false) }
7
+ let(:role) { MyRole.new(my_locks: [ read_lock ]) }
8
8
  let(:ability) { Ability.new(role) }
9
9
 
10
10
  # ---------------------------------------------------------------------
11
11
 
12
+ before do
13
+ MySubject.default_locks = [ MyLock.new(action: :read, outcome: true) ]
14
+ end
15
+
16
+ # ---------------------------------------------------------------------
17
+
12
18
  it 'role can?' do
13
- ability.can?(:read, TestAbilitySubject).must_equal false
19
+ ability.can?(:read, MySubject).must_equal false
14
20
  end
15
21
 
16
22
  it 'role cannot?' do
17
- ability.cannot?(:update, TestAbilitySubject).must_equal false
23
+ ability.cannot?(:read, MySubject).must_equal true
18
24
  end
19
25
 
20
26
  it 'is accessible by' do
21
- TestAbilitySubject.accessible_by(ability, :read).must_be_kind_of Mongoid::Criteria
27
+ MySubject.accessible_by(ability, :read).must_be_kind_of Mongoid::Criteria
22
28
  end
23
29
 
24
30
  end
@@ -2,50 +2,53 @@ require "test_helper"
2
2
 
3
3
  module MongoidAbility
4
4
  describe Ability do
5
-
6
- let(:user) { TestUser.new }
7
- let(:ability) { Ability.new(user) }
8
-
9
- # ---------------------------------------------------------------------
5
+ let(:owner) { MyOwner.new }
6
+ let(:ability) { Ability.new(owner) }
10
7
 
11
8
  it 'exposes owner' do
12
- ability.owner.must_equal user
9
+ ability.owner.must_equal owner
13
10
  end
14
11
 
15
- # ---------------------------------------------------------------------
16
-
17
12
  describe 'default locks' do
13
+ before do
14
+ # NOTE: we might need to use the .default_lock macro in case we propagate down directly
15
+ MySubject.default_locks = [ MyLock.new(subject_type: MySubject, action: :update, outcome: true) ]
16
+ MySubject1.default_locks = []
17
+ MySubject2.default_locks = []
18
+ end
19
+
18
20
  it 'propagates from superclass to all subclasses' do
19
- ability.can?(:update, TestAbilitySubjectSuper1).must_equal true
20
- ability.can?(:update, TestAbilitySubject).must_equal true
21
+ ability.can?(:update, MySubject).must_equal true
22
+ ability.can?(:update, MySubject1).must_equal true
23
+ ability.can?(:update, MySubject2).must_equal true
21
24
  end
25
+ end
22
26
 
23
- describe 'when defined for all superclasses' do
24
- it 'propagates default locks to subclasses' do
25
- ability.can?(:read, TestAbilitySubjectSuper2).must_equal false
26
- TestAbilitySubjectSuper1.stub(:default_locks, [
27
- TestLock.new(subject_type: TestAbilitySubjectSuper1.to_s, action: :read, outcome: false)
28
- ]) do
29
- ability.can?(:read, TestAbilitySubjectSuper1).must_equal false
30
- end
31
- TestAbilitySubject.stub(:default_locks, [
32
- TestLock.new(subject_type: TestAbilitySubject.to_s, action: :read, outcome: true)
33
- ]) do
34
- ability.can?(:read, TestAbilitySubject).must_equal true
35
- end
36
- end
27
+ describe 'when defined for all superclasses' do
28
+ before do
29
+ MySubject.default_locks = [ MyLock.new(subject_type: MySubject, action: :read, outcome: false) ]
30
+ MySubject1.default_locks = [ MyLock.new(subject_type: MySubject1, action: :read, outcome: true) ]
31
+ MySubject2.default_locks = [ MyLock.new(subject_type: MySubject2, action: :read, outcome: false) ]
37
32
  end
38
33
 
39
- describe 'when defined for some superclasses' do
40
- it 'propagates default locks to subclasses' do
41
- ability.can?(:read, TestAbilitySubjectSuper2).must_equal false
42
- ability.can?(:read, TestAbilitySubjectSuper1).must_equal false
43
- TestAbilitySubject.stub(:default_locks, [
44
- TestLock.new(subject_type: TestAbilitySubjectSuper1.to_s, action: :read, outcome: true)
45
- ]) do
46
- ability.can?(:read, TestAbilitySubject).must_equal true
47
- end
48
- end
34
+ it 'respects the definitions' do
35
+ ability.can?(:read, MySubject).must_equal false
36
+ ability.can?(:read, MySubject1).must_equal true
37
+ ability.can?(:read, MySubject2).must_equal false
38
+ end
39
+ end
40
+
41
+ describe 'when defined for some superclasses' do
42
+ before do
43
+ MySubject.default_locks = [ MyLock.new(subject_type: MySubject, action: :read, outcome: false) ]
44
+ MySubject1.default_locks = []
45
+ MySubject2.default_locks = [ MyLock.new(subject_type: MySubject2, action: :read, outcome: true) ]
46
+ end
47
+
48
+ it 'propagates default locks to subclasses' do
49
+ ability.can?(:read, MySubject).must_equal false
50
+ ability.can?(:read, MySubject1).must_equal false
51
+ ability.can?(:read, MySubject2).must_equal true
49
52
  end
50
53
  end
51
54
 
@@ -54,49 +57,45 @@ module MongoidAbility
54
57
  describe 'user locks' do
55
58
  describe 'when defined for superclass' do
56
59
  before do
57
- user.tap do |u|
58
- u.test_locks = [TestLock.new(subject_type: TestAbilitySubjectSuper2.to_s, action: :read, outcome: true)]
59
- end
60
+ MySubject.default_locks = [ MyLock.new(subject_type: MySubject, action: :read, outcome: false) ]
61
+ MySubject1.default_locks = []
62
+ MySubject2.default_locks = []
63
+ owner.my_locks = [ MyLock.new(subject_type: MySubject, action: :read, outcome: true) ]
60
64
  end
65
+
61
66
  it 'applies the superclass lock' do
62
- ability.can?(:read, TestAbilitySubject).must_equal true
67
+ ability.can?(:read, MySubject2).must_equal true
63
68
  end
64
69
  end
65
70
  end
66
71
 
67
72
  # ---------------------------------------------------------------------
68
73
 
69
- describe 'role locks' do
70
- describe 'when multiple roles' do
74
+ describe 'inherited owner locks' do
75
+ describe 'when multiple inherited owners' do
71
76
  before do
72
- user.tap do |u|
73
- u.roles = [
74
- TestRole.new(name: 'Editor', test_locks: [
75
- TestLock.new(subject_type: TestAbilitySubjectSuper2.to_s, action: :read, outcome: true)
76
- ]),
77
- TestRole.new(name: 'SysOp', test_locks: [
78
- TestLock.new(subject_type: TestAbilitySubjectSuper2.to_s, action: :read, outcome: false)
79
- ])
80
- ]
81
- end
77
+ MySubject.default_locks = [ MyLock.new(subject_type: MySubject, action: :read, outcome: false) ]
78
+ owner.my_roles = [
79
+ MyRole.new(my_locks: [ MyLock.new(subject_type: MySubject, action: :read, outcome: true) ]),
80
+ MyRole.new(my_locks: [ MyLock.new(subject_type: MySubject, action: :read, outcome: false) ])
81
+ ]
82
82
  end
83
+
83
84
  it 'prefers positive outcome' do
84
- ability.can?(:read, TestAbilitySubjectSuper2).must_equal true
85
+ ability.can?(:read, MySubject).must_equal true
85
86
  end
86
87
  end
87
88
 
88
89
  describe 'when defined for superclass' do
89
90
  before do
90
- user.tap do |u|
91
- u.roles = [
92
- TestRole.new(test_locks: [
93
- TestLock.new(subject_type: TestAbilitySubjectSuper2.to_s, action: :read, outcome: true)
94
- ])
95
- ]
96
- end
91
+ MySubject.default_locks = [ MyLock.new(subject_type: MySubject, action: :read, outcome: false) ]
92
+ MySubject1.default_locks = []
93
+ MySubject2.default_locks = []
94
+ owner.my_roles = [ MyRole.new(my_locks: [ MyLock.new(subject_type: MySubject, action: :read, outcome: true) ]) ]
97
95
  end
96
+
98
97
  it 'applies the superclass lock' do
99
- ability.can?(:read, TestAbilitySubject).must_equal true
98
+ ability.can?(:read, MySubject2).must_equal true
100
99
  end
101
100
  end
102
101
  end
@@ -106,47 +105,24 @@ module MongoidAbility
106
105
  describe 'combined locks' do
107
106
  describe 'user and role locks' do
108
107
  before do
109
- user.tap do |u|
110
- u.test_locks = [
111
- TestLock.new(subject_type: TestAbilitySubjectSuper2.to_s, action: :read, outcome: false)
112
- ]
113
- u.roles = [
114
- TestRole.new(test_locks: [
115
- TestLock.new(subject_type: TestAbilitySubjectSuper2.to_s, action: :read, outcome: true)
116
- ])
117
- ]
118
- end
108
+ MySubject.default_locks = [ MyLock.new(subject_type: MySubject, action: :read, outcome: false) ]
109
+ owner.my_locks = [ MyLock.new(subject_type: MySubject, action: :read, outcome: false) ]
110
+ owner.my_roles = [ MyRole.new(my_locks: [ MyLock.new(subject_type: MySubject, action: :read, outcome: true) ]) ]
119
111
  end
112
+
120
113
  it 'prefers user locks' do
121
- ability.can?(:read, TestAbilitySubjectSuper2).must_equal false
114
+ ability.can?(:read, MySubject).must_equal false
122
115
  end
123
116
  end
124
117
 
125
118
  describe 'roles and default locks' do
126
119
  before do
127
- user.tap do |u|
128
- u.roles = [
129
- TestRole.new(test_locks: [
130
- TestLock.new(subject_type: TestAbilitySubjectSuper2.to_s, action: :read, outcome: true)
131
- ])
132
- ]
133
- end
120
+ MySubject.default_locks = [ MyLock.new(subject_type: MySubject, action: :read, outcome: false) ]
121
+ owner.my_roles = [ MyRole.new(my_locks: [ MyLock.new(subject_type: MySubject, action: :read, outcome: true) ]) ]
134
122
  end
135
- it 'prefers role locks' do
136
- ability.can?(:read, TestAbilitySubjectSuper2).must_equal true
137
- end
138
- end
139
- end
140
123
 
141
- # ---------------------------------------------------------------------
142
-
143
- describe 'class locks' do
144
- it 'prefers negative outcome across same class' do
145
- TestAbilityResolverSubject.stub(:default_locks, [
146
- TestLock.new(subject_type: TestAbilityResolverSubject.to_s, action: :read, outcome: false),
147
- TestLock.new(subject_type: TestAbilityResolverSubject.to_s, action: :read, outcome: true)
148
- ]) do
149
- ability.can?(:read, TestAbilityResolverSubject).must_equal false
124
+ it 'prefers role locks' do
125
+ ability.can?(:read, MySubject).must_equal true
150
126
  end
151
127
  end
152
128
  end