policies.md 11.5 KB
Newer Older
Amy Qualls's avatar
Amy Qualls committed
1
---
2
stage: Manage
3
group: Authentication and Authorization
4
info: 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
Amy Qualls's avatar
Amy Qualls committed
5 6
---

7 8 9 10
# `DeclarativePolicy` framework

The 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.

11
The 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 14 15 16 17 18

## Managing Permission Rules

Permissions 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.

### Conditions

19
Conditions 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
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
class FooPolicy < BasePolicy
  condition(:is_public) do
    # @subject guaranteed to be an instance of Foo
    @subject.public?
  end

  # instance methods can be called from the condition as well
  condition(:thing) { check_thing }

  def check_thing
    # ...
  end
end
```

37
When 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

39
Conditions are cached according to their scope. Scope and ordering is covered later.
40 41 42 43 44

### Rules

A `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`:

45
```ruby
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
class FooPolicy < BasePolicy
  # ...

  rule { is_public }.enable :read
  rule { thing }.prevent :read

  # equivalently,
  rule { is_public }.policy do
    enable :read
  end

  rule { ~thing }.policy do
    prevent :read
  end
end
```

Within the rule DSL, you can use:

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 68
- `&` and `|` are logical combinations, also available as `all?(...)` and `any?(...)`.
- `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 73 74 75 76 77 78 79

Do not use boolean operators such as `&&` and `||` within the rule DSL,
as conditions within rule blocks are objects, not booleans. The same
applies for ternary operators (`condition ? ... : ...`), and `if`
blocks. These operators cannot be overridden, and are hence banned via a
[custom
cop](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49771).

80 81
## Scores, Order, Performance

Sam Figueroa's avatar
Sam Figueroa committed
82
To 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

84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
For example, let's say you wanted to debug `IssuePolicy`. You might run
the debugger in this way:

```ruby
user = User.find_by(username: 'john')
issue = Issue.first
policy = IssuePolicy.new(user, issue)
policy.debug(:read_issue)
```

An example debug output would look as follows:

```ruby
- [0] prevent when all?(confidential, ~can_read_confidential) ((@john : Issue/1))
- [0] prevent when archived ((@john : Project/4))
- [0] prevent when issues_disabled ((@john : Project/4))
- [0] prevent when all?(anonymous, ~public_project) ((@john : Project/4))
+ [32] enable when can?(:reporter_access) ((@john : Project/4))
```

Each line represents a rule that was evaluated. There are a few things to note:

1. The `-` or `+` symbol indicates whether the rule block was evaluated to be
107
   `false` or `true`, respectively.
108
1. The number inside the brackets indicates the score.
109
1. The last part of the line (for example, `@john : Issue/1`) shows the username
110
   and subject for that rule.
111 112 113

Here you can see that the first four rules were evaluated `false` for
which user and subject. For example, you can see in the last line that
114
the rule was activated because the user `john` had the Reporter [role](../user/permissions.md) on
115
`Project/4`.
116 117 118 119 120 121

When a policy is asked whether a particular ability is allowed
(`policy.allowed?(:some_ability)`), it does not necessarily have to
compute all the conditions on the policy. First, only the rules relevant
to that particular ability are selected. Then, the execution model takes
advantage of short-circuiting, and attempts to sort rules based on a
122 123 124
heuristic of how expensive they are to calculate. The sorting is
dynamic and cache-aware, so that previously calculated conditions are
considered first, before computing other conditions.
125 126 127 128

Note that the score is chosen by a developer via the `score:` parameter
in a `condition` to denote how expensive evaluating this rule would be
relative to other rules.
129 130 131

## Scope

132
Sometimes, 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
135 136 137 138 139 140 141
class FooPolicy < BasePolicy
  condition(:expensive_condition) { @subject.expensive_query? }

  rule { expensive_condition }.enable :some_ability
end
```

142
Naively, 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 146 147
  condition(:expensive_condition, scope: :subject) { @subject.expensive_query? }
```

148
then 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
151
both user and subject (including a simple anonymous check!) your result is cached at too global of a scope and results in cache bugs.
152

153
Sometimes 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
156 157 158 159 160 161 162
def users_that_can_read_project(users, project)
  DeclarativePolicy.subject_scope do
    users.select { |u| allowed?(u, :read_project, project) }
  end
end
```

163
This, for example, prefers checking `project.public?` to checking `user.admin?`.
164 165 166

## Delegation

167
Delegation is the inclusion of rules from another policy, on a different subject. For example:
168

169
```ruby
170 171 172 173 174
class FooPolicy < BasePolicy
  delegate { @subject.project }
