declarative_policy 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|