1---
2stage: Manage
3group: Access
4info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
5---
6
7# `DeclarativePolicy` framework
8
9The DeclarativePolicy framework is designed to assist in performance of policy checks, and to enable ease of extension for EE. The DSL code in `app/policies` is what `Ability.allowed?` uses to check whether a particular action is allowed on a subject.
10
11The policy used is based on the subject's class name - so `Ability.allowed?(user, :some_ability, project)` creates a `ProjectPolicy` and check permissions on that.
12
13## Managing Permission Rules
14
15Permissions are broken into two parts: `conditions` and `rules`. Conditions are boolean expressions that can access the database and the environment, while rules are statically configured combinations of expressions and other rules that enable or prevent certain abilities. For an ability to be allowed, it must be enabled by at least one rule, and not prevented by any.
16
17### Conditions
18
19Conditions are defined by the `condition` method, and are given a name and a block. The block is executed in the context of the policy object - so it can access `@user` and `@subject`, as well as call any methods defined on the policy. Note that `@user` may be nil (in the anonymous case), but `@subject` is guaranteed to be a real instance of the subject class.
20
21```ruby
22class FooPolicy < BasePolicy
23  condition(:is_public) do
24    # @subject guaranteed to be an instance of Foo
25    @subject.public?
26  end
27
28  # instance methods can be called from the condition as well
29  condition(:thing) { check_thing }
30
31  def check_thing
32    # ...
33  end
34end
35```
36
37When you define a condition, a predicate method is defined on the policy to check whether that condition passes - so in the above example, an instance of `FooPolicy` also responds to `#is_public?` and `#thing?`.
38
39Conditions are cached according to their scope. Scope and ordering is covered later.
40
41### Rules
42
43A `rule` is a logical combination of conditions and other rules, that are configured to enable or prevent certain abilities. It is important to note that the rule configuration is static - a rule's logic cannot touch the database or know about `@user` or `@subject`. This allows us to cache only at the condition level. Rules are specified through the `rule` method, which takes a block of DSL configuration, and returns an object that responds to `#enable` or `#prevent`:
44
45```ruby
46class FooPolicy < BasePolicy
47  # ...
48
49  rule { is_public }.enable :read
50  rule { thing }.prevent :read
51
52  # equivalently,
53  rule { is_public }.policy do
54    enable :read
55  end
56
57  rule { ~thing }.policy do
58    prevent :read
59  end
60end
61```
62
63Within the rule DSL, you can use:
64
65- A regular word mentions a condition by name - a rule that is in effect when that condition is truthy.
66- `~` indicates negation, also available as `negate`.
67- `&` and `|` are logical combinations, also available as `all?(...)` and `any?(...)`.
68- `can?(:other_ability)` delegates to the rules that apply to `:other_ability`. Note that this is distinct from the instance method `can?`, which can check dynamically - this only configures a delegation to another ability.
69
70`~`, `&` and `|` operators are overridden methods in
71[`DeclarativePolicy::Rule::Base`](https://gitlab.com/gitlab-org/declarative-policy/-/blob/main/lib/declarative_policy/rule.rb).
72
73Do not use boolean operators such as `&&` and `||` within the rule DSL,
74as conditions within rule blocks are objects, not booleans. The same
75applies for ternary operators (`condition ? ... : ...`), and `if`
76blocks. These operators cannot be overridden, and are hence banned via a
77[custom
78cop](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49771).
79
80## Scores, Order, Performance
81
82To see how the rules get evaluated into a judgment, open a Rails console and run: `policy.debug(:some_ability)`. This prints the rules in the order they are evaluated.
83
84For example, let's say you wanted to debug `IssuePolicy`. You might run
85the debugger in this way:
86
87```ruby
88user = User.find_by(username: 'john')
89issue = Issue.first
90policy = IssuePolicy.new(user, issue)
91policy.debug(:read_issue)
92```
93
94An example debug output would look as follows:
95
96```ruby
97- [0] prevent when all?(confidential, ~can_read_confidential) ((@john : Issue/1))
98- [0] prevent when archived ((@john : Project/4))
99- [0] prevent when issues_disabled ((@john : Project/4))
100- [0] prevent when all?(anonymous, ~public_project) ((@john : Project/4))
101+ [32] enable when can?(:reporter_access) ((@john : Project/4))
102```
103
104Each line represents a rule that was evaluated. There are a few things to note:
105
1061. The `-` or `+` symbol indicates whether the rule block was evaluated to be
107   `false` or `true`, respectively.
1081. The number inside the brackets indicates the score.
1091. The last part of the line (for example, `@john : Issue/1`) shows the username
110   and subject for that rule.
111
112Here you can see that the first four rules were evaluated `false` for
113which user and subject. For example, you can see in the last line that
114the rule was activated because the user `john` had the Reporter [role](../user/permissions.md) on
115`Project/4`.
116
117When a policy is asked whether a particular ability is allowed
118(`policy.allowed?(:some_ability)`), it does not necessarily have to
119compute all the conditions on the policy. First, only the rules relevant
120to that particular ability are selected. Then, the execution model takes
121advantage of short-circuiting, and attempts to sort rules based on a
122heuristic of how expensive they are to calculate. The sorting is
123dynamic and cache-aware, so that previously calculated conditions are
124considered first, before computing other conditions.
125
126Note that the score is chosen by a developer via the `score:` parameter
127in a `condition` to denote how expensive evaluating this rule would be
128relative to other rules.
129
130## Scope
131
132Sometimes, a condition only uses data from `@user` or only from `@subject`. In this case, we want to change the scope of the caching, so that we don't recalculate conditions unnecessarily. For example, given:
133
134```ruby
135class FooPolicy < BasePolicy
136  condition(:expensive_condition) { @subject.expensive_query? }
137
138  rule { expensive_condition }.enable :some_ability
139end
140```
141
142Naively, if we call `Ability.allowed?(user1, :some_ability, foo)` and `Ability.allowed?(user2, :some_ability, foo)`, we would have to calculate the condition twice - since they are for different users. But if we use the `scope: :subject` option:
143
144```ruby
145  condition(:expensive_condition, scope: :subject) { @subject.expensive_query? }
146```
147
148then the result of the condition is cached globally only based on the subject - so it is not calculated repeatedly for different users. Similarly, `scope: :user` caches only based on the user.
149
150**DANGER**: If you use a `:scope` option when the condition actually uses data from
151both user and subject (including a simple anonymous check!) your result is cached at too global of a scope and results in cache bugs.
152
153Sometimes we are checking permissions for a lot of users for one subject, or a lot of subjects for one user. In this case, we want to set a *preferred scope* - that is, tell the system that we prefer rules that can be cached on the repeated parameter. For example, in `Ability.users_that_can_read_project`:
154
155```ruby
156def users_that_can_read_project(users, project)
157  DeclarativePolicy.subject_scope do
158    users.select { |u| allowed?(u, :read_project, project) }
159  end
160end
161```
162
163This, for example, prefers checking `project.public?` to checking `user.admin?`.
164
165## Delegation
166
167Delegation is the inclusion of rules from another policy, on a different subject. For example:
168
169```ruby
170class FooPolicy < BasePolicy
171  delegate { @subject.project }
172end
173```
174
175includes all rules from `ProjectPolicy`. The delegated conditions are evaluated with the correct delegated subject, and are sorted along with the regular rules in the policy. Note that only the relevant rules for a particular ability are actually considered.
176
177### Overrides
178
179We allow policies to opt-out of delegated abilities.
180
181Delegated policies may define some abilities in a way that is incorrect for the
182delegating policy. Take for example a child/parent relationship, where some
183abilities can be inferred, and some cannot:
184
185```ruby
186class ParentPolicy < BasePolicy
187  condition(:speaks_spanish) { @subject.spoken_languages.include?(:es) }
188  condition(:has_license) { @subject.driving_license.present? }
189  condition(:enjoys_broccoli) { @subject.enjoyment_of(:broccoli) > 0 }
190
191  rule { speaks_spanish }.enable :read_spanish
192  rule { has_license }.enable :drive_car
193  rule { enjoys_broccoli }.enable :eat_broccoli
194  rule { ~enjoys_broccoli }.prevent :eat_broccoli
195end
196```
197
198Here, if we delegated the child policy to the parent policy, some values would be
199incorrect - we might correctly infer that the child can speak their parent's
200language, but it would be incorrect to infer that the child can drive or would
201eat broccoli just because the parent can and does.
202
203Some of these things we can deal with - we can forbid driving universally in the
204child policy, for example:
205
206```ruby
207class ChildPolicy < BasePolicy
208  delegate { @subject.parent }
209
210  rule { default }.prevent :drive_car
211end
212```
213
214But the food preferences one is harder - because of the `prevent` call in the
215parent policy, if the parent dislikes it, even calling `enable` in the child
216does not enable `:eat_broccoli`.
217
218We could remove the `prevent` call in the parent policy, but that still doesn't
219help us, since the rules are different: parents get to eat what they like, and
220children eat what they are given, provided they are well behaved. Allowing
221delegation would end up with only children whose parents enjoy green vegetables
222eating it. But a parent may well give their child broccoli, even if they dislike
223it themselves, because it is good for their child.
224
225The solution is to override the `:eat_broccoli` ability in the child policy:
226
227```ruby
228class ChildPolicy < BasePolicy
229  delegate { @subject.parent }
230
231  overrides :eat_broccoli
232
233  condition(:good_kid) { @subject.behavior_level >= Child::GOOD }
234
235  rule { good_kid }.enable :eat_broccoli
236end
237```
238
239With this definition, the `ChildPolicy` _never_ looks in the `ParentPolicy` to
240satisfy `:eat_broccoli`, but it _will_ use it for any other abilities. The child
241policy can then define `:eat_broccoli` in a way that makes sense for `Child` and not
242`Parent`.
243
244### Alternatives to using `overrides`
245
246Overriding policy delegation is complex, for the same reason delegation is
247complex - it involves reasoning about logical inference, and being clear about
248semantics. Misuse of `override` has the potential to duplicate code, and
249potentially introduce security bugs, allowing things that should be prevented.
250For this reason, it should be used only when other approaches are not feasible.
251
252Other approaches can include for example using different ability names. Choosing
253to eat a food and eating foods you are given are semantically distinct, and they
254could be named differently (perhaps `chooses_to_eat_broccoli` and
255`eats_what_is_given` in this case). It can depend on how polymorphic the call
256site is. If you know that we always check the policy with a `Parent` or a
257`Child`, then we can choose the appropriate ability name. If the call site is
258polymorphic, then we cannot do that.
259
260## Specifying Policy Class
261
262You can also override the Policy used for a given subject:
263
264```ruby
265class Foo
266
267  def self.declarative_policy_class
268    'SomeOtherPolicy'
269  end
270end
271```
272
273This uses and checks permissions on the `SomeOtherPolicy` class rather than the usual calculated `FooPolicy` class.
274