@via-profit/ability 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +0 -127
- package/README.md +74 -25
- package/dist/core/AbilityCondition.d.ts +4 -2
- package/dist/core/AbilityPolicy.d.ts +1 -1
- package/dist/core/AbilityResolver.d.ts +1 -1
- package/dist/core/AbilityResult.d.ts +1 -1
- package/dist/core/AbilityRuleSet.d.ts +1 -1
- package/dist/core/AbilityTypeGenerator.d.ts +55 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +80 -64
- package/dist/parsers/dsl/AbilityDSLParser.d.ts +1 -2
- package/dist/parsers/dsl/AbilityDSLToken.d.ts +3 -1
- package/dist/parsers/json/AbilityJSONParser.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,129 +1,2 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [3.2.0] - 2026-x3-xx
|
|
4
|
-
|
|
5
|
-
### Изменено
|
|
6
|
-
|
|
7
|
-
- Метод `AbilityPolicy.parseAll()` переименован в `AbilityPolicy.fromJSONAll()`
|
|
8
|
-
- Метод `AbilityPolicy.parse()` переименован в `AbilityPolicy.toJSON()`
|
|
9
|
-
- Метод `AbilityRule.parse()` переименован в `AbilityRule.toJSON()`
|
|
10
|
-
- Метод `AbilityRuleSet.parse()` переименован в `.toJSON()`
|
|
11
|
-
|
|
12
|
-
## [3.1.0] - 2026-03-20
|
|
13
|
-
|
|
14
|
-
### Добавлено
|
|
15
|
-
|
|
16
|
-
- Реализован кэш
|
|
17
|
-
- Добавлен кэш-провайдер `AbilityCacheProvider` для реализации кастомного кэша
|
|
18
|
-
- Реализован и включён по умолчанию `AbilityInMemoryCache` (кэш в памяти)
|
|
19
|
-
- Добавлена полноценная поддержка `environment` как третьего аргумента в:
|
|
20
|
-
- `resolver.resolve(action, resource, environment)`
|
|
21
|
-
- `resolver.enforce(action, resource, environment)`
|
|
22
|
-
- Введена возможность использовать пути вида `env.*` в правилах политик.
|
|
23
|
-
- Пример: `"subject": "env.time.hour"`
|
|
24
|
-
- Добавлена поддержка смешанных сравнений:
|
|
25
|
-
- `resource.*` ↔ `env.*`
|
|
26
|
-
- литерал ↔ `env.*`
|
|
27
|
-
- `env.*` ↔ литерал
|
|
28
|
-
|
|
29
|
-
### Breaking changes
|
|
30
|
-
|
|
31
|
-
Асинхронизация механизма проверки политик.
|
|
32
|
-
Все методы, участвующие в цепочке вычисления разрешений, теперь возвращают `Promise`.
|
|
33
|
-
|
|
34
|
-
#### Изменено
|
|
35
|
-
|
|
36
|
-
- `AbilityRule.check(resource): Promise<AbilityMatch>`
|
|
37
|
-
Ранее возвращал `AbilityMatch` синхронно.
|
|
38
|
-
|
|
39
|
-
- `AbilityRuleSet.check(resource): Promise<AbilityMatch>`
|
|
40
|
-
Теперь выполняет правила последовательно и асинхронно.
|
|
41
|
-
|
|
42
|
-
- `AbilityPolicy.check(resource): Promise<AbilityMatch>`
|
|
43
|
-
Асинхронно проверяет ruleSet в строгом порядке.
|
|
44
|
-
|
|
45
|
-
- `AbilityResolver.resolve(action, resource): Promise<AbilityResult>`
|
|
46
|
-
Теперь асинхронный метод, который дожидается выполнения всех политик.
|
|
47
|
-
|
|
48
|
-
- `AbilityResolver.enforce(action, resource): Promise<void | never>`
|
|
49
|
-
Теперь работает асинхронно.
|
|
50
|
-
|
|
51
|
-
### Миграция
|
|
52
|
-
|
|
53
|
-
1. Все вызовы `check()` должны быть обновлены:
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
await rule.check(resource);
|
|
57
|
-
await ruleSet.check(resource);
|
|
58
|
-
await policy.check(resource);
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
2. Все вызовы `resolver.resolve()` и `resolver.enforce()` теперь требуют `await`:
|
|
62
|
-
|
|
63
|
-
```ts
|
|
64
|
-
await resolver.resolve('order.update', resource);
|
|
65
|
-
await resolver.enforce('order.update', resource);
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## [3.0.1] - 2026-03-19
|
|
69
|
-
|
|
70
|
-
## Добавлено
|
|
71
|
-
|
|
72
|
-
### 1. **Лицензия MIT** (`LICENSE`)
|
|
73
|
-
|
|
74
|
-
- Добавлен официальный файл лицензии MIT от Via Profit
|
|
75
|
-
|
|
76
|
-
### 2. **Класс `AbilityExplain.ts`**
|
|
77
|
-
|
|
78
|
-
- Новый класс для получения человекочитаемых объяснений результатов проверки
|
|
79
|
-
- Классы-наследники:
|
|
80
|
-
- `AbilityExplainRule` - объяснение для правила
|
|
81
|
-
- `AbilityExplainRuleSet` - объяснение для группы правил
|
|
82
|
-
- `AbilityExplainPolicy` - объяснение для политики
|
|
83
|
-
- Метод `toString()` форматирует вывод с отступами и символами ✓/✗
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
## Обновлено
|
|
88
|
-
|
|
89
|
-
### **AbilityParser.ts** (полная переработка)
|
|
90
|
-
|
|
91
|
-
- **Было**: Базовая генерация типов
|
|
92
|
-
- **Стало**: Расширенная система генерации TypeScript типов
|
|
93
|
-
- Новые методы:
|
|
94
|
-
- `determineTypeFromRule()` - определение типа на основе правила
|
|
95
|
-
- `getArrayType()` - обработка массивов
|
|
96
|
-
- `getPrimitiveType()` - определение примитивных типов
|
|
97
|
-
- `buildNestedStructure()` - трансформация плоской структуры во вложенную
|
|
98
|
-
- `formatTypeDefinitions()` - форматирование финального вывода
|
|
99
|
-
- `formatNestedObject()` - рекурсивное форматирование объектов
|
|
100
|
-
|
|
101
|
-
### **AbilityRule.ts**
|
|
102
|
-
|
|
103
|
-
- `id` и `name` теперь опциональные (`?`)
|
|
104
|
-
- Автогенерация `id` и `name` если не предоставлены
|
|
105
|
-
- **Новые статические методы** (фабричные методы):
|
|
106
|
-
- `equal()`, `notEqual()`, `in()`, `notIn()`
|
|
107
|
-
- `lessThan()`, `lessOrEqual()`, `moreThan()`, `moreOrEqual()`
|
|
108
|
-
|
|
109
|
-
### **AbilityRuleSet.ts**
|
|
110
|
-
|
|
111
|
-
- `id` и `name` теперь опциональные
|
|
112
|
-
- Добавлены статические методы:
|
|
113
|
-
- `and()` - создание группы с логическим И
|
|
114
|
-
- `or()` - создание группы с логическим ИЛИ
|
|
115
|
-
|
|
116
|
-
### **AbilityPolicy.ts**
|
|
117
|
-
|
|
118
|
-
- Новый метод `explain()` - получение объяснения проверки
|
|
119
|
-
- Новый статический метод `parseAll()` - парсинг массива конфигураций
|
|
120
|
-
- Улучшены комментарии к полю `action`
|
|
121
|
-
|
|
122
|
-
### **AbilityResolver.ts**
|
|
123
|
-
|
|
124
|
-
- Новый метод `resolveWithExplain()` - проверка с детальным объяснением
|
|
125
|
-
- Возвращает массив `AbilityExplain[]` для анализа результатов
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
## Обновлена документация и примеры
|
package/README.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
> A set of services that partially implement the [Attribute Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control) principle.
|
|
4
4
|
> The package allows you to describe rules, combine them into groups, form policies, and apply them to data to determine permissions.
|
|
5
5
|
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
|
|
6
15
|
## Language / Язык
|
|
7
16
|
|
|
8
17
|
- [🇬🇧 English](/docs/en/README.md)
|
|
@@ -543,6 +552,34 @@ order.total
|
|
|
543
552
|
| **length greater than** | `len >` | `tags length greater than 2` | Length greater than | array, string |
|
|
544
553
|
| **length less than** | `len <` | `tags length less than 5` | Length less than | array, string |
|
|
545
554
|
|
|
555
|
+
Here is the English version, keeping the structure and tone consistent with your documentation style.
|
|
556
|
+
|
|
557
|
+
**Special Operators**
|
|
558
|
+
|
|
559
|
+
| DSL Operator | Synonyms | Example | Description | Types |
|
|
560
|
+
|--------------|----------|---------|-------------|--------|
|
|
561
|
+
| **always** | — | `always` | The condition is always true. Used for global allow rules or simplifying logic. | special operator |
|
|
562
|
+
| **never** | — | `never` | The condition is always false. Used for global deny rules or disabling a rule. | special operator |
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
**always**
|
|
566
|
+
An operator that always returns `true`.
|
|
567
|
+
Used for:
|
|
568
|
+
|
|
569
|
+
- global allow (`permit permission.* if all: always`)
|
|
570
|
+
- testing
|
|
571
|
+
- disabling complex conditions
|
|
572
|
+
- creating fallback rules
|
|
573
|
+
|
|
574
|
+
**never**
|
|
575
|
+
An operator that always returns `false`.
|
|
576
|
+
Used for:
|
|
577
|
+
|
|
578
|
+
- global deny (`deny permission.* if all: never`)
|
|
579
|
+
- temporarily disabling a rule
|
|
580
|
+
- explicit unconditional rejection
|
|
581
|
+
|
|
582
|
+
|
|
546
583
|
#### Value
|
|
547
584
|
|
|
548
585
|
Supported values:
|
|
@@ -693,18 +730,14 @@ This allows:
|
|
|
693
730
|
|
|
694
731
|
## TypeScript Type Generator
|
|
695
732
|
|
|
696
|
-
`
|
|
733
|
+
`AbilityTypeGenerator.generateTypeDefs(policies)` generates TypeScript types based on policies, allowing you to avoid inconsistencies between types and the data in the policies.
|
|
697
734
|
|
|
698
|
-
**
|
|
735
|
+
**Example usage**
|
|
699
736
|
|
|
700
|
-
|
|
737
|
+
Policies can be stored in DSL or JSON. This example uses a DSL file.
|
|
701
738
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
import { AbilityDSLParser } from '@via-profit/ability';
|
|
706
|
-
|
|
707
|
-
const dsl = `
|
|
739
|
+
_policies/policies.dsl_
|
|
740
|
+
```
|
|
708
741
|
# @name Update order
|
|
709
742
|
permit permission.order.update if all:
|
|
710
743
|
|
|
@@ -712,25 +745,46 @@ permit permission.order.update if all:
|
|
|
712
745
|
all of:
|
|
713
746
|
# @name User is owner
|
|
714
747
|
user.id = order.ownerId
|
|
715
|
-
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
_scripts/policies.js_
|
|
751
|
+
```js
|
|
752
|
+
const fs = require('node:fs');
|
|
753
|
+
const path = require('node:path');
|
|
754
|
+
const { AbilityTypeGenerator, AbilityDSLParser } = require('@via-profit/ability');
|
|
755
|
+
|
|
756
|
+
// Prepare paths
|
|
757
|
+
const dslPath = path.resolve(__dirname, '../src/policies/policies.dsl');
|
|
758
|
+
const typeDefsPath = path.join(path.dirname(dslPath), 'policies.types.ts');
|
|
716
759
|
|
|
760
|
+
// Read DSL as a string
|
|
761
|
+
const dsl = fs.readFileSync(dslPath, {encoding: 'utf-8'});
|
|
762
|
+
|
|
763
|
+
// Create policies
|
|
717
764
|
const policies = new AbilityDSLParser(dsl).parse();
|
|
718
765
|
|
|
719
|
-
|
|
766
|
+
// Generate TypeScript types
|
|
767
|
+
const typeDefs = new AbilityTypeGenerator(policies).generateTypeDefs();
|
|
768
|
+
|
|
769
|
+
// Save TypeScript types to file
|
|
770
|
+
fs.writeFileSync(typeDefsPath, typeDefs, {encoding: 'utf-8'});
|
|
720
771
|
```
|
|
721
772
|
|
|
773
|
+
_policies/index.ts_
|
|
722
774
|
```ts
|
|
723
|
-
|
|
724
|
-
import {
|
|
725
|
-
import
|
|
726
|
-
import policies from './policies.json';
|
|
775
|
+
import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
|
|
776
|
+
import type { Resources } from './policies.types';
|
|
777
|
+
import dsl from './policies.dsl';
|
|
727
778
|
|
|
728
|
-
const
|
|
779
|
+
const policies = new AbilityDSLParser<Resources>(dsl).parse();
|
|
780
|
+
|
|
781
|
+
export const policyResolver = new AbilityResolver(new AbilityDSLParser<Resources>(dsl).parse());
|
|
782
|
+
|
|
783
|
+
export default policyResolver;
|
|
729
784
|
|
|
730
|
-
writeFileSync('./src/ability/types.generated.ts', typedefs, 'utf8');
|
|
731
785
|
```
|
|
732
786
|
|
|
733
|
-
**Generated
|
|
787
|
+
**Generated file (example)**
|
|
734
788
|
|
|
735
789
|
```ts
|
|
736
790
|
// src/ability/types.generated.ts
|
|
@@ -752,12 +806,7 @@ export type Resources = {
|
|
|
752
806
|
**Usage in code**
|
|
753
807
|
|
|
754
808
|
```ts
|
|
755
|
-
import {
|
|
756
|
-
import type { Resources } from './ability/types.generated';
|
|
757
|
-
|
|
758
|
-
const resolver = new AbilityResolver<Resources>(
|
|
759
|
-
AbilityPolicy.parseAll(policies),
|
|
760
|
-
);
|
|
809
|
+
import { policyResolver } from './policies';
|
|
761
810
|
|
|
762
811
|
await resolver.enforce('order.update', {
|
|
763
812
|
user: { id: 'u1' },
|
|
@@ -1322,4 +1371,4 @@ Throughput (ops/s)
|
|
|
1322
1371
|
|
|
1323
1372
|
## License
|
|
1324
1373
|
|
|
1325
|
-
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
1374
|
+
This project is licensed under the MIT License. See the [LICENSE](/LICENSE) file for details.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import AbilityCode from './AbilityCode';
|
|
2
|
-
export type AbilityConditionCodeType = '=' | '<>' | '>' | '<' | '>=' | '<=' | 'in' | 'not in' | 'contains' | 'not contains' | 'length greater than' | 'length less than' | 'length equals';
|
|
3
|
-
export type AbilityConditionLiteralType = 'equals' | 'not_equals' | 'contains' | 'no_contains' | 'in' | 'not_in' | 'greater_than' | 'less_than' | 'less_or_equal' | 'greater_or_equal' | 'length_greater_than' | 'length_less_than' | 'length_equals';
|
|
2
|
+
export type AbilityConditionCodeType = '=' | '<>' | '>' | '<' | '>=' | '<=' | 'in' | 'not in' | 'contains' | 'not contains' | 'length greater than' | 'length less than' | 'length equals' | 'always' | 'never';
|
|
3
|
+
export type AbilityConditionLiteralType = 'equals' | 'not_equals' | 'contains' | 'no_contains' | 'in' | 'not_in' | 'greater_than' | 'less_than' | 'less_or_equal' | 'greater_or_equal' | 'length_greater_than' | 'length_less_than' | 'length_equals' | 'always' | 'never';
|
|
4
4
|
export declare class AbilityCondition extends AbilityCode<AbilityConditionCodeType> {
|
|
5
5
|
static equals: AbilityCondition;
|
|
6
6
|
static not_equals: AbilityCondition;
|
|
@@ -15,6 +15,8 @@ export declare class AbilityCondition extends AbilityCode<AbilityConditionCodeTy
|
|
|
15
15
|
static length_greater_than: AbilityCondition;
|
|
16
16
|
static length_less_than: AbilityCondition;
|
|
17
17
|
static length_equals: AbilityCondition;
|
|
18
|
+
static always: AbilityCondition;
|
|
19
|
+
static never: AbilityCondition;
|
|
18
20
|
static fromLiteral(literal: AbilityConditionLiteralType): AbilityCondition;
|
|
19
21
|
get literal(): AbilityConditionLiteralType;
|
|
20
22
|
}
|
|
@@ -3,7 +3,7 @@ import AbilityMatch from './AbilityMatch';
|
|
|
3
3
|
import AbilityCompare, { AbilityCompareCodeType } from './AbilityCompare';
|
|
4
4
|
import AbilityPolicyEffect, { AbilityPolicyEffectCodeType } from './AbilityPolicyEffect';
|
|
5
5
|
import { AbilityExplain } from './AbilityExplain';
|
|
6
|
-
import { ResourceObject } from './
|
|
6
|
+
import { ResourceObject } from './AbilityTypeGenerator';
|
|
7
7
|
export type AbilityPolicyConfig = {
|
|
8
8
|
readonly permission: string;
|
|
9
9
|
readonly effect: AbilityPolicyEffectCodeType;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import AbilityPolicy from './AbilityPolicy';
|
|
2
2
|
import { AbilityResult } from './AbilityResult';
|
|
3
|
-
import { ResourcesMap } from './
|
|
3
|
+
import { ResourcesMap } from './AbilityTypeGenerator';
|
|
4
4
|
import { AbilityCacheAdapter } from '../cache/AbilityCacheAdapter';
|
|
5
5
|
export type AbilityResolverOptions = {
|
|
6
6
|
readonly cache?: AbilityCacheAdapter | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AbilityExplain } from './AbilityExplain';
|
|
2
|
-
import { ResourceObject } from './
|
|
2
|
+
import { ResourceObject } from './AbilityTypeGenerator';
|
|
3
3
|
import AbilityPolicy from './AbilityPolicy';
|
|
4
4
|
import AbilityPolicyEffect from './AbilityPolicyEffect';
|
|
5
5
|
export declare class AbilityResult<Resource extends ResourceObject = Record<string, unknown>> {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import AbilityRule, { AbilityRuleConfig } from './AbilityRule';
|
|
2
2
|
import AbilityCompare, { AbilityCompareCodeType } from './AbilityCompare';
|
|
3
3
|
import AbilityMatch from './AbilityMatch';
|
|
4
|
-
import { ResourceObject } from './
|
|
4
|
+
import { ResourceObject } from './AbilityTypeGenerator';
|
|
5
5
|
export type AbilityRuleSetConfig = {
|
|
6
6
|
readonly id?: string | null;
|
|
7
7
|
readonly name?: string | null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import AbilityPolicy from './AbilityPolicy';
|
|
2
|
+
export type Primitive = string | number | boolean | null | undefined;
|
|
3
|
+
export type NestedDict<T = Primitive> = {
|
|
4
|
+
[key: string]: NestedDict<T> | T;
|
|
5
|
+
};
|
|
6
|
+
export type ResourceObject = Record<string, unknown>;
|
|
7
|
+
export type ResourcesMap = Record<string, ResourceObject>;
|
|
8
|
+
export declare class AbilityTypeGenerator {
|
|
9
|
+
readonly policies: readonly AbilityPolicy[];
|
|
10
|
+
constructor(policies: readonly AbilityPolicy[]);
|
|
11
|
+
/**
|
|
12
|
+
* Generates TypeScript type definitions based on the provided policies.
|
|
13
|
+
* @returns A generated type definitions.
|
|
14
|
+
*/
|
|
15
|
+
generateTypeDefs(): string;
|
|
16
|
+
/**
|
|
17
|
+
* Determines TypeScript type based on the rule
|
|
18
|
+
* @param rule - The rule to analyze
|
|
19
|
+
* @returns TypeScript type as string
|
|
20
|
+
*/
|
|
21
|
+
private determineTypeFromRule;
|
|
22
|
+
/**
|
|
23
|
+
* Gets TypeScript type for array values
|
|
24
|
+
* @param resource - The resource value to analyze
|
|
25
|
+
* @returns TypeScript array type as string
|
|
26
|
+
*/
|
|
27
|
+
private getArrayType;
|
|
28
|
+
/**
|
|
29
|
+
* Gets primitive TypeScript type for a value
|
|
30
|
+
* @param value - The value to analyze
|
|
31
|
+
* @returns TypeScript primitive type as string
|
|
32
|
+
*/
|
|
33
|
+
private getPrimitiveType;
|
|
34
|
+
/**
|
|
35
|
+
* Builds nested structure from flat paths
|
|
36
|
+
* Example: 'user.profile.name' -> { user: { profile: { name: 'string' } } }
|
|
37
|
+
* @param flatStructure - Flat structure with dot notation paths
|
|
38
|
+
* @returns Nested object structure
|
|
39
|
+
*/
|
|
40
|
+
private buildNestedStructure;
|
|
41
|
+
/**
|
|
42
|
+
* Formats type structure into a string
|
|
43
|
+
* @param structure - Nested type structure
|
|
44
|
+
* @returns Formatted TypeScript type definition string
|
|
45
|
+
*/
|
|
46
|
+
private formatTypeDefinitions;
|
|
47
|
+
/**
|
|
48
|
+
* Recursively formats nested object
|
|
49
|
+
* @param obj - Object to format
|
|
50
|
+
* @param indent - Current indentation level
|
|
51
|
+
* @returns Formatted string
|
|
52
|
+
*/
|
|
53
|
+
private formatNestedObject;
|
|
54
|
+
}
|
|
55
|
+
export default AbilityTypeGenerator;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export * from './core/AbilityCompare';
|
|
|
3
3
|
export * from './core/AbilityCondition';
|
|
4
4
|
export * from './core/AbilityError';
|
|
5
5
|
export * from './core/AbilityMatch';
|
|
6
|
-
export * from './core/
|
|
6
|
+
export * from './core/AbilityTypeGenerator';
|
|
7
7
|
export * from './core/AbilityPolicy';
|
|
8
8
|
export * from './core/AbilityPolicyEffect';
|
|
9
9
|
export * from './core/AbilityResolver';
|
package/dist/index.js
CHANGED
|
@@ -46,6 +46,8 @@ class AbilityCondition extends AbilityCode {
|
|
|
46
46
|
static length_greater_than = new AbilityCondition('length greater than');
|
|
47
47
|
static length_less_than = new AbilityCondition('length less than');
|
|
48
48
|
static length_equals = new AbilityCondition('length equals');
|
|
49
|
+
static always = new AbilityCondition('always');
|
|
50
|
+
static never = new AbilityCondition('never');
|
|
49
51
|
static fromLiteral(literal) {
|
|
50
52
|
switch (literal) {
|
|
51
53
|
case 'equals':
|
|
@@ -72,6 +74,10 @@ class AbilityCondition extends AbilityCode {
|
|
|
72
74
|
return this.length_greater_than;
|
|
73
75
|
case 'length_equals':
|
|
74
76
|
return this.length_equals;
|
|
77
|
+
case 'always':
|
|
78
|
+
return this.always;
|
|
79
|
+
case 'never':
|
|
80
|
+
return this.never;
|
|
75
81
|
default:
|
|
76
82
|
throw new AbilityParserError(`Literal ${literal} does not found in AbilityCondition class`);
|
|
77
83
|
}
|
|
@@ -94,56 +100,20 @@ class AbilityMatch extends AbilityCode {
|
|
|
94
100
|
static mismatch = new AbilityMatch('mismatch');
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
class
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
* @param path - The path to the property in dot/bracket notation.
|
|
102
|
-
* @param value - The value to set at the specified path.
|
|
103
|
-
*/
|
|
104
|
-
static setValueDotValue(object, path, value) {
|
|
105
|
-
if (!path || path.trim().length === 0) {
|
|
106
|
-
throw new AbilityParserError(`Invalid path provided on a [${path}]`);
|
|
107
|
-
}
|
|
108
|
-
const way = path.replace(/\[/g, '.').replace(/]/g, '').split('.');
|
|
109
|
-
const last = way.pop();
|
|
110
|
-
if (!last) {
|
|
111
|
-
throw new AbilityParserError(`Invalid path provided on a [${path}]`);
|
|
112
|
-
}
|
|
113
|
-
const lastObj = way.reduce((acc, key, index, array) => {
|
|
114
|
-
const currentValue = acc[key];
|
|
115
|
-
const nextKey = array[index + 1];
|
|
116
|
-
const shouldBeArray = nextKey !== undefined && isFinite(Number(nextKey));
|
|
117
|
-
if (currentValue === undefined || currentValue === null) {
|
|
118
|
-
// Create missing property
|
|
119
|
-
const newValue = shouldBeArray ? [] : {};
|
|
120
|
-
acc[key] = newValue;
|
|
121
|
-
return newValue;
|
|
122
|
-
}
|
|
123
|
-
if (typeof currentValue !== 'object') {
|
|
124
|
-
throw new AbilityParserError(`Cannot set property '${key}' on non-object value at path: ${path}`);
|
|
125
|
-
}
|
|
126
|
-
return currentValue;
|
|
127
|
-
}, object);
|
|
128
|
-
const existingValue = lastObj[last];
|
|
129
|
-
if (existingValue !== undefined &&
|
|
130
|
-
typeof existingValue === 'object' &&
|
|
131
|
-
existingValue !== null &&
|
|
132
|
-
!Array.isArray(existingValue)) {
|
|
133
|
-
throw new AbilityParserError(`Cannot set primitive value on existing object at path: ${path}`);
|
|
134
|
-
}
|
|
135
|
-
lastObj[last] = value;
|
|
103
|
+
class AbilityTypeGenerator {
|
|
104
|
+
policies;
|
|
105
|
+
constructor(policies) {
|
|
106
|
+
this.policies = policies;
|
|
136
107
|
}
|
|
137
108
|
/**
|
|
138
109
|
* Generates TypeScript type definitions based on the provided policies.
|
|
139
|
-
* @param policies - An array of AbilityPolicy instances.
|
|
140
110
|
* @returns A generated type definitions.
|
|
141
111
|
*/
|
|
142
|
-
|
|
112
|
+
generateTypeDefs() {
|
|
143
113
|
// Structure to store types: { [action]: { [subjectPath]: type } }
|
|
144
114
|
const typeStructure = {};
|
|
145
115
|
// Iterate through all policies
|
|
146
|
-
policies.forEach(policy => {
|
|
116
|
+
this.policies.forEach(policy => {
|
|
147
117
|
const action = policy.permission;
|
|
148
118
|
// Initialize object for action if it doesn't exist
|
|
149
119
|
if (!typeStructure[action]) {
|
|
@@ -175,7 +145,7 @@ class AbilityParser {
|
|
|
175
145
|
* @param rule - The rule to analyze
|
|
176
146
|
* @returns TypeScript type as string
|
|
177
147
|
*/
|
|
178
|
-
|
|
148
|
+
determineTypeFromRule(rule) {
|
|
179
149
|
// Numeric comparisons - always number
|
|
180
150
|
if (rule.condition.isEqual(AbilityCondition.greater_than) ||
|
|
181
151
|
rule.condition.isEqual(AbilityCondition.less_than) ||
|
|
@@ -200,7 +170,7 @@ class AbilityParser {
|
|
|
200
170
|
* @param resource - The resource value to analyze
|
|
201
171
|
* @returns TypeScript array type as string
|
|
202
172
|
*/
|
|
203
|
-
|
|
173
|
+
getArrayType(resource) {
|
|
204
174
|
if (Array.isArray(resource)) {
|
|
205
175
|
if (resource.length === 0)
|
|
206
176
|
return 'any[]';
|
|
@@ -209,18 +179,18 @@ class AbilityParser {
|
|
|
209
179
|
const elementType = elementTypes.size === 1
|
|
210
180
|
? Array.from(elementTypes)[0]
|
|
211
181
|
: `(${Array.from(elementTypes).join(' | ')})`;
|
|
212
|
-
return
|
|
182
|
+
return `readonly ${elementType}[]`;
|
|
213
183
|
}
|
|
214
184
|
// If resource is not an array but condition is in/not_in,
|
|
215
185
|
// it expects an array of such elements
|
|
216
|
-
return
|
|
186
|
+
return `readonly ${this.getPrimitiveType(resource)}[]`;
|
|
217
187
|
}
|
|
218
188
|
/**
|
|
219
189
|
* Gets primitive TypeScript type for a value
|
|
220
190
|
* @param value - The value to analyze
|
|
221
191
|
* @returns TypeScript primitive type as string
|
|
222
192
|
*/
|
|
223
|
-
|
|
193
|
+
getPrimitiveType(value) {
|
|
224
194
|
if (value === null)
|
|
225
195
|
return 'null';
|
|
226
196
|
if (value === undefined)
|
|
@@ -247,7 +217,7 @@ class AbilityParser {
|
|
|
247
217
|
* @param flatStructure - Flat structure with dot notation paths
|
|
248
218
|
* @returns Nested object structure
|
|
249
219
|
*/
|
|
250
|
-
|
|
220
|
+
buildNestedStructure(flatStructure) {
|
|
251
221
|
const result = {};
|
|
252
222
|
Object.entries(flatStructure).forEach(([action, paths]) => {
|
|
253
223
|
result[action] = {};
|
|
@@ -279,10 +249,9 @@ class AbilityParser {
|
|
|
279
249
|
* @param structure - Nested type structure
|
|
280
250
|
* @returns Formatted TypeScript type definition string
|
|
281
251
|
*/
|
|
282
|
-
|
|
252
|
+
formatTypeDefinitions(structure) {
|
|
283
253
|
let output = '// Automatically generated by via-profit/ability\n';
|
|
284
254
|
output += '// Do not edit manually\n';
|
|
285
|
-
output += '\n/* eslint-disable */\n\n';
|
|
286
255
|
output += 'export type Resources = {\n';
|
|
287
256
|
// Sort actions for stable output
|
|
288
257
|
const sortedActions = Object.keys(structure).sort();
|
|
@@ -300,7 +269,7 @@ class AbilityParser {
|
|
|
300
269
|
* @param indent - Current indentation level
|
|
301
270
|
* @returns Formatted string
|
|
302
271
|
*/
|
|
303
|
-
|
|
272
|
+
formatNestedObject(obj, indent) {
|
|
304
273
|
const spaces = ' '.repeat(indent);
|
|
305
274
|
let output = '';
|
|
306
275
|
// Sort keys for stable output
|
|
@@ -587,8 +556,12 @@ class AbilityResolver {
|
|
|
587
556
|
async enforce(permission, resource, environment) {
|
|
588
557
|
const result = await this.resolve(permission, resource, environment);
|
|
589
558
|
if (result.isDenied()) {
|
|
590
|
-
const
|
|
591
|
-
|
|
559
|
+
const lastPolicy = result.getLastMatchedPolicy();
|
|
560
|
+
if (lastPolicy) {
|
|
561
|
+
throw new AbilityError(`Permission denied by policy "${lastPolicy.name.toString()}"`);
|
|
562
|
+
}
|
|
563
|
+
// No policy matched → implicit deny
|
|
564
|
+
throw new AbilityError(`Permission denied: no matching policy found (implicit deny)`);
|
|
592
565
|
}
|
|
593
566
|
}
|
|
594
567
|
/**
|
|
@@ -660,6 +633,10 @@ class AbilityRule {
|
|
|
660
633
|
let is = false;
|
|
661
634
|
const [subjectValue, resourceValue] = this.extractValues(resource, environment);
|
|
662
635
|
const isValue = (v) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null;
|
|
636
|
+
// always
|
|
637
|
+
if (AbilityCondition.always.isEqual(this.condition)) {
|
|
638
|
+
is = true;
|
|
639
|
+
}
|
|
663
640
|
// equals
|
|
664
641
|
if (AbilityCondition.equals.isEqual(this.condition)) {
|
|
665
642
|
is = subjectValue === resourceValue;
|
|
@@ -1254,6 +1231,8 @@ class AbilityDSLToken extends AbilityCode {
|
|
|
1254
1231
|
static LEN_LT = 'LEN_LT';
|
|
1255
1232
|
static LEN_EQ = 'LEN_EQ';
|
|
1256
1233
|
static NOT_EQ = 'NOT_EQ';
|
|
1234
|
+
static ALWAYS = 'ALWAYS';
|
|
1235
|
+
static NEVER = 'NEVER';
|
|
1257
1236
|
static STRING = 'STRING';
|
|
1258
1237
|
static NUMBER = 'NUMBER';
|
|
1259
1238
|
static BOOLEAN = 'BOOLEAN';
|
|
@@ -1296,6 +1275,8 @@ class AbilityDSLLexer {
|
|
|
1296
1275
|
'is',
|
|
1297
1276
|
'or',
|
|
1298
1277
|
'than',
|
|
1278
|
+
'always',
|
|
1279
|
+
'never',
|
|
1299
1280
|
]);
|
|
1300
1281
|
constructor(input) {
|
|
1301
1282
|
this.input = input;
|
|
@@ -1441,6 +1422,12 @@ class AbilityDSLLexer {
|
|
|
1441
1422
|
}
|
|
1442
1423
|
}
|
|
1443
1424
|
const word = this.input.slice(start, this.pos);
|
|
1425
|
+
if (word === 'always') {
|
|
1426
|
+
return new AbilityDSLToken(AbilityDSLToken.ALWAYS, word, startLine, startColumn);
|
|
1427
|
+
}
|
|
1428
|
+
if (word === 'never') {
|
|
1429
|
+
return new AbilityDSLToken(AbilityDSLToken.NEVER, word, startLine, startColumn);
|
|
1430
|
+
}
|
|
1444
1431
|
// Если есть точка — это путь (identifier или permission)
|
|
1445
1432
|
if (word.includes('.')) {
|
|
1446
1433
|
const last = this.tokens[this.tokens.length - 1];
|
|
@@ -1703,7 +1690,9 @@ class AbilityDSLParser {
|
|
|
1703
1690
|
if (this.isStartOfGroup() || this.isStartOfPolicy()) {
|
|
1704
1691
|
break;
|
|
1705
1692
|
}
|
|
1706
|
-
if (this.check(AbilityDSLToken.IDENTIFIER)
|
|
1693
|
+
if (this.check(AbilityDSLToken.IDENTIFIER) ||
|
|
1694
|
+
this.check(AbilityDSLToken.ALWAYS) ||
|
|
1695
|
+
this.check(AbilityDSLToken.NEVER)) {
|
|
1707
1696
|
group.addRule(this.parseRule());
|
|
1708
1697
|
}
|
|
1709
1698
|
else {
|
|
@@ -1720,7 +1709,7 @@ class AbilityDSLParser {
|
|
|
1720
1709
|
parseGroup() {
|
|
1721
1710
|
this.consumeLeadingComments();
|
|
1722
1711
|
const meta = this.takeAnnotations();
|
|
1723
|
-
const compareToken = this.consumeOneOf([AbilityDSLToken.ALL, AbilityDSLToken.ANY], 'Expected "all" or "any"');
|
|
1712
|
+
const compareToken = this.consumeOneOf([AbilityDSLToken.ALL, AbilityDSLToken.ANY, AbilityDSLToken.ALWAYS, AbilityDSLToken.NEVER], 'Expected "all" or "any" or "always" or "never"');
|
|
1724
1713
|
const compareMethod = compareToken.code === AbilityDSLToken.ALL ? AbilityCompare.and : AbilityCompare.or;
|
|
1725
1714
|
if (this.check(AbilityDSLToken.OF)) {
|
|
1726
1715
|
this.advance();
|
|
@@ -1750,11 +1739,26 @@ class AbilityDSLParser {
|
|
|
1750
1739
|
parseRule() {
|
|
1751
1740
|
this.consumeLeadingComments();
|
|
1752
1741
|
const meta = this.takeAnnotations();
|
|
1753
|
-
if (
|
|
1754
|
-
|
|
1742
|
+
// if (this.check(AbilityDSLToken.ALWAYS) || this.check(AbilityDSLToken.NEVER)) {
|
|
1743
|
+
// // Checking that there are no extra tokens after the value
|
|
1744
|
+
// // (skip comments)
|
|
1745
|
+
// this.consumeLeadingComments();
|
|
1746
|
+
// const specOperator = this.consume();
|
|
1747
|
+
// // return new AbilityRule({
|
|
1748
|
+
// // subject: '',
|
|
1749
|
+
// // resource,
|
|
1750
|
+
// // condition,
|
|
1751
|
+
// // name: meta.name,
|
|
1752
|
+
// // });
|
|
1753
|
+
// }
|
|
1754
|
+
const isNeverAlways = this.check(AbilityDSLToken.ALWAYS) || this.check(AbilityDSLToken.NEVER);
|
|
1755
|
+
if (!isNeverAlways && !this.check(AbilityDSLToken.IDENTIFIER)) {
|
|
1756
|
+
this.syntaxError(`Expected identifier, but got ${this.peek().code}`, this.peek());
|
|
1755
1757
|
}
|
|
1756
1758
|
// Subject (e.g., "user.roles")
|
|
1757
|
-
const subject =
|
|
1759
|
+
const subject = isNeverAlways
|
|
1760
|
+
? ''
|
|
1761
|
+
: this.consume(AbilityDSLToken.IDENTIFIER, 'Expected field').value;
|
|
1758
1762
|
// Operator (e.g., "contains", "equals", "is not null")
|
|
1759
1763
|
const { condition, operator } = this.parseConditionOperator();
|
|
1760
1764
|
let resource;
|
|
@@ -1762,7 +1766,9 @@ class AbilityDSLParser {
|
|
|
1762
1766
|
// Special operators that don't consume a value token.
|
|
1763
1767
|
if (operator === AbilityDSLToken.EQ_NULL ||
|
|
1764
1768
|
operator === AbilityDSLToken.NOT_EQ_NULL ||
|
|
1765
|
-
operator === AbilityDSLToken.NULL
|
|
1769
|
+
operator === AbilityDSLToken.NULL ||
|
|
1770
|
+
operator === AbilityDSLToken.ALWAYS ||
|
|
1771
|
+
operator === AbilityDSLToken.NEVER) {
|
|
1766
1772
|
resource = null;
|
|
1767
1773
|
}
|
|
1768
1774
|
else {
|
|
@@ -1794,6 +1800,16 @@ class AbilityDSLParser {
|
|
|
1794
1800
|
*/
|
|
1795
1801
|
parseConditionOperator() {
|
|
1796
1802
|
const savedPos = this.pos;
|
|
1803
|
+
// "always"
|
|
1804
|
+
if (this.matchWord('always')) {
|
|
1805
|
+
return { condition: AbilityCondition.always, operator: AbilityDSLToken.ALWAYS };
|
|
1806
|
+
}
|
|
1807
|
+
this.pos = savedPos;
|
|
1808
|
+
// "never"
|
|
1809
|
+
if (this.matchWord('never')) {
|
|
1810
|
+
return { condition: AbilityCondition.never, operator: AbilityDSLToken.NEVER };
|
|
1811
|
+
}
|
|
1812
|
+
this.pos = savedPos;
|
|
1797
1813
|
// "length equals"
|
|
1798
1814
|
if (this.matchWord('length') && this.matchWord('equals')) {
|
|
1799
1815
|
return { condition: AbilityCondition.length_equals, operator: AbilityDSLToken.LEN_EQ };
|
|
@@ -1989,7 +2005,10 @@ class AbilityDSLParser {
|
|
|
1989
2005
|
return false;
|
|
1990
2006
|
}
|
|
1991
2007
|
const token = this.peek();
|
|
1992
|
-
if ((token.code === AbilityDSLToken.KEYWORD ||
|
|
2008
|
+
if ((token.code === AbilityDSLToken.KEYWORD ||
|
|
2009
|
+
token.code === AbilityDSLToken.IDENTIFIER ||
|
|
2010
|
+
token.code === AbilityDSLToken.ALWAYS ||
|
|
2011
|
+
token.code === AbilityDSLToken.NEVER) &&
|
|
1993
2012
|
token.value === word) {
|
|
1994
2013
|
this.advance();
|
|
1995
2014
|
return true;
|
|
@@ -2134,9 +2153,6 @@ class AbilityDSLParser {
|
|
|
2134
2153
|
}
|
|
2135
2154
|
throw new AbilityDSLSyntaxError(token.line, token.column, context + '\n', finalDetails);
|
|
2136
2155
|
}
|
|
2137
|
-
getLine(lineNumber) {
|
|
2138
|
-
return this.dsl.split(/\r?\n/)[lineNumber - 1] ?? '';
|
|
2139
|
-
}
|
|
2140
2156
|
suggest(actual, expectedTypes) {
|
|
2141
2157
|
const candidates = [];
|
|
2142
2158
|
for (const type of expectedTypes) {
|
|
@@ -2235,7 +2251,6 @@ exports.AbilityExplainRuleSet = AbilityExplainRuleSet;
|
|
|
2235
2251
|
exports.AbilityInMemoryCache = AbilityInMemoryCache;
|
|
2236
2252
|
exports.AbilityJSONParser = AbilityJSONParser;
|
|
2237
2253
|
exports.AbilityMatch = AbilityMatch;
|
|
2238
|
-
exports.AbilityParser = AbilityParser;
|
|
2239
2254
|
exports.AbilityParserError = AbilityParserError;
|
|
2240
2255
|
exports.AbilityPolicy = AbilityPolicy;
|
|
2241
2256
|
exports.AbilityPolicyEffect = AbilityPolicyEffect;
|
|
@@ -2243,3 +2258,4 @@ exports.AbilityResolver = AbilityResolver;
|
|
|
2243
2258
|
exports.AbilityResult = AbilityResult;
|
|
2244
2259
|
exports.AbilityRule = AbilityRule;
|
|
2245
2260
|
exports.AbilityRuleSet = AbilityRuleSet;
|
|
2261
|
+
exports.AbilityTypeGenerator = AbilityTypeGenerator;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import AbilityPolicy from '../../core/AbilityPolicy';
|
|
2
|
-
import { ResourceObject } from '../../core/
|
|
2
|
+
import { ResourceObject } from '../../core/AbilityTypeGenerator';
|
|
3
3
|
/**
|
|
4
4
|
* Parser for the Ability DSL.
|
|
5
5
|
*
|
|
@@ -72,7 +72,6 @@ export declare class AbilityDSLParser<Resource extends ResourceObject = Record<s
|
|
|
72
72
|
private processCommentToken;
|
|
73
73
|
private takeAnnotations;
|
|
74
74
|
private syntaxError;
|
|
75
|
-
private getLine;
|
|
76
75
|
private suggest;
|
|
77
76
|
private levenshteinDistance;
|
|
78
77
|
private consumeOneOf;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import AbilityCode from '../../core/AbilityCode';
|
|
2
|
-
export type TokenType = 'EFFECT' | 'IF' | 'PERMISSION' | 'IDENTIFIER' | 'COLON' | 'COMMA' | 'DOT' | 'LBRACKET' | 'RBRACKET' | 'ALL' | 'ANY' | 'OF' | 'EOF' | 'COMMENT' | 'EQ' | 'CONTAINS' | 'IN' | 'NOT_IN' | 'NOT_CONTAINS' | 'GT' | 'GTE' | 'LT' | 'LTE' | 'NULL' | 'EQ_NULL' | 'NOT_EQ_NULL' | 'NOT_EQ' | 'LEN_GT' | 'LEN_LT' | 'LEN_EQ' | 'STRING' | 'NUMBER' | 'BOOLEAN' | 'SYMBOL' | 'KEYWORD' | 'UNKNOWN';
|
|
2
|
+
export type TokenType = 'EFFECT' | 'IF' | 'PERMISSION' | 'IDENTIFIER' | 'COLON' | 'COMMA' | 'DOT' | 'LBRACKET' | 'RBRACKET' | 'ALL' | 'ANY' | 'OF' | 'EOF' | 'COMMENT' | 'EQ' | 'CONTAINS' | 'IN' | 'NOT_IN' | 'NOT_CONTAINS' | 'GT' | 'GTE' | 'LT' | 'LTE' | 'NULL' | 'EQ_NULL' | 'NOT_EQ_NULL' | 'NOT_EQ' | 'LEN_GT' | 'LEN_LT' | 'LEN_EQ' | 'ALWAYS' | 'NEVER' | 'STRING' | 'NUMBER' | 'BOOLEAN' | 'SYMBOL' | 'KEYWORD' | 'UNKNOWN';
|
|
3
3
|
/**
|
|
4
4
|
* Represents a single token produced by the Ability DSL lexer.
|
|
5
5
|
* Each token carries a type (e.g., EFFECT, IDENTIFIER, STRING) and its raw string value.
|
|
@@ -47,6 +47,8 @@ export declare class AbilityDSLToken<Code extends TokenType = TokenType> extends
|
|
|
47
47
|
static readonly LEN_LT: TokenType;
|
|
48
48
|
static readonly LEN_EQ: TokenType;
|
|
49
49
|
static readonly NOT_EQ: TokenType;
|
|
50
|
+
static readonly ALWAYS: TokenType;
|
|
51
|
+
static readonly NEVER: TokenType;
|
|
50
52
|
static readonly STRING: TokenType;
|
|
51
53
|
static readonly NUMBER: TokenType;
|
|
52
54
|
static readonly BOOLEAN: TokenType;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AbilityRule, AbilityRuleConfig } from '../../core/AbilityRule';
|
|
2
2
|
import { AbilityRuleSet, AbilityRuleSetConfig } from '../../core/AbilityRuleSet';
|
|
3
|
-
import { ResourceObject } from '../../core/
|
|
3
|
+
import { ResourceObject } from '../../core/AbilityTypeGenerator';
|
|
4
4
|
import { AbilityPolicy, AbilityPolicyConfig } from '../../core/AbilityPolicy';
|
|
5
5
|
export declare class AbilityJSONParser {
|
|
6
6
|
/**
|