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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c08845902bc432f4c737ba76d2b9a5f0cc456fed1d0f26353755146ab49b2a1
4
- data.tar.gz: 0f64e5d7707dff73572484cd700b529704f4d41dfd0b1972c8d675e4281d3582
3
+ metadata.gz: 704af6c0500c00a0e6c6797dd5b496c098db86f33ccc1ef2496be37e43b33e03
4
+ data.tar.gz: 2236adb02dbee28b6565fc72541fe2fbee5a087ef15cc2fe1f0d060ce1eece13
5
5
  SHA512:
6
- metadata.gz: 0b5bc3cfd66be62b483aa8beb673b8b858121a18a0dcb64bd9f6fa79d268ebf3ddb566ede84c1032beac05bc81f6fa8f0642e846a42f6bcf21083eb04de7fa2c
7
- data.tar.gz: fabb732587403af0e1cfe8cfcc25033f0d8ccaee066e8310bd0bacf38fba052758e97e3aedbc42e6c62f898a07eaf598885a731f4b80fa9fdf1fe321dfa2901b
6
+ metadata.gz: c3841495e1922ae72f704524e40ca080da4838cfe075d401350212f5b31f116f6003aa3e371d2a0084a11986b4745bb82946ad84b61247a19b59470a26e24755
7
+ data.tar.gz: f3f0d97ba2b9e56079d5307c135ec2b15c73759661f32e6cda2db44ab0fa21002598725cf0c762ca85c0580ae1c5b7c397115f185ec94c9da600cfaf19f6e590
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ 2.0.0:
2
+
3
+ - Drop explicit support for Ruby 2.6 and 2.7 by removing those versions from
4
+ the CI matrix. These Ruby versions are now past EOL.
5
+ - Rename default condition scope name to `:user_and_subject`
6
+ - Clarify the use of `User` and `Subject` in README and documentation
7
+ - Update `ruby-git` in Gemfile.lock
8
+
9
+ 1.1.1:
10
+
11
+ - Define development dependencies
12
+
13
+ 1.1.0:
14
+
15
+ - Add cache invalidation API: `DeclarativePolicy.invalidate(cache, keys)`
16
+ - Include actor class name in cache key
17
+
18
+ 1.0.1:
19
+
20
+ - Added unit level tests for `lib/declarative_policy/rule.rb`
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,41 @@
1
+ ## Developer Certificate of Origin and License
2
+
3
+ By contributing to GitLab B.V., you accept and agree to the following terms and
4
+ conditions for your present and future contributions submitted to GitLab B.V.
5
+ Except for the license granted herein to GitLab B.V. and recipients of software
6
+ distributed by GitLab B.V., you reserve all right, title, and interest in and to
7
+ your Contributions.
8
+
9
+ All contributions are subject to the Developer Certificate of Origin and license set out at [docs.gitlab.com/ce/legal/developer_certificate_of_origin](https://docs.gitlab.com/ce/legal/developer_certificate_of_origin).
10
+
11
+ _This notice should stay as the first item in the CONTRIBUTING.md file._
12
+
13
+ ## Code of conduct
14
+
15
+ As contributors and maintainers of this project, we pledge to respect all people
16
+ who contribute through reporting issues, posting feature requests, updating
17
+ documentation, submitting pull requests or patches, and other activities.
18
+
19
+ We are committed to making participation in this project a harassment-free
20
+ experience for everyone, regardless of level of experience, gender, gender
21
+ identity and expression, sexual orientation, disability, personal appearance,
22
+ body size, race, ethnicity, age, or religion.
23
+
24
+ Examples of unacceptable behavior by participants include the use of sexual
25
+ language or imagery, derogatory comments or personal attacks, trolling, public
26
+ or private harassment, insults, or other unprofessional conduct.
27
+
28
+ Project maintainers have the right and responsibility to remove, edit, or reject
29
+ comments, commits, code, wiki edits, issues, and other contributions that are
30
+ not aligned to this Code of Conduct. Project maintainers who do not follow the
31
+ Code of Conduct may be removed from the project team.
32
+
33
+ This code of conduct applies both within project spaces and in public spaces
34
+ when an individual is representing the project or its community.
35
+
36
+ Instances of abusive, harassing, or otherwise unacceptable behavior can be
37
+ reported by emailing contact@gitlab.com.
38
+
39
+ This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org), version 1.1.0,
40
+ available at [https://contributor-covenant.org/version/1/1/0/](https://contributor-covenant.org/version/1/1/0/).
41
+
data/LICENSE.txt CHANGED
@@ -1,6 +1,9 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2021 Alex Kalderimis
3
+ Copyright (c) 2021 GitLab
4
+
5
+ The original author of this library is [Jeanine Adkisson](http://jneen.net),
6
+ and copyright is held by GitLab.
4
7
 
5
8
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
9
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # `DeclarativePolicy`: A Declarative Authorization Library
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/declarative_policy.svg)](https://badge.fury.io/rb/declarative_policy)
4
+
3
5
  This library provides a DSL for writing authorization policies.
4
6
 
5
7
  It can be used to separate logic from permissions, and has been
@@ -18,14 +20,72 @@ gem 'declarative_policy'
18
20
 
19
21
  And then execute:
20
22
 
21
- $ bundle install
23
+ ```plain
24
+ $ bundle install
25
+ ```
22
26
 
23
27
  Or install it yourself as:
24
28
 
25
- $ gem install declarative_policy
29
+ ```plain
30
+ $ gem install declarative_policy
31
+ ```
26
32
 
27
- ## Usage
33
+ ## Example
28
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) }
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
29
89
 
