@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/README.md CHANGED
@@ -7,27 +7,52 @@
7
7
 
8
8
  ## Содержание
9
9
 
10
- ## Оглавление
11
- - [Обзор](#overview)
12
- - [Состав пакета](#structure)
13
- - [Основные принципы](#principles)
14
- - [Правила](#rules)
15
- - [Создание правила](#rule-creation)
16
- - [Проверка правила](#rule-check)
17
- - [Группы правил](#rule-sets)
18
- - [Создание группы правил](#ruleset-creation)
19
- - [Проверка группы правил](#ruleset-check)
20
- - [Политики](#policies)
21
- - [Создание политики](#policy-creattion)
22
- - [Проверка политики](#policy-check)
23
- - [Управление политиками](#policy-management)
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
- ## Обзор <a name="overview"></a>
53
+ ## Обзор
29
54
 
30
- ### Состав пакета <a name="structure"></a>
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
- ### Основные принципы <a name="principles"></a>
69
+ ### Основные принципы
44
70
 
45
71
  Работа сервиса основана на формировании **правил**, объединении их в **политики** и проверке доступа с их помощью.
46
72
 
@@ -51,7 +77,7 @@
51
77
 
52
78
  Структура политики:
53
79
 
54
- ![ability-01.drawio.png](assets/ability-01.drawio.png)
80
+ ![ability-01.png](./assets/ability-01.drawio.png)
55
81
 
56
82
  JSON-конфигурация:
57
83
 
@@ -110,12 +136,12 @@ AbilityPolicy.parse(jsonConfig).check({
110
136
 
111
137
  ---
112
138
 
113
- ## Правила <a name="rules"></a>
139
+ ## Правила
114
140
 
115
141
  **Правила** выполняют условие проверки и возвращают результат. **Основная цель** - выполнить сравнение переданных
116
142
  значений субъекта и ресурса, а затем вернуть результат такого сравнения.
117
143
 
118
- ### Создание правила <a name="rule-creation"></a>
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
- ### Проверка правила <a name="rule-check"></a>
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
- ## Группы правил <a name="rule-sets"></a>
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
- ### Создание группы правил <a name="ruleset-creation"></a>
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
- ### Проверка группы правил <a name="ruleset-check"></a>
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
- ## Политики <a name="policies"></a>
415
+ ## Политики
280
416
 
281
417
  **Политики** включают в себя группы правил. Основная цель - выполнить проверку всех вложенных групп, сравнить результат
282
418
  выполнения групп и вернуть один единственный результат.
283
419
 
284
- ### Создание политики <a name="policy-creattion"></a>
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[]` Массив групп (см. [Группы правил](#rule-sets))
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
- ### Проверка политики <a name="policy-check"></a>
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
- ## Управление политиками <a name="policy-management"></a>
516
+ ## Управление политиками
517
+
518
+ Класс `AbilityResolver` — это основной инструмент для применения политик в рантайме. Он решает две ключевые задачи:
519
+
520
+ 1. **Фильтрация политик по действию** — выбирает только те политики, которые применимы к выполняемой операции
521
+ 2. **Оценка разрешений** — последовательно проверяет отобранные политики и возвращает итоговый результат (разрешено/запрещено)
381
522
 
382
- Для управления политиками реализован специальный класс `AbilityResolver`.
523
+ ### Зачем нужен AbilityResolver?
383
524
 
384
- В случае, если вам необходимо запустить лишь разовую проверку данных, то данный раздел можно опустить.
525
+ Представим, что в системе есть десятки политик, каждая на своё действие:
526
+ - `order.create` — правила создания заказа
527
+ - `order.update` — правила обновления заказа
528
+ - `order.delete` — правила удаления заказа
529
+ - `user.profile.update` — правила обновления профиля
530
+ - и так далее...
385
531
 
386
- **AbilityResolver** необходим для возможности запуска проверки разных политик в разный период времени.
532
+ Когда пользователь пытается создать заказ, нам нужно проверить только политики, связанные с действием `order.create`, игнорируя все остальные. Именно это и делает `AbilityResolver`.
387
533
 
388
- Политики содержат название экшена (поле `action`) определяемого разработчиком. Запуск метода `enforce` или `resolve`
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
- import { AbilityPolicy, AbilityPolicyConfig, AbilityResolver } from './AbilityPolicy';
393
-
394
- const config: AbilityPolicyConfig[] = [...]; // массив различных политик (JSON)
395
- const policies: AbilityPolicy<Resources>[] = config.map(cfg => AbilityPolicy.parse(cfg)); // массив уже созданных политик
396
-
397
- // Проверка политик с экшеном `order.create`
398
- // Варинат 1. Будет выброшено исключение AbilityError
399
- // c название политики, которая вернула deny,
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
- // Вариант 2.
407
- const isDeny = new AbilityResolver(policies)
408
- .resolve('order.create', {
409
- user: { department: 'managers' },
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
- .isDeny();
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
- if (isDeny) {
414
- throw new AbilityError('Permission denied');
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
- ['order.status']: { // <-- название экшена
421
- readonly account: { // <-- данные ресурса
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
- ['order.create']: {
789
+
790
+ 'order.update': {
426
791
  readonly user: {
427
- readonly department: string;
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
- по указанному экшену, а самое главное, что при помощи типа `Resources`, который необходимо формировать
437
- вручную, **TypeScript** подскажет какие именно данные следует передать вторым аргументом (ресурс)._
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).