@via-profit/ability 3.1.0 → 3.1.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 +384 -414
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,35 +1,38 @@
|
|
|
1
1
|
# @via-profit/Ability
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
> Пакет позволяет описывать правила, объединять их в группы, формировать политики и применять их к данным для определения разрешений.
|
|
3
|
+
> A set of services that partially implement the [Attribute Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control) principle.
|
|
4
|
+
> The package allows you to describe rules, combine them into groups, form policies, and apply them to data to determine permissions.
|
|
6
5
|
|
|
7
|
-
##
|
|
6
|
+
## Language / Язык
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
- [🇬🇧 English](/docs/en/README.md)
|
|
9
|
+
- [🇷🇺 Русский](/docs/ru/README.md)
|
|
11
10
|
|
|
12
|
-
##
|
|
11
|
+
## Purpose
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- [Environment политик](#environment-политик)
|
|
19
|
-
- [Генератор типов для TypeScript](#генератор-типов-для-typescript)
|
|
20
|
-
- [Отладка политик](#отладка-политик)
|
|
21
|
-
- [Решение проблем](#решение-проблем)
|
|
22
|
-
- [Рекомендации по проектированию](#рекомендации-по-проектированию)
|
|
23
|
-
- [Примеры](#примеры)
|
|
24
|
-
- [Производительность](#производительность)
|
|
25
|
-
- [Api-Reference](./docs/ru/api.md)
|
|
13
|
+
The package is intended as a **lightweight and extremely simple alternative** to heavy access control systems.
|
|
14
|
+
Without complex configurations, without dependencies — just a minimal set of tools that allows you to describe rules and policies in a maximally simple DSL.
|
|
15
|
+
|
|
16
|
+
## Table of Contents
|
|
26
17
|
|
|
18
|
+
- [Quick Start](#quick-start)
|
|
19
|
+
- [Fundamentals](#fundamentals)
|
|
20
|
+
- [DSL](#dsl)
|
|
21
|
+
- [Combining Policies](#combining-policies)
|
|
22
|
+
- [Policy Environment](#policy-environment)
|
|
23
|
+
- [TypeScript Type Generator](#typescript-type-generator)
|
|
24
|
+
- [Policy Debugging](#policy-debugging)
|
|
25
|
+
- [Troubleshooting](#troubleshooting)
|
|
26
|
+
- [Design Recommendations](#design-recommendations)
|
|
27
|
+
- [Examples](#examples)
|
|
28
|
+
- [Performance](#performance)
|
|
29
|
+
- [API Reference](./api.md)
|
|
27
30
|
|
|
28
|
-
##
|
|
31
|
+
## Quick Start
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
Install the package, write DSL, call the parser, and run the resolver.
|
|
31
34
|
|
|
32
|
-
###
|
|
35
|
+
### Installation
|
|
33
36
|
|
|
34
37
|
```bash
|
|
35
38
|
npm install @via-profit/ability
|
|
@@ -43,10 +46,9 @@ yarn add @via-profit/ability
|
|
|
43
46
|
pnpm add @via-profit/ability
|
|
44
47
|
```
|
|
45
48
|
|
|
49
|
+
### Example: Deny access to `passwordHash` for everyone except the owner
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Допустим, у нас есть пользовательские данные:
|
|
51
|
+
Suppose we have user data:
|
|
50
52
|
|
|
51
53
|
```ts
|
|
52
54
|
const user = {
|
|
@@ -56,30 +58,29 @@ const user = {
|
|
|
56
58
|
};
|
|
57
59
|
```
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
We need to deny reading `passwordHash` to everyone except the user themselves.
|
|
60
62
|
|
|
61
|
-
#### DSL
|
|
63
|
+
#### DSL Policy
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
In the policy language, this looks like:
|
|
64
66
|
|
|
65
67
|
```
|
|
66
68
|
deny permission.user.passwordHash if any:
|
|
67
69
|
viewer.id is not equals owner.id
|
|
68
70
|
```
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- `deny` — эффект политики (запретить доступ)
|
|
73
|
-
- `permission.user.passwordHash` — ключ разрешения.
|
|
74
|
-
- `if any:` — начало блока условий
|
|
75
|
-
- `viewer.id is not equals owner.id` — правило: если идентификатор запрашивающего не равен идентификатору владельца
|
|
72
|
+
**Explanation:**
|
|
76
73
|
|
|
74
|
+
- `deny` — policy effect (deny access)
|
|
75
|
+
- `permission.user.passwordHash` — permission key.
|
|
76
|
+
- `if any:` — start of the condition block
|
|
77
|
+
- `viewer.id is not equals owner.id` — rule: if the requester's ID is not equal to the owner's ID
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
If `viewer.id` is not equal to `owner.id`, the rule is 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.
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
*Note: The permission key is formed according to the principle: `permission.` + your custom key in dot notation. For example, the key `foo.bar.baz` in DSL would be `permission.foo.bar.baz`.*
|
|
81
82
|
|
|
82
|
-
####
|
|
83
|
+
#### Check in Code
|
|
83
84
|
|
|
84
85
|
```ts
|
|
85
86
|
import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
|
|
@@ -89,28 +90,28 @@ deny permission.user.passwordHash if any:
|
|
|
89
90
|
viewer.id is not equals owner.id
|
|
90
91
|
`;
|
|
91
92
|
|
|
92
|
-
const policies = new AbilityDSLParser(dsl).parse(); //
|
|
93
|
-
const resolver = new AbilityResolver(policies); //
|
|
93
|
+
const policies = new AbilityDSLParser(dsl).parse(); // obtain policies
|
|
94
|
+
const resolver = new AbilityResolver(policies); // create resolver
|
|
94
95
|
|
|
95
96
|
resolver.enforce('user.passwordHash', {
|
|
96
97
|
viewer: { id: '1' },
|
|
97
98
|
owner: { id: '2' },
|
|
98
|
-
}); //
|
|
99
|
+
}); // will throw an error — access denied
|
|
99
100
|
```
|
|
100
|
-
|
|
101
|
+
In `enforce`, the key is passed without the `permission.` prefix — it is automatically removed by the parser.
|
|
101
102
|
|
|
102
|
-
##
|
|
103
|
+
## Fundamentals
|
|
103
104
|
|
|
104
|
-
|
|
105
|
+
Let’s briefly list the key points you need to know before starting to use the package:
|
|
105
106
|
|
|
106
|
-
1.
|
|
107
|
-
2.
|
|
108
|
-
3.
|
|
109
|
-
4.
|
|
110
|
-
5.
|
|
111
|
-
6.
|
|
112
|
-
7.
|
|
113
|
-
8.
|
|
107
|
+
1. The resolver (`AbilityResolver`) follows the **Default Deny** principle. This means that if no policy matches, the result is `deny` ([more details here](#troubleshooting)). To avoid unexpected `deny`, ensure there is at least one `permit` policy that can match. Only then add `deny` policies.
|
|
108
|
+
2. Policies are applied sequentially. If multiple policies match, the result is determined by the last matching policy.
|
|
109
|
+
3. Rules are executed sequentially.
|
|
110
|
+
4. In a rule set (`RuleSet`) with the `all` comparison operator, further rule execution stops as soon as the first rule returns `mismatch`.
|
|
111
|
+
5. Use [DSL](#dsl) to compose policies — it's simpler and more convenient.
|
|
112
|
+
6. For storing policies on the server, use JSON. Policies can be exported to JSON and imported from JSON.
|
|
113
|
+
7. Generally, rely on the principle: if permission is not explicitly granted → access is denied.
|
|
114
|
+
8. Use the built-in cache only if your policies are incredibly complex and contain a large number of rules.
|
|
114
115
|
|
|
115
116
|
---
|
|
116
117
|
|
|
@@ -118,28 +119,28 @@ resolver.enforce('user.passwordHash', {
|
|
|
118
119
|
|
|
119
120
|
> DSL - Domain-Specific Language
|
|
120
121
|
|
|
121
|
-
Ability DSL
|
|
122
|
-
|
|
122
|
+
Ability DSL is a declarative language for describing access policies.
|
|
123
|
+
It allows you to define rules in a human-readable form using simple constructs: *policies*, *groups*, *rules*, and *annotations*.
|
|
123
124
|
|
|
124
|
-
###
|
|
125
|
+
### Policy Structure
|
|
125
126
|
|
|
126
|
-
|
|
127
|
+
A policy consists of:
|
|
127
128
|
|
|
128
129
|
```
|
|
129
130
|
<effect> <permission> if <all|any>:
|
|
130
131
|
<group>...
|
|
131
132
|
```
|
|
132
133
|
|
|
133
|
-
|
|
134
|
+
Where:
|
|
134
135
|
|
|
135
|
-
- **effect** — `permit`
|
|
136
|
-
- **permission** —
|
|
137
|
-
- **if all:** —
|
|
138
|
-
- **if any:** —
|
|
136
|
+
- **effect** — `permit` or `deny`
|
|
137
|
+
- **permission** — a string of the form `permission.foo.bar`, where the `permission.` prefix is mandatory.
|
|
138
|
+
- **if all:** — all groups must be true
|
|
139
|
+
- **if any:** — at least one group must be true
|
|
139
140
|
|
|
140
|
-
|
|
141
|
+
A policy can contain one or more rule groups.
|
|
141
142
|
|
|
142
|
-
|
|
143
|
+
Example:
|
|
143
144
|
|
|
144
145
|
```dsl
|
|
145
146
|
permit permission.order.update if any:
|
|
@@ -152,38 +153,36 @@ permit permission.order.update if any:
|
|
|
152
153
|
user.login is equals 'dev'
|
|
153
154
|
```
|
|
154
155
|
|
|
155
|
-
>
|
|
156
|
+
> The `permission.` prefix is mandatory in DSL but is automatically removed by the parser. Internally, the permission is stored as `order.update`.
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
1. user.roles
|
|
159
|
-
2. user.roles
|
|
158
|
+
The example policy above says: permission `order.update` will be allowed if one of two conditions is met:
|
|
159
|
+
1. `user.roles` contains 'admin' **and** `user.token` is not null
|
|
160
|
+
2. `user.roles` contains 'developer' **or** `user.login` equals 'dev'
|
|
160
161
|
|
|
161
|
-
###
|
|
162
|
+
### Permission Key
|
|
162
163
|
|
|
163
|
-
|
|
164
|
-
помощи символа `*`. Это позволяет группировать ключи, а так же переопределять политики с похожими ключами.
|
|
164
|
+
Permission keys are written in dot notation but support the use of wildcard patterns with the `*` character. This allows grouping of keys and overriding policies with similar keys.
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
If multiple policies match a key, **all of them are executed**. The final result is determined by the **last matching policy**:
|
|
167
167
|
|
|
168
|
+
**Example of using wildcards**
|
|
168
169
|
|
|
169
|
-
|
|
170
|
+
| Policy (permission) | Key | Matches |
|
|
171
|
+
|---------------------|-----------------------|---------|
|
|
172
|
+
| `order.*` | `order.create` | yes |
|
|
173
|
+
| `order.*` | `order.update` | yes |
|
|
174
|
+
| `order.*` | `user.create` | no |
|
|
175
|
+
| `*.create` | `order.create` | yes |
|
|
176
|
+
| `*.create` | `user.create` | yes |
|
|
177
|
+
| `*.create` | `order.update` | no |
|
|
178
|
+
| `user.profile.*` | `user.profile.update` | yes |
|
|
179
|
+
| `user.profile.*` | `user.settings.update`| no |
|
|
170
180
|
|
|
171
|
-
|
|
172
|
-
|-------------------|------------------------|-----------|
|
|
173
|
-
| `order.*` | `order.create` | да |
|
|
174
|
-
| `order.*` | `order.update` | да |
|
|
175
|
-
| `order.*` | `user.create` | нет |
|
|
176
|
-
| `*.create` | `order.create` | да |
|
|
177
|
-
| `*.create` | `user.create` | да |
|
|
178
|
-
| `*.create` | `order.update` | нет |
|
|
179
|
-
| `user.profile.*` | `user.profile.update` | да |
|
|
180
|
-
| `user.profile.*` | `user.settings.update` | нет |
|
|
181
|
-
|
|
182
|
-
**Пример политики с wildcard**
|
|
181
|
+
**Example of a policy with wildcard**
|
|
183
182
|
```ts
|
|
184
183
|
import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
|
|
185
184
|
|
|
186
|
-
// DSL
|
|
185
|
+
// DSL is not complete, shown for illustration only
|
|
187
186
|
const dsl = `
|
|
188
187
|
permit permission.order.*
|
|
189
188
|
deny permission.order.update
|
|
@@ -192,21 +191,20 @@ deny permission.order.update
|
|
|
192
191
|
const policies = new AbilityDSLParser(dsl).parse();
|
|
193
192
|
const resolver = new AbilityResolver(policies);
|
|
194
193
|
|
|
195
|
-
await resolver.enforce('order.update', resource); //
|
|
196
|
-
|
|
194
|
+
await resolver.enforce('order.update', resource); // will throw AbilityError
|
|
197
195
|
```
|
|
198
196
|
|
|
199
|
-
|
|
197
|
+
**Explanation**
|
|
200
198
|
|
|
201
|
-
|
|
202
|
-
|
|
199
|
+
In DSL, the order of policies matters:
|
|
200
|
+
the last matching policy wins.
|
|
203
201
|
|
|
204
|
-
|
|
202
|
+
Therefore:
|
|
205
203
|
|
|
206
|
-
1. `permit` `permission.order.*`
|
|
207
|
-
2. `deny` `permission.order.update`
|
|
204
|
+
1. `permit` `permission.order.*` allows everything that starts with `order.`
|
|
205
|
+
2. `deny` `permission.order.update` overrides this permission.
|
|
208
206
|
|
|
209
|
-
|
|
207
|
+
Execution result:
|
|
210
208
|
|
|
211
209
|
```
|
|
212
210
|
order.update → deny
|
|
@@ -215,30 +213,29 @@ order.delete → permit
|
|
|
215
213
|
order.view → permit
|
|
216
214
|
```
|
|
217
215
|
|
|
216
|
+
### Comments
|
|
218
217
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
Строки, начинающиеся с символа `#` считаются комментариями и не влияют на результат работы правил и политик.
|
|
218
|
+
Lines starting with the `#` symbol are considered comments and do not affect the evaluation of rules and policies.
|
|
222
219
|
|
|
223
220
|
---
|
|
224
221
|
|
|
225
|
-
###
|
|
222
|
+
### Annotations
|
|
226
223
|
|
|
227
|
-
|
|
224
|
+
Currently, only one annotation is supported: `name`, which will be used as the name for a policy, rule group, or rule.
|
|
228
225
|
|
|
229
|
-
|
|
226
|
+
Annotations are specified via comments:
|
|
230
227
|
|
|
231
228
|
```
|
|
232
|
-
# @name
|
|
229
|
+
# @name <name>
|
|
233
230
|
```
|
|
234
231
|
|
|
235
|
-
|
|
232
|
+
Annotations apply to the **following entity**:
|
|
236
233
|
|
|
237
|
-
-
|
|
238
|
-
-
|
|
239
|
-
-
|
|
234
|
+
- policy
|
|
235
|
+
- group
|
|
236
|
+
- rule
|
|
240
237
|
|
|
241
|
-
|
|
238
|
+
Example:
|
|
242
239
|
|
|
243
240
|
```dsl
|
|
244
241
|
# @name can order update
|
|
@@ -251,9 +248,9 @@ permit permission.order.update if any:
|
|
|
251
248
|
|
|
252
249
|
---
|
|
253
250
|
|
|
254
|
-
###
|
|
251
|
+
### Rule Groups
|
|
255
252
|
|
|
256
|
-
|
|
253
|
+
A group defines how the rules within it are combined:
|
|
257
254
|
|
|
258
255
|
```
|
|
259
256
|
all of:
|
|
@@ -265,17 +262,16 @@ any of:
|
|
|
265
262
|
<rule>
|
|
266
263
|
```
|
|
267
264
|
|
|
268
|
-
- `all of:` —
|
|
269
|
-
- `any of:` —
|
|
270
|
-
|
|
271
|
-
`all of` - значит, что группа считается выполненной, если все правила внутри группы сработали.
|
|
265
|
+
- `all of:` — logical AND
|
|
266
|
+
- `any of:` — logical OR
|
|
272
267
|
|
|
273
|
-
`
|
|
268
|
+
`all of` means that the group is considered satisfied if all rules within the group match.
|
|
274
269
|
|
|
275
|
-
|
|
270
|
+
`any of` means that the group is considered satisfied if at least one rule within the group matches.
|
|
276
271
|
|
|
272
|
+
Each group within a policy will be evaluated independently of other groups. The final result is determined by comparing the results of all groups in the policy.
|
|
277
273
|
|
|
278
|
-
|
|
274
|
+
Groups can have annotations:
|
|
279
275
|
|
|
280
276
|
```dsl
|
|
281
277
|
# @name developer group
|
|
@@ -285,19 +281,19 @@ any of:
|
|
|
285
281
|
|
|
286
282
|
---
|
|
287
283
|
|
|
288
|
-
###
|
|
284
|
+
### Rules
|
|
289
285
|
|
|
290
|
-
|
|
286
|
+
A rule is an atomic condition inside a policy. It defines under what data the policy is considered matched. Rules set the conditions that determine the effectiveness of a policy (`permit` or `deny`).
|
|
291
287
|
|
|
292
|
-
|
|
288
|
+
A rule has the form:
|
|
293
289
|
|
|
294
290
|
```
|
|
295
|
-
<subject> <operator> <value?> —
|
|
291
|
+
<subject> <operator> <value?> — the value is not required for some operators (e.g., `is null` does not require a value).
|
|
296
292
|
```
|
|
297
293
|
|
|
298
|
-
#### Subject
|
|
294
|
+
#### Subject
|
|
299
295
|
|
|
300
|
-
|
|
296
|
+
Identifier in dot notation:
|
|
301
297
|
|
|
302
298
|
```
|
|
303
299
|
user.roles
|
|
@@ -305,101 +301,97 @@ env.time.hour
|
|
|
305
301
|
order.total
|
|
306
302
|
```
|
|
307
303
|
|
|
308
|
-
#### Operators
|
|
304
|
+
#### Operators
|
|
309
305
|
|
|
310
|
-
|
|
306
|
+
*Synonyms are alternative forms of writing that are also supported by the parser.*
|
|
311
307
|
|
|
312
|
-
|
|
308
|
+
**Basic Comparison Operators**
|
|
313
309
|
|
|
314
|
-
|
|
|
315
|
-
|
|
316
|
-
| **is equals** | `=`, `==`, `equals` | `age is equals 18` |
|
|
317
|
-
| **is not equals** | `!=`, `<>`, `not equals` | `role is not equals 'admin'` |
|
|
318
|
-
| **greater than** | `>`, `gt` | `age greater than 18` |
|
|
319
|
-
| **greater than or equal** | `>=`, `gte` | `age greater than or equal 18` |
|
|
320
|
-
| **less than** | `<`, `lt` | `age less than 18` |
|
|
321
|
-
| **less than or equal** | `<=`, `lte` | `age less than or equal 18` |
|
|
310
|
+
| DSL Operator | Synonyms | Example | Description | Types |
|
|
311
|
+
|--------------|----------|---------|-------------|-------|
|
|
312
|
+
| **is equals** | `=`, `==`, `equals` | `age is equals 18` | Strict equality | number, string, boolean |
|
|
313
|
+
| **is not equals** | `!=`, `<>`, `not equals` | `role is not equals 'admin'` | Strict inequality | number, string, boolean |
|
|
314
|
+
| **greater than** | `>`, `gt` | `age greater than 18` | Greater than | number, date |
|
|
315
|
+
| **greater than or equal** | `>=`, `gte` | `age greater than or equal 18` | Greater than or equal | number, date |
|
|
316
|
+
| **less than** | `<`, `lt` | `age less than 18` | Less than | number, date |
|
|
317
|
+
| **less than or equal** | `<=`, `lte` | `age less than or equal 18` | Less than or equal | number, date |
|
|
322
318
|
|
|
319
|
+
**Null Operators**
|
|
323
320
|
|
|
324
|
-
|
|
321
|
+
| DSL Operator | Synonyms | Example | Description | Types |
|
|
322
|
+
|--------------|----------|---------|-------------|-------|
|
|
323
|
+
| **is null** | `== null`, `= null` | `middleName is null` | Value is absent | any |
|
|
324
|
+
| **is not null** | `!= null` | `middleName is not null` | Value is present | any |
|
|
325
325
|
|
|
326
|
-
|
|
327
|
-
|--------------|----------|--------|----------|------|
|
|
328
|
-
| **is null** | `== null`, `= null` | `middleName is null` | Значение отсутствует | any |
|
|
329
|
-
| **is not null** | `!= null` | `middleName is not null` | Значение присутствует | any |
|
|
326
|
+
**Operators for Lists (Arrays)**
|
|
330
327
|
|
|
331
|
-
|
|
328
|
+
| DSL Operator | Synonyms | Example | Description | Types |
|
|
329
|
+
|--------------|---------------------------|---------|-------------|-------|
|
|
330
|
+
| **in [...]** | - | `role in ['admin', 'manager']` | Value is in the list | number, string |
|
|
331
|
+
| **not in [...]** | - | `role not in ['banned']` | Value is not in the list | number, string |
|
|
332
|
+
| **contains** | `includes`, `has` | `tags contains 'vip'` | Array contains the element | array |
|
|
333
|
+
| **not contains** | `not includes`, `not has` | `tags not contains 'vip'` | Array does not contain the element | array |
|
|
332
334
|
|
|
333
|
-
|
|
334
|
-
|--------------|---------------------------|--------|----------|------|
|
|
335
|
-
| **in [...]** | - | `role in ['admin', 'manager']` | Значение входит в список | number, string |
|
|
336
|
-
| **not in [...]** | - | `role not in ['banned']` | Значение не входит | number, string |
|
|
337
|
-
| **contains** | `includes`, `has` | `tags contains 'vip'` | Массив содержит элемент | array |
|
|
338
|
-
| **not contains** | `not includes`, `not has` | `tags not contains 'vip'` | Массив не содержит элемент | array |
|
|
335
|
+
**String Operators**
|
|
339
336
|
|
|
337
|
+
| DSL Operator | Synonyms | Example | Description | Types |
|
|
338
|
+
|--------------|----------|---------|-------------|-------|
|
|
339
|
+
| **starts with** | `begins with` | `email starts with 'admin@'` | String starts with | string |
|
|
340
|
+
| **not starts with** | — | `email not starts with 'test'` | String does not start with | string |
|
|
341
|
+
| **ends with** | — | `email ends with '.ru'` | String ends with | string |
|
|
342
|
+
| **not ends with** | — | `email not ends with '.com'` | String does not end with | string |
|
|
343
|
+
| **includes** | `contains substring` | `name includes 'lex'` | String contains substring | string |
|
|
344
|
+
| **not includes** | — | `name not includes 'test'` | String does not contain substring | string |
|
|
340
345
|
|
|
341
|
-
|
|
346
|
+
**Boolean Operators**
|
|
342
347
|
|
|
343
|
-
|
|
|
344
|
-
|
|
345
|
-
| **
|
|
346
|
-
| **
|
|
347
|
-
| **ends with** | — | `email ends with '.ru'` | Строка заканчивается на | string |
|
|
348
|
-
| **not ends with** | — | `email not ends with '.com'` | Строка не заканчивается на | string |
|
|
349
|
-
| **includes** | `contains substring` | `name includes 'lex'` | Строка содержит подстроку | string |
|
|
350
|
-
| **not includes** | — | `name not includes 'test'` | Строка не содержит подстроку | string |
|
|
348
|
+
| DSL Operator | Synonyms | Example | Description | Types |
|
|
349
|
+
|--------------|----------|---------|-------------|-------|
|
|
350
|
+
| **is true** | `= true` | `isActive is true` | Value is true | boolean |
|
|
351
|
+
| **is false** | `= false` | `isActive is false` | Value is false | boolean |
|
|
351
352
|
|
|
352
|
-
|
|
353
|
+
**Length Operators**
|
|
353
354
|
|
|
354
|
-
|
|
|
355
|
-
|
|
356
|
-
| **
|
|
357
|
-
| **
|
|
355
|
+
| DSL Operator | Synonyms | Example | Description | Types |
|
|
356
|
+
|--------------|----------|---------|-------------|-------|
|
|
357
|
+
| **length equals** | `len =` | `tags length equals 3` | Length equals | array, string |
|
|
358
|
+
| **length greater than** | `len >` | `tags length greater than 2` | Length greater than | array, string |
|
|
359
|
+
| **length less than** | `len <` | `tags length less than 5` | Length less than | array, string |
|
|
358
360
|
|
|
359
|
-
|
|
361
|
+
#### Value
|
|
360
362
|
|
|
361
|
-
|
|
362
|
-
|--------------|----------|--------|----------|------|
|
|
363
|
-
| **length equals** | `len =` | `tags length equals 3` | Длина равна | array, string |
|
|
364
|
-
| **length greater than** | `len >` | `tags length greater than 2` | Длина больше | array, string |
|
|
365
|
-
| **length less than** | `len <` | `tags length less than 5` | Длина меньше | array, string |
|
|
363
|
+
Supported values:
|
|
366
364
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
- строки `'text'`
|
|
372
|
-
- числа `42`
|
|
373
|
-
- булевы `true` / `false`
|
|
365
|
+
- strings `'text'`
|
|
366
|
+
- numbers `42`
|
|
367
|
+
- booleans `true` / `false`
|
|
374
368
|
- `null`
|
|
375
|
-
-
|
|
369
|
+
- arrays `[1, 2, 3]` / `['foo', false, null, 1, 2, '999']`
|
|
376
370
|
|
|
377
|
-
|
|
371
|
+
Examples:
|
|
378
372
|
|
|
379
373
|
```dsl
|
|
380
|
-
#
|
|
374
|
+
# user age greater than 18
|
|
381
375
|
user.age greater than 18
|
|
382
376
|
|
|
383
|
-
#
|
|
377
|
+
# array of roles contains the role 'admin'
|
|
384
378
|
user.roles contains 'admin'
|
|
385
379
|
|
|
386
|
-
#
|
|
380
|
+
# order tag is either 'vip' or 'priority'
|
|
387
381
|
order.tag in ['vip', 'priority']
|
|
388
382
|
|
|
389
|
-
#
|
|
383
|
+
# user token is not null
|
|
390
384
|
user.token is not null
|
|
391
385
|
|
|
392
|
-
#
|
|
386
|
+
# user login is longer than 12 characters
|
|
393
387
|
user.login length greater than 12
|
|
394
388
|
```
|
|
395
389
|
|
|
396
|
-
|
|
397
|
-
|
|
398
390
|
---
|
|
399
391
|
|
|
400
|
-
###
|
|
392
|
+
### Implicit Group
|
|
401
393
|
|
|
402
|
-
|
|
394
|
+
If rules are written without `all of:` or `any of:`, they are combined using the policy operator:
|
|
403
395
|
|
|
404
396
|
```dsl
|
|
405
397
|
permit permission.order.update if all:
|
|
@@ -407,7 +399,7 @@ permit permission.order.update if all:
|
|
|
407
399
|
user.token is not null
|
|
408
400
|
```
|
|
409
401
|
|
|
410
|
-
|
|
402
|
+
Equivalent to:
|
|
411
403
|
|
|
412
404
|
```dsl
|
|
413
405
|
permit permission.order.update if all:
|
|
@@ -416,48 +408,46 @@ permit permission.order.update if all:
|
|
|
416
408
|
user.token is not null
|
|
417
409
|
```
|
|
418
410
|
|
|
419
|
-
|
|
411
|
+
The implicit group always matches the policy operator (`if all` or `if any`).
|
|
420
412
|
|
|
421
413
|
---
|
|
422
414
|
|
|
423
|
-
###
|
|
415
|
+
### Complete Example
|
|
424
416
|
|
|
425
417
|
```dsl
|
|
426
|
-
# @name
|
|
418
|
+
# @name order update allowed
|
|
427
419
|
permit permission.order.update if any:
|
|
428
420
|
|
|
429
|
-
# @name
|
|
421
|
+
# @name if admin
|
|
430
422
|
all of:
|
|
431
423
|
user.roles contains 'admin'
|
|
432
424
|
user.token is not null
|
|
433
425
|
|
|
434
|
-
# @name
|
|
426
|
+
# @name if developer
|
|
435
427
|
any of:
|
|
436
428
|
user.roles contains 'developer'
|
|
437
429
|
user.login is equals 'dev'
|
|
438
430
|
```
|
|
439
431
|
|
|
432
|
+
## Combining Policies
|
|
440
433
|
|
|
434
|
+
In a real project, you should use multiple policies at once.
|
|
441
435
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
В реальном проекте следует использовать несколько политик сразу
|
|
436
|
+
TODO: using multiple policies
|
|
445
437
|
|
|
446
|
-
|
|
438
|
+
## Policy Environment
|
|
447
439
|
|
|
448
|
-
|
|
440
|
+
**Environment** is an object containing context data that does not belong to either the user or the resource.
|
|
441
|
+
The content of the object is defined by the developer and can be any object consisting of primitives.
|
|
449
442
|
|
|
450
|
-
|
|
451
|
-
|
|
443
|
+
- request time,
|
|
444
|
+
- IP address,
|
|
445
|
+
- device parameters,
|
|
446
|
+
- request headers,
|
|
447
|
+
- session context,
|
|
448
|
+
- any other external conditions.
|
|
452
449
|
|
|
453
|
-
|
|
454
|
-
- IP‑адрес,
|
|
455
|
-
- параметры устройства,
|
|
456
|
-
- заголовки запроса,
|
|
457
|
-
- контекст сессии,
|
|
458
|
-
- любые другие внешние условия.
|
|
459
|
-
|
|
460
|
-
**Примеры:**
|
|
450
|
+
**Examples:**
|
|
461
451
|
|
|
462
452
|
```ts
|
|
463
453
|
type Environment = {
|
|
@@ -471,18 +461,18 @@ type Environment = {
|
|
|
471
461
|
};
|
|
472
462
|
```
|
|
473
463
|
|
|
474
|
-
Environment
|
|
464
|
+
Environment is passed to `resolve()` and `enforce()` as the third argument:
|
|
475
465
|
|
|
476
466
|
```ts
|
|
477
467
|
await resolver.resolve('order.update', resource, environment);
|
|
478
468
|
await resolver.enforce('order.update', resource, environment);
|
|
479
469
|
```
|
|
480
470
|
|
|
481
|
-
###
|
|
471
|
+
### Using environment in rules
|
|
482
472
|
|
|
483
|
-
|
|
473
|
+
In a policy, you can refer to environment via the `env.*` path.
|
|
484
474
|
|
|
485
|
-
|
|
475
|
+
Example policy that denies order updates at night (10 PM – 6 AM):
|
|
486
476
|
|
|
487
477
|
```dsl
|
|
488
478
|
# @name Deny updates at night
|
|
@@ -491,15 +481,15 @@ deny permission.order.update if all:
|
|
|
491
481
|
env.time.hour greater or equal than 22
|
|
492
482
|
```
|
|
493
483
|
|
|
494
|
-
|
|
484
|
+
**Retrieving values from environment**
|
|
495
485
|
|
|
496
|
-
|
|
486
|
+
If a path is specified in a rule:
|
|
497
487
|
|
|
498
|
-
- `env.*` →
|
|
499
|
-
- `user.*`, `order.*`, `profile.*` →
|
|
500
|
-
-
|
|
488
|
+
- `env.*` → value is taken from environment
|
|
489
|
+
- `user.*`, `order.*`, `profile.*` → from resource
|
|
490
|
+
- literal (`18`, `"admin"`, `true`) → used as is
|
|
501
491
|
|
|
502
|
-
|
|
492
|
+
Example:
|
|
503
493
|
|
|
504
494
|
```ts
|
|
505
495
|
subject: "env.geo.country"
|
|
@@ -507,31 +497,29 @@ resource: "user.country"
|
|
|
507
497
|
condition: "equal"
|
|
508
498
|
```
|
|
509
499
|
|
|
510
|
-
### Environment
|
|
500
|
+
### Environment in TypeScript
|
|
511
501
|
|
|
512
|
-
|
|
502
|
+
The Environment type is set at the `AbilityResolver` level:
|
|
513
503
|
|
|
514
504
|
```ts
|
|
515
505
|
const resolver = new AbilityResolver<Resources, Environment>(policies);
|
|
516
506
|
```
|
|
517
507
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
- получать автодополнение в IDE,
|
|
521
|
-
- проверять корректность путей `env.*`,
|
|
522
|
-
- избегать ошибок при передаче environment.
|
|
508
|
+
This allows:
|
|
523
509
|
|
|
524
|
-
|
|
510
|
+
- getting autocompletion in IDE,
|
|
511
|
+
- checking the correctness of `env.*` paths,
|
|
512
|
+
- avoiding errors when passing environment.
|
|
525
513
|
|
|
514
|
+
> If a rule uses `env.*` but environment is not passed, then the value of `env.*` will be `undefined`, and the comparison will be performed as if the environment were absent.
|
|
526
515
|
|
|
516
|
+
## TypeScript Type Generator
|
|
527
517
|
|
|
528
|
-
|
|
518
|
+
`AbilityParser.generateTypeDefs()` generates TypeScript types based on policies, allowing you to avoid discrepancies between types and data in policies.
|
|
529
519
|
|
|
530
|
-
|
|
520
|
+
**Usage Example**
|
|
531
521
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
Сначала необходимо подготовить массив политик. Политики можно хранить в DSL или в JSON и парсить их в массив готовых политик. В данном примере, для наглядности, политики хранятся в DSL.
|
|
522
|
+
First, you need to prepare an array of policies. Policies can be stored in DSL or JSON and parsed into an array of ready-made policies. In this example, for clarity, policies are stored in DSL.
|
|
535
523
|
|
|
536
524
|
```ts
|
|
537
525
|
// scripts/policies.ts
|
|
@@ -564,7 +552,7 @@ const typedefs = AbilityParser.generateTypeDefs(policies);
|
|
|
564
552
|
writeFileSync('./src/ability/types.generated.ts', typedefs, 'utf8');
|
|
565
553
|
```
|
|
566
554
|
|
|
567
|
-
|
|
555
|
+
**Generated File (example)**
|
|
568
556
|
|
|
569
557
|
```ts
|
|
570
558
|
// src/ability/types.generated.ts
|
|
@@ -583,7 +571,7 @@ export type Resources = {
|
|
|
583
571
|
};
|
|
584
572
|
```
|
|
585
573
|
|
|
586
|
-
|
|
574
|
+
**Usage in code**
|
|
587
575
|
|
|
588
576
|
```ts
|
|
589
577
|
import { AbilityResolver, AbilityPolicy } from '@via-profit/ability';
|
|
@@ -599,19 +587,19 @@ await resolver.enforce('order.update', {
|
|
|
599
587
|
});
|
|
600
588
|
```
|
|
601
589
|
|
|
602
|
-
##
|
|
590
|
+
## Policy Debugging
|
|
603
591
|
|
|
604
|
-
###
|
|
592
|
+
### Explanations
|
|
605
593
|
|
|
606
|
-
|
|
594
|
+
To simplify policy debugging, a special `AbilityResult` class is used, which is already included in the final evaluation result. `AbilityResult` encapsulates the outcome of applying all matching policies to a permission key and resource.
|
|
607
595
|
|
|
608
|
-
`AbilityResult`
|
|
596
|
+
`AbilityResult` contains:
|
|
609
597
|
|
|
610
|
-
-
|
|
611
|
-
-
|
|
612
|
-
-
|
|
598
|
+
- a list of evaluated policies,
|
|
599
|
+
- methods to determine the final effect,
|
|
600
|
+
- methods to get explanations in textual representation.
|
|
613
601
|
|
|
614
|
-
|
|
602
|
+
Example:
|
|
615
603
|
|
|
616
604
|
```ts
|
|
617
605
|
const result = await resolver.resolve('order.update', resource);
|
|
@@ -627,14 +615,14 @@ const explanations = result.explain(); // AbilityExplain
|
|
|
627
615
|
|
|
628
616
|
### AbilityExplain
|
|
629
617
|
|
|
630
|
-
`AbilityExplain`
|
|
618
|
+
`AbilityExplain` and related classes (`AbilityExplainPolicy`, `AbilityExplainRuleSet`, `AbilityExplainRule`) allow you to get a human-readable explanation:
|
|
631
619
|
|
|
632
|
-
-
|
|
633
|
-
-
|
|
634
|
-
-
|
|
635
|
-
-
|
|
620
|
+
- which policy matched,
|
|
621
|
+
- which rule groups matched,
|
|
622
|
+
- which rules did not pass,
|
|
623
|
+
- which effect was applied.
|
|
636
624
|
|
|
637
|
-
|
|
625
|
+
Usage example:
|
|
638
626
|
|
|
639
627
|
```ts
|
|
640
628
|
const result = await resolver.resolve('order.update', resource);
|
|
@@ -643,31 +631,30 @@ const explanations = result.explain();
|
|
|
643
631
|
console.log(explanations.toString());
|
|
644
632
|
```
|
|
645
633
|
|
|
646
|
-
|
|
634
|
+
Example output:
|
|
647
635
|
|
|
648
636
|
```
|
|
649
|
-
✓ policy
|
|
650
|
-
✓ ruleSet
|
|
651
|
-
✓ rule
|
|
652
|
-
✗ rule
|
|
653
|
-
✓ ruleSet
|
|
654
|
-
✓ rule
|
|
637
|
+
✓ policy «Deny order update for managers» is match
|
|
638
|
+
✓ ruleSet «Managers» is match
|
|
639
|
+
✓ rule «Department managers» is match
|
|
640
|
+
✗ rule «Role manager» is mismatch
|
|
641
|
+
✓ ruleSet «Not administrators» is match
|
|
642
|
+
✓ rule «No role administrator» is match
|
|
655
643
|
```
|
|
656
644
|
|
|
657
|
-
###
|
|
658
|
-
|
|
659
|
-
В настоящий момент поддерживается только один формат вывода - текстовый.
|
|
645
|
+
### Output Format
|
|
660
646
|
|
|
661
|
-
|
|
647
|
+
Currently, only one output format is supported — textual.
|
|
662
648
|
|
|
649
|
+
The output follows the principle: `<policy | ruleSet | rule> <name> <is match | is mismatch>`
|
|
663
650
|
|
|
664
|
-
##
|
|
651
|
+
## Troubleshooting
|
|
665
652
|
|
|
666
|
-
###
|
|
653
|
+
### Decision‑Making Model (Default Deny)
|
|
667
654
|
|
|
668
|
-
>
|
|
655
|
+
> Why does a `deny` policy not turn into `permit` if its conditions are not met?
|
|
669
656
|
|
|
670
|
-
|
|
657
|
+
Consider a policy that **denies** access to a user aged 16:
|
|
671
658
|
|
|
672
659
|
```ts
|
|
673
660
|
const dsl = `
|
|
@@ -686,10 +673,10 @@ console.log(result.isDenied()); // true ✔
|
|
|
686
673
|
console.log(result.isAllowed()); // false ✔
|
|
687
674
|
```
|
|
688
675
|
|
|
689
|
-
|
|
690
|
-
|
|
676
|
+
In this case, everything is obvious:
|
|
677
|
+
the condition is met → the policy matches → effect `deny` → access denied.
|
|
691
678
|
|
|
692
|
-
|
|
679
|
+
**What happens if the conditions are *not met*?**
|
|
693
680
|
|
|
694
681
|
```ts
|
|
695
682
|
const result = await resolver.resolve('test', {
|
|
@@ -700,69 +687,68 @@ console.log(result.isDenied()); // true ✔
|
|
|
700
687
|
console.log(result.isAllowed()); // false ✔
|
|
701
688
|
```
|
|
702
689
|
|
|
703
|
-
|
|
704
|
-
|
|
690
|
+
At first glance, it might seem that if the condition is not met, the policy should “allow” access.
|
|
691
|
+
But that is **not the case**.
|
|
705
692
|
|
|
706
|
-
|
|
693
|
+
**Decision‑Making Model: `Default Deny`**
|
|
707
694
|
|
|
708
|
-
`AbilityResolver`
|
|
695
|
+
`AbilityResolver` uses the classic security model:
|
|
709
696
|
|
|
710
|
-
>
|
|
697
|
+
> **If there is no matching permit‑policy → access is denied.**
|
|
711
698
|
|
|
712
|
-
|
|
699
|
+
**What happens in this example:**
|
|
713
700
|
|
|
714
|
-
1.
|
|
715
|
-
→
|
|
701
|
+
1. The `deny` policy exists, but its condition is **not met**
|
|
702
|
+
→ the policy gets status `mismatch`.
|
|
716
703
|
|
|
717
|
-
2.
|
|
704
|
+
2. The `deny` policy **is not applied** because the conditions did not match.
|
|
718
705
|
|
|
719
|
-
3.
|
|
706
|
+
3. There is no `permit` policy.
|
|
720
707
|
|
|
721
|
-
4.
|
|
722
|
-
**deny (
|
|
708
|
+
4. Since there is no permit policy → the final decision:
|
|
709
|
+
**deny (by default)**.
|
|
723
710
|
|
|
711
|
+
**Summary**
|
|
724
712
|
|
|
725
|
-
|
|
713
|
+
- `deny` with matching conditions → **deny**
|
|
714
|
+
- `deny` with non‑matching conditions → **deny (default deny)**
|
|
715
|
+
- `permit` with matching conditions → **allow**
|
|
716
|
+
- `permit` with non‑matching conditions → **deny (default deny)**
|
|
726
717
|
|
|
727
|
-
|
|
728
|
-
- `deny` с несовпавшими условиями → **deny (default deny)**
|
|
729
|
-
- `permit` с совпавшими условиями → **allow**
|
|
730
|
-
- `permit` с несовпавшими условиями → **deny (default deny)**
|
|
718
|
+
**Conclusion**
|
|
731
719
|
|
|
732
|
-
|
|
720
|
+
**Access is allowed only if there is an explicit permit.**
|
|
733
721
|
|
|
734
|
-
|
|
722
|
+
## Design Recommendations
|
|
735
723
|
|
|
736
|
-
|
|
724
|
+
### Naming Access Keys
|
|
737
725
|
|
|
738
|
-
|
|
726
|
+
- Use hierarchical keys: `permission.order.create`, `permission.order.update.status`, `permission.user.profile.update`.
|
|
727
|
+
- Group by domains: `permission.user.*`, `permission.order.*`, `permission.product.*`.
|
|
728
|
+
- Do not mix different domains in one key.
|
|
739
729
|
|
|
740
|
-
|
|
741
|
-
- Группируйте по доменам: `permission.user.*`, `permission.order.*`, `permission.product.*`.
|
|
742
|
-
- Не смешивайте разные домены в одном ключе.
|
|
730
|
+
### Data Structure
|
|
743
731
|
|
|
744
|
-
|
|
732
|
+
- Explicitly describe `Resources` in TypeScript.
|
|
733
|
+
- Do not pass “extra” fields — this complicates understanding.
|
|
734
|
+
- Strive to keep the data structure for a given `permission` stable.
|
|
745
735
|
|
|
746
|
-
|
|
747
|
-
- Не передавайте «лишние» поля — это усложняет понимание.
|
|
748
|
-
- Старайтесь, чтобы структура данных для одного `permission` была стабильной.
|
|
736
|
+
### Policy Design
|
|
749
737
|
|
|
750
|
-
|
|
738
|
+
- General rules — via wildcard (`permission.order.*`).
|
|
739
|
+
- Specific restrictions — via exact actions (`permission.order.update`).
|
|
740
|
+
- Use `effect: deny` for prohibitions.
|
|
741
|
+
- Use `effect: permit` for permissions.
|
|
751
742
|
|
|
752
|
-
|
|
753
|
-
- Специфичные ограничения — через точные действия (`permission.order.update`).
|
|
754
|
-
- Для запретов используйте `effect: deny`.
|
|
755
|
-
- Для разрешений — `effect: permit`.
|
|
743
|
+
### Common Mistakes
|
|
756
744
|
|
|
757
|
-
|
|
745
|
+
- Expecting that absence of matching policies means allow.
|
|
746
|
+
- Mixing business logic and access policies.
|
|
747
|
+
- Too large policies with dozens of rules — better to break them down.
|
|
758
748
|
|
|
759
|
-
|
|
760
|
-
- Смешивание бизнес-логики и политик доступа.
|
|
761
|
-
- Слишком крупные политики с десятками правил — лучше разбивать.
|
|
749
|
+
### Example of Use on the Frontend (React)
|
|
762
750
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
**Хук для проверки политик**
|
|
751
|
+
**Hook for checking policies**
|
|
766
752
|
|
|
767
753
|
```tsx
|
|
768
754
|
// hooks/use-ability.ts
|
|
@@ -804,7 +790,7 @@ export function useAbility<Permission extends keyof Resources>(
|
|
|
804
790
|
}
|
|
805
791
|
```
|
|
806
792
|
|
|
807
|
-
|
|
793
|
+
**Usage in a component**
|
|
808
794
|
|
|
809
795
|
```tsx
|
|
810
796
|
function OrderUpdateButton({ order, user }) {
|
|
@@ -814,7 +800,7 @@ function OrderUpdateButton({ order, user }) {
|
|
|
814
800
|
});
|
|
815
801
|
|
|
816
802
|
if (allowed === null) {
|
|
817
|
-
return null; //
|
|
803
|
+
return null; // or loading spinner
|
|
818
804
|
}
|
|
819
805
|
|
|
820
806
|
if (!allowed) {
|
|
@@ -825,79 +811,75 @@ function OrderUpdateButton({ order, user }) {
|
|
|
825
811
|
}
|
|
826
812
|
```
|
|
827
813
|
|
|
814
|
+
## Examples
|
|
828
815
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
### Пример сложной многоступенчатой политики
|
|
833
|
-
|
|
834
|
-
Ниже - многоступенчатый набор политик, на примере использования в кинотеатре (выдуманный пример).
|
|
835
|
-
|
|
836
|
-
**Пример демонстрирует:**
|
|
837
|
-
- работу с ролями (admin, seller, manager, VIP, banned),
|
|
838
|
-
- временн́ые ограничения (`env.time.hour`),
|
|
839
|
-
- wildcard‑права (`permission.*`),
|
|
840
|
-
- ограничения по количеству билетов,
|
|
841
|
-
- запрет на продажу уже проданных билетов,
|
|
842
|
-
- комбинацию `permit`/`deny`‑политик,
|
|
843
|
-
- приоритет политик и модель Default Deny.
|
|
816
|
+
### Example of a Complex Multi‑Level Policy
|
|
844
817
|
|
|
818
|
+
Below is a multi‑level set of policies, using a cinema example (fictional).
|
|
845
819
|
|
|
846
|
-
|
|
847
|
-
-
|
|
848
|
-
|
|
849
|
-
|
|
820
|
+
**The example demonstrates:**
|
|
821
|
+
- working with roles (admin, seller, manager, VIP, banned),
|
|
822
|
+
- time constraints (`env.time.hour`),
|
|
823
|
+
- wildcard permissions (`permission.*`),
|
|
824
|
+
- ticket quantity limits,
|
|
825
|
+
- prohibition on selling already sold tickets,
|
|
826
|
+
- combination of `permit`/`deny` policies,
|
|
827
|
+
- policy priority and Default Deny model.
|
|
850
828
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
- билет уже продан.
|
|
829
|
+
**Brief description of rules**
|
|
830
|
+
- **Administrator**
|
|
831
|
+
Has wildcard permissions (`permission.*`) and can perform any action.
|
|
832
|
+
Can edit ticket prices.
|
|
856
833
|
|
|
857
|
-
-
|
|
858
|
-
|
|
834
|
+
- **Seller**
|
|
835
|
+
Can sell tickets only during working hours (09:00–23:00).
|
|
836
|
+
Cannot sell tickets if:
|
|
837
|
+
- the cinema is closed,
|
|
838
|
+
- the ticket is already sold.
|
|
859
839
|
|
|
860
|
-
-
|
|
861
|
-
|
|
862
|
-
- VIP‑пользователь может покупать билеты в любое время.
|
|
863
|
-
- Заблокированный пользователь (`status = banned`) не может покупать билеты.
|
|
864
|
-
- Любой пользователь не может купить более 6 билетов.
|
|
840
|
+
- **Manager**
|
|
841
|
+
Has the same rights as a seller.
|
|
865
842
|
|
|
843
|
+
- **Buyers**
|
|
844
|
+
- A user older than 21 can buy tickets.
|
|
845
|
+
- A VIP user can buy tickets at any time.
|
|
846
|
+
- A banned user (`status = banned`) cannot buy tickets.
|
|
847
|
+
- Any user cannot buy more than 6 tickets.
|
|
866
848
|
|
|
867
|
-
|
|
849
|
+
**Policy Diagram**
|
|
868
850
|
|
|
869
851
|
```mermaid
|
|
870
852
|
flowchart LR
|
|
871
853
|
|
|
872
854
|
%% ==== ROLES ====
|
|
873
855
|
|
|
874
|
-
subgraph Roles[
|
|
875
|
-
A[
|
|
876
|
-
B[
|
|
877
|
-
C[
|
|
856
|
+
subgraph Roles[Roles]
|
|
857
|
+
A[Administrator]
|
|
858
|
+
B[Seller]
|
|
859
|
+
C[Manager]
|
|
878
860
|
end
|
|
879
861
|
|
|
880
|
-
subgraph Buyers[
|
|
881
|
-
U1[
|
|
882
|
-
U2[VIP
|
|
883
|
-
U3[
|
|
862
|
+
subgraph Buyers[Buyers]
|
|
863
|
+
U1[User > 21]
|
|
864
|
+
U2[VIP user]
|
|
865
|
+
U3[Banned user]
|
|
884
866
|
end
|
|
885
867
|
|
|
886
868
|
%% ==== ADMIN ====
|
|
887
869
|
|
|
888
870
|
A --> A1[Wildcard: permission.*]
|
|
889
|
-
A --> A2[
|
|
871
|
+
A --> A2[Edit ticket price]
|
|
890
872
|
|
|
891
|
-
A1 --> FINAL[
|
|
873
|
+
A1 --> FINAL[Final decision]
|
|
892
874
|
A2 --> FINAL
|
|
893
875
|
|
|
894
876
|
%% ==== SELLER ====
|
|
895
877
|
|
|
896
|
-
B --> B1[
|
|
878
|
+
B --> B1[Sell tickets]
|
|
897
879
|
|
|
898
|
-
B1 -->|09:00–23:00| B2[
|
|
899
|
-
B1
|
|
900
|
-
B1 -->|ticket.status = sold| D3[
|
|
880
|
+
B1 -->|09:00–23:00| B2[Allowed]
|
|
881
|
+
B1 -->|Outside hours| D2[Denied]
|
|
882
|
+
B1 -->|ticket.status = sold| D3[Denied]
|
|
901
883
|
|
|
902
884
|
B2 --> FINAL
|
|
903
885
|
D2 --> FINAL
|
|
@@ -905,20 +887,20 @@ flowchart LR
|
|
|
905
887
|
|
|
906
888
|
%% ==== MANAGER ====
|
|
907
889
|
|
|
908
|
-
C --> C1[
|
|
890
|
+
C --> C1[Sell tickets as seller]
|
|
909
891
|
C1 --> FINAL
|
|
910
892
|
|
|
911
893
|
%% ==== BUYERS ====
|
|
912
894
|
|
|
913
|
-
U1 --> U1A[
|
|
914
|
-
U1A -->|ticketsCount < 6| U1OK[
|
|
915
|
-
U1A -->|ticketsCount ≥ 6| U1DENY[
|
|
895
|
+
U1 --> U1A[Buy tickets]
|
|
896
|
+
U1A -->|ticketsCount < 6| U1OK[Allowed]
|
|
897
|
+
U1A -->|ticketsCount ≥ 6| U1DENY[Denied]
|
|
916
898
|
|
|
917
|
-
U2 --> U2A[
|
|
918
|
-
U2A -->|ticketsCount < 6| U2OK[
|
|
919
|
-
U2A -->|ticketsCount ≥ 6| U2DENY[
|
|
899
|
+
U2 --> U2A[Buy tickets anytime]
|
|
900
|
+
U2A -->|ticketsCount < 6| U2OK[Allowed]
|
|
901
|
+
U2A -->|ticketsCount ≥ 6| U2DENY[Denied]
|
|
920
902
|
|
|
921
|
-
U3 --> U3A[
|
|
903
|
+
U3 --> U3A[Denied to buy tickets]
|
|
922
904
|
|
|
923
905
|
U1OK --> FINAL
|
|
924
906
|
U1DENY --> FINAL
|
|
@@ -928,11 +910,10 @@ flowchart LR
|
|
|
928
910
|
|
|
929
911
|
%% ==== DENY RULES ====
|
|
930
912
|
|
|
931
|
-
D1[
|
|
932
|
-
|
|
913
|
+
D1[Denied to buy tickets if user.status = banned] --> FINAL
|
|
933
914
|
```
|
|
934
915
|
|
|
935
|
-
**DSL
|
|
916
|
+
**DSL Policies**
|
|
936
917
|
|
|
937
918
|
```dsl
|
|
938
919
|
############################################################
|
|
@@ -998,13 +979,11 @@ deny permission.ticket.buy if all:
|
|
|
998
979
|
# @name Cannot sell already sold tickets
|
|
999
980
|
deny permission.ticket.sell if all:
|
|
1000
981
|
ticket.status is equals 'sold'
|
|
1001
|
-
|
|
1002
982
|
```
|
|
1003
983
|
|
|
984
|
+
Below is how to use the policies above in Node.js + TypeScript.
|
|
1004
985
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
**Подготовка политик**
|
|
986
|
+
**Preparing Policies**
|
|
1008
987
|
|
|
1009
988
|
```ts
|
|
1010
989
|
import { AbilityDSLParser } from '@via-profit/ability';
|
|
@@ -1013,7 +992,7 @@ import cinemaDSL from './policies/cinema.dsl';
|
|
|
1013
992
|
export const policies = new AbilityDSLParser(cinemaDSL).parse();
|
|
1014
993
|
```
|
|
1015
994
|
|
|
1016
|
-
|
|
995
|
+
**Creating the Resolver**
|
|
1017
996
|
|
|
1018
997
|
```ts
|
|
1019
998
|
import { AbilityResolver } from '@via-profit/ability';
|
|
@@ -1022,27 +1001,24 @@ import { policies } from './policies';
|
|
|
1022
1001
|
const resolver = new AbilityResolver(policies);
|
|
1023
1002
|
```
|
|
1024
1003
|
|
|
1025
|
-
|
|
1004
|
+
**Checking Permissions (enforce)**
|
|
1026
1005
|
|
|
1027
|
-
|
|
1006
|
+
Example: buying a ticket.
|
|
1028
1007
|
|
|
1029
|
-
|
|
1008
|
+
The `enforce` method throws an `AbilityError` if access is denied.
|
|
1030
1009
|
|
|
1031
1010
|
```ts
|
|
1032
1011
|
await resolver.enforce('ticket.buy', {
|
|
1033
1012
|
user: { age: 25, ticketsCount: 1 },
|
|
1034
1013
|
env: { time: { hour: 18 } },
|
|
1035
1014
|
});
|
|
1036
|
-
|
|
1037
1015
|
```
|
|
1038
|
-
|
|
1039
|
-
|
|
1016
|
+
If allowed — the code continues execution.
|
|
1017
|
+
If denied — an `AbilityError` exception is thrown.
|
|
1040
1018
|
|
|
1019
|
+
**Checking Permissions Without Exceptions (resolve)**
|
|
1041
1020
|
|
|
1042
|
-
|
|
1043
|
-
**Проверка разрешений без исключений (resolve)**
|
|
1044
|
-
|
|
1045
|
-
`resolve` возвращает объект результата:
|
|
1021
|
+
`resolve` returns a result object:
|
|
1046
1022
|
|
|
1047
1023
|
```ts
|
|
1048
1024
|
const result = await resolver.resolve('ticket.buy', {
|
|
@@ -1051,14 +1027,13 @@ const result = await resolver.resolve('ticket.buy', {
|
|
|
1051
1027
|
});
|
|
1052
1028
|
|
|
1053
1029
|
if (result.isAllowed()) {
|
|
1054
|
-
console.log('
|
|
1030
|
+
console.log('Purchase allowed');
|
|
1055
1031
|
} else {
|
|
1056
|
-
console.log('
|
|
1032
|
+
console.log('Purchase denied');
|
|
1057
1033
|
}
|
|
1058
|
-
|
|
1059
1034
|
```
|
|
1060
1035
|
|
|
1061
|
-
|
|
1036
|
+
**Seller can only sell during working hours**
|
|
1062
1037
|
|
|
1063
1038
|
```ts
|
|
1064
1039
|
await resolver.enforce('ticket.sell', {
|
|
@@ -1066,12 +1041,11 @@ await resolver.enforce('ticket.sell', {
|
|
|
1066
1041
|
env: { time: { hour: 15 } },
|
|
1067
1042
|
ticket: { status: 'available' },
|
|
1068
1043
|
});
|
|
1069
|
-
|
|
1070
1044
|
```
|
|
1071
1045
|
|
|
1072
|
-
|
|
1046
|
+
**Preparing Data for the Resolver**
|
|
1073
1047
|
|
|
1074
|
-
|
|
1048
|
+
In the examples above, constant objects are passed to the resolver:
|
|
1075
1049
|
|
|
1076
1050
|
```ts
|
|
1077
1051
|
resolver.enforce('ticket.buy', {
|
|
@@ -1080,33 +1054,32 @@ resolver.enforce('ticket.buy', {
|
|
|
1080
1054
|
});
|
|
1081
1055
|
```
|
|
1082
1056
|
|
|
1083
|
-
|
|
1057
|
+
This is done for clarity. In a real application, the data for the resolver should be built dynamically — from the sources available to your server.
|
|
1084
1058
|
|
|
1085
|
-
|
|
1059
|
+
**User** (`user`) is usually taken from:
|
|
1086
1060
|
|
|
1061
|
+
- JWT token
|
|
1062
|
+
- session
|
|
1063
|
+
- database
|
|
1064
|
+
- authorization middleware
|
|
1087
1065
|
|
|
1088
|
-
|
|
1089
|
-
- сессии
|
|
1090
|
-
- базы данных
|
|
1091
|
-
- middleware авторизации
|
|
1092
|
-
|
|
1093
|
-
Пример:
|
|
1066
|
+
Example:
|
|
1094
1067
|
|
|
1095
1068
|
```ts
|
|
1096
1069
|
const user = await db.users.findById(session.userId);
|
|
1097
1070
|
```
|
|
1098
1071
|
|
|
1099
|
-
|
|
1072
|
+
**Environment** (`env`)
|
|
1100
1073
|
|
|
1101
|
-
|
|
1074
|
+
These are any external parameters that can affect access:
|
|
1102
1075
|
|
|
1103
|
-
-
|
|
1104
|
-
-
|
|
1105
|
-
- IP
|
|
1106
|
-
-
|
|
1107
|
-
-
|
|
1076
|
+
- current server time
|
|
1077
|
+
- time zone
|
|
1078
|
+
- IP address
|
|
1079
|
+
- request headers
|
|
1080
|
+
- system configuration
|
|
1108
1081
|
|
|
1109
|
-
|
|
1082
|
+
Example:
|
|
1110
1083
|
|
|
1111
1084
|
```ts
|
|
1112
1085
|
const env = {
|
|
@@ -1117,32 +1090,31 @@ const env = {
|
|
|
1117
1090
|
};
|
|
1118
1091
|
```
|
|
1119
1092
|
|
|
1120
|
-
|
|
1093
|
+
**Resource** (e.g., `ticket`)
|
|
1121
1094
|
|
|
1122
|
-
|
|
1095
|
+
If the action is associated with a specific object, it also needs to be loaded:
|
|
1123
1096
|
|
|
1124
1097
|
```ts
|
|
1125
1098
|
const ticket = await db.tickets.findById(req.params.ticketId);
|
|
1126
1099
|
```
|
|
1127
1100
|
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
Контекст — это объект, который вы передаёте в `resolve` или `enforce`.
|
|
1131
|
-
Он содержит **все данные**, которые могут понадобиться политикам:
|
|
1101
|
+
**Context**
|
|
1132
1102
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
- `resource` или `ticket` — данные о сущности, над которой выполняется действие
|
|
1136
|
-
- любые другие объекты, которые вы используете в DSL
|
|
1103
|
+
Context is the object that you pass to `resolve` or `enforce`.
|
|
1104
|
+
It contains **all the data** that policies might need:
|
|
1137
1105
|
|
|
1138
|
-
|
|
1106
|
+
- `user` — data about the current user
|
|
1107
|
+
- `env` — environment data (time, IP, geography, system settings)
|
|
1108
|
+
- `resource` or `ticket` — data about the entity on which the action is performed
|
|
1109
|
+
- any other objects that you use in DSL
|
|
1139
1110
|
|
|
1140
|
-
|
|
1111
|
+
**It is important to understand:**
|
|
1141
1112
|
|
|
1113
|
+
> Context is formed for a specific action and specific policies. It does not need to be stored in advance — you gather it dynamically before calling the resolver.
|
|
1142
1114
|
|
|
1143
|
-
##
|
|
1115
|
+
## Performance
|
|
1144
1116
|
|
|
1145
|
-
|
|
1117
|
+
The tests used policies with 10 conditions, nested fields, and environment.
|
|
1146
1118
|
|
|
1147
1119
|
**Tinybench** ([https://github.com/tinylibs/tinybench](https://github.com/tinylibs/tinybench))
|
|
1148
1120
|
|
|
@@ -1168,10 +1140,8 @@ Throughput (ops/s)
|
|
|
1168
1140
|
1580 | █████████████████████████████████████ resolve() — no cache
|
|
1169
1141
|
--------------------------------------------------------------
|
|
1170
1142
|
no cache cold cache warm cache
|
|
1171
|
-
|
|
1172
1143
|
```
|
|
1173
1144
|
|
|
1145
|
+
## License
|
|
1174
1146
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
Этот проект лицензирован под лицензией MIT. Подробности в файле [LICENSE](LICENSE).
|
|
1147
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|