30
90
  The core abstraction of this library is a `Policy`. Policies combine:
31
91
 
@@ -34,9 +94,12 @@ The core abstraction of this library is a `Policy`. Policies combine:
34
94
 
35
95
  This library exists to determine the truth value of statements of the form:
36
96
 
97
+ ```plain
98
+ User Predicate [Subject]
37
99
  ```
38
- Subject Predicate [Object]
39
- ```
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).
40
103
 
41
104
  For example:
42
105
 
@@ -61,15 +124,15 @@ class VehiclePolicy < DeclarativePolicy::Base
61
124
  # expensive rules can have 'score'. Higher scores are 'more expensive' to calculate
62
125
  condition(:owns, score: 0) { @subject.owner == @user }
63
126
  condition(:has_access_to, score: 3) { @subject.owner.trusts?(@user) }
64
- condition(:intoxicated, score: 5) { @user.blood_alcohol < laws.max_blood_alcohol }
65
-
127
+ condition(:intoxicated, score: 5) { @user.blood_alcohol > laws.max_blood_alcohol }
128
+
66
129
  # conclusions we can draw:
67
130
  rule { owns }.enable :drive_vehicle
68
131
  rule { has_access_to }.enable :drive_vehicle
69
132
  rule { ~old_enough_to_drive }.prevent :drive_vehicle
70
133
  rule { intoxicated }.prevent :drive_vehicle
71
134
  rule { ~has_driving_license }.prevent :drive_vehicle
72
-
135
+
73
136
  # we can use methods to abstract common logic
74
137
  def laws
75
138
  @subject.registration.country.driving_laws
@@ -116,20 +179,26 @@ policy = DeclarativePolicy.policy_for(user, car, cache: cache)
116
179
  policy.can?(:drive_vehicle)
117
180
  ```
118
181
 
119
- For more usage details, see the [documentation](docs/usage.md).
182
+ For more usage details, see the [documentation](doc).
120
183
 
121
184
  ## Development
122
185
 
123
- After checking out the repository, run `bin/setup` to install dependencies.
186
+ After checking out the repository, run `bundle install` to install dependencies.
124
187
  Then, run `rake spec` to run the tests. You can also run `bin/console` for an
125
188
  interactive prompt that will allow you to experiment.
126
189
 
127
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).
128
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
+
129
198
  ## Contributing
130
199
 
131
- Bug reports and pull requests are welcome on GitHub at
132
- https://gitlab.com/gitlab-org/declarative-policy. This project is intended to be
200
+ Bug reports and merge requests are welcome on GitLab at
201
+ https://gitlab.com/gitlab-org/ruby/gems/declarative-policy. This project is intended to be
133
202
  a safe, welcoming space for collaboration, and contributors are expected to
