declarative_policy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeclarativePolicy
4
+ VERSION = '1.0.0'
5
+ 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: []