declarative_policy 1.0.0 → 2.0.1

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.
@@ -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
@@ -15,7 +15,7 @@ module DeclarativePolicy
15
15
  @rule_dsl.delegate(@delegate_name, msg)
16
16
  end
17
17
 
18
- def respond_to_missing?(msg, include_all)
18
+ def respond_to_missing?(_msg, _include_all)
19
19
  true
20
20
  end
21
21
  end
@@ -33,10 +33,10 @@ module DeclarativePolicy
33
33
  @context_class.prevent_all_when(@rule)
34
34
  end
35
35
 
36
- def method_missing(msg, *args, &block)
36
+ def method_missing(msg, ...)
37
37
  return super unless @context_class.respond_to?(msg)
38
38
 
39
- @context_class.__send__(msg, *args, &block) # rubocop: disable GitlabSecurity/PublicSend
39
+ @context_class.__send__(msg, ...) # rubocop: disable GitlabSecurity/PublicSend
40
40
  end
41
41
 
42
42
  def respond_to_missing?(msg)
@@ -2,7 +2,7 @@
2
2
 
3
3
  module DeclarativePolicy
4
4
  module PreferredScope
5
- PREFERRED_SCOPE_KEY = :"DeclarativePolicy.preferred_scope"
5
+ PREFERRED_SCOPE_KEY = :'DeclarativePolicy.preferred_scope'
6
6
 
7
7
  def with_preferred_scope(scope)
8
8
  old_scope = Thread.current[PREFERRED_SCOPE_KEY]
@@ -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
@@ -44,7 +44,7 @@ module DeclarativePolicy
44
44
  end
45
45
  end
46
46
 
47
- def respond_to_missing?(symbol, include_all)
47
+ def respond_to_missing?(_symbol, _include_all)
48
48
  true
49
49
  end
50
50
  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
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 = '2.0.1'
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,16 +1,113 @@
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: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
- - Jeanine Adkisson
8
- - Alexis Kalderimis
9
- autorequire:
7
+ - group::authorization
8
+ autorequire:
10
9
  bindir: exe
11
10
  cert_chain: []
12
- date: 2021-04-12 00:00:00.000000000 Z
13
- dependencies: []
11
+ date: 2025-08-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: benchmark-ips
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: gitlab-dangerfiles
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.8'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.8'
41
+ - !ruby/object:Gem::Dependency
42
+ name: gitlab-styles
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '12.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '12.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry-byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '12.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '12.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.10'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.10'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec-parameterized
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.0'
14
111
  description: |
15
112
  This library provides an authorization framework with a declarative DSL
16
113
 
@@ -19,28 +116,21 @@ description: |
19
116
 
20
117
  This library is in production use at GitLab.com
21
118
  email:
22
- - akalderimis@gitlab.com
119
+ - engineering@gitlab.com
23
120
  executables: []
24
121
  extensions: []
25
122
  extra_rdoc_files: []
26
123
  files:
27
- - ".gitignore"
28
- - ".gitlab-ci.yml"
29
- - ".rspec"
30
- - ".rubocop.yml"
124
+ - CHANGELOG.md
31
125
  - CODE_OF_CONDUCT.md
32
- - Dangerfile
33
- - Gemfile
34
- - Gemfile.lock
126
+ - CONTRIBUTING.md
35
127
  - LICENSE.txt
36
128
  - README.md
37
- - Rakefile
38
- - danger/plugins/project_helper.rb
39
- - danger/roulette/Dangerfile
40
- - declarative_policy.gemspec
129
+ - declarative-policy.gemspec
41
130
  - doc/caching.md
42
131
  - doc/configuration.md
43
132
  - doc/defining-policies.md
133
+ - doc/optimization.md
44
134
  - lib/declarative_policy.rb
45
135
  - lib/declarative_policy/base.rb
46
136
  - lib/declarative_policy/cache.rb
@@ -55,14 +145,15 @@ files:
55
145
  - lib/declarative_policy/runner.rb
56
146
  - lib/declarative_policy/step.rb
57
147
  - lib/declarative_policy/version.rb
58
- homepage: https://gitlab.com/gitlab-org/declarative-policy
148
+ homepage: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy
59
149
  licenses:
60
150
  - MIT
61
151
  metadata:
62
- homepage_uri: https://gitlab.com/gitlab-org/declarative-policy
63
- source_code_uri: https://gitlab.com/gitlab-org/declarative-policy
64
- changelog_uri: https://gitlab.com/gitlab-org/declarative-policy/-/blobs/master/CHANGELOG.md
65
- post_install_message:
152
+ homepage_uri: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy
153
+ source_code_uri: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy
154
+ changelog_uri: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/CHANGELOG.md
155
+ rubygems_mfa_required: 'false'
156
+ post_install_message:
66
157
  rdoc_options: []
