@via-profit/ability 2.0.0-rc.8 → 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
@@ -3,264 +3,1179 @@
3
3
  > Набор сервисов, частично реализующих
4
4
  > принцип [Attribute Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control)
5
5
 
6
- Данный сервис позволяет создать правила или политики, а затем применить их по отношению к каким-либо данным для того
7
- чтобы проверить наличие доступа к этим данным.
8
-
9
- # Draft
6
+ Этот сервис позволяет создавать правила и политики, применять их к данным и проверять доступ на их основе.
10
7
 
11
8
  ## Содержание
12
9
 
13
- 1. [Описание и общие принципы](#overview)
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
+ - [Лицензия](#лицензия)
50
+
51
+ ---
52
+
53
+ ## Обзор
54
+
55
+ ### Состав пакета
56
+
57
+ - **`AbilityRule`** — класс отдельного правила
58
+ - **`AbilityRuleSet`** — класс группы правил
59
+ - **`AbilityPolicy`** — класс политики
60
+ - **`AbilityResolver`** — управление политиками
61
+ - **`AbilityMatch`** — константы состояния правил (`pending`, `match`, `mismatch`)
62
+ - **`AbilityCompare`** — способы сравнения (`or`, `and`)
63
+ - **`AbilityCondition`** — методы вычисления (`equal`, `not_equal`, `more_than`, `less_than`, `in`, `not_in` и др.)
64
+ - **`AbilityPolicyEffect`** — эффекты политики (`deny`, `permit`)
65
+ - **`AbilityParser`** — парсер конфигурационных правил (JSON) и генератор `Typescript` типов
66
+ - **`AbilityError`** — инстанс ошибок
67
+ - **`AbilityExplain`** — вспомогательный инструмент, который позволяет получить человекочитаемое объяснение того, почему конкретное действие разрешено или запрещено текущей конфигурацией Ability
68
+
69
+ ### Основные принципы
70
+
71
+ Работа сервиса основана на формировании **правил**, объединении их в **политики** и проверке доступа с их помощью.
72
+
73
+ Пример: необходимо **запретить доступ** пользователям, связанным с отделом менеджеров, **за исключением администраторов**.
74
+
75
+ - Менеджеры — если их отдел `managers` или есть роль `manager`
76
+ - Администраторы — пользователи с ролью `administrator`
77
+
78
+ Структура политики:
79
+
80
+ ![ability-01.png](./assets/ability-01.drawio.png)
81
+
82
+ JSON-конфигурация:
83
+
84
+ ```json
85
+ {
86
+ "name": "Запрет доступа для менеджеров (исключение: администраторы)",
87
+ "compareMethod": "and",
88
+ "action": "order.update",
89
+ "effect": "deny",
90
+ "ruleSet": [
91
+ {
92
+ "name": "Менеджеры",
93
+ "compareMethod": "or",
94
+ "rules": [
95
+ {
96
+ "name": "Отдел managers",
97
+ "subject": "user.department",
98
+ "resource": "managers",
99
+ "condition": "in"
100
+ },
101
+ {
102
+ "name": "Роль manager",
103
+ "subject": "user.roles",
104
+ "resource": "manager",
105
+ "condition": "in"
106
+ }
107
+ ]
108
+ },
109
+ {
110
+ "name": "Не администраторы",
111
+ "compareMethod": "and",
112
+ "rules": [
113
+ {
114
+ "name": "Нет роли administrator",
115
+ "subject": "user.roles",
116
+ "resource": "administrator",
117
+ "condition": "not in"
118
+ }
119
+ ]
120
+ }
121
+ ]
122
+ }
123
+ ```
124
+
125
+ Применение политики:
126
+
127
+ ```ts
128
+ const jsonConfig = { ... };
129
+ AbilityPolicy.parse(jsonConfig).check({
130
+ user: {
131
+ department: 'managers',
132
+ roles: ['manager', 'coach'],
133
+ }
134
+ });
135
+ ```
136
+
137
+ ---
14
138
 
15
- 1.1 [Состав пакета](#structure)
139
+ ## Правила
16
140
 
17
- 1.2 [Общие принципы](#principles)
141
+ **Правила** выполняют условие проверки и возвращают результат. **Основная цель** - выполнить сравнение переданных
142
+ значений субъекта и ресурса, а затем вернуть результат такого сравнения.
18
143
 
19
- 2. [Правила](#rules)
144
+ ### Создание правила
20
145
 
21
- 2.1 [Синтаксис правил](#rule-syntax)
146
+ Создать правило можно двумя способами: создание через конструктор класса и парсинг JSON-конфига правила.
22
147
 
23
- 2.2 [Примеры](#rule-recipes)
148
+ При создании необходимо указать следующие параметры:
24
149
 
25
- 2.3 [Класс AbilityRule](#ability-rule-class)
150
+ - **id** - `string` Уникальный идентификатор.
151
+ - **name** - `string` Название правила.
152
+ - **condition** - `AbilityCondition` Определяет условия сравнения переданных данных
153
+ - **subject** - `string` Dot notation путь в проверяемом субъекте, например: `user.name`.
154
+ - **resource** - `string | number | boolean | (string | number)[]` Dot notation путь в проверяемом ресурсе, например:
155
+ `user.name` или значение, которое может быть строкой, числом, булеан значением или массивом строк, или чисел.
26
156
 
27
- 3. [Политики](#policies)
157
+ _Создание правила через конструктор класса:_
28
158
 
29
- 3.1 [Синтаксис правил](#policy-syntax)
159
+ ```ts
160
+ import { AbilityRule, AbilityCondition } from '@via-profit/ability';
30
161
 
31
- 3.2 [Примеры](#policy-recipes)
162
+ const rule = new AbilityRule({
163
+ id: '<rule-id>',
164
+ name: 'Пользователь из отдела managers',
165
+ subject: 'user.department',
166
+ resource: 'managers',
167
+ condition: AbilityCondition.equal
168
+ });
32
169
 
33
- 3.3 [Класс AbilityPolicy](#ability-policy-class)
170
+ // сокращённая запись
171
+ const rule2 = AbilityRule.equal(
172
+ 'user.department', // subject
173
+ 'managers' // resource
174
+ );
34
175
 
35
- 4. [Создание политики из конфига](#policy-config)
176
+ ```
36
177
 
37
- ## Описание и общие принципы <a name="overview"></a>
178
+ _Создание правила через парсинг JSON-конфигурации:_
38
179
 
39
- ### Состав пакета <a name="structure"></a>
180
+ ```ts
181
+ import { AbilityRule } from '@via-profit/ability';
182
+
183
+ const rule = AbilityRule.parse({
184
+ "id": "<rule-id>",
185
+ "name": "Пользователь из отдела managers",
186
+ "subject": "user.department",
187
+ "resource": "managers",
188
+ "condition": "="
189
+ });
40
190
 
41
- - `AbilityPolicy` - класс политики
42
- - `AbilityRuleSet` - класс группы правил
43
- - `AbilityRule` - класс правила
44
- - `AbilityParser` - парсер конфигурационных правил из/в JSON
45
- - `AbilityResolver` - класс управления политиками
46
- - `AbilityMatch` - Класс констант для определния соответствия правил (`PENDING` `MATCH` `MISMATCH`)
47
- - `AbilityPolicyEffect` - Класс констант для определния эффекта политик (`DENY` `PERMIT`)
48
- - `AbilityCompare` - Класс констант для определния способа сравнения правил и групп (`OR` `AND`)
49
- - `AbilityCondition` - Класс констант для определния метода вычисления правил (`EQUAL` `NOT_EQUAL` `MORE_THAN`
50
- `LESS_THAN` `LESS_OR_EQUAL` `MORE_OR_EQUAL` `IN` `NOT_IN`)
51
- - `AbilityError` - Класс инстанса ошибки
52
- - `AbilityCode` - Базовый клас констанкт
191
+ ```
53
192
 
54
- ### Общие принципы <a name="principles"></a>
193
+ ### Проверка правила
55
194
 
56
- Принцип работы основан на формировании правил, объединения их в политики и запуске этих политик.
195
+ Для проверки правила следует вызвать метод `check` класса `AbilityRule` передав объект проверяемого ресурса. Этот метод
196
+ вернёт экземпляр класса
197
+ `AbilityMatch`, при помощи методов которого можно определить имеется ли совпадение правила и переданных значений.
57
198
 
58
- Предположим, что необходимо запретить доступ пользователям из отдела менеджеров и причастным к ним, за исключением
59
- администраторов. Пользователи относятся к отделу менеджеров, если их отдел называется `managers`. Причастные
60
- пользователи являются те пользователи, среди ролей которых имеется роль `manager`. Администраторы - пользователи имеющие
61
- соответствующую роль (`administrator`).
199
+ ```ts
200
+ import { AbilityRule } from '@via-profit/ability';
201
+
202
+ const rule = AbilityRule.parse({
203
+ "id": "<rule-id>",
204
+ "name": "Пользователь из отдела managers",
205
+ "subject": "user.department",
206
+ "resource": "managers",
207
+ "condition": "="
208
+ });
62
209
 
63
- Для решения поставленной задачи нам понадобится объединить несколько правил в группы согласно изображению ниже:
210
+ const match = rule.check({
211
+ user: {
212
+ department: 'managers',
213
+ },
214
+ });
64
215
 
65
- ![ability-01.drawio.png](assets/ability-01.drawio.png)
216
+ const is = match.isEqual(AbilityMatch.match); // true
66
217
 
67
- JSON представление такой политики будет иметь следующий вид:
218
+ ```
68
219
 
69
- ```json
70
- {
71
- "name": "Запретить доступ пользователям из отдела менеджеров и причастным к ним за исключением администраторов",
72
- "compareMethod": 1,
73
- "action": "order.update",
74
- "effect": 0,
75
- "ruleSet": [
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: [
76
234
  {
77
- "name": "Менеджеры",
78
- "compareMethod": 0,
79
- "rules": [
235
+ id: '9cc009e5-0aa9-453a-a668-cb3f418ced92',
236
+ name: 'Не администратор',
237
+ compareMethod: 'and',
238
+ rules: [
80
239
  {
81
- "name": "Пользователь состоит в отделе managers",
82
- "matches": [
83
- "user.department",
84
- "=",
85
- "managers"
86
- ]
240
+ id: '4093cd50-e54f-4062-8053-2d3b5966fad3',
241
+ name: 'Нет роли администраторов',
242
+ subject: 'user.roles',
243
+ resource: 'administrator',
244
+ condition: 'not in',
87
245
  },
246
+ ],
247
+ },
248
+ {
249
+ id: '2f8f9d71-860b-4fa6-b395-9331f1f0848e',
250
+ name: 'Проверка статуса `не обработан` -> `завершен`',
251
+ compareMethod: 'and',
252
+ rules: [
88
253
  {
89
- "name": "Пользователь имеет роль manager",
90
- "matches": [
91
- "user.roles",
92
- "in",
93
- "manager"
94
- ]
95
- }
96
- ]
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
+ ],
97
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
+
317
+ ---
318
+
319
+ ## Группы правил
320
+
321
+ **Группы правил** необходимы для объединения нескольких правил в группу. **Основная цель** - выполнить проверку каждого
322
+ правила в группе и вернуть лишь один результат.
323
+
324
+ Создавая группу следует указывать метод сравнения (`compareMethod`), который необходим для вычисления значения всей
325
+ группы при проверке правил.
326
+
327
+ При создании необходимо указать следующие параметры:
328
+
329
+ - **id** - `string` Уникальный идентификатор.
330
+ - **name** - `string` Название группы.
331
+ - **compareMethod** - `AbilityCompare` Способ сравнения правил в группе (`or` или `and`).
332
+
333
+ _Влияние **compareMethod** на результат вычисления группы:_
334
+
335
+ - **`or`** - Результат всей группы примет значение `match`, если хотя бы одно из правил вернуло `match`.
336
+ - **`and`** - Результат всей группы примет значение `match`, если все правила вернули `match`.
337
+
338
+ ### Создание группы правил
339
+
340
+ Создать группу правил можно двумя способами: создание через конструктор класса и парсинг JSON-конфига группы.
341
+
342
+ _Создание группы через конструктор класса_:
343
+
344
+ ```ts
345
+ import { AbilityRuleSet, AbilityCompare } from '@via-profit/ability';
346
+
347
+ const ruleSet = new AbilityRuleSet({
348
+ id: '<set-id>',
349
+ name: 'Название группы',
350
+ compareMethod: AbilityCompare.and,
351
+ });
352
+
353
+ // Добавление правил в группу
354
+ ruleSet.addRules([
355
+ new AbilityRule(...),
356
+ new AbilityRule(...),
357
+ ]);
358
+
359
+
360
+ // Сокращённая запись
361
+ const ruleSet2 = AbilityRuleSet.and([
362
+ new AbilityRule(...),
363
+ new AbilityRule(...),
364
+ ]);
365
+
366
+ ```
367
+
368
+ _Создание группы через парсинг JSON-конфига группы_:
369
+
370
+ ```ts
371
+ import { AbilityRuleSet } from '@via-profit/ability';
372
+
373
+ const ruleSet = AbilityRuleSet.parse({
374
+ 'id': '<set-id>',
375
+ 'name': 'Название группы',
376
+ 'compareMethod': 'and',
377
+ 'rules': [
98
378
  {
99
- "name": "Не администраторы",
100
- "compareMethod": 1,
379
+ 'id': '<rule-id>',
380
+ 'name': 'Пользователь из отдела managers',
381
+ 'subject': 'user.department',
382
+ 'resource': 'managers',
383
+ 'condition': '=',
384
+ },
385
+ ],
386
+ });
387
+
388
+ ```
389
+
390
+ ### Проверка группы правил
391
+
392
+ Для проверки группы правил следует вызвать метод `check` класса `AbilityRuleSet` передав объект проверяемого ресурса.
393
+ Этот метод вернёт экземпляр класса `AbilityMatch`, при помощи методов которого можно определить имеется ли совпадение
394
+ для группы и переданных значений.
395
+
396
+ ```ts
397
+ import { AbilityRuleSet, AbilityCompare } from '@via-profit/ability';
398
+
399
+ const ruleSet = new AbilityRuleSet({
400
+ id: '<set-id>',
401
+ name: 'Название группы',
402
+ compareMethod: AbilityCompare.and,
403
+ }).addRules([
404
+ new AbilityRule(...),
405
+ new AbilityRule(...),
406
+ ]);
407
+
408
+ const match = rule.check({ ... });
409
+
410
+ const is = match.isEqual(AbilityMatch.match);
411
+ ```
412
+
413
+ ___
414
+
415
+ ## Политики
416
+
417
+ **Политики** включают в себя группы правил. Основная цель - выполнить проверку всех вложенных групп, сравнить результат
418
+ выполнения групп и вернуть один единственный результат.
419
+
420
+ ### Создание политики
421
+
422
+ Создать политику можно двумя способами: создание через конструктор класса и парсинг JSON-конфига политики.
423
+
424
+ При создании политики необходимо указать следующие параметры:
425
+
426
+ - **id** - `string` Уникальный идентификатор.
427
+ - **name** - `string` Название политики.
428
+ - **action** - `string` Ключ политики, в формате Dot notation, определяющий схожесть политик. В названии может
429
+ применяться символ звездочки (`*`). Политики с одинаковым экшеном обрабатываются вместе как группа политик. Экшен
430
+ `users.account` не считается похожим с экшеном `users.account.login`, но в это же время `users.account.*` равен экшену
431
+ `users.account.login` (из-за использования звездочки).
432
+ - **compareMethod** - `AbilityCompare` Метод сравнения групп правил, входящих в политику (`or` или `and`)
433
+ - **effect** - `AbilityPolicyEffect` Определяет итоговый результат всех вычислений (`permit` или `deny`). В слчае
434
+ использования класса `AbilityResolver` (метод `enforce`) последний выкинет исключение `AbilityError`, если политика
435
+ вернёт `deny`. Текст сообщения `AbilityError` будет соответствовать названию сработавшей политики. В остальных случаях
436
+ ничего не произойдет.
437
+ - **ruleSet** - `AbilityRuleSet[]` Массив групп (см. [Группы правил](#группы-правил))
438
+
439
+ **Замечание** - Политика может быть запрещающей (`effect` = `deny`) и разрешающей (`effect` = `permit`). Если вам
440
+ необходимо ограничить какой-либо доступ, например, пользователю с недостаточными правами, то следует создавать политику
441
+ с эффектом `deny`.
442
+
443
+ _Создание политики через конструктор класса_:
444
+
445
+ ```ts
446
+ import { AbilityPolicy, AbilityCondition } from '@via-profit/ability';
447
+
448
+ const policy = new AbilityPolicy({
449
+ id: '<policy-id>',
450
+ name: 'Пример политики',
451
+ effect: 'deny',
452
+ action: 'users.update',
453
+ compareMethod: 'and',
454
+ ruleSet: [
455
+ new AbilityRule({
456
+ id: '<rule-id>',
457
+ name: 'Пользователь является владельцем заказа',
458
+ subject: 'user.id',
459
+ resource: 'order.owner',
460
+ condition: AbilityCondition.equal
461
+ })
462
+ ]
463
+ });
464
+
465
+ ```
466
+
467
+ _Создание политики через парсинг JSON-конфига_:
468
+
469
+ ```ts
470
+ import { AbilityPolicy } from '@via-profit/ability';
471
+
472
+ const policy = AbilityPolicy.parse({
473
+ "id": "bb758c1b-1015-4894-ba25-d23156e063cf",
474
+ "name": "Status hui",
475
+ "action": "order.status",
476
+ "effect": "deny",
477
+ "compareMethod": "and",
478
+ "ruleSet": [
479
+ {
480
+ "id": "9cc009e5-0aa9-453a-a668-cb3f418ced92",
481
+ "name": "Не администратор",
482
+ "compareMethod": "and",
101
483
  "rules": [
102
484
  {
103
- "name": "Пользователь не является администратором",
104
- "matches": [
105
- "user.roles",
106
- "not in",
107
- "administrator"
108
- ]
485
+ "id": "4093cd50-e54f-4062-8053-2d3b5966fad3",
486
+ "name": "Нет роли администраторв",
487
+ "subject": "account.roles",
488
+ "resource": "administrator",
489
+ "condition": "<>"
109
490
  }
110
491
  ]
111
492
  }
112
493
  ]
494
+ });
495
+
496
+ ```
497
+
498
+ ### Проверка политики
499
+
500
+ Для проверки политики правил следует вызвать метод `check` класса `AbilityPolicy` передав объект проверяемого ресурса.
501
+ Этот метод вернёт экземпляр класса `AbilityMatch`, при помощи методов которого можно определить имеется ли совпадение
502
+ для группы и переданных значений.
503
+
504
+ ```ts
505
+ import { AbilityPolicy } from '@via-profit/ability';
506
+
507
+ const policy = AbilityPolicy.parse({ ... });
508
+
509
+ const match = policy.check({ ... });
510
+
511
+ const is = match.isEqual(AbilityMatch.match);
512
+ ```
513
+
514
+ ___
515
+
516
+ ## Управление политиками
517
+
518
+ Класс `AbilityResolver` — это основной инструмент для применения политик в рантайме. Он решает две ключевые задачи:
519
+
520
+ 1. **Фильтрация политик по действию** — выбирает только те политики, которые применимы к выполняемой операции
521
+ 2. **Оценка разрешений** — последовательно проверяет отобранные политики и возвращает итоговый результат (разрешено/запрещено)
522
+
523
+ ### Зачем нужен AbilityResolver?
524
+
525
+ Представим, что в системе есть десятки политик, каждая на своё действие:
526
+ - `order.create` — правила создания заказа
527
+ - `order.update` — правила обновления заказа
528
+ - `order.delete` — правила удаления заказа
529
+ - `user.profile.update` — правила обновления профиля
530
+ - и так далее...
531
+
532
+ Когда пользователь пытается создать заказ, нам нужно проверить только политики, связанные с действием `order.create`, игнорируя все остальные. Именно это и делает `AbilityResolver`.
533
+
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:
554
+
555
+ ```ts
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
+ }
564
+
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
+ // ... правила
113
581
  }
114
582
  ```
115
583
 
116
- Теперь для того чтобы применить (проверить политику) необходимо её восстановить из JSON (метод `parse`) и запустить
117
- проверку (метод `check`):
584
+ #### Приоритет и множественное совпадение
585
+
586
+ Если несколько политик подходят под проверяемое действие, будут применены **все** подходящие политики. Результат определяется последней сработавшей политикой:
118
587
 
119
588
  ```ts
120
- const jsonConfig = { ... }
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
+ // ... правила
599
+ })
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);
121
690
 
122
- const result = AbilityPolicy.parse(jsonConfig).check({
691
+ // При выполнении действия указываем только нужный action
692
+ // AbilityResolver автоматически отфильтрует политики и проверит их
693
+ resolver.enforce('order.create', {
123
694
  user: {
124
695
  department: 'managers',
125
- roles: ['manager', 'couch']
696
+ roles: ['manager']
697
+ },
698
+ order: {
699
+ amount: 5000
126
700
  }
127
701
  });
128
- ````
702
+ ```
129
703
 
130
- ## Правила <a name="rules"></a>
704
+ ### Проверка соответствия действия
131
705
 
132
- Правила позволяют создавать условия, которые в последствии будут сгруппированы в группу правил, а те, в свою очередь, в
133
- политику.
706
+ Метод `isInActionContain` позволяет проверить, соответствует ли действие шаблону с wildcard:
134
707
 
135
- Класс `AbilityRule`
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
+ ```
136
715
 
137
- Правило определяется необязательных названием и обязательным массивом `matches`, который и несёт самую важную роль в
138
- данном модуле.
716
+ Этот метод используется внутри `AbilityResolver` для фильтрации политик, но может быть полезен и в пользовательском коде.
139
717
 
140
- _Пример простого правила:_
718
+ ### Методы AbilityResolver
141
719
 
142
- ```ts
143
- import { AbilityRule, AbilityCondition } from '@via-profit/ability';
144
-
145
- const rule = new AbilityRule({
146
- name: 'Simple rule',
147
- matches: [
148
- 'user.department', // dot notation путь до проверяемого поля
149
- AbilityCondition.EQUAL, // определяет метод сравнения "="
150
- 'managers' // искомое значение
151
- ],
152
- });
720
+ #### `enforce()` — строгая проверка
153
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
+ }
154
735
  ```
155
736
 
156
- Правило выше будет выполнено в случае, если среди проверямых данных будет ключ `user`, содержащий ключ `department`,
157
- значение которого будет равно (`=`) `managers`
737
+ **Важно**: `enforce()` выбрасывает исключение, если хотя бы одна подходящая политика вернула `deny`. Если ни одна политика не сработала или все вернули `permit` исключения не будет.
158
738
 
159
- Адрес поля `user.department`, представляет собой запись в
160
- формате [dot notation](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Basics#dot_notation), что
161
- указывает, что для сравнения данных будет использоваться поле department ресурса `user`.
162
- Для сравнения двух отделов будет использоваться оператор сравнения `=`.
739
+ #### `resolve()` мягкая проверка
163
740
 
164
- Теперь, для того чтобы проверить правило, необходимо выполнить метод `check` передав необходимые ресурсы:
741
+ ```typescript
742
+ const result = resolver
743
+ .resolve('order.update', data)
744
+ .isDeny(); // true если доступ запрещен
165
745
 
166
- ```ts
167
- import { AbilityRule, AbilityCondition } from '@via-profit/ability';
746
+ if (result) {
747
+ // Самостоятельно обрабатываем запрет
748
+ return { error: 'Access denied' };
749
+ }
168
750
 
169
- const rule = new AbilityRule({
170
- name: 'Simple rule',
171
- matches: [
172
- 'user.department', // dot notation путь до проверяемого поля
173
- AbilityCondition.EQUAL, // определяет метод сравнения "="
174
- 'managers' // искомое значение
175
- ],
751
+ // Продолжаем выполнение
752
+ await updateOrder(data);
753
+ ```
754
+
755
+ #### `resolveWithExplain()` проверка с объяснением
756
+
757
+ ```typescript
758
+ const explanations = resolver.resolveWithExplain('order.update', data);
759
+
760
+ explanations.forEach(explain => {
761
+ console.log(explain.toString());
762
+ // ✓ policy «Политика обновления заказа» is match
763
+ // ✗ ruleSet «Проверка владельца» is mismatch
764
+ // ✓ ruleSet «Проверка статуса» is match
176
765
  });
177
766
 
178
- const result = rule.check({
767
+ if (resolver.isDeny()) {
768
+ console.log('Доступ запрещен по причине:');
769
+ explanations.forEach(e => console.log(e.toString()));
770
+ }
771
+ ```
772
+
773
+ ### Интеграция с TypeScript
774
+
775
+ Для полной типобезопасности определите интерфейс `Resources`, где ключи — это возможные действия, а значения — структура данных, требуемая для проверки:
776
+
777
+ ```typescript
778
+ // Определяем типы данных для каждого действия
779
+ type Resources = {
780
+ 'order.create': {
781
+ readonly user: {
782
+ readonly department: string;
783
+ readonly roles: readonly string[];
784
+ };
785
+ readonly order: {
786
+ readonly amount: number;
787
+ };
788
+ };
789
+
790
+ 'order.update': {
791
+ readonly user: {
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;
807
+ };
808
+ };
809
+ };
810
+
811
+ // Теперь TypeScript будет проверять корректность передаваемых данных
812
+ resolver.enforce('order.create', {
179
813
  user: {
180
814
  department: 'managers',
815
+ roles: ['manager']
816
+ },
817
+ order: {
818
+ amount: 5000 // ✅ все поля на месте
181
819
  }
182
820
  });
183
821
 
822
+ // ❌ Ошибка компиляции - не хватает required полей
823
+ resolver.enforce('order.create', {
824
+ user: {
825
+ department: 'managers'
826
+ // missing roles
827
+ }
828
+ });
184
829
  ```
185
830
 
186
- ### Синтаксис правил <a name="rule-syntax"></a>
831
+ ### Как формируется итоговое решение?
187
832
 
188
- Для описания правил используется массив следующего типа:
833
+ 1. Resolver собирает все политики, у которых `action` соответствует запрошенному (поддерживается wildcard `*`)
834
+ 2. Последовательно проверяет каждую политику методом `check()`
835
+ 3. Если политика вернула `match`, запоминает её `effect` (permit/deny)
836
+ 4. Возвращается `effect` **последней сработавшей политики**
189
837
 
190
- ```ts
191
- type AbilityRuleMatches = [
192
- string, // dot notation путь до поля в субъекте или энвайронменте
193
- '=' | '<>' | '>' | '<' | '<=' | '>=' | 'in', // оператор сравнения
194
- string | number | boolean, // dot notation путь до поля в ресурсе или энвайронменте или просто значение
838
+ ```typescript
839
+ // Пример с несколькими политиками на одно действие
840
+ const policies = [
841
+ permitPolicy, // permit, но не match
842
+ denyPolicy, // deny и match
843
+ permitPolicy2 // permit, но не match
195
844
  ];
845
+
846
+ resolver.enforce('some.action', data);
847
+ // Результат: deny (от последней сработавшей политики)
196
848
  ```
197
849
 
198
- ### Операторы сравнения <a name="rule-operators"></a>
850
+ ### Когда использовать Resolver?
199
851
 
200
- - `AbilityCondition.EQUAL` (`=`) Прямое сравнение
201
- - `AbilityCondition.NOT_EQUAL` (`<>`) Не равно
202
- - `AbilityCondition.MORE_THAN` (`>`) Больше
203
- - `AbilityCondition.LESS_THAN` (`<`) Меньше
204
- - `AbilityCondition.LESS_OR_EQUAL` (`<=`) Меньше или равно
205
- - `AbilityCondition.MORE_OR_EQUAL` (`>=`) Больше или равно
206
- - `AbilityCondition.IN` (`in` Вхождение в массив. Позволяет проверять вхождение значения в массив
207
- - `AbilityCondition.NOT_IN` (`not in` Нет вхождения в массив. Позволяет проверять отсутствие значения в массив
852
+ - **В API endpoints** — проверка прав перед выполнением операции
853
+ - **В middleware** централизованная проверка доступа
854
+ - **В сервисах** — защита бизнес-логики
855
+ - **В клиентском коде** — условный рендеринг UI на основе прав
208
856
 
209
- ### Примеры правил <a name="rule-recipes"></a>
857
+ Resolver делает систему прав предсказуемой, типобезопасной и легко расширяемой.
210
858
 
211
- <details>
212
- <summary>Пользователь старше 21 года</summary>
859
+ ## API Reference
213
860
 
214
- ```ts
215
- const user = {
216
- age: 18,
217
- };
861
+ ### Класс AbilityCode
218
862
 
219
- const isPermit = new AbilityRule('User age', ['subject.age', '>=', 21]).isPermit(user); // true
220
- ```
863
+ Базовый абстрактный класс для всех кодовых значений в системе.
221
864
 
222
- </details>
865
+ | Метод | Аргументы | Возвращаемое значение | Описание |
866
+ |-------|-----------|----------------------|----------|
867
+ | `isEqual()` | `compareWith: AbilityCode<T> \| null` | `boolean` | Сравнивает текущий код с другим экземпляром |
868
+ | `isNotEqual()` | `compareWith: AbilityCode<T> \| null` | `boolean` | Проверяет неравенство с другим экземпляром |
223
869
 
224
- <details>
225
- <summary>Пользователь имеет роль администратора</summary>
870
+ **Геттеры:**
871
+ - `code: T` - возвращает сырое значение кода (строку или число)
226
872
 
227
- ```ts
228
- const user = {
229
- roles: ['administrator', 'manager'],
230
- };
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
+ Определяет методы логического сравнения для групп правил.
231
914
 
232
- const isPermit = new AbilityRule('has role', ['subject.roles', 'in', 'administrator']).isPermit(
233
- user,
234
- ); // true
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
+ // }
235
1116
  ```
236
1117
 
237
- </details>
1118
+ ---
238
1119
 
239
- <details>
240
- <summary>Пользователь является владельцем заказа</summary>
1120
+ ### Классы ошибок
241
1121
 
242
- ```ts
243
- const user = {
244
- id: '1',
245
- };
1122
+ | Класс | Назначение |
1123
+ |-------|------------|
1124
+ | `AbilityError` | Общая ошибка доступа, выбрасывается при запрете в `enforce()` |
1125
+ | `AbilityParserError` | Ошибка парсинга конфигурации или генерации типов |
246
1126
 
247
- const order = {
248
- owner: '1',
249
- };
1127
+ ## Рекомендации по использованию
1128
+
1129
+ ### Именование экшенов
1130
+
1131
+ Используйте точечную нотацию для иерархии действий:
1132
+
1133
+ - `order.create` - создание заказа
1134
+ - `order.update` - обновление заказа
1135
+ - `order.status.update` - обновление статуса заказа
250
1136
 
251
- const isPermit = new AbilityRule('owner', ['subject.id', '=', 'resource.owner']).isPermit(
252
- user,
253
- order,
254
- ); // true
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
+ }
255
1155
  ```
256
1156
 
257
- </details>
1157
+ ### Проектирование политик
258
1158
 
259
- ## Политики <a name="policies"></a>
1159
+ - Используйте `deny` для запрещающих политик, `permit` для разрешающих
1160
+ - Комбинируйте простые правила для сложной логики
1161
+ - Давайте понятные имена правилам и политикам для упрощения отладки
260
1162
 
261
- Политики позволяют группировать правила или создавать вложенные друг в друга политики.
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
+ ```
262
1177
 
263
1178
 
264
- ## Создание политики из конфига <a name="policy-config"></a>
1179
+ ## Лицензия
265
1180
 
266
- Политику и правила можно создавать не только по средствам классов, но и при помощи конфигураций.
1181
+ Этот проект лицензирован под лицензией MIT. Подробности в файле [LICENSE](LICENSE).