declarative_policy 1.0.0 → 1.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.
@@ -33,6 +33,24 @@ module DeclarativePolicy
33
33
  end
34
34
  end
35
35
 
36
+ class Options
37
+ def initialize
38
+ @hash = {}
39
+ end
40
+
41
+ def []=(key, value)
42
+ @hash[key.to_sym] = value
43
+ end
44
+
45
+ def [](key)
46
+ @hash[key.to_sym]
47
+ end
48
+
49
+ def to_h
50
+ @hash
51
+ end
52
+ end
53
+
36
54
  class << self
37
55
  # The `own_ability_map` vs `ability_map` distinction is used so that
38
56
  # the data structure is properly inherited - with subclasses recursively
@@ -146,46 +164,40 @@ module DeclarativePolicy
146
164
 
147
165
  # A hash in which to store calls to `desc` and `with_scope`, etc.
148
166
  def last_options
149
- @last_options ||= {}.with_indifferent_access
167
+ @last_options ||= Options.new
150
168
  end
151
169
 
152
- # retrieve and zero out the previously set options (used in .condition)
153
- def last_options!
154
- last_options.tap { @last_options = nil }
170
+ def with_options(opts = {})
171
+ last_options.to_h.merge!(opts.to_h)
155
172
  end
156
173
 
157
174
  # Declare a description for the following condition. Currently unused,
158
175
  # but opens the potential for explaining to users why they were or were
159
176
  # not able to do something.
160
177
  def desc(description)
161
- last_options[:description] = description
162
- end
163
-
164
- def with_options(opts = {})
165
- last_options.merge!(opts)
178
+ with_options description: description
166
179
  end
167
180
 
181
+ # Declare a scope for the following condition.
168
182
  def with_scope(scope)
169
183
  with_options scope: scope
170
184
  end
171
185
 
186
+ # Declare a score for the following condition.
172
187
  def with_score(score)
173
188
  with_options score: score
174
189
  end
175
190
 
176
191
  # Declares a condition. It gets stored in `own_conditions`, and generates
177
192
  # a query method based on the condition's name.
178
- def condition(name, opts = {}, &value)
179
- name = name.to_sym
193
+ def condition(condition_name, opts = {}, &value)
194
+ condition_name = condition_name.to_sym
180
195
 
181
- opts = last_options!.merge(opts)
182
- opts[:context_key] ||= self.name
196
+ condition = Condition.new(condition_name, condition_options(opts), &value)
183
197
 
184
- condition = Condition.new(name, opts, &value)
198
+ own_conditions[condition_name] = condition
185
199
 
186
- own_conditions[name] = condition
187
-
188
- define_method(:"#{name}?") { condition(name).pass? }
200
+ define_method(:"#{condition_name}?") { condition(condition_name).pass? }
189
201
  end
190
202
 
191
203
  # These next three methods are mainly called from PolicyDsl,
@@ -206,6 +218,16 @@ module DeclarativePolicy
206
218
  def prevent_all_when(rule)
207
219
  own_global_actions << [:prevent, rule]
208
220
  end
221
+
222
+ private
223
+
224
+ # retrieve and zero out the previously set options (used in .condition)
225
+ def condition_options(opts)
226
+ # The context_key distinguishes two conditions of the same name.
227
+ # For anonymous classes, use object_id.
228
+ opts[:context_key] ||= (name || object_id)
229
+ with_options(opts).tap { @last_options = nil }
230
+ end
209
231
  end
210
232
 
211
233
  # A policy object contains a specific user and subject on which
@@ -254,16 +276,23 @@ module DeclarativePolicy
254
276
  condition(:default, scope: :global, score: 0) { true }
255
277
 
256
278
  def repr
257
- subject_repr =
258
- if @subject.respond_to?(:id)
259
- "#{@subject.class.name}/#{@subject.id}"
260
- else
261
- @subject.inspect
262
- end
279
+ "(#{identify_user} : #{identify_subject})"
280
+ end
263
281
 
264
- user_repr = @user.try(:to_reference) || '<anonymous>'
282
+ def identify_user
283
+ return '<anonymous>' unless @user
265
284
 