67
158
  require_paths:
68
159
  - lib
@@ -70,15 +161,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
70
161
  requirements:
71
162
  - - ">="
72
163
  - !ruby/object:Gem::Version
73
- version: 2.6.0
164
+ version: 3.0.0
74
165
  required_rubygems_version: !ruby/object:Gem::Requirement
75
166
  requirements:
76
167
  - - ">="
77
168
  - !ruby/object:Gem::Version
78
169
  version: '0'
79
170
  requirements: []
80
- rubygems_version: 3.1.4
81
- signing_key:
171
+ rubygems_version: 3.5.22
172
+ signing_key:
82
173
  specification_version: 4
83
174
  summary: An authorization library with a focus on declarative policy definitions.
84
175
  test_files: []
data/.gitignore DELETED
@@ -1,10 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /pkg/
6
- /spec/reports/
7
- /tmp/
8
-
9
- # rspec failure tracking
10
- .rspec_status
data/.gitlab-ci.yml DELETED
@@ -1,48 +0,0 @@
1
- image: "ruby:2.7"
2
-
3
- .tests:
4
- stage: test
5
- only:
6
- refs:
7
- - master
8
- - tags
9
- - merge_requests
10
-
11
- # Cache gems in between builds
12
- cache:
13
- paths:
14
- - vendor/ruby
15
-
16
- before_script:
17
- - ruby -v # Print out ruby version for debugging
18
- - bundle install -j $(nproc) --path vendor # Install dependencies into ./vendor/ruby
19
-
20
- rubocop:
21
- extends: .tests
22
- script:
23
- - bundle exec rubocop
24
-
25
- rspec:
26
- extends: .tests
27
- image: "ruby:$RUBY_VERSION"
28
- script:
29
- - bundle exec rspec
30
- parallel:
31
- matrix:
32
- - RUBY_VERSION:
33
- - "2.7"
34
- - "3.0"
35
-
36
- danger-review:
37
- extends: .tests
38
- needs: []
39
- script:
40
- - >
41
- if [ -z "$DANGER_GITLAB_API_TOKEN" ]; then
42
- # Force danger to skip CI source GitLab and fallback to "local only git repo".
43
- unset GITLAB_CI
44
- # We need to base SHA to help danger determine the base commit for this shallow clone.
45
- bundle exec danger dry_run --fail-on-errors=true --verbose --base="$CI_MERGE_REQUEST_DIFF_BASE_SHA"
46
- else
47
- bundle exec danger --fail-on-errors=true --verbose
48
- fi
data/.rspec DELETED
@@ -1,4 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
4
- --order rand
data/.rubocop.yml DELETED
@@ -1,10 +0,0 @@
1
- inherit_gem:
2
- gitlab-styles:
3
- - rubocop-default.yml
4
-
5
- AllCops:
6
- TargetRubyVersion: 2.6
7
- NewCops: enable
8
-
9
- RSpec/MultipleMemoizedHelpers:
10
- Max: 10
data/Dangerfile DELETED
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'gitlab-dangerfiles'
4
-
5
- Gitlab::Dangerfiles.import_plugins(danger)
6
- danger.import_plugin('danger/plugins/*.rb')
7
-
8
- return if helper.release_automation?
9
-
10
- danger.import_dangerfile(path: File.join('danger', 'roulette'))
11
-
12
- anything_to_post = status_report.values.any?(&:any?)
13
-
14
- if helper.ci? && anything_to_post
15
- markdown("**If needed, you can retry the [`danger-review` job](#{ENV['CI_JOB_URL']}) that generated this comment.**")
16
- end
data/Gemfile DELETED
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
-
5
- # Specify your gem's dependencies in declarative-policy.gemspec
6
- gemspec
7
-
8
- gem 'activesupport', '>= 6.0'
9
- gem 'rake', '~> 12.0'
10
- gem 'rubocop', require: false
11
-
12
- group :test do
13
- gem 'rspec', '~> 3.0'
14
- gem 'rspec-parameterized', require: false
15
- gem 'pry-byebug'
16
- end
17
-
18
- group :development, :test do
19
- gem 'gitlab-styles', '~> 6.1.0', require: false
20
- end
21
-
22
- group :development, :test, :danger do
23
- gem 'gitlab-dangerfiles', '~> 1.1.0', require: false
24
- end