declarative_policy 1.1.0 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6dffd68fb3da1c6d7629901c2436d47e87d7a2b275dfa7282371ef97e7e623b9
4
- data.tar.gz: 9d07ae900c5c2de61025ac2ecff512da42a235f2365db2696babc4b41a654ec2
3
+ metadata.gz: 05f875265a827bd025947cd0098d09804051cf0fa2c02857aa230e03d8554f02
4
+ data.tar.gz: 38452f2308ad61d9cf54bb604f68cbdd73cb75d342ea24a55c265ee708f588b4
5
5
  SHA512:
6
- metadata.gz: e95c536a5b724dc302e192c975b7adf9a3096a7b2fca2ebe63cc1a6fcead19bb37928fecd796b63403902c7885f577859beff3a51237ce37d7a4deff9a51318d
7
- data.tar.gz: acfa272dae2fce1bb4ea9be06c45d10f3da93a8a87ea8c329db1b2e8cd8cf707615104945872bc391e4eb35bc9b2adba0d768ecdd9ddf1bb5ce480ba7dd337c0
6
+ metadata.gz: 82ac859206da667fb45b59662aa9a35fb760f59179e0976f9e7ed9ffba92259ba271fa6732ec9b84e3e14e14961f1362fb13077deb7191c75436c83ef60ab342
7
+ data.tar.gz: c9e41debc20ab2ecc2dc8da44c2e782f50e58ec7d9cba23986c5494c7c00d334de82a3f6f2c9a5b35739a402ecc097f144cb05314ab8584f07aa46dab548b6c0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ Starting from version 2.0, changelog entries are tracked via https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/releases
2
+
3
+ 2.0.0:
4
+
5
+ - Drop explicit support for Ruby 2.6 and 2.7 by removing those versions from
6
+ the CI matrix. These Ruby versions are now past EOL.
7
+ - Rename default condition scope name to `:user_and_subject`
8
+ - Clarify the use of `User` and `Subject` in README and documentation
9
+ - Update `ruby-git` in Gemfile.lock
10
+
11
+ 1.1.1:
12
+
13
+ - Define development dependencies
14
+
1
15
  1.1.0:
2
16
 
3
17
  - Add cache invalidation API: `DeclarativePolicy.invalidate(cache, keys)`
data/README.md CHANGED
@@ -20,14 +20,72 @@ gem 'declarative_policy'
20
20
 
21
21
  And then execute:
22
22
 
23
- $ bundle install
23
+ ```plain
24
+ $ bundle install
25
+ ```
24
26
 
25
27
  Or install it yourself as:
26
28
 
27
- $ gem install declarative_policy
29
+ ```plain
30
+ $ gem install declarative_policy
31
+ ```
28
32
 
29
- ## Usage
33
+ ## Example
34
+
35
+ ```ruby
36
+ require 'declarative_policy'
37
+
38
+ class User
39
+ attr_reader :name
40
+
41
+ def initialize(name:)
42
+ @name = name
43
+ end
44
+ end
45
+
46
+ class Vehicle
47
+ def initialize(owner:, trusted: [])
48
+ @owner = owner
49
+ @trusted = trusted
50
+ end
51
+
52
+ def owner?(user)
53
+ @owner.name == user.name
54
+ end
55
+
56
+ def trusted?(user)
57
+ @owner.name == user.name || @trusted.detect { |t| t.name == user.name }
58
+ end
59
+ end
60
+
61
+ class VehiclePolicy < DeclarativePolicy::Base
62
+ condition(:owns) { @subject.owner?(@user) }
63
+ condition(:trusted) { @subject.trusted?(@user) }
30
64
 
65
+ rule { owns }.enable :sell_vehicle
66
+ rule { trusted }.enable :drive_vehicle
67
+ end
68
+
69
+ jack = User.new(name: 'jack')
70
+ jill = User.new(name: 'jill')
71
+ jacks_vehicle = Vehicle.new(owner: jack, trusted: [jill])
72
+ jills_vehicle = Vehicle.new(owner: jill, trusted: [jack])
73
+
74
+ puts "Jack can drive Jack's vehicle? -> #{DeclarativePolicy.policy_for(jack, jacks_vehicle).can?(:drive_vehicle)}"
75
+ puts "Jack can drive Jill's vehicle? -> #{DeclarativePolicy.policy_for(jack, jills_vehicle).can?(:drive_vehicle)}"
76
+ puts "Jack can sell Jack's vehicle? -> #{DeclarativePolicy.policy_for(jack, jacks_vehicle).can?(:sell_vehicle)}"
77
+ puts "Jack can sell Jill's vehicle? -> #{DeclarativePolicy.policy_for(jack, jills_vehicle).can?(:sell_vehicle)}"
78
+ ```
79
+
80
+ ```plain
81
+ $ ruby example.rb
82
+ Jack can drive Jack's vehicle? -> true
83
+ Jack can drive Jill's vehicle? -> true
84
+ Jack can sell Jack's vehicle? -> true
85
+ Jack can sell Jill's vehicle? -> false
86
+ ```
87
+
88
+ ## Usage
31
89
 