134
203
  adhere to the [GitLab code of conduct](https://about.gitlab.com/community/contribute/code-of-conduct/).
135
204
 
@@ -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.6.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/-/blob/main/CHANGELOG.md'
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
@@ -1,4 +1,302 @@
1
1
  # Caching
2
2
 
3
- **TODO**: see https://gitlab.com/gitlab-org/declarative-policy/-/issues/11
3
+ This library deals with making observations about the state of
4
+ a system (usually performing I/O, such as making a database query),
5
+ and combining these facts into logical propositions.
4
6
 
7
+ In order to make this performant, the library transparently caches repeated
8
+ observations of conditions. Understanding how caching works is useful for
9
+ designing good policies, using them effectively.
10
+
11
+ ## What is cached?
12
+
13
+ If a policy is instantiated with a cache, then the following things will be
14
+ stored in it:
15
+
16
+ - Policy instances (there will only ever be one policy per `user/subject` pair
17
+ for the lifetime of the cache).
18
+ - Condition results
19
+
20
+ The correctness of these cached values depends on the correctness of the
21
+ cache-keys. We assume the objects in your domain have a `#id` method that
22
+ fully captures the notion of object identity. See [Cache keys](#cache-keys) for
23
+ details. All cache keys begin with `"/dp/"`.
24
+
25
+ Policies themselves cache the results of the abilities they compute.
26
+
27
+ Policies distinguish between facts based on the type of the fact:
28
+
29
+ - Boolean facts: implemented with `condition`.
30
+ - Abilities: implemented with `rule` blocks.
31
+ - Non-boolean facts: implemented by policy instance methods.
32
+
33
+ For example, consider a policy for countries:
34
+
35
+ ```ruby
36
+ class CountryPolicy < DeclarativePolicy::Base
37
+ condition(:citizen) { @user.citizen_of?(country.country_code) }
38
+ condition(:eu_citizen, scope: :user) { @user.citizen_of?(*Unions::EU) }
39
+ condition(:eu_member, scope: :subject) { Unions::EU.include?(country.country_code) }
40
+
41
+ condition(:has_visa_waiver) { country.visa_waivers.any? { |c| @user.citizen_of?(c) } }
42
+ condition(:permanent_resident) { visa_category == :permanent }
43
+ condition(:has_work_visa) { visa_category == :work }
44
+ condition(:has_current_visa) { has_visa_waiver? || current_visa.present? }
45
+ condition(:has_business_visa) { has_visa_waiver? || has_work_visa? || visa_category == :business }
46
+
47
+ condition(:full_rights, score: 20) { citizen? || permanent_resident? }
48
+ condition(:banned) { country.banned_list.include?(@user) }
49
+
50
+ rule { eu_member & eu_citizen }.enable :freedom_of_movement
51
+ rule { full_rights | can?(:freedom_of_movement) }.enable :settle
52
+ rule { can?(:settle) | has_current_visa }.enable :enter_country
53
+ rule { can?(:settle) | has_business_visa }.enable :attend_meetings
54
+ rule { can?(:settle) | has_work_visa }.enable :work
55
+ rule { citizen }.enable :vote
56
+ rule { ~citizen & ~permanent_resident }.enable :apply_for_visa
57
+ rule { banned }.prevent :enter_country, :apply_for_visa
58
+
59
+ def current_visa
60
+ return @current_visa if defined?(@current_visa)
61
+
62
+ @current_visa = country.active_visas.find_by(applicant: @user)
63
+ end
64
+
65
+ def visa_category
66
+ current_visa&.category
67
+ end
68
+
69
+ def country
70
+ @subject
71
+ end
72
+ end
73
+ ```
74
+
75
+ This is a reasonably realistic policy - there are a few pieces of state (the
76
+ country, the list of visa waiver agreements, the list of citizenships the user
77
+ holds, the kind of visa the user has, if they have one, the current list of
78
+ banned users), and these are combined to determine a range of abilities (whether
79
+ one can visit or live in or vote in a certain country). Importantly, these
80
+ pieces of information are re-used between abilities - the citizenship status is
81
+ relevant to all abilities, whereas the banned list is only considered on entry
82
+ and when applying for a new visa).
83
+
84
+ If we imagine that some of these operations are reasonably expensive (fetching
85
+ the current visa status, or checking the banned list, for example), then it
86
+ follows that we really care about avoiding re-computation of these facts. In the
87
+ policy above we can see a few strategies that are taken to avoid this:
88
+
89
+ - Conditions are re-used liberally.
90
+ - Non-boolean facts are cached at the policy level.
91
+
92
+ ## Re-using conditions
93
+
94
+ Rules can and should re-use conditions as much as possible. Condition
95
+ observations are cached automatically, so referring to the same condition in
96
+ multiple rules is encouraged. Conditions can also refer to other conditions by
97
+ using the predicate methods that are created for them (see `full_rights`, which
98
+ refers to the `:citizen` condition as `citizen?`).
99
+
100
+ Note that referring to conditions inside other conditions can be DRY, but it
101
+ limits the ability of the library to optimize the steps (see
102
+ [optimization](./optimization.md)). For example in the `:has_current_visa`
103
+ condition, the sub-conditions will always be tested in the order
104
+ `has_visa_waiver` then `current_visa.present?`. It is recommended not to rely
105
+ heavily on this kind of abstraction.
106
+
107
+ ## Re-using rules
108
+
109
+ Entire rule-sets can be re-used with `can?`. This is a form of logical
110
+ implication where a previous conclusion can be used in a further rule. Examples
111
+ of this here are `can?(:settle)` and `can?(:freedom_of_movement)`. This can
112
+ prevent having to repeat long groups of conditions in rule definitions. This
113
+ abstraction is transparent to the optimizer.
114
+
115
+ ## Non-boolean values must be managed manually
116
+
117
+ The condition `has_current_visa` and the more specific
118
+ `has_{work,business}_visa` all refer to the same piece of state - the
119
+ `#current_visa`. Since this is not a boolean (but is here a database record with
120
+ a `#category` attribute), this cannot be a condition, but must be managed by the
121
+ policy itself.
122
+
123
+ The best approach here is to use normal Ruby methods and instance variables for
124
+ such values. The policy instances themselves are cached, so that any two
125
+ invocations of `DeclarativePolicy.policy_for(user, subject)` with identical
126
+ `user` and `subject` arguments will always return the same policy object. This
127
+ means instance variables stored on the policy will be available for the lifetime
128
+ of the cache.
129
+
130
+ Methods can be used for the usual reasons of clarity (such as referring to the
131
+ `@subject` as `country`) and brevity (such as `visa_category`).
132
+
133
+ ## Cache lifetime
134
+
135
+ The cache is provided by the user of the library, passing it to the
136
+ `.policy_for` method. For example:
137
+
138
+ ```ruby
139
+ DeclarativePolicy.policy_for(user, country, cache: some_cache_value)
140
+ ```
141
+
142
+ The object only needs to implement the following methods:
143
+
144
+ - `cache[key: String] -> Boolean?`: Fetch the cached value
145
+ - `cache.key?(key: String) -> Boolean`: Test if the key is cached
146
+ - `cache[key: String] = Boolean`: Cache a value
147
+
148
+ Obviously, a `HashMap` will work just fine, but so will a wrapper around a
149
+ [`Concurrent::Map`](https://ruby-concurrency.github.io/concurrent-ruby/1.1.4/Concurrent/Map.html),
150
+ or even a map that delegates to Redis with a TTL for each key, so long as the
151
+ object supports these methods. Keys are never deleted by the library, and values
152
+ are only computed if the key is not cached, so it is up to the application code
153
+ to determine the life-time of each key.
154
+
155
+ Clearly, cache-invalidation is a hard problem. At GitLab we share a single cache
156
+ object for each request - so any single request can freely request a permission
157
+ check multiple times (or even compute related abilities, such as
158
+ `:enter_country` and `:settle`) and know that no work is duplicated. This
159
+ allows developers to reason declaratively, and add permission checks where
160
+ needed, without worrying about performance.
161
+
162
+ ## Cache sharing: scopes
163
+
164
+ Not all conditions are equally specific. The condition `citizen` refers to
165
+ both the user and the country, and so can only be used when checking both the
166
+ user and the country. We say that this is the `normal` scope.
167
+
168
+ This is not always true however. Sometimes a condition refers only to the user.
169
+ For example, above we have two conditions: `eu_citizen` and `eu_member`:
170
+
171
+ ```ruby
172
+ condition(:eu_citizen, scope: :user) { @user.citizen_of?(*Unions::EU) }
173
+ condition(:eu_member, scope: :subject) { Unions::EU.include?(country.country_code) }
174
+ ```
175
+
176
+ `eu_citizen` refers only to the user, and `eu_member` refers only to the
177
+ country.
178
+
179
+ If we have a user that wants to enter multiple countries on a grand European
180
+ tour, we could check this with:
181
+
182
+ ```ruby
183
+ itinerary.countries.all? { |c| DeclarativePolicy.policy_for(user, c).allowed?(:enter_country) }
184
+ ```
185
+
186
+ If `eu_citizen` were declared with the `normal` scope, then this would have a lot of cache
187
+ misses. By using the `:user` scope on `eu_citizen`, we only check EU citizenship
188
+ once.
189
+
190
+ Similarly for `eu_member`, if a team of football players want to visit a
191
+ country, then we could check this with:
192
+
193
+ ```ruby
194
+ team.players.all? { |user| DeclarativePolicy.policy_for(user, country).allowed?(:enter_country) }
195
+ ```
196
+
197
+ Again, by declaring `eu_member` as having the `:subject` scope, this ensures we
198
+ only check EU membership once, not once for each football player.
199
+
200
+ The last scope is `:global`, used when the condition is universally true:
201
+
202
+ ```ruby
203
+ condition(:earth_destroyed_by_meteor, scope: global) { !Planet::Earth.exists? }
204
+
205
+ rule { earth_destroyed_by_meteor }.prevent_all
206
+ ```
207
+
208
+ In this case, it doesn't matter who the user is or even where they are going:
209
+ the condition will be computed once (per cache lifetime) for all combinations.
210
+
211
+ Because of the implications for sharing, the scope determines the
212
+ [`#score`](https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/blob/2ab9dbdf44fb37beb8d0f7c131742d47ae9ef5d0/lib/declarative_policy/condition.rb#L58-77) of
213
+ the condition (if not provided explicitly). The intention is to prefer values we
214
+ are more likely (all other things being equal) to re-use:
215
+
216
+ - Conditions we have already cached get a score of `0`.
217
+ - Conditions that are in the `:global` scope get a score of `2`.
218
+ - Conditions that are in the `:user` or `:subject` scopes get a score of `8`.
219
+ - Conditions that are in the `:user_and_subject` scope get a score of `16`.
220
+
221
+ Bear helper-methods in mind when defining scopes. While the instance level cache
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 `:user_and_subject`
224
+ scope), helper-methods will also benefit from improved cache hits.
225
+
226
+ ### Preferred scope
227
+
228
+ In the example situations above (a single user visiting many countries, or a
229
+ football team visiting one country), we know which is more likely to be useful,
230
+ the `:subject` or the `:user` scope. We can inform the optimizer of this
231
+ by setting `DeclarativePolicy.preferred_scope`.
232
+
233
+ To do this, check the abilities within a block bounded
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
+ For example:
236
+
237
+ ```ruby
238
+ cache = {}
239
+
240
+ # preferring to run user-scoped conditions
241
+ DeclarativePolicy.with_preferred_scope(:user) do
242
+ itinerary.countries.all? do |c|
243
+ DeclarativePolicy.policy_for(user, c, cache: cache).allowed?(:enter_country)
244
+ end
245
+ end
246
+
247
+ # preferring to run subject-scoped conditions
248
+ DeclarativePolicy.with_preferred_scope(:subject) do
249
+ team.players.all? do |player|
250
+ DeclarativePolicy.policy_for(player, c, cache: cache).allowed?(:enter_country)
251
+ end
252
+ end
253
+
254
+ ```
255
+
256
+ When we set `preferred_scope`, this reduces the default score for conditions in
257
+ that scope, so that they are more likely to be executed first. Instead of `8`,
258
+ they are given a default score of `4`.
259
+
260
+ ## Cache keys
261
+
262
+ In order for an object to be cached, it should be able to identify itself
263
+ with a suitable cache key. A good cache key will identify an object, without
264
+ containing irrelevant information - a database `#id` is perfect, and this
265
+ library defaults to calling an `#id` method on objects, falling back to
266
+ `object_id`.
267
+
268
+ Relying on `object_id` is not recommended since otherwise equivalent objects
269
+ have different `object_id` values, and using `object_id` will not get optimal caching. All
270
+ policy subjects should implement `#id` for this reason. `ActiveRecord` models
271
+ with an `id` primary ID attribute do not need any extra configuration.
272
+
273
+ Please see: [`DeclarativePolicy::Cache`](https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/blob/main/lib/declarative_policy/cache.rb).
274
+
275
+ ## Cache invalidation
276
+
277
+ Generally, cache invalidation is best avoided. It is very hard to get right, and
278
+ relying on it opens you up to subtle but pernicious bugs that are hard to
279
+ reproduce and debug.
280
+
281
+ The best strategy is to run all permission checks upfront, before mutating any
282
+ state that might change a permission computation. For instance, if you want to
283
+ make a user an administrator, then check for permission **before** assigning
284
+ administrator privileges.
285
+
286
+ However, it isn't always possible to avoid needing to mark certain parts of the
287
+ cached state as dirty (in need of re-computation). If this is needed, then you
288
+ can call the `DeclarativePolicy.invalidate(cache, keys)` method. This takes an
289
+ enumerable of dirty keys, and:
290
+
291
+ - removes the cached condition results from the cache
292
+ - marks the abilities that depend on those conditions as dirty, and in need of
293
+ re-computation.
294
+
295
+ The responsibility for determining which cache-keys are dirty falls on the
296
+ client. You could, for example, do this by observing which keys are added to the
297
+ cache (knowing that condition keys all start with `"/dp/condition/"`), or by
298
+ scanning the cache for keys that match a heuristic.
299
+
300
+ This method is the only place where the `#delete` method is called on the cache.
301
+ If you do not call `.invalidate`, there is no need for the cache to implement
302
+ `#delete`.