@via-profit/ability 3.0.1 → 3.1.0
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 +65 -0
- package/README.md +920 -924
- package/dist/cache/AbilityCacheAdapter.d.ts +8 -0
- package/dist/cache/AbilityInMemoryCache.d.ts +12 -0
- package/dist/core/AbilityCondition.d.ts +21 -0
- package/dist/{AbilityExplain.d.ts → core/AbilityExplain.d.ts} +3 -3
- package/dist/{AbilityParser.d.ts → core/AbilityParser.d.ts} +2 -0
- package/dist/{AbilityPolicy.d.ts → core/AbilityPolicy.d.ts} +22 -13
- package/dist/core/AbilityResolver.d.ts +35 -0
- package/dist/core/AbilityResult.d.ts +27 -0
- package/dist/core/AbilityRule.d.ts +77 -0
- package/dist/{AbilityRuleSet.d.ts → core/AbilityRuleSet.d.ts} +10 -9
- package/dist/index.d.ts +19 -12
- package/dist/index.js +1718 -254
- package/dist/parsers/dsl/AbilityDSLLexer.d.ts +24 -0
- package/dist/parsers/dsl/AbilityDSLParser.d.ts +86 -0
- package/dist/parsers/dsl/AbilityDSLSyntaxError.d.ts +13 -0
- package/dist/parsers/dsl/AbilityDSLToken.d.ts +55 -0
- package/dist/parsers/json/AbilityJSONParser.d.ts +22 -0
- package/package.json +5 -3
- package/dist/AbilityCondition.d.ts +0 -16
- package/dist/AbilityResolver.d.ts +0 -32
- package/dist/AbilityRule.d.ts +0 -78
- /package/dist/{AbilityCode.d.ts → core/AbilityCode.d.ts} +0 -0
- /package/dist/{AbilityCompare.d.ts → core/AbilityCompare.d.ts} +0 -0
- /package/dist/{AbilityError.d.ts → core/AbilityError.d.ts} +0 -0
- /package/dist/{AbilityMatch.d.ts → core/AbilityMatch.d.ts} +0 -0
- /package/dist/{AbilityPolicyEffect.d.ts → core/AbilityPolicyEffect.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -2,1177 +2,1173 @@
|
|
|
2
2
|
|
|
3
3
|
> Набор сервисов, частично реализующих
|
|
4
4
|
> принцип [Attribute Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control)
|
|
5
|
+
> Пакет позволяет описывать правила, объединять их в группы, формировать политики и применять их к данным для определения разрешений.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
## Для чего
|
|
8
|
+
|
|
9
|
+
Пакет задуман как **лёгкая и предельно простая альтернатива** тяжёлым системам управления доступом.
|
|
10
|
+
Без сложных конфигураций, без зависимостей — только минимальный набор инструментов, который позволяет описывать правила и политики в максимально простом DSL.
|
|
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
|
-
- [Проектирование политик](#проектирование-политик)
|
|
14
|
+
- [Быстрый старт](#быстрый-старт)
|
|
15
|
+
- [Основные положения](#основные-положения)
|
|
16
|
+
- [DSL](#dsl)
|
|
17
|
+
- [Объединение политик](#объединение-политик)
|
|
18
|
+
- [Environment политик](#environment-политик)
|
|
19
|
+
- [Генератор типов для TypeScript](#генератор-типов-для-typescript)
|
|
48
20
|
- [Отладка политик](#отладка-политик)
|
|
49
|
-
- [
|
|
21
|
+
- [Решение проблем](#решение-проблем)
|
|
22
|
+
- [Рекомендации по проектированию](#рекомендации-по-проектированию)
|
|
23
|
+
- [Примеры](#примеры)
|
|
24
|
+
- [Производительность](#производительность)
|
|
25
|
+
- [Api-Reference](./docs/ru/api.md)
|
|
50
26
|
|
|
51
|
-
---
|
|
52
27
|
|
|
53
|
-
##
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
}
|
|
28
|
+
## Быстрый старт
|
|
29
|
+
|
|
30
|
+
Установить пакет, написать DSL, вызвать парсер, запустить резолвер.
|
|
31
|
+
|
|
32
|
+
### Установка
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @via-profit/ability
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
yarn add @via-profit/ability
|
|
123
40
|
```
|
|
124
41
|
|
|
125
|
-
|
|
42
|
+
```bash
|
|
43
|
+
pnpm add @via-profit/ability
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
### Пример: запретить доступ к `passwordHash` всем, кроме владельца
|
|
48
|
+
|
|
49
|
+
Допустим, у нас есть пользовательские данные:
|
|
126
50
|
|
|
127
51
|
```ts
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
});
|
|
52
|
+
const user = {
|
|
53
|
+
id: '1',
|
|
54
|
+
login: 'user-001',
|
|
55
|
+
passwordHash: '...',
|
|
56
|
+
};
|
|
135
57
|
```
|
|
136
58
|
|
|
137
|
-
|
|
59
|
+
Нужно запретить чтение `passwordHash` всем, кроме самого пользователя.
|
|
60
|
+
|
|
61
|
+
#### DSL‑политика
|
|
138
62
|
|
|
139
|
-
|
|
63
|
+
На языке политик это выглядит так:
|
|
140
64
|
|
|
141
|
-
|
|
142
|
-
|
|
65
|
+
```
|
|
66
|
+
deny permission.user.passwordHash if any:
|
|
67
|
+
viewer.id is not equals owner.id
|
|
68
|
+
```
|
|
143
69
|
|
|
144
|
-
|
|
70
|
+
**Пояснение:**
|
|
145
71
|
|
|
146
|
-
|
|
72
|
+
- `deny` — эффект политики (запретить доступ)
|
|
73
|
+
- `permission.user.passwordHash` — ключ разрешения.
|
|
74
|
+
- `if any:` — начало блока условий
|
|
75
|
+
- `viewer.id is not equals owner.id` — правило: если идентификатор запрашивающего не равен идентификатору владельца
|
|
147
76
|
|
|
148
|
-
При создании необходимо указать следующие параметры:
|
|
149
77
|
|
|
150
|
-
|
|
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` или значение, которое может быть строкой, числом, булеан значением или массивом строк, или чисел.
|
|
78
|
+
Если `viewer.id` не равен `owner.id`, правило считается выполненным, и политика возвращает `deny` — доступ запрещён. Если же идентификаторы совпадают (т.е. пользователь запрашивает свои собственные данные), правило не срабатывает, и доступ разрешается.
|
|
156
79
|
|
|
157
|
-
_
|
|
80
|
+
_Замечание: Ключ разрешения формируется по принципу: `permission.` + ваш кастомный ключ в формате **dot notation**, например, ключ `foo.bar.baz` в DSL будет иметь вид `permission.foo.bar.baz`_
|
|
81
|
+
|
|
82
|
+
#### Проверка в коде
|
|
158
83
|
|
|
159
84
|
```ts
|
|
160
|
-
import {
|
|
161
|
-
|
|
162
|
-
const rule = new AbilityRule({
|
|
163
|
-
id: '<rule-id>',
|
|
164
|
-
name: 'Пользователь из отдела managers',
|
|
165
|
-
subject: 'user.department',
|
|
166
|
-
resource: 'managers',
|
|
167
|
-
condition: AbilityCondition.equal
|
|
168
|
-
});
|
|
85
|
+
import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
|
|
169
86
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
);
|
|
87
|
+
const dsl = `
|
|
88
|
+
deny permission.user.passwordHash if any:
|
|
89
|
+
viewer.id is not equals owner.id
|
|
90
|
+
`;
|
|
175
91
|
|
|
92
|
+
const policies = new AbilityDSLParser(dsl).parse(); // получение политик
|
|
93
|
+
const resolver = new AbilityResolver(policies); // создание резолвера
|
|
94
|
+
|
|
95
|
+
resolver.enforce('user.passwordHash', {
|
|
96
|
+
viewer: { id: '1' },
|
|
97
|
+
owner: { id: '2' },
|
|
98
|
+
}); // выбросит ошибку — доступ запрещён
|
|
176
99
|
```
|
|
100
|
+
В `enforce` передаётся ключ без префикса `permission.` — он автоматически удаляется парсером.
|
|
177
101
|
|
|
178
|
-
|
|
102
|
+
## Основные положения
|
|
179
103
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
104
|
+
Тезисно перечислим основные положения, которые необходимо знать перед тем как начать пользоваться пакетом:
|
|
105
|
+
|
|
106
|
+
1. Резолвер (`AbilityResolver`) настроен по принципу `Default Deny`. Это значит, что если ни одна политика не сработала, то результат будет `deny` ([подробнее здесь](#решение-проблем)). Чтобы избежать неожиданного `deny`, убедитесь, что существует хотя бы одна `permit`‑политика, которая может совпасть. Только после этого добавляйте `deny`‑политики.
|
|
107
|
+
2. Политики применяются последовательно. Если несколько политик совпали, результат определяется последней совпавшей политикой.
|
|
108
|
+
3. Правила выполняются последовательно.
|
|
109
|
+
4. В группе правил (`RuleSet`) с оператором сравнения `all` дальнейшее выполнение правил прекращается как только первое же правило вернёт `mismatch`.
|
|
110
|
+
5. Для составления политик используйте [DSL](#dsl) — это проще и удобнее
|
|
111
|
+
6. Для хранения политик на сервере используйте JSON. Политики возможно экспортировать в JSON и импортировать из JSON
|
|
112
|
+
7. Чаще всего следует опираться на утверждение если разрешение не выдано явно → доступ запрещён.
|
|
113
|
+
8. Используйте встроенный кэш только в случаях, если ваши политики неимоверно сложны и содержат большое количество правил
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## DSL
|
|
118
|
+
|
|
119
|
+
> DSL - Domain-Specific Language
|
|
120
|
+
|
|
121
|
+
Ability DSL — это декларативный язык для описания политик доступа.
|
|
122
|
+
Он позволяет определять правила в человекочитаемой форме, используя простые конструкции: *политики*, *группы*, *правила* и *аннотации*.
|
|
123
|
+
|
|
124
|
+
### Структура политики
|
|
125
|
+
|
|
126
|
+
Политика состоит из:
|
|
190
127
|
|
|
191
128
|
```
|
|
129
|
+
<effect> <permission> if <all|any>:
|
|
130
|
+
<group>...
|
|
131
|
+
```
|
|
192
132
|
|
|
193
|
-
|
|
133
|
+
Где:
|
|
194
134
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
135
|
+
- **effect** — `permit` или `deny`
|
|
136
|
+
- **permission** — строка вида `permission.foo.bar`, где суффикс `permission.` обязателен.
|
|
137
|
+
- **if all:** — все группы должны быть истинны
|
|
138
|
+
- **if any:** — хотя бы одна группа должна быть истинна
|
|
198
139
|
|
|
199
|
-
|
|
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
|
-
});
|
|
140
|
+
Политика может содержать одну или несколько групп правил.
|
|
209
141
|
|
|
210
|
-
|
|
211
|
-
user: {
|
|
212
|
-
department: 'managers',
|
|
213
|
-
},
|
|
214
|
-
});
|
|
142
|
+
Пример:
|
|
215
143
|
|
|
216
|
-
|
|
144
|
+
```dsl
|
|
145
|
+
permit permission.order.update if any:
|
|
146
|
+
all of:
|
|
147
|
+
user.roles contains 'admin'
|
|
148
|
+
user.token is not null
|
|
217
149
|
|
|
150
|
+
any of:
|
|
151
|
+
user.roles contains 'developer'
|
|
152
|
+
user.login is equals 'dev'
|
|
218
153
|
```
|
|
219
154
|
|
|
220
|
-
|
|
155
|
+
> Префикс `permission.` обязателен в DSL, но автоматически удаляется парсером. Внутри системы разрешение хранится как `order.update`.
|
|
156
|
+
|
|
157
|
+
Пример политики выше гласит - разрешение `permission.order.update` будет разрешено при выполнении одного из двух условий:
|
|
158
|
+
1. user.roles содержит 'admin' **и** user.token не null
|
|
159
|
+
2. user.roles содержит 'developer' **или** user.login равен 'dev'
|
|
160
|
+
|
|
161
|
+
### Ключ разрешения (permission key)
|
|
221
162
|
|
|
222
|
-
|
|
163
|
+
Ключ разрешения записываются в `dot notation` виде, но поддерживают возможность использования wildcard шаблонов при
|
|
164
|
+
помощи символа `*`. Это позволяет группировать ключи, а так же переопределять политики с похожими ключами.
|
|
223
165
|
|
|
224
|
-
|
|
166
|
+
Если под ключ подходит несколько политик, **выполняются все**. Итог определяется **последней совпавшей политикой**:
|
|
225
167
|
|
|
168
|
+
|
|
169
|
+
**Пример использования шаблонов**
|
|
170
|
+
|
|
171
|
+
| Политика (permission) | ключ | Совпадает |
|
|
172
|
+
|-------------------|------------------------|-----------|
|
|
173
|
+
| `order.*` | `order.create` | да |
|
|
174
|
+
| `order.*` | `order.update` | да |
|
|
175
|
+
| `order.*` | `user.create` | нет |
|
|
176
|
+
| `*.create` | `order.create` | да |
|
|
177
|
+
| `*.create` | `user.create` | да |
|
|
178
|
+
| `*.create` | `order.update` | нет |
|
|
179
|
+
| `user.profile.*` | `user.profile.update` | да |
|
|
180
|
+
| `user.profile.*` | `user.settings.update` | нет |
|
|
181
|
+
|
|
182
|
+
**Пример политики с wildcard**
|
|
226
183
|
```ts
|
|
227
|
-
|
|
228
|
-
id: 'bb758c1b-1015-4894-ba25-d23156e063cf',
|
|
229
|
-
name: 'Запрещает менять статус заявки с `не обработан` на `завершен` всем, кроме администраторам',
|
|
230
|
-
action: 'order.status',
|
|
231
|
-
effect: 'deny',
|
|
232
|
-
compareMethod: 'and',
|
|
233
|
-
ruleSet: [
|
|
234
|
-
{
|
|
235
|
-
id: '9cc009e5-0aa9-453a-a668-cb3f418ced92',
|
|
236
|
-
name: 'Не администратор',
|
|
237
|
-
compareMethod: 'and',
|
|
238
|
-
rules: [
|
|
239
|
-
{
|
|
240
|
-
id: '4093cd50-e54f-4062-8053-2d3b5966fad3',
|
|
241
|
-
name: 'Нет роли администраторов',
|
|
242
|
-
subject: 'user.roles',
|
|
243
|
-
resource: 'administrator',
|
|
244
|
-
condition: 'not in',
|
|
245
|
-
},
|
|
246
|
-
],
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
id: '2f8f9d71-860b-4fa6-b395-9331f1f0848e',
|
|
250
|
-
name: 'Проверка статуса `не обработан` -> `завершен`',
|
|
251
|
-
compareMethod: 'and',
|
|
252
|
-
rules: [
|
|
253
|
-
{
|
|
254
|
-
id: 'a3c7d66f-5c2d-4a24-83bc-03b0a2d9c32b',
|
|
255
|
-
name: 'Текущий статус `не обработан`',
|
|
256
|
-
subject: 'order.status',
|
|
257
|
-
resource: 'не обработан',
|
|
258
|
-
condition: '=',
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
id: 'a3c7d66f-5c2d-4a24-83bc-03b0a2d9c32b',
|
|
262
|
-
name: 'Будущий статус `завершен`',
|
|
263
|
-
subject: 'feature.status',
|
|
264
|
-
resource: 'завершен',
|
|
265
|
-
condition: '=',
|
|
266
|
-
},
|
|
267
|
-
],
|
|
268
|
-
},
|
|
269
|
-
],
|
|
270
|
-
};
|
|
184
|
+
import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
|
|
271
185
|
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
},
|
|
278
|
-
order: {
|
|
279
|
-
status: 'не обработан',
|
|
280
|
-
},
|
|
281
|
-
feature: {
|
|
282
|
-
status: 'завершен',
|
|
283
|
-
},
|
|
284
|
-
});
|
|
186
|
+
// DSL не полный и показан только ради примера
|
|
187
|
+
const dsl = `
|
|
188
|
+
permit permission.order.*
|
|
189
|
+
deny permission.order.update
|
|
190
|
+
`;
|
|
285
191
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
});
|
|
192
|
+
const policies = new AbilityDSLParser(dsl).parse();
|
|
193
|
+
const resolver = new AbilityResolver(policies);
|
|
289
194
|
|
|
290
|
-
|
|
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
|
-
};
|
|
195
|
+
await resolver.enforce('order.update', resource); // выбросит AbilityError
|
|
303
196
|
|
|
304
197
|
```
|
|
305
198
|
|
|
306
|
-
|
|
199
|
+
**Пояснение**
|
|
200
|
+
|
|
201
|
+
В DSL порядок политик имеет значение:
|
|
202
|
+
последняя совпавшая политика выигрывает.
|
|
203
|
+
|
|
204
|
+
Поэтому:
|
|
205
|
+
|
|
206
|
+
1. `permit` `permission.order.*` разрешает всё, что начинается с `order.`
|
|
207
|
+
2. `deny` `permission.order.update` перекрывает это разрешение.
|
|
208
|
+
|
|
209
|
+
Итог выполнения:
|
|
307
210
|
|
|
308
211
|
```
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
✓ rule «Текущий статус "не обработан"» is match
|
|
314
|
-
✓ rule «Будущий статус "завершен"» is match
|
|
212
|
+
order.update → deny
|
|
213
|
+
order.create → permit
|
|
214
|
+
order.delete → permit
|
|
215
|
+
order.view → permit
|
|
315
216
|
```
|
|
316
217
|
|
|
317
|
-
---
|
|
318
218
|
|
|
319
|
-
|
|
219
|
+
### Комментарии
|
|
320
220
|
|
|
321
|
-
|
|
322
|
-
правила в группе и вернуть лишь один результат.
|
|
221
|
+
Строки, начинающиеся с символа `#` считаются комментариями и не влияют на результат работы правил и политик.
|
|
323
222
|
|
|
324
|
-
|
|
325
|
-
группы при проверке правил.
|
|
223
|
+
---
|
|
326
224
|
|
|
327
|
-
|
|
225
|
+
### Аннотации
|
|
328
226
|
|
|
329
|
-
|
|
330
|
-
- **name** - `string` Название группы.
|
|
331
|
-
- **compareMethod** - `AbilityCompare` Способ сравнения правил в группе (`or` или `and`).
|
|
227
|
+
В настоящий момент поддерживается только одна аннотация ’name’, которая будет использована в качестве имени для политики, либо группы правил, либо правила.
|
|
332
228
|
|
|
333
|
-
|
|
229
|
+
Аннотации задаются через комментарии:
|
|
334
230
|
|
|
335
|
-
|
|
336
|
-
|
|
231
|
+
```
|
|
232
|
+
# @name <имя>
|
|
233
|
+
```
|
|
337
234
|
|
|
338
|
-
|
|
235
|
+
Аннотации применяются к **следующей сущности**:
|
|
339
236
|
|
|
340
|
-
|
|
237
|
+
- политике
|
|
238
|
+
- группе
|
|
239
|
+
- правилу
|
|
341
240
|
|
|
342
|
-
|
|
241
|
+
Пример:
|
|
343
242
|
|
|
344
|
-
```
|
|
345
|
-
|
|
243
|
+
```dsl
|
|
244
|
+
# @name can order update
|
|
245
|
+
permit permission.order.update if any:
|
|
246
|
+
# @name authorized admin
|
|
247
|
+
all of:
|
|
248
|
+
# @name contains role admin
|
|
249
|
+
user.roles contains 'admin'
|
|
250
|
+
```
|
|
346
251
|
|
|
347
|
-
|
|
348
|
-
id: '<set-id>',
|
|
349
|
-
name: 'Название группы',
|
|
350
|
-
compareMethod: AbilityCompare.and,
|
|
351
|
-
});
|
|
252
|
+
---
|
|
352
253
|
|
|
353
|
-
|
|
354
|
-
ruleSet.addRules([
|
|
355
|
-
new AbilityRule(...),
|
|
356
|
-
new AbilityRule(...),
|
|
357
|
-
]);
|
|
254
|
+
### Группы правил
|
|
358
255
|
|
|
256
|
+
Группа определяет, как объединяются правила внутри неё:
|
|
359
257
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
]);
|
|
258
|
+
```
|
|
259
|
+
all of:
|
|
260
|
+
<rule>
|
|
261
|
+
<rule>
|
|
365
262
|
|
|
263
|
+
any of:
|
|
264
|
+
<rule>
|
|
265
|
+
<rule>
|
|
366
266
|
```
|
|
367
267
|
|
|
368
|
-
|
|
268
|
+
- `all of:` — логическое AND
|
|
269
|
+
- `any of:` — логическое OR
|
|
369
270
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const ruleSet = AbilityRuleSet.parse({
|
|
374
|
-
'id': '<set-id>',
|
|
375
|
-
'name': 'Название группы',
|
|
376
|
-
'compareMethod': 'and',
|
|
377
|
-
'rules': [
|
|
378
|
-
{
|
|
379
|
-
'id': '<rule-id>',
|
|
380
|
-
'name': 'Пользователь из отдела managers',
|
|
381
|
-
'subject': 'user.department',
|
|
382
|
-
'resource': 'managers',
|
|
383
|
-
'condition': '=',
|
|
384
|
-
},
|
|
385
|
-
],
|
|
386
|
-
});
|
|
271
|
+
`all of` - значит, что группа считается выполненной, если все правила внутри группы сработали.
|
|
272
|
+
|
|
273
|
+
`any of` - значит, что группа считается выполненной, если хотя бы одно правило внутри группы сработало.
|
|
387
274
|
|
|
275
|
+
Каждая группа внутри политики будет вычисляться независимо от других групп. Итоговая оценка результата будет определена путем сравнения результата вычисления всех групп в политике.
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
Группы могут иметь аннотации:
|
|
279
|
+
|
|
280
|
+
```dsl
|
|
281
|
+
# @name developer group
|
|
282
|
+
any of:
|
|
283
|
+
user.roles contains 'developer'
|
|
388
284
|
```
|
|
389
285
|
|
|
390
|
-
|
|
286
|
+
---
|
|
391
287
|
|
|
392
|
-
|
|
393
|
-
Этот метод вернёт экземпляр класса `AbilityMatch`, при помощи методов которого можно определить имеется ли совпадение
|
|
394
|
-
для группы и переданных значений.
|
|
288
|
+
### Правила
|
|
395
289
|
|
|
396
|
-
|
|
397
|
-
import { AbilityRuleSet, AbilityCompare } from '@via-profit/ability';
|
|
290
|
+
Правило — это атомарное условие внутри политики. Оно определяет, при каких данных политика будет считаться совпавшей. С помощью правил задаются условия по которым определяется эффективность политики (`permit` или `deny`)
|
|
398
291
|
|
|
399
|
-
|
|
400
|
-
id: '<set-id>',
|
|
401
|
-
name: 'Название группы',
|
|
402
|
-
compareMethod: AbilityCompare.and,
|
|
403
|
-
}).addRules([
|
|
404
|
-
new AbilityRule(...),
|
|
405
|
-
new AbilityRule(...),
|
|
406
|
-
]);
|
|
292
|
+
Правило имеет форму:
|
|
407
293
|
|
|
408
|
-
|
|
294
|
+
```
|
|
295
|
+
<subject> <operator> <value?> — значение указывается не для всех операторов (например, is null не требует значения).
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
#### Subject (субъект)
|
|
409
299
|
|
|
410
|
-
|
|
300
|
+
Идентификатор в dot‑нотации:
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
user.roles
|
|
304
|
+
env.time.hour
|
|
305
|
+
order.total
|
|
411
306
|
```
|
|
412
307
|
|
|
413
|
-
|
|
308
|
+
#### Operators (операторы)
|
|
414
309
|
|
|
415
|
-
|
|
310
|
+
_Синонимы — это альтернативные формы записи, которые также поддерживаются парсером._
|
|
416
311
|
|
|
417
|
-
|
|
418
|
-
выполнения групп и вернуть один единственный результат.
|
|
312
|
+
**Базовые операторы сравнения**
|
|
419
313
|
|
|
420
|
-
|
|
314
|
+
| Оператор DSL | Синонимы | Пример | Описание | Типы |
|
|
315
|
+
|--------------|----------|--------|----------|------|
|
|
316
|
+
| **is equals** | `=`, `==`, `equals` | `age is equals 18` | Строгое равенство | number, string, boolean |
|
|
317
|
+
| **is not equals** | `!=`, `<>`, `not equals` | `role is not equals 'admin'` | Строгое неравенство | number, string, boolean |
|
|
318
|
+
| **greater than** | `>`, `gt` | `age greater than 18` | Больше | number, date |
|
|
319
|
+
| **greater than or equal** | `>=`, `gte` | `age greater than or equal 18` | Больше или равно | number, date |
|
|
320
|
+
| **less than** | `<`, `lt` | `age less than 18` | Меньше | number, date |
|
|
321
|
+
| **less than or equal** | `<=`, `lte` | `age less than or equal 18` | Меньше или равно | number, date |
|
|
421
322
|
|
|
422
|
-
Создать политику можно двумя способами: создание через конструктор класса и парсинг JSON-конфига политики.
|
|
423
323
|
|
|
424
|
-
|
|
324
|
+
**Null‑операторы**
|
|
425
325
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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[]` Массив групп (см. [Группы правил](#группы-правил))
|
|
326
|
+
| Оператор DSL | Синонимы | Пример | Описание | Типы |
|
|
327
|
+
|--------------|----------|--------|----------|------|
|
|
328
|
+
| **is null** | `== null`, `= null` | `middleName is null` | Значение отсутствует | any |
|
|
329
|
+
| **is not null** | `!= null` | `middleName is not null` | Значение присутствует | any |
|
|
438
330
|
|
|
439
|
-
|
|
440
|
-
необходимо ограничить какой-либо доступ, например, пользователю с недостаточными правами, то следует создавать политику
|
|
441
|
-
с эффектом `deny`.
|
|
331
|
+
**Операторы для списков (массивов)**
|
|
442
332
|
|
|
443
|
-
|
|
333
|
+
| Оператор DSL | Синонимы | Пример | Описание | Типы |
|
|
334
|
+
|--------------|---------------------------|--------|----------|------|
|
|
335
|
+
| **in [...]** | - | `role in ['admin', 'manager']` | Значение входит в список | number, string |
|
|
336
|
+
| **not in [...]** | - | `role not in ['banned']` | Значение не входит | number, string |
|
|
337
|
+
| **contains** | `includes`, `has` | `tags contains 'vip'` | Массив содержит элемент | array |
|
|
338
|
+
| **not contains** | `not includes`, `not has` | `tags not contains 'vip'` | Массив не содержит элемент | array |
|
|
444
339
|
|
|
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
340
|
|
|
465
|
-
|
|
341
|
+
**Строковые операторы**
|
|
466
342
|
|
|
467
|
-
|
|
343
|
+
| Оператор DSL | Синонимы | Пример | Описание | Типы |
|
|
344
|
+
|--------------|----------|--------|----------|------|
|
|
345
|
+
| **starts with** | `begins with` | `email starts with 'admin@'` | Строка начинается с | string |
|
|
346
|
+
| **not starts with** | — | `email not starts with 'test'` | Строка не начинается с | string |
|
|
347
|
+
| **ends with** | — | `email ends with '.ru'` | Строка заканчивается на | string |
|
|
348
|
+
| **not ends with** | — | `email not ends with '.com'` | Строка не заканчивается на | string |
|
|
349
|
+
| **includes** | `contains substring` | `name includes 'lex'` | Строка содержит подстроку | string |
|
|
350
|
+
| **not includes** | — | `name not includes 'test'` | Строка не содержит подстроку | string |
|
|
468
351
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
"id": "4093cd50-e54f-4062-8053-2d3b5966fad3",
|
|
486
|
-
"name": "Нет роли администраторв",
|
|
487
|
-
"subject": "account.roles",
|
|
488
|
-
"resource": "administrator",
|
|
489
|
-
"condition": "<>"
|
|
490
|
-
}
|
|
491
|
-
]
|
|
492
|
-
}
|
|
493
|
-
]
|
|
494
|
-
});
|
|
352
|
+
**Булевые операторы**
|
|
353
|
+
|
|
354
|
+
| Оператор DSL | Синонимы | Пример | Описание | Типы |
|
|
355
|
+
|--------------|----------|--------|----------|------|
|
|
356
|
+
| **is true** | `= true` | `isActive is true` | Значение истинно | boolean |
|
|
357
|
+
| **is false** | `= false` | `isActive is false` | Значение ложно | boolean |
|
|
358
|
+
|
|
359
|
+
**Операторы длины**
|
|
360
|
+
|
|
361
|
+
| Оператор DSL | Синонимы | Пример | Описание | Типы |
|
|
362
|
+
|--------------|----------|--------|----------|------|
|
|
363
|
+
| **length equals** | `len =` | `tags length equals 3` | Длина равна | array, string |
|
|
364
|
+
| **length greater than** | `len >` | `tags length greater than 2` | Длина больше | array, string |
|
|
365
|
+
| **length less than** | `len <` | `tags length less than 5` | Длина меньше | array, string |
|
|
366
|
+
|
|
367
|
+
#### Value (значение)
|
|
495
368
|
|
|
369
|
+
Поддерживаются:
|
|
370
|
+
|
|
371
|
+
- строки `'text'`
|
|
372
|
+
- числа `42`
|
|
373
|
+
- булевы `true` / `false`
|
|
374
|
+
- `null`
|
|
375
|
+
- массивы `[1, 2, 3]` / `['foo', false, null, 1, 2, '999']`
|
|
376
|
+
|
|
377
|
+
Примеры:
|
|
378
|
+
|
|
379
|
+
```dsl
|
|
380
|
+
# возраст пользователя больше 18
|
|
381
|
+
user.age greater than 18
|
|
382
|
+
|
|
383
|
+
# массив ролей содержит роль 'admin'
|
|
384
|
+
user.roles contains 'admin'
|
|
385
|
+
|
|
386
|
+
# тэг заказа либо 'vip', либо 'priority'
|
|
387
|
+
order.tag in ['vip', 'priority']
|
|
388
|
+
|
|
389
|
+
# токен пользователя не null
|
|
390
|
+
user.token is not null
|
|
391
|
+
|
|
392
|
+
# логин пользователя длиннее 12 символов
|
|
393
|
+
user.login length greater than 12
|
|
496
394
|
```
|
|
497
395
|
|
|
498
|
-
### Проверка политики
|
|
499
396
|
|
|
500
|
-
Для проверки политики правил следует вызвать метод `check` класса `AbilityPolicy` передав объект проверяемого ресурса.
|
|
501
|
-
Этот метод вернёт экземпляр класса `AbilityMatch`, при помощи методов которого можно определить имеется ли совпадение
|
|
502
|
-
для группы и переданных значений.
|
|
503
397
|
|
|
504
|
-
|
|
505
|
-
import { AbilityPolicy } from '@via-profit/ability';
|
|
398
|
+
---
|
|
506
399
|
|
|
507
|
-
|
|
400
|
+
### Неявная группа (implicit group)
|
|
508
401
|
|
|
509
|
-
|
|
402
|
+
Если правила идут без `all of:` или `any of:`, они объединяются оператором политики:
|
|
510
403
|
|
|
511
|
-
|
|
404
|
+
```dsl
|
|
405
|
+
permit permission.order.update if all:
|
|
406
|
+
user.roles contains 'admin'
|
|
407
|
+
user.token is not null
|
|
512
408
|
```
|
|
513
409
|
|
|
514
|
-
|
|
410
|
+
Эквивалентно:
|
|
515
411
|
|
|
516
|
-
|
|
412
|
+
```dsl
|
|
413
|
+
permit permission.order.update if all:
|
|
414
|
+
all of:
|
|
415
|
+
user.roles contains 'admin'
|
|
416
|
+
user.token is not null
|
|
417
|
+
```
|
|
517
418
|
|
|
518
|
-
|
|
419
|
+
Неявная группа всегда соответствует оператору политики (`if all` или `if any`).
|
|
519
420
|
|
|
520
|
-
|
|
521
|
-
2. **Оценка разрешений** — последовательно проверяет отобранные политики и возвращает итоговый результат (разрешено/запрещено)
|
|
421
|
+
---
|
|
522
422
|
|
|
523
|
-
###
|
|
423
|
+
### Полный пример
|
|
524
424
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
- `order.delete` — правила удаления заказа
|
|
529
|
-
- `user.profile.update` — правила обновления профиля
|
|
530
|
-
- и так далее...
|
|
425
|
+
```dsl
|
|
426
|
+
# @name разрешено обновление заказа
|
|
427
|
+
permit permission.order.update if any:
|
|
531
428
|
|
|
532
|
-
|
|
429
|
+
# @name если это администратор
|
|
430
|
+
all of:
|
|
431
|
+
user.roles contains 'admin'
|
|
432
|
+
user.token is not null
|
|
533
433
|
|
|
534
|
-
|
|
434
|
+
# @name если это разработчик
|
|
435
|
+
any of:
|
|
436
|
+
user.roles contains 'developer'
|
|
437
|
+
user.login is equals 'dev'
|
|
438
|
+
```
|
|
535
439
|
|
|
536
|
-
`AbilityResolver` поддерживает использование символа звездочки (`*`) в названиях действий. Это позволяет создавать политики, которые применяются к целым группам операций.
|
|
537
440
|
|
|
538
|
-
#### Правила сопоставления с wildcard:
|
|
539
441
|
|
|
540
|
-
|
|
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` | ❌ Не совпадает |
|
|
442
|
+
## Объединение политик
|
|
552
443
|
|
|
553
|
-
|
|
444
|
+
В реальном проекте следует использовать несколько политик сразу
|
|
554
445
|
|
|
555
|
-
|
|
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
|
-
}
|
|
446
|
+
TODO: использование нескольких политик
|
|
564
447
|
|
|
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
|
-
}
|
|
448
|
+
## Environment политик
|
|
573
449
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
id: 'user-module',
|
|
577
|
-
name: 'User module base policy',
|
|
578
|
-
action: 'user.*.*', // Применится к user.profile.update, user.settings.delete и т.д.
|
|
579
|
-
effect: 'deny',
|
|
580
|
-
// ... правила
|
|
581
|
-
}
|
|
582
|
-
```
|
|
450
|
+
**Environment** — это объект, содержащий данные окружения, которые не принадлежат ни пользователю, ни ресурсу.
|
|
451
|
+
Содержимое объекта определяется разработчиком и может быть любым объектом состоящим из примитивов.
|
|
583
452
|
|
|
584
|
-
|
|
453
|
+
- время запроса,
|
|
454
|
+
- IP‑адрес,
|
|
455
|
+
- параметры устройства,
|
|
456
|
+
- заголовки запроса,
|
|
457
|
+
- контекст сессии,
|
|
458
|
+
- любые другие внешние условия.
|
|
585
459
|
|
|
586
|
-
|
|
460
|
+
**Примеры:**
|
|
587
461
|
|
|
588
462
|
```ts
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
})
|
|
600
|
-
];
|
|
463
|
+
type Environment = {
|
|
464
|
+
time: {
|
|
465
|
+
hour: number;
|
|
466
|
+
};
|
|
467
|
+
ip: string;
|
|
468
|
+
geo: {
|
|
469
|
+
country: string;
|
|
470
|
+
};
|
|
471
|
+
};
|
|
472
|
+
```
|
|
601
473
|
|
|
602
|
-
|
|
474
|
+
Environment передаётся в `resolve()` и `enforce()` как третий аргумент:
|
|
603
475
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
// Это применительно для ситуаций, когда необходимо, что называется, наложить вето
|
|
608
|
-
// на принятые ранее решения вышестоящих политик
|
|
609
|
-
resolver.enforce('order.update', data);
|
|
476
|
+
```ts
|
|
477
|
+
await resolver.resolve('order.update', resource, environment);
|
|
478
|
+
await resolver.enforce('order.update', resource, environment);
|
|
610
479
|
```
|
|
611
480
|
|
|
612
|
-
|
|
613
|
-
Это применительно для ситуаций, когда необходимо, что называется, наложить вето
|
|
614
|
-
на принятые ранее решения вышестоящих политик**
|
|
481
|
+
### Использование environment в правилах
|
|
615
482
|
|
|
616
|
-
|
|
483
|
+
В политике можно ссылаться на environment через путь `env.*`.
|
|
617
484
|
|
|
618
|
-
|
|
485
|
+
Пример политики, которая запрещает обновление заказов ночью (22:00–06:00).:
|
|
619
486
|
|
|
620
|
-
```
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
];
|
|
487
|
+
```dsl
|
|
488
|
+
# @name Deny updates at night
|
|
489
|
+
deny permission.order.update if all:
|
|
490
|
+
env.time.hour less than 6
|
|
491
|
+
env.time.hour greater or equal than 22
|
|
643
492
|
```
|
|
644
493
|
|
|
645
|
-
|
|
494
|
+
**Извлечение значений из environment**
|
|
646
495
|
|
|
647
|
-
|
|
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
|
-
];
|
|
496
|
+
Если в правиле указан путь:
|
|
684
497
|
|
|
685
|
-
|
|
686
|
-
|
|
498
|
+
- `env.*` → значение берётся из environment
|
|
499
|
+
- `user.*`, `order.*`, `profile.*` → из resource
|
|
500
|
+
- литерал (`18`, `"admin"`, `true`) → используется как есть
|
|
687
501
|
|
|
688
|
-
|
|
689
|
-
const resolver = new AbilityResolver(policies);
|
|
502
|
+
Пример:
|
|
690
503
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
department: 'managers',
|
|
696
|
-
roles: ['manager']
|
|
697
|
-
},
|
|
698
|
-
order: {
|
|
699
|
-
amount: 5000
|
|
700
|
-
}
|
|
701
|
-
});
|
|
504
|
+
```ts
|
|
505
|
+
subject: "env.geo.country"
|
|
506
|
+
resource: "user.country"
|
|
507
|
+
condition: "equal"
|
|
702
508
|
```
|
|
703
509
|
|
|
704
|
-
###
|
|
510
|
+
### Environment в TypeScript
|
|
705
511
|
|
|
706
|
-
|
|
512
|
+
Тип Environment задаётся на уровне `AbilityResolver`:
|
|
707
513
|
|
|
708
514
|
```ts
|
|
709
|
-
|
|
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
|
|
515
|
+
const resolver = new AbilityResolver<Resources, Environment>(policies);
|
|
714
516
|
```
|
|
715
517
|
|
|
716
|
-
|
|
518
|
+
Это позволяет:
|
|
717
519
|
|
|
718
|
-
|
|
520
|
+
- получать автодополнение в IDE,
|
|
521
|
+
- проверять корректность путей `env.*`,
|
|
522
|
+
- избегать ошибок при передаче environment.
|
|
719
523
|
|
|
720
|
-
|
|
524
|
+
> Если правило использует `env.*`, но environment не передан, то значение `env.*` будет `undefined`, и сравнение будет выполнено так, как если бы environment не было вовсе
|
|
721
525
|
|
|
722
|
-
```typescript
|
|
723
|
-
try {
|
|
724
|
-
resolver.enforce('order.update', data);
|
|
725
|
-
// Доступ разрешен - продолжаем выполнение
|
|
726
|
-
await updateOrder(data);
|
|
727
|
-
} catch (error) {
|
|
728
|
-
if (error instanceof AbilityError) {
|
|
729
|
-
// Доступ запрещен - error.message содержит название сработавшей политики
|
|
730
|
-
console.error(`Доступ запрещен политикой: ${error.message}`);
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
throw error;
|
|
734
|
-
}
|
|
735
|
-
```
|
|
736
526
|
|
|
737
|
-
**Важно**: `enforce()` выбрасывает исключение, если хотя бы одна подходящая политика вернула `deny`. Если ни одна политика не сработала или все вернули `permit` — исключения не будет.
|
|
738
527
|
|
|
739
|
-
|
|
528
|
+
## Генератор типов для TypeScript
|
|
740
529
|
|
|
741
|
-
|
|
742
|
-
const result = resolver
|
|
743
|
-
.resolve('order.update', data)
|
|
744
|
-
.isDeny(); // true если доступ запрещен
|
|
530
|
+
`AbilityParser.generateTypeDefs()` генерирует типы для TypeScript на основе политик, что позволяет не беспокоиться о расхождении между типами и данными в политиках.
|
|
745
531
|
|
|
746
|
-
|
|
747
|
-
// Самостоятельно обрабатываем запрет
|
|
748
|
-
return { error: 'Access denied' };
|
|
749
|
-
}
|
|
532
|
+
**Пример использования**
|
|
750
533
|
|
|
751
|
-
|
|
752
|
-
await updateOrder(data);
|
|
753
|
-
```
|
|
534
|
+
Сначала необходимо подготовить массив политик. Политики можно хранить в DSL или в JSON и парсить их в массив готовых политик. В данном примере, для наглядности, политики хранятся в DSL.
|
|
754
535
|
|
|
755
|
-
|
|
536
|
+
```ts
|
|
537
|
+
// scripts/policies.ts
|
|
756
538
|
|
|
757
|
-
|
|
758
|
-
const explanations = resolver.resolveWithExplain('order.update', data);
|
|
539
|
+
import { AbilityDSLParser } from './AbilityDSLParser';
|
|
759
540
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
// ✗ ruleSet «Проверка владельца» is mismatch
|
|
764
|
-
// ✓ ruleSet «Проверка статуса» is match
|
|
765
|
-
});
|
|
541
|
+
const dsl = `
|
|
542
|
+
# @name Update order
|
|
543
|
+
permit permission.order.update if all:
|
|
766
544
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
545
|
+
# @name Owner check
|
|
546
|
+
all of:
|
|
547
|
+
# @name User is owner
|
|
548
|
+
user.id = order.ownerId
|
|
549
|
+
`;
|
|
550
|
+
|
|
551
|
+
const policies = new AbilityDSLParser(dsl).parse();
|
|
552
|
+
|
|
553
|
+
export default policies;
|
|
771
554
|
```
|
|
772
555
|
|
|
773
|
-
|
|
556
|
+
```ts
|
|
557
|
+
// scripts/generate-types.ts
|
|
558
|
+
import { writeFileSync } from 'node:fs';
|
|
559
|
+
import { AbilityParser } from '@via-profit/ability';
|
|
560
|
+
import policies from './policies.json';
|
|
774
561
|
|
|
775
|
-
|
|
562
|
+
const typedefs = AbilityParser.generateTypeDefs(policies);
|
|
776
563
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
};
|
|
789
|
-
|
|
564
|
+
writeFileSync('./src/ability/types.generated.ts', typedefs, 'utf8');
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**Сгенерированный файл (пример)**
|
|
568
|
+
|
|
569
|
+
```ts
|
|
570
|
+
// src/ability/types.generated.ts
|
|
571
|
+
|
|
572
|
+
// Automatically generated by via-profit/ability
|
|
573
|
+
// Do not edit manually
|
|
574
|
+
export type Resources = {
|
|
790
575
|
'order.update': {
|
|
791
576
|
readonly user: {
|
|
792
577
|
readonly id: string;
|
|
793
578
|
};
|
|
794
579
|
readonly order: {
|
|
795
|
-
readonly id: string;
|
|
796
|
-
readonly status: string;
|
|
797
580
|
readonly ownerId: string;
|
|
798
581
|
};
|
|
799
582
|
};
|
|
800
|
-
|
|
801
|
-
'user.profile.update': {
|
|
802
|
-
readonly user: {
|
|
803
|
-
readonly id: string;
|
|
804
|
-
};
|
|
805
|
-
readonly profile: {
|
|
806
|
-
readonly userId: string;
|
|
807
|
-
};
|
|
808
|
-
};
|
|
809
583
|
};
|
|
584
|
+
```
|
|
810
585
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
586
|
+
**Использование в коде**
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
import { AbilityResolver, AbilityPolicy } from '@via-profit/ability';
|
|
590
|
+
import type { Resources } from './ability/types.generated';
|
|
591
|
+
|
|
592
|
+
const resolver = new AbilityResolver<Resources>(
|
|
593
|
+
AbilityPolicy.parseAll(policies),
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
await resolver.enforce('order.update', {
|
|
597
|
+
user: { id: 'u1' },
|
|
598
|
+
order: { ownerId: 'u1' },
|
|
820
599
|
});
|
|
600
|
+
```
|
|
821
601
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
602
|
+
## Отладка политик
|
|
603
|
+
|
|
604
|
+
### Объяснения
|
|
605
|
+
|
|
606
|
+
Для упрощения отладки политик применяется специальный класс `AbilityResult`, который уже включён в итоговый результат вычислений. `AbilityResult` инкапсулирует итог применения всех подходящих политик к ключу разрешений и ресурсу.
|
|
607
|
+
|
|
608
|
+
`AbilityResult` содержит:
|
|
609
|
+
|
|
610
|
+
- список проверенных политик,
|
|
611
|
+
- методы для определения итогового эффекта,
|
|
612
|
+
- методы для получения объяснений в текстовом представлении.
|
|
613
|
+
|
|
614
|
+
Пример:
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
const result = await resolver.resolve('order.update', resource);
|
|
618
|
+
|
|
619
|
+
if (result.isDenied()) {
|
|
620
|
+
console.log('Access denied');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const explanations = result.explain(); // AbilityExplain
|
|
624
|
+
|
|
625
|
+
// console.log(explanations.toString());
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### AbilityExplain
|
|
629
|
+
|
|
630
|
+
`AbilityExplain` и связанные классы (`AbilityExplainPolicy`, `AbilityExplainRuleSet`, `AbilityExplainRule`) позволяют получить человекочитаемое объяснение:
|
|
631
|
+
|
|
632
|
+
- какая политика сработала,
|
|
633
|
+
- какие группы правил совпали,
|
|
634
|
+
- какие правила не прошли,
|
|
635
|
+
- какой эффект был применён.
|
|
636
|
+
|
|
637
|
+
Пример использования:
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
const result = await resolver.resolve('order.update', resource);
|
|
641
|
+
const explanations = result.explain();
|
|
642
|
+
|
|
643
|
+
console.log(explanations.toString());
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Пример вывода:
|
|
647
|
+
|
|
648
|
+
```
|
|
649
|
+
✓ policy «Запрет обновления заказа для менеджеров» is match
|
|
650
|
+
✓ ruleSet «Менеджеры» is match
|
|
651
|
+
✓ rule «Отдел managers» is match
|
|
652
|
+
✗ rule «Роль manager» is mismatch
|
|
653
|
+
✓ ruleSet «Не администраторы» is match
|
|
654
|
+
✓ rule «Нет роли administrator» is match
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
### Формат вывода
|
|
658
|
+
|
|
659
|
+
В настоящий момент поддерживается только один формат вывода - текстовый.
|
|
660
|
+
|
|
661
|
+
Вывод строится по принципу: <policy | ruleSet | rule > <название> <is match | is mismatch>
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
## Решение проблем
|
|
665
|
+
|
|
666
|
+
### Модель принятия решений (Default Deny)
|
|
667
|
+
|
|
668
|
+
> Почему политика `deny` не превращается в `permit`, если её условия не выполнены?
|
|
669
|
+
|
|
670
|
+
Рассмотрим политику, которая **запрещает** доступ пользователю с возрастом 16 лет:
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
const dsl = `
|
|
674
|
+
deny permission.test if all:
|
|
675
|
+
user.age is equals 16
|
|
676
|
+
`;
|
|
677
|
+
|
|
678
|
+
const policies = new AbilityDSLParser(dsl).parse();
|
|
679
|
+
const resolver = new AbilityResolver(policies);
|
|
680
|
+
|
|
681
|
+
const result = await resolver.resolve('test', {
|
|
682
|
+
user: { age: 16 },
|
|
828
683
|
});
|
|
684
|
+
|
|
685
|
+
console.log(result.isDenied()); // true ✔
|
|
686
|
+
console.log(result.isAllowed()); // false ✔
|
|
829
687
|
```
|
|
830
688
|
|
|
831
|
-
|
|
689
|
+
В этом случае всё очевидно:
|
|
690
|
+
условие выполнено → политика совпала → эффект `deny` → доступ запрещён.
|
|
832
691
|
|
|
833
|
-
|
|
834
|
-
2. Последовательно проверяет каждую политику методом `check()`
|
|
835
|
-
3. Если политика вернула `match`, запоминает её `effect` (permit/deny)
|
|
836
|
-
4. Возвращается `effect` **последней сработавшей политики**
|
|
692
|
+
**Что происходит, если условия `не выполнены`?**
|
|
837
693
|
|
|
838
|
-
```
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
denyPolicy, // deny и match
|
|
843
|
-
permitPolicy2 // permit, но не match
|
|
844
|
-
];
|
|
694
|
+
```ts
|
|
695
|
+
const result = await resolver.resolve('test', {
|
|
696
|
+
user: { age: 12 },
|
|
697
|
+
});
|
|
845
698
|
|
|
846
|
-
|
|
847
|
-
|
|
699
|
+
console.log(result.isDenied()); // true ✔
|
|
700
|
+
console.log(result.isAllowed()); // false ✔
|
|
848
701
|
```
|
|
849
702
|
|
|
850
|
-
|
|
703
|
+
На первый взгляд может показаться, что если условие не выполнено, то политика должна «разрешить» доступ.
|
|
704
|
+
Но это **не так**.
|
|
851
705
|
|
|
852
|
-
|
|
853
|
-
- **В middleware** — централизованная проверка доступа
|
|
854
|
-
- **В сервисах** — защита бизнес-логики
|
|
855
|
-
- **В клиентском коде** — условный рендеринг UI на основе прав
|
|
706
|
+
**Модель принятия решений: `Default Deny`**
|
|
856
707
|
|
|
857
|
-
|
|
708
|
+
`AbilityResolver` использует классическую модель безопасности:
|
|
858
709
|
|
|
859
|
-
|
|
710
|
+
> **Если нет ни одной совпавшей permit‑политики → доступ запрещён.**
|
|
860
711
|
|
|
861
|
-
|
|
712
|
+
**Что происходит в данном примере:**
|
|
862
713
|
|
|
863
|
-
|
|
714
|
+
1. Политика `deny` существует, но её условие **не выполнено**
|
|
715
|
+
→ политика получает статус `mismatch`.
|
|
864
716
|
|
|
865
|
-
|
|
866
|
-
|-------|-----------|----------------------|----------|
|
|
867
|
-
| `isEqual()` | `compareWith: AbilityCode<T> \| null` | `boolean` | Сравнивает текущий код с другим экземпляром |
|
|
868
|
-
| `isNotEqual()` | `compareWith: AbilityCode<T> \| null` | `boolean` | Проверяет неравенство с другим экземпляром |
|
|
717
|
+
2. Политика `deny` **не применяется**, потому что условия не совпали.
|
|
869
718
|
|
|
870
|
-
|
|
871
|
-
- `code: T` - возвращает сырое значение кода (строку или число)
|
|
719
|
+
3. Политики `permit` **нет**.
|
|
872
720
|
|
|
873
|
-
|
|
721
|
+
4. Раз нет ни одной разрешающей политики → итоговое решение:
|
|
722
|
+
**deny (по умолчанию)**.
|
|
874
723
|
|
|
875
|
-
### Класс AbilityMatch
|
|
876
724
|
|
|
877
|
-
|
|
725
|
+
**Итог**
|
|
878
726
|
|
|
879
|
-
|
|
880
|
-
- `
|
|
881
|
-
- `
|
|
882
|
-
- `
|
|
727
|
+
- `deny` с совпавшими условиями → **deny**
|
|
728
|
+
- `deny` с несовпавшими условиями → **deny (default deny)**
|
|
729
|
+
- `permit` с совпавшими условиями → **allow**
|
|
730
|
+
- `permit` с несовпавшими условиями → **deny (default deny)**
|
|
883
731
|
|
|
884
|
-
|
|
732
|
+
**Заключение**
|
|
885
733
|
|
|
886
|
-
|
|
734
|
+
**Доступ разрешается только при наличии явного permit.**
|
|
887
735
|
|
|
888
|
-
|
|
736
|
+
## Рекомендации по проектированию
|
|
889
737
|
|
|
890
|
-
|
|
738
|
+
### Именование ключей доступа
|
|
891
739
|
|
|
892
|
-
|
|
893
|
-
- `
|
|
894
|
-
-
|
|
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`)
|
|
740
|
+
- Используйте иерархические ключи: `permission.order.create`, `permission.order.update.status`, `permission.user.profile.update`.
|
|
741
|
+
- Группируйте по доменам: `permission.user.*`, `permission.order.*`, `permission.product.*`.
|
|
742
|
+
- Не смешивайте разные домены в одном ключе.
|
|
901
743
|
|
|
902
|
-
|
|
903
|
-
|-------|-----------|----------------------|----------|
|
|
904
|
-
| `fromLiteral()` | `literal: AbilityConditionLiteralType` | `AbilityCondition` | Создает экземпляр условия из литерального имени (например, 'equal' → '=') |
|
|
744
|
+
### Структура данных
|
|
905
745
|
|
|
906
|
-
|
|
907
|
-
-
|
|
746
|
+
- Явно описывайте `Resources` в TypeScript.
|
|
747
|
+
- Не передавайте «лишние» поля — это усложняет понимание.
|
|
748
|
+
- Старайтесь, чтобы структура данных для одного `permission` была стабильной.
|
|
908
749
|
|
|
909
|
-
|
|
750
|
+
### Проектирование политик
|
|
910
751
|
|
|
911
|
-
|
|
752
|
+
- Общие правила — через wildcard (`permission.order.*`).
|
|
753
|
+
- Специфичные ограничения — через точные действия (`permission.order.update`).
|
|
754
|
+
- Для запретов используйте `effect: deny`.
|
|
755
|
+
- Для разрешений — `effect: permit`.
|
|
912
756
|
|
|
913
|
-
|
|
757
|
+
### Типичные ошибки
|
|
914
758
|
|
|
915
|
-
|
|
916
|
-
-
|
|
917
|
-
-
|
|
759
|
+
- Ожидание, что отсутствие совпавших политик означает deny.
|
|
760
|
+
- Смешивание бизнес-логики и политик доступа.
|
|
761
|
+
- Слишком крупные политики с десятками правил — лучше разбивать.
|
|
918
762
|
|
|
919
|
-
|
|
763
|
+
### Пример использования на фронтенде (React)
|
|
920
764
|
|
|
921
|
-
|
|
765
|
+
**Хук для проверки политик**
|
|
922
766
|
|
|
923
|
-
|
|
767
|
+
```tsx
|
|
768
|
+
// hooks/use-ability.ts
|
|
769
|
+
import { useEffect, useState } from 'react';
|
|
770
|
+
import { AbilityResolver } from '@via-profit/ability';
|
|
771
|
+
import { Resources } from './generated-types';
|
|
924
772
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
773
|
+
export function useAbility<Permission extends keyof Resources>(
|
|
774
|
+
resolver: AbilityResolver<Resources>,
|
|
775
|
+
permission: Permission,
|
|
776
|
+
resource: Resources[Permission],
|
|
777
|
+
) {
|
|
778
|
+
const [allowed, setAllowed] = useState<boolean | null>(null);
|
|
928
779
|
|
|
929
|
-
|
|
780
|
+
useEffect(() => {
|
|
781
|
+
let cancelled = false;
|
|
930
782
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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` | Создает правило с условием "больше или равно" |
|
|
783
|
+
async function check() {
|
|
784
|
+
try {
|
|
785
|
+
const result = await resolver.resolve(permission, resource);
|
|
786
|
+
if (!cancelled) {
|
|
787
|
+
setAllowed(result.isAllowed());
|
|
788
|
+
}
|
|
789
|
+
} catch {
|
|
790
|
+
if (!cancelled) {
|
|
791
|
+
setAllowed(false);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
963
795
|
|
|
964
|
-
|
|
796
|
+
check();
|
|
965
797
|
|
|
966
|
-
|
|
798
|
+
return () => {
|
|
799
|
+
cancelled = true;
|
|
800
|
+
};
|
|
801
|
+
}, [resolver, permission, resource]);
|
|
967
802
|
|
|
968
|
-
|
|
803
|
+
return allowed;
|
|
804
|
+
}
|
|
805
|
+
```
|
|
969
806
|
|
|
970
|
-
|
|
971
|
-
- `state: AbilityMatch` - текущее состояние группы после вызова `check()`
|
|
972
|
-
- `rules: AbilityRule[]` - массив правил в группе
|
|
973
|
-
- `compareMethod: AbilityCompare` - метод сравнения (AND/OR)
|
|
974
|
-
- `name: string` - название группы
|
|
975
|
-
- `id: string` - уникальный идентификатор
|
|
807
|
+
**Использование в компоненте**
|
|
976
808
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
| `parse()` | `config: AbilityRuleSetConfig` | `AbilityRuleSet` | Статический метод. Создает экземпляр группы из конфигурации |
|
|
984
|
-
| `and()` | `rules: AbilityRule[]` | `AbilityRuleSet` | Статический метод. Создает группу с логическим И |
|
|
985
|
-
| `or()` | `rules: AbilityRule[]` | `AbilityRuleSet` | Статический метод. Создает группу с логическим ИЛИ |
|
|
809
|
+
```tsx
|
|
810
|
+
function OrderUpdateButton({ order, user }) {
|
|
811
|
+
const allowed = useAbility(resolver, 'order.update', {
|
|
812
|
+
user,
|
|
813
|
+
order,
|
|
814
|
+
});
|
|
986
815
|
|
|
987
|
-
|
|
816
|
+
if (allowed === null) {
|
|
817
|
+
return null; // или бейдж загрузки
|
|
818
|
+
}
|
|
988
819
|
|
|
989
|
-
|
|
820
|
+
if (!allowed) {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
990
823
|
|
|
991
|
-
|
|
824
|
+
return <button>Update order</button>;
|
|
825
|
+
}
|
|
826
|
+
```
|
|
992
827
|
|
|
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
828
|
|
|
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. Удобен для загрузки нескольких политик одновременно. |
|
|
829
|
+
## Примеры
|
|
1010
830
|
|
|
1011
|
-
---
|
|
1012
831
|
|
|
1013
|
-
###
|
|
832
|
+
### Пример сложной многоступенчатой политики
|
|
1014
833
|
|
|
1015
|
-
|
|
834
|
+
Ниже - многоступенчатый набор политик, на примере использования в кинотеатре (выдуманный пример).
|
|
1016
835
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
| `isDeny()` | — | `boolean` | Проверяет, запрещен ли доступ |
|
|
1026
|
-
| `getMatchedPolicy()` | — | `AbilityPolicy \| null` | Возвращает последнюю сработавшую политику |
|
|
1027
|
-
| `isInActionContain()` | `actionA: string, actionB: string` | `boolean` | Статический метод. Проверяет, соответствует ли действие шаблону (поддерживает `*`) |
|
|
836
|
+
**Пример демонстрирует:**
|
|
837
|
+
- работу с ролями (admin, seller, manager, VIP, banned),
|
|
838
|
+
- временн́ые ограничения (`env.time.hour`),
|
|
839
|
+
- wildcard‑права (`permission.*`),
|
|
840
|
+
- ограничения по количеству билетов,
|
|
841
|
+
- запрет на продажу уже проданных билетов,
|
|
842
|
+
- комбинацию `permit`/`deny`‑политик,
|
|
843
|
+
- приоритет политик и модель Default Deny.
|
|
1028
844
|
|
|
1029
|
-
---
|
|
1030
845
|
|
|
1031
|
-
|
|
846
|
+
**Краткое описание правил**
|
|
847
|
+
- **Администратор**
|
|
848
|
+
Имеет wildcard‑права (`permission.*`) и может выполнять любые действия.
|
|
849
|
+
Может редактировать стоимость билетов.
|
|
1032
850
|
|
|
1033
|
-
|
|
851
|
+
- **Продавец**
|
|
852
|
+
Может продавать билеты только в рабочие часы (09:00–23:00).
|
|
853
|
+
Не может продавать билеты, если:
|
|
854
|
+
- кинотеатр закрыт,
|
|
855
|
+
- билет уже продан.
|
|
1034
856
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
- `children: AbilityExplain[]` - дочерние элементы объяснения
|
|
1038
|
-
- `name: string` - название элемента
|
|
1039
|
-
- `match: AbilityMatch` - результат проверки
|
|
857
|
+
- **Менеджер**
|
|
858
|
+
Имеет те же права, что и продавец.
|
|
1040
859
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
860
|
+
- **Покупатели**
|
|
861
|
+
- Пользователь старше 21 года может покупать билеты.
|
|
862
|
+
- VIP‑пользователь может покупать билеты в любое время.
|
|
863
|
+
- Заблокированный пользователь (`status = banned`) не может покупать билеты.
|
|
864
|
+
- Любой пользователь не может купить более 6 билетов.
|
|
1044
865
|
|
|
1045
|
-
**Наследники:**
|
|
1046
|
-
- `AbilityExplainRule` - объяснение для правила
|
|
1047
|
-
- `AbilityExplainRuleSet` - объяснение для группы правил
|
|
1048
|
-
- `AbilityExplainPolicy` - объяснение для политики
|
|
1049
866
|
|
|
1050
|
-
|
|
867
|
+
**Общая диаграмма политик**
|
|
868
|
+
|
|
869
|
+
```mermaid
|
|
870
|
+
flowchart LR
|
|
871
|
+
|
|
872
|
+
%% ==== ROLES ====
|
|
873
|
+
|
|
874
|
+
subgraph Roles[Роли]
|
|
875
|
+
A[Администратор]
|
|
876
|
+
B[Продавец]
|
|
877
|
+
C[Менеджер]
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
subgraph Buyers[Покупатели]
|
|
881
|
+
U1[Пользователь > 21]
|
|
882
|
+
U2[VIP пользователь]
|
|
883
|
+
U3[Заблокированный пользователь]
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
%% ==== ADMIN ====
|
|
887
|
+
|
|
888
|
+
A --> A1[Wildcard: permission.*]
|
|
889
|
+
A --> A2[Редактировать цену билетов]
|
|
890
|
+
|
|
891
|
+
A1 --> FINAL[Итоговое решение]
|
|
892
|
+
A2 --> FINAL
|
|
893
|
+
|
|
894
|
+
%% ==== SELLER ====
|
|
895
|
+
|
|
896
|
+
B --> B1[Продавать билеты]
|
|
897
|
+
|
|
898
|
+
B1 -->|09:00–23:00| B2[Разрешено]
|
|
899
|
+
B1 -->|Вне времени| D2[Запрещено]
|
|
900
|
+
B1 -->|ticket.status = sold| D3[Запрещено]
|
|
901
|
+
|
|
902
|
+
B2 --> FINAL
|
|
903
|
+
D2 --> FINAL
|
|
904
|
+
D3 --> FINAL
|
|
905
|
+
|
|
906
|
+
%% ==== MANAGER ====
|
|
907
|
+
|
|
908
|
+
C --> C1[Продавать билеты как продавец]
|
|
909
|
+
C1 --> FINAL
|
|
910
|
+
|
|
911
|
+
%% ==== BUYERS ====
|
|
912
|
+
|
|
913
|
+
U1 --> U1A[Покупать билеты]
|
|
914
|
+
U1A -->|ticketsCount < 6| U1OK[Разрешено]
|
|
915
|
+
U1A -->|ticketsCount ≥ 6| U1DENY[Запрещено]
|
|
916
|
+
|
|
917
|
+
U2 --> U2A[Покупать билеты в любое время]
|
|
918
|
+
U2A -->|ticketsCount < 6| U2OK[Разрешено]
|
|
919
|
+
U2A -->|ticketsCount ≥ 6| U2DENY[Запрещено]
|
|
920
|
+
|
|
921
|
+
U3 --> U3A[Запрещено покупать билеты]
|
|
922
|
+
|
|
923
|
+
U1OK --> FINAL
|
|
924
|
+
U1DENY --> FINAL
|
|
925
|
+
U2OK --> FINAL
|
|
926
|
+
U2DENY --> FINAL
|
|
927
|
+
U3A --> FINAL
|
|
928
|
+
|
|
929
|
+
%% ==== DENY RULES ====
|
|
930
|
+
|
|
931
|
+
D1[Запрещено покупать билеты, если user.status = banned] --> FINAL
|
|
1051
932
|
|
|
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
|
-
// }
|
|
1116
933
|
```
|
|
1117
934
|
|
|
1118
|
-
|
|
935
|
+
**DSL политик**
|
|
1119
936
|
|
|
1120
|
-
|
|
937
|
+
```dsl
|
|
938
|
+
############################################################
|
|
939
|
+
# @name Admin can edit ticket price
|
|
940
|
+
permit permission.ticket.price.edit if all:
|
|
941
|
+
user.role is equals 'admin'
|
|
1121
942
|
|
|
1122
|
-
| Класс | Назначение |
|
|
1123
|
-
|-------|------------|
|
|
1124
|
-
| `AbilityError` | Общая ошибка доступа, выбрасывается при запрете в `enforce()` |
|
|
1125
|
-
| `AbilityParserError` | Ошибка парсинга конфигурации или генерации типов |
|
|
1126
943
|
|
|
1127
|
-
|
|
944
|
+
############################################################
|
|
945
|
+
# @name Seller can sell tickets during working hours
|
|
946
|
+
permit permission.ticket.sell if all:
|
|
947
|
+
user.role is equals 'seller'
|
|
948
|
+
all of:
|
|
949
|
+
env.time.hour greater than or equal 9
|
|
950
|
+
env.time.hour less than or equal 23
|
|
1128
951
|
|
|
1129
|
-
### Именование экшенов
|
|
1130
952
|
|
|
1131
|
-
|
|
953
|
+
############################################################
|
|
954
|
+
# @name Users older than 21 can buy tickets
|
|
955
|
+
permit permission.ticket.buy if all:
|
|
956
|
+
user.age greater than 21
|
|
1132
957
|
|
|
1133
|
-
- `order.create` - создание заказа
|
|
1134
|
-
- `order.update` - обновление заказа
|
|
1135
|
-
- `order.status.update` - обновление статуса заказа
|
|
1136
958
|
|
|
1137
|
-
|
|
959
|
+
############################################################
|
|
960
|
+
# @name VIP users can buy tickets anytime
|
|
961
|
+
permit permission.ticket.buy if all:
|
|
962
|
+
user.isVIP is true
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
############################################################
|
|
966
|
+
# @name Deny buying tickets if user is banned
|
|
967
|
+
deny permission.ticket.buy if all:
|
|
968
|
+
user.status is equals 'banned'
|
|
1138
969
|
|
|
1139
|
-
|
|
970
|
+
|
|
971
|
+
############################################################
|
|
972
|
+
# @name Deny selling tickets if cinema is closed
|
|
973
|
+
deny permission.ticket.sell if all:
|
|
974
|
+
any of:
|
|
975
|
+
env.time.hour less than 9
|
|
976
|
+
env.time.hour greater than 23
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
############################################################
|
|
980
|
+
# @name Manager can do everything seller can
|
|
981
|
+
permit permission.ticket.sell if all:
|
|
982
|
+
user.role is equals 'manager'
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
############################################################
|
|
986
|
+
# @name Admin wildcard permissions
|
|
987
|
+
permit permission.* if all:
|
|
988
|
+
user.role is equals 'admin'
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
############################################################
|
|
992
|
+
# @name Limit tickets per user (max 6)
|
|
993
|
+
deny permission.ticket.buy if all:
|
|
994
|
+
user.ticketsCount greater than or equal 6
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
############################################################
|
|
998
|
+
# @name Cannot sell already sold tickets
|
|
999
|
+
deny permission.ticket.sell if all:
|
|
1000
|
+
ticket.status is equals 'sold'
|
|
1001
|
+
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
Ниже показано, как использовать приведённые выше политики в Node.js + TypeScript.
|
|
1006
|
+
|
|
1007
|
+
**Подготовка политик**
|
|
1140
1008
|
|
|
1141
1009
|
```ts
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1010
|
+
import { AbilityDSLParser } from '@via-profit/ability';
|
|
1011
|
+
import cinemaDSL from './policies/cinema.dsl';
|
|
1012
|
+
|
|
1013
|
+
export const policies = new AbilityDSLParser(cinemaDSL).parse();
|
|
1014
|
+
```
|
|
1147
1015
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1016
|
+
**Создание резолвера**
|
|
1017
|
+
|
|
1018
|
+
```ts
|
|
1019
|
+
import { AbilityResolver } from '@via-profit/ability';
|
|
1020
|
+
import { policies } from './policies';
|
|
1021
|
+
|
|
1022
|
+
const resolver = new AbilityResolver(policies);
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
**Проверка разрешений (enforce)**
|
|
1026
|
+
|
|
1027
|
+
Пример: покупка билета.
|
|
1028
|
+
|
|
1029
|
+
Метод enforce выбрасывает исключение `AbilityError`, если доступ запрещён.
|
|
1030
|
+
|
|
1031
|
+
```ts
|
|
1032
|
+
await resolver.enforce('ticket.buy', {
|
|
1033
|
+
user: { age: 25, ticketsCount: 1 },
|
|
1034
|
+
env: { time: { hour: 18 } },
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
```
|
|
1038
|
+
Если разрешено — код продолжит выполнение.
|
|
1039
|
+
Если запрещено — будет выброшено исключение `AbilityError`.
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
**Проверка разрешений без исключений (resolve)**
|
|
1044
|
+
|
|
1045
|
+
`resolve` возвращает объект результата:
|
|
1046
|
+
|
|
1047
|
+
```ts
|
|
1048
|
+
const result = await resolver.resolve('ticket.buy', {
|
|
1049
|
+
user: { age: 25, ticketsCount: 1 },
|
|
1050
|
+
env: { time: { hour: 18 } },
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
if (result.isAllowed()) {
|
|
1054
|
+
console.log('Покупка разрешена');
|
|
1055
|
+
} else {
|
|
1056
|
+
console.log('Покупка запрещена');
|
|
1154
1057
|
}
|
|
1058
|
+
|
|
1155
1059
|
```
|
|
1156
1060
|
|
|
1157
|
-
|
|
1061
|
+
**Продавец может продавать только в рабочие часы***
|
|
1158
1062
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1063
|
+
```ts
|
|
1064
|
+
await resolver.enforce('ticket.sell', {
|
|
1065
|
+
user: { role: 'seller' },
|
|
1066
|
+
env: { time: { hour: 15 } },
|
|
1067
|
+
ticket: { status: 'available' },
|
|
1068
|
+
});
|
|
1162
1069
|
|
|
1163
|
-
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
**Подготовка данных для резолвера**
|
|
1073
|
+
|
|
1074
|
+
В примерах выше в резолвер передаются простые константные объекты:
|
|
1075
|
+
|
|
1076
|
+
```ts
|
|
1077
|
+
resolver.enforce('ticket.buy', {
|
|
1078
|
+
user: { age: 25 },
|
|
1079
|
+
env: { time: { hour: 18 } },
|
|
1080
|
+
});
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
Это сделано для наглядности. В реальном приложении данные для резолвера должны формироваться динамически — из тех источников, которые доступны вашему серверу.
|
|
1084
|
+
|
|
1085
|
+
**Пользователь** (`user`) обычно берётся из:
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
- JWT‑токена
|
|
1089
|
+
- сессии
|
|
1090
|
+
- базы данных
|
|
1091
|
+
- middleware авторизации
|
|
1092
|
+
|
|
1093
|
+
Пример:
|
|
1094
|
+
|
|
1095
|
+
```ts
|
|
1096
|
+
const user = await db.users.findById(session.userId);
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
**Окружение (Environment)** (`env`)
|
|
1100
|
+
|
|
1101
|
+
Это любые внешние параметры, которые могут влиять на доступ:
|
|
1102
|
+
|
|
1103
|
+
- текущее время сервера
|
|
1104
|
+
- часовой пояс
|
|
1105
|
+
- IP‑адрес
|
|
1106
|
+
- заголовки запроса
|
|
1107
|
+
- конфигурация системы
|
|
1108
|
+
|
|
1109
|
+
Пример:
|
|
1110
|
+
|
|
1111
|
+
```ts
|
|
1112
|
+
const env = {
|
|
1113
|
+
time: {
|
|
1114
|
+
hour: new Date().getHours(),
|
|
1115
|
+
},
|
|
1116
|
+
ip: req.ip,
|
|
1117
|
+
};
|
|
1118
|
+
```
|
|
1164
1119
|
|
|
1165
|
-
|
|
1120
|
+
**Ресурс** (например, `ticket`)
|
|
1121
|
+
|
|
1122
|
+
Если действие связано с конкретным объектом — его тоже нужно загрузить:
|
|
1166
1123
|
|
|
1167
1124
|
```ts
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1125
|
+
const ticket = await db.tickets.findById(req.params.ticketId);
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
**Контекст**
|
|
1129
|
+
|
|
1130
|
+
Контекст — это объект, который вы передаёте в `resolve` или `enforce`.
|
|
1131
|
+
Он содержит **все данные**, которые могут понадобиться политикам:
|
|
1132
|
+
|
|
1133
|
+
- `user` — данные о текущем пользователе
|
|
1134
|
+
- `env` — данные окружения (время, IP, география, настройки системы)
|
|
1135
|
+
- `resource` или `ticket` — данные о сущности, над которой выполняется действие
|
|
1136
|
+
- любые другие объекты, которые вы используете в DSL
|
|
1137
|
+
|
|
1138
|
+
**Важно понимать:**
|
|
1139
|
+
|
|
1140
|
+
> Контекст формируется под конкретное действие и конкретные политики. Его не нужно хранить заранее — вы собираете его динамически перед вызовом резолвера.
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
## Производительность
|
|
1144
|
+
|
|
1145
|
+
В тестах задействовались политики на 10 условий, вложенные поля, environment.
|
|
1146
|
+
|
|
1147
|
+
**Tinybench** ([https://github.com/tinylibs/tinybench](https://github.com/tinylibs/tinybench))
|
|
1148
|
+
|
|
1149
|
+
| # | Task name | Latency avg (ns) | Latency med (ns) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples |
|
|
1150
|
+
|---|-----------------------------------------|------------------------|------------------------|--------------------------|--------------------------|---------|
|
|
1151
|
+
| 0 | resolve() — no cache (heavy rules) | 646317 ± 0.32% | 632319 ± 8446.0 | 1555 ± 0.21% | 1581 ± 21 | 3095 |
|
|
1152
|
+
| 1 | resolve() — cold cache (heavy rules) | 636363 ± 0.38% | 623092 ± 7885.0 | 1581 ± 0.21% | 1605 ± 20 | 3143 |
|
|
1153
|
+
| 2 | resolve() — warm cache (heavy rules) | 631328 ± 0.26% | 621152 ± 6562.5 | 1590 ± 0.17% | 1610 ± 17 | 3168 |
|
|
1154
|
+
|
|
1155
|
+
```
|
|
1156
|
+
Latency (ns)
|
|
1157
|
+
650k | ███████████████████████████████████████ resolve() — no cache
|
|
1158
|
+
640k | █████████████████████████████████████ resolve() — cold cache
|
|
1159
|
+
630k | ████████████████████████████████████ resolve() — warm cache
|
|
1160
|
+
--------------------------------------------------------------
|
|
1161
|
+
no cache cold cache warm cache
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
```
|
|
1165
|
+
Throughput (ops/s)
|
|
1166
|
+
1600 | ███████████████████████████████████████ resolve() — warm cache
|
|
1167
|
+
1590 | ██████████████████████████████████████ resolve() — cold cache
|
|
1168
|
+
1580 | █████████████████████████████████████ resolve() — no cache
|
|
1169
|
+
--------------------------------------------------------------
|
|
1170
|
+
no cache cold cache warm cache
|
|
1171
|
+
|
|
1176
1172
|
```
|
|
1177
1173
|
|
|
1178
1174
|
|