end
```

175
includes 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

Alex Kalderimis's avatar
Alex Kalderimis committed
177 178 179 180 181 182 183 184 185 186 187 188 189
### Overrides

We allow policies to opt-out of delegated abilities.

Delegated policies may define some abilities in a way that is incorrect for the
delegating policy. Take for example a child/parent relationship, where some
abilities can be inferred, and some cannot:

```ruby
class ParentPolicy < BasePolicy
  condition(:speaks_spanish) { @subject.spoken_languages.include?(:es) }
  condition(:has_license) { @subject.driving_license.present? }
  condition(:enjoys_broccoli) { @subject.enjoyment_of(:broccoli) > 0 }
Marcel Amirault's avatar
Marcel Amirault committed
190

Alex Kalderimis's avatar
Alex Kalderimis committed
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
  rule { speaks_spanish }.enable :read_spanish
  rule { has_license }.enable :drive_car
  rule { enjoys_broccoli }.enable :eat_broccoli
  rule { ~enjoys_broccoli }.prevent :eat_broccoli
end
```

Here, if we delegated the child policy to the parent policy, some values would be
incorrect - we might correctly infer that the child can speak their parent's
language, but it would be incorrect to infer that the child can drive or would
eat broccoli just because the parent can and does.

Some of these things we can deal with - we can forbid driving universally in the
child policy, for example:

```ruby
class ChildPolicy < BasePolicy
  delegate { @subject.parent }
Marcel Amirault's avatar
Marcel Amirault committed
209

Alex Kalderimis's avatar
Alex Kalderimis committed
210 211 212 213 214 215
  rule { default }.prevent :drive_car
end
```

But the food preferences one is harder - because of the `prevent` call in the
parent policy, if the parent dislikes it, even calling `enable` in the child
216
does not enable `:eat_broccoli`.
Alex Kalderimis's avatar
Alex Kalderimis committed
217 218 219 220 221 222 223 224

We could remove the `prevent` call in the parent policy, but that still doesn't
help us, since the rules are different: parents get to eat what they like, and
children eat what they are given, provided they are well behaved. Allowing
delegation would end up with only children whose parents enjoy green vegetables
eating it. But a parent may well give their child broccoli, even if they dislike
it themselves, because it is good for their child.

Sam Figueroa's avatar
Sam Figueroa committed
225
The solution is to override the `:eat_broccoli` ability in the child policy:
Alex Kalderimis's avatar
Alex Kalderimis committed
226 227 228 229

```ruby
class ChildPolicy < BasePolicy
  delegate { @subject.parent }
Marcel Amirault's avatar
Marcel Amirault committed
230

Alex Kalderimis's avatar
Alex Kalderimis committed
231
  overrides :eat_broccoli
Marcel Amirault's avatar
Marcel Amirault committed
232

Alex Kalderimis's avatar
Alex Kalderimis committed
233
  condition(:good_kid) { @subject.behavior_level >= Child::GOOD }
Marcel Amirault's avatar
Marcel Amirault committed
234

Alex Kalderimis's avatar
Alex Kalderimis committed
235 236 237 238
  rule { good_kid }.enable :eat_broccoli
end
```

239
With this definition, the `ChildPolicy` _never_ looks in the `ParentPolicy` to
Alex Kalderimis's avatar
Alex Kalderimis committed
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
satisfy `:eat_broccoli`, but it _will_ use it for any other abilities. The child
policy can then define `:eat_broccoli` in a way that makes sense for `Child` and not
`Parent`.

### Alternatives to using `overrides`

Overriding policy delegation is complex, for the same reason delegation is
complex - it involves reasoning about logical inference, and being clear about
semantics. Misuse of `override` has the potential to duplicate code, and
potentially introduce security bugs, allowing things that should be prevented.
For this reason, it should be used only when other approaches are not feasible.

Other approaches can include for example using different ability names. Choosing
to eat a food and eating foods you are given are semantically distinct, and they
could be named differently (perhaps `chooses_to_eat_broccoli` and
`eats_what_is_given` in this case). It can depend on how polymorphic the call
256
site is. If you know that we always check the policy with a `Parent` or a
Alex Kalderimis's avatar
Alex Kalderimis committed
257 258 259
`Child`, then we can choose the appropriate ability name. If the call site is
polymorphic, then we cannot do that.

260 261 262 263 264 265 266 267 268 269 270 271 272
## Specifying Policy Class

You can also override the Policy used for a given subject:

```ruby
class Foo

  def self.declarative_policy_class
    'SomeOtherPolicy'
  end
end
```

273
This uses and checks permissions on the `SomeOtherPolicy` class rather than the usual calculated `FooPolicy` class.