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.
- 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
|