declarative_policy 2.1.0 → 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/lib/declarative_policy/base.rb +100 -31
- data/lib/declarative_policy/condition.rb +5 -8
- data/lib/declarative_policy/runner.rb +49 -23
- data/lib/declarative_policy/version.rb +1 -1
- data/lib/declarative_policy.rb +1 -1
- metadata +2 -2
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
|
|
@@ -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,28 +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
148
|
# ones, filtering out any global actions that have excepted this ability.
|
|
117
149
|
def configuration_for(ability)
|
|
118
|
-
|
|
119
|
-
next if exceptions&.include?(ability)
|
|
120
|
-
|
|
121
|
-
[action, rule]
|
|
122
|
-
end
|
|
150
|
+
return build_configuration_for(ability) unless ability_map.key?(ability)
|
|
123
151
|
|
|
124
|
-
|
|
152
|
+
configurations = memoized_definition(:configurations) { {} }
|
|
153
|
+
configurations[ability] ||= build_configuration_for(ability)
|
|
125
154
|
end
|
|
126
155
|
|
|
127
156
|
### declaration methods ###
|
|
@@ -140,6 +169,7 @@ module DeclarativePolicy
|
|
|
140
169
|
# rubocop: enable GitlabSecurity/PublicSend
|
|
141
170
|
|
|
142
171
|
own_delegations[name] = delegation_block
|
|
172
|
+
invalidate_definitions!
|
|
143
173
|
end
|
|
144
174
|
|
|
145
175
|
# Declare that the given abilities should not be read from delegates.
|
|
@@ -202,6 +232,7 @@ module DeclarativePolicy
|
|
|
202
232
|
condition = Condition.new(condition_name, condition_options(opts), &value)
|
|
203
233
|
|
|
204
234
|
own_conditions[condition_name] = condition
|
|
235
|
+
invalidate_definitions!
|
|
205
236
|
|
|
206
237
|
define_method(:"#{condition_name}?") { condition(condition_name).pass? }
|
|
207
238
|
end
|
|
@@ -213,10 +244,12 @@ module DeclarativePolicy
|
|
|
213
244
|
# symbol indicating :prevent or :enable.
|
|
214
245
|
def enable_when(abilities, rule)
|
|
215
246
|
abilities.each { |a| own_ability_map.enable(a, rule) }
|
|
247
|
+
invalidate_definitions!
|
|
216
248
|
end
|
|
217
249
|
|
|
218
250
|
def prevent_when(abilities, rule)
|
|
219
251
|
abilities.each { |a| own_ability_map.prevent(a, rule) }
|
|
252
|
+
invalidate_definitions!
|
|
220
253
|
end
|
|
221
254
|
|
|
222
255
|
# we store global prevents (from `prevent_all`) separately,
|
|
@@ -225,10 +258,37 @@ module DeclarativePolicy
|
|
|
225
258
|
# that should be excluded from the blanket prevent.
|
|
226
259
|
def prevent_all_when(rule, except: nil)
|
|
227
260
|
own_global_actions << [:prevent, rule, except]
|
|
261
|
+
invalidate_definitions!
|
|
228
262
|
end
|
|
229
263
|
|
|
230
264
|
private
|
|
231
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
|
+
|
|
232
292
|
# retrieve and zero out the previously set options (used in .condition)
|
|
233
293
|
def condition_options(opts)
|
|
234
294
|
# The context_key distinguishes two conditions of the same name.
|
|
@@ -353,12 +413,21 @@ module DeclarativePolicy
|
|
|
353
413
|
@_conditions ||= {}
|
|
354
414
|
@_conditions[name] ||=
|
|
355
415
|
begin
|
|
356
|
-
|
|
416
|
+
definition = self.class.conditions[name]
|
|
417
|
+
raise "invalid condition #{name}" unless definition
|
|
357
418
|
|
|
358
|
-
ManifestCondition.new(
|
|
419
|
+
ManifestCondition.new(definition, self)
|
|
359
420
|
end
|
|
360
421
|
end
|
|
361
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
|
+
|
|
362
431
|
# used in specs - returns true if there is no possible way for any action
|
|
363
432
|
# to be allowed, determined only by the global :prevent_all rules.
|
|
364
433
|
# Only considers global actions with no exceptions (a prevent_all with
|
|
@@ -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
|
|
@@ -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
|
@@ -37,7 +37,7 @@ module DeclarativePolicy
|
|
|
37
37
|
policies = cache.select { |k, _| k.is_a?(String) && k.start_with?('/dp/policy/') }
|
|
38
38
|
|
|
39
39
|
policies.each_value do |policy|
|
|
40
|
-
policy.runners.
|
|
40
|
+
policy.runners.each_value do |runner|
|
|
41
41
|
runner.uncache! if keys.intersect?(runner.dependencies)
|
|
42
42
|
end
|
|
43
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.
|
|
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: 2026-
|
|
11
|
+
date: 2026-09-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: benchmark-ips
|