@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/CHANGELOG.md +64 -0
- package/CONTRIBUTING.md +14 -0
- package/LICENSE +21 -0
- package/README.md +1081 -166
- package/SECURITY.md +33 -0
- package/dist/AbilityCode.d.ts +6 -6
- package/dist/AbilityCompare.d.ts +2 -2
- package/dist/AbilityCondition.d.ts +13 -10
- package/dist/AbilityError.d.ts +0 -3
- package/dist/AbilityExplain.d.ts +27 -0
- package/dist/AbilityMatch.d.ts +2 -2
- package/dist/AbilityParser.d.ts +45 -4
- package/dist/AbilityPolicy.d.ts +23 -12
- package/dist/AbilityPolicyEffect.d.ts +2 -2
- package/dist/AbilityResolver.d.ts +2 -0
- package/dist/AbilityRule.d.ts +28 -5
- package/dist/AbilityRuleSet.d.ts +15 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +441 -101
- package/package.json +11 -3
- package/assets/ability-01.drawio.png +0 -0
- package/build/playground.js +0 -814
- package/build/playground.js.map +0 -1
- package/dist/AbilityPolicyResult.d.ts +0 -6
- package/dist/playground.d.ts +0 -26
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
|
-
|
|
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
|
+

|
|
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
|
-
|
|
139
|
+
## Правила
|
|
16
140
|
|
|
17
|
-
|
|
141
|
+
**Правила** выполняют условие проверки и возвращают результат. **Основная цель** - выполнить сравнение переданных
|
|
142
|
+
значений субъекта и ресурса, а затем вернуть результат такого сравнения.
|
|
18
143
|
|
|
19
|
-
|
|
144
|
+
### Создание правила
|
|
20
145
|
|
|
21
|
-
|
|
146
|
+
Создать правило можно двумя способами: создание через конструктор класса и парсинг JSON-конфига правила.
|
|
22
147
|
|
|
23
|
-
|
|
148
|
+
При создании необходимо указать следующие параметры:
|
|
24
149
|
|
|
25
|
-
|
|
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
|
-
|
|
157
|
+
_Создание правила через конструктор класса:_
|
|
28
158
|
|
|
29
|
-
|
|
159
|
+
```ts
|
|
160
|
+
import { AbilityRule, AbilityCondition } from '@via-profit/ability';
|
|
30
161
|
|
|
31
|
-
|
|
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
|
-
|
|
170
|
+
// сокращённая запись
|
|
171
|
+
const rule2 = AbilityRule.equal(
|
|
172
|
+
'user.department', // subject
|
|
173
|
+
'managers' // resource
|
|
174
|
+
);
|
|
34
175
|
|
|
35
|
-
|
|
176
|
+
```
|
|
36
177
|
|
|
37
|
-
|
|
178
|
+
_Создание правила через парсинг JSON-конфигурации:_
|
|
38
179
|
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
193
|
+
### Проверка правила
|
|
55
194
|
|
|
56
|
-
|
|
195
|
+
Для проверки правила следует вызвать метод `check` класса `AbilityRule` передав объект проверяемого ресурса. Этот метод
|
|
196
|
+
вернёт экземпляр класса
|
|
197
|
+
`AbilityMatch`, при помощи методов которого можно определить имеется ли совпадение правила и переданных значений.
|
|
57
198
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
216
|
+
const is = match.isEqual(AbilityMatch.match); // true
|
|
66
217
|
|
|
67
|
-
|
|
218
|
+
```
|
|
68
219
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
235
|
+
id: '9cc009e5-0aa9-453a-a668-cb3f418ced92',
|
|
236
|
+
name: 'Не администратор',
|
|
237
|
+
compareMethod: 'and',
|
|
238
|
+
rules: [
|
|
80
239
|
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
"
|
|
104
|
-
"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
117
|
-
|
|
584
|
+
#### Приоритет и множественное совпадение
|
|
585
|
+
|
|
586
|
+
Если несколько политик подходят под проверяемое действие, будут применены **все** подходящие политики. Результат определяется последней сработавшей политикой:
|
|
118
587
|
|
|
119
588
|
```ts
|
|
120
|
-
const
|
|
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
|
-
|
|
691
|
+
// При выполнении действия указываем только нужный action
|
|
692
|
+
// AbilityResolver автоматически отфильтрует политики и проверит их
|
|
693
|
+
resolver.enforce('order.create', {
|
|
123
694
|
user: {
|
|
124
695
|
department: 'managers',
|
|
125
|
-
roles: ['manager'
|
|
696
|
+
roles: ['manager']
|
|
697
|
+
},
|
|
698
|
+
order: {
|
|
699
|
+
amount: 5000
|
|
126
700
|
}
|
|
127
701
|
});
|
|
128
|
-
|
|
702
|
+
```
|
|
129
703
|
|
|
130
|
-
|
|
704
|
+
### Проверка соответствия действия
|
|
131
705
|
|
|
132
|
-
|
|
133
|
-
политику.
|
|
706
|
+
Метод `isInActionContain` позволяет проверить, соответствует ли действие шаблону с wildcard:
|
|
134
707
|
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
данном модуле.
|
|
716
|
+
Этот метод используется внутри `AbilityResolver` для фильтрации политик, но может быть полезен и в пользовательском коде.
|
|
139
717
|
|
|
140
|
-
|
|
718
|
+
### Методы AbilityResolver
|
|
141
719
|
|
|
142
|
-
|
|
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
|
-
|
|
157
|
-
значение которого будет равно (`=`) `managers`
|
|
737
|
+
**Важно**: `enforce()` выбрасывает исключение, если хотя бы одна подходящая политика вернула `deny`. Если ни одна политика не сработала или все вернули `permit` — исключения не будет.
|
|
158
738
|
|
|
159
|
-
|
|
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
|
-
|
|
741
|
+
```typescript
|
|
742
|
+
const result = resolver
|
|
743
|
+
.resolve('order.update', data)
|
|
744
|
+
.isDeny(); // true если доступ запрещен
|
|
165
745
|
|
|
166
|
-
|
|
167
|
-
|
|
746
|
+
if (result) {
|
|
747
|
+
// Самостоятельно обрабатываем запрет
|
|
748
|
+
return { error: 'Access denied' };
|
|
749
|
+
}
|
|
168
750
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
831
|
+
### Как формируется итоговое решение?
|
|
187
832
|
|
|
188
|
-
|
|
833
|
+
1. Resolver собирает все политики, у которых `action` соответствует запрошенному (поддерживается wildcard `*`)
|
|
834
|
+
2. Последовательно проверяет каждую политику методом `check()`
|
|
835
|
+
3. Если политика вернула `match`, запоминает её `effect` (permit/deny)
|
|
836
|
+
4. Возвращается `effect` **последней сработавшей политики**
|
|
189
837
|
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
###
|
|
850
|
+
### Когда использовать Resolver?
|
|
199
851
|
|
|
200
|
-
-
|
|
201
|
-
-
|
|
202
|
-
-
|
|
203
|
-
-
|
|
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
|
-
|
|
857
|
+
Resolver делает систему прав предсказуемой, типобезопасной и легко расширяемой.
|
|
210
858
|
|
|
211
|
-
|
|
212
|
-
<summary>Пользователь старше 21 года</summary>
|
|
859
|
+
## API Reference
|
|
213
860
|
|
|
214
|
-
|
|
215
|
-
const user = {
|
|
216
|
-
age: 18,
|
|
217
|
-
};
|
|
861
|
+
### Класс AbilityCode
|
|
218
862
|
|
|
219
|
-
|
|
220
|
-
```
|
|
863
|
+
Базовый абстрактный класс для всех кодовых значений в системе.
|
|
221
864
|
|
|
222
|
-
|
|
865
|
+
| Метод | Аргументы | Возвращаемое значение | Описание |
|
|
866
|
+
|-------|-----------|----------------------|----------|
|
|
867
|
+
| `isEqual()` | `compareWith: AbilityCode<T> \| null` | `boolean` | Сравнивает текущий код с другим экземпляром |
|
|
868
|
+
| `isNotEqual()` | `compareWith: AbilityCode<T> \| null` | `boolean` | Проверяет неравенство с другим экземпляром |
|
|
223
869
|
|
|
224
|
-
|
|
225
|
-
|
|
870
|
+
**Геттеры:**
|
|
871
|
+
- `code: T` - возвращает сырое значение кода (строку или число)
|
|
226
872
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
1118
|
+
---
|
|
238
1119
|
|
|
239
|
-
|
|
240
|
-
<summary>Пользователь является владельцем заказа</summary>
|
|
1120
|
+
### Классы ошибок
|
|
241
1121
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
1122
|
+
| Класс | Назначение |
|
|
1123
|
+
|-------|------------|
|
|
1124
|
+
| `AbilityError` | Общая ошибка доступа, выбрасывается при запрете в `enforce()` |
|
|
1125
|
+
| `AbilityParserError` | Ошибка парсинга конфигурации или генерации типов |
|
|
246
1126
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
1127
|
+
## Рекомендации по использованию
|
|
1128
|
+
|
|
1129
|
+
### Именование экшенов
|
|
1130
|
+
|
|
1131
|
+
Используйте точечную нотацию для иерархии действий:
|
|
1132
|
+
|
|
1133
|
+
- `order.create` - создание заказа
|
|
1134
|
+
- `order.update` - обновление заказа
|
|
1135
|
+
- `order.status.update` - обновление статуса заказа
|
|
250
1136
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
1157
|
+
### Проектирование политик
|
|
258
1158
|
|
|
259
|
-
|
|
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
|
-
##
|
|
1179
|
+
## Лицензия
|
|
265
1180
|
|
|
266
|
-
|
|
1181
|
+
Этот проект лицензирован под лицензией MIT. Подробности в файле [LICENSE](LICENSE).
|