266
- "(#{user_repr} : #{subject_repr})"
285
+ @user.to_reference
286
+ rescue NoMethodError
287
+ "<#{@user.class}: #{@user.object_id}>"
288
+ end
289
+
290
+ def identify_subject
291
+ if @subject.respond_to?(:id)
292
+ "#{@subject.class.name}/#{@subject.id}"
293
+ else
294
+ @subject.inspect
295
+ end
267
296
  end
268
297
 
269
298
  def inspect
@@ -276,19 +305,22 @@ module DeclarativePolicy
276
305
  # at the ability level.
277
306
  def runner(ability)
278
307
  ability = ability.to_sym
279
- @runners ||= {}
280
- @runners[ability] ||=
308
+ runners[ability] ||=
281
309
  begin
282
310
  own_runner = Runner.new(own_steps(ability))
283
311
  if self.class.overrides.include?(ability)
284
312
  own_runner
285
313
  else
286
314
  delegated_runners = delegated_policies.values.compact.map { |p| p.runner(ability) }
287
- delegated_runners.inject(own_runner, &:merge_runner)
315
+ delegated_runners.reduce(own_runner, &:merge_runner)
288
316
  end
289
317
  end
290
318
  end
291
319
 
320
+ def runners
321
+ @runners ||= {}
322
+ end
323
+
292
324
  # Helpers for caching. Used by ManifestCondition in performing condition
293
325
  # computation.
294
326
  #
@@ -6,7 +6,7 @@ module DeclarativePolicy
6
6
  def user_key(user)
7
7
  return '<anonymous>' if user.nil?
8
8
 
9
- id_for(user)
9
+ "#{user.class.name}:#{id_for(user)}"
10
10
  end
11
11
 
12
12
  def policy_key(user, subject)
@@ -40,6 +40,8 @@ module DeclarativePolicy
40
40
  # the context's cache here so that we can share in the global
41
41
  # cache (often RequestStore or similar).
42
42
  def pass?
43
+ Thread.current[:declarative_policy_current_runner_state]&.register(self)
44
+
43
45
  @context.cache(cache_key) { @condition.compute(@context) }
44
46
  end
45
47
 
@@ -76,8 +78,6 @@ module DeclarativePolicy
76
78
  8
77
79
  end
78
80
 
79
- private
80
-
81
81
  # This method controls the caching for the condition. This is where
82
82
  # the condition(scope: ...) option comes into play. Notice that
83
83
  # depending on the scope, we may cache only by the user or only by
@@ -93,6 +93,8 @@ module DeclarativePolicy
93
93
  end
94
94
  end
95
95
 
96
+ private
97
+
96
98
  def user_key
97
99
  Cache.user_key(@context.user)
98
100
  end
@@ -7,6 +7,7 @@ module DeclarativePolicy
7
7
  def initialize
8
8
  @named_policies = {}
9
9
  @name_transformation = ->(name) { "#{name}Policy" }
10
+ @class_for = ->(name) { Object.const_get(name) }
10
11
  end
11
12
 
12
13
  def named_policy(name, policy = nil)
@@ -26,10 +27,15 @@ module DeclarativePolicy
26
27
  nil
27
28
  end
28
29
 
30
+ def class_for(&block)
31
+ @class_for = block
32
+ nil
33
+ end
34
+
29
35
  def policy_class(domain_class_name)
30
36
  return unless domain_class_name
31
37
 
32
- @name_transformation.call(domain_class_name).constantize
38
+ @class_for.call((@name_transformation.call(domain_class_name)))
33
39
  rescue NameError
34
40
  nil
35
41
  end
@@ -210,7 +210,7 @@ module DeclarativePolicy
210
210
  cached = cached_pass?(context)
211
211
  return cached unless cached.nil?
212
212
 
213
- @rules.all? { |r| r.pass?(context) }
213
+ @rules.sort_by { |r| r.score(context) }.all? { |r| r.pass?(context) }
214
214
  end
215
215
 
216
216
  def cached_pass?(context)
@@ -240,7 +240,7 @@ module DeclarativePolicy
240
240
  cached = cached_pass?(context)
241
241
  return cached unless cached.nil?
242
242
 
