@via-profit/ability 3.0.1 → 3.1.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 +65 -0
- package/README.md +900 -934
- 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
|
@@ -1,1181 +1,1147 @@
|
|
|
1
1
|
# @via-profit/Ability
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
|
|
6
|
-
Этот сервис позволяет создавать правила и политики, применять их к данным и проверять доступ на их основе.
|
|
7
|
-
|
|
8
|
-
## Содержание
|
|
9
|
-
|
|
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
|
-
- [Лицензия](#лицензия)
|
|
3
|
+
> A set of services that partially implement the [Attribute Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control) principle.
|
|
4
|
+
> The package allows you to describe rules, combine them into groups, form policies, and apply them to data to determine permissions.
|
|
50
5
|
|
|
51
|
-
|
|
6
|
+
## Language / Язык
|
|
52
7
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
}
|
|
8
|
+
- [🇬🇧 English](/docs/en/README.md)
|
|
9
|
+
- [🇷🇺 Русский](/docs/ru/README.md)
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
The package is intended as a **lightweight and extremely simple alternative** to heavy access control systems.
|
|
14
|
+
Without complex configurations, without dependencies — just a minimal set of tools that allows you to describe rules and policies in a maximally simple DSL.
|
|
15
|
+
|
|
16
|
+
## Table of Contents
|
|
17
|
+
|
|
18
|
+
- [Quick Start](#quick-start)
|
|
19
|
+
- [Fundamentals](#fundamentals)
|
|
20
|
+
- [DSL](#dsl)
|
|
21
|
+
- [Combining Policies](#combining-policies)
|
|
22
|
+
- [Policy Environment](#policy-environment)
|
|
23
|
+
- [TypeScript Type Generator](#typescript-type-generator)
|
|
24
|
+
- [Policy Debugging](#policy-debugging)
|
|
25
|
+
- [Troubleshooting](#troubleshooting)
|
|
26
|
+
- [Design Recommendations](#design-recommendations)
|
|
27
|
+
- [Examples](#examples)
|
|
28
|
+
- [Performance](#performance)
|
|
29
|
+
- [API Reference](./api.md)
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
Install the package, write DSL, call the parser, and run the resolver.
|
|
34
|
+
|
|
35
|
+
### Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @via-profit/ability
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
yarn add @via-profit/ability
|
|
123
43
|
```
|
|
124
44
|
|
|
125
|
-
|
|
45
|
+
```bash
|
|
46
|
+
pnpm add @via-profit/ability
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Example: Deny access to `passwordHash` for everyone except the owner
|
|
50
|
+
|
|
51
|
+
Suppose we have user data:
|
|
126
52
|
|
|
127
53
|
```ts
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
});
|
|
54
|
+
const user = {
|
|
55
|
+
id: '1',
|
|
56
|
+
login: 'user-001',
|
|
57
|
+
passwordHash: '...',
|
|
58
|
+
};
|
|
135
59
|
```
|
|
136
60
|
|
|
137
|
-
|
|
61
|
+
We need to deny reading `passwordHash` to everyone except the user themselves.
|
|
62
|
+
|
|
63
|
+
#### DSL Policy
|
|
138
64
|
|
|
139
|
-
|
|
65
|
+
In the policy language, this looks like:
|
|
140
66
|
|
|
141
|
-
|
|
142
|
-
|
|
67
|
+
```
|
|
68
|
+
deny permission.user.passwordHash if any:
|
|
69
|
+
viewer.id is not equals owner.id
|
|
70
|
+
```
|
|
143
71
|
|
|
144
|
-
|
|
72
|
+
**Explanation:**
|
|
145
73
|
|
|
146
|
-
|
|
74
|
+
- `deny` — policy effect (deny access)
|
|
75
|
+
- `permission.user.passwordHash` — permission key.
|
|
76
|
+
- `if any:` — start of the condition block
|
|
77
|
+
- `viewer.id is not equals owner.id` — rule: if the requester's ID is not equal to the owner's ID
|
|
147
78
|
|
|
148
|
-
|
|
79
|
+
If `viewer.id` is not equal to `owner.id`, the rule is satisfied and the policy returns `deny` — access denied. If the IDs match (i.e., the user requests their own data), the rule does not trigger, and access is allowed.
|
|
149
80
|
|
|
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` или значение, которое может быть строкой, числом, булеан значением или массивом строк, или чисел.
|
|
81
|
+
*Note: The permission key is formed according to the principle: `permission.` + your custom key in dot notation. For example, the key `foo.bar.baz` in DSL would be `permission.foo.bar.baz`.*
|
|
156
82
|
|
|
157
|
-
|
|
83
|
+
#### Check in Code
|
|
158
84
|
|
|
159
85
|
```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
|
-
});
|
|
86
|
+
import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
|
|
169
87
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
88
|
+
const dsl = `
|
|
89
|
+
deny permission.user.passwordHash if any:
|
|
90
|
+
viewer.id is not equals owner.id
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
const policies = new AbilityDSLParser(dsl).parse(); // obtain policies
|
|
94
|
+
const resolver = new AbilityResolver(policies); // create resolver
|
|
175
95
|
|
|
96
|
+
resolver.enforce('user.passwordHash', {
|
|
97
|
+
viewer: { id: '1' },
|
|
98
|
+
owner: { id: '2' },
|
|
99
|
+
}); // will throw an error — access denied
|
|
176
100
|
```
|
|
101
|
+
In `enforce`, the key is passed without the `permission.` prefix — it is automatically removed by the parser.
|
|
177
102
|
|
|
178
|
-
|
|
103
|
+
## Fundamentals
|
|
179
104
|
|
|
180
|
-
|
|
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
|
-
});
|
|
105
|
+
Let’s briefly list the key points you need to know before starting to use the package:
|
|
190
106
|
|
|
107
|
+
1. The resolver (`AbilityResolver`) follows the **Default Deny** principle. This means that if no policy matches, the result is `deny` ([more details here](#troubleshooting)). To avoid unexpected `deny`, ensure there is at least one `permit` policy that can match. Only then add `deny` policies.
|
|
108
|
+
2. Policies are applied sequentially. If multiple policies match, the result is determined by the last matching policy.
|
|
109
|
+
3. Rules are executed sequentially.
|
|
110
|
+
4. In a rule set (`RuleSet`) with the `all` comparison operator, further rule execution stops as soon as the first rule returns `mismatch`.
|
|
111
|
+
5. Use [DSL](#dsl) to compose policies — it's simpler and more convenient.
|
|
112
|
+
6. For storing policies on the server, use JSON. Policies can be exported to JSON and imported from JSON.
|
|
113
|
+
7. Generally, rely on the principle: if permission is not explicitly granted → access is denied.
|
|
114
|
+
8. Use the built-in cache only if your policies are incredibly complex and contain a large number of rules.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## DSL
|
|
119
|
+
|
|
120
|
+
> DSL - Domain-Specific Language
|
|
121
|
+
|
|
122
|
+
Ability DSL is a declarative language for describing access policies.
|
|
123
|
+
It allows you to define rules in a human-readable form using simple constructs: *policies*, *groups*, *rules*, and *annotations*.
|
|
124
|
+
|
|
125
|
+
### Policy Structure
|
|
126
|
+
|
|
127
|
+
A policy consists of:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
<effect> <permission> if <all|any>:
|
|
131
|
+
<group>...
|
|
191
132
|
```
|
|
192
133
|
|
|
193
|
-
|
|
134
|
+
Where:
|
|
194
135
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
136
|
+
- **effect** — `permit` or `deny`
|
|
137
|
+
- **permission** — a string of the form `permission.foo.bar`, where the `permission.` prefix is mandatory.
|
|
138
|
+
- **if all:** — all groups must be true
|
|
139
|
+
- **if any:** — at least one group must be true
|
|
198
140
|
|
|
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
|
-
});
|
|
141
|
+
A policy can contain one or more rule groups.
|
|
209
142
|
|
|
210
|
-
|
|
211
|
-
user: {
|
|
212
|
-
department: 'managers',
|
|
213
|
-
},
|
|
214
|
-
});
|
|
143
|
+
Example:
|
|
215
144
|
|
|
216
|
-
|
|
145
|
+
```dsl
|
|
146
|
+
permit permission.order.update if any:
|
|
147
|
+
all of:
|
|
148
|
+
user.roles contains 'admin'
|
|
149
|
+
user.token is not null
|
|
217
150
|
|
|
151
|
+
any of:
|
|
152
|
+
user.roles contains 'developer'
|
|
153
|
+
user.login is equals 'dev'
|
|
218
154
|
```
|
|
219
155
|
|
|
220
|
-
|
|
156
|
+
> The `permission.` prefix is mandatory in DSL but is automatically removed by the parser. Internally, the permission is stored as `order.update`.
|
|
157
|
+
|
|
158
|
+
The example policy above says: permission `order.update` will be allowed if one of two conditions is met:
|
|
159
|
+
1. `user.roles` contains 'admin' **and** `user.token` is not null
|
|
160
|
+
2. `user.roles` contains 'developer' **or** `user.login` equals 'dev'
|
|
221
161
|
|
|
222
|
-
|
|
162
|
+
### Permission Key
|
|
223
163
|
|
|
224
|
-
|
|
164
|
+
Permission keys are written in dot notation but support the use of wildcard patterns with the `*` character. This allows grouping of keys and overriding policies with similar keys.
|
|
225
165
|
|
|
166
|
+
If multiple policies match a key, **all of them are executed**. The final result is determined by the **last matching policy**:
|
|
167
|
+
|
|
168
|
+
**Example of using wildcards**
|
|
169
|
+
|
|
170
|
+
| Policy (permission) | Key | Matches |
|
|
171
|
+
|---------------------|-----------------------|---------|
|
|
172
|
+
| `order.*` | `order.create` | yes |
|
|
173
|
+
| `order.*` | `order.update` | yes |
|
|
174
|
+
| `order.*` | `user.create` | no |
|
|
175
|
+
| `*.create` | `order.create` | yes |
|
|
176
|
+
| `*.create` | `user.create` | yes |
|
|
177
|
+
| `*.create` | `order.update` | no |
|
|
178
|
+
| `user.profile.*` | `user.profile.update` | yes |
|
|
179
|
+
| `user.profile.*` | `user.settings.update`| no |
|
|
180
|
+
|
|
181
|
+
**Example of a policy with wildcard**
|
|
226
182
|
```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
|
-
};
|
|
183
|
+
import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
|
|
271
184
|
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
},
|
|
278
|
-
order: {
|
|
279
|
-
status: 'не обработан',
|
|
280
|
-
},
|
|
281
|
-
feature: {
|
|
282
|
-
status: 'завершен',
|
|
283
|
-
},
|
|
284
|
-
});
|
|
185
|
+
// DSL is not complete, shown for illustration only
|
|
186
|
+
const dsl = `
|
|
187
|
+
permit permission.order.*
|
|
188
|
+
deny permission.order.update
|
|
189
|
+
`;
|
|
285
190
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
});
|
|
191
|
+
const policies = new AbilityDSLParser(dsl).parse();
|
|
192
|
+
const resolver = new AbilityResolver(policies);
|
|
289
193
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
194
|
+
await resolver.enforce('order.update', resource); // will throw AbilityError
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Explanation**
|
|
198
|
+
|
|
199
|
+
In DSL, the order of policies matters:
|
|
200
|
+
the last matching policy wins.
|
|
201
|
+
|
|
202
|
+
Therefore:
|
|
203
|
+
|
|
204
|
+
1. `permit` `permission.order.*` allows everything that starts with `order.`
|
|
205
|
+
2. `deny` `permission.order.update` overrides this permission.
|
|
206
|
+
|
|
207
|
+
Execution result:
|
|
303
208
|
|
|
304
209
|
```
|
|
210
|
+
order.update → deny
|
|
211
|
+
order.create → permit
|
|
212
|
+
order.delete → permit
|
|
213
|
+
order.view → permit
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Comments
|
|
305
217
|
|
|
306
|
-
|
|
218
|
+
Lines starting with the `#` symbol are considered comments and do not affect the evaluation of rules and policies.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### Annotations
|
|
223
|
+
|
|
224
|
+
Currently, only one annotation is supported: `name`, which will be used as the name for a policy, rule group, or rule.
|
|
225
|
+
|
|
226
|
+
Annotations are specified via comments:
|
|
307
227
|
|
|
308
228
|
```
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
229
|
+
# @name <name>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Annotations apply to the **following entity**:
|
|
233
|
+
|
|
234
|
+
- policy
|
|
235
|
+
- group
|
|
236
|
+
- rule
|
|
237
|
+
|
|
238
|
+
Example:
|
|
239
|
+
|
|
240
|
+
```dsl
|
|
241
|
+
# @name can order update
|
|
242
|
+
permit permission.order.update if any:
|
|
243
|
+
# @name authorized admin
|
|
244
|
+
all of:
|
|
245
|
+
# @name contains role admin
|
|
246
|
+
user.roles contains 'admin'
|
|
315
247
|
```
|
|
316
248
|
|
|
317
249
|
---
|
|
318
250
|
|
|
319
|
-
|
|
251
|
+
### Rule Groups
|
|
320
252
|
|
|
321
|
-
|
|
322
|
-
правила в группе и вернуть лишь один результат.
|
|
253
|
+
A group defines how the rules within it are combined:
|
|
323
254
|
|
|
324
|
-
|
|
325
|
-
|
|
255
|
+
```
|
|
256
|
+
all of:
|
|
257
|
+
<rule>
|
|
258
|
+
<rule>
|
|
326
259
|
|
|
327
|
-
|
|
260
|
+
any of:
|
|
261
|
+
<rule>
|
|
262
|
+
<rule>
|
|
263
|
+
```
|
|
328
264
|
|
|
329
|
-
-
|
|
330
|
-
-
|
|
331
|
-
- **compareMethod** - `AbilityCompare` Способ сравнения правил в группе (`or` или `and`).
|
|
265
|
+
- `all of:` — logical AND
|
|
266
|
+
- `any of:` — logical OR
|
|
332
267
|
|
|
333
|
-
|
|
268
|
+
`all of` means that the group is considered satisfied if all rules within the group match.
|
|
334
269
|
|
|
335
|
-
|
|
336
|
-
- **`and`** - Результат всей группы примет значение `match`, если все правила вернули `match`.
|
|
270
|
+
`any of` means that the group is considered satisfied if at least one rule within the group matches.
|
|
337
271
|
|
|
338
|
-
|
|
272
|
+
Each group within a policy will be evaluated independently of other groups. The final result is determined by comparing the results of all groups in the policy.
|
|
339
273
|
|
|
340
|
-
|
|
274
|
+
Groups can have annotations:
|
|
341
275
|
|
|
342
|
-
|
|
276
|
+
```dsl
|
|
277
|
+
# @name developer group
|
|
278
|
+
any of:
|
|
279
|
+
user.roles contains 'developer'
|
|
280
|
+
```
|
|
343
281
|
|
|
344
|
-
|
|
345
|
-
import { AbilityRuleSet, AbilityCompare } from '@via-profit/ability';
|
|
282
|
+
---
|
|
346
283
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
compareMethod: AbilityCompare.and,
|
|
351
|
-
});
|
|
284
|
+
### Rules
|
|
285
|
+
|
|
286
|
+
A rule is an atomic condition inside a policy. It defines under what data the policy is considered matched. Rules set the conditions that determine the effectiveness of a policy (`permit` or `deny`).
|
|
352
287
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
288
|
+
A rule has the form:
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
<subject> <operator> <value?> — the value is not required for some operators (e.g., `is null` does not require a value).
|
|
292
|
+
```
|
|
358
293
|
|
|
294
|
+
#### Subject
|
|
359
295
|
|
|
360
|
-
|
|
361
|
-
const ruleSet2 = AbilityRuleSet.and([
|
|
362
|
-
new AbilityRule(...),
|
|
363
|
-
new AbilityRule(...),
|
|
364
|
-
]);
|
|
296
|
+
Identifier in dot notation:
|
|
365
297
|
|
|
298
|
+
```
|
|
299
|
+
user.roles
|
|
300
|
+
env.time.hour
|
|
301
|
+
order.total
|
|
366
302
|
```
|
|
367
303
|
|
|
368
|
-
|
|
304
|
+
#### Operators
|
|
369
305
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
306
|
+
*Synonyms are alternative forms of writing that are also supported by the parser.*
|
|
307
|
+
|
|
308
|
+
**Basic Comparison Operators**
|
|
309
|
+
|
|
310
|
+
| DSL Operator | Synonyms | Example | Description | Types |
|
|
311
|
+
|--------------|----------|---------|-------------|-------|
|
|
312
|
+
| **is equals** | `=`, `==`, `equals` | `age is equals 18` | Strict equality | number, string, boolean |
|
|
313
|
+
| **is not equals** | `!=`, `<>`, `not equals` | `role is not equals 'admin'` | Strict inequality | number, string, boolean |
|
|
314
|
+
| **greater than** | `>`, `gt` | `age greater than 18` | Greater than | number, date |
|
|
315
|
+
| **greater than or equal** | `>=`, `gte` | `age greater than or equal 18` | Greater than or equal | number, date |
|
|
316
|
+
| **less than** | `<`, `lt` | `age less than 18` | Less than | number, date |
|
|
317
|
+
| **less than or equal** | `<=`, `lte` | `age less than or equal 18` | Less than or equal | number, date |
|
|
318
|
+
|
|
319
|
+
**Null Operators**
|
|
320
|
+
|
|
321
|
+
| DSL Operator | Synonyms | Example | Description | Types |
|
|
322
|
+
|--------------|----------|---------|-------------|-------|
|
|
323
|
+
| **is null** | `== null`, `= null` | `middleName is null` | Value is absent | any |
|
|
324
|
+
| **is not null** | `!= null` | `middleName is not null` | Value is present | any |
|
|
325
|
+
|
|
326
|
+
**Operators for Lists (Arrays)**
|
|
327
|
+
|
|
328
|
+
| DSL Operator | Synonyms | Example | Description | Types |
|
|
329
|
+
|--------------|---------------------------|---------|-------------|-------|
|
|
330
|
+
| **in [...]** | - | `role in ['admin', 'manager']` | Value is in the list | number, string |
|
|
331
|
+
| **not in [...]** | - | `role not in ['banned']` | Value is not in the list | number, string |
|
|
332
|
+
| **contains** | `includes`, `has` | `tags contains 'vip'` | Array contains the element | array |
|
|
333
|
+
| **not contains** | `not includes`, `not has` | `tags not contains 'vip'` | Array does not contain the element | array |
|
|
387
334
|
|
|
335
|
+
**String Operators**
|
|
336
|
+
|
|
337
|
+
| DSL Operator | Synonyms | Example | Description | Types |
|
|
338
|
+
|--------------|----------|---------|-------------|-------|
|
|
339
|
+
| **starts with** | `begins with` | `email starts with 'admin@'` | String starts with | string |
|
|
340
|
+
| **not starts with** | — | `email not starts with 'test'` | String does not start with | string |
|
|
341
|
+
| **ends with** | — | `email ends with '.ru'` | String ends with | string |
|
|
342
|
+
| **not ends with** | — | `email not ends with '.com'` | String does not end with | string |
|
|
343
|
+
| **includes** | `contains substring` | `name includes 'lex'` | String contains substring | string |
|
|
344
|
+
| **not includes** | — | `name not includes 'test'` | String does not contain substring | string |
|
|
345
|
+
|
|
346
|
+
**Boolean Operators**
|
|
347
|
+
|
|
348
|
+
| DSL Operator | Synonyms | Example | Description | Types |
|
|
349
|
+
|--------------|----------|---------|-------------|-------|
|
|
350
|
+
| **is true** | `= true` | `isActive is true` | Value is true | boolean |
|
|
351
|
+
| **is false** | `= false` | `isActive is false` | Value is false | boolean |
|
|
352
|
+
|
|
353
|
+
**Length Operators**
|
|
354
|
+
|
|
355
|
+
| DSL Operator | Synonyms | Example | Description | Types |
|
|
356
|
+
|--------------|----------|---------|-------------|-------|
|
|
357
|
+
| **length equals** | `len =` | `tags length equals 3` | Length equals | array, string |
|
|
358
|
+
| **length greater than** | `len >` | `tags length greater than 2` | Length greater than | array, string |
|
|
359
|
+
| **length less than** | `len <` | `tags length less than 5` | Length less than | array, string |
|
|
360
|
+
|
|
361
|
+
#### Value
|
|
362
|
+
|
|
363
|
+
Supported values:
|
|
364
|
+
|
|
365
|
+
- strings `'text'`
|
|
366
|
+
- numbers `42`
|
|
367
|
+
- booleans `true` / `false`
|
|
368
|
+
- `null`
|
|
369
|
+
- arrays `[1, 2, 3]` / `['foo', false, null, 1, 2, '999']`
|
|
370
|
+
|
|
371
|
+
Examples:
|
|
372
|
+
|
|
373
|
+
```dsl
|
|
374
|
+
# user age greater than 18
|
|
375
|
+
user.age greater than 18
|
|
376
|
+
|
|
377
|
+
# array of roles contains the role 'admin'
|
|
378
|
+
user.roles contains 'admin'
|
|
379
|
+
|
|
380
|
+
# order tag is either 'vip' or 'priority'
|
|
381
|
+
order.tag in ['vip', 'priority']
|
|
382
|
+
|
|
383
|
+
# user token is not null
|
|
384
|
+
user.token is not null
|
|
385
|
+
|
|
386
|
+
# user login is longer than 12 characters
|
|
387
|
+
user.login length greater than 12
|
|
388
388
|
```
|
|
389
389
|
|
|
390
|
-
|
|
390
|
+
---
|
|
391
391
|
|
|
392
|
-
|
|
393
|
-
Этот метод вернёт экземпляр класса `AbilityMatch`, при помощи методов которого можно определить имеется ли совпадение
|
|
394
|
-
для группы и переданных значений.
|
|
392
|
+
### Implicit Group
|
|
395
393
|
|
|
396
|
-
|
|
397
|
-
import { AbilityRuleSet, AbilityCompare } from '@via-profit/ability';
|
|
394
|
+
If rules are written without `all of:` or `any of:`, they are combined using the policy operator:
|
|
398
395
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
new AbilityRule(...),
|
|
405
|
-
new AbilityRule(...),
|
|
406
|
-
]);
|
|
396
|
+
```dsl
|
|
397
|
+
permit permission.order.update if all:
|
|
398
|
+
user.roles contains 'admin'
|
|
399
|
+
user.token is not null
|
|
400
|
+
```
|
|
407
401
|
|
|
408
|
-
|
|
402
|
+
Equivalent to:
|
|
409
403
|
|
|
410
|
-
|
|
404
|
+
```dsl
|
|
405
|
+
permit permission.order.update if all:
|
|
406
|
+
all of:
|
|
407
|
+
user.roles contains 'admin'
|
|
408
|
+
user.token is not null
|
|
411
409
|
```
|
|
412
410
|
|
|
413
|
-
|
|
411
|
+
The implicit group always matches the policy operator (`if all` or `if any`).
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
### Complete Example
|
|
414
416
|
|
|
415
|
-
|
|
417
|
+
```dsl
|
|
418
|
+
# @name order update allowed
|
|
419
|
+
permit permission.order.update if any:
|
|
416
420
|
|
|
417
|
-
|
|
418
|
-
|
|
421
|
+
# @name if admin
|
|
422
|
+
all of:
|
|
423
|
+
user.roles contains 'admin'
|
|
424
|
+
user.token is not null
|
|
419
425
|
|
|
420
|
-
|
|
426
|
+
# @name if developer
|
|
427
|
+
any of:
|
|
428
|
+
user.roles contains 'developer'
|
|
429
|
+
user.login is equals 'dev'
|
|
430
|
+
```
|
|
421
431
|
|
|
422
|
-
|
|
432
|
+
## Combining Policies
|
|
423
433
|
|
|
424
|
-
|
|
434
|
+
In a real project, you should use multiple policies at once.
|
|
425
435
|
|
|
426
|
-
|
|
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[]` Массив групп (см. [Группы правил](#группы-правил))
|
|
436
|
+
TODO: using multiple policies
|
|
438
437
|
|
|
439
|
-
|
|
440
|
-
необходимо ограничить какой-либо доступ, например, пользователю с недостаточными правами, то следует создавать политику
|
|
441
|
-
с эффектом `deny`.
|
|
438
|
+
## Policy Environment
|
|
442
439
|
|
|
443
|
-
|
|
440
|
+
**Environment** is an object containing context data that does not belong to either the user or the resource.
|
|
441
|
+
The content of the object is defined by the developer and can be any object consisting of primitives.
|
|
444
442
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
});
|
|
443
|
+
- request time,
|
|
444
|
+
- IP address,
|
|
445
|
+
- device parameters,
|
|
446
|
+
- request headers,
|
|
447
|
+
- session context,
|
|
448
|
+
- any other external conditions.
|
|
449
|
+
|
|
450
|
+
**Examples:**
|
|
464
451
|
|
|
452
|
+
```ts
|
|
453
|
+
type Environment = {
|
|
454
|
+
time: {
|
|
455
|
+
hour: number;
|
|
456
|
+
};
|
|
457
|
+
ip: string;
|
|
458
|
+
geo: {
|
|
459
|
+
country: string;
|
|
460
|
+
};
|
|
461
|
+
};
|
|
465
462
|
```
|
|
466
463
|
|
|
467
|
-
|
|
464
|
+
Environment is passed to `resolve()` and `enforce()` as the third argument:
|
|
468
465
|
|
|
469
466
|
```ts
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
"compareMethod": "and",
|
|
478
|
-
"ruleSet": [
|
|
479
|
-
{
|
|
480
|
-
"id": "9cc009e5-0aa9-453a-a668-cb3f418ced92",
|
|
481
|
-
"name": "Не администратор",
|
|
482
|
-
"compareMethod": "and",
|
|
483
|
-
"rules": [
|
|
484
|
-
{
|
|
485
|
-
"id": "4093cd50-e54f-4062-8053-2d3b5966fad3",
|
|
486
|
-
"name": "Нет роли администраторв",
|
|
487
|
-
"subject": "account.roles",
|
|
488
|
-
"resource": "administrator",
|
|
489
|
-
"condition": "<>"
|
|
490
|
-
}
|
|
491
|
-
]
|
|
492
|
-
}
|
|
493
|
-
]
|
|
494
|
-
});
|
|
467
|
+
await resolver.resolve('order.update', resource, environment);
|
|
468
|
+
await resolver.enforce('order.update', resource, environment);
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Using environment in rules
|
|
472
|
+
|
|
473
|
+
In a policy, you can refer to environment via the `env.*` path.
|
|
495
474
|
|
|
475
|
+
Example policy that denies order updates at night (10 PM – 6 AM):
|
|
476
|
+
|
|
477
|
+
```dsl
|
|
478
|
+
# @name Deny updates at night
|
|
479
|
+
deny permission.order.update if all:
|
|
480
|
+
env.time.hour less than 6
|
|
481
|
+
env.time.hour greater or equal than 22
|
|
496
482
|
```
|
|
497
483
|
|
|
498
|
-
|
|
484
|
+
**Retrieving values from environment**
|
|
485
|
+
|
|
486
|
+
If a path is specified in a rule:
|
|
487
|
+
|
|
488
|
+
- `env.*` → value is taken from environment
|
|
489
|
+
- `user.*`, `order.*`, `profile.*` → from resource
|
|
490
|
+
- literal (`18`, `"admin"`, `true`) → used as is
|
|
499
491
|
|
|
500
|
-
|
|
501
|
-
Этот метод вернёт экземпляр класса `AbilityMatch`, при помощи методов которого можно определить имеется ли совпадение
|
|
502
|
-
для группы и переданных значений.
|
|
492
|
+
Example:
|
|
503
493
|
|
|
504
494
|
```ts
|
|
505
|
-
|
|
495
|
+
subject: "env.geo.country"
|
|
496
|
+
resource: "user.country"
|
|
497
|
+
condition: "equal"
|
|
498
|
+
```
|
|
506
499
|
|
|
507
|
-
|
|
500
|
+
### Environment in TypeScript
|
|
508
501
|
|
|
509
|
-
|
|
502
|
+
The Environment type is set at the `AbilityResolver` level:
|
|
510
503
|
|
|
511
|
-
|
|
504
|
+
```ts
|
|
505
|
+
const resolver = new AbilityResolver<Resources, Environment>(policies);
|
|
512
506
|
```
|
|
513
507
|
|
|
514
|
-
|
|
508
|
+
This allows:
|
|
515
509
|
|
|
516
|
-
|
|
510
|
+
- getting autocompletion in IDE,
|
|
511
|
+
- checking the correctness of `env.*` paths,
|
|
512
|
+
- avoiding errors when passing environment.
|
|
517
513
|
|
|
518
|
-
|
|
514
|
+
> If a rule uses `env.*` but environment is not passed, then the value of `env.*` will be `undefined`, and the comparison will be performed as if the environment were absent.
|
|
519
515
|
|
|
520
|
-
|
|
521
|
-
2. **Оценка разрешений** — последовательно проверяет отобранные политики и возвращает итоговый результат (разрешено/запрещено)
|
|
516
|
+
## TypeScript Type Generator
|
|
522
517
|
|
|
523
|
-
|
|
518
|
+
`AbilityParser.generateTypeDefs()` generates TypeScript types based on policies, allowing you to avoid discrepancies between types and data in policies.
|
|
524
519
|
|
|
525
|
-
|
|
526
|
-
- `order.create` — правила создания заказа
|
|
527
|
-
- `order.update` — правила обновления заказа
|
|
528
|
-
- `order.delete` — правила удаления заказа
|
|
529
|
-
- `user.profile.update` — правила обновления профиля
|
|
530
|
-
- и так далее...
|
|
520
|
+
**Usage Example**
|
|
531
521
|
|
|
532
|
-
|
|
522
|
+
First, you need to prepare an array of policies. Policies can be stored in DSL or JSON and parsed into an array of ready-made policies. In this example, for clarity, policies are stored in DSL.
|
|
533
523
|
|
|
534
|
-
|
|
524
|
+
```ts
|
|
525
|
+
// scripts/policies.ts
|
|
526
|
+
|
|
527
|
+
import { AbilityDSLParser } from './AbilityDSLParser';
|
|
535
528
|
|
|
536
|
-
|
|
529
|
+
const dsl = `
|
|
530
|
+
# @name Update order
|
|
531
|
+
permit permission.order.update if all:
|
|
537
532
|
|
|
538
|
-
|
|
533
|
+
# @name Owner check
|
|
534
|
+
all of:
|
|
535
|
+
# @name User is owner
|
|
536
|
+
user.id = order.ownerId
|
|
537
|
+
`;
|
|
539
538
|
|
|
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` | ❌ Не совпадает |
|
|
539
|
+
const policies = new AbilityDSLParser(dsl).parse();
|
|
552
540
|
|
|
553
|
-
|
|
541
|
+
export default policies;
|
|
542
|
+
```
|
|
554
543
|
|
|
555
544
|
```ts
|
|
556
|
-
//
|
|
557
|
-
{
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
action: 'order.*', // Применится к order.create, order.update, order.delete и т.д.
|
|
561
|
-
effect: 'permit',
|
|
562
|
-
// ... правила
|
|
563
|
-
}
|
|
545
|
+
// scripts/generate-types.ts
|
|
546
|
+
import { writeFileSync } from 'node:fs';
|
|
547
|
+
import { AbilityParser } from '@via-profit/ability';
|
|
548
|
+
import policies from './policies.json';
|
|
564
549
|
|
|
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
|
-
}
|
|
550
|
+
const typedefs = AbilityParser.generateTypeDefs(policies);
|
|
573
551
|
|
|
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
|
-
}
|
|
552
|
+
writeFileSync('./src/ability/types.generated.ts', typedefs, 'utf8');
|
|
582
553
|
```
|
|
583
554
|
|
|
584
|
-
|
|
555
|
+
**Generated File (example)**
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
// src/ability/types.generated.ts
|
|
559
|
+
|
|
560
|
+
// Automatically generated by via-profit/ability
|
|
561
|
+
// Do not edit manually
|
|
562
|
+
export type Resources = {
|
|
563
|
+
'order.update': {
|
|
564
|
+
readonly user: {
|
|
565
|
+
readonly id: string;
|
|
566
|
+
};
|
|
567
|
+
readonly order: {
|
|
568
|
+
readonly ownerId: string;
|
|
569
|
+
};
|
|
570
|
+
};
|
|
571
|
+
};
|
|
572
|
+
```
|
|
585
573
|
|
|
586
|
-
|
|
574
|
+
**Usage in code**
|
|
587
575
|
|
|
588
576
|
```ts
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
action: 'order.*',
|
|
592
|
-
effect: 'permit',
|
|
593
|
-
// ... правила
|
|
594
|
-
}),
|
|
595
|
-
AbilityPolicy.parse({
|
|
596
|
-
action: 'order.update',
|
|
597
|
-
effect: 'deny',
|
|
598
|
-
// ... правила
|
|
599
|
-
})
|
|
600
|
-
];
|
|
577
|
+
import { AbilityResolver, AbilityPolicy } from '@via-profit/ability';
|
|
578
|
+
import type { Resources } from './ability/types.generated';
|
|
601
579
|
|
|
602
|
-
const resolver = new AbilityResolver(
|
|
580
|
+
const resolver = new AbilityResolver<Resources>(
|
|
581
|
+
AbilityPolicy.parseAll(policies),
|
|
582
|
+
);
|
|
603
583
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
// на принятые ранее решения вышестоящих политик
|
|
609
|
-
resolver.enforce('order.update', data);
|
|
584
|
+
await resolver.enforce('order.update', {
|
|
585
|
+
user: { id: 'u1' },
|
|
586
|
+
order: { ownerId: 'u1' },
|
|
587
|
+
});
|
|
610
588
|
```
|
|
611
589
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
590
|
+
## Policy Debugging
|
|
591
|
+
|
|
592
|
+
### Explanations
|
|
593
|
+
|
|
594
|
+
To simplify policy debugging, a special `AbilityResult` class is used, which is already included in the final evaluation result. `AbilityResult` encapsulates the outcome of applying all matching policies to a permission key and resource.
|
|
615
595
|
|
|
616
|
-
|
|
596
|
+
`AbilityResult` contains:
|
|
617
597
|
|
|
618
|
-
|
|
598
|
+
- a list of evaluated policies,
|
|
599
|
+
- methods to determine the final effect,
|
|
600
|
+
- methods to get explanations in textual representation.
|
|
601
|
+
|
|
602
|
+
Example:
|
|
619
603
|
|
|
620
604
|
```ts
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
];
|
|
605
|
+
const result = await resolver.resolve('order.update', resource);
|
|
606
|
+
|
|
607
|
+
if (result.isDenied()) {
|
|
608
|
+
console.log('Access denied');
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const explanations = result.explain(); // AbilityExplain
|
|
612
|
+
|
|
613
|
+
// console.log(explanations.toString());
|
|
643
614
|
```
|
|
644
615
|
|
|
645
|
-
###
|
|
616
|
+
### AbilityExplain
|
|
617
|
+
|
|
618
|
+
`AbilityExplain` and related classes (`AbilityExplainPolicy`, `AbilityExplainRuleSet`, `AbilityExplainRule`) allow you to get a human-readable explanation:
|
|
619
|
+
|
|
620
|
+
- which policy matched,
|
|
621
|
+
- which rule groups matched,
|
|
622
|
+
- which rules did not pass,
|
|
623
|
+
- which effect was applied.
|
|
624
|
+
|
|
625
|
+
Usage example:
|
|
646
626
|
|
|
647
627
|
```ts
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
];
|
|
628
|
+
const result = await resolver.resolve('order.update', resource);
|
|
629
|
+
const explanations = result.explain();
|
|
684
630
|
|
|
685
|
-
|
|
686
|
-
|
|
631
|
+
console.log(explanations.toString());
|
|
632
|
+
```
|
|
687
633
|
|
|
688
|
-
|
|
634
|
+
Example output:
|
|
635
|
+
|
|
636
|
+
```
|
|
637
|
+
✓ policy «Deny order update for managers» is match
|
|
638
|
+
✓ ruleSet «Managers» is match
|
|
639
|
+
✓ rule «Department managers» is match
|
|
640
|
+
✗ rule «Role manager» is mismatch
|
|
641
|
+
✓ ruleSet «Not administrators» is match
|
|
642
|
+
✓ rule «No role administrator» is match
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Output Format
|
|
646
|
+
|
|
647
|
+
Currently, only one output format is supported — textual.
|
|
648
|
+
|
|
649
|
+
The output follows the principle: `<policy | ruleSet | rule> <name> <is match | is mismatch>`
|
|
650
|
+
|
|
651
|
+
## Troubleshooting
|
|
652
|
+
|
|
653
|
+
### Decision‑Making Model (Default Deny)
|
|
654
|
+
|
|
655
|
+
> Why does a `deny` policy not turn into `permit` if its conditions are not met?
|
|
656
|
+
|
|
657
|
+
Consider a policy that **denies** access to a user aged 16:
|
|
658
|
+
|
|
659
|
+
```ts
|
|
660
|
+
const dsl = `
|
|
661
|
+
deny permission.test if all:
|
|
662
|
+
user.age is equals 16
|
|
663
|
+
`;
|
|
664
|
+
|
|
665
|
+
const policies = new AbilityDSLParser(dsl).parse();
|
|
689
666
|
const resolver = new AbilityResolver(policies);
|
|
690
667
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
resolver.enforce('order.create', {
|
|
694
|
-
user: {
|
|
695
|
-
department: 'managers',
|
|
696
|
-
roles: ['manager']
|
|
697
|
-
},
|
|
698
|
-
order: {
|
|
699
|
-
amount: 5000
|
|
700
|
-
}
|
|
668
|
+
const result = await resolver.resolve('test', {
|
|
669
|
+
user: { age: 16 },
|
|
701
670
|
});
|
|
671
|
+
|
|
672
|
+
console.log(result.isDenied()); // true ✔
|
|
673
|
+
console.log(result.isAllowed()); // false ✔
|
|
702
674
|
```
|
|
703
675
|
|
|
704
|
-
|
|
676
|
+
In this case, everything is obvious:
|
|
677
|
+
the condition is met → the policy matches → effect `deny` → access denied.
|
|
705
678
|
|
|
706
|
-
|
|
679
|
+
**What happens if the conditions are *not met*?**
|
|
707
680
|
|
|
708
681
|
```ts
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
682
|
+
const result = await resolver.resolve('test', {
|
|
683
|
+
user: { age: 12 },
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
console.log(result.isDenied()); // true ✔
|
|
687
|
+
console.log(result.isAllowed()); // false ✔
|
|
714
688
|
```
|
|
715
689
|
|
|
716
|
-
|
|
690
|
+
At first glance, it might seem that if the condition is not met, the policy should “allow” access.
|
|
691
|
+
But that is **not the case**.
|
|
717
692
|
|
|
718
|
-
|
|
693
|
+
**Decision‑Making Model: `Default Deny`**
|
|
719
694
|
|
|
720
|
-
|
|
695
|
+
`AbilityResolver` uses the classic security model:
|
|
721
696
|
|
|
722
|
-
|
|
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
|
-
```
|
|
697
|
+
> **If there is no matching permit‑policy → access is denied.**
|
|
736
698
|
|
|
737
|
-
|
|
699
|
+
**What happens in this example:**
|
|
738
700
|
|
|
739
|
-
|
|
701
|
+
1. The `deny` policy exists, but its condition is **not met**
|
|
702
|
+
→ the policy gets status `mismatch`.
|
|
740
703
|
|
|
741
|
-
|
|
742
|
-
const result = resolver
|
|
743
|
-
.resolve('order.update', data)
|
|
744
|
-
.isDeny(); // true если доступ запрещен
|
|
704
|
+
2. The `deny` policy **is not applied** because the conditions did not match.
|
|
745
705
|
|
|
746
|
-
|
|
747
|
-
// Самостоятельно обрабатываем запрет
|
|
748
|
-
return { error: 'Access denied' };
|
|
749
|
-
}
|
|
706
|
+
3. There is no `permit` policy.
|
|
750
707
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
```
|
|
708
|
+
4. Since there is no permit policy → the final decision:
|
|
709
|
+
**deny (by default)**.
|
|
754
710
|
|
|
755
|
-
|
|
711
|
+
**Summary**
|
|
756
712
|
|
|
757
|
-
|
|
758
|
-
|
|
713
|
+
- `deny` with matching conditions → **deny**
|
|
714
|
+
- `deny` with non‑matching conditions → **deny (default deny)**
|
|
715
|
+
- `permit` with matching conditions → **allow**
|
|
716
|
+
- `permit` with non‑matching conditions → **deny (default deny)**
|
|
759
717
|
|
|
760
|
-
|
|
761
|
-
console.log(explain.toString());
|
|
762
|
-
// ✓ policy «Политика обновления заказа» is match
|
|
763
|
-
// ✗ ruleSet «Проверка владельца» is mismatch
|
|
764
|
-
// ✓ ruleSet «Проверка статуса» is match
|
|
765
|
-
});
|
|
718
|
+
**Conclusion**
|
|
766
719
|
|
|
767
|
-
if
|
|
768
|
-
console.log('Доступ запрещен по причине:');
|
|
769
|
-
explanations.forEach(e => console.log(e.toString()));
|
|
770
|
-
}
|
|
771
|
-
```
|
|
720
|
+
**Access is allowed only if there is an explicit permit.**
|
|
772
721
|
|
|
773
|
-
|
|
722
|
+
## Design Recommendations
|
|
774
723
|
|
|
775
|
-
|
|
724
|
+
### Naming Access Keys
|
|
776
725
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
726
|
+
- Use hierarchical keys: `permission.order.create`, `permission.order.update.status`, `permission.user.profile.update`.
|
|
727
|
+
- Group by domains: `permission.user.*`, `permission.order.*`, `permission.product.*`.
|
|
728
|
+
- Do not mix different domains in one key.
|
|
729
|
+
|
|
730
|
+
### Data Structure
|
|
731
|
+
|
|
732
|
+
- Explicitly describe `Resources` in TypeScript.
|
|
733
|
+
- Do not pass “extra” fields — this complicates understanding.
|
|
734
|
+
- Strive to keep the data structure for a given `permission` stable.
|
|
735
|
+
|
|
736
|
+
### Policy Design
|
|
737
|
+
|
|
738
|
+
- General rules — via wildcard (`permission.order.*`).
|
|
739
|
+
- Specific restrictions — via exact actions (`permission.order.update`).
|
|
740
|
+
- Use `effect: deny` for prohibitions.
|
|
741
|
+
- Use `effect: permit` for permissions.
|
|
742
|
+
|
|
743
|
+
### Common Mistakes
|
|
744
|
+
|
|
745
|
+
- Expecting that absence of matching policies means allow.
|
|
746
|
+
- Mixing business logic and access policies.
|
|
747
|
+
- Too large policies with dozens of rules — better to break them down.
|
|
748
|
+
|
|
749
|
+
### Example of Use on the Frontend (React)
|
|
750
|
+
|
|
751
|
+
**Hook for checking policies**
|
|
752
|
+
|
|
753
|
+
```tsx
|
|
754
|
+
// hooks/use-ability.ts
|
|
755
|
+
import { useEffect, useState } from 'react';
|
|
756
|
+
import { AbilityResolver } from '@via-profit/ability';
|
|
757
|
+
import { Resources } from './generated-types';
|
|
758
|
+
|
|
759
|
+
export function useAbility<Permission extends keyof Resources>(
|
|
760
|
+
resolver: AbilityResolver<Resources>,
|
|
761
|
+
permission: Permission,
|
|
762
|
+
resource: Resources[Permission],
|
|
763
|
+
) {
|
|
764
|
+
const [allowed, setAllowed] = useState<boolean | null>(null);
|
|
765
|
+
|
|
766
|
+
useEffect(() => {
|
|
767
|
+
let cancelled = false;
|
|
768
|
+
|
|
769
|
+
async function check() {
|
|
770
|
+
try {
|
|
771
|
+
const result = await resolver.resolve(permission, resource);
|
|
772
|
+
if (!cancelled) {
|
|
773
|
+
setAllowed(result.isAllowed());
|
|
774
|
+
}
|
|
775
|
+
} catch {
|
|
776
|
+
if (!cancelled) {
|
|
777
|
+
setAllowed(false);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
check();
|
|
783
|
+
|
|
784
|
+
return () => {
|
|
785
|
+
cancelled = true;
|
|
807
786
|
};
|
|
808
|
-
};
|
|
809
|
-
};
|
|
787
|
+
}, [resolver, permission, resource]);
|
|
810
788
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
789
|
+
return allowed;
|
|
790
|
+
}
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
**Usage in a component**
|
|
794
|
+
|
|
795
|
+
```tsx
|
|
796
|
+
function OrderUpdateButton({ order, user }) {
|
|
797
|
+
const allowed = useAbility(resolver, 'order.update', {
|
|
798
|
+
user,
|
|
799
|
+
order,
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
if (allowed === null) {
|
|
803
|
+
return null; // or loading spinner
|
|
819
804
|
}
|
|
820
|
-
});
|
|
821
805
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
user: {
|
|
825
|
-
department: 'managers'
|
|
826
|
-
// missing roles
|
|
806
|
+
if (!allowed) {
|
|
807
|
+
return null;
|
|
827
808
|
}
|
|
828
|
-
|
|
809
|
+
|
|
810
|
+
return <button>Update order</button>;
|
|
811
|
+
}
|
|
829
812
|
```
|
|
830
813
|
|
|
831
|
-
|
|
814
|
+
## Examples
|
|
832
815
|
|
|
833
|
-
|
|
834
|
-
2. Последовательно проверяет каждую политику методом `check()`
|
|
835
|
-
3. Если политика вернула `match`, запоминает её `effect` (permit/deny)
|
|
836
|
-
4. Возвращается `effect` **последней сработавшей политики**
|
|
816
|
+
### Example of a Complex Multi‑Level Policy
|
|
837
817
|
|
|
838
|
-
|
|
839
|
-
// Пример с несколькими политиками на одно действие
|
|
840
|
-
const policies = [
|
|
841
|
-
permitPolicy, // permit, но не match
|
|
842
|
-
denyPolicy, // deny и match
|
|
843
|
-
permitPolicy2 // permit, но не match
|
|
844
|
-
];
|
|
818
|
+
Below is a multi‑level set of policies, using a cinema example (fictional).
|
|
845
819
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
820
|
+
**The example demonstrates:**
|
|
821
|
+
- working with roles (admin, seller, manager, VIP, banned),
|
|
822
|
+
- time constraints (`env.time.hour`),
|
|
823
|
+
- wildcard permissions (`permission.*`),
|
|
824
|
+
- ticket quantity limits,
|
|
825
|
+
- prohibition on selling already sold tickets,
|
|
826
|
+
- combination of `permit`/`deny` policies,
|
|
827
|
+
- policy priority and Default Deny model.
|
|
849
828
|
|
|
850
|
-
|
|
829
|
+
**Brief description of rules**
|
|
830
|
+
- **Administrator**
|
|
831
|
+
Has wildcard permissions (`permission.*`) and can perform any action.
|
|
832
|
+
Can edit ticket prices.
|
|
851
833
|
|
|
852
|
-
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
-
|
|
834
|
+
- **Seller**
|
|
835
|
+
Can sell tickets only during working hours (09:00–23:00).
|
|
836
|
+
Cannot sell tickets if:
|
|
837
|
+
- the cinema is closed,
|
|
838
|
+
- the ticket is already sold.
|
|
856
839
|
|
|
857
|
-
|
|
840
|
+
- **Manager**
|
|
841
|
+
Has the same rights as a seller.
|
|
858
842
|
|
|
859
|
-
|
|
843
|
+
- **Buyers**
|
|
844
|
+
- A user older than 21 can buy tickets.
|
|
845
|
+
- A VIP user can buy tickets at any time.
|
|
846
|
+
- A banned user (`status = banned`) cannot buy tickets.
|
|
847
|
+
- Any user cannot buy more than 6 tickets.
|
|
860
848
|
|
|
861
|
-
|
|
849
|
+
**Policy Diagram**
|
|
862
850
|
|
|
863
|
-
|
|
851
|
+
```mermaid
|
|
852
|
+
flowchart LR
|
|
864
853
|
|
|
865
|
-
|
|
866
|
-
|-------|-----------|----------------------|----------|
|
|
867
|
-
| `isEqual()` | `compareWith: AbilityCode<T> \| null` | `boolean` | Сравнивает текущий код с другим экземпляром |
|
|
868
|
-
| `isNotEqual()` | `compareWith: AbilityCode<T> \| null` | `boolean` | Проверяет неравенство с другим экземпляром |
|
|
854
|
+
%% ==== ROLES ====
|
|
869
855
|
|
|
870
|
-
|
|
871
|
-
|
|
856
|
+
subgraph Roles[Roles]
|
|
857
|
+
A[Administrator]
|
|
858
|
+
B[Seller]
|
|
859
|
+
C[Manager]
|
|
860
|
+
end
|
|
872
861
|
|
|
873
|
-
|
|
862
|
+
subgraph Buyers[Buyers]
|
|
863
|
+
U1[User > 21]
|
|
864
|
+
U2[VIP user]
|
|
865
|
+
U3[Banned user]
|
|
866
|
+
end
|
|
874
867
|
|
|
875
|
-
|
|
868
|
+
%% ==== ADMIN ====
|
|
876
869
|
|
|
877
|
-
|
|
870
|
+
A --> A1[Wildcard: permission.*]
|
|
871
|
+
A --> A2[Edit ticket price]
|
|
878
872
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
- `AbilityMatch.match` - совпадение найдено
|
|
882
|
-
- `AbilityMatch.mismatch` - совпадение не найдено
|
|
873
|
+
A1 --> FINAL[Final decision]
|
|
874
|
+
A2 --> FINAL
|
|
883
875
|
|
|
884
|
-
|
|
876
|
+
%% ==== SELLER ====
|
|
885
877
|
|
|
886
|
-
|
|
878
|
+
B --> B1[Sell tickets]
|
|
887
879
|
|
|
888
|
-
|
|
880
|
+
B1 -->|09:00–23:00| B2[Allowed]
|
|
881
|
+
B1 -->|Outside hours| D2[Denied]
|
|
882
|
+
B1 -->|ticket.status = sold| D3[Denied]
|
|
889
883
|
|
|
890
|
-
|
|
884
|
+
B2 --> FINAL
|
|
885
|
+
D2 --> FINAL
|
|
886
|
+
D3 --> FINAL
|
|
891
887
|
|
|
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`)
|
|
888
|
+
%% ==== MANAGER ====
|
|
901
889
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
| `fromLiteral()` | `literal: AbilityConditionLiteralType` | `AbilityCondition` | Создает экземпляр условия из литерального имени (например, 'equal' → '=') |
|
|
890
|
+
C --> C1[Sell tickets as seller]
|
|
891
|
+
C1 --> FINAL
|
|
905
892
|
|
|
906
|
-
|
|
907
|
-
- `literal: AbilityConditionLiteralType` - возвращает литеральное имя оператора ('equal', 'not_equal' и т.д.)
|
|
893
|
+
%% ==== BUYERS ====
|
|
908
894
|
|
|
909
|
-
|
|
895
|
+
U1 --> U1A[Buy tickets]
|
|
896
|
+
U1A -->|ticketsCount < 6| U1OK[Allowed]
|
|
897
|
+
U1A -->|ticketsCount ≥ 6| U1DENY[Denied]
|
|
910
898
|
|
|
911
|
-
|
|
899
|
+
U2 --> U2A[Buy tickets anytime]
|
|
900
|
+
U2A -->|ticketsCount < 6| U2OK[Allowed]
|
|
901
|
+
U2A -->|ticketsCount ≥ 6| U2DENY[Denied]
|
|
912
902
|
|
|
913
|
-
|
|
903
|
+
U3 --> U3A[Denied to buy tickets]
|
|
914
904
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
905
|
+
U1OK --> FINAL
|
|
906
|
+
U1DENY --> FINAL
|
|
907
|
+
U2OK --> FINAL
|
|
908
|
+
U2DENY --> FINAL
|
|
909
|
+
U3A --> FINAL
|
|
918
910
|
|
|
919
|
-
|
|
911
|
+
%% ==== DENY RULES ====
|
|
920
912
|
|
|
921
|
-
|
|
913
|
+
D1[Denied to buy tickets if user.status = banned] --> FINAL
|
|
914
|
+
```
|
|
922
915
|
|
|
923
|
-
|
|
916
|
+
**DSL Policies**
|
|
924
917
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
918
|
+
```dsl
|
|
919
|
+
############################################################
|
|
920
|
+
# @name Admin can edit ticket price
|
|
921
|
+
permit permission.ticket.price.edit if all:
|
|
922
|
+
user.role is equals 'admin'
|
|
928
923
|
|
|
929
|
-
---
|
|
930
924
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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` | Создает правило с условием "больше или равно" |
|
|
925
|
+
############################################################
|
|
926
|
+
# @name Seller can sell tickets during working hours
|
|
927
|
+
permit permission.ticket.sell if all:
|
|
928
|
+
user.role is equals 'seller'
|
|
929
|
+
all of:
|
|
930
|
+
env.time.hour greater than or equal 9
|
|
931
|
+
env.time.hour less than or equal 23
|
|
963
932
|
|
|
964
|
-
---
|
|
965
933
|
|
|
966
|
-
|
|
934
|
+
############################################################
|
|
935
|
+
# @name Users older than 21 can buy tickets
|
|
936
|
+
permit permission.ticket.buy if all:
|
|
937
|
+
user.age greater than 21
|
|
967
938
|
|
|
968
|
-
Группирует несколько правил для совместной проверки.
|
|
969
939
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
- `name: string` - название группы
|
|
975
|
-
- `id: string` - уникальный идентификатор
|
|
940
|
+
############################################################
|
|
941
|
+
# @name VIP users can buy tickets anytime
|
|
942
|
+
permit permission.ticket.buy if all:
|
|
943
|
+
user.isVIP is true
|
|
976
944
|
|
|
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
945
|
|
|
987
|
-
|
|
946
|
+
############################################################
|
|
947
|
+
# @name Deny buying tickets if user is banned
|
|
948
|
+
deny permission.ticket.buy if all:
|
|
949
|
+
user.status is equals 'banned'
|
|
988
950
|
|
|
989
|
-
### Класс AbilityPolicy
|
|
990
951
|
|
|
991
|
-
|
|
952
|
+
############################################################
|
|
953
|
+
# @name Deny selling tickets if cinema is closed
|
|
954
|
+
deny permission.ticket.sell if all:
|
|
955
|
+
any of:
|
|
956
|
+
env.time.hour less than 9
|
|
957
|
+
env.time.hour greater than 23
|
|
992
958
|
|
|
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
959
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
| `explain()` | — | `AbilityExplain` | Возвращает объяснение результата проверки (должен быть вызван после `check()`) |
|
|
1007
|
-
| `export()` | — | `AbilityPolicyConfig` | Экспортирует политику в конфигурационный объект |
|
|
1008
|
-
| `parse()` | `config: AbilityPolicyConfig` | `AbilityPolicy` | Статический метод. Создает экземпляр политики из конфигурации |
|
|
1009
|
-
| `parseAll()` | `configs: readonly AbilityPolicyConfig[]` | `AbilityPolicy[]` | Статический метод. Парсит массив конфигураций политик и возвращает массив экземпляров AbilityPolicy. Удобен для загрузки нескольких политик одновременно. |
|
|
960
|
+
############################################################
|
|
961
|
+
# @name Manager can do everything seller can
|
|
962
|
+
permit permission.ticket.sell if all:
|
|
963
|
+
user.role is equals 'manager'
|
|
1010
964
|
|
|
1011
|
-
---
|
|
1012
965
|
|
|
1013
|
-
|
|
966
|
+
############################################################
|
|
967
|
+
# @name Admin wildcard permissions
|
|
968
|
+
permit permission.* if all:
|
|
969
|
+
user.role is equals 'admin'
|
|
1014
970
|
|
|
1015
|
-
Управляет множеством политик и их выполнением.
|
|
1016
971
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
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` | Статический метод. Проверяет, соответствует ли действие шаблону (поддерживает `*`) |
|
|
972
|
+
############################################################
|
|
973
|
+
# @name Limit tickets per user (max 6)
|
|
974
|
+
deny permission.ticket.buy if all:
|
|
975
|
+
user.ticketsCount greater than or equal 6
|
|
1028
976
|
|
|
1029
|
-
---
|
|
1030
977
|
|
|
1031
|
-
|
|
978
|
+
############################################################
|
|
979
|
+
# @name Cannot sell already sold tickets
|
|
980
|
+
deny permission.ticket.sell if all:
|
|
981
|
+
ticket.status is equals 'sold'
|
|
982
|
+
```
|
|
1032
983
|
|
|
1033
|
-
|
|
984
|
+
Below is how to use the policies above in Node.js + TypeScript.
|
|
1034
985
|
|
|
1035
|
-
|
|
1036
|
-
- `type: AbilityExplainType` - тип элемента ('policy' \| 'rule' \| 'ruleSet')
|
|
1037
|
-
- `children: AbilityExplain[]` - дочерние элементы объяснения
|
|
1038
|
-
- `name: string` - название элемента
|
|
1039
|
-
- `match: AbilityMatch` - результат проверки
|
|
986
|
+
**Preparing Policies**
|
|
1040
987
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
988
|
+
```ts
|
|
989
|
+
import { AbilityDSLParser } from '@via-profit/ability';
|
|
990
|
+
import cinemaDSL from './policies/cinema.dsl';
|
|
1044
991
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
- `AbilityExplainRuleSet` - объяснение для группы правил
|
|
1048
|
-
- `AbilityExplainPolicy` - объяснение для политики
|
|
992
|
+
export const policies = new AbilityDSLParser(cinemaDSL).parse();
|
|
993
|
+
```
|
|
1049
994
|
|
|
1050
|
-
|
|
995
|
+
**Creating the Resolver**
|
|
1051
996
|
|
|
1052
|
-
|
|
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
|
-
// }
|
|
997
|
+
```ts
|
|
998
|
+
import { AbilityResolver } from '@via-profit/ability';
|
|
999
|
+
import { policies } from './policies';
|
|
1000
|
+
|
|
1001
|
+
const resolver = new AbilityResolver(policies);
|
|
1116
1002
|
```
|
|
1117
1003
|
|
|
1118
|
-
|
|
1004
|
+
**Checking Permissions (enforce)**
|
|
1119
1005
|
|
|
1120
|
-
|
|
1006
|
+
Example: buying a ticket.
|
|
1121
1007
|
|
|
1122
|
-
|
|
1123
|
-
|-------|------------|
|
|
1124
|
-
| `AbilityError` | Общая ошибка доступа, выбрасывается при запрете в `enforce()` |
|
|
1125
|
-
| `AbilityParserError` | Ошибка парсинга конфигурации или генерации типов |
|
|
1008
|
+
The `enforce` method throws an `AbilityError` if access is denied.
|
|
1126
1009
|
|
|
1127
|
-
|
|
1010
|
+
```ts
|
|
1011
|
+
await resolver.enforce('ticket.buy', {
|
|
1012
|
+
user: { age: 25, ticketsCount: 1 },
|
|
1013
|
+
env: { time: { hour: 18 } },
|
|
1014
|
+
});
|
|
1015
|
+
```
|
|
1016
|
+
If allowed — the code continues execution.
|
|
1017
|
+
If denied — an `AbilityError` exception is thrown.
|
|
1128
1018
|
|
|
1129
|
-
|
|
1019
|
+
**Checking Permissions Without Exceptions (resolve)**
|
|
1130
1020
|
|
|
1131
|
-
|
|
1021
|
+
`resolve` returns a result object:
|
|
1132
1022
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1023
|
+
```ts
|
|
1024
|
+
const result = await resolver.resolve('ticket.buy', {
|
|
1025
|
+
user: { age: 25, ticketsCount: 1 },
|
|
1026
|
+
env: { time: { hour: 18 } },
|
|
1027
|
+
});
|
|
1136
1028
|
|
|
1137
|
-
|
|
1029
|
+
if (result.isAllowed()) {
|
|
1030
|
+
console.log('Purchase allowed');
|
|
1031
|
+
} else {
|
|
1032
|
+
console.log('Purchase denied');
|
|
1033
|
+
}
|
|
1034
|
+
```
|
|
1138
1035
|
|
|
1139
|
-
|
|
1036
|
+
**Seller can only sell during working hours**
|
|
1140
1037
|
|
|
1141
1038
|
```ts
|
|
1142
|
-
|
|
1143
|
-
{
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
}
|
|
1039
|
+
await resolver.enforce('ticket.sell', {
|
|
1040
|
+
user: { role: 'seller' },
|
|
1041
|
+
env: { time: { hour: 15 } },
|
|
1042
|
+
ticket: { status: 'available' },
|
|
1043
|
+
});
|
|
1044
|
+
```
|
|
1147
1045
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
}
|
|
1046
|
+
**Preparing Data for the Resolver**
|
|
1047
|
+
|
|
1048
|
+
In the examples above, constant objects are passed to the resolver:
|
|
1049
|
+
|
|
1050
|
+
```ts
|
|
1051
|
+
resolver.enforce('ticket.buy', {
|
|
1052
|
+
user: { age: 25 },
|
|
1053
|
+
env: { time: { hour: 18 } },
|
|
1054
|
+
});
|
|
1155
1055
|
```
|
|
1156
1056
|
|
|
1157
|
-
|
|
1057
|
+
This is done for clarity. In a real application, the data for the resolver should be built dynamically — from the sources available to your server.
|
|
1058
|
+
|
|
1059
|
+
**User** (`user`) is usually taken from:
|
|
1060
|
+
|
|
1061
|
+
- JWT token
|
|
1062
|
+
- session
|
|
1063
|
+
- database
|
|
1064
|
+
- authorization middleware
|
|
1065
|
+
|
|
1066
|
+
Example:
|
|
1067
|
+
|
|
1068
|
+
```ts
|
|
1069
|
+
const user = await db.users.findById(session.userId);
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
**Environment** (`env`)
|
|
1073
|
+
|
|
1074
|
+
These are any external parameters that can affect access:
|
|
1075
|
+
|
|
1076
|
+
- current server time
|
|
1077
|
+
- time zone
|
|
1078
|
+
- IP address
|
|
1079
|
+
- request headers
|
|
1080
|
+
- system configuration
|
|
1158
1081
|
|
|
1159
|
-
|
|
1160
|
-
- Комбинируйте простые правила для сложной логики
|
|
1161
|
-
- Давайте понятные имена правилам и политикам для упрощения отладки
|
|
1082
|
+
Example:
|
|
1162
1083
|
|
|
1163
|
-
|
|
1084
|
+
```ts
|
|
1085
|
+
const env = {
|
|
1086
|
+
time: {
|
|
1087
|
+
hour: new Date().getHours(),
|
|
1088
|
+
},
|
|
1089
|
+
ip: req.ip,
|
|
1090
|
+
};
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
**Resource** (e.g., `ticket`)
|
|
1164
1094
|
|
|
1165
|
-
|
|
1095
|
+
If the action is associated with a specific object, it also needs to be loaded:
|
|
1166
1096
|
|
|
1167
1097
|
```ts
|
|
1168
|
-
const
|
|
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
|
|
1098
|
+
const ticket = await db.tickets.findById(req.params.ticketId);
|
|
1176
1099
|
```
|
|
1177
1100
|
|
|
1101
|
+
**Context**
|
|
1102
|
+
|
|
1103
|
+
Context is the object that you pass to `resolve` or `enforce`.
|
|
1104
|
+
It contains **all the data** that policies might need:
|
|
1105
|
+
|
|
1106
|
+
- `user` — data about the current user
|
|
1107
|
+
- `env` — environment data (time, IP, geography, system settings)
|
|
1108
|
+
- `resource` or `ticket` — data about the entity on which the action is performed
|
|
1109
|
+
- any other objects that you use in DSL
|
|
1110
|
+
|
|
1111
|
+
**It is important to understand:**
|
|
1112
|
+
|
|
1113
|
+
> Context is formed for a specific action and specific policies. It does not need to be stored in advance — you gather it dynamically before calling the resolver.
|
|
1114
|
+
|
|
1115
|
+
## Performance
|
|
1116
|
+
|
|
1117
|
+
The tests used policies with 10 conditions, nested fields, and environment.
|
|
1118
|
+
|
|
1119
|
+
**Tinybench** ([https://github.com/tinylibs/tinybench](https://github.com/tinylibs/tinybench))
|
|
1120
|
+
|
|
1121
|
+
| # | Task name | Latency avg (ns) | Latency med (ns) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples |
|
|
1122
|
+
|---|-----------------------------------------|------------------------|------------------------|--------------------------|--------------------------|---------|
|
|
1123
|
+
| 0 | resolve() — no cache (heavy rules) | 646317 ± 0.32% | 632319 ± 8446.0 | 1555 ± 0.21% | 1581 ± 21 | 3095 |
|
|
1124
|
+
| 1 | resolve() — cold cache (heavy rules) | 636363 ± 0.38% | 623092 ± 7885.0 | 1581 ± 0.21% | 1605 ± 20 | 3143 |
|
|
1125
|
+
| 2 | resolve() — warm cache (heavy rules) | 631328 ± 0.26% | 621152 ± 6562.5 | 1590 ± 0.17% | 1610 ± 17 | 3168 |
|
|
1126
|
+
|
|
1127
|
+
```
|
|
1128
|
+
Latency (ns)
|
|
1129
|
+
650k | ███████████████████████████████████████ resolve() — no cache
|
|
1130
|
+
640k | █████████████████████████████████████ resolve() — cold cache
|
|
1131
|
+
630k | ████████████████████████████████████ resolve() — warm cache
|
|
1132
|
+
--------------------------------------------------------------
|
|
1133
|
+
no cache cold cache warm cache
|
|
1134
|
+
```
|
|
1135
|
+
|
|
1136
|
+
```
|
|
1137
|
+
Throughput (ops/s)
|
|
1138
|
+
1600 | ███████████████████████████████████████ resolve() — warm cache
|
|
1139
|
+
1590 | ██████████████████████████████████████ resolve() — cold cache
|
|
1140
|
+
1580 | █████████████████████████████████████ resolve() — no cache
|
|
1141
|
+
--------------------------------------------------------------
|
|
1142
|
+
no cache cold cache warm cache
|
|
1143
|
+
```
|
|
1178
1144
|
|
|
1179
|
-
##
|
|
1145
|
+
## License
|
|
1180
1146
|
|
|
1181
|
-
|
|
1147
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|