declarative_policy 2.0.1 → 2.2.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 +2 -0
- data/README.md +15 -1
- data/declarative-policy.gemspec +1 -1
- data/doc/defining-policies.md +36 -0
- data/lib/declarative_policy/base.rb +118 -30
- data/lib/declarative_policy/condition.rb +5 -8
- data/lib/declarative_policy/policy_dsl.rb +8 -2
- data/lib/declarative_policy/prevent_all_dsl.rb +25 -0
- data/lib/declarative_policy/runner.rb +49 -23
- data/lib/declarative_policy/version.rb +1 -1
- data/lib/declarative_policy.rb +2 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 735e59059f970e9e39c6fc37b1d97f7b20c030a472ca287cb95d4503a168c3a3
|
|
4
|
+
data.tar.gz: e4bd527d1ab32d86a77666e9b0c175a2e5c1cc8d4627b94fa3d01eb8c6dc6e10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 65796bc8d62ce7648433adba6feb99e78e2fff32c27902318a8394973fad38a8b7a476b8a13806178c8d4b20f8f5385e398336c2872a6c58122d213c8d864fbb
|
|
7
|
+
data.tar.gz: 4ab6e8f55e9efadbf2169bd4b385ec8620fdca8247cfcc1b977fc3ac5fb02f045fb1616caef9878fd0e92f37f6a98c0a58b04f458c9b2c67aec98d4b22545b99
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -202,6 +202,20 @@ https://gitlab.com/gitlab-org/ruby/gems/declarative-policy. This project is inte
|
|
|
202
202
|
a safe, welcoming space for collaboration, and contributors are expected to
|
|
203
203
|
adhere to the [GitLab code of conduct](https://about.gitlab.com/community/contribute/code-of-conduct/).
|
|
204
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
|
+
|
|
205
219
|
## License
|
|
206
220
|
|
|
207
221
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -210,4 +224,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
210
224
|
|
|
211
225
|
Everyone interacting in the `DeclarativePolicy` project's codebase, issue
|
|
212
226
|
trackers, chat rooms and mailing lists is expected to follow
|
|
213
|
-
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).
|
data/declarative-policy.gemspec
CHANGED
|
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
|
|
|
23
23
|
|
|
24
24
|
spec.metadata['homepage_uri'] = spec.homepage
|
|
25
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/-/
|
|
26
|
+
spec.metadata['changelog_uri'] = 'https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/releases'
|
|
27
27
|
|
|
28
28
|
spec.metadata['rubygems_mfa_required'] = 'false'
|
|
29
29
|
|
data/doc/defining-policies.md
CHANGED
|
@@ -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
|
|
@@ -6,6 +6,8 @@ module DeclarativePolicy
|
|
|
6
6
|
# or :prevent actions. Used to look up which rules apply to
|
|
7
7
|
# a given ability. See Base.ability_map
|
|
8
8
|
class AbilityMap
|
|
9
|
+
EMPTY_ACTIONS = [].freeze
|
|
10
|
+
|
|
9
11
|
attr_reader :map
|
|
10
12
|
|
|
11
13
|
def initialize(map = {})
|
|
@@ -20,16 +22,31 @@ module DeclarativePolicy
|
|
|
20
22
|
AbilityMap.new(@map.merge(other.map, &conflict_proc))
|
|
21
23
|
end
|
|
22
24
|
|
|
25
|
+
# Does not add the ability if it is missing. An application may check
|
|
26
|
+
# abilities named by user input, and storing those would grow the map
|
|
27
|
+
# without limit.
|
|
23
28
|
def actions(key)
|
|
24
|
-
@map[key]
|
|
29
|
+
@map[key] || EMPTY_ACTIONS
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def key?(key)
|
|
33
|
+
@map.key?(key)
|
|
25
34
|
end
|
|
26
35
|
|
|
27
36
|
def enable(key, rule)
|
|
28
|
-
|
|
37
|
+
rules_for(key) << [:enable, rule]
|
|
29
38
|
end
|
|
30
39
|
|
|
31
40
|
def prevent(key, rule)
|
|
32
|
-
|
|
41
|
+
rules_for(key) << [:prevent, rule]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# The writer's counterpart to #actions: this one does create the list,
|
|
47
|
+
# because a rule has to be appended to something.
|
|
48
|
+
def rules_for(key)
|
|
49
|
+
@map[key] ||= []
|
|
33
50
|
end
|
|
34
51
|
end
|
|
35
52
|
|
|
@@ -51,18 +68,25 @@ module DeclarativePolicy
|
|
|
51
68
|
end
|
|
52
69
|
end
|
|
53
70
|
|
|
71
|
+
# Counts declarations across all policies, so the caches below can tell
|
|
72
|
+
# that something was declared since they were built. Without it a policy
|
|
73
|
+
# loaded or reopened later would be seen with an out of date set of rules.
|
|
74
|
+
@definition_generation = 0
|
|
75
|
+
|
|
54
76
|
class << self
|
|
55
77
|
# The `own_ability_map` vs `ability_map` distinction is used so that
|
|
56
78
|
# the data structure is properly inherited - with subclasses recursively
|
|
57
79
|
# merging their parent class.
|
|
58
80
|
#
|
|
59
81
|
# This pattern is also used for conditions, global_actions, and delegations.
|
|
82
|
+
#
|
|
83
|
+
# The combined result is cached because rebuilding it is expensive, and
|
|
84
|
+
# every policy object would otherwise redo it, once per subject when
|
|
85
|
+
# authorizing a batch.
|
|
60
86
|
def ability_map
|
|
61
|
-
if self == Base
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
superclass.ability_map.merge(own_ability_map)
|
|
65
|
-
end
|
|
87
|
+
return own_ability_map if self == Base
|
|
88
|
+
|
|
89
|
+
memoized_definition(:ability_map) { superclass.ability_map.merge(own_ability_map) }
|
|
66
90
|
end
|
|
67
91
|
|
|
68
92
|
def own_ability_map
|
|
@@ -71,11 +95,9 @@ module DeclarativePolicy
|
|
|
71
95
|
|
|
72
96
|
# an inheritable map of conditions, by name
|
|
73
97
|
def conditions
|
|
74
|
-
if self == Base
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
superclass.conditions.merge(own_conditions)
|
|
78
|
-
end
|
|
98
|
+
return own_conditions if self == Base
|
|
99
|
+
|
|
100
|
+
memoized_definition(:conditions) { superclass.conditions.merge(own_conditions) }
|
|
79
101
|
end
|
|
80
102
|
|
|
81
103
|
def own_conditions
|
|
@@ -86,11 +108,9 @@ module DeclarativePolicy
|
|
|
86
108
|
# stored in `ability_map` because they aren't indexed by a particular
|
|
87
109
|
# ability.
|
|
88
110
|
def global_actions
|
|
89
|
-
if self == Base
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
superclass.global_actions + own_global_actions
|
|
93
|
-
end
|
|
111
|
+
return own_global_actions if self == Base
|
|
112
|
+
|
|
113
|
+
memoized_definition(:global_actions) { superclass.global_actions + own_global_actions }
|
|
94
114
|
end
|
|
95
115
|
|
|
96
116
|
def own_global_actions
|
|
@@ -100,22 +120,37 @@ module DeclarativePolicy
|
|
|
100
120
|
# an inheritable map of delegations, indexed by name (which may be
|
|
101
121
|
# autogenerated)
|
|
102
122
|
def delegations
|
|
103
|
-
if self == Base
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
superclass.delegations.merge(own_delegations)
|
|
107
|
-
end
|
|
123
|
+
return own_delegations if self == Base
|
|
124
|
+
|
|
125
|
+
memoized_definition(:delegations) { superclass.delegations.merge(own_delegations) }
|
|
108
126
|
end
|
|
109
127
|
|
|
110
128
|
def own_delegations
|
|
111
129
|
@own_delegations ||= {}
|
|
112
130
|
end
|
|
113
131
|
|
|
132
|
+
# How many declarations (conditions, rules, delegations) have been made
|
|
133
|
+
# across all policies so far. Save this value and compare it later to
|
|
134
|
+
# tell whether any policy has been defined or reopened since.
|
|
135
|
+
def definition_generation
|
|
136
|
+
Base.instance_variable_get(:@definition_generation)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Clears the cached results for every policy class. Declaration methods
|
|
140
|
+
# like condition and enable_when call this for you; call it yourself
|
|
141
|
+
# only if you write to own_conditions and friends directly.
|
|
142
|
+
def invalidate_definitions!
|
|
143
|
+
Base.instance_variable_set(:@definition_generation, definition_generation + 1)
|
|
144
|
+
end
|
|
145
|
+
|
|
114
146
|
# all the [rule, action] pairs that apply to a particular ability.
|
|
115
147
|
# we combine the specific ones looked up in ability_map with the global
|
|
116
|
-
# ones.
|
|
148
|
+
# ones, filtering out any global actions that have excepted this ability.
|
|
117
149
|
def configuration_for(ability)
|
|
118
|
-
ability_map.
|
|
150
|
+
return build_configuration_for(ability) unless ability_map.key?(ability)
|
|
151
|
+
|
|
152
|
+
configurations = memoized_definition(:configurations) { {} }
|
|
153
|
+
configurations[ability] ||= build_configuration_for(ability)
|
|
119
154
|
end
|
|
120
155
|
|
|
121
156
|
### declaration methods ###
|
|
@@ -134,6 +169,7 @@ module DeclarativePolicy
|
|
|
134
169
|
# rubocop: enable GitlabSecurity/PublicSend
|
|
135
170
|
|
|
136
171
|
own_delegations[name] = delegation_block
|
|
172
|
+
invalidate_definitions!
|
|
137
173
|
end
|
|
138
174
|
|
|
139
175
|
# Declare that the given abilities should not be read from delegates.
|
|
@@ -196,6 +232,7 @@ module DeclarativePolicy
|
|
|
196
232
|
condition = Condition.new(condition_name, condition_options(opts), &value)
|
|
197
233
|
|
|
198
234
|
own_conditions[condition_name] = condition
|
|
235
|
+
invalidate_definitions!
|
|
199
236
|
|
|
200
237
|
define_method(:"#{condition_name}?") { condition(condition_name).pass? }
|
|
201
238
|
end
|
|
@@ -207,20 +244,51 @@ module DeclarativePolicy
|
|
|
207
244
|
# symbol indicating :prevent or :enable.
|
|
208
245
|
def enable_when(abilities, rule)
|
|
209
246
|
abilities.each { |a| own_ability_map.enable(a, rule) }
|
|
247
|
+
invalidate_definitions!
|
|
210
248
|
end
|
|
211
249
|
|
|
212
250
|
def prevent_when(abilities, rule)
|
|
213
251
|
abilities.each { |a| own_ability_map.prevent(a, rule) }
|
|
252
|
+
invalidate_definitions!
|
|
214
253
|
end
|
|
215
254
|
|
|
216
255
|
# we store global prevents (from `prevent_all`) separately,
|
|
217
256
|
# so that they can be combined into every decision made.
|
|
218
|
-
|
|
219
|
-
|
|
257
|
+
# The optional `except` parameter is a Set of abilities
|
|
258
|
+
# that should be excluded from the blanket prevent.
|
|
259
|
+
def prevent_all_when(rule, except: nil)
|
|
260
|
+
own_global_actions << [:prevent, rule, except]
|
|
261
|
+
invalidate_definitions!
|
|
220
262
|
end
|
|
221
263
|
|
|
222
264
|
private
|
|
223
265
|
|
|
266
|
+
def build_configuration_for(ability)
|
|
267
|
+
applicable_globals = global_actions.filter_map do |(action, rule, exceptions)|
|
|
268
|
+
next if exceptions&.include?(ability)
|
|
269
|
+
|
|
270
|
+
[action, rule]
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
(ability_map.actions(ability) + applicable_globals).freeze
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Caches this class's combined result (its own declarations plus its
|
|
277
|
+
# superclass's) until any policy anywhere declares something new.
|
|
278
|
+
def memoized_definition(name)
|
|
279
|
+
generation = Base.definition_generation
|
|
280
|
+
|
|
281
|
+
unless @memoized_definitions_at == generation
|
|
282
|
+
@definitions = {}
|
|
283
|
+
@memoized_definitions_at = generation
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
defs = @definitions
|
|
287
|
+
return defs[name] if defs.key?(name)
|
|
288
|
+
|
|
289
|
+
defs.store(name, yield)
|
|
290
|
+
end
|
|
291
|
+
|
|
224
292
|
# retrieve and zero out the previously set options (used in .condition)
|
|
225
293
|
def condition_options(opts)
|
|
226
294
|
# The context_key distinguishes two conditions of the same name.
|
|
@@ -255,6 +323,8 @@ module DeclarativePolicy
|
|
|
255
323
|
# This is the main entry point for permission checks. It constructs
|
|
256
324
|
# or looks up a Runner for the given ability and asks it if it passes.
|
|
257
325
|
def allowed?(*abilities)
|
|
326
|
+
return false if abilities.empty?
|
|
327
|
+
|
|
258
328
|
abilities.all? { |a| runner(a).pass? }
|
|
259
329
|
end
|
|
260
330
|
|
|
@@ -343,16 +413,34 @@ module DeclarativePolicy
|
|
|
343
413
|
@_conditions ||= {}
|
|
344
414
|
@_conditions[name] ||=
|
|
345
415
|
begin
|
|
346
|
-
|
|
416
|
+
definition = self.class.conditions[name]
|
|
417
|
+
raise "invalid condition #{name}" unless definition
|
|
347
418
|
|
|
348
|
-
ManifestCondition.new(
|
|
419
|
+
ManifestCondition.new(definition, self)
|
|
349
420
|
end
|
|
350
421
|
end
|
|
351
422
|
|
|
423
|
+
def user_cache_key
|
|
424
|
+
@user_cache_key ||= Cache.user_key(@user)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def subject_cache_key
|
|
428
|
+
@subject_cache_key ||= Cache.subject_key(@subject)
|
|
429
|
+
end
|
|
430
|
+
|
|
352
431
|
# used in specs - returns true if there is no possible way for any action
|
|
353
432
|
# to be allowed, determined only by the global :prevent_all rules.
|
|
433
|
+
# Only considers global actions with no exceptions (a prevent_all with
|
|
434
|
+
# exceptions cannot fully ban, since the excepted abilities may still pass).
|
|
354
435
|
def banned?
|
|
355
|
-
global_steps = self.class.global_actions.
|
|
436
|
+
global_steps = self.class.global_actions.filter_map do |(action, rule, exceptions)|
|
|
437
|
+
next if exceptions && !exceptions.empty?
|
|
438
|
+
|
|
439
|
+
Step.new(self, rule, action)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
return false if global_steps.empty?
|
|
443
|
+
|
|
356
444
|
!Runner.new(global_steps).pass?
|
|
357
445
|
end
|
|
358
446
|
|
|
@@ -7,7 +7,7 @@ module DeclarativePolicy
|
|
|
7
7
|
# declaration. It holds on to the block to be instance_eval'd
|
|
8
8
|
# on a context (instance of Base) later, via #compute.
|
|
9
9
|
class Condition
|
|
10
|
-
attr_reader :name, :description, :scope, :manual_score, :context_key
|
|
10
|
+
attr_reader :name, :description, :scope, :manual_score, :context_key, :key
|
|
11
11
|
|
|
12
12
|
VALID_SCOPES = %i[user_and_subject user subject global].freeze
|
|
13
13
|
ALLOWED_SCOPES = VALID_SCOPES + %i[normal]
|
|
@@ -20,16 +20,13 @@ module DeclarativePolicy
|
|
|
20
20
|
@description = opts.delete(:description)
|
|
21
21
|
@context_key = opts[:context_key]
|
|
22
22
|
@manual_score = opts.fetch(:score, nil)
|
|
23
|
+
@key = "#{@context_key}/#{@name}".freeze
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
def compute(context)
|
|
26
27
|
!!context.instance_eval(&@compute)
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
def key
|
|
30
|
-
"#{@context_key}/#{@name}"
|
|
31
|
-
end
|
|
32
|
-
|
|
33
30
|
private
|
|
34
31
|
|
|
35
32
|
def fetch_scope(options)
|
|
@@ -107,17 +104,17 @@ module DeclarativePolicy
|
|
|
107
104
|
when :user then "/dp/condition/#{@condition.key}/#{user_key}"
|
|
108
105
|
when :subject then "/dp/condition/#{@condition.key}/#{subject_key}"
|
|
109
106
|
else "/dp/condition/#{@condition.key}/#{user_key},#{subject_key}"
|
|
110
|
-
end
|
|
107
|
+
end.freeze
|
|
111
108
|
end
|
|
112
109
|
|
|
113
110
|
private
|
|
114
111
|
|
|
115
112
|
def user_key
|
|
116
|
-
|
|
113
|
+
@context.user_cache_key
|
|
117
114
|
end
|
|
118
115
|
|
|
119
116
|
def subject_key
|
|
120
|
-
|
|
117
|
+
@context.subject_cache_key
|
|
121
118
|
end
|
|
122
119
|
end
|
|
123
120
|
end
|
|
@@ -29,8 +29,14 @@ 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
42
|
def method_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
|
|
@@ -5,12 +5,10 @@ require 'set'
|
|
|
5
5
|
module DeclarativePolicy
|
|
6
6
|
class Runner
|
|
7
7
|
class State
|
|
8
|
-
attr_reader :called_conditions
|
|
9
|
-
|
|
10
8
|
def initialize
|
|
11
9
|
@enabled = false
|
|
12
10
|
@prevented = false
|
|
13
|
-
@
|
|
11
|
+
@called = nil
|
|
14
12
|
end
|
|
15
13
|
|
|
16
14
|
def enable!
|
|
@@ -34,7 +32,31 @@ module DeclarativePolicy
|
|
|
34
32
|
end
|
|
35
33
|
|
|
36
34
|
def register(manifest_condition)
|
|
37
|
-
|
|
35
|
+
called[manifest_condition.cache_key] = true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def called_conditions
|
|
39
|
+
return Set.new unless @called
|
|
40
|
+
|
|
41
|
+
Set.new(@called.keys)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def absorb(other)
|
|
45
|
+
return if other.equal?(self) || other.called_any.nil?
|
|
46
|
+
|
|
47
|
+
called.update(other.called_any)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
protected
|
|
51
|
+
|
|
52
|
+
def called_any
|
|
53
|
+
@called
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def called
|
|
59
|
+
@called ||= {}
|
|
38
60
|
end
|
|
39
61
|
end
|
|
40
62
|
|
|
@@ -81,7 +103,7 @@ module DeclarativePolicy
|
|
|
81
103
|
run unless cached?
|
|
82
104
|
|
|
83
105
|
parent_state = Thread.current[:declarative_policy_current_runner_state]
|
|
84
|
-
parent_state&.
|
|
106
|
+
parent_state&.absorb(@state)
|
|
85
107
|
|
|
86
108
|
@state.pass?
|
|
87
109
|
end
|
|
@@ -175,49 +197,53 @@ module DeclarativePolicy
|
|
|
175
197
|
return
|
|
176
198
|
end
|
|
177
199
|
|
|
178
|
-
|
|
179
|
-
|
|
200
|
+
remaining = @steps.uniq
|
|
201
|
+
enablers_left = remaining.count(&:enable?)
|
|
202
|
+
preventers_only = false
|
|
180
203
|
|
|
181
204
|
loop do
|
|
182
205
|
if @state.enabled?
|
|
183
206
|
# Once we set this, we never need to unset it, because a single
|
|
184
207
|
# prevent will stop this from being enabled
|
|
185
|
-
|
|
186
|
-
|
|
208
|
+
unless preventers_only
|
|
209
|
+
remaining.reject!(&:enable?)
|
|
210
|
+
preventers_only = true
|
|
211
|
+
end
|
|
212
|
+
elsif enablers_left.zero?
|
|
187
213
|
# if the permission hasn't yet been enabled and we only have
|
|
188
214
|
# prevent steps left, we short-circuit the state here
|
|
189
215
|
@state.prevent!
|
|
190
216
|
return unless debugging
|
|
191
217
|
end
|
|
192
218
|
|
|
193
|
-
return if
|
|
219
|
+
return if remaining.empty?
|
|
194
220
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
set.delete(next_step)
|
|
199
|
-
end
|
|
221
|
+
lowest_index, lowest_score = cheapest_step(remaining)
|
|
222
|
+
step = remaining.delete_at(lowest_index)
|
|
223
|
+
enablers_left -= 1 if step.enable?
|
|
200
224
|
|
|
201
|
-
yield
|
|
225
|
+
yield step, lowest_score
|
|
202
226
|
end
|
|
203
227
|
end
|
|
204
228
|
|
|
205
|
-
def
|
|
229
|
+
def cheapest_step(remaining)
|
|
206
230
|
lowest_score = Float::INFINITY
|
|
207
|
-
|
|
231
|
+
lowest_index = 0
|
|
232
|
+
index = 0
|
|
208
233
|
|
|
209
|
-
|
|
210
|
-
score =
|
|
234
|
+
while index < remaining.size
|
|
235
|
+
score = remaining[index].score
|
|
211
236
|
|
|
212
237
|
if score < lowest_score
|
|
213
|
-
next_step = step
|
|
214
238
|
lowest_score = score
|
|
239
|
+
lowest_index = index
|
|
240
|
+
break if score.zero?
|
|
215
241
|
end
|
|
216
242
|
|
|
217
|
-
|
|
243
|
+
index += 1
|
|
218
244
|
end
|
|
219
245
|
|
|
220
|
-
[
|
|
246
|
+
[lowest_index, lowest_score]
|
|
221
247
|
end
|
|
222
248
|
|
|
223
249
|
# Formatter for debugging output.
|
data/lib/declarative_policy.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'set'
|
|
|
4
4
|
require_relative 'declarative_policy/cache'
|
|
5
5
|
require_relative 'declarative_policy/condition'
|
|
6
6
|
require_relative 'declarative_policy/delegate_dsl'
|
|
7
|
+
require_relative 'declarative_policy/prevent_all_dsl'
|
|
7
8
|
require_relative 'declarative_policy/policy_dsl'
|
|
8
9
|
require_relative 'declarative_policy/rule_dsl'
|
|
9
10
|
require_relative 'declarative_policy/preferred_scope'
|
|
@@ -36,7 +37,7 @@ module DeclarativePolicy
|
|
|
36
37
|
policies = cache.select { |k, _| k.is_a?(String) && k.start_with?('/dp/policy/') }
|
|
37
38
|
|
|
38
39
|
policies.each_value do |policy|
|
|
39
|
-
policy.runners.
|
|
40
|
+
policy.runners.each_value do |runner|
|
|
40
41
|
runner.uncache! if keys.intersect?(runner.dependencies)
|
|
41
42
|
end
|
|
42
43
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: declarative_policy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0
|
|
4
|
+
version: 2.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- group::authorization
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-09-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: benchmark-ips
|
|
@@ -140,6 +140,7 @@ files:
|
|
|
140
140
|
- lib/declarative_policy/nil_policy.rb
|
|
141
141
|
- lib/declarative_policy/policy_dsl.rb
|
|
142
142
|
- lib/declarative_policy/preferred_scope.rb
|
|
143
|
+
- lib/declarative_policy/prevent_all_dsl.rb
|
|
143
144
|
- lib/declarative_policy/rule.rb
|
|
144
145
|
- lib/declarative_policy/rule_dsl.rb
|
|
145
146
|
- lib/declarative_policy/runner.rb
|
|
@@ -151,7 +152,7 @@ licenses:
|
|
|
151
152
|
metadata:
|
|
152
153
|
homepage_uri: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy
|
|
153
154
|
source_code_uri: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy
|
|
154
|
-
changelog_uri: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/
|
|
155
|
+
changelog_uri: https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/releases
|
|
155
156
|
rubygems_mfa_required: 'false'
|
|
156
157
|
post_install_message:
|
|
157
158
|
rdoc_options: []
|