32
90
  The core abstraction of this library is a `Policy`. Policies combine:
33
91
 
@@ -36,10 +94,13 @@ The core abstraction of this library is a `Policy`. Policies combine:
36
94
 
37
95
  This library exists to determine the truth value of statements of the form:
38
96
 
39
- ```
40
- Subject Predicate [Object]
97
+ ```plain
98
+ User Predicate [Subject]
41
99
  ```
42
100
 
101
+ Renaming `User` to `Actor` and `Subject` to `Resource` is discussed in
102
+ [this issue](https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/issues/6).
103
+
43
104
  For example:
44
105
 
45
106
  - `user :is_alive`
@@ -64,14 +125,14 @@ class VehiclePolicy < DeclarativePolicy::Base
64
125
  condition(:owns, score: 0) { @subject.owner == @user }
65
126
  condition(:has_access_to, score: 3) { @subject.owner.trusts?(@user) }
66
127
  condition(:intoxicated, score: 5) { @user.blood_alcohol > laws.max_blood_alcohol }
67
-
128
+
68
129
  # conclusions we can draw:
69
130
  rule { owns }.enable :drive_vehicle
70
131
  rule { has_access_to }.enable :drive_vehicle
71
132
  rule { ~old_enough_to_drive }.prevent :drive_vehicle
72
133
  rule { intoxicated }.prevent :drive_vehicle
73
134
  rule { ~has_driving_license }.prevent :drive_vehicle
74
-
135
+
75
136
  # we can use methods to abstract common logic
76
137
  def laws
77
138
  @subject.registration.country.driving_laws
@@ -128,13 +189,33 @@ interactive prompt that will allow you to experiment.
128
189
 
129
190
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
130
191
 
192
+ ## Additional Reading Material
193
+
194
+ More details on policies and custom roles can be found in the following pages:
195
+ - [Development Process for the DeclarativePolicy framework](https://docs.gitlab.com/ee/development/policies.html)
196
+ - [Custom Roles docs](https://docs.gitlab.com/ee/development/permissions/custom_roles.html)
197
+
131
198
  ## Contributing
132
199
 
133
200
  Bug reports and merge requests are welcome on GitLab at
134
- https://gitlab.com/gitlab-org/declarative-policy. This project is intended to be
201
+ https://gitlab.com/gitlab-org/ruby/gems/declarative-policy. This project is intended to be
135
202
  a safe, welcoming space for collaboration, and contributors are expected to
136
203
  adhere to the [GitLab code of conduct](https://about.gitlab.com/community/contribute/code-of-conduct/).
137
204
 
205
+ ## Release process
206
+
207
+ We release `declarative_policy` on an ad-hoc basis. There is no regularity to when
208
+ we release, we just release when we make a change - no matter the size of the
209
+ change.
210
+
211
+ To release a new version:
212
+
213
+ 1. Create a Merge Request.
214
+ 1. Use Merge Request template [Release.md](https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/main/.gitlab/merge_request_templates/Release.md).
215
+ 1. Follow the instructions.
216
+ 1. After the Merge Request has been merged, a new gem version is [published automatically](https://gitlab.com/gitlab-org/components/gem-release).
217
+ 1. Once the new gem version is visible on [RubyGems.org](https://rubygems.org/gems/declarative_policy), it is recommended to update [GitLab's `Gemfile`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/Gemfile) to bump the `declarative_policy` Ruby gem to the new version also.
218
+
138
219
  ## License
139
220
 
140
221
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -143,4 +224,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
143
224
 
144
225
  Everyone interacting in the `DeclarativePolicy` project's codebase, issue
145
226
  trackers, chat rooms and mailing lists is expected to follow
146
- the [code of conduct](https://github.com/[USERNAME]/declarative-policy/blob/master/CODE_OF_CONDUCT.md).
227
+ the [code of conduct](https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/blob/main/CODE_OF_CONDUCT.md).
@@ -5,8 +5,8 @@ require_relative 'lib/declarative_policy/version'
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'declarative_policy'
7
7
  spec.version = DeclarativePolicy::VERSION
8
- spec.authors = ['Jeanine Adkisson', 'Alexis Kalderimis']
9
- spec.email = ['akalderimis@gitlab.com']
8
+ spec.authors = ['group::authorization']
9
+ spec.email = ['engineering@gitlab.com']
10
10
 
11
11
  spec.summary = 'An authorization library with a focus on declarative policy definitions.'
12
12
  spec.description = <<~DESC
@@ -17,20 +17,35 @@ Gem::Specification.new do |spec|
17
17
 
18
18
  This library is in production use at GitLab.com
19
19
  DESC
20
- spec.homepage = 'https://gitlab.com/gitlab-org/declarative-policy'
20
+ spec.homepage = 'https://gitlab.com/gitlab-org/ruby/gems/declarative-policy'
21
21
  spec.license = 'MIT'
22
- spec.required_ruby_version = Gem::Requirement.new('>= 2.5.0')
22
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.0.0')
23
23
 
24
24
  spec.metadata['homepage_uri'] = spec.homepage
25
- spec.metadata['source_code_uri'] = 'https://gitlab.com/gitlab-org/declarative-policy'
26
- spec.metadata['changelog_uri'] = 'https://gitlab.com/gitlab-org/declarative-policy/-/blobs/master/CHANGELOG.md'
25
+ spec.metadata['source_code_uri'] = 'https://gitlab.com/gitlab-org/ruby/gems/declarative-policy'
26
+ spec.metadata['changelog_uri'] = 'https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/releases'
27
+
28
+ spec.metadata['rubygems_mfa_required'] = 'false'
27
29
 
28
30
  # Specify which files should be added to the gem when it is released.
29
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
31
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
31
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
32
+ %w[
33
+ *.gemspec
34
+ lib/**/*.rb
35
+ *.{md,txt}
36
+ doc/**/*
37
+ ].flat_map { |pattern| Dir.glob(pattern) }
32
38
  end
33
39
  spec.bindir = 'exe'
34
40
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
41
  spec.require_paths = ['lib']
42
+
43
+ # Development dependencies:
44
+ spec.add_development_dependency 'benchmark-ips', '~> 2.12'
45
+ spec.add_development_dependency 'gitlab-dangerfiles', '~> 3.8'
46
+ spec.add_development_dependency 'gitlab-styles', '~> 12.0'
47
+ spec.add_development_dependency 'pry-byebug'
48
+ spec.add_development_dependency 'rake', '~> 12.0'
49
+ spec.add_development_dependency 'rspec', '~> 3.10'
50
+ spec.add_development_dependency 'rspec-parameterized', '~> 1.0'
36
51
  end
data/doc/caching.md CHANGED
@@ -122,8 +122,8 @@ policy itself.
122
122
 
123
123
  The best approach here is to use normal Ruby methods and instance variables for
124
124
  such values. The policy instances themselves are cached, so that any two
125
- invocations of `DeclarativePolicy.policy_for(user, object)` with identical
126
- `user` and `object` arguments will always return the same policy object. This
125
+ invocations of `DeclarativePolicy.policy_for(user, subject)` with identical
126
+ `user` and `subject` arguments will always return the same policy object. This
127
127
  means instance variables stored on the policy will be available for the lifetime
128
128
  of the cache.
129
129
 
@@ -209,18 +209,18 @@ In this case, it doesn't matter who the user is or even where they are going:
209
209
  the condition will be computed once (per cache lifetime) for all combinations.
210
210
 
211
211
  Because of the implications for sharing, the scope determines the
212
- [`#score`](https://gitlab.com/gitlab-org/declarative-policy/blob/2ab9dbdf44fb37beb8d0f7c131742d47ae9ef5d0/lib/declarative_policy/condition.rb#L58-77) of
212
+ [`#score`](https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/blob/2ab9dbdf44fb37beb8d0f7c131742d47ae9ef5d0/lib/declarative_policy/condition.rb#L58-77) of
213
213
  the condition (if not provided explicitly). The intention is to prefer values we
214
214
  are more likely (all other things being equal) to re-use:
215
215
 
216
216
  - Conditions we have already cached get a score of `0`.
217
217
  - Conditions that are in the `:global` scope get a score of `2`.
218
218
  - Conditions that are in the `:user` or `:subject` scopes get a score of `8`.
219
- - Conditions that are in the `:normal` scope get a score of `16`.
219
+ - Conditions that are in the `:user_and_subject` scope get a score of `16`.
220
220
 
221
221
  Bear helper-methods in mind when defining scopes. While the instance level cache
222
222
  for non-boolean values would not be shared, as long as the derived condition is
223
- shared (for example by being in the `:user` scope, rather than the `:normal`
223
+ shared (for example by being in the `:user` scope, rather than the `:user_and_subject`
224
224
  scope), helper-methods will also benefit from improved cache hits.
225
225
 
226
226
  ### Preferred scope
@@ -231,7 +231,7 @@ the `:subject` or the `:user` scope. We can inform the optimizer of this
231
231
  by setting `DeclarativePolicy.preferred_scope`.
232
232
 
233
233
  To do this, check the abilities within a block bounded
234
- by [`DeclarativePolicy.with_preferred_scope`](https://gitlab.com/gitlab-org/declarative-policy/blob/481c322a74f76c325d3ccab7f2f3cc2773e8168b/lib/declarative_policy/preferred_scope.rb#L7-13).
234
+ by [`DeclarativePolicy.with_preferred_scope`](https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/blob/481c322a74f76c325d3ccab7f2f3cc2773e8168b/lib/declarative_policy/preferred_scope.rb#L7-13).
235
235
  For example:
236
236
 
237
237
  ```ruby
@@ -270,7 +270,7 @@ have different `object_id` values, and using `object_id` will not get optimal ca
270
270
  policy subjects should implement `#id` for this reason. `ActiveRecord` models
271
271
  with an `id` primary ID attribute do not need any extra configuration.
272
272
 
273
- Please see: [`DeclarativePolicy::Cache`](https://gitlab.com/gitlab-org/declarative-policy/blob/master/lib/declarative_policy/cache.rb).
273
+ Please see: [`DeclarativePolicy::Cache`](https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/blob/main/lib/declarative_policy/cache.rb).
274
274
 
275
275
  ## Cache invalidation
276
276
 
@@ -22,9 +22,9 @@ and then evaluating them with `DeclarativePolicy::Base#allowed?`.
22
22
  You may wish to define a method to abstract policy evaluation. Something like:
23
23
 
24
24
  ```ruby
25
- def allowed?(user, ability, object)
25
+ def allowed?(user, ability, subject)
26
26
  opts = { cache: Cache.current_cache } # re-using a cache between checks eliminates duplication of work
27
- policy = DeclarativePolicy.policy_for(user, object, opts)
27
+ policy = DeclarativePolicy.policy_for(user, subject, opts)
28
28
  policy.allowed?(ability)
29
29
  end
30
30
  ```
@@ -41,9 +41,9 @@ want to know if a user can drive a vehicle. We need a `VehiclePolicy`:
41
41
  ```ruby
42
42
  class VehiclePolicy < DeclarativePolicy::Base
43
43
  # conditions go here by convention
44
-
44
+
45
45
  # rules go here by convention
46
-
46
+
47
47
  # helper methods go last
48
48
  end
49
49
  ```
@@ -55,14 +55,14 @@ Conditions are facts about the state of the system.
55
55
  They have access to two elements of the proposition:
56
56
 
57
57
  - `@user` - the representation of a user in your system: the *subject* of the proposition.
58
- `user` in `allowed?(user, ability, object)`. `@user` may be `nil`, which means
58
+ `user` in `allowed?(user, ability, subject)`. `@user` may be `nil`, which means
59
59
  that the current user is anonymous (for example this may reflect an
60
60
  unauthenticated request in your system).
61
61
  - `@subject` - any domain object that has an associated policy: the *object* of
62
- the predicate of the proposition. `object` in `allowed?(user, ability, object)`.
62
+ the predicate of the proposition. `subject` in `allowed?(user, ability, subject)`.
63
63
  `@subject` is never `nil`. See [handling `nil` values](./configuration.md#handling-nil-values)
64
64
  for details of how to apply policies to `nil` values.
65
-
65
+
66
66
 
67
67
  They are defined as `condition(name, **options, &block)`, where the block is
68
68
  evaluated in the context of an instance of the policy.
@@ -124,6 +124,42 @@ rule { old_enough_to_drive }.policy do
124
124
  end
125
125
  ```
126
126
 
127
+ #### `prevent_all`
128
+
129
+ To prevent all abilities at once, use `prevent_all`:
130
+
131
+ ```ruby
132
+ rule { banned }.prevent_all
133
+ ```
134
+
135
+ This is equivalent to adding a `prevent` for every ability in the policy. It is
136
+ commonly used to deny all access when a precondition fails (e.g. a user is
137
+ suspended or a resource is locked).
138
+
139
+ #### `prevent_all` with exceptions
140
+
141
+ To prevent all abilities **except** specific ones, pass a block with `except`
142
+ declarations:
143
+
144
+ ```ruby
145
+ rule { suspended }.prevent_all do
146
+ except :read
147
+ except :appeal_suspension
148
+ end
149
+ ```
150
+
151
+ Multiple abilities can also be listed in a single `except` call:
152
+
153
+ ```ruby
154
+ rule { suspended }.prevent_all do
155
+ except :read, :list, :appeal_suspension
156
+ end
157
+ ```
158
+
159
+ Excepted abilities are excluded from the blanket prevent and follow normal
160
+ enable/prevent evaluation. Non-excepted abilities are prevented when the rule's
161
+ condition holds, regardless of any `enable` rules.
162
+
127
163
  Rule blocks do not have access to the internal state of the policy, and cannot
128
164
  access the `@user` or `@subject`, or any methods on the policy instance. You
129
165
  should not perform I/O in a rule. They exist solely to define the logical rules
@@ -184,7 +220,7 @@ like:
184
220
  ```ruby
185
221
  class DrivingLicensePolicy < DeclarativePolicy::Base
186
222
  condition(:expired) { @subject.expires_at <= Time.current }
187
-
223
+
188
224
  rule { expired }.prevent :drive_vehicle
189
225
  end
190
226
  ```
@@ -194,7 +230,7 @@ And a registration policy:
194
230
  ```ruby
195
231
  class RegistrationPolicy < DeclarativePolicy::Base
196
232
  condition(:valid) { @subject.valid_for?(@user.current_location) }
197
-
233
+
198
234
  rule { ~valid }.prevent :drive_vehicle
199
235
  end
200
236
  ```
@@ -209,3 +245,41 @@ delegate { @subject.registration }
209
245
 
210
246
  This is a powerful mechanism for inferring rules based on relationships between
211
247
  objects.
248
+
249
+ #### Overrides
250
+
251
+ It can be useful to declare that the given abilities should not be read
252
+ from delegates. This declaration is useful if you have an ability you want
253
+ to define differently in a policy than in a delegated policy, but you still
254
+ want to delegate all other abilities.
255
+
256
+ ```ruby
257
+ delegate { @subject.parent }
258
+
259
+ overrides :drive_car, :watch_tv
260
+ ```
261
+
262
+ NOTE:
263
+ Rules with `prevent_all` present in the delegated policy are properly
264
+ not used during overridden abilities evaluation.
265
+
266
+ #### Delegated conditions
267
+
268
+ When named delegates are defined, their [conditions](#conditions) can be
269
+ referenced in [rules](#rules) using bare words.
270
+
271
+ Given a registration policy:
272
+
273
+ ```ruby
274
+ class RegistrationPolicy < DeclarativePolicy::Base
275
+ condition(:valid) { @subject.valid_for?(@user.current_location) }
276
+ end
277
+ ```
278
+
279
+ The vehicle policy can reference the `:valid` condition as follows:
280
+
281
+ ```ruby
282
+ delegate(:registration) { @subject.registration }
283
+
284
+ rule { registration.valid }.enable :drive_vehicle
285
+ ```
data/doc/optimization.md CHANGED
@@ -97,7 +97,7 @@ They aren't necessarily run in this order, however. Instead, we try to order
97
97
  the list to minimize unnecessary work.
98
98
 
99
99
  The
100
- [logic](https://gitlab.com/gitlab-org/declarative-policy/blob/659ac0525773a76cf8712d47b3c2dadd03b758c9/lib/declarative_policy/runner.rb#L80-112)
100
+ [logic](https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/blob/659ac0525773a76cf8712d47b3c2dadd03b758c9/lib/declarative_policy/runner.rb#L80-112)
101
101
  to process this list is (in pseudo-code):
102
102
 
103
103
  ```pseudo
@@ -113,9 +113,15 @@ module DeclarativePolicy
113
113
 
114
114
  # all the [rule, action] pairs that apply to a particular ability.
115
115
  # we combine the specific ones looked up in ability_map with the global
116
- # ones.
116
+ # ones, filtering out any global actions that have excepted this ability.
117
117
  def configuration_for(ability)
118
- ability_map.actions(ability) + global_actions
118
+ applicable_globals = global_actions.filter_map do |(action, rule, exceptions)|
119
+ next if exceptions&.include?(ability)
120
+
121
+ [action, rule]
122
+ end
123
+
124
+ ability_map.actions(ability) + applicable_globals
119
125
  end
120
126
 
121
127
  ### declaration methods ###
@@ -144,7 +150,7 @@ module DeclarativePolicy
144
150
  #
145
151
  # example:
146
152
  #
147
- # delegate { @subect.parent }
153
+ # delegate { @subject.parent }
148
154
  #
149
155
  # overrides :drive_car, :watch_tv
150
156
  #
@@ -215,8 +221,10 @@ module DeclarativePolicy
215
221
 
216
222
  # we store global prevents (from `prevent_all`) separately,
217
223
  # so that they can be combined into every decision made.
218
- def prevent_all_when(rule)
219
- own_global_actions << [:prevent, rule]
224
+ # The optional `except` parameter is a Set of abilities
225
+ # that should be excluded from the blanket prevent.
226
+ def prevent_all_when(rule, except: nil)
227
+ own_global_actions << [:prevent, rule, except]
220
228
  end
221
229
 
222
230
  private
@@ -255,6 +263,8 @@ module DeclarativePolicy
255
263
  # This is the main entry point for permission checks. It constructs
256
264
  # or looks up a Runner for the given ability and asks it if it passes.
257
265
  def allowed?(*abilities)
266
+ return false if abilities.empty?
267
+
258
268
  abilities.all? { |a| runner(a).pass? }
259
269
  end
260
270
 
@@ -351,8 +361,17 @@ module DeclarativePolicy
351
361
 
352
362
  # used in specs - returns true if there is no possible way for any action
353
363
  # to be allowed, determined only by the global :prevent_all rules.
364
+ # Only considers global actions with no exceptions (a prevent_all with
365
+ # exceptions cannot fully ban, since the excepted abilities may still pass).
354
366
  def banned?
355
- global_steps = self.class.global_actions.map { |(action, rule)| Step.new(self, rule, action) }
367
+ global_steps = self.class.global_actions.filter_map do |(action, rule, exceptions)|
368
+ next if exceptions && !exceptions.empty?
369
+
370
+ Step.new(self, rule, action)
371
+ end
372
+
373
+ return false if global_steps.empty?
374
+
356
375
  !Runner.new(global_steps).pass?
357
376
  end
358
377
 
@@ -9,10 +9,14 @@ module DeclarativePolicy
9
9
  class Condition
10
10
  attr_reader :name, :description, :scope, :manual_score, :context_key
11
11
 
12
+ VALID_SCOPES = %i[user_and_subject user subject global].freeze
13
+ ALLOWED_SCOPES = VALID_SCOPES + %i[normal]
14
+
12
15
  def initialize(name, opts = {}, &compute)
13
16
  @name = name
14
17
  @compute = compute
15
- @scope = opts.fetch(:scope, :normal)
18
+ @scope = fetch_scope(opts)
19
+
16
20
  @description = opts.delete(:description)
17
21
  @context_key = opts[:context_key]
18
22
  @manual_score = opts.fetch(:score, nil)
@@ -25,6 +29,20 @@ module DeclarativePolicy
25
29
  def key
26
30
  "#{@context_key}/#{@name}"
27
31
  end
32
+
33
+ private
34
+
35
+ def fetch_scope(options)
36
+ result = options.fetch(:scope, :user_and_subject)
37
+ if result == :normal
38
+ warn "[DEPRECATION] `:normal` is deprecated and will be removed in 2.0. Please use new name `:user_and_subject`"
39
+ result = :user_and_subject
40
+ end
41
+
42
+ raise "Invalid scope #{result}. Allowed values: #{VALID_SCOPES.inspect}" unless ALLOWED_SCOPES.include?(result)
43
+
44
+ result
45
+ end
28
46
  end
29
47
 
30
48
  # In contrast to a Condition, a ManifestCondition contains
@@ -68,7 +86,7 @@ module DeclarativePolicy
68
86
  return 2 if @condition.scope == :global
69
87
 
70
88
  # "Normal" rules can't share caches with any other policies
71
- return 16 if @condition.scope == :normal
89
+ return 16 if @condition.scope == :user_and_subject
72
90
 
73
91
  # otherwise, we're :user or :subject scope, so it's 4 if
74
92
  # the caller has declared a preference
@@ -85,11 +103,10 @@ module DeclarativePolicy
85
103
  def cache_key
86
104
  @cache_key ||=
87
105
  case @condition.scope
88
- when :normal then "/dp/condition/#{@condition.key}/#{user_key},#{subject_key}"
106
+ when :global then "/dp/condition/#{@condition.key}"
89
107
  when :user then "/dp/condition/#{@condition.key}/#{user_key}"
90
108
  when :subject then "/dp/condition/#{@condition.key}/#{subject_key}"
91
- when :global then "/dp/condition/#{@condition.key}"
92
- else raise 'invalid scope'
109
+ else "/dp/condition/#{@condition.key}/#{user_key},#{subject_key}"
93
110
  end
94
111
  end
95
112
 
@@ -35,7 +35,7 @@ module DeclarativePolicy
35
35
  def policy_class(domain_class_name)
36
36
  return unless domain_class_name
37
37
 
38
- @class_for.call((@name_transformation.call(domain_class_name)))
38
+ @class_for.call(@name_transformation.call(domain_class_name))
39
39
  rescue NameError
40
40
  nil
41
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
@@ -29,14 +29,20 @@ module DeclarativePolicy
29
29
  @context_class.prevent_when(abilities, @rule)
30
30
  end
31
31
 
32
- def prevent_all
33
- @context_class.prevent_all_when(@rule)
32
+ def prevent_all(&block)
33
+ if block
34
+ dsl = PreventAllDsl.new
35
+ dsl.instance_eval(&block)
36
+ @context_class.prevent_all_when(@rule, except: dsl.exceptions)
37
+ else
38
+ @context_class.prevent_all_when(@rule)
39
+ end
34
40
  end
35
41
 
36
- def method_missing(msg, *args, &block)
42
+ def method_missing(msg, ...)
37
43
  return super unless @context_class.respond_to?(msg)
38
44
 
39
- @context_class.__send__(msg, *args, &block) # rubocop: disable GitlabSecurity/PublicSend
45
+ @context_class.__send__(msg, ...) # rubocop: disable GitlabSecurity/PublicSend
40
46
  end
41
47
 
42
48
  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]
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module DeclarativePolicy
6
+ # A small DSL class used within a prevent_all { ... } block
7
+ # to capture exception abilities.
8
+ #
9
+ # Usage:
10
+ # rule { some_condition }.prevent_all do
11
+ # except :read
12
+ # except :list
13
+ # end
14
+ class PreventAllDsl
15
+ attr_reader :exceptions
16
+
17
+ def initialize
18
+ @exceptions = Set.new
19
+ end
20
+
21
+ def except(*abilities)
22
+ @exceptions.merge(abilities)
23
+ end
24
+ end
25
+ 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
@@ -93,7 +93,7 @@ module DeclarativePolicy
93
93
 
94
94
  private
95
95
 
96
- def with_state(&block)
96
+ def with_state
97
97
  @state = State.new
98
98
  old_runner_state = Thread.current[:declarative_policy_current_runner_state]
99
99
  Thread.current[:declarative_policy_current_runner_state] = @state