243
- @rules.any? { |r| r.pass?(context) }
243
+ @rules.sort_by { |r| r.score(context) }.any? { |r| r.pass?(context) }
244
244
  end
245
245
 
246
246
  def simplify
@@ -285,9 +285,9 @@ module DeclarativePolicy
285
285
 
286
286
  def simplify
287
287
  case @rule
288
- when And then Or.new(@rule.rules.map(&:negate)).simplify
289
- when Or then And.new(@rule.rules.map(&:negate)).simplify
290
- when Not then @rule.rule.simplify
288
+ when And then Or.new(@rule.rules.map(&:negate)).simplify # DeMorgan's laws
289
+ when Or then And.new(@rule.rules.map(&:negate)).simplify # DeMorgan's laws
290
+ when Not then @rule.rule.simplify # double negation
291
291
  else Not.new(@rule.simplify)
292
292
  end
293
293
  end
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  module DeclarativePolicy
4
6
  class Runner
5
7
  class State
8
+ attr_reader :called_conditions
9
+
6
10
  def initialize
7
11
  @enabled = false
8
12
  @prevented = false
13
+ @called_conditions = Set.new
9
14
  end
10
15
 
11
16
  def enable!
@@ -27,6 +32,10 @@ module DeclarativePolicy
27
32
  def pass?
28
33
  !prevented? && enabled?
29
34
  end
35
+
36
+ def register(manifest_condition)
37
+ @called_conditions << manifest_condition.cache_key
38
+ end
30
39
  end
31
40
 
32
41
  # a Runner contains a list of Steps to be run.
@@ -44,6 +53,11 @@ module DeclarativePolicy
44
53
  !!@state
45
54
  end
46
55
 
56
+ # Delete the cached state - allowing this runner to be re-used if the facts have changed.
57
+ def uncache!
58
+ @state = nil
59
+ end
60
+
47
61
  # used by Rule::Ability. See #steps_by_score
48
62
  def score
49
63
  return 0 if cached?
@@ -55,11 +69,20 @@ module DeclarativePolicy
55
69
  Runner.new(@steps + other.steps)
56
70
  end
57
71
 
72
+ def dependencies
73
+ return Set.new unless @state
74
+
75
+ @state.called_conditions
76
+ end
77
+
58
78
  # The main entry point, called for making an ability decision.
59
79
  # See #run and DeclarativePolicy::Base#can?
60
80
  def pass?
61
81
  run unless cached?
62
82
 
83
+ parent_state = Thread.current[:declarative_policy_current_runner_state]
84
+ parent_state&.called_conditions&.merge(@state.called_conditions)
85
+
63
86
  @state.pass?
64
87
  end
65
88
 
@@ -70,6 +93,16 @@ module DeclarativePolicy
70
93
 
71
94
  private
72
95
 
96
+ def with_state(&block)
97
+ @state = State.new
98
+ old_runner_state = Thread.current[:declarative_policy_current_runner_state]
99
+ Thread.current[:declarative_policy_current_runner_state] = @state
100
+
101
+ yield
102
+ ensure
103
+ Thread.current[:declarative_policy_current_runner_state] = old_runner_state
104
+ end
105
+
73
106
  def flatten_steps!
74
107
  @steps = @steps.flat_map { |s| s.flattened(@steps) }
75
108
  end
@@ -78,33 +111,31 @@ module DeclarativePolicy
78
111
  # It relies on #steps_by_score for the main loop, and updates @state
79
112
  # with the result of the step.
80
113
  def run(debug = nil)
