@via-profit/ability 2.1.0 → 3.0.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/CHANGELOG.md +64 -0
- package/CONTRIBUTING.md +14 -0
- package/LICENSE +21 -0
- package/README.md +809 -66
- package/SECURITY.md +33 -0
- package/dist/AbilityExplain.d.ts +27 -0
- package/dist/AbilityParser.d.ts +45 -4
- package/dist/AbilityPolicy.d.ts +11 -1
- package/dist/AbilityResolver.d.ts +2 -0
- package/dist/AbilityRule.d.ts +10 -2
- package/dist/AbilityRuleSet.d.ts +6 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +383 -53
- package/package.json +11 -2
- package/assets/ability-01.drawio.png +0 -0
- package/build/playground.js +0 -456
- package/build/playground.js.map +0 -1
package/README.md
CHANGED
|
@@ -7,27 +7,52 @@
|
|
|
7
7
|
|
|
8
8
|
## Содержание
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
- [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- [
|
|
24
|
-
|
|
10
|
+
- [Обзор](#обзор)
|
|
11
|
+
- [Состав пакета](#состав-пакета)
|
|
12
|
+
- [Основные принципы](#основные-принципы)
|
|
13
|
+
- [Правила](#правила)
|
|
14
|
+
- [Создание правила](#создание-правила)
|
|
15
|
+
- [Проверка правила](#проверка-правила)
|
|
16
|
+
- [Группы правил](#группы-правил)
|
|
17
|
+
- [Создание группы правил](#создание-группы-правил)
|
|
18
|
+
- [Проверка группы правил](#проверка-группы-правил)
|
|
19
|
+
- [Политики](#политики)
|
|
20
|
+
- [Создание политики](#создание-политики)
|
|
21
|
+
- [Проверка политики](#проверка-политики)
|
|
22
|
+
- [Управление политиками](#управление-политиками)
|
|
23
|
+
- [Зачем нужен AbilityResolver?](#зачем-нужен-abilityresolver)
|
|
24
|
+
- [Использование wildcard (*) в действиях](#использование-wildcard--в-действиях)
|
|
25
|
+
- [Как это работает](#как-это-работает)
|
|
26
|
+
- [Проверка соответствия действия](#проверка-соответствия-действия)
|
|
27
|
+
- [Методы AbilityResolver](#методы-abilityresolver)
|
|
28
|
+
- [Интеграция с TypeScript](#интеграция-с-typescript)
|
|
29
|
+
- [Как формируется итоговое решение?](#как-формируется-итоговое-решение)
|
|
30
|
+
- [Когда использовать Resolver?](#когда-использовать-resolver)
|
|
31
|
+
- [API Reference](#api-reference)
|
|
32
|
+
- [Класс AbilityCode](#класс-abilitycode)
|
|
33
|
+
- [Класс AbilityMatch](#класс-abilitymatch)
|
|
34
|
+
- [Класс AbilityCondition](#класс-abilitycondition)
|
|
35
|
+
- [Класс AbilityCompare](#класс-abilitycompare)
|
|
36
|
+
- [Класс AbilityPolicyEffect](#класс-abilitypolicyeffect)
|
|
37
|
+
- [Класс AbilityRule](#класс-abilityrule)
|
|
38
|
+
- [Класс AbilityRuleSet](#класс-abilityruleset)
|
|
39
|
+
- [Класс AbilityPolicy](#класс-abilitypolicy)
|
|
40
|
+
- [Класс AbilityResolver](#класс-abilityresolver)
|
|
41
|
+
- [Класс AbilityExplain](#класс-abilityexplain)
|
|
42
|
+
- [Класс AbilityParser](#класс-abilityparser)
|
|
43
|
+
- [Классы ошибок](#классы-ошибок)
|
|
44
|
+
- [Рекомендации по использованию](#рекомендации-по-использованию)
|
|
45
|
+
- [Именование экшенов](#именование-экшенов)
|
|
46
|
+
- [Структура данных](#структура-данных)
|
|
47
|
+
- [Проектирование политик](#проектирование-политик)
|
|
48
|
+
- [Отладка политик](#отладка-политик)
|
|
49
|
+
- [Лицензия](#лицензия)
|
|
25
50
|
|
|
26
51
|
---
|
|
27
52
|
|
|
28
|
-
## Обзор
|
|
53
|
+
## Обзор
|
|
29
54
|
|
|
30
|
-
### Состав пакета
|
|
55
|
+
### Состав пакета
|
|
31
56
|
|
|
32
57
|
- **`AbilityRule`** — класс отдельного правила
|
|
33
58
|
- **`AbilityRuleSet`** — класс группы правил
|
|
@@ -37,10 +62,11 @@
|
|
|
37
62
|
- **`AbilityCompare`** — способы сравнения (`or`, `and`)
|
|
38
63
|
- **`AbilityCondition`** — методы вычисления (`equal`, `not_equal`, `more_than`, `less_than`, `in`, `not_in` и др.)
|
|
39
64
|
- **`AbilityPolicyEffect`** — эффекты политики (`deny`, `permit`)
|
|
40
|
-
- **`AbilityParser`** — парсер конфигурационных правил (JSON)
|
|
65
|
+
- **`AbilityParser`** — парсер конфигурационных правил (JSON) и генератор `Typescript` типов
|
|
41
66
|
- **`AbilityError`** — инстанс ошибок
|
|
67
|
+
- **`AbilityExplain`** — вспомогательный инструмент, который позволяет получить человекочитаемое объяснение того, почему конкретное действие разрешено или запрещено текущей конфигурацией Ability
|
|
42
68
|
|
|
43
|
-
### Основные принципы
|
|
69
|
+
### Основные принципы
|
|
44
70
|
|
|
45
71
|
Работа сервиса основана на формировании **правил**, объединении их в **политики** и проверке доступа с их помощью.
|
|
46
72
|
|
|
@@ -51,7 +77,7 @@
|
|
|
51
77
|
|
|
52
78
|
Структура политики:
|
|
53
79
|
|
|
54
|
-

|
|
55
81
|
|
|
56
82
|
JSON-конфигурация:
|
|
57
83
|
|
|
@@ -110,12 +136,12 @@ AbilityPolicy.parse(jsonConfig).check({
|
|
|
110
136
|
|
|
111
137
|
---
|
|
112
138
|
|
|
113
|
-
## Правила
|
|
139
|
+
## Правила
|
|
114
140
|
|
|
115
141
|
**Правила** выполняют условие проверки и возвращают результат. **Основная цель** - выполнить сравнение переданных
|
|
116
142
|
значений субъекта и ресурса, а затем вернуть результат такого сравнения.
|
|
117
143
|
|
|
118
|
-
### Создание правила
|
|
144
|
+
### Создание правила
|
|
119
145
|
|
|
120
146
|
Создать правило можно двумя способами: создание через конструктор класса и парсинг JSON-конфига правила.
|
|
121
147
|
|
|
@@ -126,7 +152,7 @@ AbilityPolicy.parse(jsonConfig).check({
|
|
|
126
152
|
- **condition** - `AbilityCondition` Определяет условия сравнения переданных данных
|
|
127
153
|
- **subject** - `string` Dot notation путь в проверяемом субъекте, например: `user.name`.
|
|
128
154
|
- **resource** - `string | number | boolean | (string | number)[]` Dot notation путь в проверяемом ресурсе, например:
|
|
129
|
-
`user.name` или значение, которое может быть строкой, числом, булеан значением или массивом
|
|
155
|
+
`user.name` или значение, которое может быть строкой, числом, булеан значением или массивом строк, или чисел.
|
|
130
156
|
|
|
131
157
|
_Создание правила через конструктор класса:_
|
|
132
158
|
|
|
@@ -141,6 +167,12 @@ const rule = new AbilityRule({
|
|
|
141
167
|
condition: AbilityCondition.equal
|
|
142
168
|
});
|
|
143
169
|
|
|
170
|
+
// сокращённая запись
|
|
171
|
+
const rule2 = AbilityRule.equal(
|
|
172
|
+
'user.department', // subject
|
|
173
|
+
'managers' // resource
|
|
174
|
+
);
|
|
175
|
+
|
|
144
176
|
```
|
|
145
177
|
|
|
146
178
|
_Создание правила через парсинг JSON-конфигурации:_
|
|
@@ -158,7 +190,7 @@ const rule = AbilityRule.parse({
|
|
|
158
190
|
|
|
159
191
|
```
|
|
160
192
|
|
|
161
|
-
### Проверка правила
|
|
193
|
+
### Проверка правила
|
|
162
194
|
|
|
163
195
|
Для проверки правила следует вызвать метод `check` класса `AbilityRule` передав объект проверяемого ресурса. Этот метод
|
|
164
196
|
вернёт экземпляр класса
|
|
@@ -185,9 +217,106 @@ const is = match.isEqual(AbilityMatch.match); // true
|
|
|
185
217
|
|
|
186
218
|
```
|
|
187
219
|
|
|
220
|
+
## Получение пояснений (AbilityExplain)
|
|
221
|
+
|
|
222
|
+
Для отладки или аудита может быть полезно понять, *почему* было вынесено то или иное суждение о правах доступа. Метод `resolveWithExplain()` политики возвращает детальную информацию о проверке.
|
|
223
|
+
|
|
224
|
+
### Использование
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
const config: AbilityPolicyConfig = {
|
|
228
|
+
id: 'bb758c1b-1015-4894-ba25-d23156e063cf',
|
|
229
|
+
name: 'Запрещает менять статус заявки с `не обработан` на `завершен` всем, кроме администраторам',
|
|
230
|
+
action: 'order.status',
|
|
231
|
+
effect: 'deny',
|
|
232
|
+
compareMethod: 'and',
|
|
233
|
+
ruleSet: [
|
|
234
|
+
{
|
|
235
|
+
id: '9cc009e5-0aa9-453a-a668-cb3f418ced92',
|
|
236
|
+
name: 'Не администратор',
|
|
237
|
+
compareMethod: 'and',
|
|
238
|
+
rules: [
|
|
239
|
+
{
|
|
240
|
+
id: '4093cd50-e54f-4062-8053-2d3b5966fad3',
|
|
241
|
+
name: 'Нет роли администраторов',
|
|
242
|
+
subject: 'user.roles',
|
|
243
|
+
resource: 'administrator',
|
|
244
|
+
condition: 'not in',
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: '2f8f9d71-860b-4fa6-b395-9331f1f0848e',
|
|
250
|
+
name: 'Проверка статуса `не обработан` -> `завершен`',
|
|
251
|
+
compareMethod: 'and',
|
|
252
|
+
rules: [
|
|
253
|
+
{
|
|
254
|
+
id: 'a3c7d66f-5c2d-4a24-83bc-03b0a2d9c32b',
|
|
255
|
+
name: 'Текущий статус `не обработан`',
|
|
256
|
+
subject: 'order.status',
|
|
257
|
+
resource: 'не обработан',
|
|
258
|
+
condition: '=',
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: 'a3c7d66f-5c2d-4a24-83bc-03b0a2d9c32b',
|
|
262
|
+
name: 'Будущий статус `завершен`',
|
|
263
|
+
subject: 'feature.status',
|
|
264
|
+
resource: 'завершен',
|
|
265
|
+
condition: '=',
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const policy = AbilityPolicy.parse<Resources>(config);
|
|
273
|
+
const resolver = new AbilityResolver(policy);
|
|
274
|
+
const explain = resolver.resolveWithExplain('order.status', {
|
|
275
|
+
user: {
|
|
276
|
+
roles: ['user', 'couch'],
|
|
277
|
+
},
|
|
278
|
+
order: {
|
|
279
|
+
status: 'не обработан',
|
|
280
|
+
},
|
|
281
|
+
feature: {
|
|
282
|
+
status: 'завершен',
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
explain.forEach((e) => {
|
|
287
|
+
console.debug(e.toString());
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
type Resources = {
|
|
291
|
+
['order.status']: {
|
|
292
|
+
readonly user: {
|
|
293
|
+
readonly roles: readonly string[];
|
|
294
|
+
};
|
|
295
|
+
readonly order: {
|
|
296
|
+
readonly status: string;
|
|
297
|
+
};
|
|
298
|
+
readonly feature: {
|
|
299
|
+
readonly status: string;
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Результат `console.debug`
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
✓ policy «Запрещает менять статус заявки...» is match
|
|
310
|
+
✓ ruleSet «Не администратор» is match
|
|
311
|
+
✓ rule «Нет роли администраторов» is match
|
|
312
|
+
✓ ruleSet «Проверка статуса...» is match
|
|
313
|
+
✓ rule «Текущий статус "не обработан"» is match
|
|
314
|
+
✓ rule «Будущий статус "завершен"» is match
|
|
315
|
+
```
|
|
316
|
+
|
|
188
317
|
---
|
|
189
318
|
|
|
190
|
-
## Группы правил
|
|
319
|
+
## Группы правил
|
|
191
320
|
|
|
192
321
|
**Группы правил** необходимы для объединения нескольких правил в группу. **Основная цель** - выполнить проверку каждого
|
|
193
322
|
правила в группе и вернуть лишь один результат.
|
|
@@ -206,7 +335,7 @@ _Влияние **compareMethod** на результат вычисления
|
|
|
206
335
|
- **`or`** - Результат всей группы примет значение `match`, если хотя бы одно из правил вернуло `match`.
|
|
207
336
|
- **`and`** - Результат всей группы примет значение `match`, если все правила вернули `match`.
|
|
208
337
|
|
|
209
|
-
### Создание группы правил
|
|
338
|
+
### Создание группы правил
|
|
210
339
|
|
|
211
340
|
Создать группу правил можно двумя способами: создание через конструктор класса и парсинг JSON-конфига группы.
|
|
212
341
|
|
|
@@ -227,6 +356,13 @@ ruleSet.addRules([
|
|
|
227
356
|
new AbilityRule(...),
|
|
228
357
|
]);
|
|
229
358
|
|
|
359
|
+
|
|
360
|
+
// Сокращённая запись
|
|
361
|
+
const ruleSet2 = AbilityRuleSet.and([
|
|
362
|
+
new AbilityRule(...),
|
|
363
|
+
new AbilityRule(...),
|
|
364
|
+
]);
|
|
365
|
+
|
|
230
366
|
```
|
|
231
367
|
|
|
232
368
|
_Создание группы через парсинг JSON-конфига группы_:
|
|
@@ -251,7 +387,7 @@ const ruleSet = AbilityRuleSet.parse({
|
|
|
251
387
|
|
|
252
388
|
```
|
|
253
389
|
|
|
254
|
-
### Проверка группы правил
|
|
390
|
+
### Проверка группы правил
|
|
255
391
|
|
|
256
392
|
Для проверки группы правил следует вызвать метод `check` класса `AbilityRuleSet` передав объект проверяемого ресурса.
|
|
257
393
|
Этот метод вернёт экземпляр класса `AbilityMatch`, при помощи методов которого можно определить имеется ли совпадение
|
|
@@ -276,12 +412,12 @@ const is = match.isEqual(AbilityMatch.match);
|
|
|
276
412
|
|
|
277
413
|
___
|
|
278
414
|
|
|
279
|
-
## Политики
|
|
415
|
+
## Политики
|
|
280
416
|
|
|
281
417
|
**Политики** включают в себя группы правил. Основная цель - выполнить проверку всех вложенных групп, сравнить результат
|
|
282
418
|
выполнения групп и вернуть один единственный результат.
|
|
283
419
|
|
|
284
|
-
### Создание политики
|
|
420
|
+
### Создание политики
|
|
285
421
|
|
|
286
422
|
Создать политику можно двумя способами: создание через конструктор класса и парсинг JSON-конфига политики.
|
|
287
423
|
|
|
@@ -298,7 +434,7 @@ ___
|
|
|
298
434
|
использования класса `AbilityResolver` (метод `enforce`) последний выкинет исключение `AbilityError`, если политика
|
|
299
435
|
вернёт `deny`. Текст сообщения `AbilityError` будет соответствовать названию сработавшей политики. В остальных случаях
|
|
300
436
|
ничего не произойдет.
|
|
301
|
-
- **ruleSet** - `AbilityRuleSet[]` Массив групп (см. [Группы правил](
|
|
437
|
+
- **ruleSet** - `AbilityRuleSet[]` Массив групп (см. [Группы правил](#группы-правил))
|
|
302
438
|
|
|
303
439
|
**Замечание** - Политика может быть запрещающей (`effect` = `deny`) и разрешающей (`effect` = `permit`). Если вам
|
|
304
440
|
необходимо ограничить какой-либо доступ, например, пользователю с недостаточными правами, то следует создавать политику
|
|
@@ -359,7 +495,7 @@ const policy = AbilityPolicy.parse({
|
|
|
359
495
|
|
|
360
496
|
```
|
|
361
497
|
|
|
362
|
-
### Проверка политики
|
|
498
|
+
### Проверка политики
|
|
363
499
|
|
|
364
500
|
Для проверки политики правил следует вызвать метод `check` класса `AbilityPolicy` передав объект проверяемого ресурса.
|
|
365
501
|
Этот метод вернёт экземпляр класса `AbilityMatch`, при помощи методов которого можно определить имеется ли совпадение
|
|
@@ -377,62 +513,669 @@ const is = match.isEqual(AbilityMatch.match);
|
|
|
377
513
|
|
|
378
514
|
___
|
|
379
515
|
|
|
380
|
-
## Управление политиками
|
|
516
|
+
## Управление политиками
|
|
517
|
+
|
|
518
|
+
Класс `AbilityResolver` — это основной инструмент для применения политик в рантайме. Он решает две ключевые задачи:
|
|
519
|
+
|
|
520
|
+
1. **Фильтрация политик по действию** — выбирает только те политики, которые применимы к выполняемой операции
|
|
521
|
+
2. **Оценка разрешений** — последовательно проверяет отобранные политики и возвращает итоговый результат (разрешено/запрещено)
|
|
381
522
|
|
|
382
|
-
|
|
523
|
+
### Зачем нужен AbilityResolver?
|
|
383
524
|
|
|
384
|
-
|
|
525
|
+
Представим, что в системе есть десятки политик, каждая на своё действие:
|
|
526
|
+
- `order.create` — правила создания заказа
|
|
527
|
+
- `order.update` — правила обновления заказа
|
|
528
|
+
- `order.delete` — правила удаления заказа
|
|
529
|
+
- `user.profile.update` — правила обновления профиля
|
|
530
|
+
- и так далее...
|
|
385
531
|
|
|
386
|
-
|
|
532
|
+
Когда пользователь пытается создать заказ, нам нужно проверить только политики, связанные с действием `order.create`, игнорируя все остальные. Именно это и делает `AbilityResolver`.
|
|
387
533
|
|
|
388
|
-
|
|
389
|
-
|
|
534
|
+
### Использование wildcard (*) в действиях
|
|
535
|
+
|
|
536
|
+
`AbilityResolver` поддерживает использование символа звездочки (`*`) в названиях действий. Это позволяет создавать политики, которые применяются к целым группам операций.
|
|
537
|
+
|
|
538
|
+
#### Правила сопоставления с wildcard:
|
|
539
|
+
|
|
540
|
+
| Политика (action) | Проверяемое действие | Результат |
|
|
541
|
+
|-------------------|---------------------|-----------|
|
|
542
|
+
| `order.*` | `order.create` | ✅ Совпадает |
|
|
543
|
+
| `order.*` | `order.update` | ✅ Совпадает |
|
|
544
|
+
| `order.*` | `order.delete` | ✅ Совпадает |
|
|
545
|
+
| `order.*` | `user.create` | ❌ Не совпадает |
|
|
546
|
+
| `*.create` | `order.create` | ✅ Совпадает |
|
|
547
|
+
| `*.create` | `user.create` | ✅ Совпадает |
|
|
548
|
+
| `*.create` | `order.update` | ❌ Не совпадает |
|
|
549
|
+
| `user.profile.*` | `user.profile.update` | ✅ Совпадает |
|
|
550
|
+
| `user.profile.*` | `user.profile.delete` | ✅ Совпадает |
|
|
551
|
+
| `user.profile.*` | `user.settings.update` | ❌ Не совпадает |
|
|
552
|
+
|
|
553
|
+
#### Примеры использования wildcard:
|
|
390
554
|
|
|
391
555
|
```ts
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
//
|
|
399
|
-
|
|
400
|
-
// либо ничего не произойдет, если ни одна из политик
|
|
401
|
-
// не вернет deny
|
|
402
|
-
new AbilityResolver(policies).enforce('order.create', {
|
|
403
|
-
user: { department: 'managers' },
|
|
404
|
-
});
|
|
556
|
+
// Политика, применяемая ко всем действиям с заказами
|
|
557
|
+
{
|
|
558
|
+
id: 'orders-audit',
|
|
559
|
+
name: 'Audit all order operations',
|
|
560
|
+
action: 'order.*', // Применится к order.create, order.update, order.delete и т.д.
|
|
561
|
+
effect: 'permit',
|
|
562
|
+
// ... правила
|
|
563
|
+
}
|
|
405
564
|
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
565
|
+
// Политика, применяемая ко всем операциям создания
|
|
566
|
+
{
|
|
567
|
+
id: 'create-audit',
|
|
568
|
+
name: 'Audit all create operations',
|
|
569
|
+
action: '*.create', // Применится к order.create, user.create, product.create и т.д.
|
|
570
|
+
effect: 'permit',
|
|
571
|
+
// ... правила
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Политика для всех операций в модуле пользователей
|
|
575
|
+
{
|
|
576
|
+
id: 'user-module',
|
|
577
|
+
name: 'User module base policy',
|
|
578
|
+
action: 'user.*.*', // Применится к user.profile.update, user.settings.delete и т.д.
|
|
579
|
+
effect: 'deny',
|
|
580
|
+
// ... правила
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
#### Приоритет и множественное совпадение
|
|
585
|
+
|
|
586
|
+
Если несколько политик подходят под проверяемое действие, будут применены **все** подходящие политики. Результат определяется последней сработавшей политикой:
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
const policies = [
|
|
590
|
+
AbilityPolicy.parse({
|
|
591
|
+
action: 'order.*',
|
|
592
|
+
effect: 'permit',
|
|
593
|
+
// ... правила
|
|
594
|
+
}),
|
|
595
|
+
AbilityPolicy.parse({
|
|
596
|
+
action: 'order.update',
|
|
597
|
+
effect: 'deny',
|
|
598
|
+
// ... правила
|
|
410
599
|
})
|
|
411
|
-
|
|
600
|
+
];
|
|
601
|
+
|
|
602
|
+
const resolver = new AbilityResolver(policies);
|
|
603
|
+
|
|
604
|
+
// При проверке order.update сработают ОБЕ политики
|
|
605
|
+
// Результат будет deny, так как это эффект последней сработавшей политики,
|
|
606
|
+
// таким образом, каждая последующая политика считается важнее предыдущей.
|
|
607
|
+
// Это применительно для ситуаций, когда необходимо, что называется, наложить вето
|
|
608
|
+
// на принятые ранее решения вышестоящих политик
|
|
609
|
+
resolver.enforce('order.update', data);
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
**Каждая последующая политика считается важнее предыдущей.
|
|
613
|
+
Это применительно для ситуаций, когда необходимо, что называется, наложить вето
|
|
614
|
+
на принятые ранее решения вышестоящих политик**
|
|
615
|
+
|
|
616
|
+
#### Комбинирование точных действий и wildcard
|
|
617
|
+
|
|
618
|
+
Вы можете комбинировать точные действия и wildcard для создания гибкой системы прав:
|
|
619
|
+
|
|
620
|
+
```ts
|
|
621
|
+
const policies = [
|
|
622
|
+
// Общее правило для всех заказов
|
|
623
|
+
{
|
|
624
|
+
action: 'order.*',
|
|
625
|
+
effect: 'deny', // По умолчанию запрещено
|
|
626
|
+
// ...
|
|
627
|
+
},
|
|
628
|
+
// Исключение для создания заказа
|
|
629
|
+
{
|
|
630
|
+
action: 'order.create',
|
|
631
|
+
effect: 'permit', // Создавать можно
|
|
632
|
+
// ...
|
|
633
|
+
},
|
|
634
|
+
// Дополнительная проверка для обновления
|
|
635
|
+
{
|
|
636
|
+
action: 'order.update',
|
|
637
|
+
effect: 'deny', // Обновление требует особых условий
|
|
638
|
+
ruleSet: [
|
|
639
|
+
// ... сложные правила для обновления
|
|
640
|
+
]
|
|
641
|
+
}
|
|
642
|
+
];
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Как это работает
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
import { AbilityPolicy, AbilityResolver } from '@via-profit/ability';
|
|
649
|
+
import type { AbilityPolicyConfig } from '@via-profit/ability';
|
|
650
|
+
|
|
651
|
+
// Загружаем все политики системы (например, из JSON-файлов)
|
|
652
|
+
const configs: AbilityPolicyConfig[] = [
|
|
653
|
+
{
|
|
654
|
+
id: 'order-create-policy',
|
|
655
|
+
name: 'Политика создания заказа',
|
|
656
|
+
action: 'order.create',
|
|
657
|
+
effect: 'permit',
|
|
658
|
+
compareMethod: 'and',
|
|
659
|
+
ruleSet: [
|
|
660
|
+
// ... правила для создания заказа
|
|
661
|
+
]
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
id: 'order-update-policy',
|
|
665
|
+
name: 'Политика обновления заказа',
|
|
666
|
+
action: 'order.update',
|
|
667
|
+
effect: 'deny',
|
|
668
|
+
compareMethod: 'and',
|
|
669
|
+
ruleSet: [
|
|
670
|
+
// ... правила для обновления заказа
|
|
671
|
+
]
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
id: 'orders-base-policy',
|
|
675
|
+
name: 'Базовая политика для всех операций с заказами',
|
|
676
|
+
action: 'order.*',
|
|
677
|
+
effect: 'deny',
|
|
678
|
+
compareMethod: 'and',
|
|
679
|
+
ruleSet: [
|
|
680
|
+
// ... общие правила для всех заказов
|
|
681
|
+
]
|
|
682
|
+
}
|
|
683
|
+
];
|
|
684
|
+
|
|
685
|
+
// Создаем экземпляры политик
|
|
686
|
+
const policies = AbilityPolicy.parseAll(configs);
|
|
687
|
+
|
|
688
|
+
// Создаем резолвер со всеми политиками
|
|
689
|
+
const resolver = new AbilityResolver(policies);
|
|
690
|
+
|
|
691
|
+
// При выполнении действия указываем только нужный action
|
|
692
|
+
// AbilityResolver автоматически отфильтрует политики и проверит их
|
|
693
|
+
resolver.enforce('order.create', {
|
|
694
|
+
user: {
|
|
695
|
+
department: 'managers',
|
|
696
|
+
roles: ['manager']
|
|
697
|
+
},
|
|
698
|
+
order: {
|
|
699
|
+
amount: 5000
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
### Проверка соответствия действия
|
|
705
|
+
|
|
706
|
+
Метод `isInActionContain` позволяет проверить, соответствует ли действие шаблону с wildcard:
|
|
707
|
+
|
|
708
|
+
```ts
|
|
709
|
+
// Статический метод класса AbilityResolver
|
|
710
|
+
AbilityResolver.isInActionContain('order.*', 'order.create'); // true
|
|
711
|
+
AbilityResolver.isInActionContain('order.*', 'user.create'); // false
|
|
712
|
+
AbilityResolver.isInActionContain('*.update', 'order.update'); // true
|
|
713
|
+
AbilityResolver.isInActionContain('*.update', 'order.create'); // false
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
Этот метод используется внутри `AbilityResolver` для фильтрации политик, но может быть полезен и в пользовательском коде.
|
|
717
|
+
|
|
718
|
+
### Методы AbilityResolver
|
|
719
|
+
|
|
720
|
+
#### `enforce()` — строгая проверка
|
|
721
|
+
|
|
722
|
+
```typescript
|
|
723
|
+
try {
|
|
724
|
+
resolver.enforce('order.update', data);
|
|
725
|
+
// Доступ разрешен - продолжаем выполнение
|
|
726
|
+
await updateOrder(data);
|
|
727
|
+
} catch (error) {
|
|
728
|
+
if (error instanceof AbilityError) {
|
|
729
|
+
// Доступ запрещен - error.message содержит название сработавшей политики
|
|
730
|
+
console.error(`Доступ запрещен политикой: ${error.message}`);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
**Важно**: `enforce()` выбрасывает исключение, если хотя бы одна подходящая политика вернула `deny`. Если ни одна политика не сработала или все вернули `permit` — исключения не будет.
|
|
738
|
+
|
|
739
|
+
#### `resolve()` — мягкая проверка
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
const result = resolver
|
|
743
|
+
.resolve('order.update', data)
|
|
744
|
+
.isDeny(); // true если доступ запрещен
|
|
745
|
+
|
|
746
|
+
if (result) {
|
|
747
|
+
// Самостоятельно обрабатываем запрет
|
|
748
|
+
return { error: 'Access denied' };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Продолжаем выполнение
|
|
752
|
+
await updateOrder(data);
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
#### `resolveWithExplain()` — проверка с объяснением
|
|
756
|
+
|
|
757
|
+
```typescript
|
|
758
|
+
const explanations = resolver.resolveWithExplain('order.update', data);
|
|
412
759
|
|
|
413
|
-
|
|
414
|
-
|
|
760
|
+
explanations.forEach(explain => {
|
|
761
|
+
console.log(explain.toString());
|
|
762
|
+
// ✓ policy «Политика обновления заказа» is match
|
|
763
|
+
// ✗ ruleSet «Проверка владельца» is mismatch
|
|
764
|
+
// ✓ ruleSet «Проверка статуса» is match
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
if (resolver.isDeny()) {
|
|
768
|
+
console.log('Доступ запрещен по причине:');
|
|
769
|
+
explanations.forEach(e => console.log(e.toString()));
|
|
415
770
|
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Интеграция с TypeScript
|
|
416
774
|
|
|
775
|
+
Для полной типобезопасности определите интерфейс `Resources`, где ключи — это возможные действия, а значения — структура данных, требуемая для проверки:
|
|
417
776
|
|
|
418
|
-
|
|
777
|
+
```typescript
|
|
778
|
+
// Определяем типы данных для каждого действия
|
|
419
779
|
type Resources = {
|
|
420
|
-
|
|
421
|
-
readonly
|
|
780
|
+
'order.create': {
|
|
781
|
+
readonly user: {
|
|
782
|
+
readonly department: string;
|
|
422
783
|
readonly roles: readonly string[];
|
|
423
784
|
};
|
|
785
|
+
readonly order: {
|
|
786
|
+
readonly amount: number;
|
|
787
|
+
};
|
|
424
788
|
};
|
|
425
|
-
|
|
789
|
+
|
|
790
|
+
'order.update': {
|
|
426
791
|
readonly user: {
|
|
427
|
-
readonly
|
|
792
|
+
readonly id: string;
|
|
793
|
+
};
|
|
794
|
+
readonly order: {
|
|
795
|
+
readonly id: string;
|
|
796
|
+
readonly status: string;
|
|
797
|
+
readonly ownerId: string;
|
|
798
|
+
};
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
'user.profile.update': {
|
|
802
|
+
readonly user: {
|
|
803
|
+
readonly id: string;
|
|
804
|
+
};
|
|
805
|
+
readonly profile: {
|
|
806
|
+
readonly userId: string;
|
|
428
807
|
};
|
|
429
808
|
};
|
|
430
|
-
...
|
|
431
809
|
};
|
|
432
810
|
|
|
811
|
+
// Теперь TypeScript будет проверять корректность передаваемых данных
|
|
812
|
+
resolver.enforce('order.create', {
|
|
813
|
+
user: {
|
|
814
|
+
department: 'managers',
|
|
815
|
+
roles: ['manager']
|
|
816
|
+
},
|
|
817
|
+
order: {
|
|
818
|
+
amount: 5000 // ✅ все поля на месте
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// ❌ Ошибка компиляции - не хватает required полей
|
|
823
|
+
resolver.enforce('order.create', {
|
|
824
|
+
user: {
|
|
825
|
+
department: 'managers'
|
|
826
|
+
// missing roles
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### Как формируется итоговое решение?
|
|
832
|
+
|
|
833
|
+
1. Resolver собирает все политики, у которых `action` соответствует запрошенному (поддерживается wildcard `*`)
|
|
834
|
+
2. Последовательно проверяет каждую политику методом `check()`
|
|
835
|
+
3. Если политика вернула `match`, запоминает её `effect` (permit/deny)
|
|
836
|
+
4. Возвращается `effect` **последней сработавшей политики**
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
// Пример с несколькими политиками на одно действие
|
|
840
|
+
const policies = [
|
|
841
|
+
permitPolicy, // permit, но не match
|
|
842
|
+
denyPolicy, // deny и match
|
|
843
|
+
permitPolicy2 // permit, но не match
|
|
844
|
+
];
|
|
845
|
+
|
|
846
|
+
resolver.enforce('some.action', data);
|
|
847
|
+
// Результат: deny (от последней сработавшей политики)
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
### Когда использовать Resolver?
|
|
851
|
+
|
|
852
|
+
- **В API endpoints** — проверка прав перед выполнением операции
|
|
853
|
+
- **В middleware** — централизованная проверка доступа
|
|
854
|
+
- **В сервисах** — защита бизнес-логики
|
|
855
|
+
- **В клиентском коде** — условный рендеринг UI на основе прав
|
|
856
|
+
|
|
857
|
+
Resolver делает систему прав предсказуемой, типобезопасной и легко расширяемой.
|
|
858
|
+
|
|
859
|
+
## API Reference
|
|
860
|
+
|
|
861
|
+
### Класс AbilityCode
|
|
862
|
+
|
|
863
|
+
Базовый абстрактный класс для всех кодовых значений в системе.
|
|
864
|
+
|
|
865
|
+
| Метод | Аргументы | Возвращаемое значение | Описание |
|
|
866
|
+
|-------|-----------|----------------------|----------|
|
|
867
|
+
| `isEqual()` | `compareWith: AbilityCode<T> \| null` | `boolean` | Сравнивает текущий код с другим экземпляром |
|
|
868
|
+
| `isNotEqual()` | `compareWith: AbilityCode<T> \| null` | `boolean` | Проверяет неравенство с другим экземпляром |
|
|
869
|
+
|
|
870
|
+
**Геттеры:**
|
|
871
|
+
- `code: T` - возвращает сырое значение кода (строку или число)
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
### Класс AbilityMatch
|
|
876
|
+
|
|
877
|
+
Представляет возможные состояния результата проверки правил.
|
|
878
|
+
|
|
879
|
+
**Статические свойства:**
|
|
880
|
+
- `AbilityMatch.pending` - ожидание проверки
|
|
881
|
+
- `AbilityMatch.match` - совпадение найдено
|
|
882
|
+
- `AbilityMatch.mismatch` - совпадение не найдено
|
|
883
|
+
|
|
884
|
+
Каждое свойство является экземпляром класса `AbilityMatch` и наследует все его методы.
|
|
885
|
+
|
|
886
|
+
---
|
|
887
|
+
|
|
888
|
+
### Класс AbilityCondition
|
|
889
|
+
|
|
890
|
+
Определяет операторы сравнения для правил доступа.
|
|
891
|
+
|
|
892
|
+
**Статические свойства:**
|
|
893
|
+
- `AbilityCondition.equal` - равно (`=`)
|
|
894
|
+
- `AbilityCondition.not_equal` - не равно (`<>`)
|
|
895
|
+
- `AbilityCondition.more_than` - больше (`>`)
|
|
896
|
+
- `AbilityCondition.less_than` - меньше (`<`)
|
|
897
|
+
- `AbilityCondition.less_or_equal` - меньше или равно (`<=`)
|
|
898
|
+
- `AbilityCondition.more_or_equal` - больше или равно (`>=`)
|
|
899
|
+
- `AbilityCondition.in` - входит в массив (`in`)
|
|
900
|
+
- `AbilityCondition.not_in` - не входит в массив (`not in`)
|
|
901
|
+
|
|
902
|
+
| Метод | Аргументы | Возвращаемое значение | Описание |
|
|
903
|
+
|-------|-----------|----------------------|----------|
|
|
904
|
+
| `fromLiteral()` | `literal: AbilityConditionLiteralType` | `AbilityCondition` | Создает экземпляр условия из литерального имени (например, 'equal' → '=') |
|
|
905
|
+
|
|
906
|
+
**Геттеры:**
|
|
907
|
+
- `literal: AbilityConditionLiteralType` - возвращает литеральное имя оператора ('equal', 'not_equal' и т.д.)
|
|
908
|
+
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
### Класс AbilityCompare
|
|
912
|
+
|
|
913
|
+
Определяет методы логического сравнения для групп правил.
|
|
914
|
+
|
|
915
|
+
**Статические свойства:**
|
|
916
|
+
- `AbilityCompare.and` - логическое И (все правила должны совпасть)
|
|
917
|
+
- `AbilityCompare.or` - логическое ИЛИ (достаточно одного совпадения)
|
|
918
|
+
|
|
919
|
+
---
|
|
920
|
+
|
|
921
|
+
### Класс AbilityPolicyEffect
|
|
922
|
+
|
|
923
|
+
Определяет эффект применения политики.
|
|
924
|
+
|
|
925
|
+
**Статические свойства:**
|
|
926
|
+
- `AbilityPolicyEffect.deny` - запрет доступа
|
|
927
|
+
- `AbilityPolicyEffect.permit` - разрешение доступа
|
|
928
|
+
|
|
929
|
+
---
|
|
930
|
+
|
|
931
|
+
### Класс AbilityRule
|
|
932
|
+
|
|
933
|
+
Представляет отдельное правило проверки доступа.
|
|
934
|
+
|
|
935
|
+
**Свойства:**
|
|
936
|
+
- `subject: string` - путь к значению субъекта в dot-нотации
|
|
937
|
+
- `resource: string | number | boolean | (string | number)[]` - значение или путь для сравнения
|
|
938
|
+
- `condition: AbilityCondition` - условие сравнения
|
|
939
|
+
- `name: string` - название правила
|
|
940
|
+
- `id: string` - уникальный идентификатор
|
|
941
|
+
- `state: AbilityMatch` - текущее состояние после вызова `check()`
|
|
942
|
+
|
|
943
|
+
| Метод | Аргументы | Возвращаемое значение | Описание |
|
|
944
|
+
|-------|-----------|----------------------|----------|
|
|
945
|
+
| `check()` | `resource: Resources \| null` | `AbilityMatch` | Проверяет правило на переданных данных, обновляет `state` и возвращает результат |
|
|
946
|
+
| `extractValues()` | `resourceData: Resources \| null` | `[any, any]` | Извлекает значения для сравнения из субъекта и ресурса по указанным путям |
|
|
947
|
+
| `getDotNotationValue()` | `resource: unknown, desc: string` | `T \| undefined` | Извлекает значение из объекта по dot-нотации (поддерживает массивы: `users[0].name`) |
|
|
948
|
+
| `export()` | — | `AbilityRuleConfig` | Экспортирует правило в конфигурационный объект для сериализации |
|
|
949
|
+
| `parse()` | `config: AbilityRuleConfig` | `AbilityRule` | Статический метод. Создает экземпляр правила из конфигурационного объекта |
|
|
950
|
+
|
|
951
|
+
**Статические фабричные методы (все возвращают `AbilityRule`):**
|
|
952
|
+
|
|
953
|
+
| Метод | Аргументы | Описание |
|
|
954
|
+
|-------|-----------|----------|
|
|
955
|
+
| `equal()` | `subject: string, resource: any` | Создает правило с условием "равно" |
|
|
956
|
+
| `notEqual()` | `subject: string, resource: any` | Создает правило с условием "не равно" |
|
|
957
|
+
| `in()` | `subject: string, resource: any` | Создает правило с условием "входит в массив" |
|
|
958
|
+
| `notIn()` | `subject: string, resource: any` | Создает правило с условием "не входит в массив" |
|
|
959
|
+
| `lessThan()` | `subject: string, resource: any` | Создает правило с условием "меньше" |
|
|
960
|
+
| `lessOrEqual()` | `subject: string, resource: any` | Создает правило с условием "меньше или равно" |
|
|
961
|
+
| `moreThan()` | `subject: string, resource: any` | Создает правило с условием "больше" |
|
|
962
|
+
| `moreOrEqual()` | `subject: string, resource: any` | Создает правило с условием "больше или равно" |
|
|
963
|
+
|
|
964
|
+
---
|
|
965
|
+
|
|
966
|
+
### Класс AbilityRuleSet
|
|
967
|
+
|
|
968
|
+
Группирует несколько правил для совместной проверки.
|
|
969
|
+
|
|
970
|
+
**Свойства:**
|
|
971
|
+
- `state: AbilityMatch` - текущее состояние группы после вызова `check()`
|
|
972
|
+
- `rules: AbilityRule[]` - массив правил в группе
|
|
973
|
+
- `compareMethod: AbilityCompare` - метод сравнения (AND/OR)
|
|
974
|
+
- `name: string` - название группы
|
|
975
|
+
- `id: string` - уникальный идентификатор
|
|
976
|
+
|
|
977
|
+
| Метод | Аргументы | Возвращаемое значение | Описание |
|
|
978
|
+
|-------|-----------|----------------------|----------|
|
|
979
|
+
| `addRule()` | `rule: AbilityRule` | `this` | Добавляет одно правило в группу (поддерживает цепочку вызовов) |
|
|
980
|
+
| `addRules()` | `rules: AbilityRule[]` | `this` | Добавляет массив правил в группу |
|
|
981
|
+
| `check()` | `resources: Resources \| null` | `AbilityMatch` | Проверяет все правила группы, применяет метод сравнения и возвращает результат |
|
|
982
|
+
| `export()` | — | `AbilityRuleSetConfig` | Экспортирует группу в конфигурационный объект |
|
|
983
|
+
| `parse()` | `config: AbilityRuleSetConfig` | `AbilityRuleSet` | Статический метод. Создает экземпляр группы из конфигурации |
|
|
984
|
+
| `and()` | `rules: AbilityRule[]` | `AbilityRuleSet` | Статический метод. Создает группу с логическим И |
|
|
985
|
+
| `or()` | `rules: AbilityRule[]` | `AbilityRuleSet` | Статический метод. Создает группу с логическим ИЛИ |
|
|
986
|
+
|
|
987
|
+
---
|
|
988
|
+
|
|
989
|
+
### Класс AbilityPolicy
|
|
990
|
+
|
|
991
|
+
Объединяет группы правил в полноценную политику доступа.
|
|
992
|
+
|
|
993
|
+
**Свойства:**
|
|
994
|
+
- `matchState: AbilityMatch` - состояние политики после проверки
|
|
995
|
+
- `ruleSet: AbilityRuleSet[]` - массив групп правил
|
|
996
|
+
- `effect: AbilityPolicyEffect` - эффект политики (permit/deny)
|
|
997
|
+
- `compareMethod: AbilityCompare` - метод сравнения групп
|
|
998
|
+
- `name: string` - название политики
|
|
999
|
+
- `id: string` - уникальный идентификатор
|
|
1000
|
+
- `action: string` - действие, к которому применяется политика
|
|
1001
|
+
|
|
1002
|
+
| Метод | Аргументы | Возвращаемое значение | Описание |
|
|
1003
|
+
|-------|-----------|----------------------|----------|
|
|
1004
|
+
| `addRuleSet()` | `ruleSet: AbilityRuleSet` | `this` | Добавляет группу правил в политику |
|
|
1005
|
+
| `check()` | `resources: Resources` | `AbilityMatch` | Проверяет все группы правил и возвращает результат |
|
|
1006
|
+
| `explain()` | — | `AbilityExplain` | Возвращает объяснение результата проверки (должен быть вызван после `check()`) |
|
|
1007
|
+
| `export()` | — | `AbilityPolicyConfig` | Экспортирует политику в конфигурационный объект |
|
|
1008
|
+
| `parse()` | `config: AbilityPolicyConfig` | `AbilityPolicy` | Статический метод. Создает экземпляр политики из конфигурации |
|
|
1009
|
+
| `parseAll()` | `configs: readonly AbilityPolicyConfig[]` | `AbilityPolicy[]` | Статический метод. Парсит массив конфигураций политик и возвращает массив экземпляров AbilityPolicy. Удобен для загрузки нескольких политик одновременно. |
|
|
1010
|
+
|
|
1011
|
+
---
|
|
1012
|
+
|
|
1013
|
+
### Класс AbilityResolver
|
|
1014
|
+
|
|
1015
|
+
Управляет множеством политик и их выполнением.
|
|
1016
|
+
|
|
1017
|
+
| Метод | Аргументы | Возвращаемое значение | Описание |
|
|
1018
|
+
|-------|-----------|----------------------|----------|
|
|
1019
|
+
| `constructor()` | `policies: AbilityPolicy[] \| AbilityPolicy` | `AbilityResolver` | Создает экземпляр с одной или несколькими политиками |
|
|
1020
|
+
| `resolve()` | `action: keyof Resources, resource: Resources[Action]` | `this` | Фильтрует политики по действию и проверяет их, возвращает себя для цепочки вызовов |
|
|
1021
|
+
| `resolveWithExplain()` | `action: keyof Resources, resource: Resources[Action]` | `readonly AbilityExplain[]` | Выполняет проверку и возвращает массив объяснений для каждой политики |
|
|
1022
|
+
| `enforce()` | `action: keyof Resources, resource: Resources[Action]` | `void \| never` | Выполняет проверку и выбрасывает `AbilityError` если результат Deny |
|
|
1023
|
+
| `getEffect()` | — | `AbilityPolicyEffect \| null` | Возвращает эффект последней сработавшей политики |
|
|
1024
|
+
| `isPermit()` | — | `boolean` | Проверяет, разрешен ли доступ |
|
|
1025
|
+
| `isDeny()` | — | `boolean` | Проверяет, запрещен ли доступ |
|
|
1026
|
+
| `getMatchedPolicy()` | — | `AbilityPolicy \| null` | Возвращает последнюю сработавшую политику |
|
|
1027
|
+
| `isInActionContain()` | `actionA: string, actionB: string` | `boolean` | Статический метод. Проверяет, соответствует ли действие шаблону (поддерживает `*`) |
|
|
1028
|
+
|
|
1029
|
+
---
|
|
1030
|
+
|
|
1031
|
+
### Класс AbilityExplain
|
|
1032
|
+
|
|
1033
|
+
Представляет человекочитаемое объяснение результата проверки.
|
|
1034
|
+
|
|
1035
|
+
**Свойства:**
|
|
1036
|
+
- `type: AbilityExplainType` - тип элемента ('policy' \| 'rule' \| 'ruleSet')
|
|
1037
|
+
- `children: AbilityExplain[]` - дочерние элементы объяснения
|
|
1038
|
+
- `name: string` - название элемента
|
|
1039
|
+
- `match: AbilityMatch` - результат проверки
|
|
1040
|
+
|
|
1041
|
+
| Метод | Аргументы | Возвращаемое значение | Описание |
|
|
1042
|
+
|-------|-----------|----------------------|----------|
|
|
1043
|
+
| `toString()` | `indent: number = 0` | `string` | Форматирует объяснение в читаемый текст с отступами |
|
|
1044
|
+
|
|
1045
|
+
**Наследники:**
|
|
1046
|
+
- `AbilityExplainRule` - объяснение для правила
|
|
1047
|
+
- `AbilityExplainRuleSet` - объяснение для группы правил
|
|
1048
|
+
- `AbilityExplainPolicy` - объяснение для политики
|
|
1049
|
+
|
|
1050
|
+
---
|
|
1051
|
+
|
|
1052
|
+
### Класс AbilityParser
|
|
1053
|
+
|
|
1054
|
+
Предназначен для парсинга и генерации типов из конфигураций.
|
|
1055
|
+
|
|
1056
|
+
| Метод | Аргументы | Возвращаемое значение | Описание |
|
|
1057
|
+
|-------|-----------|----------------------|----------|
|
|
1058
|
+
| `generateTypeDefs()` | `policies: readonly AbilityPolicy[]` | `string` | Статический метод. Генерирует TypeScript тип `Resources` на основе правил во всех политиках. Анализирует условия правил и определяет соответствующие типы (string, number, boolean, массивы). Возвращает строку с готовым TypeScript определением типа. |
|
|
1059
|
+
|
|
1060
|
+
#### Пример использования:
|
|
1061
|
+
|
|
1062
|
+
```typescript
|
|
1063
|
+
import { AbilityParser, AbilityPolicy } from '@via-profit/ability';
|
|
1064
|
+
import fs from 'node:fs';
|
|
1065
|
+
|
|
1066
|
+
// Массив политик
|
|
1067
|
+
const policies: AbilityPolicy[] = AbilityPolicy.parseAll([
|
|
1068
|
+
{
|
|
1069
|
+
id: 'policy-1',
|
|
1070
|
+
name: 'Example policy',
|
|
1071
|
+
action: 'order.status',
|
|
1072
|
+
effect: 'deny',
|
|
1073
|
+
compareMethod: 'and',
|
|
1074
|
+
ruleSet: [
|
|
1075
|
+
{
|
|
1076
|
+
id: 'rule-set-1',
|
|
1077
|
+
name: 'Example rule set',
|
|
1078
|
+
compareMethod: 'and',
|
|
1079
|
+
rules: [
|
|
1080
|
+
{
|
|
1081
|
+
subject: 'user.roles',
|
|
1082
|
+
resource: ['admin'],
|
|
1083
|
+
condition: 'in',
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
subject: 'order.amount',
|
|
1087
|
+
resource: 1000,
|
|
1088
|
+
condition: '<=',
|
|
1089
|
+
},
|
|
1090
|
+
],
|
|
1091
|
+
},
|
|
1092
|
+
],
|
|
1093
|
+
},
|
|
1094
|
+
]);
|
|
1095
|
+
|
|
1096
|
+
// Генерация TypeScript типа
|
|
1097
|
+
const typeDefinitions = AbilityParser.generateTypeDefs(policies);
|
|
1098
|
+
|
|
1099
|
+
// Запись в файл
|
|
1100
|
+
fs.writeFileSync('./src/types/ability.ts', typeDefinitions);
|
|
1101
|
+
|
|
1102
|
+
// Результат в файле:
|
|
1103
|
+
// // Automatically generated by via-profit/ability
|
|
1104
|
+
// // Do not edit manually
|
|
1105
|
+
//
|
|
1106
|
+
// export type Resources = {
|
|
1107
|
+
// ['order.status']: {
|
|
1108
|
+
// readonly order: {
|
|
1109
|
+
// readonly amount: number;
|
|
1110
|
+
// };
|
|
1111
|
+
// readonly user: {
|
|
1112
|
+
// readonly roles: string[];
|
|
1113
|
+
// };
|
|
1114
|
+
// };
|
|
1115
|
+
// }
|
|
433
1116
|
```
|
|
434
1117
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
1118
|
+
---
|
|
1119
|
+
|
|
1120
|
+
### Классы ошибок
|
|
1121
|
+
|
|
1122
|
+
| Класс | Назначение |
|
|
1123
|
+
|-------|------------|
|
|
1124
|
+
| `AbilityError` | Общая ошибка доступа, выбрасывается при запрете в `enforce()` |
|
|
1125
|
+
| `AbilityParserError` | Ошибка парсинга конфигурации или генерации типов |
|
|
1126
|
+
|
|
1127
|
+
## Рекомендации по использованию
|
|
1128
|
+
|
|
1129
|
+
### Именование экшенов
|
|
1130
|
+
|
|
1131
|
+
Используйте точечную нотацию для иерархии действий:
|
|
1132
|
+
|
|
1133
|
+
- `order.create` - создание заказа
|
|
1134
|
+
- `order.update` - обновление заказа
|
|
1135
|
+
- `order.status.update` - обновление статуса заказа
|
|
1136
|
+
|
|
1137
|
+
### Структура данных
|
|
1138
|
+
|
|
1139
|
+
Старайтесь группировать связанные данные под общими ключами:
|
|
1140
|
+
|
|
1141
|
+
```ts
|
|
1142
|
+
// Хорошо
|
|
1143
|
+
{
|
|
1144
|
+
user: { id: '123', roles: ['admin'] },
|
|
1145
|
+
order: { status: 'new', amount: 1000 }
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Плохо
|
|
1149
|
+
{
|
|
1150
|
+
userId: '123',
|
|
1151
|
+
userRoles: ['admin'],
|
|
1152
|
+
orderStatus: 'new',
|
|
1153
|
+
orderAmount: 1000
|
|
1154
|
+
}
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
### Проектирование политик
|
|
1158
|
+
|
|
1159
|
+
- Используйте `deny` для запрещающих политик, `permit` для разрешающих
|
|
1160
|
+
- Комбинируйте простые правила для сложной логики
|
|
1161
|
+
- Давайте понятные имена правилам и политикам для упрощения отладки
|
|
1162
|
+
|
|
1163
|
+
## Отладка политик
|
|
1164
|
+
|
|
1165
|
+
Используйте `resolveWithExplain()` для получения детальной информации о процессе проверки:
|
|
1166
|
+
|
|
1167
|
+
```ts
|
|
1168
|
+
const explanations = resolver.resolveWithExplain('order.update', data);
|
|
1169
|
+
explanations.forEach(exp => console.log(exp.toString()));
|
|
1170
|
+
// ✓ policy «Запрет доступа для менеджеров» is match
|
|
1171
|
+
// ✓ ruleSet «Менеджеры» is match
|
|
1172
|
+
// ✓ rule «Отдел managers» is match
|
|
1173
|
+
// ✗ rule «Роль manager» is mismatch
|
|
1174
|
+
// ✓ ruleSet «Не администраторы» is match
|
|
1175
|
+
// ✓ rule «Нет роли administrator» is match
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
## Лицензия
|
|
438
1180
|
|
|
1181
|
+
Этот проект лицензирован под лицензией MIT. Подробности в файле [LICENSE](LICENSE).
|