File: defining-policies.md

package info (click to toggle)
ruby-declarative-policy 1.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 264 kB
  • sloc: ruby: 1,020; makefile: 4
file content (211 lines) | stat: -rw-r--r-- 7,378 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# Defining policies

A policy is a set of conditions and rules for domain objects. They are defined
using a DSL, and mapped to domain objects by class name.

## Class name determines policy choice

If there is a domain class `Foo`, then we can link it to a policy by defining a
class `FooPolicy`. This class can be placed anywhere, as long as it is loaded
before the call to `DeclarativePolicy.policy_for`.

Our recommendation for large applications, such as Rails apps, is to add a new
top-level application directory: `app/policies`, and place all policy
definitions in there. If you have an `Invoice` model at `app/models/invoice.rb`,
then you would create an `InvoicePolicy` at `app/policies/invoice_policy.rb`.

## Invocation

We evaluate policies by instantiating them with `DeclarativePolicy::policy_for`,
and then evaluating them with `DeclarativePolicy::Base#allowed?`.

You may wish to define a method to abstract policy evaluation. Something like:

```ruby
def allowed?(user, ability, object)
  opts = { cache: Cache.current_cache } # re-using a cache between checks eliminates duplication of work
  policy = DeclarativePolicy.policy_for(user, object, opts)
  policy.allowed?(ability)
end
```

We will assume the presence of such a method below.

## Defining rules in the DSL

The DSL has two primary parts: defining **conditions** and **rules**.

For example, imagine we have a data model containing vehicles and users, and we
want to know if a user can drive a vehicle. We need a `VehiclePolicy`:

```ruby
class VehiclePolicy < DeclarativePolicy::Base
  # conditions go here by convention
  
  # rules go here by convention
  
  # helper methods go last
end
```

### Conditions

Conditions are facts about the state of the system.

They have access to two elements of the proposition:

- `@user` - the representation of a user in your system: the *subject* of the proposition.
  `user` in `allowed?(user, ability, object)`. `@user` may be `nil`, which means
  that the current user is anonymous (for example this may reflect an
  unauthenticated request in your system).
- `@subject` - any domain object that has an associated policy: the *object* of
  the predicate of the proposition. `object` in `allowed?(user, ability, object)`.
  `@subject` is never `nil`. See [handling `nil` values](./configuration.md#handling-nil-values)
  for details of how to apply policies to `nil` values.
  

They are defined as `condition(name, **options, &block)`, where the block is
evaluated in the context of an instance of the policy.

For example:

```ruby
condition(:owns) { @subject.owner == @user }
condition(:has_access_to) { @subject.owner.trusts?(@user) }
condition(:old_enough_to_drive) { @user.age >= laws.minimum_age }
condition(:has_driving_license) { @user.driving_license&.valid? }
condition(:intoxicated, score: 5) { @user.blood_alcohol > laws.max_blood_alcohol }
condition(:has_access_to, score: 3) { @subject.owner.trusts?(@user) }
```

These can be defined in any order, but we consider it best practice to define
conditions at the top of the file.

Conditions may call methods of the policy class, which can be helpful for
memoizing some intermediate state:

```ruby
condition(:full_license) { license.full? }
condition(:learner_license) { license.learner? }
condition(:hgv_license) { license.heavy_goods? }

def license
  @license ||= Licenses.by_country(@user.country_of_residence).for_user(@user)
end
```

Conditions are evaluated at most once, and their values are automatically
memoized and cached (see [caching](./caching.md) for more detail).

If you want to perform I/O (such as database access) or expensive computations,
place this access in a condition.

### Rules

Rules are conclusions we can draw based on the facts:

```ruby
rule { owns }.enable :drive_vehicle
rule { has_access_to }.enable :drive_vehicle
rule { ~old_enough_to_drive }.prevent :drive_vehicle
rule { intoxicated | ~has_driving_license }.prevent :drive_vehicle
```

Rules are combined such that each ability must be enabled at least once, and not
prevented in order to be permitted. So `enable` calls are implicitly combined
with `ANY`, and `prevent` calls are implicitly combined with `ALL`.

A set of conclusions can be defined for a single condition:

```ruby
rule { old_enough_to_drive }.policy do
  enable :drive_vehicle
  enable :vote
end
```

Rule blocks do not have access to the internal state of the policy, and cannot
access the `@user` or `@subject`, or any methods on the policy instance. You
should not perform I/O in a rule. They exist solely to define the logical rules
of implication and combination between conditions.

The available operations inside a rule block are:

- Bare words to refer to conditions in the policy, or on any delegate.
  For example `owns`. This is equivalent to `cond(:owns)`, but as a matter of
  general style, bare words are preferred.
- `~` to negate any rule. For example `~owns`, or `~(intoxicated | banned)`.
- `&` or `all?` to combine rules such that all must succeed. For example:
  `old_enough_to_drive & has_driving_license` or `all?(old_enough_to_drive, has_driving_license)`.
- `|` or `any?` to combine rules such that one must succeed. For example:
  `intoxicated | banned` or `any?(intoxicated, banned)`.
- `can?` to refer to the result of evaluating an ability. For example,
  `can?(:sell_vehicle)`.
- `delegate(:delegate_name, :condition_name)` to refer to a specific
  condition on a named delegate. Use of this is rare, but can be used to
  handle overrides. For example if a vehicle policy defines a delegate as
  `delegate :registration`, then we could refer to that
  as `rule { delegate(:registration, :valid) }`.

Note: Be careful not to confuse `DeclarativePolicy::Base.condition` with
`DeclarativePolicy::RuleDSL#cond`.

- `condition` constructs a condition from a name and a block. For example:
  `condition(:adult) { @subject.age >= country.age_of_majority }`.
- `cond` constructs a rule which refers to a condition by name. For example:
  `rule { cond(:adult) }.enable :vote`. Use of `cond` is rare - it is nicer to
  use the bare word form: `rule { adult }.enable :vote`.

### Complex conditions

Conditions may be combined in the rule blocks:

```ruby
# A or B
rule { owns | has_access_to }.enable :drive_vehicle
# A and B
rule { has_driving_license & old_enough_to_drive }.enable :drive_vehicle
# Not A
rule { ~has_driving_license }.prevent :drive_vehicle
```

And conditions can be implied from abilities:

```ruby
rule { can?(:drive_vehicle) }.enable :drive_taxi
```

### Delegation

Policies may delegate to other policies. For example we could have a
`DrivingLicense` class, and a `DrivingLicensePolicy`, which might contain rules
like:

```ruby
class DrivingLicensePolicy < DeclarativePolicy::Base
  condition(:expired) { @subject.expires_at <= Time.current }
  
  rule { expired }.prevent :drive_vehicle
end
```

And a registration policy:

```ruby
class RegistrationPolicy < DeclarativePolicy::Base
  condition(:valid) { @subject.valid_for?(@user.current_location) }
  
  rule { ~valid }.prevent :drive_vehicle
end
```

Then in our `VehiclePolicy` we can delegate the license and registration
checking to these two policies:

```ruby
delegate { @user.driving_license }
delegate { @subject.registration }
```

This is a powerful mechanism for inferring rules based on relationships between
objects.