81
- @state = State.new
82
-
83
- steps_by_score do |step, score|
84
- break if !debug && @state.prevented?
85
-
86
- passed = nil
87
- case step.action
88
- when :enable
89
- # we only check :enable actions if they have a chance of
90
- # changing the outcome - if no other rule has enabled or
91
- # prevented.
92
- unless @state.enabled? || @state.prevented?
93
- passed = step.pass?
94
- @state.enable! if passed
95
- end
96
-
97
- debug << inspect_step(step, score, passed) if debug
98
- when :prevent
99
- # we only check :prevent actions if the state hasn't already
100
- # been prevented.
101
- unless @state.prevented?
102
- passed = step.pass?
103
- @state.prevent! if passed
114
+ with_state do
115
+ steps_by_score(!!debug) do |step, score|
116
+ break if !debug && @state.prevented?
117
+
118
+ passed = nil
119
+ case step.action
120
+ when :enable
121
+ # we only check :enable actions if they have a chance of
122
+ # changing the outcome - if no other rule has enabled or
123
+ # prevented.
124
+ unless @state.enabled? || @state.prevented?
125
+ passed = step.pass?
126
+ @state.enable! if passed
127
+ end
128
+ when :prevent
129
+ # we only check :prevent actions if the state hasn't already
130
+ # been prevented.
131
+ unless @state.prevented?
132
+ passed = step.pass?
133
+ @state.prevent! if passed
134
+ end
135
+ else raise "invalid action #{step.action.inspect}"
104
136
  end
105
137
 
106
138
  debug << inspect_step(step, score, passed) if debug
107
- else raise "invalid action #{step.action.inspect}"
108
139
  end
109
140
  end
110
141
 
@@ -131,7 +162,7 @@ module DeclarativePolicy
131
162
  #
132
163
  # For each step, we yield the step object along with the computed score
133
164
  # for debugging purposes.
134
- def steps_by_score
165
+ def steps_by_score(debugging)
135
166
  flatten_steps!
136
167
 
137
168
  if @steps.size > 50
@@ -156,6 +187,7 @@ module DeclarativePolicy
156
187
  # if the permission hasn't yet been enabled and we only have
157
188
  # prevent steps left, we short-circuit the state here
158
189
  @state.prevent!
190
+ return unless debugging
159
191
  end
160
192
 
161
193
  return if remaining_steps.empty?
@@ -185,7 +217,7 @@ module DeclarativePolicy
185
217
  break if lowest_score.zero?
186
218
  end
187
219
 
188
- [next_step, score]
220
+ [next_step, lowest_score]
189
221
  end
190
222
 
191
223
  # Formatter for debugging output.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclarativePolicy
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/dependencies'
4
- require 'active_support/core_ext'
5
-
3
+ require 'set'
6
4
  require_relative 'declarative_policy/cache'
7
5
  require_relative 'declarative_policy/condition'
8
6
  require_relative 'declarative_policy/delegate_dsl'
@@ -20,21 +18,32 @@ require_relative 'declarative_policy/configuration'
20
18
  module DeclarativePolicy
21
19
  extend PreferredScope
22
20
 
23
- CLASS_CACHE_MUTEX = Mutex.new
24
- CLASS_CACHE_IVAR = :@__DeclarativePolicy_CLASS_CACHE
25
-
26
21
  class << self
27
22
  def policy_for(user, subject, opts = {})
28
23
  cache = opts[:cache] || {}
29
24
  key = Cache.policy_key(user, subject)
30
25
 
31
- cache[key] ||=
32
- # to avoid deadlocks in multi-threaded environment when
33
- # autoloading is enabled, we allow concurrent loads,
34
- # https://gitlab.com/gitlab-org/gitlab-foss/issues/48263
35
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
36
- class_for(subject).new(user, subject, opts)
26
+ cache[key] ||= class_for(subject).new(user, subject, opts)
27
+ end
28
+
29
+ # Find the list of runners with now invalidated keys, and invalidate the runners
30
+ def invalidate(cache, invalidated_keys)
31
+ return unless cache&.any?
32
+ return unless invalidated_keys&.any?
33
+
34
+ keys = invalidated_keys.to_set
35
+
36
+ policies = cache.select { |k, _| k.is_a?(String) && k.start_with?('/dp/policy/') }
37
+
38
+ policies.each_value do |policy|
39
+ policy.runners.each do |runner|
40
+ runner.uncache! if keys.intersect?(runner.dependencies)
37
41
  end
42
+ end
43
+
44
+ invalidated_keys.each { |k| cache.delete(k) }
45
+
46
+ nil
38
47
  end
39
48
 
40
49
  def class_for(subject)
@@ -72,38 +81,19 @@ module DeclarativePolicy
72
81
  @configuration ||= DeclarativePolicy::Configuration.new
73
82
  end
74
83
 
