@via-profit/ability 3.5.0 → 3.5.1
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 +336 -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,7 @@
|
|
|
1
|
-
|
|
1
|
+
```markdown
|
|
2
|
+
# @via-profit/ability
|
|
2
3
|
|
|
3
|
-
> A set of services
|
|
4
|
+
> A set of services partially implementing the [Attribute Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control) principle.
|
|
4
5
|
> The package allows you to describe rules, combine them into groups, form policies, and apply them to data to determine permissions.
|
|
5
6
|
|
|
6
7
|

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

|
|
12
13
|

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