@via-profit/ability 3.7.1 → 3.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -362
- package/dist/index.cjs +1 -3435
- package/dist/index.js +1 -3399
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
# @via-profit/
|
|
1
|
+
# @via-profit/ability
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
> Allows you to describe rules, combine them into groups, form policies, and apply them to data to determine permissions.
|
|
3
|
+
> Легкий ABAC-движок с DSL, TypeScript-типами и нулевыми зависимостями
|
|
5
4
|
|
|
6
5
|

|
|
7
6
|

|
|
@@ -11,388 +10,65 @@
|
|
|
11
10
|

|
|
12
11
|

|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
> ### 🌐 Language Selection / Выбор языка
|
|
14
|
+
>
|
|
15
|
+
> * 🇷🇺 **[Документация на русском](./docs/ru/README.md)**
|
|
16
|
+
> * 🇬🇧 **English docs**
|
|
17
|
+
>
|
|
18
|
+
> **English documentation? Error 404: Not Found.**
|
|
19
|
+
> Since I’ve spent years learning English just to read your code, here is your perfect chance to practice your
|
|
20
|
+
Russian! Alternatively, Google Translate is your best friend here.
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
- [🇷🇺 Русский](/docs/ru/README.md)
|
|
22
|
+
## Для чего
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
Проект задумывался для того, чтобы закрыть типовые сценарии контроля доступа без лишних сложностей. Нам потребовался
|
|
25
|
+
лёгкий ABAC-движок, работающий и на сервере и в браузере, с простым DSL, автоматической генерацией TypeScript-типов — и
|
|
26
|
+
без внешних зависимостей.
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
## Основные возможности
|
|
22
29
|
|
|
30
|
+
- Простой DSL
|
|
31
|
+
- 9 встроенных стратегий разрешения
|
|
32
|
+
- TypeScript-first с генерацией типов
|
|
33
|
+
- Ноль зависимостей
|
|
34
|
+
- Explain для отладки
|
|
35
|
+
- Работает и на сервере и в браузере
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
- [Installation](#installation)
|
|
26
|
-
- [Quick Start](#quick-start)
|
|
27
|
-
- [Core Concepts](#core-concepts)
|
|
28
|
-
- [DSL](./dsl.md)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
## Key Features
|
|
32
|
-
|
|
33
|
-
1. Simple and expressive DSL — rules read like natural language.
|
|
34
|
-
2. Support for grouping rules with `all of:` / `any of:`.
|
|
35
|
-
3. `except` operator for describing exceptions within a policy.
|
|
36
|
-
4. 9 built-in strategies — DenyOverrides, PermitOverrides, FirstMatch, Priority, and others. Custom strategies can be added.
|
|
37
|
-
5. Cross-platform — works in Node.js and browsers.
|
|
38
|
-
6. TypeScript-first — automatic type generation for resources from policies.
|
|
39
|
-
7. Zero dependencies — lightweight and no external libraries.
|
|
40
|
-
8. Built-in explain — decision tree with results of each rule for debugging.
|
|
41
|
-
9. Serialization — export and import policies to/from JSON.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
## Installation
|
|
37
|
+
## Установка
|
|
45
38
|
|
|
46
39
|
```bash
|
|
47
40
|
npm install @via-profit/ability
|
|
48
41
|
```
|
|
49
42
|
|
|
50
|
-
##
|
|
43
|
+
## Быстрый старт
|
|
51
44
|
|
|
52
|
-
```
|
|
53
|
-
import {
|
|
54
|
-
AbilityResolver,
|
|
55
|
-
DenyOverridesStrategy,
|
|
56
|
-
ability
|
|
57
|
-
} from '@via-profit/ability';
|
|
45
|
+
```ts
|
|
46
|
+
import { ability, AbilityResolver, DenyOverridesStrategy } from '@via-profit/ability';
|
|
58
47
|
|
|
48
|
+
// Создание политик, в частности, одной политики document.read
|
|
59
49
|
const policies = ability`
|
|
60
|
-
@name "
|
|
50
|
+
@name "Разрешить чтение документа только авторам или если он опубликован"
|
|
61
51
|
permit permission.document.read if all:
|
|
62
52
|
|
|
63
|
-
@name "
|
|
64
|
-
document.
|
|
53
|
+
@name "Пользователь является автором"
|
|
54
|
+
document.author is equals user.id
|
|
65
55
|
|
|
66
|
-
@name "
|
|
56
|
+
@name "Документ опубликован"
|
|
67
57
|
document.status in ["published", "archived"]
|
|
68
|
-
|
|
69
|
-
@name "Today must be Saturday"
|
|
70
|
-
env.today.dayName is 'Saturday'
|
|
71
58
|
`;
|
|
72
59
|
|
|
60
|
+
// Создание резолвера
|
|
73
61
|
const resolver = new AbilityResolver(policies, DenyOverridesStrategy);
|
|
74
62
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
dayName: new Date().toLocaleDateString('en-US', { weekday: 'long' }),
|
|
78
|
-
},
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Check the permission
|
|
82
|
-
const result = resolver.resolve('document.read', {
|
|
83
|
-
document: {
|
|
84
|
-
ownerId: 123,
|
|
85
|
-
status: 'published',
|
|
86
|
-
},
|
|
87
|
-
}, environment);
|
|
88
|
-
|
|
89
|
-
console.log(result.isAllowed()); // true
|
|
90
|
-
|
|
91
|
-
// Detailed results
|
|
92
|
-
console.log(result.explain());
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Core Concepts
|
|
96
|
-
|
|
97
|
-
| Component | Description |
|
|
98
|
-
|----------------|-----------------------------------------------------------------------------|
|
|
99
|
-
| `AbilityPolicy` | A policy – has an `effect` (permit/deny), a `permission`, and a set of rules. |
|
|
100
|
-
| `AbilityRuleSet`| A group of rules combined with `all` (AND) or `any` (OR) operator. |
|
|
101
|
-
| `AbilityRule` | An elementary rule: `subject` `operator` `resource`. |
|
|
102
|
-
| `AbilityCondition` | Comparison operator: `=`, `<>`, `>`, `contains`, `in`, `length >`, etc. |
|
|
103
|
-
| `AbilityMatch` | Check state: `match`, `mismatch`, `pending`, `except-mismatch`. |
|
|
104
|
-
| `AbilityStrategy` | Algorithm for selecting the final effect from multiple matched policies. |
|
|
105
|
-
|
|
106
|
-
## DSL
|
|
107
|
-
|
|
108
|
-
Ability DSL is a declarative language for describing access policies.
|
|
109
|
-
It allows you to describe rules in a human-readable form and then use them at runtime to make decisions.
|
|
110
|
-
|
|
111
|
-
Ability supports two ways to create policies from DSL:
|
|
112
|
-
|
|
113
|
-
1. **Using DSL literal**
|
|
114
|
-
2. **Using a regular string + AbilityDSLParser**
|
|
115
|
-
|
|
116
|
-
### Creating Policies via DSL Literal
|
|
117
|
-
|
|
118
|
-
```ts
|
|
119
|
-
import { ability } from '@via-profit/ability';
|
|
120
|
-
|
|
121
|
-
const policies = ability`
|
|
122
|
-
permit permission.document.read if all:
|
|
123
|
-
document.ownerId equals user.id
|
|
124
|
-
document.status in ["published", "archived"]
|
|
125
|
-
`;
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
- the string inside `ability``…`` is parsed by AbilityDSLParser
|
|
129
|
-
- returns an array of policies (`AbilityPolicy[]`)
|
|
130
|
-
|
|
131
|
-
### Creating Policies via Regular String
|
|
132
|
-
|
|
133
|
-
If the literal is not available (e.g., in a dynamic environment):
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
import { AbilityDSLParser } from '@via-profit/ability';
|
|
137
|
-
|
|
138
|
-
const dsl = `
|
|
139
|
-
permit permission.document.read if all:
|
|
140
|
-
document.ownerId equals user.id
|
|
141
|
-
document.status in ["published", "archived"]
|
|
142
|
-
`;
|
|
143
|
-
|
|
144
|
-
const policies = new AbilityDSLParser(dsl).parse();
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
Both methods produce the same result.
|
|
148
|
-
|
|
149
|
-
### Typing DSL with Generics
|
|
150
|
-
|
|
151
|
-
The DSL literal can accept types:
|
|
152
|
-
|
|
153
|
-
```ts
|
|
154
|
-
const policies = ability<Resources, Environment, PolicyTags>`
|
|
155
|
-
permit permission.document.read if all:
|
|
156
|
-
document.ownerId equals user.id
|
|
157
|
-
document.status in ["published", "archived"]
|
|
158
|
-
`;
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
### What These Types Provide:
|
|
162
|
-
|
|
163
|
-
| Type | Description |
|
|
164
|
-
|---------------|--------------------------------------------------------------------|
|
|
165
|
-
| `Resources` | Type of resources available in DSL (`document.*`, `order.*`, etc.) |
|
|
166
|
-
| `Environment` | Type of environment data (`env.time.*`, `env.user.*`) |
|
|
167
|
-
| `PolicyTags` | Types of policy tags (if used) |
|
|
168
|
-
|
|
169
|
-
### Generating Types from Policies
|
|
170
|
-
|
|
171
|
-
Ability can automatically generate types based on policies:
|
|
172
|
-
|
|
173
|
-
```ts
|
|
174
|
-
import { AbilityTypeGenerator } from '@via-profit/ability';
|
|
175
|
-
|
|
176
|
-
const typeDefs = new AbilityTypeGenerator(policies).generateTypeDefs();
|
|
177
|
-
|
|
178
|
-
fs.writeFileSync('types.gen.ts', typeDefs, { encoding: 'utf-8' });
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
This creates a file:
|
|
182
|
-
|
|
183
|
-
```
|
|
184
|
-
types.gen.ts
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
It will contain:
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
export type Resources = { ... };
|
|
191
|
-
export type Environment = { ... };
|
|
192
|
-
export type PolicyTags = "myTag1" | "myTag2" |
|
|
193
|
-
...
|
|
194
|
-
;
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
### Using Generated Types
|
|
198
|
-
|
|
199
|
-
After generating types:
|
|
200
|
-
|
|
201
|
-
```ts
|
|
202
|
-
import { ability, AbilityResolver, DenyOverridesStrategy } from '@via-profit/ability';
|
|
203
|
-
import type { Resources, Environment, PolicyTags } from './types.gen';
|
|
204
|
-
|
|
205
|
-
const policies = ability<Resources, Environment, PolicyTags>`
|
|
206
|
-
permit permission.document.read if all:
|
|
207
|
-
document.ownerId equals '1'
|
|
208
|
-
document.status in ["published", "archived"]
|
|
209
|
-
`;
|
|
210
|
-
const resolver = new AbilityResolver(policies, DenyOverridesStrategy);
|
|
211
|
-
|
|
212
|
-
resolver.enforce('document.read', {
|
|
213
|
-
document: {
|
|
214
|
-
ownerId: 1, // ❌ Type number is not assignable to type string
|
|
215
|
-
status: 'published'
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
Now:
|
|
222
|
-
|
|
223
|
-
- `document.ownerId` and `document.status` are checked for existence and types
|
|
224
|
-
- `document.read` is validated for correctness
|
|
225
|
-
- operators (`equals`, `in`, etc.) are checked for type compatibility
|
|
226
|
-
|
|
227
|
-
### Basic Structure
|
|
228
|
-
|
|
229
|
-
```
|
|
230
|
-
# <comment-line>
|
|
231
|
-
@<annotation> <annotation-value>
|
|
232
|
-
<effect> <permission> if <all|any>:
|
|
233
|
-
<all|any> of: <subject> <operator> <value|resource|env>
|
|
234
|
-
<all|any> of: <subject> <operator> <value|resource|env>
|
|
235
|
-
...
|
|
236
|
-
except <all|any> of:
|
|
237
|
-
<subject> <operator> <value|resource|env>
|
|
238
|
-
<subject> <operator> <value|resource|env>
|
|
239
|
-
...
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
- `comment-line` - comment
|
|
243
|
-
- `annotation` - annotation (`id`, `name`, `disabled`, `tags`, `priority`)
|
|
244
|
-
- `effect` – `permit` or `deny`
|
|
245
|
-
- `permission` – permission key with `permission.` prefix (e.g., `permission.order.update`)
|
|
246
|
-
- `all` / `any` – logical operator for the rule group
|
|
247
|
-
- `except` - start of the exception block
|
|
248
|
-
|
|
249
|
-
### Rule
|
|
250
|
-
|
|
251
|
-
A rule is the simplest structure that describes what is compared with what and how.
|
|
252
|
-
|
|
253
|
-
Each rule must start with a resource path (dot-notation), followed by a comparison operator and the value to compare against.
|
|
254
|
-
|
|
255
|
-
*Rule structure*:
|
|
256
|
-
|
|
257
|
-
```
|
|
258
|
-
<subject> <operator> <value|resource|env>
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
```
|
|
262
|
-
# Simple rule
|
|
263
|
-
user.role equals "admin"
|
|
264
|
-
|
|
265
|
-
# Comparison with a number
|
|
266
|
-
user.age >= 18
|
|
267
|
-
|
|
268
|
-
# Array membership check
|
|
269
|
-
user.status in ["active", "verified"]
|
|
270
|
-
|
|
271
|
-
# Working with array/string length
|
|
272
|
-
user.roles length greater than 2
|
|
273
|
-
|
|
274
|
-
# Null check
|
|
275
|
-
user.deletedAt is null
|
|
276
|
-
|
|
277
|
-
# Undefined check
|
|
278
|
-
user.middleName is defined
|
|
279
|
-
|
|
280
|
-
# Negation
|
|
281
|
-
user.banned not equals true
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### Groups and Exceptions
|
|
285
|
-
|
|
286
|
-
A rule group is a block containing one or more rules.
|
|
287
|
-
|
|
288
|
-
```
|
|
289
|
-
permit permission.article.edit if all of:
|
|
290
|
-
article.authorId equals user.id
|
|
291
|
-
any of:
|
|
292
|
-
article.status equals "draft"
|
|
293
|
-
user.role equals "editor"
|
|
63
|
+
// Загрузка данных (ваш вариант)
|
|
64
|
+
const document = await db.loadDocument();
|
|
294
65
|
|
|
295
|
-
|
|
296
|
-
|
|
66
|
+
// Проверка разрешения
|
|
67
|
+
resolver.enforce('document.read', { document });
|
|
297
68
|
```
|
|
298
69
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
```
|
|
302
|
-
@name "High priority"
|
|
303
|
-
@description "Description"
|
|
304
|
-
@priority 100
|
|
305
|
-
@disabled true
|
|
306
|
-
deny permission.admin.all if all:
|
|
307
|
-
always
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
## Resolution Strategies
|
|
311
|
-
|
|
312
|
-
| Strategy | Behavior |
|
|
313
|
-
|-----------------------------|-----------------------------------------------------------------|
|
|
314
|
-
| `DenyOverridesStrategy` | If there is at least one `deny` → `deny`, otherwise `permit` (default) |
|
|
315
|
-
| `PermitOverridesStrategy` | If there is at least one `permit` → `permit`, otherwise `deny` |
|
|
316
|
-
| `FirstMatchStrategy` | Result of the first matching policy |
|
|
317
|
-
| `SequentialLastMatchStrategy` | Result of the last matching policy |
|
|
318
|
-
| `PriorityStrategy` | Selects the policy with the highest `priority` |
|
|
319
|
-
| `AllMustPermitStrategy` | `permit` only if **all** matching policies are `permit` |
|
|
320
|
-
| `OnlyOneApplicableStrategy` | `deny` if more than one policy matches |
|
|
321
|
-
| `AnyPermitStrategy` | `permit` if there is at least one `permit` |
|
|
322
|
-
|
|
323
|
-
Example of using a strategy:
|
|
324
|
-
|
|
325
|
-
```typescript
|
|
326
|
-
import { PriorityStrategy } from '@via-profit/ability';
|
|
327
|
-
|
|
328
|
-
const resolver = new AbilityResolver(policies, PriorityStrategy);
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
## TypeScript Type Generator
|
|
332
|
-
|
|
333
|
-
Automatically creates a `Resources` type based on all rules in the policies.
|
|
334
|
-
|
|
335
|
-
```typescript
|
|
336
|
-
import { AbilityTypeGenerator } from '@via-profit/ability';
|
|
337
|
-
|
|
338
|
-
const generator = new AbilityTypeGenerator(policies);
|
|
339
|
-
const typeDefs = generator.generateTypeDefs();
|
|
340
|
-
|
|
341
|
-
// Output: export type Resources = {
|
|
342
|
-
// ['document.read']: { readonly ownerId: number; readonly status: string; };
|
|
343
|
-
// ...
|
|
344
|
-
// }
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
The generated types can be used for strict typing of resources when calling `resolver.resolve()`.
|
|
348
|
-
|
|
349
|
-
## API Reference
|
|
350
|
-
|
|
351
|
-
### `AbilityPolicy`
|
|
352
|
-
|
|
353
|
-
```typescript
|
|
354
|
-
new AbilityPolicy({ id, name, permission, effect, compareMethod, priority })
|
|
355
|
-
.addRuleSet(ruleSet)
|
|
356
|
-
.check(resource, environment) // -> AbilityMatch
|
|
357
|
-
.explain() // -> AbilityExplain
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
### `AbilityRule`
|
|
361
|
-
|
|
362
|
-
```typescript
|
|
363
|
-
new AbilityRule({ subject, resource, condition })
|
|
364
|
-
// or static factories:
|
|
365
|
-
AbilityRule.equals('user.id', 123)
|
|
366
|
-
AbilityRule.contains('tags', 'admin')
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
### `AbilityResolver`
|
|
370
|
-
|
|
371
|
-
```typescript
|
|
372
|
-
const resolver = new AbilityResolver(policies, strategy?);
|
|
373
|
-
resolver.resolve(permission, resource, environment) -> AbilityResult
|
|
374
|
-
resolver.enforce(permission, resource, environment) // throws an error on deny
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
### `AbilityResult`
|
|
378
|
-
|
|
379
|
-
```typescript
|
|
380
|
-
result.isAllowed() // boolean
|
|
381
|
-
result.isDenied() // boolean
|
|
382
|
-
result.explain() // AbilityExplain[]
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
## Principles
|
|
386
|
-
|
|
387
|
-
The package does not perform asynchronous operations. Preparing data for verification is the responsibility of the calling code. This makes the engine's behavior deterministic and easy to test.
|
|
388
|
-
|
|
389
|
-
## License
|
|
390
|
-
|
|
391
|
-
MIT
|
|
392
|
-
|
|
393
|
-
---
|
|
70
|
+
## Лицензия
|
|
394
71
|
|
|
395
|
-
|
|
72
|
+
Распространяется под лицензией MIT © [Via-Profit](https://via-profit.ru)
|
|
396
73
|
|
|
397
|
-
|
|
398
|
-
- [DSL](./dsl.md)
|
|
74
|
+
С условиями лицензии можно ознакомиться в файле [LICENSE](./LICENSE).
|