75
- # This method is heavily cached because there are a lot of anonymous
76
- # modules in play in a typical rails app, and #name performs quite
77
- # slowly for anonymous classes and modules.
78
- #
79
- # See https://bugs.ruby-lang.org/issues/11119
80
- #
81
- # if the above bug is resolved, this caching could likely be removed.
82
84
  def class_for_class(subject_class)
83
- unless subject_class.instance_variable_defined?(CLASS_CACHE_IVAR)
84
- CLASS_CACHE_MUTEX.synchronize do
85
- # re-check in case of a race
86
- break if subject_class.instance_variable_defined?(CLASS_CACHE_IVAR)
87
-
88
- policy_class = compute_class_for_class(subject_class)
89
- subject_class.instance_variable_set(CLASS_CACHE_IVAR, policy_class)
85
+ if subject_class.respond_to?(:declarative_policy_class)
86
+ Object.const_get(subject_class.declarative_policy_class)
87
+ else
88
+ subject_class.ancestors.each do |klass|
89
+ name = klass.name
90
+ klass = policy_class(name)
91
+
92
+ return klass if klass
90
93
  end
91
- end
92
-
93
- subject_class.instance_variable_get(CLASS_CACHE_IVAR)
94
- end
95
94
 
96
- def compute_class_for_class(subject_class)
97
- return subject_class.declarative_policy_class.constantize if subject_class.respond_to?(:declarative_policy_class)
98
-
99
- subject_class.ancestors.each do |klass|
100
- name = klass.name
101
- klass = policy_class(name)
102
-
103
- return klass if klass
95
+ nil
104
96
  end
105
-
106
- nil
107
97
  end
108
98
 
109
99
  def policy_class(name)
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: declarative_policy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeanine Adkisson
8
8
  - Alexis Kalderimis
9
- autorequire:
9
+ autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2021-04-12 00:00:00.000000000 Z
12
+ date: 2021-11-05 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: |
15
15
  This library provides an authorization framework with a declarative DSL
@@ -28,19 +28,23 @@ files:
28
28
  - ".gitlab-ci.yml"
29
29
  - ".rspec"
30
30
  - ".rubocop.yml"
31
+ - CHANGELOG.md
31
32
  - CODE_OF_CONDUCT.md
33
+ - CONTRIBUTING.md
32
34
  - Dangerfile
33
35
  - Gemfile
34
36
  - Gemfile.lock
35
37
  - LICENSE.txt
36
38
  - README.md
37
39
  - Rakefile
40
+ - benchmarks/repeated_invocation.rb
38
41
  - danger/plugins/project_helper.rb
39
42
  - danger/roulette/Dangerfile
40
43
  - declarative_policy.gemspec
41
44
  - doc/caching.md
42
45
  - doc/configuration.md
43
46
  - doc/defining-policies.md
47
+ - doc/optimization.md
44
48
  - lib/declarative_policy.rb
45
49
  - lib/declarative_policy/base.rb
46
50
  - lib/declarative_policy/cache.rb
@@ -62,7 +66,7 @@ metadata:
62
66
  homepage_uri: https://gitlab.com/gitlab-org/declarative-policy
63
67
  source_code_uri: https://gitlab.com/gitlab-org/declarative-policy
64
68
  changelog_uri: https://gitlab.com/gitlab-org/declarative-policy/-/blobs/master/CHANGELOG.md
65
- post_install_message:
69
+ post_install_message:
66
70
  rdoc_options: []
67
71
  require_paths:
68
72
  - lib
@@ -70,15 +74,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
70
74
  requirements:
71
75
  - - ">="
72
76
  - !ruby/object:Gem::Version
73
- version: 2.6.0
77
+ version: 2.5.0
74
78
  required_rubygems_version: !ruby/object:Gem::Requirement
75
79
  requirements:
76
80
  - - ">="
77
81
  - !ruby/object:Gem::Version
78
82
  version: '0'
79
83
  requirements: []
80
- rubygems_version: 3.1.4
81
- signing_key:
84
+ rubygems_version: 3.2.15
85
+ signing_key:
82
86
  specification_version: 4
83
87
  summary: An authorization library with a focus on declarative policy definitions.
84
88
  test_files: []