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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1541d93003dfaadfd76721962e2e8c299ac86f81
4
- data.tar.gz: cd39407e3a4f06d0320493bfd327b7127b7e96d4
3
+ metadata.gz: 5c94126d8c7b257f7a07a4523aa5967a197e4ed5
4
+ data.tar.gz: 0a7daeea08caa81ed98c488185928942eb144aa4
5
5
  SHA512:
6
- metadata.gz: b44dd1f63f22cc8f3caac82f6da16475fb17fcaa6a84c45a5a3cc889009ed4405e98d10a0aee161a6e89f4efa8954a74dea1d6c2616a54d3114c30e8be8b5eff
7
- data.tar.gz: ffeda7ae0ad1ffa0a0d9d682030a47b3e466967dd403f9413fe953f07fdea3f8de9c0a51593f6d9bd0ce5240103a862bf3601b8e71caa2067d2cea4cf83a993a
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
- # returns true/false
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 gem supports two levels of ownership of a lock: a `User` and its many `Role`s. The locks can be either embedded (via `.embeds_many`) or associated (via `.has_many`). Make sure to include the `as: :owner` option.
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.roles_relation_name
105
- :roles
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
@@ -1,12 +1,18 @@
1
1
  require "mongoid_ability/version"
2
2
 
3
3
  require "mongoid_ability/ability"
4
- require "mongoid_ability/ability_resolver"
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
- subject_class = subject_type.to_s.constantize
17
- outcome = nil
18
-
19
- subject_class.self_and_ancestors_with_default_locks.each do |cls|
20
- outcome = combined_outcome(owner, action, cls, subject)
21
- break unless outcome.nil?
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
- private # =============================================================
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
- if owner.respond_to?(owner.class.roles_relation_name)
35
- ro = owner.roles_relation.collect{ |role| AbilityResolver.new(role, action, cls.to_s, subject).outcome }.compact
36
- return ro.any?{ |i| i == true } unless ro.empty?
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 user_outcome owner, action, cls, subject
45
- AbilityResolver.new(owner, action, cls.to_s, subject).outcome
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 role_outcome role, action, cls, subject
49
- AbilityResolver.new(role, action, cls.to_s, subject).outcome
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 class_outcome subject_class, action
53
- class_locks = subject_class.default_locks.select{ |l| l.action == action }
54
- return false if class_locks.any?(&:closed?)
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
- criteria = base_criteria
12
-
13
- base_class_and_descendants.each do |cls|
14
- criteria = criteria.merge(criteria_for_class(cls))
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 roles
51
- return unless owner.respond_to?(owner.class.roles_relation_name)
52
- owner.roles_relation
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 user_id_locks_for_subject_type cls
58
- owner.locks_relation.id_locks.for_action(action).for_subject_type(cls.to_s)
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 roles_ids_locks_for_subject_type cls
62
- return [] unless roles
63
- roles.collect { |role| role.locks_relation.id_locks.for_action(action).for_subject_type(cls.to_s) }.flatten
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
- roles_ids_locks_for_subject_type(cls).
70
- select(&:open?).
71
- map(&:subject_id).
72
- include?(subject_id)
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 user_has_open_id_lock? cls, subject_id
76
- user_id_locks_for_subject_type(cls).
77
- select(&:open?).
78
- map(&:subject_id).
79
- include?(subject_id)
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
- ability.can?(action, cls) ? exclude_criteria(cls) : include_criteria(cls)
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
- id_locks = roles_ids_locks_for_subject_type(cls).select(&:closed?)
90
- id_locks = id_locks.reject{ |lock| role_has_open_id_lock?(cls, lock.subject_id) }
91
- id_locks = id_locks.reject{ |lock| user_has_open_id_lock?(cls, lock.subject_id) }
92
- id_locks += user_id_locks_for_subject_type(cls).select(&:closed?)
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
- excluded_ids = id_locks.map(&:subject_id).flatten
110
+ excluded_ids = id_locks.map(&:subject_id).flatten
95
111
 
96
- conditions = { :_id.nin => excluded_ids }
97
- conditions = conditions.merge(_type: cls.to_s) if hereditary?
112
+ conditions = { :_id.nin => excluded_ids }
113
+ conditions = conditions.merge(_type: cls.to_s) if hereditary?
98
114
 
99
- base_criteria.or(conditions)
115
+ base_criteria.or(conditions)
116
+ end
100
117
  end
101
118
 
102
119
  def include_criteria cls
103
- id_locks = roles_ids_locks_for_subject_type(cls).select(&:open?)
104
- id_locks += user_id_locks_for_subject_type(cls).select(&:open?)
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
- included_ids = id_locks.map(&:subject_id).flatten
125
+ included_ids = id_locks.map(&:subject_id).flatten
107
126
 
108
- conditions = { :_id.in => included_ids }
109
- conditions = conditions.merge(_type: cls.to_s) if hereditary?
127
+ conditions = { :_id.in => included_ids }
128
+ conditions = conditions.merge(_type: cls.to_s) if hereditary?
110
129
 
111
- base_criteria.or(conditions)
130
+ base_criteria.or(conditions)
131
+ end
112
132
  end
113
133
 
114
134
  end
@@ -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
- module ClassMethods
30
+ # NOTE: override for more complicated results
31
+ def calculated_outcome options={}
32
+ outcome
36
33
  end
37
34
 
38
- # =====================================================================
39
-
40
- def calculated_outcome
41
- self.outcome
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
- self.calculated_outcome == true
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
- self.subject_id.present?
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