@via-profit/ability 3.5.0 → 3.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +335 -350
- package/dist/index.d.ts +10 -8
- package/dist/index.js +31 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# @via-profit/
|
|
1
|
+
# @via-profit/ability
|
|
2
2
|
|
|
3
|
-
> A set of services
|
|
3
|
+
> A set of services partially implementing the [Attribute Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control) principle.
|
|
4
4
|
> The package allows you to describe rules, combine them into groups, form policies, and apply them to data to determine permissions.
|
|
5
5
|
|
|
6
6
|

|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|

|
|
12
12
|

|
|
13
13
|
|
|
14
|
-
|
|
15
14
|
## Language / Язык
|
|
16
15
|
|
|
17
16
|
- [🇬🇧 English](/docs/en/README.md)
|
|
@@ -19,27 +18,34 @@
|
|
|
19
18
|
|
|
20
19
|
## Purpose
|
|
21
20
|
|
|
22
|
-
The package is intended as a **lightweight and extremely simple alternative** to heavy access control systems.
|
|
23
|
-
|
|
21
|
+
The package is intended as a **lightweight and extremely simple alternative** to heavy access control systems.
|
|
22
|
+
No complex configurations, no dependencies – just a minimal set of tools that allows you to describe rules and policies in a very simple DSL.
|
|
23
|
+
|
|
24
|
+
Unlike classic ABAC models, where policies work on the principle *"matched → applied, didn't match → ignored"*, Ability uses a **simplified and more predictable state‑machine model**:
|
|
25
|
+
|
|
26
|
+
- **all** matching policies are executed in the order they are declared,
|
|
27
|
+
- each policy can **set** a state (`permit` or `deny`),
|
|
28
|
+
- each policy can **reset** the state if its conditions are not met,
|
|
29
|
+
- the final result is determined by the **last processed policy**, not just the one that matched.
|
|
24
30
|
|
|
25
|
-
##
|
|
31
|
+
## Contents
|
|
26
32
|
|
|
27
|
-
- [Quick
|
|
28
|
-
- [
|
|
33
|
+
- [Quick start](#quick-start)
|
|
34
|
+
- [Key concepts](#key-concepts)
|
|
29
35
|
- [DSL](#dsl)
|
|
30
|
-
- [Combining
|
|
36
|
+
- [Combining policies](#combining-policies)
|
|
31
37
|
- [Policy Environment](#policy-environment)
|
|
32
|
-
- [TypeScript
|
|
33
|
-
- [
|
|
38
|
+
- [TypeScript type generator](#typescript-type-generator)
|
|
39
|
+
- [Debugging policies](#debugging-policies)
|
|
34
40
|
- [Troubleshooting](#troubleshooting)
|
|
35
|
-
- [Design
|
|
41
|
+
- [Design recommendations](#design-recommendations)
|
|
36
42
|
- [Examples](#examples)
|
|
37
43
|
- [Performance](#performance)
|
|
38
|
-
- [
|
|
44
|
+
- [Api-Reference](./api.md)
|
|
39
45
|
|
|
40
|
-
## Quick
|
|
46
|
+
## Quick start
|
|
41
47
|
|
|
42
|
-
Install the package, write DSL, call the parser,
|
|
48
|
+
Install the package, write DSL, call the parser, run the resolver.
|
|
43
49
|
|
|
44
50
|
### Installation
|
|
45
51
|
|
|
@@ -55,7 +61,7 @@ yarn add @via-profit/ability
|
|
|
55
61
|
pnpm add @via-profit/ability
|
|
56
62
|
```
|
|
57
63
|
|
|
58
|
-
### Example:
|
|
64
|
+
### Example: deny access to `passwordHash` to everyone except the owner
|
|
59
65
|
|
|
60
66
|
Suppose we have user data:
|
|
61
67
|
|
|
@@ -69,7 +75,7 @@ const user = {
|
|
|
69
75
|
|
|
70
76
|
We need to deny reading `passwordHash` to everyone except the user themselves.
|
|
71
77
|
|
|
72
|
-
#### DSL
|
|
78
|
+
#### DSL policy
|
|
73
79
|
|
|
74
80
|
In the policy language, this looks like:
|
|
75
81
|
|
|
@@ -80,16 +86,16 @@ deny permission.user.passwordHash if any:
|
|
|
80
86
|
|
|
81
87
|
**Explanation:**
|
|
82
88
|
|
|
83
|
-
- `deny`
|
|
84
|
-
- `permission.user.passwordHash`
|
|
85
|
-
- `if any:`
|
|
86
|
-
- `viewer.id is not equals owner.id`
|
|
89
|
+
- `deny` – policy effect (deny access)
|
|
90
|
+
- `permission.user.passwordHash` – permission key.
|
|
91
|
+
- `if any:` – start of conditions block
|
|
92
|
+
- `viewer.id is not equals owner.id` – rule: if the requester's ID does not equal the owner's ID
|
|
87
93
|
|
|
88
|
-
If `viewer.id`
|
|
94
|
+
If `viewer.id` does not equal `owner.id`, the rule is considered satisfied, and the policy returns `deny` – access denied. If the IDs match (i.e., the user requests their own data), the rule does not trigger, and access is allowed.
|
|
89
95
|
|
|
90
|
-
|
|
96
|
+
_Note: The permission key is formed as `permission.` + your custom key in **dot notation**, e.g., the key `foo.bar.baz` in DSL would be `permission.foo.bar.baz`_
|
|
91
97
|
|
|
92
|
-
####
|
|
98
|
+
#### Code check
|
|
93
99
|
|
|
94
100
|
```ts
|
|
95
101
|
import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
|
|
@@ -99,60 +105,77 @@ deny permission.user.passwordHash if any:
|
|
|
99
105
|
viewer.id is not equals owner.id
|
|
100
106
|
`;
|
|
101
107
|
|
|
102
|
-
const policies = new AbilityDSLParser(dsl).parse(); //
|
|
108
|
+
const policies = new AbilityDSLParser(dsl).parse(); // get policies
|
|
103
109
|
const resolver = new AbilityResolver(policies); // create resolver
|
|
104
110
|
|
|
105
111
|
resolver.enforce('user.passwordHash', {
|
|
106
112
|
viewer: { id: '1' },
|
|
107
113
|
owner: { id: '2' },
|
|
108
|
-
}); // will throw an error
|
|
114
|
+
}); // will throw an error – access denied
|
|
109
115
|
```
|
|
110
|
-
In `enforce`, the key is passed without the `permission.` prefix
|
|
116
|
+
In `enforce`, the key is passed without the `permission.` prefix – it is automatically removed by the parser.
|
|
117
|
+
|
|
118
|
+
## Key concepts
|
|
119
|
+
|
|
120
|
+
Let's list the key points you need to know before starting to use the package:
|
|
121
|
+
|
|
122
|
+
1. **The resolver (`AbilityResolver`) works on the `Default Deny` principle.**
|
|
123
|
+
If no policy has set a final state, the result will be `deny`
|
|
124
|
+
([see here](#troubleshooting)).
|
|
125
|
+
To avoid unexpected `deny`, ensure there is at least one `permit` policy that can match.
|
|
126
|
+
Only then add `deny` policies.
|
|
127
|
+
|
|
128
|
+
2. **Policies are processed sequentially, from top to bottom.**
|
|
129
|
+
Unlike classic ABAC models, where a `mismatch` is simply ignored, Ability uses a **state‑machine model**:
|
|
130
|
+
|
|
131
|
+
- `match` sets the state (`permit` → allow, `deny` → deny),
|
|
132
|
+
- `mismatch` **resets the state to neutral**,
|
|
133
|
+
- the final result is determined by the **last processed policy**, not just the one that matched.
|
|
134
|
+
|
|
135
|
+
3. **Rules within a policy are also executed sequentially.**
|
|
111
136
|
|
|
112
|
-
|
|
137
|
+
4. **In a rule set with the `all` operator, execution stops at the first `mismatch`.**
|
|
138
|
+
In `any` – at the first `match`.
|
|
113
139
|
|
|
114
|
-
|
|
140
|
+
5. **Use [DSL](#dsl) to compose policies** – it's simpler and more convenient.
|
|
115
141
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
3. Rules are executed sequentially.
|
|
119
|
-
4. In a rule set (`RuleSet`) with the `all` comparison operator, further rule execution stops as soon as the first rule returns `mismatch`.
|
|
120
|
-
5. Use [DSL](#dsl) to compose policies — it's simpler and more convenient.
|
|
121
|
-
6. For storing policies on the server, use JSON. Policies can be exported to JSON and imported from JSON.
|
|
122
|
-
7. Generally, rely on the principle: if permission is not explicitly granted → access is denied.
|
|
123
|
-
8. Use the built-in cache only if your policies are incredibly complex and contain a large number of rules.
|
|
142
|
+
6. **Use JSON to store policies on the server.**
|
|
143
|
+
Policies can be exported to JSON and imported back.
|
|
124
144
|
|
|
125
|
-
|
|
145
|
+
7. **Follow the principle: if permission is not explicitly granted → access is denied.**
|
|
146
|
+
This is a natural consequence of the `Default Deny` model and state‑machine behavior.
|
|
126
147
|
|
|
127
|
-
|
|
148
|
+
### Interaction model
|
|
128
149
|
|
|
129
|
-
|
|
150
|
+
First, you describe "raw" policies (SDL, JSON, or using classes). Then, from the "raw" data, you form ready policies (an array of policies). This is done once and allows you to have a single source of truth. Then you can run permission checks in the necessary parts of your code using the already prepared policies and resolver.
|
|
151
|
+
|
|
152
|
+
Policies, groups, and rules can be created using:
|
|
130
153
|
|
|
131
154
|
- DSL (Domain-Specific Language)
|
|
132
155
|
- Classes (classic approach)
|
|
133
156
|
- JSON
|
|
134
157
|
|
|
135
|
-
**Creating policies
|
|
158
|
+
**Creating policies using DSL**
|
|
136
159
|
|
|
137
160
|
```ts
|
|
138
161
|
import { AbilityDSLParser } from '@via-profit/ability';
|
|
139
162
|
|
|
140
|
-
// Describe policies
|
|
163
|
+
// Describe policies in Ability-DSL language
|
|
141
164
|
const dsl = `
|
|
142
|
-
# @name
|
|
165
|
+
# @name Creating an order is available only to persons over 18 years old
|
|
143
166
|
permit permission.order.action.create if all:
|
|
144
167
|
all of:
|
|
145
168
|
user.age gte 18
|
|
146
169
|
|
|
147
|
-
# @name
|
|
170
|
+
# @name Editing the price is available only to the administrator
|
|
148
171
|
permit permission.order.data.price if all:
|
|
149
172
|
all of:
|
|
150
173
|
user.roles contains 'administrator'
|
|
151
174
|
`;
|
|
152
175
|
|
|
153
176
|
// Define resource types for TypeScript
|
|
154
|
-
// Types can be generated automatically (more on
|
|
155
|
-
// In this example, for simplicity, types are
|
|
177
|
+
// Types can be generated automatically (more on that later), or described manually
|
|
178
|
+
// In this example, for simplicity, types are described manually
|
|
156
179
|
type Resources = {
|
|
157
180
|
['order.action.create']: {
|
|
158
181
|
user: {
|
|
@@ -167,29 +190,29 @@ type Resources = {
|
|
|
167
190
|
}
|
|
168
191
|
|
|
169
192
|
// Use the parser to create policies
|
|
170
|
-
// Pass the resource type as a generic
|
|
193
|
+
// Pass the resource type as a generic
|
|
171
194
|
const policies = new AbilityDSLParser<Resources>(dsl).parse(); // AbilityPolicy[]
|
|
172
195
|
|
|
173
|
-
// The parser
|
|
196
|
+
// The parser will return an array of policies even
|
|
174
197
|
// if only one policy is described in the DSL
|
|
175
198
|
console.log(policies); // [AbilityPolicy, AbilityPolicy, ...]
|
|
176
199
|
|
|
177
|
-
//
|
|
200
|
+
// export ready policies
|
|
178
201
|
export default policies;
|
|
179
202
|
```
|
|
180
203
|
|
|
181
|
-
For more details
|
|
204
|
+
For more details on DSL, see the section (DSL)[#dsl]
|
|
182
205
|
|
|
183
206
|
**Creating policies using classes (classic approach)**
|
|
184
207
|
|
|
185
|
-
This approach is quite verbose but gives you full control over
|
|
208
|
+
This approach is quite verbose, but gives you full control over policies
|
|
186
209
|
|
|
187
210
|
```ts
|
|
188
211
|
import { AbilityPolicy, AbilityRuleSet, AbilityRule, AbilityCompare, AbilityPolicyEffect } from '@via-profit/ability';
|
|
189
212
|
|
|
190
213
|
// Define resource types for TypeScript
|
|
191
|
-
// Types can be generated automatically (more on
|
|
192
|
-
// In this example, for simplicity, types are
|
|
214
|
+
// Types can be generated automatically (more on that later), or described manually
|
|
215
|
+
// In this example, for simplicity, types are described manually
|
|
193
216
|
type Resources = {
|
|
194
217
|
['order.action.create']: {
|
|
195
218
|
user: {
|
|
@@ -207,7 +230,7 @@ const policies = [
|
|
|
207
230
|
// first policy
|
|
208
231
|
new AbilityPolicy<Resources>({
|
|
209
232
|
id: '1',
|
|
210
|
-
name: '
|
|
233
|
+
name: 'Creating an order is available only to persons over 18 years old',
|
|
211
234
|
compareMethod: AbilityCompare.and,
|
|
212
235
|
effect: AbilityPolicyEffect.permit,
|
|
213
236
|
permission: 'order.action.create',
|
|
@@ -221,7 +244,7 @@ const policies = [
|
|
|
221
244
|
// second policy
|
|
222
245
|
new AbilityPolicy<Resources>({
|
|
223
246
|
id: '2',
|
|
224
|
-
name: '
|
|
247
|
+
name: 'Editing the price is available only to the administrator',
|
|
225
248
|
compareMethod: AbilityCompare.and,
|
|
226
249
|
effect: AbilityPolicyEffect.permit,
|
|
227
250
|
permission: 'order.data.price',
|
|
@@ -233,22 +256,22 @@ const policies = [
|
|
|
233
256
|
),
|
|
234
257
|
];
|
|
235
258
|
|
|
236
|
-
//
|
|
259
|
+
// export ready policies
|
|
237
260
|
export default policies;
|
|
238
261
|
```
|
|
239
262
|
|
|
240
|
-
**Creating policies
|
|
263
|
+
**Creating policies using JSON**
|
|
241
264
|
|
|
242
265
|
JSON allows you to store policies in a file or database, for example, in PostgreSQL, which supports working with JSON data.
|
|
243
266
|
|
|
244
|
-
Policy,
|
|
267
|
+
Policy, group, and rule classes have methods to export to JSON, so you can form policies in any way and export them to JSON whenever you need it
|
|
245
268
|
|
|
246
269
|
```ts
|
|
247
270
|
import { AbilityJSONParser } from '@via-profit/ability';
|
|
248
271
|
|
|
249
272
|
// Define resource types for TypeScript
|
|
250
|
-
// Types can be generated automatically (more on
|
|
251
|
-
// In this example, for simplicity, types are
|
|
273
|
+
// Types can be generated automatically (more on that later), or described manually
|
|
274
|
+
// In this example, for simplicity, types are described manually
|
|
252
275
|
type Resources = {
|
|
253
276
|
['order.action.create']: {
|
|
254
277
|
user: {
|
|
@@ -263,11 +286,11 @@ type Resources = {
|
|
|
263
286
|
}
|
|
264
287
|
|
|
265
288
|
// Parse JSON using AbilityJSONParser
|
|
266
|
-
// Pass the resource types as a generic
|
|
289
|
+
// Pass the resource types as a generic
|
|
267
290
|
const policies = AbilityJSONParser.parse<Resources>([
|
|
268
291
|
{
|
|
269
292
|
id: '1',
|
|
270
|
-
name: '
|
|
293
|
+
name: 'Creating an order is available only to persons over 18 years old',
|
|
271
294
|
effect: 'permit',
|
|
272
295
|
permission: 'order.action.create',
|
|
273
296
|
compareMethod: 'and',
|
|
@@ -286,7 +309,7 @@ const policies = AbilityJSONParser.parse<Resources>([
|
|
|
286
309
|
},
|
|
287
310
|
{
|
|
288
311
|
id: '2',
|
|
289
|
-
name: '
|
|
312
|
+
name: 'Editing the price is available only to the administrator',
|
|
290
313
|
effect: 'permit',
|
|
291
314
|
permission: 'order.data.price',
|
|
292
315
|
compareMethod: 'and',
|
|
@@ -307,18 +330,19 @@ const policies = AbilityJSONParser.parse<Resources>([
|
|
|
307
330
|
|
|
308
331
|
export default policies;
|
|
309
332
|
```
|
|
333
|
+
|
|
310
334
|
---
|
|
311
335
|
|
|
312
336
|
## DSL
|
|
313
337
|
|
|
314
338
|
> DSL - Domain-Specific Language
|
|
315
339
|
|
|
316
|
-
Ability DSL is a declarative language for describing access policies.
|
|
340
|
+
Ability DSL is a declarative language for describing access policies.
|
|
317
341
|
It allows you to define rules in a human-readable form using simple constructs: *policies*, *groups*, *rules*, and *annotations*.
|
|
318
342
|
|
|
319
|
-
### Policy
|
|
343
|
+
### Policy structure
|
|
320
344
|
|
|
321
|
-
A policy consists of:
|
|
345
|
+
A policy consists of the following construct:
|
|
322
346
|
|
|
323
347
|
```
|
|
324
348
|
<effect> <permission> if <all|any>:
|
|
@@ -327,14 +351,14 @@ A policy consists of:
|
|
|
327
351
|
|
|
328
352
|
Where:
|
|
329
353
|
|
|
330
|
-
- **effect**
|
|
331
|
-
- **permission**
|
|
332
|
-
|
|
333
|
-
- **if
|
|
354
|
+
- **effect** – `permit` or `deny`
|
|
355
|
+
- **permission** – a string like `permission.foo.bar`
|
|
356
|
+
(the `permission.` prefix is required in DSL but automatically removed by the parser)
|
|
357
|
+
- **if all:** – all groups must be true
|
|
358
|
+
- **if any:** – at least one group must be true
|
|
359
|
+
- a policy can contain one or more rule groups
|
|
334
360
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
Example:
|
|
361
|
+
**Example**
|
|
338
362
|
|
|
339
363
|
```dsl
|
|
340
364
|
permit permission.order.update if any:
|
|
@@ -347,36 +371,67 @@ permit permission.order.update if any:
|
|
|
347
371
|
user.login is equals 'dev'
|
|
348
372
|
```
|
|
349
373
|
|
|
350
|
-
|
|
374
|
+
This policy means:
|
|
375
|
+
|
|
376
|
+
The `permission.order.update` permission will be granted if **at least one** of the two groups is satisfied:
|
|
377
|
+
|
|
378
|
+
1. `user.roles` contains `'admin'` **and** `user.token` is not `null`
|
|
379
|
+
2. `user.roles` contains `'developer'` **or** `user.login` equals `'dev'`
|
|
380
|
+
|
|
381
|
+
If multiple policies match the key, they are **all executed**, top to bottom.
|
|
382
|
+
|
|
383
|
+
Each policy:
|
|
384
|
+
|
|
385
|
+
- **match**
|
|
386
|
+
→ sets a new state (`permit` → allow, `deny` → deny)
|
|
351
387
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
388
|
+
- **mismatch**
|
|
389
|
+
→ **resets the state** to `neutral`
|
|
390
|
+
(i.e., cancels the result of the previous policy)
|
|
355
391
|
|
|
356
|
-
|
|
392
|
+
The final decision is determined by the **last processed policy**, not just the one that matched.
|
|
357
393
|
|
|
358
|
-
|
|
394
|
+
This means:
|
|
359
395
|
|
|
360
|
-
|
|
396
|
+
- a policy can **override** the previous one
|
|
397
|
+
- a policy can **cancel** the previous one (via mismatch)
|
|
398
|
+
- the order of policies in DSL is critically important
|
|
361
399
|
|
|
362
|
-
|
|
400
|
+
### Permission key
|
|
363
401
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
402
|
+
Permission keys are written in `dot notation` and support wildcard patterns using the `*` symbol.
|
|
403
|
+
This allows grouping permissions and overriding behavior for entire families of operations.
|
|
404
|
+
|
|
405
|
+
**How policy matching works**
|
|
406
|
+
|
|
407
|
+
If multiple policies match the key, **all policies are executed in order**, top to bottom.
|
|
408
|
+
The final decision is determined by the **last state** set during processing.
|
|
409
|
+
|
|
410
|
+
This means:
|
|
411
|
+
|
|
412
|
+
- a policy can **override** the result of the previous one
|
|
413
|
+
- a policy can **cancel** the result of the previous one (via mismatch)
|
|
414
|
+
- the order of policies in DSL is critically important
|
|
415
|
+
|
|
416
|
+
### Example of using wildcards
|
|
417
|
+
|
|
418
|
+
| Policy (permission) | key | Matches |
|
|
419
|
+
|---------------------|------------------------|---------|
|
|
420
|
+
| `order.*` | `order.create` | yes |
|
|
421
|
+
| `order.*` | `order.update` | yes |
|
|
422
|
+
| `order.*` | `user.create` | no |
|
|
423
|
+
| `*.create` | `order.create` | yes |
|
|
424
|
+
| `*.create` | `user.create` | yes |
|
|
425
|
+
| `*.create` | `order.update` | no |
|
|
426
|
+
| `user.profile.*` | `user.profile.update` | yes |
|
|
427
|
+
| `user.profile.*` | `user.settings.update` | no |
|
|
428
|
+
|
|
429
|
+
### Example policy with wildcard
|
|
374
430
|
|
|
375
|
-
**Example of a policy with wildcard**
|
|
376
431
|
```ts
|
|
377
432
|
import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
|
|
378
433
|
|
|
379
|
-
// DSL is
|
|
434
|
+
// DSL is incomplete and shown only for example
|
|
380
435
|
const dsl = `
|
|
381
436
|
permit permission.order.*
|
|
382
437
|
deny permission.order.update
|
|
@@ -388,42 +443,47 @@ const resolver = new AbilityResolver(policies);
|
|
|
388
443
|
resolver.enforce('order.update', resource); // will throw AbilityError
|
|
389
444
|
```
|
|
390
445
|
|
|
446
|
+
---
|
|
447
|
+
|
|
391
448
|
**Explanation**
|
|
392
449
|
|
|
393
|
-
|
|
394
|
-
the last matching policy wins.
|
|
450
|
+
The order of policies in DSL determines the final decision.
|
|
395
451
|
|
|
396
|
-
|
|
452
|
+
Processing goes top to bottom:
|
|
397
453
|
|
|
398
|
-
1. `permit
|
|
399
|
-
|
|
454
|
+
1. `permit permission.order.*`
|
|
455
|
+
- match → state = `allow`
|
|
400
456
|
|
|
401
|
-
|
|
457
|
+
2. `deny permission.order.update`
|
|
458
|
+
- match → state = `deny`
|
|
459
|
+
- the final state overwrites the previous one
|
|
460
|
+
|
|
461
|
+
Result:
|
|
402
462
|
|
|
403
463
|
```
|
|
404
464
|
order.update → deny
|
|
405
|
-
order.create →
|
|
406
|
-
order.delete →
|
|
407
|
-
order.view →
|
|
465
|
+
order.create → allow
|
|
466
|
+
order.delete → allow
|
|
467
|
+
order.view → allow
|
|
408
468
|
```
|
|
409
469
|
|
|
410
470
|
### Comments
|
|
411
471
|
|
|
412
|
-
Lines starting with the `#` symbol are considered comments and do not affect the
|
|
472
|
+
Lines starting with the `#` symbol are considered comments and do not affect the result of rules and policies.
|
|
413
473
|
|
|
414
474
|
---
|
|
415
475
|
|
|
416
476
|
### Annotations
|
|
417
477
|
|
|
418
|
-
Currently, only one annotation is supported
|
|
478
|
+
Currently, only one annotation is supported – ’name’, which will be used as the name for the policy, rule group, or rule.
|
|
419
479
|
|
|
420
|
-
Annotations are
|
|
480
|
+
Annotations are set via comments:
|
|
421
481
|
|
|
422
482
|
```
|
|
423
483
|
# @name <name>
|
|
424
484
|
```
|
|
425
485
|
|
|
426
|
-
Annotations apply to the **
|
|
486
|
+
Annotations apply to the **next entity**:
|
|
427
487
|
|
|
428
488
|
- policy
|
|
429
489
|
- group
|
|
@@ -442,9 +502,9 @@ permit permission.order.update if any:
|
|
|
442
502
|
|
|
443
503
|
---
|
|
444
504
|
|
|
445
|
-
### Rule
|
|
505
|
+
### Rule groups
|
|
446
506
|
|
|
447
|
-
A group defines how
|
|
507
|
+
A group defines how rules are combined within it:
|
|
448
508
|
|
|
449
509
|
```
|
|
450
510
|
all of:
|
|
@@ -456,14 +516,14 @@ any of:
|
|
|
456
516
|
<rule>
|
|
457
517
|
```
|
|
458
518
|
|
|
459
|
-
- `all of:`
|
|
460
|
-
- `any of:`
|
|
519
|
+
- `all of:` – logical AND
|
|
520
|
+
- `any of:` – logical OR
|
|
461
521
|
|
|
462
|
-
`all of` means
|
|
522
|
+
`all of` – means the group is considered satisfied if all rules within the group matched.
|
|
463
523
|
|
|
464
|
-
`any of` means
|
|
524
|
+
`any of` – means the group is considered satisfied if at least one rule within the group matched.
|
|
465
525
|
|
|
466
|
-
Each group
|
|
526
|
+
Each group inside a policy will be evaluated independently of other groups. The final result will be determined by comparing the evaluation results of all groups in the policy.
|
|
467
527
|
|
|
468
528
|
Groups can have annotations:
|
|
469
529
|
|
|
@@ -477,12 +537,12 @@ any of:
|
|
|
477
537
|
|
|
478
538
|
### Rules
|
|
479
539
|
|
|
480
|
-
A rule is an atomic condition inside a policy. It defines under what data the policy
|
|
540
|
+
A rule is an atomic condition inside a policy. It defines under what data the policy will be considered matching. Rules are used to set conditions that determine the policy's effect (`permit` or `deny`).
|
|
481
541
|
|
|
482
542
|
A rule has the form:
|
|
483
543
|
|
|
484
544
|
```
|
|
485
|
-
<subject> <operator> <value?> —
|
|
545
|
+
<subject> <operator> <value?> — value is not specified for all operators (e.g., is null does not require a value).
|
|
486
546
|
```
|
|
487
547
|
|
|
488
548
|
#### Subject
|
|
@@ -497,12 +557,12 @@ order.total
|
|
|
497
557
|
|
|
498
558
|
#### Operators
|
|
499
559
|
|
|
500
|
-
|
|
560
|
+
_Synonyms are alternative forms of notation that are also supported by the parser._
|
|
501
561
|
|
|
502
|
-
**Basic
|
|
562
|
+
**Basic comparison operators**
|
|
503
563
|
|
|
504
564
|
| DSL Operator | Synonyms | Example | Description | Types |
|
|
505
|
-
|
|
565
|
+
|--------------|----------|--------|----------|------|
|
|
506
566
|
| **is equals** | `=`, `==`, `equals` | `age is equals 18` | Strict equality | number, string, boolean |
|
|
507
567
|
| **is not equals** | `!=`, `<>`, `not equals` | `role is not equals 'admin'` | Strict inequality | number, string, boolean |
|
|
508
568
|
| **greater than** | `>`, `gt` | `age greater than 18` | Greater than | number, date |
|
|
@@ -510,68 +570,64 @@ order.total
|
|
|
510
570
|
| **less than** | `<`, `lt` | `age less than 18` | Less than | number, date |
|
|
511
571
|
| **less than or equal** | `<=`, `lte` | `age less than or equal 18` | Less than or equal | number, date |
|
|
512
572
|
|
|
513
|
-
**Null
|
|
573
|
+
**Null operators**
|
|
514
574
|
|
|
515
575
|
| DSL Operator | Synonyms | Example | Description | Types |
|
|
516
|
-
|
|
576
|
+
|--------------|----------|--------|----------|------|
|
|
517
577
|
| **is null** | `== null`, `= null` | `middleName is null` | Value is absent | any |
|
|
518
578
|
| **is not null** | `!= null` | `middleName is not null` | Value is present | any |
|
|
519
579
|
|
|
520
|
-
**Operators for
|
|
580
|
+
**Operators for lists (arrays)**
|
|
521
581
|
|
|
522
582
|
| DSL Operator | Synonyms | Example | Description | Types |
|
|
523
|
-
|
|
583
|
+
|--------------|---------------------------|--------|----------|------|
|
|
524
584
|
| **in [...]** | - | `role in ['admin', 'manager']` | Value is in the list | number, string |
|
|
525
585
|
| **not in [...]** | - | `role not in ['banned']` | Value is not in the list | number, string |
|
|
526
|
-
| **contains** | `includes`, `has` | `tags contains 'vip'` | Array contains
|
|
527
|
-
| **not contains** | `not includes`, `not has` | `tags not contains 'vip'` | Array does not contain
|
|
586
|
+
| **contains** | `includes`, `has` | `tags contains 'vip'` | Array contains element | array |
|
|
587
|
+
| **not contains** | `not includes`, `not has` | `tags not contains 'vip'` | Array does not contain element | array |
|
|
528
588
|
|
|
529
|
-
**Boolean
|
|
589
|
+
**Boolean operators**
|
|
530
590
|
|
|
531
591
|
| DSL Operator | Synonyms | Example | Description | Types |
|
|
532
|
-
|
|
592
|
+
|--------------|----------|--------|----------|------|
|
|
533
593
|
| **is true** | `= true` | `isActive is true` | Value is true | boolean |
|
|
534
594
|
| **is false** | `= false` | `isActive is false` | Value is false | boolean |
|
|
535
595
|
|
|
536
|
-
**Length
|
|
596
|
+
**Length operators**
|
|
537
597
|
|
|
538
598
|
| DSL Operator | Synonyms | Example | Description | Types |
|
|
539
|
-
|
|
599
|
+
|--------------|----------|--------|----------|------|
|
|
540
600
|
| **length equals** | `len =` | `tags length equals 3` | Length equals | array, string |
|
|
541
601
|
| **length greater than** | `len >` | `tags length greater than 2` | Length greater than | array, string |
|
|
542
602
|
| **length less than** | `len <` | `tags length less than 5` | Length less than | array, string |
|
|
543
603
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
**Special Operators**
|
|
604
|
+
**Special operators**
|
|
547
605
|
|
|
548
606
|
| DSL Operator | Synonyms | Example | Description | Types |
|
|
549
|
-
|
|
550
|
-
| **always** | — | `always` |
|
|
551
|
-
| **never** | — | `never` |
|
|
552
|
-
|
|
607
|
+
|--------------|----------|--------|----------|------|
|
|
608
|
+
| **always** | — | `always` | Condition always true. Used for global permission or simplifying logic. | special operator |
|
|
609
|
+
| **never** | — | `never` | Condition always false. Used for global denial or disabling a rule. | special operator |
|
|
553
610
|
|
|
554
611
|
**always**
|
|
555
|
-
An operator that always returns `true`.
|
|
612
|
+
An operator that always returns `true`.
|
|
556
613
|
Used for:
|
|
557
614
|
|
|
558
|
-
- global
|
|
615
|
+
- global permission (`permit permission.* if all: always`)
|
|
559
616
|
- testing
|
|
560
617
|
- disabling complex conditions
|
|
561
618
|
- creating fallback rules
|
|
562
619
|
|
|
563
620
|
**never**
|
|
564
|
-
An operator that always returns `false`.
|
|
621
|
+
An operator that always returns `false`.
|
|
565
622
|
Used for:
|
|
566
623
|
|
|
567
|
-
- global
|
|
624
|
+
- global denial (`deny permission.* if all: never`)
|
|
568
625
|
- temporarily disabling a rule
|
|
569
|
-
- explicit
|
|
570
|
-
|
|
626
|
+
- explicit negation without conditions
|
|
571
627
|
|
|
572
628
|
#### Value
|
|
573
629
|
|
|
574
|
-
Supported
|
|
630
|
+
Supported:
|
|
575
631
|
|
|
576
632
|
- strings `'text'`
|
|
577
633
|
- numbers `42`
|
|
@@ -585,7 +641,7 @@ Examples:
|
|
|
585
641
|
# user age greater than 18
|
|
586
642
|
user.age greater than 18
|
|
587
643
|
|
|
588
|
-
# array of roles contains
|
|
644
|
+
# array of roles contains role 'admin'
|
|
589
645
|
user.roles contains 'admin'
|
|
590
646
|
|
|
591
647
|
# order tag is either 'vip' or 'priority'
|
|
@@ -600,9 +656,9 @@ user.login length greater than 12
|
|
|
600
656
|
|
|
601
657
|
---
|
|
602
658
|
|
|
603
|
-
### Implicit
|
|
659
|
+
### Implicit group
|
|
604
660
|
|
|
605
|
-
If rules are written without `all of:` or `any of:`, they are combined
|
|
661
|
+
If rules are written without `all of:` or `any of:`, they are combined by the policy operator:
|
|
606
662
|
|
|
607
663
|
```dsl
|
|
608
664
|
permit permission.order.update if all:
|
|
@@ -623,33 +679,33 @@ The implicit group always matches the policy operator (`if all` or `if any`).
|
|
|
623
679
|
|
|
624
680
|
---
|
|
625
681
|
|
|
626
|
-
###
|
|
682
|
+
### Full example
|
|
627
683
|
|
|
628
684
|
```dsl
|
|
629
685
|
# @name order update allowed
|
|
630
686
|
permit permission.order.update if any:
|
|
631
687
|
|
|
632
|
-
# @name if admin
|
|
688
|
+
# @name if this is admin
|
|
633
689
|
all of:
|
|
634
690
|
user.roles contains 'admin'
|
|
635
691
|
user.token is not null
|
|
636
692
|
|
|
637
|
-
# @name if developer
|
|
693
|
+
# @name if this is developer
|
|
638
694
|
any of:
|
|
639
695
|
user.roles contains 'developer'
|
|
640
696
|
user.login is equals 'dev'
|
|
641
697
|
```
|
|
642
698
|
|
|
643
|
-
## Combining
|
|
699
|
+
## Combining policies
|
|
644
700
|
|
|
645
|
-
In a real project, you should use
|
|
701
|
+
In a real project, you should use several policies at once.
|
|
646
702
|
|
|
647
703
|
TODO: using multiple policies
|
|
648
704
|
|
|
649
705
|
## Policy Environment
|
|
650
706
|
|
|
651
|
-
**Environment** is an object containing
|
|
652
|
-
The content of the object is defined by the developer and can be any object
|
|
707
|
+
**Environment** is an object containing environment data that does not belong to either the user or the resource.
|
|
708
|
+
The content of the object is defined by the developer and can be any object composed of primitives.
|
|
653
709
|
|
|
654
710
|
- request time,
|
|
655
711
|
- IP address,
|
|
@@ -658,8 +714,7 @@ The content of the object is defined by the developer and can be any object cons
|
|
|
658
714
|
- session context,
|
|
659
715
|
- any other external conditions.
|
|
660
716
|
|
|
661
|
-
|
|
662
|
-
Environment is passed to `resolve()` and `enforce()` as the third argument:
|
|
717
|
+
The environment is passed to `resolve()` and `enforce()` as the third argument:
|
|
663
718
|
|
|
664
719
|
```ts
|
|
665
720
|
const environment = {
|
|
@@ -674,7 +729,7 @@ resolver.enforce('order.update', resource, environment);
|
|
|
674
729
|
|
|
675
730
|
### Using environment in rules
|
|
676
731
|
|
|
677
|
-
In a policy, you can refer to environment via the `env
|
|
732
|
+
In a policy, you can refer to the environment via the path `env.*`.
|
|
678
733
|
|
|
679
734
|
Example policy that denies order updates at night (10 PM – 6 AM):
|
|
680
735
|
|
|
@@ -687,7 +742,7 @@ deny permission.order.update if all:
|
|
|
687
742
|
|
|
688
743
|
**Retrieving values from environment**
|
|
689
744
|
|
|
690
|
-
If
|
|
745
|
+
If the rule specifies a path:
|
|
691
746
|
|
|
692
747
|
- `env.*` → value is taken from environment
|
|
693
748
|
- `user.*`, `order.*`, `profile.*` → from resource
|
|
@@ -703,7 +758,7 @@ condition: "equal"
|
|
|
703
758
|
|
|
704
759
|
### Environment in TypeScript
|
|
705
760
|
|
|
706
|
-
The Environment type is
|
|
761
|
+
The Environment type is specified at the `AbilityResolver` level:
|
|
707
762
|
|
|
708
763
|
```ts
|
|
709
764
|
const resolver = new AbilityResolver<Resources, Environment>(policies);
|
|
@@ -711,17 +766,17 @@ const resolver = new AbilityResolver<Resources, Environment>(policies);
|
|
|
711
766
|
|
|
712
767
|
This allows:
|
|
713
768
|
|
|
714
|
-
- getting autocompletion in IDE,
|
|
769
|
+
- getting autocompletion in the IDE,
|
|
715
770
|
- checking the correctness of `env.*` paths,
|
|
716
|
-
- avoiding errors when passing environment.
|
|
771
|
+
- avoiding errors when passing the environment.
|
|
717
772
|
|
|
718
|
-
> If a rule uses `env.*` but environment is not passed,
|
|
773
|
+
> If a rule uses `env.*` but the environment is not passed, the `env.*` value will be `undefined`, and the comparison will be performed as if the environment were not present at all.
|
|
719
774
|
|
|
720
|
-
## TypeScript
|
|
775
|
+
## TypeScript type generator
|
|
721
776
|
|
|
722
|
-
`AbilityTypeGenerator.generateTypeDefs(policies)` generates TypeScript
|
|
777
|
+
`AbilityTypeGenerator.generateTypeDefs(policies)` generates types for TypeScript based on policies, allowing you not to worry about discrepancies between types and data in policies.
|
|
723
778
|
|
|
724
|
-
**
|
|
779
|
+
**Usage example**
|
|
725
780
|
|
|
726
781
|
Policies can be stored in DSL or JSON. This example uses a DSL file.
|
|
727
782
|
|
|
@@ -746,7 +801,7 @@ const { AbilityTypeGenerator, AbilityDSLParser } = require('@via-profit/ability'
|
|
|
746
801
|
const dslPath = path.resolve(__dirname, '../src/policies/policies.dsl');
|
|
747
802
|
const typeDefsPath = path.join(path.dirname(dslPath), 'policies.types.ts');
|
|
748
803
|
|
|
749
|
-
// Read DSL as
|
|
804
|
+
// Read DSL as string
|
|
750
805
|
const dsl = fs.readFileSync(dslPath, {encoding: 'utf-8'});
|
|
751
806
|
|
|
752
807
|
// Create policies
|
|
@@ -770,7 +825,6 @@ const policies = new AbilityDSLParser<Resources>(dsl).parse();
|
|
|
770
825
|
export const policyResolver = new AbilityResolver(new AbilityDSLParser<Resources>(dsl).parse());
|
|
771
826
|
|
|
772
827
|
export default policyResolver;
|
|
773
|
-
|
|
774
828
|
```
|
|
775
829
|
|
|
776
830
|
**Generated file (example)**
|
|
@@ -803,17 +857,17 @@ resolver.enforce('order.update', {
|
|
|
803
857
|
});
|
|
804
858
|
```
|
|
805
859
|
|
|
806
|
-
##
|
|
860
|
+
## Debugging policies
|
|
807
861
|
|
|
808
862
|
### Explanations
|
|
809
863
|
|
|
810
|
-
To simplify policy debugging, a special `AbilityResult`
|
|
864
|
+
To simplify policy debugging, a special class `AbilityResult` is used, which is already included in the final calculation result. `AbilityResult` encapsulates the result of applying all matching policies to the permission key and resource.
|
|
811
865
|
|
|
812
866
|
`AbilityResult` contains:
|
|
813
867
|
|
|
814
|
-
-
|
|
868
|
+
- list of evaluated policies,
|
|
815
869
|
- methods to determine the final effect,
|
|
816
|
-
- methods to get explanations in
|
|
870
|
+
- methods to get explanations in text representation.
|
|
817
871
|
|
|
818
872
|
Example:
|
|
819
873
|
|
|
@@ -835,8 +889,8 @@ const explanations = result.explain(); // AbilityExplain
|
|
|
835
889
|
|
|
836
890
|
- which policy matched,
|
|
837
891
|
- which rule groups matched,
|
|
838
|
-
- which rules
|
|
839
|
-
-
|
|
892
|
+
- which rules failed,
|
|
893
|
+
- what effect was applied.
|
|
840
894
|
|
|
841
895
|
Usage example:
|
|
842
896
|
|
|
@@ -858,17 +912,17 @@ Example output:
|
|
|
858
912
|
✓ rule «No role administrator» is match
|
|
859
913
|
```
|
|
860
914
|
|
|
861
|
-
### Output
|
|
915
|
+
### Output format
|
|
862
916
|
|
|
863
|
-
Currently, only one output format is supported
|
|
917
|
+
Currently, only one output format is supported – text.
|
|
864
918
|
|
|
865
|
-
The output
|
|
919
|
+
The output is structured as: <policy | ruleSet | rule > <name> <is match | is mismatch>
|
|
866
920
|
|
|
867
921
|
## Troubleshooting
|
|
868
922
|
|
|
869
|
-
### Decision
|
|
923
|
+
### Decision model (Default Deny)
|
|
870
924
|
|
|
871
|
-
> Why
|
|
925
|
+
> Why doesn't a `deny` policy turn into `permit` if its conditions are not met?
|
|
872
926
|
|
|
873
927
|
Consider a policy that **denies** access to a user aged 16:
|
|
874
928
|
|
|
@@ -889,10 +943,10 @@ console.log(result.isDenied()); // true ✔
|
|
|
889
943
|
console.log(result.isAllowed()); // false ✔
|
|
890
944
|
```
|
|
891
945
|
|
|
892
|
-
In this case, everything is obvious:
|
|
893
|
-
|
|
946
|
+
In this case, everything is obvious:
|
|
947
|
+
condition satisfied → policy matches → effect `deny` → access denied.
|
|
894
948
|
|
|
895
|
-
**What happens if the conditions are
|
|
949
|
+
**What happens if the conditions are `not satisfied`?**
|
|
896
950
|
|
|
897
951
|
```ts
|
|
898
952
|
const result = resolver.resolve('test', {
|
|
@@ -903,66 +957,66 @@ console.log(result.isDenied()); // true ✔
|
|
|
903
957
|
console.log(result.isAllowed()); // false ✔
|
|
904
958
|
```
|
|
905
959
|
|
|
906
|
-
At first glance, it might seem that if the condition is not met, the policy should
|
|
907
|
-
But that
|
|
960
|
+
At first glance, it might seem that if the condition is not met, the policy should "allow" access.
|
|
961
|
+
But that's **not the case**.
|
|
908
962
|
|
|
909
|
-
**Decision
|
|
963
|
+
**Decision model: `Default Deny`**
|
|
910
964
|
|
|
911
965
|
`AbilityResolver` uses the classic security model:
|
|
912
966
|
|
|
913
|
-
> **If there is no matching permit
|
|
967
|
+
> **If there is no matching permit policy → access denied.**
|
|
914
968
|
|
|
915
969
|
**What happens in this example:**
|
|
916
970
|
|
|
917
|
-
1. The `deny` policy exists, but its condition is **not
|
|
918
|
-
→ the policy gets
|
|
971
|
+
1. The `deny` policy exists, but its condition is **not satisfied**
|
|
972
|
+
→ the policy gets `mismatch` status.
|
|
919
973
|
|
|
920
|
-
2. The `deny` policy **
|
|
974
|
+
2. The `deny` policy **does not apply** because the conditions did not match.
|
|
921
975
|
|
|
922
976
|
3. There is no `permit` policy.
|
|
923
977
|
|
|
924
|
-
4. Since there is no
|
|
978
|
+
4. Since there is no granting policy → final decision:
|
|
925
979
|
**deny (by default)**.
|
|
926
980
|
|
|
927
981
|
**Summary**
|
|
928
982
|
|
|
929
983
|
- `deny` with matching conditions → **deny**
|
|
930
|
-
- `deny` with non
|
|
984
|
+
- `deny` with non-matching conditions → **deny (default deny)**
|
|
931
985
|
- `permit` with matching conditions → **allow**
|
|
932
|
-
- `permit` with non
|
|
986
|
+
- `permit` with non-matching conditions → **deny (default deny)**
|
|
933
987
|
|
|
934
988
|
**Conclusion**
|
|
935
989
|
|
|
936
|
-
**Access is
|
|
990
|
+
**Access is only granted when there is an explicit permit.**
|
|
937
991
|
|
|
938
|
-
## Design
|
|
992
|
+
## Design recommendations
|
|
939
993
|
|
|
940
|
-
### Naming
|
|
994
|
+
### Naming access keys
|
|
941
995
|
|
|
942
996
|
- Use hierarchical keys: `permission.order.create`, `permission.order.update.status`, `permission.user.profile.update`.
|
|
943
997
|
- Group by domains: `permission.user.*`, `permission.order.*`, `permission.product.*`.
|
|
944
998
|
- Do not mix different domains in one key.
|
|
945
999
|
|
|
946
|
-
### Data
|
|
1000
|
+
### Data structure
|
|
947
1001
|
|
|
948
1002
|
- Explicitly describe `Resources` in TypeScript.
|
|
949
|
-
- Do not pass
|
|
950
|
-
-
|
|
1003
|
+
- Do not pass "extra" fields – this complicates understanding.
|
|
1004
|
+
- Try to keep the data structure for a single `permission` stable.
|
|
951
1005
|
|
|
952
|
-
###
|
|
1006
|
+
### Designing policies
|
|
953
1007
|
|
|
954
|
-
-
|
|
955
|
-
- Specific restrictions
|
|
1008
|
+
- Common rules – via wildcard (`permission.order.*`).
|
|
1009
|
+
- Specific restrictions – via exact actions (`permission.order.update`).
|
|
956
1010
|
- Use `effect: deny` for prohibitions.
|
|
957
1011
|
- Use `effect: permit` for permissions.
|
|
958
1012
|
|
|
959
|
-
###
|
|
1013
|
+
### Typical mistakes
|
|
960
1014
|
|
|
961
|
-
- Expecting that absence of matching policies means
|
|
1015
|
+
- Expecting that the absence of matching policies means deny.
|
|
962
1016
|
- Mixing business logic and access policies.
|
|
963
|
-
- Too large policies with dozens of rules
|
|
1017
|
+
- Too large policies with dozens of rules – it's better to split them.
|
|
964
1018
|
|
|
965
|
-
### Example of
|
|
1019
|
+
### Example of use on the frontend (React)
|
|
966
1020
|
|
|
967
1021
|
**Hook for checking policies**
|
|
968
1022
|
|
|
@@ -1006,7 +1060,7 @@ export function useAbility<Permission extends keyof Resources>(
|
|
|
1006
1060
|
}
|
|
1007
1061
|
```
|
|
1008
1062
|
|
|
1009
|
-
**Usage in
|
|
1063
|
+
**Usage in component**
|
|
1010
1064
|
|
|
1011
1065
|
```tsx
|
|
1012
1066
|
function OrderUpdateButton({ order, user }) {
|
|
@@ -1016,7 +1070,7 @@ function OrderUpdateButton({ order, user }) {
|
|
|
1016
1070
|
});
|
|
1017
1071
|
|
|
1018
1072
|
if (allowed === null) {
|
|
1019
|
-
return null; // or loading
|
|
1073
|
+
return null; // or loading badge
|
|
1020
1074
|
}
|
|
1021
1075
|
|
|
1022
1076
|
if (!allowed) {
|
|
@@ -1029,177 +1083,125 @@ function OrderUpdateButton({ order, user }) {
|
|
|
1029
1083
|
|
|
1030
1084
|
## Examples
|
|
1031
1085
|
|
|
1032
|
-
### Example of a
|
|
1086
|
+
### Example of a complex multi-stage policy
|
|
1033
1087
|
|
|
1034
|
-
Below is a
|
|
1088
|
+
Below is an example of a set of policies for a cinema.
|
|
1089
|
+
It demonstrates:
|
|
1035
1090
|
|
|
1036
|
-
**The example demonstrates:**
|
|
1037
1091
|
- working with roles (admin, seller, manager, VIP, banned),
|
|
1038
|
-
- time
|
|
1092
|
+
- time restrictions (`env.time.hour`),
|
|
1039
1093
|
- wildcard permissions (`permission.*`),
|
|
1040
|
-
- ticket quantity
|
|
1094
|
+
- ticket quantity restrictions,
|
|
1041
1095
|
- prohibition on selling already sold tickets,
|
|
1042
|
-
- combination of `permit`/`deny
|
|
1043
|
-
-
|
|
1044
|
-
|
|
1045
|
-
**Brief description of rules**
|
|
1046
|
-
- **Administrator**
|
|
1047
|
-
Has wildcard permissions (`permission.*`) and can perform any action.
|
|
1048
|
-
Can edit ticket prices.
|
|
1049
|
-
|
|
1050
|
-
- **Seller**
|
|
1051
|
-
Can sell tickets only during working hours (09:00–23:00).
|
|
1052
|
-
Cannot sell tickets if:
|
|
1053
|
-
- the cinema is closed,
|
|
1054
|
-
- the ticket is already sold.
|
|
1055
|
-
|
|
1056
|
-
- **Manager**
|
|
1057
|
-
Has the same rights as a seller.
|
|
1096
|
+
- combination of `permit`/`deny`,
|
|
1097
|
+
- **sequential processing of policies**,
|
|
1098
|
+
- **state‑machine model**, where each policy can **set or reset the state**.
|
|
1058
1099
|
|
|
1059
|
-
|
|
1060
|
-
- A user older than 21 can buy tickets.
|
|
1061
|
-
- A VIP user can buy tickets at any time.
|
|
1062
|
-
- A banned user (`status = banned`) cannot buy tickets.
|
|
1063
|
-
- Any user cannot buy more than 6 tickets.
|
|
1064
|
-
|
|
1065
|
-
**Policy Diagram**
|
|
1066
|
-
|
|
1067
|
-
```mermaid
|
|
1068
|
-
flowchart LR
|
|
1069
|
-
|
|
1070
|
-
%% ==== ROLES ====
|
|
1071
|
-
|
|
1072
|
-
subgraph Roles[Roles]
|
|
1073
|
-
A[Administrator]
|
|
1074
|
-
B[Seller]
|
|
1075
|
-
C[Manager]
|
|
1076
|
-
end
|
|
1077
|
-
|
|
1078
|
-
subgraph Buyers[Buyers]
|
|
1079
|
-
U1[User > 21]
|
|
1080
|
-
U2[VIP user]
|
|
1081
|
-
U3[Banned user]
|
|
1082
|
-
end
|
|
1083
|
-
|
|
1084
|
-
%% ==== ADMIN ====
|
|
1085
|
-
|
|
1086
|
-
A --> A1[Wildcard: permission.*]
|
|
1087
|
-
A --> A2[Edit ticket price]
|
|
1088
|
-
|
|
1089
|
-
A1 --> FINAL[Final decision]
|
|
1090
|
-
A2 --> FINAL
|
|
1100
|
+
---
|
|
1091
1101
|
|
|
1092
|
-
|
|
1102
|
+
**Unlike classic ABAC systems, where `mismatch` is ignored, Ability uses a `state‑machine` model:**
|
|
1093
1103
|
|
|
1094
|
-
|
|
1104
|
+
- **match** → policy sets a state (`allow` or `deny`)
|
|
1105
|
+
- **mismatch** → policy **resets the state to neutral**
|
|
1106
|
+
- the final result is determined by the **last processed policy**
|
|
1095
1107
|
|
|
1096
|
-
|
|
1097
|
-
B1 -->|Outside hours| D2[Denied]
|
|
1098
|
-
B1 -->|ticket.status = sold| D3[Denied]
|
|
1108
|
+
This means:
|
|
1099
1109
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1110
|
+
- a policy can **override** the previous one
|
|
1111
|
+
- a policy can **cancel** the previous one (via mismatch)
|
|
1112
|
+
- the order of policies in DSL is critically important
|
|
1113
|
+
- the final decision does not always match the “intuitive” reading of rules from top to bottom
|
|
1103
1114
|
|
|
1104
|
-
|
|
1115
|
+
### Brief description of the rules
|
|
1105
1116
|
|
|
1106
|
-
|
|
1107
|
-
C1 --> FINAL
|
|
1117
|
+
**Administrator**
|
|
1108
1118
|
|
|
1109
|
-
|
|
1119
|
+
- Has wildcard rights (`permission.*`)
|
|
1120
|
+
- Can edit ticket prices
|
|
1110
1121
|
|
|
1111
|
-
|
|
1112
|
-
U1A -->|ticketsCount < 6| U1OK[Allowed]
|
|
1113
|
-
U1A -->|ticketsCount ≥ 6| U1DENY[Denied]
|
|
1122
|
+
**Seller**
|
|
1114
1123
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1124
|
+
- Can sell tickets only during working hours (09:00–23:00)
|
|
1125
|
+
- Cannot sell tickets if:
|
|
1126
|
+
- the cinema is closed,
|
|
1127
|
+
- the ticket is already sold
|
|
1118
1128
|
|
|
1119
|
-
|
|
1129
|
+
**Manager**
|
|
1120
1130
|
|
|
1121
|
-
|
|
1122
|
-
U1DENY --> FINAL
|
|
1123
|
-
U2OK --> FINAL
|
|
1124
|
-
U2DENY --> FINAL
|
|
1125
|
-
U3A --> FINAL
|
|
1131
|
+
- Has the same rights as the seller
|
|
1126
1132
|
|
|
1127
|
-
|
|
1133
|
+
**Buyers**
|
|
1128
1134
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1135
|
+
- A user over 21 years old can buy tickets
|
|
1136
|
+
- A VIP user can buy tickets at any time
|
|
1137
|
+
- A banned user (`status = banned`) cannot buy tickets
|
|
1138
|
+
- Any user cannot buy more than 6 tickets
|
|
1131
1139
|
|
|
1132
|
-
|
|
1140
|
+
### DSL policies
|
|
1133
1141
|
|
|
1134
1142
|
```dsl
|
|
1135
|
-
############################################################
|
|
1136
|
-
# @name Admin can edit ticket price
|
|
1137
1143
|
permit permission.ticket.price.edit if all:
|
|
1138
1144
|
user.role is equals 'admin'
|
|
1139
1145
|
|
|
1140
|
-
|
|
1141
|
-
############################################################
|
|
1142
|
-
# @name Seller can sell tickets during working hours
|
|
1143
1146
|
permit permission.ticket.sell if all:
|
|
1144
1147
|
user.role is equals 'seller'
|
|
1145
1148
|
all of:
|
|
1146
1149
|
env.time.hour greater than or equal 9
|
|
1147
1150
|
env.time.hour less than or equal 23
|
|
1148
1151
|
|
|
1149
|
-
|
|
1150
|
-
############################################################
|
|
1151
|
-
# @name Users older than 21 can buy tickets
|
|
1152
1152
|
permit permission.ticket.buy if all:
|
|
1153
1153
|
user.age greater than 21
|
|
1154
1154
|
|
|
1155
|
-
|
|
1156
|
-
############################################################
|
|
1157
|
-
# @name VIP users can buy tickets anytime
|
|
1158
1155
|
permit permission.ticket.buy if all:
|
|
1159
1156
|
user.isVIP is true
|
|
1160
1157
|
|
|
1161
|
-
|
|
1162
|
-
############################################################
|
|
1163
|
-
# @name Deny buying tickets if user is banned
|
|
1164
1158
|
deny permission.ticket.buy if all:
|
|
1165
1159
|
user.status is equals 'banned'
|
|
1166
1160
|
|
|
1167
|
-
|
|
1168
|
-
############################################################
|
|
1169
|
-
# @name Deny selling tickets if cinema is closed
|
|
1170
1161
|
deny permission.ticket.sell if all:
|
|
1171
1162
|
any of:
|
|
1172
1163
|
env.time.hour less than 9
|
|
1173
1164
|
env.time.hour greater than 23
|
|
1174
1165
|
|
|
1175
|
-
|
|
1176
|
-
############################################################
|
|
1177
|
-
# @name Manager can do everything seller can
|
|
1178
1166
|
permit permission.ticket.sell if all:
|
|
1179
1167
|
user.role is equals 'manager'
|
|
1180
1168
|
|
|
1181
|
-
|
|
1182
|
-
############################################################
|
|
1183
|
-
# @name Admin wildcard permissions
|
|
1184
1169
|
permit permission.* if all:
|
|
1185
1170
|
user.role is equals 'admin'
|
|
1186
1171
|
|
|
1187
|
-
|
|
1188
|
-
############################################################
|
|
1189
|
-
# @name Limit tickets per user (max 6)
|
|
1190
1172
|
deny permission.ticket.buy if all:
|
|
1191
1173
|
user.ticketsCount greater than or equal 6
|
|
1192
1174
|
|
|
1193
|
-
|
|
1194
|
-
############################################################
|
|
1195
|
-
# @name Cannot sell already sold tickets
|
|
1196
1175
|
deny permission.ticket.sell if all:
|
|
1197
1176
|
ticket.status is equals 'sold'
|
|
1198
1177
|
```
|
|
1199
1178
|
|
|
1200
|
-
|
|
1179
|
+
**Example: seller sells a ticket at 3:00 PM**
|
|
1180
|
+
|
|
1181
|
+
1. permit seller match → `allow`
|
|
1182
|
+
2. deny closed mismatch → `neutral`
|
|
1183
|
+
3. deny sold mismatch → `neutral`
|
|
1184
|
+
|
|
1185
|
+
Result: `neutral → deny`
|
|
1186
|
+
|
|
1187
|
+
**Example: VIP buys a ticket at night**
|
|
1188
|
+
|
|
1189
|
+
1. permit age>21 mismatch → `neutral`
|
|
1190
|
+
2. permit VIP match → `allow`
|
|
1191
|
+
3. deny banned mismatch → `neutral`
|
|
1192
|
+
4. deny limit mismatch → `neutral`
|
|
1193
|
+
|
|
1194
|
+
Result: `neutral → deny`
|
|
1195
|
+
|
|
1196
|
+
**Example: administrator sells a ticket at night**
|
|
1197
|
+
|
|
1198
|
+
1. permit admin wildcard match → `allow`
|
|
1199
|
+
2. deny closed match → `deny`
|
|
1200
|
+
3. deny sold mismatch → `neutral`
|
|
1201
|
+
|
|
1202
|
+
Result: `neutral → deny`
|
|
1201
1203
|
|
|
1202
|
-
|
|
1204
|
+
### Preparing policies
|
|
1203
1205
|
|
|
1204
1206
|
```ts
|
|
1205
1207
|
import { AbilityDSLParser } from '@via-profit/ability';
|
|
@@ -1208,7 +1210,7 @@ import cinemaDSL from './policies/cinema.dsl';
|
|
|
1208
1210
|
export const policies = new AbilityDSLParser(cinemaDSL).parse();
|
|
1209
1211
|
```
|
|
1210
1212
|
|
|
1211
|
-
|
|
1213
|
+
### Creating a resolver
|
|
1212
1214
|
|
|
1213
1215
|
```ts
|
|
1214
1216
|
import { AbilityResolver } from '@via-profit/ability';
|
|
@@ -1217,11 +1219,7 @@ import { policies } from './policies';
|
|
|
1217
1219
|
const resolver = new AbilityResolver(policies);
|
|
1218
1220
|
```
|
|
1219
1221
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
Example: buying a ticket.
|
|
1223
|
-
|
|
1224
|
-
The `enforce` method throws an `AbilityError` if access is denied.
|
|
1222
|
+
### enforce (throws an error on deny)
|
|
1225
1223
|
|
|
1226
1224
|
```ts
|
|
1227
1225
|
resolver.enforce('ticket.buy', {
|
|
@@ -1229,12 +1227,8 @@ resolver.enforce('ticket.buy', {
|
|
|
1229
1227
|
env: { time: { hour: 18 } },
|
|
1230
1228
|
});
|
|
1231
1229
|
```
|
|
1232
|
-
If allowed — the code continues execution.
|
|
1233
|
-
If denied — an `AbilityError` exception is thrown.
|
|
1234
|
-
|
|
1235
|
-
**Checking Permissions Without Exceptions (resolve)**
|
|
1236
1230
|
|
|
1237
|
-
|
|
1231
|
+
### resolve (without exceptions)
|
|
1238
1232
|
|
|
1239
1233
|
```ts
|
|
1240
1234
|
const result = resolver.resolve('ticket.buy', {
|
|
@@ -1249,19 +1243,9 @@ if (result.isAllowed()) {
|
|
|
1249
1243
|
}
|
|
1250
1244
|
```
|
|
1251
1245
|
|
|
1252
|
-
**
|
|
1246
|
+
**Preparing data for the resolver**
|
|
1253
1247
|
|
|
1254
|
-
|
|
1255
|
-
resolver.enforce('ticket.sell', {
|
|
1256
|
-
user: { role: 'seller' },
|
|
1257
|
-
env: { time: { hour: 15 } },
|
|
1258
|
-
ticket: { status: 'available' },
|
|
1259
|
-
});
|
|
1260
|
-
```
|
|
1261
|
-
|
|
1262
|
-
**Preparing Data for the Resolver**
|
|
1263
|
-
|
|
1264
|
-
In the examples above, constant objects are passed to the resolver:
|
|
1248
|
+
In the examples above, simple constant objects are passed to the resolver:
|
|
1265
1249
|
|
|
1266
1250
|
```ts
|
|
1267
1251
|
resolver.enforce('ticket.buy', {
|
|
@@ -1270,7 +1254,7 @@ resolver.enforce('ticket.buy', {
|
|
|
1270
1254
|
});
|
|
1271
1255
|
```
|
|
1272
1256
|
|
|
1273
|
-
This is done for clarity. In a real application, the data for the resolver should be
|
|
1257
|
+
This is done for clarity. In a real application, the data for the resolver should be formed dynamically – from the sources available to your server.
|
|
1274
1258
|
|
|
1275
1259
|
**User** (`user`) is usually taken from:
|
|
1276
1260
|
|
|
@@ -1285,7 +1269,7 @@ Example:
|
|
|
1285
1269
|
const user = await db.users.findById(session.userId);
|
|
1286
1270
|
```
|
|
1287
1271
|
|
|
1288
|
-
**Environment
|
|
1272
|
+
**Environment (`env`)**
|
|
1289
1273
|
|
|
1290
1274
|
These are any external parameters that can affect access:
|
|
1291
1275
|
|
|
@@ -1308,7 +1292,7 @@ const env = {
|
|
|
1308
1292
|
|
|
1309
1293
|
**Resource** (e.g., `ticket`)
|
|
1310
1294
|
|
|
1311
|
-
If the action is
|
|
1295
|
+
If the action is related to a specific object – it also needs to be loaded:
|
|
1312
1296
|
|
|
1313
1297
|
```ts
|
|
1314
1298
|
const ticket = await db.tickets.findById(req.params.ticketId);
|
|
@@ -1316,17 +1300,17 @@ const ticket = await db.tickets.findById(req.params.ticketId);
|
|
|
1316
1300
|
|
|
1317
1301
|
**Context**
|
|
1318
1302
|
|
|
1319
|
-
|
|
1320
|
-
It contains **all the data** that policies
|
|
1303
|
+
The context is the object you pass to `resolve` or `enforce`.
|
|
1304
|
+
It contains **all the data** that policies may need:
|
|
1321
1305
|
|
|
1322
|
-
- `user`
|
|
1323
|
-
- `env`
|
|
1324
|
-
- `resource` or `ticket`
|
|
1325
|
-
- any other objects
|
|
1306
|
+
- `user` – data about the current user
|
|
1307
|
+
- `env` – environment data (time, IP, geography, system settings)
|
|
1308
|
+
- `resource` or `ticket` – data about the entity on which the action is performed
|
|
1309
|
+
- any other objects you use in the DSL
|
|
1326
1310
|
|
|
1327
|
-
**
|
|
1311
|
+
**Important to understand:**
|
|
1328
1312
|
|
|
1329
|
-
>
|
|
1313
|
+
> The context is formed for a specific action and specific policies. You don't need to store it in advance – you collect it dynamically before calling the resolver.
|
|
1330
1314
|
|
|
1331
1315
|
## Performance
|
|
1332
1316
|
|
|
@@ -1361,3 +1345,4 @@ Throughput (ops/s)
|
|
|
1361
1345
|
## License
|
|
1362
1346
|
|
|
1363
1347
|
This project is licensed under the MIT License. See the [LICENSE](/LICENSE) file for details.
|
|
1348
|
+
|