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.
- checksums.yaml +4 -4
- data/README.md +18 -13
- data/lib/mongoid_ability.rb +8 -2
- data/lib/mongoid_ability/ability.rb +32 -30
- data/lib/mongoid_ability/accessible_query_builder.rb +61 -41
- data/lib/mongoid_ability/lock.rb +16 -29
- data/lib/mongoid_ability/owner.rb +6 -22
- data/lib/mongoid_ability/resolve_default_locks.rb +15 -0
- data/lib/mongoid_ability/resolve_inherited_locks.rb +32 -0
- data/lib/mongoid_ability/resolve_locks.rb +34 -0
- data/lib/mongoid_ability/resolve_owner_locks.rb +25 -0
- data/lib/mongoid_ability/subject.rb +28 -32
- data/lib/mongoid_ability/version.rb +1 -1
- data/test/mongoid_ability/ability_role_test.rb +11 -5
- data/test/mongoid_ability/ability_test.rb +67 -91
- data/test/mongoid_ability/accessible_query_builder_test.rb +9 -5
- data/test/mongoid_ability/can_options_test.rb +17 -0
- data/test/mongoid_ability/lock_test.rb +51 -70
- data/test/mongoid_ability/owner_locks_test.rb +42 -0
- data/test/mongoid_ability/owner_test.rb +12 -39
- data/test/mongoid_ability/resolve_default_locks_test.rb +27 -0
- data/test/mongoid_ability/resolve_inherited_locks_test.rb +49 -0
- data/test/mongoid_ability/resolve_locks_test.rb +25 -0
- data/test/mongoid_ability/resolve_owner_locks_test.rb +50 -0
- data/test/mongoid_ability/subject_accessible_by_test.rb +135 -0
- data/test/mongoid_ability/subject_test.rb +20 -201
- data/test/support/test_classes.rb +136 -61
- data/test/test_helper.rb +3 -2
- metadata +20 -5
- data/lib/mongoid_ability/ability_resolver.rb +0 -42
- 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
|
-
|
15
|
+
:locks
|
20
16
|
end
|
21
17
|
|
22
|
-
|
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
|
43
|
-
self.send(self.class.
|
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
|
18
|
-
|
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
|
23
|
-
|
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
|
27
|
-
|
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
|
-
|
37
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
57
|
-
|
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
|
|
@@ -3,22 +3,28 @@ require "test_helper"
|
|
3
3
|
module MongoidAbility
|
4
4
|
describe 'ability on Role' do
|
5
5
|
|
6
|
-
let(:read_lock) {
|
7
|
-
let(:role) {
|
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,
|
19
|
+
ability.can?(:read, MySubject).must_equal false
|
14
20
|
end
|
15
21
|
|
16
22
|
it 'role cannot?' do
|
17
|
-
ability.cannot?(:
|
23
|
+
ability.cannot?(:read, MySubject).must_equal true
|
18
24
|
end
|
19
25
|
|
20
26
|
it 'is accessible by' do
|
21
|
-
|
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(:
|
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
|
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,
|
20
|
-
ability.can?(:update,
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
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,
|
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 '
|
70
|
-
describe 'when multiple
|
74
|
+
describe 'inherited owner locks' do
|
75
|
+
describe 'when multiple inherited owners' do
|
71
76
|
before do
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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,
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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,
|
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
|
-
|
110
|
-
|
111
|
-
|
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,
|
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
|
-
|
128
|
-
|
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
|