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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +90 -9
- data/{declarative_policy.gemspec → declarative-policy.gemspec} +23 -8
- data/doc/caching.md +7 -7
- data/doc/defining-policies.md +83 -9
- data/doc/optimization.md +1 -1
- data/lib/declarative_policy/base.rb +25 -6
- data/lib/declarative_policy/condition.rb +22 -5
- data/lib/declarative_policy/configuration.rb +1 -1
- data/lib/declarative_policy/delegate_dsl.rb +1 -1
- data/lib/declarative_policy/policy_dsl.rb +10 -4
- data/lib/declarative_policy/preferred_scope.rb +1 -1
- data/lib/declarative_policy/prevent_all_dsl.rb +25 -0
- data/lib/declarative_policy/rule_dsl.rb +1 -1
- data/lib/declarative_policy/runner.rb +1 -1
- data/lib/declarative_policy/version.rb +1 -1
- data/lib/declarative_policy.rb +1 -0
- metadata +112 -24
- data/.gitignore +0 -12
- data/.gitlab-ci.yml +0 -91
- data/.rspec +0 -4
- data/.rubocop.yml +0 -13
- data/Dangerfile +0 -16
- data/Gemfile +0 -23
- data/Gemfile.lock +0 -214
- data/Rakefile +0 -8
- data/benchmarks/repeated_invocation.rb +0 -37
- data/danger/plugins/project_helper.rb +0 -58
- data/danger/roulette/Dangerfile +0 -97
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 05f875265a827bd025947cd0098d09804051cf0fa2c02857aa230e03d8554f02
|
|
4
|
+
data.tar.gz: 38452f2308ad61d9cf54bb604f68cbdd73cb75d342ea24a55c265ee708f588b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
23
|
+
```plain
|
|
24
|
+
$ bundle install
|
|
25
|
+
```
|
|
24
26
|
|
|
25
27
|
Or install it yourself as:
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
```plain
|
|
30
|
+
$ gem install declarative_policy
|
|
31
|
+
```
|
|
28
32
|
|
|
29
|
-
##
|
|
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
|
-
|
|
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://
|
|
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 = ['
|
|
9
|
-
spec.email = ['
|
|
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('>=
|
|
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/-/
|
|
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
|
-
|
|
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,
|
|
126
|
-
`user` and `
|
|
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 `:
|
|
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 `:
|
|
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/
|
|
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
|
|
data/doc/defining-policies.md
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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. `
|
|
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
|
-
|
|
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 { @
|
|
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
|
-
|
|
219
|
-
|
|
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.
|
|
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
|
|
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 == :
|
|
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 :
|
|
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
|
-
|
|
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(
|
|
38
|
+
@class_for.call(@name_transformation.call(domain_class_name))
|
|
39
39
|
rescue NameError
|
|
40
40
|
nil
|
|
41
41
|
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
|
-
|
|
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,
|
|
42
|
+
def method_missing(msg, ...)
|
|
37
43
|
return super unless @context_class.respond_to?(msg)
|
|
38
44
|
|
|
39
|
-
@context_class.__send__(msg,
|
|
45
|
+
@context_class.__send__(msg, ...) # rubocop: disable GitlabSecurity/PublicSend
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
def respond_to_missing?(msg)
|
|
@@ -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
|