mongoid_ability 0.0.11 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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