declarative_policy 1.0.0 → 1.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/.gitignore +2 -0
- data/.gitlab-ci.yml +59 -16
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +8 -0
- data/CONTRIBUTING.md +41 -0
- data/Gemfile +7 -8
- data/Gemfile.lock +37 -20
- data/LICENSE.txt +4 -1
- data/README.md +6 -4
- data/benchmarks/repeated_invocation.rb +37 -0
- data/declarative_policy.gemspec +1 -1
- data/doc/caching.md +299 -1
- data/doc/defining-policies.md +29 -3
- data/doc/optimization.md +277 -0
- data/lib/declarative_policy/base.rb +60 -28
- data/lib/declarative_policy/cache.rb +1 -1
- data/lib/declarative_policy/condition.rb +4 -2
- data/lib/declarative_policy/configuration.rb +7 -1
- data/lib/declarative_policy/rule.rb +5 -5
- data/lib/declarative_policy/runner.rb +58 -26
- data/lib/declarative_policy/version.rb +1 -1
- data/lib/declarative_policy.rb +30 -40
- metadata +11 -7
@@ -33,6 +33,24 @@ module DeclarativePolicy
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
+
class Options
|
37
|
+
def initialize
|
38
|
+
@hash = {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def []=(key, value)
|
42
|
+
@hash[key.to_sym] = value
|
43
|
+
end
|
44
|
+
|
45
|
+
def [](key)
|
46
|
+
@hash[key.to_sym]
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_h
|
50
|
+
@hash
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
36
54
|
class << self
|
37
55
|
# The `own_ability_map` vs `ability_map` distinction is used so that
|
38
56
|
# the data structure is properly inherited - with subclasses recursively
|
@@ -146,46 +164,40 @@ module DeclarativePolicy
|
|
146
164
|
|
147
165
|
# A hash in which to store calls to `desc` and `with_scope`, etc.
|
148
166
|
def last_options
|
149
|
-
@last_options ||=
|
167
|
+
@last_options ||= Options.new
|
150
168
|
end
|
151
169
|
|
152
|
-
|
153
|
-
|
154
|
-
last_options.tap { @last_options = nil }
|
170
|
+
def with_options(opts = {})
|
171
|
+
last_options.to_h.merge!(opts.to_h)
|
155
172
|
end
|
156
173
|
|
157
174
|
# Declare a description for the following condition. Currently unused,
|
158
175
|
# but opens the potential for explaining to users why they were or were
|
159
176
|
# not able to do something.
|
160
177
|
def desc(description)
|
161
|
-
|
162
|
-
end
|
163
|
-
|
164
|
-
def with_options(opts = {})
|
165
|
-
last_options.merge!(opts)
|
178
|
+
with_options description: description
|
166
179
|
end
|
167
180
|
|
181
|
+
# Declare a scope for the following condition.
|
168
182
|
def with_scope(scope)
|
169
183
|
with_options scope: scope
|
170
184
|
end
|
171
185
|
|
186
|
+
# Declare a score for the following condition.
|
172
187
|
def with_score(score)
|
173
188
|
with_options score: score
|
174
189
|
end
|
175
190
|
|
176
191
|
# Declares a condition. It gets stored in `own_conditions`, and generates
|
177
192
|
# a query method based on the condition's name.
|
178
|
-
def condition(
|
179
|
-
|
193
|
+
def condition(condition_name, opts = {}, &value)
|
194
|
+
condition_name = condition_name.to_sym
|
180
195
|
|
181
|
-
|
182
|
-
opts[:context_key] ||= self.name
|
196
|
+
condition = Condition.new(condition_name, condition_options(opts), &value)
|
183
197
|
|
184
|
-
|
198
|
+
own_conditions[condition_name] = condition
|
185
199
|
|
186
|
-
|
187
|
-
|
188
|
-
define_method(:"#{name}?") { condition(name).pass? }
|
200
|
+
define_method(:"#{condition_name}?") { condition(condition_name).pass? }
|
189
201
|
end
|
190
202
|
|
191
203
|
# These next three methods are mainly called from PolicyDsl,
|
@@ -206,6 +218,16 @@ module DeclarativePolicy
|
|
206
218
|
def prevent_all_when(rule)
|
207
219
|
own_global_actions << [:prevent, rule]
|
208
220
|
end
|
221
|
+
|
222
|
+
private
|
223
|
+
|
224
|
+
# retrieve and zero out the previously set options (used in .condition)
|
225
|
+
def condition_options(opts)
|
226
|
+
# The context_key distinguishes two conditions of the same name.
|
227
|
+
# For anonymous classes, use object_id.
|
228
|
+
opts[:context_key] ||= (name || object_id)
|
229
|
+
with_options(opts).tap { @last_options = nil }
|
230
|
+
end
|
209
231
|
end
|
210
232
|
|
211
233
|
# A policy object contains a specific user and subject on which
|
@@ -254,16 +276,23 @@ module DeclarativePolicy
|
|
254
276
|
condition(:default, scope: :global, score: 0) { true }
|
255
277
|
|
256
278
|
def repr
|
257
|
-
|
258
|
-
|
259
|
-
"#{@subject.class.name}/#{@subject.id}"
|
260
|
-
else
|
261
|
-
@subject.inspect
|
262
|
-
end
|
279
|
+
"(#{identify_user} : #{identify_subject})"
|
280
|
+
end
|
263
281
|
|
264
|
-
|
282
|
+
def identify_user
|
283
|
+
return '<anonymous>' unless @user
|
265
284
|
|
266
|
-
|
285
|
+
@user.to_reference
|
286
|
+
rescue NoMethodError
|
287
|
+
"<#{@user.class}: #{@user.object_id}>"
|
288
|
+
end
|
289
|
+
|
290
|
+
def identify_subject
|
291
|
+
if @subject.respond_to?(:id)
|
292
|
+
"#{@subject.class.name}/#{@subject.id}"
|
293
|
+
else
|
294
|
+
@subject.inspect
|
295
|
+
end
|
267
296
|
end
|
268
297
|
|
269
298
|
def inspect
|
@@ -276,19 +305,22 @@ module DeclarativePolicy
|
|
276
305
|
# at the ability level.
|
277
306
|
def runner(ability)
|
278
307
|
ability = ability.to_sym
|
279
|
-
|
280
|
-
@runners[ability] ||=
|
308
|
+
runners[ability] ||=
|
281
309
|
begin
|
282
310
|
own_runner = Runner.new(own_steps(ability))
|
283
311
|
if self.class.overrides.include?(ability)
|
284
312
|
own_runner
|
285
313
|
else
|
286
314
|
delegated_runners = delegated_policies.values.compact.map { |p| p.runner(ability) }
|
287
|
-
delegated_runners.
|
315
|
+
delegated_runners.reduce(own_runner, &:merge_runner)
|
288
316
|
end
|
289
317
|
end
|
290
318
|
end
|
291
319
|
|
320
|
+
def runners
|
321
|
+
@runners ||= {}
|
322
|
+
end
|
323
|
+
|
292
324
|
# Helpers for caching. Used by ManifestCondition in performing condition
|
293
325
|
# computation.
|
294
326
|
#
|
@@ -40,6 +40,8 @@ module DeclarativePolicy
|
|
40
40
|
# the context's cache here so that we can share in the global
|
41
41
|
# cache (often RequestStore or similar).
|
42
42
|
def pass?
|
43
|
+
Thread.current[:declarative_policy_current_runner_state]&.register(self)
|
44
|
+
|
43
45
|
@context.cache(cache_key) { @condition.compute(@context) }
|
44
46
|
end
|
45
47
|
|
@@ -76,8 +78,6 @@ module DeclarativePolicy
|
|
76
78
|
8
|
77
79
|
end
|
78
80
|
|
79
|
-
private
|
80
|
-
|
81
81
|
# This method controls the caching for the condition. This is where
|
82
82
|
# the condition(scope: ...) option comes into play. Notice that
|
83
83
|
# depending on the scope, we may cache only by the user or only by
|
@@ -93,6 +93,8 @@ module DeclarativePolicy
|
|
93
93
|
end
|
94
94
|
end
|
95
95
|
|
96
|
+
private
|
97
|
+
|
96
98
|
def user_key
|
97
99
|
Cache.user_key(@context.user)
|
98
100
|
end
|
@@ -7,6 +7,7 @@ module DeclarativePolicy
|
|
7
7
|
def initialize
|
8
8
|
@named_policies = {}
|
9
9
|
@name_transformation = ->(name) { "#{name}Policy" }
|
10
|
+
@class_for = ->(name) { Object.const_get(name) }
|
10
11
|
end
|
11
12
|
|
12
13
|
def named_policy(name, policy = nil)
|
@@ -26,10 +27,15 @@ module DeclarativePolicy
|
|
26
27
|
nil
|
27
28
|
end
|
28
29
|
|
30
|
+
def class_for(&block)
|
31
|
+
@class_for = block
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
29
35
|
def policy_class(domain_class_name)
|
30
36
|
return unless domain_class_name
|
31
37
|
|
32
|
-
@name_transformation.call(domain_class_name)
|
38
|
+
@class_for.call((@name_transformation.call(domain_class_name)))
|
33
39
|
rescue NameError
|
34
40
|
nil
|
35
41
|
end
|
@@ -210,7 +210,7 @@ module DeclarativePolicy
|
|
210
210
|
cached = cached_pass?(context)
|
211
211
|
return cached unless cached.nil?
|
212
212
|
|
213
|
-
@rules.all? { |r| r.pass?(context) }
|
213
|
+
@rules.sort_by { |r| r.score(context) }.all? { |r| r.pass?(context) }
|
214
214
|
end
|
215
215
|
|
216
216
|
def cached_pass?(context)
|
@@ -240,7 +240,7 @@ module DeclarativePolicy
|
|
240
240
|
cached = cached_pass?(context)
|
241
241
|
return cached unless cached.nil?
|
242
242
|
|
243
|
-
@rules.any? { |r| r.pass?(context) }
|
243
|
+
@rules.sort_by { |r| r.score(context) }.any? { |r| r.pass?(context) }
|
244
244
|
end
|
245
245
|
|
246
246
|
def simplify
|
@@ -285,9 +285,9 @@ module DeclarativePolicy
|
|
285
285
|
|
286
286
|
def simplify
|
287
287
|
case @rule
|
288
|
-
when And then Or.new(@rule.rules.map(&:negate)).simplify
|
289
|
-
when Or then And.new(@rule.rules.map(&:negate)).simplify
|
290
|
-
when Not then @rule.rule.simplify
|
288
|
+
when And then Or.new(@rule.rules.map(&:negate)).simplify # DeMorgan's laws
|
289
|
+
when Or then And.new(@rule.rules.map(&:negate)).simplify # DeMorgan's laws
|
290
|
+
when Not then @rule.rule.simplify # double negation
|
291
291
|
else Not.new(@rule.simplify)
|
292
292
|
end
|
293
293
|
end
|
@@ -1,11 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'set'
|
4
|
+
|
3
5
|
module DeclarativePolicy
|
4
6
|
class Runner
|
5
7
|
class State
|
8
|
+
attr_reader :called_conditions
|
9
|
+
|
6
10
|
def initialize
|
7
11
|
@enabled = false
|
8
12
|
@prevented = false
|
13
|
+
@called_conditions = Set.new
|
9
14
|
end
|
10
15
|
|
11
16
|
def enable!
|
@@ -27,6 +32,10 @@ module DeclarativePolicy
|
|
27
32
|
def pass?
|
28
33
|
!prevented? && enabled?
|
29
34
|
end
|
35
|
+
|
36
|
+
def register(manifest_condition)
|
37
|
+
@called_conditions << manifest_condition.cache_key
|
38
|
+
end
|
30
39
|
end
|
31
40
|
|
32
41
|
# a Runner contains a list of Steps to be run.
|
@@ -44,6 +53,11 @@ module DeclarativePolicy
|
|
44
53
|
!!@state
|
45
54
|
end
|
46
55
|
|
56
|
+
# Delete the cached state - allowing this runner to be re-used if the facts have changed.
|
57
|
+
def uncache!
|
58
|
+
@state = nil
|
59
|
+
end
|
60
|
+
|
47
61
|
# used by Rule::Ability. See #steps_by_score
|
48
62
|
def score
|
49
63
|
return 0 if cached?
|
@@ -55,11 +69,20 @@ module DeclarativePolicy
|
|
55
69
|
Runner.new(@steps + other.steps)
|
56
70
|
end
|
57
71
|
|
72
|
+
def dependencies
|
73
|
+
return Set.new unless @state
|
74
|
+
|
75
|
+
@state.called_conditions
|
76
|
+
end
|
77
|
+
|
58
78
|
# The main entry point, called for making an ability decision.
|
59
79
|
# See #run and DeclarativePolicy::Base#can?
|
60
80
|
def pass?
|
61
81
|
run unless cached?
|
62
82
|
|
83
|
+
parent_state = Thread.current[:declarative_policy_current_runner_state]
|
84
|
+
parent_state&.called_conditions&.merge(@state.called_conditions)
|
85
|
+
|
63
86
|
@state.pass?
|
64
87
|
end
|
65
88
|
|
@@ -70,6 +93,16 @@ module DeclarativePolicy
|
|
70
93
|
|
71
94
|
private
|
72
95
|
|
96
|
+
def with_state(&block)
|
97
|
+
@state = State.new
|
98
|
+
old_runner_state = Thread.current[:declarative_policy_current_runner_state]
|
99
|
+
Thread.current[:declarative_policy_current_runner_state] = @state
|
100
|
+
|
101
|
+
yield
|
102
|
+
ensure
|
103
|
+
Thread.current[:declarative_policy_current_runner_state] = old_runner_state
|
104
|
+
end
|
105
|
+
|
73
106
|
def flatten_steps!
|
74
107
|
@steps = @steps.flat_map { |s| s.flattened(@steps) }
|
75
108
|
end
|
@@ -78,33 +111,31 @@ module DeclarativePolicy
|
|
78
111
|
# It relies on #steps_by_score for the main loop, and updates @state
|
79
112
|
# with the result of the step.
|
80
113
|
def run(debug = nil)
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
@state.prevent! if passed
|
114
|
+
with_state do
|
115
|
+
steps_by_score(!!debug) do |step, score|
|
116
|
+
break if !debug && @state.prevented?
|
117
|
+
|
118
|
+
passed = nil
|
119
|
+
case step.action
|
120
|
+
when :enable
|
121
|
+
# we only check :enable actions if they have a chance of
|
122
|
+
# changing the outcome - if no other rule has enabled or
|
123
|
+
# prevented.
|
124
|
+
unless @state.enabled? || @state.prevented?
|
125
|
+
passed = step.pass?
|
126
|
+
@state.enable! if passed
|
127
|
+
end
|
128
|
+
when :prevent
|
129
|
+
# we only check :prevent actions if the state hasn't already
|
130
|
+
# been prevented.
|
131
|
+
unless @state.prevented?
|
132
|
+
passed = step.pass?
|
133
|
+
@state.prevent! if passed
|
134
|
+
end
|
135
|
+
else raise "invalid action #{step.action.inspect}"
|
104
136
|
end
|
105
137
|
|
106
138
|
debug << inspect_step(step, score, passed) if debug
|
107
|
-
else raise "invalid action #{step.action.inspect}"
|
108
139
|
end
|
109
140
|
end
|
110
141
|
|
@@ -131,7 +162,7 @@ module DeclarativePolicy
|
|
131
162
|
#
|
132
163
|
# For each step, we yield the step object along with the computed score
|
133
164
|
# for debugging purposes.
|
134
|
-
def steps_by_score
|
165
|
+
def steps_by_score(debugging)
|
135
166
|
flatten_steps!
|
136
167
|
|
137
168
|
if @steps.size > 50
|
@@ -156,6 +187,7 @@ module DeclarativePolicy
|
|
156
187
|
# if the permission hasn't yet been enabled and we only have
|
157
188
|
# prevent steps left, we short-circuit the state here
|
158
189
|
@state.prevent!
|
190
|
+
return unless debugging
|
159
191
|
end
|
160
192
|
|
161
193
|
return if remaining_steps.empty?
|
@@ -185,7 +217,7 @@ module DeclarativePolicy
|
|
185
217
|
break if lowest_score.zero?
|
186
218
|
end
|
187
219
|
|
188
|
-
[next_step,
|
220
|
+
[next_step, lowest_score]
|
189
221
|
end
|
190
222
|
|
191
223
|
# Formatter for debugging output.
|
data/lib/declarative_policy.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
require 'active_support/core_ext'
|
5
|
-
|
3
|
+
require 'set'
|
6
4
|
require_relative 'declarative_policy/cache'
|
7
5
|
require_relative 'declarative_policy/condition'
|
8
6
|
require_relative 'declarative_policy/delegate_dsl'
|
@@ -20,21 +18,32 @@ require_relative 'declarative_policy/configuration'
|
|
20
18
|
module DeclarativePolicy
|
21
19
|
extend PreferredScope
|
22
20
|
|
23
|
-
CLASS_CACHE_MUTEX = Mutex.new
|
24
|
-
CLASS_CACHE_IVAR = :@__DeclarativePolicy_CLASS_CACHE
|
25
|
-
|
26
21
|
class << self
|
27
22
|
def policy_for(user, subject, opts = {})
|
28
23
|
cache = opts[:cache] || {}
|
29
24
|
key = Cache.policy_key(user, subject)
|
30
25
|
|
31
|
-
cache[key] ||=
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
26
|
+
cache[key] ||= class_for(subject).new(user, subject, opts)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Find the list of runners with now invalidated keys, and invalidate the runners
|
30
|
+
def invalidate(cache, invalidated_keys)
|
31
|
+
return unless cache&.any?
|
32
|
+
return unless invalidated_keys&.any?
|
33
|
+
|
34
|
+
keys = invalidated_keys.to_set
|
35
|
+
|
36
|
+
policies = cache.select { |k, _| k.is_a?(String) && k.start_with?('/dp/policy/') }
|
37
|
+
|
38
|
+
policies.each_value do |policy|
|
39
|
+
policy.runners.each do |runner|
|
40
|
+
runner.uncache! if keys.intersect?(runner.dependencies)
|
37
41
|
end
|
42
|
+
end
|
43
|
+
|
44
|
+
invalidated_keys.each { |k| cache.delete(k) }
|
45
|
+
|
46
|
+
nil
|
38
47
|
end
|
39
48
|
|
40
49
|
def class_for(subject)
|
@@ -72,38 +81,19 @@ module DeclarativePolicy
|
|
72
81
|
@configuration ||= DeclarativePolicy::Configuration.new
|
73
82
|
end
|
74
83
|
|
75
|
-
# This method is heavily cached because there are a lot of anonymous
|
76
|
-
# modules in play in a typical rails app, and #name performs quite
|
77
|
-
# slowly for anonymous classes and modules.
|
78
|
-
#
|
79
|
-
# See https://bugs.ruby-lang.org/issues/11119
|
80
|
-
#
|
81
|
-
# if the above bug is resolved, this caching could likely be removed.
|
82
84
|
def class_for_class(subject_class)
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
85
|
+
if subject_class.respond_to?(:declarative_policy_class)
|
86
|
+
Object.const_get(subject_class.declarative_policy_class)
|
87
|
+
else
|
88
|
+
subject_class.ancestors.each do |klass|
|
89
|
+
name = klass.name
|
90
|
+
klass = policy_class(name)
|
91
|
+
|
92
|
+
return klass if klass
|
90
93
|
end
|
91
|
-
end
|
92
|
-
|
93
|
-
subject_class.instance_variable_get(CLASS_CACHE_IVAR)
|
94
|
-
end
|
95
94
|
|
96
|
-
|
97
|
-
return subject_class.declarative_policy_class.constantize if subject_class.respond_to?(:declarative_policy_class)
|
98
|
-
|
99
|
-
subject_class.ancestors.each do |klass|
|
100
|
-
name = klass.name
|
101
|
-
klass = policy_class(name)
|
102
|
-
|
103
|
-
return klass if klass
|
95
|
+
nil
|
104
96
|
end
|
105
|
-
|
106
|
-
nil
|
107
97
|
end
|
108
98
|
|
109
99
|
def policy_class(name)
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: declarative_policy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeanine Adkisson
|
8
8
|
- Alexis Kalderimis
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-
|
12
|
+
date: 2021-11-05 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: |
|
15
15
|
This library provides an authorization framework with a declarative DSL
|
@@ -28,19 +28,23 @@ files:
|
|
28
28
|
- ".gitlab-ci.yml"
|
29
29
|
- ".rspec"
|
30
30
|
- ".rubocop.yml"
|
31
|
+
- CHANGELOG.md
|
31
32
|
- CODE_OF_CONDUCT.md
|
33
|
+
- CONTRIBUTING.md
|
32
34
|
- Dangerfile
|
33
35
|
- Gemfile
|
34
36
|
- Gemfile.lock
|
35
37
|
- LICENSE.txt
|
36
38
|
- README.md
|
37
39
|
- Rakefile
|
40
|
+
- benchmarks/repeated_invocation.rb
|
38
41
|
- danger/plugins/project_helper.rb
|
39
42
|
- danger/roulette/Dangerfile
|
40
43
|
- declarative_policy.gemspec
|
41
44
|
- doc/caching.md
|
42
45
|
- doc/configuration.md
|
43
46
|
- doc/defining-policies.md
|
47
|
+
- doc/optimization.md
|
44
48
|
- lib/declarative_policy.rb
|
45
49
|
- lib/declarative_policy/base.rb
|
46
50
|
- lib/declarative_policy/cache.rb
|
@@ -62,7 +66,7 @@ metadata:
|
|
62
66
|
homepage_uri: https://gitlab.com/gitlab-org/declarative-policy
|
63
67
|
source_code_uri: https://gitlab.com/gitlab-org/declarative-policy
|
64
68
|
changelog_uri: https://gitlab.com/gitlab-org/declarative-policy/-/blobs/master/CHANGELOG.md
|
65
|
-
post_install_message:
|
69
|
+
post_install_message:
|
66
70
|
rdoc_options: []
|
67
71
|
require_paths:
|
68
72
|
- lib
|
@@ -70,15 +74,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
70
74
|
requirements:
|
71
75
|
- - ">="
|
72
76
|
- !ruby/object:Gem::Version
|
73
|
-
version: 2.
|
77
|
+
version: 2.5.0
|
74
78
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
79
|
requirements:
|
76
80
|
- - ">="
|
77
81
|
- !ruby/object:Gem::Version
|
78
82
|
version: '0'
|
79
83
|
requirements: []
|
80
|
-
rubygems_version: 3.
|
81
|
-
signing_key:
|
84
|
+
rubygems_version: 3.2.15
|
85
|
+
signing_key:
|
82
86
|
specification_version: 4
|
83
87
|
summary: An authorization library with a focus on declarative policy definitions.
|
84
88
|
test_files: []
|