declarative_policy 1.0.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 +7 -0
- data/.gitignore +10 -0
- data/.gitlab-ci.yml +48 -0
- data/.rspec +4 -0
- data/.rubocop.yml +10 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Dangerfile +16 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +197 -0
- data/LICENSE.txt +21 -0
- data/README.md +144 -0
- data/Rakefile +8 -0
- data/danger/plugins/project_helper.rb +58 -0
- data/danger/roulette/Dangerfile +97 -0
- data/declarative_policy.gemspec +36 -0
- data/doc/caching.md +4 -0
- data/doc/configuration.md +78 -0
- data/doc/defining-policies.md +185 -0
- data/lib/declarative_policy.rb +128 -0
- data/lib/declarative_policy/base.rb +351 -0
- data/lib/declarative_policy/cache.rb +39 -0
- data/lib/declarative_policy/condition.rb +104 -0
- data/lib/declarative_policy/configuration.rb +37 -0
- data/lib/declarative_policy/delegate_dsl.rb +22 -0
- data/lib/declarative_policy/nil_policy.rb +8 -0
- data/lib/declarative_policy/policy_dsl.rb +46 -0
- data/lib/declarative_policy/preferred_scope.rb +31 -0
- data/lib/declarative_policy/rule.rb +316 -0
- data/lib/declarative_policy/rule_dsl.rb +51 -0
- data/lib/declarative_policy/runner.rb +203 -0
- data/lib/declarative_policy/step.rb +89 -0
- data/lib/declarative_policy/version.rb +5 -0
- metadata +84 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclarativePolicy
|
4
|
+
# The DSL evaluation context inside rule { ... } blocks.
|
5
|
+
# Responsible for creating and combining Rule objects.
|
6
|
+
#
|
7
|
+
# See Base.rule
|
8
|
+
class RuleDsl
|
9
|
+
def initialize(context_class)
|
10
|
+
@context_class = context_class
|
11
|
+
end
|
12
|
+
|
13
|
+
def can?(ability)
|
14
|
+
Rule::Ability.new(ability)
|
15
|
+
end
|
16
|
+
|
17
|
+
def all?(*rules)
|
18
|
+
Rule::And.make(rules)
|
19
|
+
end
|
20
|
+
|
21
|
+
def any?(*rules)
|
22
|
+
Rule::Or.make(rules)
|
23
|
+
end
|
24
|
+
|
25
|
+
def none?(*rules)
|
26
|
+
~Rule::Or.new(rules)
|
27
|
+
end
|
28
|
+
|
29
|
+
def cond(condition)
|
30
|
+
Rule::Condition.new(condition)
|
31
|
+
end
|
32
|
+
|
33
|
+
def delegate(delegate_name, condition)
|
34
|
+
Rule::DelegatedCondition.new(delegate_name, condition)
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(msg, *args)
|
38
|
+
return super unless args.empty? && !block_given?
|
39
|
+
|
40
|
+
if @context_class.delegations.key?(msg)
|
41
|
+
DelegateDsl.new(self, msg)
|
42
|
+
else
|
43
|
+
cond(msg.to_sym)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def respond_to_missing?(symbol, include_all)
|
48
|
+
true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclarativePolicy
|
4
|
+
class Runner
|
5
|
+
class State
|
6
|
+
def initialize
|
7
|
+
@enabled = false
|
8
|
+
@prevented = false
|
9
|
+
end
|
10
|
+
|
11
|
+
def enable!
|
12
|
+
@enabled = true
|
13
|
+
end
|
14
|
+
|
15
|
+
def enabled?
|
16
|
+
@enabled
|
17
|
+
end
|
18
|
+
|
19
|
+
def prevent!
|
20
|
+
@prevented = true
|
21
|
+
end
|
22
|
+
|
23
|
+
def prevented?
|
24
|
+
@prevented
|
25
|
+
end
|
26
|
+
|
27
|
+
def pass?
|
28
|
+
!prevented? && enabled?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# a Runner contains a list of Steps to be run.
|
33
|
+
attr_reader :steps
|
34
|
+
|
35
|
+
def initialize(steps)
|
36
|
+
@steps = steps
|
37
|
+
@state = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
# We make sure only to run any given Runner once,
|
41
|
+
# and just continue to use the resulting @state
|
42
|
+
# that's left behind.
|
43
|
+
def cached?
|
44
|
+
!!@state
|
45
|
+
end
|
46
|
+
|
47
|
+
# used by Rule::Ability. See #steps_by_score
|
48
|
+
def score
|
49
|
+
return 0 if cached?
|
50
|
+
|
51
|
+
steps.sum(&:score)
|
52
|
+
end
|
53
|
+
|
54
|
+
def merge_runner(other)
|
55
|
+
Runner.new(@steps + other.steps)
|
56
|
+
end
|
57
|
+
|
58
|
+
# The main entry point, called for making an ability decision.
|
59
|
+
# See #run and DeclarativePolicy::Base#can?
|
60
|
+
def pass?
|
61
|
+
run unless cached?
|
62
|
+
|
63
|
+
@state.pass?
|
64
|
+
end
|
65
|
+
|
66
|
+
# see DeclarativePolicy::Base#debug
|
67
|
+
def debug(out = $stderr)
|
68
|
+
run(out)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def flatten_steps!
|
74
|
+
@steps = @steps.flat_map { |s| s.flattened(@steps) }
|
75
|
+
end
|
76
|
+
|
77
|
+
# This method implements the semantic of "one enable and no prevents".
|
78
|
+
# It relies on #steps_by_score for the main loop, and updates @state
|
79
|
+
# with the result of the step.
|
80
|
+
def run(debug = nil)
|
81
|
+
@state = State.new
|
82
|
+
|
83
|
+
steps_by_score do |step, score|
|
84
|
+
break if !debug && @state.prevented?
|
85
|
+
|
86
|
+
passed = nil
|
87
|
+
case step.action
|
88
|
+
when :enable
|
89
|
+
# we only check :enable actions if they have a chance of
|
90
|
+
# changing the outcome - if no other rule has enabled or
|
91
|
+
# prevented.
|
92
|
+
unless @state.enabled? || @state.prevented?
|
93
|
+
passed = step.pass?
|
94
|
+
@state.enable! if passed
|
95
|
+
end
|
96
|
+
|
97
|
+
debug << inspect_step(step, score, passed) if debug
|
98
|
+
when :prevent
|
99
|
+
# we only check :prevent actions if the state hasn't already
|
100
|
+
# been prevented.
|
101
|
+
unless @state.prevented?
|
102
|
+
passed = step.pass?
|
103
|
+
@state.prevent! if passed
|
104
|
+
end
|
105
|
+
|
106
|
+
debug << inspect_step(step, score, passed) if debug
|
107
|
+
else raise "invalid action #{step.action.inspect}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
@state
|
112
|
+
end
|
113
|
+
|
114
|
+
# This is the core spot where all those `#score` methods matter.
|
115
|
+
# It is critical for performance to run steps in the correct order,
|
116
|
+
# so that we don't compute expensive conditions (potentially n times
|
117
|
+
# if we're called on, say, a large list of users).
|
118
|
+
#
|
119
|
+
# In order to determine the cheapest step to run next, we rely on
|
120
|
+
# Step#score, which returns a numerical rating of how expensive
|
121
|
+
# it would be to calculate - the lower the better. It would be
|
122
|
+
# easy enough to statically sort by these scores, but we can do
|
123
|
+
# a little better - the scores are cache-aware (conditions that
|
124
|
+
# are already in the cache have score 0), which means that running
|
125
|
+
# a step can actually change the scores of other steps.
|
126
|
+
#
|
127
|
+
# So! The way we sort here involves re-scoring at every step. This
|
128
|
+
# is by necessity quadratic, but most of the time the number of steps
|
129
|
+
# will be low. But just in case, if the number of steps exceeds 50,
|
130
|
+
# we print a warning and fall back to a static sort.
|
131
|
+
#
|
132
|
+
# For each step, we yield the step object along with the computed score
|
133
|
+
# for debugging purposes.
|
134
|
+
def steps_by_score
|
135
|
+
flatten_steps!
|
136
|
+
|
137
|
+
if @steps.size > 50
|
138
|
+
warn "DeclarativePolicy: large number of steps (#{steps.size}), falling back to static sort"
|
139
|
+
|
140
|
+
@steps.map { |s| [s.score, s] }.sort_by { |(score, _)| score }.each do |(score, step)|
|
141
|
+
yield step, score
|
142
|
+
end
|
143
|
+
|
144
|
+
return
|
145
|
+
end
|
146
|
+
|
147
|
+
remaining_steps = Set.new(@steps)
|
148
|
+
remaining_enablers, remaining_preventers = remaining_steps.partition(&:enable?).map { |s| Set.new(s) }
|
149
|
+
|
150
|
+
loop do
|
151
|
+
if @state.enabled?
|
152
|
+
# Once we set this, we never need to unset it, because a single
|
153
|
+
# prevent will stop this from being enabled
|
154
|
+
remaining_steps = remaining_preventers
|
155
|
+
elsif remaining_enablers.empty?
|
156
|
+
# if the permission hasn't yet been enabled and we only have
|
157
|
+
# prevent steps left, we short-circuit the state here
|
158
|
+
@state.prevent!
|
159
|
+
end
|
160
|
+
|
161
|
+
return if remaining_steps.empty?
|
162
|
+
|
163
|
+
next_step, lowest_score = next_step_and_score(remaining_steps)
|
164
|
+
|
165
|
+
[remaining_steps, remaining_enablers, remaining_preventers].each do |set|
|
166
|
+
set.delete(next_step)
|
167
|
+
end
|
168
|
+
|
169
|
+
yield next_step, lowest_score
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def next_step_and_score(remaining_steps)
|
174
|
+
lowest_score = Float::INFINITY
|
175
|
+
next_step = nil
|
176
|
+
|
177
|
+
remaining_steps.each do |step|
|
178
|
+
score = step.score
|
179
|
+
|
180
|
+
if score < lowest_score
|
181
|
+
next_step = step
|
182
|
+
lowest_score = score
|
183
|
+
end
|
184
|
+
|
185
|
+
break if lowest_score.zero?
|
186
|
+
end
|
187
|
+
|
188
|
+
[next_step, score]
|
189
|
+
end
|
190
|
+
|
191
|
+
# Formatter for debugging output.
|
192
|
+
def inspect_step(step, original_score, passed)
|
193
|
+
symbol =
|
194
|
+
case passed
|
195
|
+
when true then '+'
|
196
|
+
when false then '-'
|
197
|
+
when nil then ' '
|
198
|
+
end
|
199
|
+
|
200
|
+
"#{symbol} [#{original_score.to_i}] #{step.repr}\n"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclarativePolicy
|
4
|
+
# This object represents one step in the runtime decision of whether
|
5
|
+
# an ability is allowed. It contains a Rule and a context (instance
|
6
|
+
# of DeclarativePolicy::Base), which contains the user, the subject,
|
7
|
+
# and the cache. It also contains an "action", which is the symbol
|
8
|
+
# :prevent or :enable.
|
9
|
+
class Step
|
10
|
+
attr_reader :context, :rule, :action
|
11
|
+
|
12
|
+
def initialize(context, rule, action)
|
13
|
+
@context = context
|
14
|
+
@rule = rule
|
15
|
+
@action = action
|
16
|
+
end
|
17
|
+
|
18
|
+
# In the flattening process, duplicate steps may be generated in the
|
19
|
+
# same rule. This allows us to eliminate those (see Runner#steps_by_score
|
20
|
+
# and note its use of a Set)
|
21
|
+
def ==(other)
|
22
|
+
@context == other.context && @rule == other.rule && @action == other.action
|
23
|
+
end
|
24
|
+
|
25
|
+
# In the runner, steps are sorted dynamically by score, so that
|
26
|
+
# we are sure to compute them in close to the optimal order.
|
27
|
+
#
|
28
|
+
# See also Rule#score, ManifestCondition#score, and Runner#steps_by_score.
|
29
|
+
def score
|
30
|
+
# we slightly prefer the preventative actions
|
31
|
+
# since they are more likely to short-circuit
|
32
|
+
case @action
|
33
|
+
when :prevent
|
34
|
+
@rule.score(@context) * (7.0 / 8)
|
35
|
+
when :enable
|
36
|
+
@rule.score(@context)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def with_action(action)
|
41
|
+
Step.new(@context, @rule, action)
|
42
|
+
end
|
43
|
+
|
44
|
+
def enable?
|
45
|
+
@action == :enable
|
46
|
+
end
|
47
|
+
|
48
|
+
def prevent?
|
49
|
+
@action == :prevent
|
50
|
+
end
|
51
|
+
|
52
|
+
# This rather complex method allows us to split rules into parts so that
|
53
|
+
# they can be sorted independently for better optimization
|
54
|
+
def flattened(roots)
|
55
|
+
case @rule
|
56
|
+
when Rule::Or
|
57
|
+
# A single `Or` step is the same as each of its elements as separate steps
|
58
|
+
@rule.rules.flat_map { |r| Step.new(@context, r, @action).flattened(roots) }
|
59
|
+
when Rule::Ability
|
60
|
+
# This looks like a weird micro-optimization but it buys us quite a lot
|
61
|
+
# in some cases. If we depend on an Ability (i.e. a `can?(...)` rule),
|
62
|
+
# and that ability *only* has :enable actions (modulo some actions that
|
63
|
+
# we already have taken care of), then its rules can be safely inlined.
|
64
|
+
steps = @context.runner(@rule.ability).steps.reject { |s| roots.include?(s) }
|
65
|
+
|
66
|
+
if steps.all?(&:enable?)
|
67
|
+
# in the case that we are a :prevent step, each inlined step becomes
|
68
|
+
# an independent :prevent, even though it was an :enable in its initial
|
69
|
+
# context.
|
70
|
+
steps.map! { |s| s.with_action(:prevent) } if prevent?
|
71
|
+
|
72
|
+
steps.flat_map { |s| s.flattened(roots) }
|
73
|
+
else
|
74
|
+
[self]
|
75
|
+
end
|
76
|
+
else
|
77
|
+
[self]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def pass?
|
82
|
+
@rule.pass?(@context)
|
83
|
+
end
|
84
|
+
|
85
|
+
def repr
|
86
|
+
"#{@action} when #{@rule.repr} (#{@context.repr})"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: declarative_policy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeanine Adkisson
|
8
|
+
- Alexis Kalderimis
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2021-04-12 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: |
|
15
|
+
This library provides an authorization framework with a declarative DSL
|
16
|
+
|
17
|
+
With this library, you can write permission policies that are separate
|
18
|
+
from business logic.
|
19
|
+
|
20
|
+
This library is in production use at GitLab.com
|
21
|
+
email:
|
22
|
+
- akalderimis@gitlab.com
|
23
|
+
executables: []
|
24
|
+
extensions: []
|
25
|
+
extra_rdoc_files: []
|
26
|
+
files:
|
27
|
+
- ".gitignore"
|
28
|
+
- ".gitlab-ci.yml"
|
29
|
+
- ".rspec"
|
30
|
+
- ".rubocop.yml"
|
31
|
+
- CODE_OF_CONDUCT.md
|
32
|
+
- Dangerfile
|
33
|
+
- Gemfile
|
34
|
+
- Gemfile.lock
|
35
|
+
- LICENSE.txt
|
36
|
+
- README.md
|
37
|
+
- Rakefile
|
38
|
+
- danger/plugins/project_helper.rb
|
39
|
+
- danger/roulette/Dangerfile
|
40
|
+
- declarative_policy.gemspec
|
41
|
+
- doc/caching.md
|
42
|
+
- doc/configuration.md
|
43
|
+
- doc/defining-policies.md
|
44
|
+
- lib/declarative_policy.rb
|
45
|
+
- lib/declarative_policy/base.rb
|
46
|
+
- lib/declarative_policy/cache.rb
|
47
|
+
- lib/declarative_policy/condition.rb
|
48
|
+
- lib/declarative_policy/configuration.rb
|
49
|
+
- lib/declarative_policy/delegate_dsl.rb
|
50
|
+
- lib/declarative_policy/nil_policy.rb
|
51
|
+
- lib/declarative_policy/policy_dsl.rb
|
52
|
+
- lib/declarative_policy/preferred_scope.rb
|
53
|
+
- lib/declarative_policy/rule.rb
|
54
|
+
- lib/declarative_policy/rule_dsl.rb
|
55
|
+
- lib/declarative_policy/runner.rb
|
56
|
+
- lib/declarative_policy/step.rb
|
57
|
+
- lib/declarative_policy/version.rb
|
58
|
+
homepage: https://gitlab.com/gitlab-org/declarative-policy
|
59
|
+
licenses:
|
60
|
+
- MIT
|
61
|
+
metadata:
|
62
|
+
homepage_uri: https://gitlab.com/gitlab-org/declarative-policy
|
63
|
+
source_code_uri: https://gitlab.com/gitlab-org/declarative-policy
|
64
|
+
changelog_uri: https://gitlab.com/gitlab-org/declarative-policy/-/blobs/master/CHANGELOG.md
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: 2.6.0
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubygems_version: 3.1.4
|
81
|
+
signing_key:
|
82
|
+
specification_version: 4
|
83
|
+
summary: An authorization library with a focus on declarative policy definitions.
|
84
|
+
test_files: []
|