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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c94126d8c7b257f7a07a4523aa5967a197e4ed5
|
4
|
+
data.tar.gz: 0a7daeea08caa81ed98c488185928942eb144aa4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bac9709c73d4d227bdb3786d8de7ffdda5aea899f294c9e3adbf0d7f543832f2c82266c41e73ef7f7bc938d409e2ab8dbafd8402c21545923745287b213411dc
|
7
|
+
data.tar.gz: 9174b6fa5c46d17cd480d1fd468874d1c67cef6562e2fd77f43406fb5e392e0d0cbe56fd32ba505534d7ffbcbbda810e426f66cf954f4aef2df56033938df930
|
data/README.md
CHANGED
@@ -50,16 +50,16 @@ This class defines a permission itself using the following fields:
|
|
50
50
|
|
51
51
|
These fields define what subject (respectively subject type, when referring to a class) the lock applies to, which action it is defined for (for example `:read`), and whether the outcome is positive or negative.
|
52
52
|
|
53
|
-
For more specific behavior, it is possible to override the `#calculated_outcome` method (should, for example, the permission depend on some additional factors).
|
53
|
+
For more specific behavior, it is possible to override the `#calculated_outcome` method (should, for example, the permission depend on some additional factors). The `#calculated_outcome` method receives options that are passed when checking the permissions using for example `can? :read, MyClass, { option_1: 1 }`
|
54
54
|
|
55
55
|
```ruby
|
56
|
-
def calculated_outcome
|
56
|
+
def calculated_outcome options={}
|
57
57
|
# custom behaviour
|
58
|
-
#
|
58
|
+
# return true/false
|
59
59
|
end
|
60
60
|
```
|
61
61
|
|
62
|
-
If you wish to check the state of a lock directly, please use the convenience methods `#open?` and `#closed?`. These take into account the `#calculated_outcome`. Using the `:outcome` field directly is discouraged.
|
62
|
+
If you wish to check the state of a lock directly, please use the convenience methods `#open?` and `#closed?`. These take into account the `#calculated_outcome`. Using the `:outcome` field directly is discouraged as it just returns the boolean attribute.
|
63
63
|
|
64
64
|
The lock class can be further subclassed in order to customise its behavior, for example per action.
|
65
65
|
|
@@ -74,23 +74,23 @@ class MySubject
|
|
74
74
|
include Mongoid::Document
|
75
75
|
include MongoidAbility::Subject
|
76
76
|
|
77
|
-
default_lock :read, true
|
78
|
-
default_lock :update, false
|
77
|
+
default_lock MyLock, :read, true
|
78
|
+
default_lock MyLock, :update, false
|
79
79
|
end
|
80
80
|
```
|
81
81
|
|
82
|
-
The subject classes can be subclassed. Subclasses inherit the default locks (unless they override them), the resulting outcome being correctly calculated bottom-up the superclass chain.
|
82
|
+
The subject classes can be subclassed. Subclasses inherit the default locks (unless they override them), the resulting outcome being correctly calculated bottom-up the superclass chain.
|
83
83
|
|
84
84
|
The subject also acquires a convenience `Mongoid::Criteria` named `.accessible_by`. This criteria can be used to query for subject based on the user's ability:
|
85
85
|
|
86
86
|
```ruby
|
87
87
|
ability = MongoidAbility::Ability.new(current_user)
|
88
|
-
MySubject.accessible_by(ability, :read)
|
88
|
+
MySubject.accessible_by(ability, :read, options={})
|
89
89
|
```
|
90
90
|
|
91
91
|
### Owner
|
92
92
|
|
93
|
-
This
|
93
|
+
This `Ability` class supports two levels of inheritance (for example User and its Roles). The locks can be either embedded (via `.embeds_many`) or associated (via `.has_many`). Make sure to include the `as: :owner` option.
|
94
94
|
|
95
95
|
```ruby
|
96
96
|
class MyUser
|
@@ -101,8 +101,13 @@ class MyUser
|
|
101
101
|
has_and_belongs_to_many :roles, class_name: 'MyRole'
|
102
102
|
|
103
103
|
# override if your relation is named differently
|
104
|
-
def self.
|
105
|
-
|
104
|
+
def self.locks_relation_name
|
105
|
+
:locks
|
106
|
+
end
|
107
|
+
|
108
|
+
# override if your relation is named differently
|
109
|
+
def self.inherit_from_relation_name
|
110
|
+
:roles
|
106
111
|
end
|
107
112
|
end
|
108
113
|
```
|
@@ -122,8 +127,8 @@ Both users and roles can be further subclassed.
|
|
122
127
|
The owner also gains the `#can?` and `#cannot?` methods, that are delegate to the user's ability. It is then easy to perform permission checks per user:
|
123
128
|
|
124
129
|
```ruby
|
125
|
-
current_user.can?(:read, resource)
|
126
|
-
other_user.can?(:read, ResourceClass)
|
130
|
+
current_user.can?(:read, resource, options)
|
131
|
+
other_user.can?(:read, ResourceClass, options)
|
127
132
|
```
|
128
133
|
|
129
134
|
### CanCanCan
|
data/lib/mongoid_ability.rb
CHANGED
@@ -1,12 +1,18 @@
|
|
1
1
|
require "mongoid_ability/version"
|
2
2
|
|
3
3
|
require "mongoid_ability/ability"
|
4
|
-
|
5
|
-
require "mongoid_ability/accessible_query_builder"
|
4
|
+
|
6
5
|
require "mongoid_ability/lock"
|
7
6
|
require "mongoid_ability/owner"
|
8
7
|
require "mongoid_ability/subject"
|
9
8
|
|
9
|
+
require "mongoid_ability/resolve_locks"
|
10
|
+
require "mongoid_ability/resolve_default_locks"
|
11
|
+
require "mongoid_ability/resolve_inherited_locks"
|
12
|
+
require "mongoid_ability/resolve_owner_locks"
|
13
|
+
|
14
|
+
require "mongoid_ability/accessible_query_builder"
|
15
|
+
|
10
16
|
# ---------------------------------------------------------------------
|
11
17
|
|
12
18
|
if defined?(Rails)
|
@@ -2,57 +2,59 @@ require 'cancancan'
|
|
2
2
|
|
3
3
|
module MongoidAbility
|
4
4
|
class Ability
|
5
|
-
|
6
5
|
include CanCan::Ability
|
7
6
|
|
8
7
|
attr_reader :owner
|
9
8
|
|
10
|
-
# =====================================================================
|
11
|
-
|
12
9
|
def initialize owner
|
13
10
|
@owner = owner
|
14
11
|
|
15
|
-
can do |action, subject_type, subject|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
12
|
+
can do |action, subject_type, subject, options|
|
13
|
+
if defined? Rails
|
14
|
+
::Rails.cache.fetch( [ cache_key ] + cache_keys(action, subject_type, subject, options) ) do
|
15
|
+
_can(action, subject_type, subject, options)
|
16
|
+
end
|
17
|
+
else
|
18
|
+
_can(action, subject_type, subject, options)
|
22
19
|
end
|
23
|
-
|
24
|
-
outcome
|
25
20
|
end
|
26
21
|
end
|
27
22
|
|
28
|
-
|
29
|
-
|
30
|
-
def combined_outcome owner, action, cls, subject
|
31
|
-
uo = user_outcome(owner, action, cls, subject)
|
32
|
-
return uo unless uo.nil?
|
23
|
+
# ---------------------------------------------------------------------
|
33
24
|
|
34
|
-
|
35
|
-
|
36
|
-
|
25
|
+
def _can action, subject_type, subject, options
|
26
|
+
subject_class = subject_type.to_s.constantize
|
27
|
+
outcome = nil
|
28
|
+
options ||= {}
|
29
|
+
subject_class.self_and_ancestors_with_default_locks.each do |cls|
|
30
|
+
outcome = ResolveInheritedLocks.call(owner, action, cls, subject, options)
|
31
|
+
break if outcome != nil
|
37
32
|
end
|
38
|
-
|
39
|
-
class_outcome(cls, action)
|
33
|
+
outcome
|
40
34
|
end
|
41
35
|
|
42
36
|
# ---------------------------------------------------------------------
|
43
37
|
|
44
|
-
def
|
45
|
-
|
38
|
+
def cache_key
|
39
|
+
["ability", owner.cache_key, inherit_from_relation_cache_keys].compact.join('/')
|
40
|
+
end
|
41
|
+
|
42
|
+
def cache_keys action, subject_type, subject, options
|
43
|
+
res = []
|
44
|
+
res << action
|
45
|
+
res << subject_type
|
46
|
+
res << subject.cache_key unless subject.nil?
|
47
|
+
res << options_cache_key(options)
|
48
|
+
res.compact
|
46
49
|
end
|
47
50
|
|
48
|
-
def
|
49
|
-
|
51
|
+
def options_cache_key options={}
|
52
|
+
Digest::SHA1.hexdigest( options.to_a.sort_by { |k,v| k.to_s }.to_s )
|
50
53
|
end
|
51
54
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
55
|
-
return true if class_locks.any?(&:open?)
|
55
|
+
def inherit_from_relation_cache_keys
|
56
|
+
return unless owner.respond_to?(owner.class.inherit_from_relation_name) && owner.inherit_from_relation != nil
|
57
|
+
owner.inherit_from_relation.map(&:cache_key)
|
56
58
|
end
|
57
59
|
|
58
60
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module MongoidAbility
|
2
|
-
class AccessibleQueryBuilder < Struct.new(:base_class, :ability, :action)
|
2
|
+
class AccessibleQueryBuilder < Struct.new(:base_class, :ability, :action, :options)
|
3
3
|
|
4
4
|
def self.call *args
|
5
5
|
new(*args).call
|
@@ -8,17 +8,20 @@ module MongoidAbility
|
|
8
8
|
# =====================================================================
|
9
9
|
|
10
10
|
def call
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
if defined? Rails
|
12
|
+
# FIXME: this is a bit of a dirty hack, since the marshalling of criteria does not preserve the embedded attributes
|
13
|
+
Rails.cache.fetch( [ 'ability-query', base_class, ability.cache_key, action, ability.options_cache_key(options) ] ) { _call }.tap { |criteria| criteria.embedded = base_criteria.embedded }
|
14
|
+
else
|
15
|
+
_call
|
15
16
|
end
|
16
|
-
|
17
|
-
criteria
|
18
17
|
end
|
19
18
|
|
20
19
|
private # =============================================================
|
21
20
|
|
21
|
+
def _call
|
22
|
+
base_class_and_descendants.inject(base_criteria) { |criteria, cls| criteria.merge!(criteria_for_class(cls)) }
|
23
|
+
end
|
24
|
+
|
22
25
|
def base_criteria
|
23
26
|
@base_criteria ||= base_class.criteria
|
24
27
|
end
|
@@ -34,7 +37,7 @@ module MongoidAbility
|
|
34
37
|
end
|
35
38
|
|
36
39
|
def base_class_and_descendants
|
37
|
-
[base_class].concat(base_class_descendants)
|
40
|
+
@base_class_and_descendants ||= [base_class].concat(base_class_descendants)
|
38
41
|
end
|
39
42
|
|
40
43
|
def hereditary?
|
@@ -44,71 +47,88 @@ module MongoidAbility
|
|
44
47
|
# ---------------------------------------------------------------------
|
45
48
|
|
46
49
|
def owner
|
47
|
-
ability.owner
|
50
|
+
@owner ||= ability.owner
|
48
51
|
end
|
49
52
|
|
50
|
-
def
|
51
|
-
return unless owner.respond_to?(owner.class.
|
52
|
-
owner.
|
53
|
+
def inherited_from_relation
|
54
|
+
return unless owner.respond_to?(owner.class.inherit_from_relation_name)
|
55
|
+
owner.inherit_from_relation
|
53
56
|
end
|
54
57
|
|
55
58
|
# ---------------------------------------------------------------------
|
56
59
|
|
57
|
-
def
|
58
|
-
|
60
|
+
def owner_id_locks_for_subject_type cls
|
61
|
+
@owner_id_locks_for_subject_type ||= {}
|
62
|
+
@owner_id_locks_for_subject_type[cls] ||= owner.locks_relation.id_locks.for_action(action).for_subject_type(cls.to_s)
|
59
63
|
end
|
60
64
|
|
61
|
-
def
|
62
|
-
return [] unless
|
63
|
-
|
65
|
+
def inherited_from_relation_ids_locks_for_subject_type cls
|
66
|
+
return [] unless inherited_from_relation
|
67
|
+
@inherited_from_relation_ids_locks_for_subject_type ||= {}
|
68
|
+
@inherited_from_relation_ids_locks_for_subject_type[cls] ||= inherited_from_relation.collect { |o|
|
69
|
+
o.locks_relation.id_locks.for_action(action).for_subject_type(cls.to_s)
|
70
|
+
}.flatten
|
64
71
|
end
|
65
72
|
|
66
73
|
# ---------------------------------------------------------------------
|
67
74
|
|
68
75
|
def role_has_open_id_lock? cls, subject_id
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
76
|
+
@role_has_open_id_lock ||= {}
|
77
|
+
@role_has_open_id_lock["#{cls}_#{subject_id}"] ||= begin
|
78
|
+
inherited_from_relation_ids_locks_for_subject_type(cls).
|
79
|
+
select{ |l| l.open?(options) }.
|
80
|
+
map(&:subject_id).
|
81
|
+
include?(subject_id)
|
82
|
+
end
|
73
83
|
end
|
74
84
|
|
75
|
-
def
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
85
|
+
def owner_has_open_id_lock? cls, subject_id
|
86
|
+
@owner_has_open_id_lock ||= {}
|
87
|
+
@owner_has_open_id_lock["#{cls}_#{subject_id}"] ||= begin
|
88
|
+
owner_id_locks_for_subject_type(cls).
|
89
|
+
select{ |l| l.open?(options) }.
|
90
|
+
map(&:subject_id).
|
91
|
+
include?(subject_id)
|
92
|
+
end
|
80
93
|
end
|
81
94
|
|
82
95
|
# ---------------------------------------------------------------------
|
83
96
|
|
84
97
|
def criteria_for_class cls
|
85
|
-
|
98
|
+
@criteria_for_class ||= {}
|
99
|
+
@criteria_for_class[cls] ||= ability.can?(action, cls, options) ? exclude_criteria(cls) : include_criteria(cls)
|
86
100
|
end
|
87
101
|
|
88
102
|
def exclude_criteria cls
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
103
|
+
@exclude_criteria ||= {}
|
104
|
+
@exclude_criteria[cls] ||= begin
|
105
|
+
id_locks = inherited_from_relation_ids_locks_for_subject_type(cls).select{ |l| l.closed?(options) }
|
106
|
+
id_locks = id_locks.reject{ |lock| role_has_open_id_lock?(cls, lock.subject_id) }
|
107
|
+
id_locks = id_locks.reject{ |lock| owner_has_open_id_lock?(cls, lock.subject_id) }
|
108
|
+
id_locks += owner_id_locks_for_subject_type(cls).select{ |l| l.closed?(options) }
|
93
109
|
|
94
|
-
|
110
|
+
excluded_ids = id_locks.map(&:subject_id).flatten
|
95
111
|
|
96
|
-
|
97
|
-
|
112
|
+
conditions = { :_id.nin => excluded_ids }
|
113
|
+
conditions = conditions.merge(_type: cls.to_s) if hereditary?
|
98
114
|
|
99
|
-
|
115
|
+
base_criteria.or(conditions)
|
116
|
+
end
|
100
117
|
end
|
101
118
|
|
102
119
|
def include_criteria cls
|
103
|
-
|
104
|
-
|
120
|
+
@include_criteria ||= {}
|
121
|
+
@include_criteria[cls] ||= begin
|
122
|
+
id_locks = inherited_from_relation_ids_locks_for_subject_type(cls).select{ |l| l.open?(options) }
|
123
|
+
id_locks += owner_id_locks_for_subject_type(cls).select{ |l| l.open?(options) }
|
105
124
|
|
106
|
-
|
125
|
+
included_ids = id_locks.map(&:subject_id).flatten
|
107
126
|
|
108
|
-
|
109
|
-
|
127
|
+
conditions = { :_id.in => included_ids }
|
128
|
+
conditions = conditions.merge(_type: cls.to_s) if hereditary?
|
110
129
|
|
111
|
-
|
130
|
+
base_criteria.or(conditions)
|
131
|
+
end
|
112
132
|
end
|
113
133
|
|
114
134
|
end
|
data/lib/mongoid_ability/lock.rb
CHANGED
@@ -1,24 +1,19 @@
|
|
1
|
+
require 'mongoid'
|
2
|
+
|
1
3
|
module MongoidAbility
|
2
4
|
module Lock
|
3
|
-
|
4
5
|
def self.included base
|
5
6
|
base.extend ClassMethods
|
6
7
|
base.class_eval do
|
7
8
|
field :action, type: Symbol, default: :read
|
8
9
|
field :outcome, type: Boolean, default: false
|
9
10
|
|
10
|
-
# ---------------------------------------------------------------------
|
11
|
-
|
12
11
|
belongs_to :subject, polymorphic: true, touch: true
|
13
12
|
|
14
|
-
# ---------------------------------------------------------------------
|
15
|
-
|
16
13
|
# TODO: validate that action is defined on subject or its superclasses
|
17
14
|
validates :action, presence: true, uniqueness: { scope: [ :subject_type, :subject_id, :outcome ] }
|
18
15
|
validates :outcome, presence: true
|
19
16
|
|
20
|
-
# ---------------------------------------------------------------------
|
21
|
-
|
22
17
|
scope :for_action, -> action { where(action: action.to_sym) }
|
23
18
|
|
24
19
|
scope :for_subject_type, -> subject_type { where(subject_type: subject_type.to_s) }
|
@@ -32,43 +27,35 @@ module MongoidAbility
|
|
32
27
|
|
33
28
|
# =====================================================================
|
34
29
|
|
35
|
-
|
30
|
+
# NOTE: override for more complicated results
|
31
|
+
def calculated_outcome options={}
|
32
|
+
outcome
|
36
33
|
end
|
37
34
|
|
38
|
-
#
|
39
|
-
|
40
|
-
|
41
|
-
|
35
|
+
# NOTE: override for more complicated results
|
36
|
+
def conditions
|
37
|
+
res = { _type: subject_type }
|
38
|
+
res = res.merge(_id: subject_id) if subject_id.present?
|
39
|
+
res = { '$not' => res } if calculated_outcome == false
|
40
|
+
res
|
42
41
|
end
|
43
42
|
|
44
43
|
# ---------------------------------------------------------------------
|
45
44
|
|
46
|
-
def open?
|
47
|
-
|
45
|
+
def open? options={}
|
46
|
+
calculated_outcome(options) == true
|
48
47
|
end
|
49
48
|
|
50
|
-
def closed?
|
51
|
-
!open?
|
49
|
+
def closed? options={}
|
50
|
+
!open?(options)
|
52
51
|
end
|
53
52
|
|
54
|
-
# ---------------------------------------------------------------------
|
55
|
-
|
56
53
|
def class_lock?
|
57
54
|
!id_lock?
|
58
55
|
end
|
59
56
|
|
60
57
|
def id_lock?
|
61
|
-
|
62
|
-
end
|
63
|
-
|
64
|
-
# ---------------------------------------------------------------------
|
65
|
-
|
66
|
-
def conditions
|
67
|
-
res = { _type: subject_type }
|
68
|
-
res = res.merge(_id: subject_id) if subject_id.present?
|
69
|
-
res = { '$not' => res } if calculated_outcome == false
|
70
|
-
res
|
58
|
+
subject_id.present?
|
71
59
|
end
|
72
|
-
|
73
60
|
end
|
74
61
|
end
|