mongoid_ability 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.
@@ -0,0 +1,59 @@
1
+ module MongoidAbility
2
+ module Owner
3
+
4
+ def self.included base
5
+ base.extend ClassMethods
6
+ base.class_eval do
7
+ delegate :can?, :cannot?, to: :ability
8
+
9
+ before_save :cleanup_locks
10
+ end
11
+ end
12
+
13
+ # =====================================================================
14
+
15
+ module ClassMethods
16
+ # override if needed
17
+ # return for example :my_locks
18
+ def locks_relation_name
19
+ @locks_relation_name ||= relations.detect{ |name, meta| meta.class_name == lock_class_name }.first.to_sym
20
+ end
21
+
22
+ # override if your relation is named differently
23
+ def roles_relation_name
24
+ :roles
25
+ end
26
+
27
+ # override if needed
28
+ # return for example 'MyLock'
29
+ def lock_class_name
30
+ @lock_class_name ||= Object.subclasses.detect{ |cls| cls < MongoidAbility::Lock }.name
31
+ end
32
+ end
33
+
34
+ # =====================================================================
35
+
36
+ def locks_relation
37
+ self.send(self.class.locks_relation_name)
38
+ end
39
+
40
+ def roles_relation
41
+ self.send(self.class.roles_relation_name)
42
+ end
43
+
44
+ # ---------------------------------------------------------------------
45
+
46
+ def ability
47
+ @ability ||= MongoidAbility::Ability.new(self)
48
+ end
49
+
50
+ private # =============================================================
51
+
52
+ def cleanup_locks
53
+ locks_relation.select(&:open?).each do |lock|
54
+ lock.destroy if locks_relation.where(action: lock.action, subject_type: lock.subject_type, subject_id: lock.subject_id).any?(&:closed?)
55
+ end
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,80 @@
1
+ module MongoidAbility
2
+ module Subject
3
+
4
+ def self.included base
5
+ base.extend ClassMethods
6
+ base.class_eval do
7
+ end
8
+ end
9
+
10
+ # =====================================================================
11
+
12
+ module ClassMethods
13
+ def default_locks
14
+ @default_locks ||= []
15
+ end
16
+
17
+ def default_locks= val
18
+ @default_locks = val
19
+ end
20
+
21
+ def default_lock action, outcome
22
+ default_locks << lock_class_name.constantize.new(subject_type: self, action: action, outcome: outcome)
23
+ end
24
+
25
+ # ---------------------------------------------------------------------
26
+
27
+ # override if needed
28
+ # return for example 'MyLock'
29
+ def lock_class_name
30
+ Object.subclasses.detect{ |cls| cls < MongoidAbility::Lock }.name
31
+ end
32
+
33
+ # ---------------------------------------------------------------------
34
+
35
+ def self_and_ancestors_with_default_locks
36
+ self.ancestors.select{ |a| a.is_a?(Class) && a.respond_to?(:default_locks) }
37
+ end
38
+
39
+ def ancestors_with_default_locks
40
+ self_and_ancestors_with_default_locks - [self]
41
+ end
42
+
43
+ # ---------------------------------------------------------------------
44
+
45
+ def accessible_by ability, action=:read
46
+ criteria = Mongoid::Criteria.new(self)
47
+
48
+ return criteria unless ability.user.present?
49
+
50
+ id_locks = [
51
+ ability.user,
52
+ ability.user.roles_relation
53
+ ].flatten.collect { |owner|
54
+ owner.locks_relation.for_subject_type(self.to_s).id_locks.for_action(action).to_a
55
+ }.flatten
56
+
57
+ if ability.can?(action, self)
58
+ criteria.nin({
59
+ _id: id_locks.map(&:subject_id).select do |subject_id|
60
+ subject = self.new
61
+ subject.id = subject_id
62
+ ability.cannot?(action, subject)
63
+ end
64
+ })
65
+ else
66
+ criteria.in({
67
+ _id: id_locks.map(&:subject_id).select do |subject_id|
68
+ subject = self.new
69
+ subject.id = subject_id
70
+ ability.can?(action, subject)
71
+ end
72
+ })
73
+ end
74
+ end
75
+ end
76
+
77
+ # =====================================================================
78
+
79
+ end
80
+ end
@@ -0,0 +1,3 @@
1
+ module MongoidAbility
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mongoid_ability/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mongoid_ability"
8
+ spec.version = MongoidAbility::VERSION
9
+ spec.authors = ["Tomas Celizna"]
10
+ spec.email = ["tomas.celizna@gmail.com"]
11
+ spec.summary = %q{Custom Ability class that allows CanCanCan authorization library store permissions in MongoDB via the Mongoid gem.}
12
+ spec.description = %q{Custom Ability class that allows CanCanCan authorization library store permissions in MongoDB via the Mongoid gem.}
13
+ spec.homepage = "https://github.com/tomasc/mongoid_ability"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "cancancan", "~> 1.9"
22
+ spec.add_dependency "mongoid", "~> 4.0"
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.6"
25
+ spec.add_development_dependency "coveralls"
26
+ spec.add_development_dependency "database_cleaner"
27
+ spec.add_development_dependency "guard"
28
+ spec.add_development_dependency "guard-minitest"
29
+ spec.add_development_dependency "minitest"
30
+ spec.add_development_dependency "rake"
31
+ end
@@ -0,0 +1,208 @@
1
+ require "test_helper"
2
+
3
+ module MongoidAbility
4
+ describe AbilityResolver do
5
+
6
+ let(:user) { TestUser.new }
7
+ let(:role_editor) { TestRole.new(name: 'Editor') }
8
+ let(:role_sysop) { TestRole.new(name: 'SysOp') }
9
+
10
+ let(:ability_resolver_subject) { TestAbilityResolverSubject.new }
11
+
12
+ subject { AbilityResolver.new(user, :read, TestAbilityResolverSubject.to_s) }
13
+ let(:subject_with_id) { AbilityResolver.new(user, :read, TestAbilityResolverSubject.to_s, ability_resolver_subject) }
14
+
15
+ # ---------------------------------------------------------------------
16
+
17
+ describe 'errors' do
18
+ it 'raises NameError for invalid subject_type' do
19
+ -> { ar = AbilityResolver.new(user, :read, 'Foo') }.must_raise NameError
20
+ end
21
+
22
+ it 'raises StandardError when subject_type does not have default_locks' do
23
+ -> { ar = AbilityResolver.new(user, :read, Object.to_s) }.must_raise StandardError
24
+ end
25
+
26
+ it 'raises StandardError when subject_type class or its ancestors does not have default_lock' do
27
+ TestAbilityResolverSubject.stub(:default_locks, []) do
28
+ -> { ar = AbilityResolver.new(user, :read, TestAbilityResolverSubject.to_s) }.must_raise StandardError
29
+ end
30
+ end
31
+ end
32
+
33
+ # ---------------------------------------------------------------------
34
+
35
+ describe '#outcome' do
36
+ describe 'when locks on both user and its roles' do
37
+ before do
38
+ user.roles = [
39
+ role_sysop.tap { |r| r.test_locks = [
40
+ TestLock.new(action: :read, outcome: true, subject_type: TestAbilityResolverSubject.to_s)
41
+ ]}
42
+ ]
43
+ user.test_locks = [
44
+ TestLock.new(action: :read, outcome: false, subject_type: TestAbilityResolverSubject.to_s)
45
+ ]
46
+ end
47
+ it 'prefers users locks' do
48
+ subject.outcome.must_equal false
49
+ end
50
+ end
51
+ describe 'when locks on roles and on class' do
52
+ before do
53
+ user.roles = [
54
+ role_sysop.tap { |r| r.test_locks = [
55
+ TestLock.new(action: :read, outcome: false, subject_type: TestAbilityResolverSubject.to_s)
56
+ ]}
57
+ ]
58
+ end
59
+ it 'prefers role locks' do
60
+ subject.outcome.must_equal false
61
+ end
62
+ end
63
+ end
64
+
65
+ # ---------------------------------------------------------------------
66
+
67
+ describe '#user_outcome' do
68
+ describe 'no locks' do
69
+ it 'returns nil if no locks for subject_type and action exists' do
70
+ subject.user_outcome.must_be_nil
71
+ end
72
+ end
73
+
74
+ describe 'id locks' do
75
+ it 'returns outcome' do
76
+ user.test_locks = [
77
+ TestLock.new(action: :read, subject: ability_resolver_subject, outcome: true)
78
+ ]
79
+ subject_with_id.user_outcome.must_equal true
80
+ end
81
+
82
+ it 'prefers negative outcome' do
83
+ user.test_locks = [
84
+ TestLock.new(action: :read, subject: ability_resolver_subject, outcome: true),
85
+ TestLock.new(action: :read, subject: ability_resolver_subject, outcome: false)
86
+ ]
87
+ subject_with_id.user_outcome.must_equal false
88
+ end
89
+ end
90
+
91
+ describe 'class locks' do
92
+ it 'returns outcome' do
93
+ user.test_locks = [
94
+ TestLock.new(action: :read, subject_type: TestAbilityResolverSubject.to_s, outcome: true)
95
+ ]
96
+ subject.user_outcome.must_equal true
97
+ end
98
+
99
+ it 'prefers negative outcome' do
100
+ user.test_locks = [
101
+ TestLock.new(action: :read, subject_type: TestAbilityResolverSubject.to_s, outcome: true),
102
+ TestLock.new(action: :read, subject_type: TestAbilityResolverSubject.to_s, outcome: false)
103
+ ]
104
+ subject.user_outcome.must_equal false
105
+ end
106
+ end
107
+ end
108
+
109
+ # ---------------------------------------------------------------------
110
+
111
+ describe '#roles_outcome' do
112
+ describe 'no locks' do
113
+ it 'returns nil if no locks for subject_type exist in any of user roles' do
114
+ subject.roles_outcome.must_be_nil
115
+ end
116
+ end
117
+
118
+ describe 'id locks' do
119
+ it 'returns outcome' do
120
+ user.roles = [
121
+ role_sysop.tap{ |r| r.test_locks = [
122
+ TestLock.new(subject: ability_resolver_subject, action: :read, outcome: true)
123
+ ]},
124
+ role_editor
125
+ ]
126
+ subject_with_id.roles_outcome.must_equal true
127
+ end
128
+
129
+ it 'prefers negative outcome across one role' do
130
+ user.roles = [
131
+ role_sysop.tap{ |r| r.test_locks = [
132
+ TestLock.new(subject: ability_resolver_subject, action: :read, outcome: true),
133
+ TestLock.new(subject: ability_resolver_subject, action: :read, outcome: false)
134
+ ]},
135
+ role_editor
136
+ ]
137
+ subject_with_id.roles_outcome.must_equal false
138
+ end
139
+
140
+ it 'prefers positive outcome across multiple roles' do
141
+ user.roles = [
142
+ role_sysop.tap{ |r| r.test_locks = [
143
+ TestLock.new(subject: ability_resolver_subject, action: :read, outcome: true)
144
+ ]},
145
+ role_editor.tap{ |r| r.test_locks = [
146
+ TestLock.new(subject: ability_resolver_subject, action: :read, outcome: false)
147
+ ]}
148
+ ]
149
+ subject_with_id.roles_outcome.must_equal true
150
+ end
151
+ end
152
+
153
+ describe 'class locks' do
154
+ it 'returns outcome' do
155
+ user.roles = [
156
+ role_sysop.tap{ |r| r.test_locks = [
157
+ TestLock.new(subject_type: TestAbilityResolverSubject.to_s, action: :read, outcome: true)
158
+ ]},
159
+ role_editor
160
+ ]
161
+ subject.roles_outcome.must_equal true
162
+ end
163
+
164
+ it 'prefers negative outcome across one role' do
165
+ user.roles = [
166
+ role_sysop.tap{ |r| r.test_locks = [
167
+ TestLock.new(subject_type: TestAbilityResolverSubject.to_s, action: :read, outcome: true),
168
+ TestLock.new(subject_type: TestAbilityResolverSubject.to_s, action: :read, outcome: false)
169
+ ]},
170
+ role_editor
171
+ ]
172
+ subject.roles_outcome.must_equal false
173
+ end
174
+
175
+ it 'prefers positive outcome across multiple roles' do
176
+ user.roles = [
177
+ role_sysop.tap{ |r| r.test_locks = [
178
+ TestLock.new(subject_type: TestAbilityResolverSubject.to_s, action: :read, outcome: true)
179
+ ]},
180
+ role_editor.tap{ |r| r.test_locks = [
181
+ TestLock.new(subject_type: TestAbilityResolverSubject.to_s, action: :read, outcome: false)
182
+ ]}
183
+ ]
184
+
185
+ subject.roles_outcome.must_equal true
186
+ end
187
+ end
188
+ end
189
+
190
+ # ---------------------------------------------------------------------
191
+
192
+ describe '#class_outcome' do
193
+ it 'returns outcome' do
194
+ subject.class_outcome.must_equal true
195
+ end
196
+
197
+ it 'prefers negative outcome across same class' do
198
+ TestAbilityResolverSubject.stub(:default_locks, [
199
+ TestLock.new(subject_type: TestAbilityResolverSubject.to_s, action: :read, outcome: false),
200
+ TestLock.new(subject_type: TestAbilityResolverSubject.to_s, action: :read, outcome: true)
201
+ ]) do
202
+ subject.class_outcome.must_equal false
203
+ end
204
+ end
205
+ end
206
+
207
+ end
208
+ end
@@ -0,0 +1,91 @@
1
+ require "test_helper"
2
+
3
+ module MongoidAbility
4
+ describe Ability do
5
+
6
+ let(:user) { TestUser.new }
7
+ let(:ability) { Ability.new(user) }
8
+
9
+ # ---------------------------------------------------------------------
10
+
11
+ describe 'user' do
12
+ it 'exposes user' do
13
+ ability.user.must_equal user
14
+ end
15
+ end
16
+
17
+ # ---------------------------------------------------------------------
18
+
19
+ describe 'default locks' do
20
+ it 'propagates from superclass to all subclasses' do
21
+ ability.can?(:update, TestAbilitySubjectSuper1).must_equal true
22
+ ability.can?(:update, TestAbilitySubject).must_equal true
23
+ end
24
+
25
+ describe 'when defined for all superclasses' do
26
+ it 'propagates default locks to subclasses' do
27
+ ability.can?(:read, TestAbilitySubjectSuper2).must_equal false
28
+ TestAbilitySubjectSuper1.stub(:default_locks, [
29
+ TestLock.new(subject_type: TestAbilitySubjectSuper1.to_s, action: :read, outcome: false)
30
+ ]) do
31
+ ability.can?(:read, TestAbilitySubjectSuper1).must_equal false
32
+ end
33
+ TestAbilitySubject.stub(:default_locks, [
34
+ TestLock.new(subject_type: TestAbilitySubject.to_s, action: :read, outcome: true)
35
+ ]) do
36
+ ability.can?(:read, TestAbilitySubject).must_equal true
37
+ end
38
+ end
39
+ end
40
+
41
+ describe 'when defined for some superclasses' do
42
+ it 'propagates default locks to subclasses' do
43
+ ability.can?(:read, TestAbilitySubjectSuper2).must_equal false
44
+ ability.can?(:read, TestAbilitySubjectSuper1).must_equal false
45
+ TestAbilitySubject.stub(:default_locks, [
46
+ TestLock.new(subject_type: TestAbilitySubjectSuper1.to_s, action: :read, outcome: true)
47
+ ]) do
48
+ ability.can?(:read, TestAbilitySubject).must_equal true
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ # ---------------------------------------------------------------------
55
+
56
+ describe 'user locks' do
57
+ describe 'when defined for superclass' do
58
+ before do
59
+ user.tap do |u|
60
+ u.test_locks = [TestLock.new(subject_type: TestAbilitySubjectSuper2.to_s, action: :read, outcome: true)]
61
+ end
62
+ end
63
+ it 'applies the superclass lock' do
64
+ ability.can?(:read, TestAbilitySubject).must_equal true
65
+ end
66
+ end
67
+ end
68
+
69
+ # ---------------------------------------------------------------------
70
+
71
+ describe 'role locks' do
72
+ describe 'when defined for superclass' do
73
+ before do
74
+ user.tap do |u|
75
+ u.roles = [
76
+ TestRole.new(test_locks: [
77
+ TestLock.new(subject_type: TestAbilitySubjectSuper2.to_s, action: :read, outcome: true)
78
+ ])
79
+ ]
80
+ end
81
+ end
82
+ it 'applies the superclass lock' do
83
+ ability.can?(:read, TestAbilitySubject).must_equal true
84
+ end
85
+ end
86
+ end
87
+
88
+ # ---------------------------------------------------------------------
89
+
90
+ end
91
+ end