@via-profit/ability 3.5.4 → 3.6.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/README.md +0 -30
- package/dist/index.d.ts +422 -195
- package/dist/index.js +1484 -695
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
constructor(code) {
|
|
6
|
-
this._code = code;
|
|
7
|
-
}
|
|
8
|
-
get code() {
|
|
9
|
-
return this._code;
|
|
10
|
-
}
|
|
11
|
-
isEqual(compareWith) {
|
|
12
|
-
return compareWith !== null && this.code === compareWith.code;
|
|
13
|
-
}
|
|
14
|
-
isNotEqual(compareWith) {
|
|
15
|
-
return !this.isEqual(compareWith);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
class AbilityCompare extends AbilityCode {
|
|
20
|
-
static and = new AbilityCompare('and');
|
|
21
|
-
static or = new AbilityCompare('or');
|
|
3
|
+
function brand$4(code) {
|
|
4
|
+
return code;
|
|
22
5
|
}
|
|
6
|
+
const AbilityCompare = {
|
|
7
|
+
or: brand$4('or'),
|
|
8
|
+
and: brand$4('and'),
|
|
9
|
+
};
|
|
23
10
|
|
|
24
11
|
class AbilityError extends Error {
|
|
25
12
|
constructor(message, options) {
|
|
@@ -40,106 +27,104 @@ class AbilityParserError extends Error {
|
|
|
40
27
|
}
|
|
41
28
|
}
|
|
42
29
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
not_in: this.not_in,
|
|
86
|
-
contains: this.contains,
|
|
87
|
-
not_contains: this.not_contains,
|
|
88
|
-
length_greater_than: this.length_greater_than,
|
|
89
|
-
length_equals: this.length_equals,
|
|
90
|
-
always: this.always,
|
|
91
|
-
never: this.never,
|
|
92
|
-
length_less_than: this.length_less_than,
|
|
93
|
-
};
|
|
94
|
-
const condition = map[literal];
|
|
95
|
-
if (!condition) {
|
|
96
|
-
throw new AbilityParserError(`Literal "${literal}" does not found in AbilityCondition class`);
|
|
97
|
-
}
|
|
98
|
-
return condition;
|
|
30
|
+
function brand$3(code) {
|
|
31
|
+
return code;
|
|
32
|
+
}
|
|
33
|
+
const AbilityCondition = {
|
|
34
|
+
equals: brand$3('='),
|
|
35
|
+
not_equals: brand$3('<>'),
|
|
36
|
+
greater_than: brand$3('>'),
|
|
37
|
+
less_than: brand$3('<'),
|
|
38
|
+
less_or_equal: brand$3('<='),
|
|
39
|
+
greater_or_equal: brand$3('>='),
|
|
40
|
+
in: brand$3('in'),
|
|
41
|
+
not_in: brand$3('not in'),
|
|
42
|
+
contains: brand$3('contains'),
|
|
43
|
+
not_contains: brand$3('not contains'),
|
|
44
|
+
length_greater_than: brand$3('length greater than'),
|
|
45
|
+
length_less_than: brand$3('length less than'),
|
|
46
|
+
length_equals: brand$3('length equals'),
|
|
47
|
+
always: brand$3('always'),
|
|
48
|
+
never: brand$3('never'),
|
|
49
|
+
};
|
|
50
|
+
function fromLiteral(literal) {
|
|
51
|
+
const map = {
|
|
52
|
+
equals: AbilityCondition.equals,
|
|
53
|
+
not_equals: AbilityCondition.not_equals,
|
|
54
|
+
greater_than: AbilityCondition.greater_than,
|
|
55
|
+
less_than: AbilityCondition.less_than,
|
|
56
|
+
less_or_equal: AbilityCondition.less_or_equal,
|
|
57
|
+
greater_or_equal: AbilityCondition.greater_or_equal,
|
|
58
|
+
in: AbilityCondition.in,
|
|
59
|
+
not_in: AbilityCondition.not_in,
|
|
60
|
+
contains: AbilityCondition.contains,
|
|
61
|
+
not_contains: AbilityCondition.not_contains,
|
|
62
|
+
length_greater_than: AbilityCondition.length_greater_than,
|
|
63
|
+
length_less_than: AbilityCondition.length_less_than,
|
|
64
|
+
length_equals: AbilityCondition.length_equals,
|
|
65
|
+
always: AbilityCondition.always,
|
|
66
|
+
never: AbilityCondition.never,
|
|
67
|
+
};
|
|
68
|
+
const value = map[literal];
|
|
69
|
+
if (!value) {
|
|
70
|
+
const expected = Object.keys(map).join(', ');
|
|
71
|
+
throw new AbilityParserError(`Literal "${literal}" does not found in AbilityCondition. Expected one of: ${expected}`);
|
|
99
72
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
function toLiteral(cond) {
|
|
76
|
+
switch (cond) {
|
|
77
|
+
case AbilityCondition.equals:
|
|
78
|
+
return 'equals';
|
|
79
|
+
case AbilityCondition.not_equals:
|
|
80
|
+
return 'not_equals';
|
|
81
|
+
case AbilityCondition.greater_than:
|
|
82
|
+
return 'greater_than';
|
|
83
|
+
case AbilityCondition.less_than:
|
|
84
|
+
return 'less_than';
|
|
85
|
+
case AbilityCondition.less_or_equal:
|
|
86
|
+
return 'less_or_equal';
|
|
87
|
+
case AbilityCondition.greater_or_equal:
|
|
88
|
+
return 'greater_or_equal';
|
|
89
|
+
case AbilityCondition.in:
|
|
90
|
+
return 'in';
|
|
91
|
+
case AbilityCondition.not_in:
|
|
92
|
+
return 'not_in';
|
|
93
|
+
case AbilityCondition.contains:
|
|
94
|
+
return 'contains';
|
|
95
|
+
case AbilityCondition.not_contains:
|
|
96
|
+
return 'not_contains';
|
|
97
|
+
case AbilityCondition.length_greater_than:
|
|
98
|
+
return 'length_greater_than';
|
|
99
|
+
case AbilityCondition.length_less_than:
|
|
100
|
+
return 'length_less_than';
|
|
101
|
+
case AbilityCondition.length_equals:
|
|
102
|
+
return 'length_equals';
|
|
103
|
+
case AbilityCondition.always:
|
|
104
|
+
return 'always';
|
|
105
|
+
case AbilityCondition.never:
|
|
106
|
+
return 'never';
|
|
107
|
+
default:
|
|
108
|
+
return 'never';
|
|
135
109
|
}
|
|
136
110
|
}
|
|
111
|
+
function isConditionEqual(a, b) {
|
|
112
|
+
return a !== null && b !== null && a === b;
|
|
113
|
+
}
|
|
114
|
+
function isConditionNotEqual(a, b) {
|
|
115
|
+
return !isConditionEqual(a, b);
|
|
116
|
+
}
|
|
137
117
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
static match = new AbilityMatch('match');
|
|
141
|
-
static mismatch = new AbilityMatch('mismatch');
|
|
118
|
+
function brand$2(code) {
|
|
119
|
+
return code;
|
|
142
120
|
}
|
|
121
|
+
const AbilityMatch = {
|
|
122
|
+
pending: brand$2('pending'),
|
|
123
|
+
match: brand$2('match'),
|
|
124
|
+
mismatch: brand$2('mismatch'),
|
|
125
|
+
exceptMismatch: brand$2('except-mismatch'),
|
|
126
|
+
disabled: brand$2('disabled'),
|
|
127
|
+
};
|
|
143
128
|
|
|
144
129
|
class AbilityTypeGenerator {
|
|
145
130
|
policies;
|
|
@@ -152,43 +137,103 @@ class AbilityTypeGenerator {
|
|
|
152
137
|
*/
|
|
153
138
|
generateTypeDefs() {
|
|
154
139
|
// Structure to store types: { [action]: { [subjectPath]: type } }
|
|
155
|
-
const
|
|
140
|
+
const resorceStructure = {};
|
|
141
|
+
const environmentStructure = {};
|
|
142
|
+
// tags
|
|
143
|
+
const allTags = new Set();
|
|
156
144
|
// Iterate through all policies
|
|
157
145
|
this.policies.forEach(policy => {
|
|
146
|
+
policy.tags.forEach(tag => allTags.add(tag));
|
|
158
147
|
const action = policy.permission;
|
|
159
148
|
// Initialize object for action if it doesn't exist
|
|
160
|
-
if (!
|
|
161
|
-
|
|
149
|
+
if (!resorceStructure[action]) {
|
|
150
|
+
resorceStructure[action] = {};
|
|
162
151
|
}
|
|
163
152
|
// Iterate through all ruleSets in the policy
|
|
164
153
|
policy.ruleSet.forEach(ruleSet => {
|
|
165
154
|
// Iterate through all rules in the ruleSet
|
|
166
155
|
ruleSet.rules.forEach(rule => {
|
|
167
156
|
const subjectPath = rule.subject;
|
|
168
|
-
const existingType = typeStructure[action][subjectPath];
|
|
169
157
|
const ruleType = this.determineTypeFromRule(rule);
|
|
170
158
|
if (!ruleType) {
|
|
171
159
|
return;
|
|
172
160
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
161
|
+
// -----------------------------
|
|
162
|
+
// ENVIRONMENT HANDLING (subject)
|
|
163
|
+
// -----------------------------
|
|
164
|
+
if (subjectPath.startsWith('env.')) {
|
|
165
|
+
const envPath = subjectPath.replace(/^env\./, '');
|
|
166
|
+
if (!environmentStructure[action]) {
|
|
167
|
+
environmentStructure[action] = {};
|
|
168
|
+
}
|
|
169
|
+
environmentStructure[action][envPath] = ruleType;
|
|
176
170
|
}
|
|
177
171
|
else {
|
|
178
|
-
|
|
172
|
+
const existingType = resorceStructure[action][subjectPath];
|
|
173
|
+
if (existingType && existingType !== ruleType) {
|
|
174
|
+
resorceStructure[action][subjectPath] = `${existingType} | ${ruleType}`;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
resorceStructure[action][subjectPath] = ruleType;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// -----------------------------
|
|
181
|
+
// RESOURCE PATH HANDLING (right side)
|
|
182
|
+
// -----------------------------
|
|
183
|
+
if (typeof rule.resource === 'string' && this.isPath(rule.resource)) {
|
|
184
|
+
const resourcePath = rule.resource;
|
|
185
|
+
// env.* справа
|
|
186
|
+
if (resourcePath.startsWith('env.')) {
|
|
187
|
+
const envPath = resourcePath.replace(/^env\./, '');
|
|
188
|
+
if (!environmentStructure[action]) {
|
|
189
|
+
environmentStructure[action] = {};
|
|
190
|
+
}
|
|
191
|
+
const existingEnvType = environmentStructure[action][envPath];
|
|
192
|
+
const targetType = ruleType; // или 'unknown', если хочешь жёстко
|
|
193
|
+
if (existingEnvType && existingEnvType !== targetType) {
|
|
194
|
+
environmentStructure[action][envPath] = `${existingEnvType} | ${targetType}`;
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
environmentStructure[action][envPath] = targetType;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// обычный ресурс справа
|
|
202
|
+
if (!resorceStructure[action]) {
|
|
203
|
+
resorceStructure[action] = {};
|
|
204
|
+
}
|
|
205
|
+
const existingResType = resorceStructure[action][resourcePath];
|
|
206
|
+
const targetType = ruleType; // или 'unknown'
|
|
207
|
+
if (existingResType && existingResType !== targetType) {
|
|
208
|
+
resorceStructure[action][resourcePath] = `${existingResType} | ${targetType}`;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
resorceStructure[action][resourcePath] = targetType;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
179
214
|
}
|
|
180
215
|
});
|
|
181
216
|
});
|
|
182
217
|
});
|
|
183
218
|
const filteredStructure = {};
|
|
184
|
-
Object.entries(
|
|
219
|
+
Object.entries(resorceStructure).forEach(([action, fields]) => {
|
|
185
220
|
if (!action.endsWith('.*')) {
|
|
186
221
|
filteredStructure[action] = fields;
|
|
187
222
|
}
|
|
188
223
|
});
|
|
189
224
|
// Transform flat structure into nested structure for easier use
|
|
190
225
|
const nestedStructure = this.buildNestedStructure(filteredStructure);
|
|
191
|
-
|
|
226
|
+
const nestedEnvironment = this.buildNestedStructure(environmentStructure);
|
|
227
|
+
return this.formatTypeDefinitions(nestedStructure, nestedEnvironment, allTags);
|
|
228
|
+
}
|
|
229
|
+
isPath(value) {
|
|
230
|
+
if (typeof value !== 'string') {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
if (value.startsWith('"') || value.startsWith("'")) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return value.includes('.');
|
|
192
237
|
}
|
|
193
238
|
/**
|
|
194
239
|
* Determines TypeScript type based on the rule
|
|
@@ -196,25 +241,32 @@ class AbilityTypeGenerator {
|
|
|
196
241
|
* @returns TypeScript type as string
|
|
197
242
|
*/
|
|
198
243
|
determineTypeFromRule(rule) {
|
|
199
|
-
if (rule.condition.
|
|
200
|
-
rule.condition.isEqual(AbilityCondition.always)) {
|
|
244
|
+
if (rule.condition === AbilityCondition.never || rule.condition === AbilityCondition.always) {
|
|
201
245
|
return null;
|
|
202
246
|
}
|
|
247
|
+
if (rule.condition === AbilityCondition.contains ||
|
|
248
|
+
rule.condition === AbilityCondition.not_contains) {
|
|
249
|
+
return this.getArrayType(rule.resource);
|
|
250
|
+
}
|
|
251
|
+
if (rule.condition === AbilityCondition.length_equals ||
|
|
252
|
+
rule.condition === AbilityCondition.length_greater_than ||
|
|
253
|
+
rule.condition === AbilityCondition.length_less_than) {
|
|
254
|
+
return 'string | readonly unknown[]';
|
|
255
|
+
}
|
|
203
256
|
// Numeric comparisons - always number
|
|
204
|
-
if (rule.condition
|
|
205
|
-
rule.condition
|
|
206
|
-
rule.condition
|
|
207
|
-
rule.condition
|
|
257
|
+
if (rule.condition === AbilityCondition.greater_than ||
|
|
258
|
+
rule.condition === AbilityCondition.greater_or_equal ||
|
|
259
|
+
rule.condition === AbilityCondition.less_than ||
|
|
260
|
+
rule.condition === AbilityCondition.less_or_equal) {
|
|
208
261
|
return 'number';
|
|
209
262
|
}
|
|
210
263
|
// Array operations
|
|
211
|
-
if (rule.condition.
|
|
212
|
-
|
|
213
|
-
return this.getArrayType(rule.resource);
|
|
264
|
+
if (rule.condition === AbilityCondition.in || rule.condition === AbilityCondition.not_in) {
|
|
265
|
+
return this.getInArrayType(rule.resource);
|
|
214
266
|
}
|
|
215
267
|
// Equality/Inequality operations
|
|
216
|
-
if (rule.condition
|
|
217
|
-
rule.condition
|
|
268
|
+
if (rule.condition === AbilityCondition.equals ||
|
|
269
|
+
rule.condition === AbilityCondition.not_equals) {
|
|
218
270
|
return this.getPrimitiveType(rule.resource);
|
|
219
271
|
}
|
|
220
272
|
return 'any';
|
|
@@ -225,20 +277,38 @@ class AbilityTypeGenerator {
|
|
|
225
277
|
* @returns TypeScript array type as string
|
|
226
278
|
*/
|
|
227
279
|
getArrayType(resource) {
|
|
280
|
+
// if (Array.isArray(resource)) {
|
|
281
|
+
// if (resource.length === 0) {
|
|
282
|
+
// return 'readonly unknown[]';
|
|
283
|
+
// }
|
|
284
|
+
// // Determine types of array elements
|
|
285
|
+
// const elementTypes = new Set(resource.map(item => this.getPrimitiveType(item)));
|
|
286
|
+
// const elementType =
|
|
287
|
+
// elementTypes.size === 1
|
|
288
|
+
// ? Array.from(elementTypes)[0]
|
|
289
|
+
// : `(${Array.from(elementTypes).join(' | ')})`;
|
|
290
|
+
// return `readonly ${elementType}[]`;
|
|
291
|
+
// }
|
|
292
|
+
// // If resource is not an array but condition is in/not_in,
|
|
293
|
+
// // it expects an array of such elements
|
|
294
|
+
// return `readonly ${this.getPrimitiveType(resource)}[]`;
|
|
295
|
+
const elementType = this.getInArrayType(resource);
|
|
296
|
+
return `readonly ${elementType}[]`;
|
|
297
|
+
}
|
|
298
|
+
getInArrayType(resource) {
|
|
228
299
|
if (Array.isArray(resource)) {
|
|
229
300
|
if (resource.length === 0) {
|
|
230
|
-
return '
|
|
301
|
+
return 'unknown';
|
|
231
302
|
}
|
|
232
303
|
// Determine types of array elements
|
|
233
304
|
const elementTypes = new Set(resource.map(item => this.getPrimitiveType(item)));
|
|
234
|
-
|
|
305
|
+
return elementTypes.size === 1
|
|
235
306
|
? Array.from(elementTypes)[0]
|
|
236
307
|
: `(${Array.from(elementTypes).join(' | ')})`;
|
|
237
|
-
return `readonly ${elementType}[]`;
|
|
238
308
|
}
|
|
239
309
|
// If resource is not an array but condition is in/not_in,
|
|
240
310
|
// it expects an array of such elements
|
|
241
|
-
return
|
|
311
|
+
return this.getPrimitiveType(resource);
|
|
242
312
|
}
|
|
243
313
|
/**
|
|
244
314
|
* Gets primitive TypeScript type for a value
|
|
@@ -247,11 +317,15 @@ class AbilityTypeGenerator {
|
|
|
247
317
|
*/
|
|
248
318
|
getPrimitiveType(value) {
|
|
249
319
|
if (value === null) {
|
|
250
|
-
return 'null';
|
|
320
|
+
return 'null | unknown';
|
|
251
321
|
}
|
|
252
322
|
if (value === undefined) {
|
|
253
323
|
return 'undefined';
|
|
254
324
|
}
|
|
325
|
+
if (typeof value === 'string' && this.isPath(value)) {
|
|
326
|
+
// This is not a string literal, but a path to another field.
|
|
327
|
+
return 'unknown';
|
|
328
|
+
}
|
|
255
329
|
switch (typeof value) {
|
|
256
330
|
case 'string':
|
|
257
331
|
return 'string';
|
|
@@ -304,9 +378,11 @@ class AbilityTypeGenerator {
|
|
|
304
378
|
/**
|
|
305
379
|
* Formats type structure into a string
|
|
306
380
|
* @param structure - Nested type structure
|
|
381
|
+
* @param environment
|
|
382
|
+
* @param allTags
|
|
307
383
|
* @returns Formatted TypeScript type definition string
|
|
308
384
|
*/
|
|
309
|
-
formatTypeDefinitions(structure) {
|
|
385
|
+
formatTypeDefinitions(structure, environment, allTags) {
|
|
310
386
|
let output = '// Automatically generated by via-profit/ability\n';
|
|
311
387
|
output += '// Do not edit manually\n';
|
|
312
388
|
output += 'export type Resources = {\n';
|
|
@@ -326,6 +402,29 @@ class AbilityTypeGenerator {
|
|
|
326
402
|
}
|
|
327
403
|
});
|
|
328
404
|
output += '}\n';
|
|
405
|
+
// tags
|
|
406
|
+
const tagsUnion = allTags.size > 0
|
|
407
|
+
? Array.from(allTags)
|
|
408
|
+
.sort()
|
|
409
|
+
.map(tag => `'${tag}'`)
|
|
410
|
+
.join(' | ')
|
|
411
|
+
: 'never';
|
|
412
|
+
output += `\n\nexport type PolicyTags = ${tagsUnion};\n`;
|
|
413
|
+
// environments
|
|
414
|
+
output += '\n\nexport type Environment = {\n';
|
|
415
|
+
Object.entries(environment).forEach(([action, envObj]) => {
|
|
416
|
+
const isEmpty = Object.keys(envObj).length === 0;
|
|
417
|
+
if (isEmpty) {
|
|
418
|
+
output += ` ['${action}']: undefined;\n`;
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
output += ` ['${action}']: {\n`;
|
|
422
|
+
output += this.formatNestedObject(envObj, 4);
|
|
423
|
+
output += ' };\n';
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
output += '}\n';
|
|
427
|
+
// complex
|
|
329
428
|
return output;
|
|
330
429
|
}
|
|
331
430
|
/**
|
|
@@ -369,12 +468,12 @@ class AbilityExplain {
|
|
|
369
468
|
}
|
|
370
469
|
toString(indent = 0) {
|
|
371
470
|
const pad = ' '.repeat(indent);
|
|
372
|
-
const mark = this.match
|
|
471
|
+
const mark = this.match === AbilityMatch.match ? '✓' : '✗';
|
|
373
472
|
let out = '';
|
|
374
473
|
if (this.type === 'policy') {
|
|
375
474
|
out += '\n';
|
|
376
475
|
}
|
|
377
|
-
out += `${pad}${mark} ${this.type} «${this.name}» is ${this.match
|
|
476
|
+
out += `${pad}${mark} ${this.type} «${this.name}» is ${this.match}`;
|
|
378
477
|
this.children.forEach(child => {
|
|
379
478
|
out += '\n' + child.toString(indent + 1);
|
|
380
479
|
});
|
|
@@ -405,7 +504,7 @@ class AbilityExplainPolicy extends AbilityExplain {
|
|
|
405
504
|
const children = policy.ruleSet.map(ruleSet => new AbilityExplainRuleSet(ruleSet));
|
|
406
505
|
super({
|
|
407
506
|
type: 'policy',
|
|
408
|
-
name: policy.name,
|
|
507
|
+
name: policy.priority > -1 ? `@priority ${policy.priority} ${policy.name}` : policy.name,
|
|
409
508
|
match: policy.matchState,
|
|
410
509
|
}, children);
|
|
411
510
|
}
|
|
@@ -432,6 +531,7 @@ class AbilityPolicy {
|
|
|
432
531
|
* Policy name
|
|
433
532
|
*/
|
|
434
533
|
name;
|
|
534
|
+
description;
|
|
435
535
|
/**
|
|
436
536
|
* Policy ID
|
|
437
537
|
*/
|
|
@@ -441,13 +541,20 @@ class AbilityPolicy {
|
|
|
441
541
|
* will select only those from all passed policies that fall under the specified permission key.
|
|
442
542
|
*/
|
|
443
543
|
permission;
|
|
544
|
+
priority = -1;
|
|
545
|
+
disabled;
|
|
546
|
+
tags;
|
|
444
547
|
constructor(params) {
|
|
445
|
-
const { name, id, permission, effect, compareMethod = AbilityCompare.and } = params;
|
|
446
|
-
this.
|
|
447
|
-
this.
|
|
548
|
+
const { name, description, id, permission, effect, compareMethod = AbilityCompare.and, priority, disabled, tags, } = params;
|
|
549
|
+
this.id = id || `policy:${effect}:${permission}`;
|
|
550
|
+
this.name = name || this.id;
|
|
448
551
|
this.permission = permission;
|
|
552
|
+
this.description = description;
|
|
449
553
|
this.effect = effect;
|
|
450
554
|
this.compareMethod = compareMethod;
|
|
555
|
+
this.priority = typeof priority === 'number' ? priority : -1;
|
|
556
|
+
this.disabled = typeof disabled === 'boolean' ? disabled : false;
|
|
557
|
+
this.tags = (tags || []);
|
|
451
558
|
}
|
|
452
559
|
/**
|
|
453
560
|
* Add rule set to the policy
|
|
@@ -467,6 +574,18 @@ class AbilityPolicy {
|
|
|
467
574
|
}
|
|
468
575
|
return this;
|
|
469
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* Extract all rules of all ruleSets of this policy
|
|
579
|
+
*/
|
|
580
|
+
extractRules() {
|
|
581
|
+
const rules = [];
|
|
582
|
+
for (const ruleSet of this.ruleSet) {
|
|
583
|
+
for (const rule of ruleSet.rules) {
|
|
584
|
+
rules.push(rule);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return rules;
|
|
588
|
+
}
|
|
470
589
|
/**
|
|
471
590
|
* Check if the policy is matched
|
|
472
591
|
* @param resource - The resource to check
|
|
@@ -474,31 +593,57 @@ class AbilityPolicy {
|
|
|
474
593
|
*/
|
|
475
594
|
check(resource, environment) {
|
|
476
595
|
this.matchState = AbilityMatch.mismatch;
|
|
596
|
+
if (this.disabled) {
|
|
597
|
+
this.matchState = AbilityMatch.disabled;
|
|
598
|
+
return this.matchState;
|
|
599
|
+
}
|
|
477
600
|
if (!this.ruleSet.length) {
|
|
478
601
|
return this.matchState;
|
|
479
602
|
}
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
if (
|
|
485
|
-
|
|
603
|
+
const normalGroups = this.ruleSet.filter(g => !g.isExcept);
|
|
604
|
+
const exceptGroups = this.ruleSet.filter(g => g.isExcept);
|
|
605
|
+
const normalStates = [];
|
|
606
|
+
for (const group of normalGroups) {
|
|
607
|
+
if (group.disabled) {
|
|
608
|
+
continue;
|
|
486
609
|
}
|
|
487
|
-
|
|
488
|
-
|
|
610
|
+
const state = group.check(resource, environment);
|
|
611
|
+
normalStates.push(state);
|
|
612
|
+
if (AbilityCompare.and === this.compareMethod && AbilityMatch.mismatch === state) {
|
|
613
|
+
this.matchState = AbilityMatch.mismatch;
|
|
489
614
|
return this.matchState;
|
|
490
615
|
}
|
|
491
|
-
|
|
492
|
-
if (AbilityCompare.and.isEqual(this.compareMethod)) {
|
|
493
|
-
if (rulesetCheckStates.every(s => AbilityMatch.match.isEqual(s))) {
|
|
616
|
+
if (AbilityCompare.or === this.compareMethod && AbilityMatch.match === state) {
|
|
494
617
|
this.matchState = AbilityMatch.match;
|
|
618
|
+
// break to check except-rule sets
|
|
619
|
+
break;
|
|
495
620
|
}
|
|
496
621
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
622
|
+
// 3. Simple rule sets
|
|
623
|
+
let normalMatch = false;
|
|
624
|
+
if (AbilityCompare.and === this.compareMethod) {
|
|
625
|
+
normalMatch = normalStates.every(s => AbilityMatch.match === s);
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
normalMatch = normalStates.some(s => AbilityMatch.match === s);
|
|
629
|
+
}
|
|
630
|
+
if (!normalMatch) {
|
|
631
|
+
this.matchState = AbilityMatch.mismatch;
|
|
632
|
+
return this.matchState;
|
|
633
|
+
}
|
|
634
|
+
// 4. except-rule sets
|
|
635
|
+
for (const group of exceptGroups) {
|
|
636
|
+
if (group.disabled) {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const state = group.check(resource, environment);
|
|
640
|
+
if (AbilityMatch.match === state) {
|
|
641
|
+
this.matchState = AbilityMatch.exceptMismatch;
|
|
642
|
+
return this.matchState;
|
|
500
643
|
}
|
|
501
644
|
}
|
|
645
|
+
// 5. match
|
|
646
|
+
this.matchState = AbilityMatch.match;
|
|
502
647
|
return this.matchState;
|
|
503
648
|
}
|
|
504
649
|
explain() {
|
|
@@ -511,6 +656,8 @@ class AbilityPolicy {
|
|
|
511
656
|
const policy = new AbilityPolicy({
|
|
512
657
|
id: props.id ?? this.id,
|
|
513
658
|
name: props.name ?? this.name,
|
|
659
|
+
description: props.description ?? this.description,
|
|
660
|
+
priority: typeof props.priority !== 'undefined' ? props.priority : this.priority,
|
|
514
661
|
permission: props.permission ?? this.permission,
|
|
515
662
|
effect: props.effect ?? this.effect,
|
|
516
663
|
compareMethod: props.compareMethod ?? this.compareMethod,
|
|
@@ -523,24 +670,20 @@ class AbilityPolicy {
|
|
|
523
670
|
}
|
|
524
671
|
}
|
|
525
672
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
static permit = new AbilityPolicyEffect('permit');
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
class AbilityResultState extends AbilityCode {
|
|
532
|
-
static allow = new AbilityResultState('allow');
|
|
533
|
-
static deny = new AbilityResultState('deny');
|
|
534
|
-
static neutral = new AbilityResultState('neutral');
|
|
673
|
+
function brand$1(code) {
|
|
674
|
+
return code;
|
|
535
675
|
}
|
|
676
|
+
const AbilityPolicyEffect = {
|
|
677
|
+
deny: brand$1('deny'),
|
|
678
|
+
permit: brand$1('permit'),
|
|
679
|
+
};
|
|
536
680
|
|
|
537
681
|
class AbilityResult {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
this.policies = policies;
|
|
682
|
+
effect;
|
|
683
|
+
strategy;
|
|
684
|
+
constructor(effect, strategy) {
|
|
685
|
+
this.effect = effect;
|
|
686
|
+
this.strategy = strategy;
|
|
544
687
|
}
|
|
545
688
|
/**
|
|
546
689
|
* Returns a list of explanations for each policy involved in the ability evaluation.
|
|
@@ -549,53 +692,37 @@ class AbilityResult {
|
|
|
549
692
|
* Useful for debugging, logging, or building UI tools that visualize permission logic.
|
|
550
693
|
*/
|
|
551
694
|
explain() {
|
|
552
|
-
return this.policies.map(policy => {
|
|
695
|
+
return this.strategy.policies.map(policy => {
|
|
553
696
|
return new AbilityExplainPolicy(policy);
|
|
554
697
|
});
|
|
555
698
|
}
|
|
556
|
-
getLastMatchedPolicy() {
|
|
557
|
-
for (let i = this.policies.length - 1; i >= 0; i--) {
|
|
558
|
-
if (this.policies[i].matchState.isEqual(AbilityMatch.match)) {
|
|
559
|
-
return this.policies[i];
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
return null;
|
|
563
|
-
}
|
|
564
|
-
getFinalState() {
|
|
565
|
-
let state = AbilityResultState.neutral;
|
|
566
|
-
for (const p of this.policies) {
|
|
567
|
-
if (p.matchState.isEqual(AbilityMatch.match)) {
|
|
568
|
-
if (p.effect.isEqual(AbilityPolicyEffect.permit)) {
|
|
569
|
-
state = AbilityResultState.allow;
|
|
570
|
-
}
|
|
571
|
-
else if (p.effect.isEqual(AbilityPolicyEffect.deny)) {
|
|
572
|
-
state = AbilityResultState.deny;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
else if (p.matchState.isEqual(AbilityMatch.mismatch)) {
|
|
576
|
-
state = AbilityResultState.neutral;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
return state;
|
|
580
|
-
}
|
|
581
699
|
isAllowed() {
|
|
582
|
-
return this.
|
|
700
|
+
return this.strategy.isAllowed();
|
|
583
701
|
}
|
|
584
702
|
isDenied() {
|
|
585
|
-
return this.
|
|
703
|
+
return this.strategy.isDenied();
|
|
586
704
|
}
|
|
587
705
|
}
|
|
588
706
|
|
|
589
707
|
class AbilityResolver {
|
|
590
|
-
|
|
708
|
+
StrategyClass;
|
|
709
|
+
policyEntries;
|
|
591
710
|
constructor(
|
|
592
711
|
/**
|
|
593
712
|
* `Important!` The incorrect Resources type was intentionally passed to AbilityPolicy so that TypeScript could suggest the name of the permission and the structure of its resource in the parse method.
|
|
594
713
|
*/
|
|
595
|
-
policyOrListOfPolicies) {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
714
|
+
policyOrListOfPolicies, strategy, options = {}) {
|
|
715
|
+
const policies = this.toArray(policyOrListOfPolicies);
|
|
716
|
+
const filtered = options.tags
|
|
717
|
+
? policies.filter(p => p.tags.some(tag => options.tags.includes(tag)))
|
|
718
|
+
: policies;
|
|
719
|
+
const sorted = [...filtered].sort((a, b) => b.priority - a.priority);
|
|
720
|
+
this.policyEntries = sorted.map(policy => ({
|
|
721
|
+
policy,
|
|
722
|
+
normalizedPermission: this.normalizePermission(policy.permission),
|
|
723
|
+
segments: this.normalizePermission(policy.permission).split('.'),
|
|
724
|
+
}));
|
|
725
|
+
this.StrategyClass = strategy;
|
|
599
726
|
}
|
|
600
727
|
/**
|
|
601
728
|
* Resolve policy for the resource and permission key
|
|
@@ -605,24 +732,30 @@ class AbilityResolver {
|
|
|
605
732
|
* @param environment
|
|
606
733
|
*/
|
|
607
734
|
resolve(permission, resource, environment) {
|
|
608
|
-
const
|
|
735
|
+
const inputNormalized = this.normalizePermission(String(permission));
|
|
736
|
+
const inputSegments = inputNormalized.split('.');
|
|
737
|
+
const filteredPolicies = this.policyEntries
|
|
738
|
+
.filter(entry => AbilityResolver.matchPermissions(entry.segments, inputSegments))
|
|
739
|
+
.map(entry => entry.policy);
|
|
740
|
+
// 2. check the policies
|
|
609
741
|
for (const policy of filteredPolicies) {
|
|
742
|
+
if (policy.disabled) {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
610
745
|
const policyMatchState = policy.check(resource, environment);
|
|
611
746
|
if (policyMatchState === AbilityMatch.pending) {
|
|
612
747
|
throw new AbilityError(`The policy "${policy.name}" is still in a pending state. Make sure to call "check" to evaluate the policy before resolving permissions.`);
|
|
613
748
|
}
|
|
614
749
|
}
|
|
615
|
-
|
|
750
|
+
// 3. Use strategy
|
|
751
|
+
const strategy = new this.StrategyClass(filteredPolicies);
|
|
752
|
+
const effect = strategy.evaluate();
|
|
753
|
+
return new AbilityResult(effect, strategy);
|
|
616
754
|
}
|
|
617
755
|
enforce(permission, resource, environment) {
|
|
618
756
|
const result = this.resolve(permission, resource, environment);
|
|
619
757
|
if (result.isDenied()) {
|
|
620
|
-
|
|
621
|
-
if (lastPolicy) {
|
|
622
|
-
throw new AbilityError(`Permission denied by policy "${lastPolicy.name.toString()}"`);
|
|
623
|
-
}
|
|
624
|
-
// No policy matched → implicit deny
|
|
625
|
-
throw new AbilityError(`Permission denied: no matching policy found (implicit deny)`);
|
|
758
|
+
throw new AbilityError(`Permission denied`);
|
|
626
759
|
}
|
|
627
760
|
}
|
|
628
761
|
/**
|
|
@@ -638,6 +771,32 @@ class AbilityResolver {
|
|
|
638
771
|
return chunk === '*' || longer[i] === '*' || chunk === longer[i];
|
|
639
772
|
});
|
|
640
773
|
}
|
|
774
|
+
toArray(value) {
|
|
775
|
+
return [...(Array.isArray(value) ? value : [value])];
|
|
776
|
+
}
|
|
777
|
+
normalizePermission(permission) {
|
|
778
|
+
return permission
|
|
779
|
+
.trim()
|
|
780
|
+
.replace(/^permission\./, '') // remove prefix
|
|
781
|
+
.replace(/\.+/g, '.') // collapse multiple dots
|
|
782
|
+
.toLowerCase(); // optional: make case-insensitive
|
|
783
|
+
}
|
|
784
|
+
static matchPermissions(policySegments, inputSegments) {
|
|
785
|
+
const maxLen = Math.max(policySegments.length, inputSegments.length);
|
|
786
|
+
for (let i = 0; i < maxLen; i++) {
|
|
787
|
+
const pSeg = policySegments[i];
|
|
788
|
+
const iSeg = inputSegments[i];
|
|
789
|
+
if (pSeg === undefined)
|
|
790
|
+
return false; // policy короче – не матчит
|
|
791
|
+
if (pSeg === '*')
|
|
792
|
+
continue; // '*' матчит любой сегмент
|
|
793
|
+
if (iSeg === undefined)
|
|
794
|
+
return false; // входной permission короче
|
|
795
|
+
if (pSeg !== iSeg)
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
641
800
|
}
|
|
642
801
|
|
|
643
802
|
/**
|
|
@@ -654,8 +813,10 @@ class AbilityRule {
|
|
|
654
813
|
resource;
|
|
655
814
|
condition;
|
|
656
815
|
name;
|
|
816
|
+
description;
|
|
657
817
|
id;
|
|
658
818
|
state = AbilityMatch.pending;
|
|
819
|
+
disabled;
|
|
659
820
|
/**
|
|
660
821
|
* Creates an instance of AbilityRule.
|
|
661
822
|
* @param {string} params.id - The unique identifier of the rule.
|
|
@@ -663,33 +824,37 @@ class AbilityRule {
|
|
|
663
824
|
* @param {AbilityCondition} params.condition - The condition to evaluate.
|
|
664
825
|
* @param {string} params.subject - The subject of the rule.
|
|
665
826
|
* @param {string} params.resource - The resource to compare against.
|
|
827
|
+
* @param {boolean} params.disabled - Disabling flag.
|
|
666
828
|
* @param params
|
|
667
829
|
*/
|
|
668
830
|
constructor(params) {
|
|
669
|
-
const { id, name, subject, resource, condition } = params;
|
|
670
|
-
this.name = name ||
|
|
831
|
+
const { id, name, subject, resource, condition, disabled, description } = params;
|
|
832
|
+
this.name = name || `rule:${JSON.stringify(subject)}:${condition}:${JSON.stringify(resource)}`;
|
|
671
833
|
this.id = id || this.name;
|
|
834
|
+
this.description = description;
|
|
835
|
+
this.disabled = typeof disabled === 'boolean' ? disabled : false;
|
|
672
836
|
this.subject = subject;
|
|
673
837
|
this.resource = resource;
|
|
674
838
|
this.condition = condition;
|
|
839
|
+
this.state = this.disabled ? AbilityMatch.disabled : this.state;
|
|
675
840
|
}
|
|
676
|
-
isPrimitive(v) {
|
|
841
|
+
static isPrimitive(v) {
|
|
677
842
|
return typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null;
|
|
678
843
|
}
|
|
679
|
-
isNumber(v) {
|
|
844
|
+
static isNumber(v) {
|
|
680
845
|
return typeof v === 'number';
|
|
681
846
|
}
|
|
682
|
-
isString(v) {
|
|
847
|
+
static isString(v) {
|
|
683
848
|
return typeof v === 'string';
|
|
684
849
|
}
|
|
685
|
-
valueLen = (v) => this.isString(v) || Array.isArray(v) ? v.length : null;
|
|
686
|
-
operatorHandlers = {
|
|
687
|
-
[AbilityCondition.always
|
|
688
|
-
[AbilityCondition.never
|
|
689
|
-
[AbilityCondition.equals
|
|
690
|
-
[AbilityCondition.not_equals
|
|
691
|
-
[AbilityCondition.contains
|
|
692
|
-
if (Array.isArray(a) &&
|
|
850
|
+
static valueLen = (v) => this.isString(v) || Array.isArray(v) ? v.length : null;
|
|
851
|
+
static operatorHandlers = {
|
|
852
|
+
[toLiteral(AbilityCondition.always)]: () => true,
|
|
853
|
+
[toLiteral(AbilityCondition.never)]: () => false,
|
|
854
|
+
[toLiteral(AbilityCondition.equals)]: (a, b) => a === b,
|
|
855
|
+
[toLiteral(AbilityCondition.not_equals)]: (a, b) => a !== b,
|
|
856
|
+
[toLiteral(AbilityCondition.contains)]: (a, b) => {
|
|
857
|
+
if (Array.isArray(a) && AbilityRule.isPrimitive(b)) {
|
|
693
858
|
return a.includes(b);
|
|
694
859
|
}
|
|
695
860
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
@@ -697,8 +862,8 @@ class AbilityRule {
|
|
|
697
862
|
}
|
|
698
863
|
return false;
|
|
699
864
|
},
|
|
700
|
-
[AbilityCondition.not_contains
|
|
701
|
-
if (Array.isArray(a) &&
|
|
865
|
+
[toLiteral(AbilityCondition.not_contains)]: (a, b) => {
|
|
866
|
+
if (Array.isArray(a) && AbilityRule.isPrimitive(b)) {
|
|
702
867
|
return !a.includes(b);
|
|
703
868
|
}
|
|
704
869
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
@@ -706,8 +871,8 @@ class AbilityRule {
|
|
|
706
871
|
}
|
|
707
872
|
return false;
|
|
708
873
|
},
|
|
709
|
-
[AbilityCondition.in
|
|
710
|
-
if (
|
|
874
|
+
[toLiteral(AbilityCondition.in)]: (a, b) => {
|
|
875
|
+
if (AbilityRule.isPrimitive(a) && Array.isArray(b)) {
|
|
711
876
|
return b.includes(a);
|
|
712
877
|
}
|
|
713
878
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
@@ -715,8 +880,8 @@ class AbilityRule {
|
|
|
715
880
|
}
|
|
716
881
|
return false;
|
|
717
882
|
},
|
|
718
|
-
[AbilityCondition.not_in
|
|
719
|
-
if (
|
|
883
|
+
[toLiteral(AbilityCondition.not_in)]: (a, b) => {
|
|
884
|
+
if (AbilityRule.isPrimitive(a) && Array.isArray(b)) {
|
|
720
885
|
return !b.includes(a);
|
|
721
886
|
}
|
|
722
887
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
@@ -724,55 +889,55 @@ class AbilityRule {
|
|
|
724
889
|
}
|
|
725
890
|
return false;
|
|
726
891
|
},
|
|
727
|
-
[AbilityCondition.greater_than
|
|
728
|
-
return
|
|
892
|
+
[toLiteral(AbilityCondition.greater_than)]: (a, b) => {
|
|
893
|
+
return AbilityRule.isNumber(a) && AbilityRule.isNumber(b) ? a > b : false;
|
|
729
894
|
},
|
|
730
|
-
[AbilityCondition.less_than
|
|
731
|
-
return
|
|
895
|
+
[toLiteral(AbilityCondition.less_than)]: (a, b) => {
|
|
896
|
+
return AbilityRule.isNumber(a) && AbilityRule.isNumber(b) ? a < b : false;
|
|
732
897
|
},
|
|
733
|
-
[AbilityCondition.greater_or_equal
|
|
734
|
-
return
|
|
898
|
+
[toLiteral(AbilityCondition.greater_or_equal)]: (a, b) => {
|
|
899
|
+
return AbilityRule.isNumber(a) && AbilityRule.isNumber(b) ? a >= b : false;
|
|
735
900
|
},
|
|
736
|
-
[AbilityCondition.less_or_equal
|
|
737
|
-
return
|
|
901
|
+
[toLiteral(AbilityCondition.less_or_equal)]: (a, b) => {
|
|
902
|
+
return AbilityRule.isNumber(a) && AbilityRule.isNumber(b) ? a <= b : false;
|
|
738
903
|
},
|
|
739
|
-
[AbilityCondition.length_greater_than
|
|
740
|
-
const alen =
|
|
904
|
+
[toLiteral(AbilityCondition.length_greater_than)]: (a, b) => {
|
|
905
|
+
const alen = AbilityRule.valueLen(a);
|
|
741
906
|
if (alen === null) {
|
|
742
907
|
return false;
|
|
743
908
|
}
|
|
744
|
-
if (
|
|
909
|
+
if (AbilityRule.isNumber(b)) {
|
|
745
910
|
return alen > b;
|
|
746
911
|
}
|
|
747
|
-
const bLen =
|
|
912
|
+
const bLen = AbilityRule.valueLen(b);
|
|
748
913
|
if (bLen !== null) {
|
|
749
914
|
return alen > bLen;
|
|
750
915
|
}
|
|
751
916
|
return false;
|
|
752
917
|
},
|
|
753
|
-
[AbilityCondition.length_less_than
|
|
754
|
-
const alen =
|
|
918
|
+
[toLiteral(AbilityCondition.length_less_than)]: (a, b) => {
|
|
919
|
+
const alen = AbilityRule.valueLen(a);
|
|
755
920
|
if (alen === null) {
|
|
756
921
|
return false;
|
|
757
922
|
}
|
|
758
|
-
if (
|
|
923
|
+
if (AbilityRule.isNumber(b)) {
|
|
759
924
|
return alen < b;
|
|
760
925
|
}
|
|
761
|
-
const bLen =
|
|
926
|
+
const bLen = AbilityRule.valueLen(b);
|
|
762
927
|
if (bLen !== null) {
|
|
763
928
|
return alen < bLen;
|
|
764
929
|
}
|
|
765
930
|
return false;
|
|
766
931
|
},
|
|
767
|
-
[AbilityCondition.length_equals
|
|
768
|
-
const alen =
|
|
932
|
+
[toLiteral(AbilityCondition.length_equals)]: (a, b) => {
|
|
933
|
+
const alen = AbilityRule.valueLen(a);
|
|
769
934
|
if (alen === null) {
|
|
770
935
|
return false;
|
|
771
936
|
}
|
|
772
|
-
if (
|
|
937
|
+
if (AbilityRule.isNumber(b)) {
|
|
773
938
|
return alen === b;
|
|
774
939
|
}
|
|
775
|
-
const bLen =
|
|
940
|
+
const bLen = AbilityRule.valueLen(b);
|
|
776
941
|
if (bLen !== null) {
|
|
777
942
|
return alen === bLen;
|
|
778
943
|
}
|
|
@@ -785,8 +950,12 @@ class AbilityRule {
|
|
|
785
950
|
* @param environment
|
|
786
951
|
*/
|
|
787
952
|
check(resource, environment) {
|
|
953
|
+
if (this.disabled) {
|
|
954
|
+
this.state = AbilityMatch.disabled;
|
|
955
|
+
return this.state;
|
|
956
|
+
}
|
|
788
957
|
const [subjectValue, resourceValue] = this.extractValues(resource, environment);
|
|
789
|
-
const handler =
|
|
958
|
+
const handler = AbilityRule.operatorHandlers[toLiteral(this.condition)];
|
|
790
959
|
const result = handler(subjectValue, resourceValue);
|
|
791
960
|
this.state = result ? AbilityMatch.match : AbilityMatch.mismatch;
|
|
792
961
|
return this.state;
|
|
@@ -833,41 +1002,62 @@ class AbilityRule {
|
|
|
833
1002
|
}
|
|
834
1003
|
return [subjectValue, resourceValue];
|
|
835
1004
|
}
|
|
1005
|
+
static _pathCache = new Map();
|
|
1006
|
+
static _parsePath(desc) {
|
|
1007
|
+
const cached = AbilityRule._pathCache.get(desc);
|
|
1008
|
+
if (cached)
|
|
1009
|
+
return cached;
|
|
1010
|
+
const parts = desc.split('.');
|
|
1011
|
+
const segments = [];
|
|
1012
|
+
for (const part of parts) {
|
|
1013
|
+
const bracketIdx = part.indexOf('[');
|
|
1014
|
+
if (bracketIdx !== -1) {
|
|
1015
|
+
// формат: "prop[index]" (индекс может быть только числом)
|
|
1016
|
+
const prop = part.slice(0, bracketIdx);
|
|
1017
|
+
const indexStr = part.slice(bracketIdx + 1, -1);
|
|
1018
|
+
const index = Number(indexStr);
|
|
1019
|
+
segments.push({ prop, index });
|
|
1020
|
+
}
|
|
1021
|
+
else {
|
|
1022
|
+
segments.push(part);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
AbilityRule._pathCache.set(desc, segments);
|
|
1026
|
+
return segments;
|
|
1027
|
+
}
|
|
836
1028
|
/**
|
|
837
1029
|
* Get the value of the object by dot notation
|
|
838
1030
|
* @param resource - The object to get the value from
|
|
839
1031
|
* @param desc - The dot notation string
|
|
840
1032
|
*/
|
|
841
1033
|
getDotNotationValue(resource, desc) {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
}
|
|
854
|
-
else {
|
|
855
|
-
resource = undefined;
|
|
856
|
-
}
|
|
1034
|
+
if (resource == null) {
|
|
1035
|
+
return undefined;
|
|
1036
|
+
}
|
|
1037
|
+
const segments = AbilityRule._parsePath(desc);
|
|
1038
|
+
let current = resource;
|
|
1039
|
+
for (const seg of segments) {
|
|
1040
|
+
if (current == null) {
|
|
1041
|
+
return undefined;
|
|
1042
|
+
}
|
|
1043
|
+
if (typeof seg === 'string') {
|
|
1044
|
+
current = current[seg];
|
|
857
1045
|
}
|
|
858
1046
|
else {
|
|
859
|
-
|
|
1047
|
+
const arr = current[seg.prop];
|
|
1048
|
+
current = Array.isArray(arr) ? arr[seg.index] : undefined;
|
|
860
1049
|
}
|
|
861
1050
|
}
|
|
862
|
-
return
|
|
1051
|
+
return current;
|
|
863
1052
|
}
|
|
864
1053
|
toString() {
|
|
865
|
-
return `AbilityRule: ${this.name} condition: ${this.condition
|
|
1054
|
+
return `AbilityRule: ${this.name} condition: ${toLiteral(this.condition)} subject: "${this.subject?.toString()}" resource: "${this.resource?.toString()}"`;
|
|
866
1055
|
}
|
|
867
1056
|
copyWith(props) {
|
|
868
1057
|
return new AbilityRule({
|
|
869
1058
|
id: props.id ?? this.id,
|
|
870
1059
|
name: props.name ?? this.name,
|
|
1060
|
+
description: props.description ?? this.description,
|
|
871
1061
|
subject: props.subject ?? this.subject,
|
|
872
1062
|
resource: props.resource ?? this.resource,
|
|
873
1063
|
condition: props.condition ?? this.condition,
|
|
@@ -969,15 +1159,22 @@ class AbilityRuleSet {
|
|
|
969
1159
|
* Group name
|
|
970
1160
|
*/
|
|
971
1161
|
name;
|
|
1162
|
+
description;
|
|
972
1163
|
/**
|
|
973
1164
|
* Group ID
|
|
974
1165
|
*/
|
|
975
1166
|
id;
|
|
1167
|
+
isExcept = false;
|
|
1168
|
+
disabled;
|
|
976
1169
|
constructor(params) {
|
|
977
|
-
const { name, id, compareMethod } = params;
|
|
978
|
-
this.name = name ||
|
|
1170
|
+
const { name, id, compareMethod, isExcept, disabled, description } = params;
|
|
1171
|
+
this.name = name || `ruleset:${compareMethod}`;
|
|
979
1172
|
this.id = id || this.name;
|
|
1173
|
+
this.description = description;
|
|
980
1174
|
this.compareMethod = compareMethod;
|
|
1175
|
+
this.isExcept = isExcept;
|
|
1176
|
+
this.disabled = typeof disabled === 'boolean' ? disabled : false;
|
|
1177
|
+
this.state = this.disabled ? AbilityMatch.disabled : this.state;
|
|
981
1178
|
}
|
|
982
1179
|
addRule(rule) {
|
|
983
1180
|
this.rules.push(rule);
|
|
@@ -989,40 +1186,48 @@ class AbilityRuleSet {
|
|
|
989
1186
|
}
|
|
990
1187
|
check(resources, environment) {
|
|
991
1188
|
this.state = AbilityMatch.mismatch;
|
|
1189
|
+
if (this.disabled) {
|
|
1190
|
+
this.state = AbilityMatch.disabled;
|
|
1191
|
+
return this.state;
|
|
1192
|
+
}
|
|
992
1193
|
if (!this.rules.length) {
|
|
993
1194
|
return this.state;
|
|
994
1195
|
}
|
|
995
1196
|
const ruleCheckStates = [];
|
|
996
1197
|
for (const rule of this.rules) {
|
|
1198
|
+
if (rule.disabled) {
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
997
1201
|
const state = rule.check(resources, environment);
|
|
998
1202
|
ruleCheckStates.push(state);
|
|
999
|
-
if (AbilityCompare.and
|
|
1203
|
+
if (AbilityCompare.and === this.compareMethod && AbilityMatch.mismatch === state) {
|
|
1000
1204
|
return this.state; // mismatch
|
|
1001
1205
|
}
|
|
1002
|
-
if (AbilityCompare.or
|
|
1206
|
+
if (AbilityCompare.or === this.compareMethod && AbilityMatch.match === state) {
|
|
1003
1207
|
this.state = AbilityMatch.match;
|
|
1004
1208
|
return this.state;
|
|
1005
1209
|
}
|
|
1006
1210
|
}
|
|
1007
|
-
if (AbilityCompare.and
|
|
1008
|
-
if (ruleCheckStates.every(s => AbilityMatch.match
|
|
1211
|
+
if (AbilityCompare.and === this.compareMethod) {
|
|
1212
|
+
if (ruleCheckStates.every(s => AbilityMatch.match === s)) {
|
|
1009
1213
|
this.state = AbilityMatch.match;
|
|
1010
1214
|
}
|
|
1011
1215
|
}
|
|
1012
|
-
if (AbilityCompare.or
|
|
1013
|
-
if (ruleCheckStates.some(s => AbilityMatch.match
|
|
1216
|
+
if (AbilityCompare.or === this.compareMethod) {
|
|
1217
|
+
if (ruleCheckStates.some(s => AbilityMatch.match === s)) {
|
|
1014
1218
|
this.state = AbilityMatch.match;
|
|
1015
1219
|
}
|
|
1016
1220
|
}
|
|
1017
1221
|
return this.state;
|
|
1018
1222
|
}
|
|
1019
1223
|
toString() {
|
|
1020
|
-
return `AbilityRuleSet: ${this.name} compareMethod: ${this.compareMethod
|
|
1224
|
+
return `AbilityRuleSet: ${this.name} compareMethod: ${this.compareMethod}, rules: ${this.rules.map(rule => rule.toString()).join('\n')}`;
|
|
1021
1225
|
}
|
|
1022
1226
|
copyWith(props) {
|
|
1023
1227
|
const next = new AbilityRuleSet({
|
|
1024
1228
|
id: props.id ?? this.id,
|
|
1025
1229
|
name: props.name ?? this.name,
|
|
1230
|
+
description: props.description ?? this.description,
|
|
1026
1231
|
compareMethod: props.compareMethod ?? this.compareMethod,
|
|
1027
1232
|
});
|
|
1028
1233
|
const nextRules = props.rules ?? this.rules;
|
|
@@ -1053,37 +1258,42 @@ class AbilityJSONParser {
|
|
|
1053
1258
|
return configs.map(config => AbilityJSONParser.parsePolicy(config));
|
|
1054
1259
|
}
|
|
1055
1260
|
static parsePolicy(config) {
|
|
1056
|
-
const { id, name, ruleSet, compareMethod, permission, effect } = config;
|
|
1261
|
+
const { id, name, ruleSet, compareMethod, permission, effect, priority, disabled, tags } = config;
|
|
1057
1262
|
// Create the empty policy
|
|
1058
1263
|
const policy = new AbilityPolicy({
|
|
1059
1264
|
name,
|
|
1060
1265
|
id,
|
|
1061
1266
|
permission: permission,
|
|
1062
|
-
|
|
1267
|
+
priority: priority,
|
|
1268
|
+
effect: effect,
|
|
1269
|
+
disabled,
|
|
1270
|
+
tags,
|
|
1063
1271
|
});
|
|
1064
|
-
policy.compareMethod =
|
|
1272
|
+
policy.compareMethod = compareMethod;
|
|
1065
1273
|
ruleSet.forEach(ruleSetConfig => {
|
|
1066
1274
|
policy.addRuleSet(AbilityJSONParser.parseRuleSet(ruleSetConfig));
|
|
1067
1275
|
});
|
|
1068
1276
|
return policy;
|
|
1069
1277
|
}
|
|
1070
1278
|
static parseRule(config) {
|
|
1071
|
-
const { id, name, subject, resource, condition } = config;
|
|
1279
|
+
const { id, name, subject, resource, condition, disabled } = config;
|
|
1072
1280
|
return new AbilityRule({
|
|
1073
1281
|
id,
|
|
1074
1282
|
name,
|
|
1075
1283
|
subject,
|
|
1076
1284
|
resource,
|
|
1077
|
-
|
|
1285
|
+
disabled,
|
|
1286
|
+
condition,
|
|
1078
1287
|
});
|
|
1079
1288
|
}
|
|
1080
1289
|
/**
|
|
1081
1290
|
* Parse the config JSON format to Group class instance
|
|
1082
1291
|
*/
|
|
1083
1292
|
static parseRuleSet(config) {
|
|
1084
|
-
const { id, name, rules, compareMethod } = config;
|
|
1293
|
+
const { id, name, rules, compareMethod, disabled } = config;
|
|
1085
1294
|
const ruleSet = new AbilityRuleSet({
|
|
1086
|
-
|
|
1295
|
+
disabled,
|
|
1296
|
+
compareMethod: compareMethod,
|
|
1087
1297
|
name,
|
|
1088
1298
|
id,
|
|
1089
1299
|
});
|
|
@@ -1100,25 +1310,30 @@ class AbilityJSONParser {
|
|
|
1100
1310
|
name: rule.name,
|
|
1101
1311
|
subject: rule.subject,
|
|
1102
1312
|
resource: rule.resource,
|
|
1103
|
-
condition: rule.condition
|
|
1313
|
+
condition: rule.condition,
|
|
1314
|
+
disabled: rule.disabled,
|
|
1104
1315
|
};
|
|
1105
1316
|
}
|
|
1106
1317
|
static ruleSetToJSON(ruleSet) {
|
|
1107
1318
|
return {
|
|
1108
1319
|
id: ruleSet.id.toString(),
|
|
1109
1320
|
name: ruleSet.name.toString(),
|
|
1110
|
-
compareMethod: ruleSet.compareMethod
|
|
1321
|
+
compareMethod: ruleSet.compareMethod,
|
|
1111
1322
|
rules: ruleSet.rules.map(rule => AbilityJSONParser.ruleToJSON(rule)),
|
|
1323
|
+
disabled: ruleSet.disabled,
|
|
1112
1324
|
};
|
|
1113
1325
|
}
|
|
1114
1326
|
static policyToJSON(policy) {
|
|
1115
1327
|
return {
|
|
1116
1328
|
id: policy.id.toString(),
|
|
1117
1329
|
name: policy.name.toString(),
|
|
1118
|
-
compareMethod: policy.compareMethod
|
|
1330
|
+
compareMethod: policy.compareMethod,
|
|
1119
1331
|
ruleSet: policy.ruleSet.map(ruleSet => AbilityJSONParser.ruleSetToJSON(ruleSet)),
|
|
1120
1332
|
permission: policy.permission,
|
|
1121
|
-
effect: policy.effect
|
|
1333
|
+
effect: policy.effect,
|
|
1334
|
+
priority: policy.priority,
|
|
1335
|
+
disabled: policy.disabled,
|
|
1336
|
+
tags: policy.tags,
|
|
1122
1337
|
};
|
|
1123
1338
|
}
|
|
1124
1339
|
static toJSON(policies) {
|
|
@@ -1126,67 +1341,66 @@ class AbilityJSONParser {
|
|
|
1126
1341
|
}
|
|
1127
1342
|
}
|
|
1128
1343
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1344
|
+
function brand(code) {
|
|
1345
|
+
return code;
|
|
1346
|
+
}
|
|
1347
|
+
const TokenTypes = {
|
|
1348
|
+
EFFECT: brand('EFFECT'),
|
|
1349
|
+
IF: brand('IF'),
|
|
1350
|
+
PERMISSION: brand('PERMISSION'),
|
|
1351
|
+
IDENTIFIER: brand('IDENTIFIER'),
|
|
1352
|
+
COLON: brand('COLON'),
|
|
1353
|
+
COMMA: brand('COMMA'),
|
|
1354
|
+
DOT: brand('DOT'),
|
|
1355
|
+
LBRACKET: brand('LBRACKET'),
|
|
1356
|
+
RBRACKET: brand('RBRACKET'),
|
|
1357
|
+
ALL: brand('ALL'),
|
|
1358
|
+
ANY: brand('ANY'),
|
|
1359
|
+
OF: brand('OF'),
|
|
1360
|
+
EOF: brand('EOF'),
|
|
1361
|
+
COMMENT: brand('COMMENT'),
|
|
1362
|
+
EQ: brand('EQ'),
|
|
1363
|
+
CONTAINS: brand('CONTAINS'),
|
|
1364
|
+
IN: brand('IN'),
|
|
1365
|
+
NOT_IN: brand('NOT_IN'),
|
|
1366
|
+
NOT_CONTAINS: brand('NOT_CONTAINS'),
|
|
1367
|
+
GT: brand('GT'),
|
|
1368
|
+
GTE: brand('GTE'),
|
|
1369
|
+
LT: brand('LT'),
|
|
1370
|
+
LTE: brand('LTE'),
|
|
1371
|
+
NULL: brand('NULL'),
|
|
1372
|
+
EQ_NULL: brand('EQ_NULL'),
|
|
1373
|
+
NOT_EQ_NULL: brand('NOT_EQ_NULL'),
|
|
1374
|
+
NOT_EQ: brand('NOT_EQ'),
|
|
1375
|
+
LEN_GT: brand('LEN_GT'),
|
|
1376
|
+
LEN_LT: brand('LEN_LT'),
|
|
1377
|
+
LEN_EQ: brand('LEN_EQ'),
|
|
1378
|
+
ALWAYS: brand('ALWAYS'),
|
|
1379
|
+
NEVER: brand('NEVER'),
|
|
1380
|
+
EXCEPT: brand('EXCEPT'),
|
|
1381
|
+
ANNOTATION: brand('ANNOTATION'),
|
|
1382
|
+
STRING: brand('STRING'),
|
|
1383
|
+
NUMBER: brand('NUMBER'),
|
|
1384
|
+
BOOLEAN: brand('BOOLEAN'),
|
|
1385
|
+
SYMBOL: brand('SYMBOL'),
|
|
1386
|
+
KEYWORD: brand('KEYWORD'),
|
|
1387
|
+
ALIAS: brand('ALIAS'),
|
|
1388
|
+
UNKNOWN: brand('UNKNOWN'),
|
|
1389
|
+
};
|
|
1390
|
+
class AbilityDSLToken {
|
|
1391
|
+
type;
|
|
1392
|
+
value;
|
|
1393
|
+
line;
|
|
1394
|
+
column;
|
|
1140
1395
|
constructor(type, value, line, column) {
|
|
1141
|
-
|
|
1396
|
+
this.type = type;
|
|
1142
1397
|
this.value = value;
|
|
1143
1398
|
this.line = line;
|
|
1144
1399
|
this.column = column;
|
|
1145
1400
|
}
|
|
1146
|
-
/**
|
|
1147
|
-
* Returns a human-readable representation of the token, useful for debugging.
|
|
1148
|
-
* Example output: "AbilityDSLToken([EFFECT] permit"
|
|
1149
|
-
*/
|
|
1150
1401
|
toString() {
|
|
1151
|
-
return `AbilityDSLToken([${this.
|
|
1152
|
-
}
|
|
1153
|
-
static EFFECT = 'EFFECT';
|
|
1154
|
-
static IF = 'IF';
|
|
1155
|
-
static PERMISSION = 'PERMISSION';
|
|
1156
|
-
static IDENTIFIER = 'IDENTIFIER';
|
|
1157
|
-
static COLON = 'COLON';
|
|
1158
|
-
static COMMA = 'COMMA';
|
|
1159
|
-
static DOT = 'DOT';
|
|
1160
|
-
static LBRACKET = 'LBRACKET';
|
|
1161
|
-
static RBRACKET = 'RBRACKET';
|
|
1162
|
-
static ALL = 'ALL';
|
|
1163
|
-
static ANY = 'ANY';
|
|
1164
|
-
static OF = 'OF';
|
|
1165
|
-
static EOF = 'EOF';
|
|
1166
|
-
static COMMENT = 'COMMENT';
|
|
1167
|
-
static EQ = 'EQ';
|
|
1168
|
-
static CONTAINS = 'CONTAINS';
|
|
1169
|
-
static IN = 'IN';
|
|
1170
|
-
static NOT_IN = 'NOT_IN';
|
|
1171
|
-
static NOT_CONTAINS = 'NOT_CONTAINS';
|
|
1172
|
-
static GT = 'GT';
|
|
1173
|
-
static GTE = 'GTE';
|
|
1174
|
-
static LT = 'LT';
|
|
1175
|
-
static LTE = 'LTE';
|
|
1176
|
-
static NULL = 'NULL';
|
|
1177
|
-
static EQ_NULL = 'EQ_NULL';
|
|
1178
|
-
static NOT_EQ_NULL = 'NOT_EQ_NULL';
|
|
1179
|
-
static LEN_GT = 'LEN_GT';
|
|
1180
|
-
static LEN_LT = 'LEN_LT';
|
|
1181
|
-
static LEN_EQ = 'LEN_EQ';
|
|
1182
|
-
static NOT_EQ = 'NOT_EQ';
|
|
1183
|
-
static ALWAYS = 'ALWAYS';
|
|
1184
|
-
static NEVER = 'NEVER';
|
|
1185
|
-
static STRING = 'STRING';
|
|
1186
|
-
static NUMBER = 'NUMBER';
|
|
1187
|
-
static BOOLEAN = 'BOOLEAN';
|
|
1188
|
-
static SYMBOL = 'SYMBOL';
|
|
1189
|
-
static KEYWORD = 'KEYWORD';
|
|
1402
|
+
return `AbilityDSLToken([${this.type}] "${this.value}" at ${this.line}:${this.column})`;
|
|
1403
|
+
}
|
|
1190
1404
|
}
|
|
1191
1405
|
|
|
1192
1406
|
class AbilityDSLLexer {
|
|
@@ -1195,7 +1409,6 @@ class AbilityDSLLexer {
|
|
|
1195
1409
|
tokens = [];
|
|
1196
1410
|
line = 1;
|
|
1197
1411
|
column = 1;
|
|
1198
|
-
// Список ключевых слов
|
|
1199
1412
|
keywords = new Set([
|
|
1200
1413
|
'if',
|
|
1201
1414
|
'all',
|
|
@@ -1226,6 +1439,8 @@ class AbilityDSLLexer {
|
|
|
1226
1439
|
'than',
|
|
1227
1440
|
'always',
|
|
1228
1441
|
'never',
|
|
1442
|
+
'except',
|
|
1443
|
+
'alias',
|
|
1229
1444
|
]);
|
|
1230
1445
|
constructor(input) {
|
|
1231
1446
|
this.input = input;
|
|
@@ -1236,6 +1451,10 @@ class AbilityDSLLexer {
|
|
|
1236
1451
|
if (this.isAtEnd())
|
|
1237
1452
|
break;
|
|
1238
1453
|
const char = this.peek();
|
|
1454
|
+
if (char === '@') {
|
|
1455
|
+
this.tokens.push(this.readAnnotation());
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1239
1458
|
if (char === '#') {
|
|
1240
1459
|
this.tokens.push(this.readComment());
|
|
1241
1460
|
continue;
|
|
@@ -1258,7 +1477,7 @@ class AbilityDSLLexer {
|
|
|
1258
1477
|
}
|
|
1259
1478
|
throw new Error(`Unexpected character '${char}' at ${this.line}:${this.column}`);
|
|
1260
1479
|
}
|
|
1261
|
-
this.tokens.push(new AbilityDSLToken(
|
|
1480
|
+
this.tokens.push(new AbilityDSLToken(TokenTypes.EOF, '', this.line, this.column));
|
|
1262
1481
|
return this.tokens;
|
|
1263
1482
|
}
|
|
1264
1483
|
readComment() {
|
|
@@ -1269,7 +1488,31 @@ class AbilityDSLLexer {
|
|
|
1269
1488
|
while (!this.isAtEnd() && !this.isNewline()) {
|
|
1270
1489
|
value += this.advance();
|
|
1271
1490
|
}
|
|
1272
|
-
return new AbilityDSLToken(
|
|
1491
|
+
return new AbilityDSLToken(TokenTypes.COMMENT, value.trim(), startLine, startColumn);
|
|
1492
|
+
}
|
|
1493
|
+
// private readAlias(): AbilityDSLToken {
|
|
1494
|
+
// const startLine = this.line;
|
|
1495
|
+
// const startColumn = this.column;
|
|
1496
|
+
//
|
|
1497
|
+
// this.advance(); // skip "alias" keyword
|
|
1498
|
+
//
|
|
1499
|
+
// // Read colon
|
|
1500
|
+
// this.readSymbol();
|
|
1501
|
+
//
|
|
1502
|
+
// let value = '';
|
|
1503
|
+
// while (!this.isAtEnd() && !this.isNewline()) {
|
|
1504
|
+
// value += this.advance();
|
|
1505
|
+
// }
|
|
1506
|
+
// return new AbilityDSLToken(TokenTypes.ALIAS, value.trim(), startLine, startColumn);
|
|
1507
|
+
// }
|
|
1508
|
+
readAnnotation() {
|
|
1509
|
+
const startLine = this.line;
|
|
1510
|
+
const startColumn = this.column;
|
|
1511
|
+
let value = '';
|
|
1512
|
+
while (!this.isAtEnd() && !this.isNewline()) {
|
|
1513
|
+
value += this.advance();
|
|
1514
|
+
}
|
|
1515
|
+
return new AbilityDSLToken(TokenTypes.ANNOTATION, value.trim(), startLine, startColumn);
|
|
1273
1516
|
}
|
|
1274
1517
|
readString() {
|
|
1275
1518
|
const startLine = this.line;
|
|
@@ -1289,7 +1532,7 @@ class AbilityDSLLexer {
|
|
|
1289
1532
|
continue;
|
|
1290
1533
|
}
|
|
1291
1534
|
if (char === quote) {
|
|
1292
|
-
return new AbilityDSLToken(
|
|
1535
|
+
return new AbilityDSLToken(TokenTypes.STRING, value, startLine, startColumn);
|
|
1293
1536
|
}
|
|
1294
1537
|
value += char;
|
|
1295
1538
|
}
|
|
@@ -1303,7 +1546,7 @@ class AbilityDSLLexer {
|
|
|
1303
1546
|
this.advance();
|
|
1304
1547
|
}
|
|
1305
1548
|
const value = this.input.slice(start, this.pos);
|
|
1306
|
-
return new AbilityDSLToken(
|
|
1549
|
+
return new AbilityDSLToken(TokenTypes.NUMBER, value, startLine, startColumn);
|
|
1307
1550
|
}
|
|
1308
1551
|
readSymbol() {
|
|
1309
1552
|
const startLine = this.line;
|
|
@@ -1311,41 +1554,41 @@ class AbilityDSLLexer {
|
|
|
1311
1554
|
const char = this.advance();
|
|
1312
1555
|
switch (char) {
|
|
1313
1556
|
case '.':
|
|
1314
|
-
return new AbilityDSLToken(
|
|
1557
|
+
return new AbilityDSLToken(TokenTypes.DOT, char, startLine, startColumn);
|
|
1315
1558
|
case ':':
|
|
1316
|
-
return new AbilityDSLToken(
|
|
1559
|
+
return new AbilityDSLToken(TokenTypes.COLON, char, startLine, startColumn);
|
|
1317
1560
|
case ',':
|
|
1318
|
-
return new AbilityDSLToken(
|
|
1561
|
+
return new AbilityDSLToken(TokenTypes.COMMA, char, startLine, startColumn);
|
|
1319
1562
|
case '[':
|
|
1320
|
-
return new AbilityDSLToken(
|
|
1563
|
+
return new AbilityDSLToken(TokenTypes.LBRACKET, char, startLine, startColumn);
|
|
1321
1564
|
case ']':
|
|
1322
|
-
return new AbilityDSLToken(
|
|
1565
|
+
return new AbilityDSLToken(TokenTypes.RBRACKET, char, startLine, startColumn);
|
|
1323
1566
|
case '>':
|
|
1324
1567
|
if (this.peek() === '=') {
|
|
1325
1568
|
this.advance();
|
|
1326
|
-
return new AbilityDSLToken(
|
|
1569
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '>=', startLine, startColumn);
|
|
1327
1570
|
}
|
|
1328
|
-
return new AbilityDSLToken(
|
|
1571
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '>', startLine, startColumn);
|
|
1329
1572
|
case '<':
|
|
1330
1573
|
if (this.peek() === '=') {
|
|
1331
1574
|
this.advance();
|
|
1332
|
-
return new AbilityDSLToken(
|
|
1575
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '<=', startLine, startColumn);
|
|
1333
1576
|
}
|
|
1334
1577
|
if (this.peek() === '>') {
|
|
1335
1578
|
this.advance();
|
|
1336
|
-
return new AbilityDSLToken(
|
|
1579
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '<>', startLine, startColumn);
|
|
1337
1580
|
}
|
|
1338
|
-
return new AbilityDSLToken(
|
|
1581
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '<', startLine, startColumn);
|
|
1339
1582
|
case '=':
|
|
1340
1583
|
if (this.peek() === '=') {
|
|
1341
1584
|
this.advance();
|
|
1342
|
-
return new AbilityDSLToken(
|
|
1585
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '==', startLine, startColumn);
|
|
1343
1586
|
}
|
|
1344
|
-
return new AbilityDSLToken(
|
|
1587
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '=', startLine, startColumn);
|
|
1345
1588
|
case '!':
|
|
1346
1589
|
if (this.peek() === '=') {
|
|
1347
1590
|
this.advance();
|
|
1348
|
-
return new AbilityDSLToken(
|
|
1591
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '!=', startLine, startColumn);
|
|
1349
1592
|
}
|
|
1350
1593
|
throw new Error(`Unexpected symbol '!' at ${this.line}:${this.column}`);
|
|
1351
1594
|
default:
|
|
@@ -1372,60 +1615,66 @@ class AbilityDSLLexer {
|
|
|
1372
1615
|
}
|
|
1373
1616
|
const word = this.input.slice(start, this.pos);
|
|
1374
1617
|
if (word === 'always') {
|
|
1375
|
-
return new AbilityDSLToken(
|
|
1618
|
+
return new AbilityDSLToken(TokenTypes.ALWAYS, word, startLine, startColumn);
|
|
1376
1619
|
}
|
|
1377
1620
|
if (word === 'never') {
|
|
1378
|
-
return new AbilityDSLToken(
|
|
1621
|
+
return new AbilityDSLToken(TokenTypes.NEVER, word, startLine, startColumn);
|
|
1379
1622
|
}
|
|
1380
1623
|
// Если есть точка — это путь (identifier или permission)
|
|
1381
1624
|
if (word.includes('.')) {
|
|
1382
1625
|
const last = this.tokens[this.tokens.length - 1];
|
|
1383
|
-
if (last?.
|
|
1626
|
+
if (last?.type === TokenTypes.EFFECT) {
|
|
1384
1627
|
if (word.startsWith('permission.')) {
|
|
1385
|
-
return new AbilityDSLToken(
|
|
1628
|
+
return new AbilityDSLToken(TokenTypes.PERMISSION, word, startLine, startColumn);
|
|
1386
1629
|
}
|
|
1387
1630
|
}
|
|
1388
|
-
return new AbilityDSLToken(
|
|
1631
|
+
return new AbilityDSLToken(TokenTypes.IDENTIFIER, word, startLine, startColumn);
|
|
1389
1632
|
}
|
|
1390
1633
|
// Ключевые слова
|
|
1391
1634
|
if (this.keywords.has(word)) {
|
|
1392
1635
|
// Эффекты
|
|
1393
1636
|
if (word === 'permit' || word === 'allow') {
|
|
1394
|
-
return new AbilityDSLToken(
|
|
1637
|
+
return new AbilityDSLToken(TokenTypes.EFFECT, 'permit', startLine, startColumn);
|
|
1395
1638
|
}
|
|
1396
1639
|
if (word === 'deny' || word === 'forbidden') {
|
|
1397
|
-
return new AbilityDSLToken(
|
|
1640
|
+
return new AbilityDSLToken(TokenTypes.EFFECT, 'deny', startLine, startColumn);
|
|
1398
1641
|
}
|
|
1399
1642
|
// Групповые ключевые слова
|
|
1400
1643
|
if (word === 'all') {
|
|
1401
|
-
return new AbilityDSLToken(
|
|
1644
|
+
return new AbilityDSLToken(TokenTypes.ALL, word, startLine, startColumn);
|
|
1402
1645
|
}
|
|
1403
1646
|
if (word === 'any') {
|
|
1404
|
-
return new AbilityDSLToken(
|
|
1647
|
+
return new AbilityDSLToken(TokenTypes.ANY, word, startLine, startColumn);
|
|
1405
1648
|
}
|
|
1406
1649
|
if (word === 'of') {
|
|
1407
|
-
return new AbilityDSLToken(
|
|
1650
|
+
return new AbilityDSLToken(TokenTypes.OF, word, startLine, startColumn);
|
|
1408
1651
|
}
|
|
1409
1652
|
if (word === 'if') {
|
|
1410
|
-
return new AbilityDSLToken(
|
|
1653
|
+
return new AbilityDSLToken(TokenTypes.IF, word, startLine, startColumn);
|
|
1411
1654
|
}
|
|
1412
1655
|
// Булевы и null
|
|
1413
1656
|
if (word === 'true' || word === 'false') {
|
|
1414
|
-
return new AbilityDSLToken(
|
|
1657
|
+
return new AbilityDSLToken(TokenTypes.BOOLEAN, word, startLine, startColumn);
|
|
1415
1658
|
}
|
|
1416
1659
|
if (word === 'null') {
|
|
1417
|
-
return new AbilityDSLToken(
|
|
1660
|
+
return new AbilityDSLToken(TokenTypes.NULL, word, startLine, startColumn);
|
|
1661
|
+
}
|
|
1662
|
+
if (word === 'except') {
|
|
1663
|
+
return new AbilityDSLToken(TokenTypes.EXCEPT, word, startLine, startColumn);
|
|
1664
|
+
}
|
|
1665
|
+
if (word === 'alias') {
|
|
1666
|
+
return new AbilityDSLToken(TokenTypes.ALIAS, word, startLine, startColumn);
|
|
1418
1667
|
}
|
|
1419
1668
|
// Остальные ключевые слова (contains, in, equals, greater, less, not, is, or, than, equal)
|
|
1420
|
-
return new AbilityDSLToken(
|
|
1669
|
+
return new AbilityDSLToken(TokenTypes.KEYWORD, word, startLine, startColumn);
|
|
1421
1670
|
}
|
|
1422
1671
|
// Если после EFFECT и нет точки — действие (например, "create")
|
|
1423
1672
|
const lastToken = this.tokens[this.tokens.length - 1];
|
|
1424
|
-
if (lastToken?.
|
|
1425
|
-
return new AbilityDSLToken(
|
|
1673
|
+
if (lastToken?.type === TokenTypes.EFFECT) {
|
|
1674
|
+
return new AbilityDSLToken(TokenTypes.PERMISSION, word, startLine, startColumn);
|
|
1426
1675
|
}
|
|
1427
1676
|
// Обычный идентификатор
|
|
1428
|
-
return new AbilityDSLToken(
|
|
1677
|
+
return new AbilityDSLToken(TokenTypes.IDENTIFIER, word, startLine, startColumn);
|
|
1429
1678
|
}
|
|
1430
1679
|
skipWhitespace() {
|
|
1431
1680
|
while (!this.isAtEnd() && /\s/.test(this.peek())) {
|
|
@@ -1525,6 +1774,215 @@ class AbilityDSLSyntaxError extends Error {
|
|
|
1525
1774
|
}
|
|
1526
1775
|
}
|
|
1527
1776
|
|
|
1777
|
+
class AbilityDSLTokenStream {
|
|
1778
|
+
tokens;
|
|
1779
|
+
pos = 0;
|
|
1780
|
+
dsl;
|
|
1781
|
+
marks = [];
|
|
1782
|
+
lastToken = null;
|
|
1783
|
+
next() {
|
|
1784
|
+
const token = this.tokens[this.pos++];
|
|
1785
|
+
this.lastToken = token;
|
|
1786
|
+
return token;
|
|
1787
|
+
}
|
|
1788
|
+
prev() {
|
|
1789
|
+
if (this.pos === 0) {
|
|
1790
|
+
return null;
|
|
1791
|
+
}
|
|
1792
|
+
const token = this.tokens[this.pos--];
|
|
1793
|
+
this.lastToken = token;
|
|
1794
|
+
return token;
|
|
1795
|
+
}
|
|
1796
|
+
lookPrev() {
|
|
1797
|
+
return this.lastToken;
|
|
1798
|
+
}
|
|
1799
|
+
constructor(tokens, dsl) {
|
|
1800
|
+
this.tokens = tokens;
|
|
1801
|
+
this.dsl = dsl;
|
|
1802
|
+
}
|
|
1803
|
+
peek() {
|
|
1804
|
+
return this.tokens[this.pos];
|
|
1805
|
+
}
|
|
1806
|
+
eof() {
|
|
1807
|
+
return this.peek().type === TokenTypes.EOF;
|
|
1808
|
+
}
|
|
1809
|
+
check(type) {
|
|
1810
|
+
if (this.eof()) {
|
|
1811
|
+
return false;
|
|
1812
|
+
}
|
|
1813
|
+
return this.peek().type === type;
|
|
1814
|
+
}
|
|
1815
|
+
match(type) {
|
|
1816
|
+
if (this.check(type)) {
|
|
1817
|
+
return this.next();
|
|
1818
|
+
}
|
|
1819
|
+
return null;
|
|
1820
|
+
}
|
|
1821
|
+
expect(type, message) {
|
|
1822
|
+
const token = this.peek();
|
|
1823
|
+
if (token && token.type === type) {
|
|
1824
|
+
return this.next();
|
|
1825
|
+
}
|
|
1826
|
+
this.syntaxError(message, token, [type]);
|
|
1827
|
+
}
|
|
1828
|
+
expectOneOf(types, message) {
|
|
1829
|
+
const token = this.peek();
|
|
1830
|
+
for (const t of types) {
|
|
1831
|
+
if (token && token.type === t) {
|
|
1832
|
+
return this.next();
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
this.syntaxError(message, token, types);
|
|
1836
|
+
}
|
|
1837
|
+
mark() {
|
|
1838
|
+
this.marks.push(this.pos);
|
|
1839
|
+
}
|
|
1840
|
+
reset() {
|
|
1841
|
+
const pos = this.marks.pop();
|
|
1842
|
+
if (pos !== undefined) {
|
|
1843
|
+
this.pos = pos;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
commit() {
|
|
1847
|
+
this.marks.pop();
|
|
1848
|
+
}
|
|
1849
|
+
syntaxError(details, token, expected) {
|
|
1850
|
+
const lines = this.dsl.split(/\r?\n/);
|
|
1851
|
+
const lineIdx = token.line - 1;
|
|
1852
|
+
const lineBefore = lineIdx > 0 ? lines[lineIdx - 1] : '';
|
|
1853
|
+
const current = lines[lineIdx];
|
|
1854
|
+
const linesAfter = lineIdx + 1 < lines.length ? lines[lineIdx + 1] : '';
|
|
1855
|
+
const wave = ' '.repeat(Math.max(0, token.column - 1)) + '~'.repeat(token.value.length);
|
|
1856
|
+
const lineNumWidth = String(token.line + 1).length;
|
|
1857
|
+
const num = (n) => String(n).padStart(lineNumWidth, ' ');
|
|
1858
|
+
let context = '';
|
|
1859
|
+
if (lineBefore.trim() !== '') {
|
|
1860
|
+
context += `${num(token.line - 1)} | ${lineBefore}\n`;
|
|
1861
|
+
}
|
|
1862
|
+
context += `${num(token.line)} | ${current}\n`;
|
|
1863
|
+
context += `${' '.repeat(lineNumWidth)} | ${wave}\n`;
|
|
1864
|
+
if (linesAfter.trim() !== '') {
|
|
1865
|
+
context += `${num(token.line + 1)} | ${linesAfter}`;
|
|
1866
|
+
}
|
|
1867
|
+
let finalDetails = details;
|
|
1868
|
+
if (expected && expected?.length > 0) {
|
|
1869
|
+
const actual = token.value;
|
|
1870
|
+
const suggestion = this.suggest(actual, expected);
|
|
1871
|
+
const detailsMsg = `${details}\nDetails: Unexpected value token \`${actual}\``;
|
|
1872
|
+
finalDetails = suggestion ? `${detailsMsg} Did you mean \`${suggestion}\`?` : detailsMsg;
|
|
1873
|
+
}
|
|
1874
|
+
throw new AbilityDSLSyntaxError(token.line, token.column, context + '\n', finalDetails);
|
|
1875
|
+
}
|
|
1876
|
+
suggest(actual, expectedTypes) {
|
|
1877
|
+
const candidates = [];
|
|
1878
|
+
for (const type of expectedTypes) {
|
|
1879
|
+
candidates.push(type);
|
|
1880
|
+
}
|
|
1881
|
+
const uniqueCandidates = [...new Set(candidates)];
|
|
1882
|
+
let best = null;
|
|
1883
|
+
let bestDist = 3;
|
|
1884
|
+
for (const candidate of uniqueCandidates) {
|
|
1885
|
+
const d = this.levenshteinDistance(actual.toLowerCase(), candidate.toLowerCase());
|
|
1886
|
+
if (d < bestDist) {
|
|
1887
|
+
bestDist = d;
|
|
1888
|
+
best = candidate;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
return best;
|
|
1892
|
+
}
|
|
1893
|
+
levenshteinDistance(a, b) {
|
|
1894
|
+
const matrix = Array.from({ length: b.length + 1 }, () => Array.from({ length: a.length + 1 }, () => 0));
|
|
1895
|
+
for (let i = 0; i <= a.length; i++)
|
|
1896
|
+
matrix[0][i] = i;
|
|
1897
|
+
for (let j = 0; j <= b.length; j++)
|
|
1898
|
+
matrix[j][0] = j;
|
|
1899
|
+
for (let j = 1; j <= b.length; j++) {
|
|
1900
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1901
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
1902
|
+
matrix[j][i] = Math.min(matrix[j][i - 1] + 1, matrix[j - 1][i] + 1, matrix[j - 1][i - 1] + cost);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
return matrix[b.length][a.length];
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
class AbilityDSLAnnotations {
|
|
1910
|
+
store = {
|
|
1911
|
+
id: undefined,
|
|
1912
|
+
name: undefined,
|
|
1913
|
+
priority: undefined,
|
|
1914
|
+
description: undefined,
|
|
1915
|
+
disabled: undefined,
|
|
1916
|
+
tags: undefined,
|
|
1917
|
+
};
|
|
1918
|
+
get(key) {
|
|
1919
|
+
return this.store[key] ?? null;
|
|
1920
|
+
}
|
|
1921
|
+
set(key, value, token) {
|
|
1922
|
+
if (value === null) {
|
|
1923
|
+
this.store[key] = undefined;
|
|
1924
|
+
}
|
|
1925
|
+
else {
|
|
1926
|
+
this.store[key] = {
|
|
1927
|
+
key,
|
|
1928
|
+
value,
|
|
1929
|
+
token,
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
return this;
|
|
1933
|
+
}
|
|
1934
|
+
clear() {
|
|
1935
|
+
for (const key of Object.keys(this.store)) {
|
|
1936
|
+
this.store[key] = undefined;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
clone() {
|
|
1940
|
+
const cloned = new AbilityDSLAnnotations();
|
|
1941
|
+
for (const key of Object.keys(this.store)) {
|
|
1942
|
+
const entry = this.store[key];
|
|
1943
|
+
cloned.store[key] = entry
|
|
1944
|
+
? { ...entry }
|
|
1945
|
+
: undefined;
|
|
1946
|
+
}
|
|
1947
|
+
return cloned;
|
|
1948
|
+
}
|
|
1949
|
+
// convenience getters
|
|
1950
|
+
get id() { return this.get('id'); }
|
|
1951
|
+
get name() { return this.get('name'); }
|
|
1952
|
+
get description() { return this.get('description'); }
|
|
1953
|
+
get priority() { return this.get('priority'); }
|
|
1954
|
+
get disabled() { return this.get('disabled'); }
|
|
1955
|
+
get tags() { return this.get('tags'); }
|
|
1956
|
+
// convenience setters
|
|
1957
|
+
setID(v, t) { return this.set('id', v, t); }
|
|
1958
|
+
setName(v, t) { return this.set('name', v, t); }
|
|
1959
|
+
setDescription(v, t) { return this.set('description', v, t); }
|
|
1960
|
+
setPriority(v, t) { return this.set('priority', v, t); }
|
|
1961
|
+
setDisabled(v, t) { return this.set('disabled', v, t); }
|
|
1962
|
+
setTags(v, t) { return this.set('tags', v, t); }
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
const AnnotationAllowed = {
|
|
1966
|
+
policy: new Set(["id", "name", "description", "priority", "disabled", "tags"]),
|
|
1967
|
+
ruleSet: new Set(["id", "name", "description", "disabled"]),
|
|
1968
|
+
rule: new Set(["id", "name", "disabled"]),
|
|
1969
|
+
alias: new Set(["name", "disabled"]),
|
|
1970
|
+
};
|
|
1971
|
+
|
|
1972
|
+
class AbilityDSLAliases {
|
|
1973
|
+
store = new Map();
|
|
1974
|
+
get(alias) {
|
|
1975
|
+
return this.store.get(alias) || null;
|
|
1976
|
+
}
|
|
1977
|
+
set(alias, rule) {
|
|
1978
|
+
this.store.set(alias, rule);
|
|
1979
|
+
return this;
|
|
1980
|
+
}
|
|
1981
|
+
has(alias) {
|
|
1982
|
+
return this.store.has(alias);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1528
1986
|
/**
|
|
1529
1987
|
* Parser for the Ability DSL.
|
|
1530
1988
|
*
|
|
@@ -1543,12 +2001,9 @@ class AbilityDSLSyntaxError extends Error {
|
|
|
1543
2001
|
*/
|
|
1544
2002
|
class AbilityDSLParser {
|
|
1545
2003
|
dsl;
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
name: null,
|
|
1550
|
-
description: null,
|
|
1551
|
-
};
|
|
2004
|
+
stream;
|
|
2005
|
+
annBuffer = new AbilityDSLAnnotations();
|
|
2006
|
+
aliasBuffer = new AbilityDSLAliases();
|
|
1552
2007
|
constructor(dsl) {
|
|
1553
2008
|
this.dsl = dsl;
|
|
1554
2009
|
}
|
|
@@ -1557,17 +2012,19 @@ class AbilityDSLParser {
|
|
|
1557
2012
|
* @returns Array of AbilityPolicy instances.
|
|
1558
2013
|
*/
|
|
1559
2014
|
parse() {
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
2015
|
+
this.annBuffer.clear();
|
|
2016
|
+
// 1. Лексер → токены
|
|
2017
|
+
const tokens = new AbilityDSLLexer(this.dsl).tokenize();
|
|
2018
|
+
// 2. Создаём TokenStream
|
|
2019
|
+
this.stream = new AbilityDSLTokenStream(tokens, this.dsl);
|
|
1563
2020
|
const policies = [];
|
|
1564
|
-
|
|
1565
|
-
while (!this.isAtEnd()) {
|
|
2021
|
+
while (!this.stream.eof()) {
|
|
1566
2022
|
this.consumeLeadingComments();
|
|
1567
|
-
|
|
2023
|
+
this.consumeLeadingAnnotations();
|
|
2024
|
+
this.consumeLeadingAliases();
|
|
1568
2025
|
if (!this.isStartOfPolicy()) {
|
|
1569
|
-
const token = this.peek();
|
|
1570
|
-
this.syntaxError(`Expected policy, got ${token.
|
|
2026
|
+
const token = this.stream.peek();
|
|
2027
|
+
this.stream.syntaxError(`Expected policy, got ${token.type}.`, token, [TokenTypes.EFFECT]);
|
|
1571
2028
|
}
|
|
1572
2029
|
policies.push(this.parsePolicy());
|
|
1573
2030
|
}
|
|
@@ -1584,31 +2041,37 @@ class AbilityDSLParser {
|
|
|
1584
2041
|
*/
|
|
1585
2042
|
parsePolicy() {
|
|
1586
2043
|
this.consumeLeadingComments();
|
|
1587
|
-
|
|
2044
|
+
this.consumeLeadingAnnotations();
|
|
2045
|
+
this.consumeLeadingAliases();
|
|
2046
|
+
const annotations = this.takeAnnotations('policy');
|
|
1588
2047
|
// Effect: "permit" or "deny"
|
|
1589
|
-
const effectToken = this.
|
|
2048
|
+
const effectToken = this.stream.expect(TokenTypes.EFFECT, 'Expected effect');
|
|
1590
2049
|
const effect = effectToken.value;
|
|
1591
2050
|
// Permission: e.g. "order.update"
|
|
1592
|
-
const permissionToken = this.
|
|
2051
|
+
const permissionToken = this.stream.expect(TokenTypes.PERMISSION, 'Expected permission');
|
|
1593
2052
|
const permission = permissionToken.value;
|
|
1594
2053
|
if (!permission.startsWith('permission.')) {
|
|
1595
|
-
return this.syntaxError(`Unexpected token. The permission key, must be starts with prefix \`permission.\`, but got \`${permission}\`.\nDid you mean \`permission.${permission}\`?`, permissionToken);
|
|
2054
|
+
return this.stream.syntaxError(`Unexpected token. The permission key, must be starts with prefix \`permission.\`, but got \`${permission}\`.\nDid you mean \`permission.${permission}\`?`, permissionToken);
|
|
1596
2055
|
}
|
|
1597
2056
|
// "if" keyword
|
|
1598
|
-
this.
|
|
2057
|
+
this.stream.expect(TokenTypes.IF, 'Expected "if"');
|
|
1599
2058
|
// Group selector: "all" or "any" – determines how the top‑level rule sets are combined.
|
|
1600
|
-
const compareToken = this.
|
|
1601
|
-
const compareMethod = compareToken.
|
|
2059
|
+
const compareToken = this.stream.expectOneOf([TokenTypes.ALL, TokenTypes.ANY], 'Expected "all" or "any"');
|
|
2060
|
+
const compareMethod = compareToken.type === TokenTypes.ALL ? AbilityCompare.and : AbilityCompare.or;
|
|
1602
2061
|
// Colon after the group keyword
|
|
1603
|
-
this.
|
|
2062
|
+
this.stream.expect(TokenTypes.COLON, 'Expected ":"');
|
|
1604
2063
|
// Parse the list of rule sets (each "all of:" or "any of:" block)
|
|
1605
2064
|
const ruleSets = this.parseRuleSets(compareMethod);
|
|
1606
2065
|
// Construct the policy instance.
|
|
1607
2066
|
return new AbilityPolicy({
|
|
1608
|
-
id:
|
|
1609
|
-
name:
|
|
2067
|
+
id: annotations.id?.value || null,
|
|
2068
|
+
name: annotations.name?.value || null,
|
|
2069
|
+
description: annotations.description?.value || null,
|
|
2070
|
+
priority: annotations.priority?.value || null,
|
|
1610
2071
|
permission: permission.replace(/^permission\./, ''),
|
|
1611
2072
|
effect: effect === 'permit' ? AbilityPolicyEffect.permit : AbilityPolicyEffect.deny,
|
|
2073
|
+
disabled: annotations.disabled?.value ?? undefined,
|
|
2074
|
+
tags: annotations.tags?.value ?? undefined,
|
|
1612
2075
|
compareMethod,
|
|
1613
2076
|
}).addRuleSets(ruleSets);
|
|
1614
2077
|
}
|
|
@@ -1620,32 +2083,41 @@ class AbilityDSLParser {
|
|
|
1620
2083
|
*/
|
|
1621
2084
|
parseRuleSets(policyCompareMethod) {
|
|
1622
2085
|
const sets = [];
|
|
1623
|
-
while (!this.
|
|
2086
|
+
while (!this.stream.eof() && !this.isStartOfPolicy()) {
|
|
1624
2087
|
this.consumeLeadingComments();
|
|
2088
|
+
this.consumeLeadingAnnotations();
|
|
2089
|
+
// Если начинается новая except группа — парсим её
|
|
2090
|
+
if (this.isStartOfExcept()) {
|
|
2091
|
+
sets.push(this.parseExceptGroup(policyCompareMethod));
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
1625
2094
|
// Если начинается новая группа — парсим её
|
|
1626
2095
|
if (this.isStartOfGroup()) {
|
|
1627
2096
|
sets.push(this.parseGroup());
|
|
1628
2097
|
continue;
|
|
1629
2098
|
}
|
|
1630
|
-
|
|
1631
|
-
const meta = this.takeAnnotations();
|
|
2099
|
+
const annotation = this.takeAnnotations('ruleSet');
|
|
1632
2100
|
const group = new AbilityRuleSet({
|
|
2101
|
+
id: annotation.id?.value || null,
|
|
1633
2102
|
compareMethod: policyCompareMethod,
|
|
1634
|
-
name:
|
|
2103
|
+
name: annotation.name?.value ?? null,
|
|
2104
|
+
description: annotation.description?.value || null,
|
|
2105
|
+
disabled: annotation.disabled?.value ?? undefined,
|
|
1635
2106
|
});
|
|
1636
2107
|
// Читаем правила implicit-группы
|
|
1637
|
-
while (!this.
|
|
2108
|
+
while (!this.stream.eof()) {
|
|
1638
2109
|
this.consumeLeadingComments();
|
|
1639
|
-
|
|
2110
|
+
this.consumeLeadingAnnotations();
|
|
2111
|
+
if (this.isStartOfGroup() || this.isStartOfPolicy() || this.isStartOfExcept()) {
|
|
1640
2112
|
break;
|
|
1641
2113
|
}
|
|
1642
|
-
if (this.check(
|
|
1643
|
-
this.check(
|
|
1644
|
-
this.check(
|
|
2114
|
+
if (this.stream.check(TokenTypes.IDENTIFIER) ||
|
|
2115
|
+
this.stream.check(TokenTypes.ALWAYS) ||
|
|
2116
|
+
this.stream.check(TokenTypes.NEVER)) {
|
|
1645
2117
|
group.addRule(this.parseRule());
|
|
1646
2118
|
}
|
|
1647
2119
|
else {
|
|
1648
|
-
this.syntaxError(`Unexpected token in implicit group: ${this.peek().
|
|
2120
|
+
this.stream.syntaxError(`Unexpected token in implicit group: ${this.stream.peek().type}`, this.stream.peek());
|
|
1649
2121
|
}
|
|
1650
2122
|
}
|
|
1651
2123
|
sets.push(group);
|
|
@@ -1657,24 +2129,83 @@ class AbilityDSLParser {
|
|
|
1657
2129
|
*/
|
|
1658
2130
|
parseGroup() {
|
|
1659
2131
|
this.consumeLeadingComments();
|
|
1660
|
-
|
|
1661
|
-
const
|
|
1662
|
-
const
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
2132
|
+
this.consumeLeadingAnnotations();
|
|
2133
|
+
const annotations = this.takeAnnotations('ruleSet');
|
|
2134
|
+
const compareToken = this.stream.expectOneOf([TokenTypes.ALL, TokenTypes.ANY, TokenTypes.ALWAYS, TokenTypes.NEVER], 'Expected "all" or "any" or "always" or "never"');
|
|
2135
|
+
const compareMethod = compareToken.type === TokenTypes.ALL ? AbilityCompare.and : AbilityCompare.or;
|
|
2136
|
+
if (this.stream.check(TokenTypes.OF)) {
|
|
2137
|
+
this.stream.next();
|
|
2138
|
+
}
|
|
2139
|
+
this.stream.expect(TokenTypes.COLON, 'Expected ":"');
|
|
2140
|
+
const group = new AbilityRuleSet({
|
|
2141
|
+
id: annotations.id?.value || null,
|
|
2142
|
+
compareMethod,
|
|
2143
|
+
name: annotations.name?.value || null,
|
|
2144
|
+
description: annotations.description?.value || null,
|
|
2145
|
+
disabled: annotations.disabled?.value ?? undefined,
|
|
2146
|
+
});
|
|
2147
|
+
while (!this.stream.eof()) {
|
|
1669
2148
|
this.consumeLeadingComments();
|
|
2149
|
+
this.consumeLeadingAnnotations();
|
|
2150
|
+
if (this.isStartOfExcept()) {
|
|
2151
|
+
break;
|
|
2152
|
+
}
|
|
1670
2153
|
if (this.isStartOfGroup() || this.isStartOfPolicy()) {
|
|
1671
2154
|
break;
|
|
1672
2155
|
}
|
|
1673
|
-
if (this.check(
|
|
2156
|
+
if (this.stream.check(TokenTypes.IDENTIFIER)) {
|
|
2157
|
+
group.addRule(this.parseRule());
|
|
2158
|
+
}
|
|
2159
|
+
else {
|
|
2160
|
+
this.stream.syntaxError(`Unexpected token in group: ${this.stream.peek().type}`, this.stream.peek());
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
return group;
|
|
2164
|
+
}
|
|
2165
|
+
// -------------------------------------------------------------------------
|
|
2166
|
+
// #region Except RuleSet parsing
|
|
2167
|
+
// -------------------------------------------------------------------------
|
|
2168
|
+
parseExceptGroup(policyCompareMethod) {
|
|
2169
|
+
this.consumeLeadingComments();
|
|
2170
|
+
this.consumeLeadingAnnotations();
|
|
2171
|
+
const annotations = this.takeAnnotations('ruleSet');
|
|
2172
|
+
// consume "except"
|
|
2173
|
+
this.stream.expect(TokenTypes.EXCEPT, 'Expected "except"');
|
|
2174
|
+
let compareMethod = policyCompareMethod;
|
|
2175
|
+
// optional: "all" / "any"
|
|
2176
|
+
if (this.stream.check(TokenTypes.ALL) || this.stream.check(TokenTypes.ANY)) {
|
|
2177
|
+
const compareToken = this.stream.next();
|
|
2178
|
+
compareMethod = compareToken.type === TokenTypes.ALL ? AbilityCompare.and : AbilityCompare.or;
|
|
2179
|
+
if (this.stream.check(TokenTypes.OF)) {
|
|
2180
|
+
this.stream.next();
|
|
2181
|
+
}
|
|
2182
|
+
this.stream.expect(TokenTypes.COLON, 'Expected ":" after except group');
|
|
2183
|
+
}
|
|
2184
|
+
else {
|
|
2185
|
+
// implicit except group — no "all/any of:"
|
|
2186
|
+
// but still must end with colon
|
|
2187
|
+
this.stream.expect(TokenTypes.COLON, 'Expected ":" after "except"');
|
|
2188
|
+
}
|
|
2189
|
+
const group = new AbilityRuleSet({
|
|
2190
|
+
id: annotations.id?.value || null,
|
|
2191
|
+
compareMethod,
|
|
2192
|
+
name: annotations.name?.value || null,
|
|
2193
|
+
description: annotations.description?.value || null,
|
|
2194
|
+
isExcept: true,
|
|
2195
|
+
disabled: annotations.disabled?.value ?? undefined,
|
|
2196
|
+
});
|
|
2197
|
+
// read rules
|
|
2198
|
+
while (!this.stream.eof()) {
|
|
2199
|
+
this.consumeLeadingComments();
|
|
2200
|
+
this.consumeLeadingAnnotations();
|
|
2201
|
+
if (this.isStartOfGroup() || this.isStartOfPolicy() || this.isStartOfExcept()) {
|
|
2202
|
+
break;
|
|
2203
|
+
}
|
|
2204
|
+
if (this.stream.check(TokenTypes.IDENTIFIER)) {
|
|
1674
2205
|
group.addRule(this.parseRule());
|
|
1675
2206
|
}
|
|
1676
2207
|
else {
|
|
1677
|
-
this.syntaxError(`Unexpected token in group: ${this.peek().
|
|
2208
|
+
this.stream.syntaxError(`Unexpected token in except group: ${this.stream.peek().type}`, this.stream.peek());
|
|
1678
2209
|
}
|
|
1679
2210
|
}
|
|
1680
2211
|
return group;
|
|
@@ -1687,57 +2218,54 @@ class AbilityDSLParser {
|
|
|
1687
2218
|
*/
|
|
1688
2219
|
parseRule() {
|
|
1689
2220
|
this.consumeLeadingComments();
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
// const specOperator = this.consume();
|
|
1696
|
-
// // return new AbilityRule({
|
|
1697
|
-
// // subject: '',
|
|
1698
|
-
// // resource,
|
|
1699
|
-
// // condition,
|
|
1700
|
-
// // name: meta.name,
|
|
1701
|
-
// // });
|
|
1702
|
-
// }
|
|
1703
|
-
const isNeverAlways = this.check(AbilityDSLToken.ALWAYS) || this.check(AbilityDSLToken.NEVER);
|
|
1704
|
-
if (!isNeverAlways && !this.check(AbilityDSLToken.IDENTIFIER)) {
|
|
1705
|
-
this.syntaxError(`Expected identifier, but got ${this.peek().code}`, this.peek());
|
|
2221
|
+
this.consumeLeadingAnnotations();
|
|
2222
|
+
const annotations = this.takeAnnotations('rule');
|
|
2223
|
+
const isNeverAlways = this.stream.check(TokenTypes.ALWAYS) || this.stream.check(TokenTypes.NEVER);
|
|
2224
|
+
if (!isNeverAlways && !this.stream.check(TokenTypes.IDENTIFIER)) {
|
|
2225
|
+
this.stream.syntaxError(`Expected identifier, but got ${this.stream.peek().type}`, this.stream.peek());
|
|
1706
2226
|
}
|
|
1707
|
-
//
|
|
2227
|
+
// subject
|
|
1708
2228
|
const subject = isNeverAlways
|
|
1709
2229
|
? ''
|
|
1710
|
-
: this.
|
|
1711
|
-
//
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
let beforePos = this.pos;
|
|
1715
|
-
// Special operators that don't consume a value token.
|
|
1716
|
-
if (operator === AbilityDSLToken.EQ_NULL ||
|
|
1717
|
-
operator === AbilityDSLToken.NOT_EQ_NULL ||
|
|
1718
|
-
operator === AbilityDSLToken.NULL ||
|
|
1719
|
-
operator === AbilityDSLToken.ALWAYS ||
|
|
1720
|
-
operator === AbilityDSLToken.NEVER) {
|
|
1721
|
-
resource = null;
|
|
2230
|
+
: this.stream.expect(TokenTypes.IDENTIFIER, 'Expected field').value;
|
|
2231
|
+
// check alias
|
|
2232
|
+
if (this.aliasBuffer.has(subject)) {
|
|
2233
|
+
return this.aliasBuffer.get(subject);
|
|
1722
2234
|
}
|
|
1723
|
-
|
|
1724
|
-
|
|
2235
|
+
// operator
|
|
2236
|
+
const { condition, operator } = this.parseConditionOperator();
|
|
2237
|
+
// value
|
|
2238
|
+
let resource = null;
|
|
2239
|
+
let valueToken = null;
|
|
2240
|
+
const operatorConsumesValue = operator !== TokenTypes.EQ_NULL &&
|
|
2241
|
+
operator !== TokenTypes.NOT_EQ_NULL &&
|
|
2242
|
+
operator !== TokenTypes.NULL &&
|
|
2243
|
+
operator !== TokenTypes.ALWAYS &&
|
|
2244
|
+
operator !== TokenTypes.NEVER;
|
|
2245
|
+
if (operatorConsumesValue) {
|
|
2246
|
+
this.stream.mark();
|
|
1725
2247
|
resource = this.parseValue();
|
|
2248
|
+
valueToken = this.stream.lookPrev();
|
|
2249
|
+
this.stream.commit();
|
|
1726
2250
|
}
|
|
1727
|
-
// Checking that there are no extra tokens after the value
|
|
1728
|
-
// (skip comments)
|
|
1729
2251
|
this.consumeLeadingComments();
|
|
1730
|
-
|
|
2252
|
+
this.consumeLeadingAnnotations();
|
|
2253
|
+
this.consumeLeadingAliases();
|
|
2254
|
+
// validation: identifier without dot → error
|
|
1731
2255
|
if (typeof resource === 'string' &&
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
2256
|
+
valueToken &&
|
|
2257
|
+
valueToken.type === TokenTypes.IDENTIFIER &&
|
|
2258
|
+
!valueToken.value.includes('.')) {
|
|
2259
|
+
this.stream.syntaxError(`Expected comparison operator or value, got \`${resource}\``, valueToken, [TokenTypes.KEYWORD]);
|
|
1735
2260
|
}
|
|
1736
2261
|
return new AbilityRule({
|
|
2262
|
+
id: annotations.id?.value || null,
|
|
1737
2263
|
subject,
|
|
1738
2264
|
resource,
|
|
1739
2265
|
condition,
|
|
1740
|
-
name:
|
|
2266
|
+
name: annotations.name?.value || null,
|
|
2267
|
+
description: annotations.description?.value || null,
|
|
2268
|
+
disabled: annotations.disabled?.value ?? undefined,
|
|
1741
2269
|
});
|
|
1742
2270
|
}
|
|
1743
2271
|
// -------------------------------------------------------------------------
|
|
@@ -1748,200 +2276,241 @@ class AbilityDSLParser {
|
|
|
1748
2276
|
* Returns both the resulting AbilityCondition and the token type that was consumed.
|
|
1749
2277
|
*/
|
|
1750
2278
|
parseConditionOperator() {
|
|
1751
|
-
const savedPos = this.pos;
|
|
1752
2279
|
// "always"
|
|
2280
|
+
this.stream.mark();
|
|
1753
2281
|
if (this.matchWord('always')) {
|
|
1754
|
-
|
|
2282
|
+
this.stream.commit();
|
|
2283
|
+
return { condition: AbilityCondition.always, operator: TokenTypes.ALWAYS };
|
|
1755
2284
|
}
|
|
1756
|
-
this.
|
|
2285
|
+
this.stream.reset();
|
|
1757
2286
|
// "never"
|
|
2287
|
+
this.stream.mark();
|
|
1758
2288
|
if (this.matchWord('never')) {
|
|
1759
|
-
|
|
2289
|
+
this.stream.commit();
|
|
2290
|
+
return { condition: AbilityCondition.never, operator: TokenTypes.NEVER };
|
|
1760
2291
|
}
|
|
1761
|
-
this.
|
|
2292
|
+
this.stream.reset();
|
|
1762
2293
|
// "length equals"
|
|
2294
|
+
this.stream.mark();
|
|
1763
2295
|
if (this.matchWord('length') && this.matchWord('equals')) {
|
|
1764
|
-
|
|
2296
|
+
this.stream.commit();
|
|
2297
|
+
return { condition: AbilityCondition.length_equals, operator: TokenTypes.LEN_EQ };
|
|
1765
2298
|
}
|
|
1766
|
-
this.
|
|
2299
|
+
this.stream.reset();
|
|
1767
2300
|
// "length ="
|
|
2301
|
+
this.stream.mark();
|
|
1768
2302
|
if (this.matchWord('length') && this.matchSymbol('=')) {
|
|
1769
|
-
|
|
2303
|
+
this.stream.commit();
|
|
2304
|
+
return { condition: AbilityCondition.length_equals, operator: TokenTypes.LEN_EQ };
|
|
1770
2305
|
}
|
|
1771
|
-
this.
|
|
2306
|
+
this.stream.reset();
|
|
1772
2307
|
// "length greater than"
|
|
2308
|
+
this.stream.mark();
|
|
1773
2309
|
if (this.matchWord('length') && this.matchWord('greater') && this.matchWord('than')) {
|
|
1774
|
-
|
|
2310
|
+
this.stream.commit();
|
|
2311
|
+
return { condition: AbilityCondition.length_greater_than, operator: TokenTypes.LEN_GT };
|
|
1775
2312
|
}
|
|
1776
|
-
this.
|
|
2313
|
+
this.stream.reset();
|
|
1777
2314
|
// "length >"
|
|
2315
|
+
this.stream.mark();
|
|
1778
2316
|
if (this.matchWord('length') && this.matchSymbol('>')) {
|
|
1779
|
-
|
|
2317
|
+
this.stream.commit();
|
|
2318
|
+
return { condition: AbilityCondition.length_greater_than, operator: TokenTypes.LEN_GT };
|
|
1780
2319
|
}
|
|
1781
|
-
this.
|
|
2320
|
+
this.stream.reset();
|
|
1782
2321
|
// "length less than"
|
|
2322
|
+
this.stream.mark();
|
|
1783
2323
|
if (this.matchWord('length') && this.matchWord('less') && this.matchWord('than')) {
|
|
1784
|
-
|
|
2324
|
+
this.stream.commit();
|
|
2325
|
+
return { condition: AbilityCondition.length_less_than, operator: TokenTypes.LEN_LT };
|
|
1785
2326
|
}
|
|
1786
|
-
this.
|
|
2327
|
+
this.stream.reset();
|
|
1787
2328
|
// "length <"
|
|
2329
|
+
this.stream.mark();
|
|
1788
2330
|
if (this.matchWord('length') && this.matchSymbol('<')) {
|
|
1789
|
-
|
|
2331
|
+
this.stream.commit();
|
|
2332
|
+
return { condition: AbilityCondition.length_less_than, operator: TokenTypes.LEN_LT };
|
|
1790
2333
|
}
|
|
1791
|
-
this.
|
|
2334
|
+
this.stream.reset();
|
|
1792
2335
|
// "greater than or equal"
|
|
2336
|
+
this.stream.mark();
|
|
1793
2337
|
if (this.matchWord('greater') &&
|
|
1794
2338
|
this.matchWord('than') &&
|
|
1795
2339
|
this.matchWord('or') &&
|
|
1796
2340
|
this.matchWord('equal')) {
|
|
1797
|
-
|
|
2341
|
+
this.stream.commit();
|
|
2342
|
+
return { condition: AbilityCondition.greater_or_equal, operator: TokenTypes.GTE };
|
|
1798
2343
|
}
|
|
1799
|
-
this.
|
|
2344
|
+
this.stream.reset();
|
|
1800
2345
|
// greater than
|
|
2346
|
+
this.stream.mark();
|
|
1801
2347
|
if (this.matchWord('greater') && this.matchWord('than')) {
|
|
1802
|
-
|
|
2348
|
+
this.stream.commit();
|
|
2349
|
+
return { condition: AbilityCondition.greater_than, operator: TokenTypes.GT };
|
|
1803
2350
|
}
|
|
1804
|
-
this.
|
|
2351
|
+
this.stream.reset();
|
|
1805
2352
|
// less than or equal
|
|
2353
|
+
this.stream.mark();
|
|
1806
2354
|
if (this.matchWord('less') &&
|
|
1807
2355
|
this.matchWord('than') &&
|
|
1808
2356
|
this.matchWord('or') &&
|
|
1809
2357
|
this.matchWord('equal')) {
|
|
1810
|
-
|
|
2358
|
+
this.stream.commit();
|
|
2359
|
+
return { condition: AbilityCondition.less_or_equal, operator: TokenTypes.LTE };
|
|
1811
2360
|
}
|
|
1812
|
-
this.
|
|
2361
|
+
this.stream.reset();
|
|
1813
2362
|
// less than
|
|
1814
2363
|
if (this.matchWord('less') && this.matchWord('than')) {
|
|
1815
|
-
return { condition: AbilityCondition.less_than, operator:
|
|
2364
|
+
return { condition: AbilityCondition.less_than, operator: TokenTypes.LT };
|
|
1816
2365
|
}
|
|
1817
|
-
this.
|
|
2366
|
+
this.stream.reset();
|
|
1818
2367
|
// "not contains"
|
|
2368
|
+
this.stream.mark();
|
|
1819
2369
|
if (this.matchWord('not') && this.matchWord('contains')) {
|
|
2370
|
+
this.stream.commit();
|
|
1820
2371
|
return {
|
|
1821
2372
|
condition: AbilityCondition.not_contains,
|
|
1822
|
-
operator:
|
|
2373
|
+
operator: TokenTypes.NOT_CONTAINS,
|
|
1823
2374
|
};
|
|
1824
2375
|
}
|
|
1825
|
-
this.
|
|
2376
|
+
this.stream.reset();
|
|
1826
2377
|
// "not includes"
|
|
2378
|
+
this.stream.mark();
|
|
1827
2379
|
if (this.matchWord('not') && this.matchWord('includes')) {
|
|
2380
|
+
this.stream.commit();
|
|
1828
2381
|
return {
|
|
1829
2382
|
condition: AbilityCondition.not_contains,
|
|
1830
|
-
operator:
|
|
2383
|
+
operator: TokenTypes.NOT_CONTAINS,
|
|
1831
2384
|
};
|
|
1832
2385
|
}
|
|
1833
|
-
this.
|
|
2386
|
+
this.stream.reset();
|
|
1834
2387
|
// "not includes"
|
|
2388
|
+
this.stream.mark();
|
|
1835
2389
|
if (this.matchWord('not') && this.matchWord('has')) {
|
|
2390
|
+
this.stream.commit();
|
|
1836
2391
|
return {
|
|
1837
2392
|
condition: AbilityCondition.not_contains,
|
|
1838
|
-
operator:
|
|
2393
|
+
operator: TokenTypes.NOT_CONTAINS,
|
|
1839
2394
|
};
|
|
1840
2395
|
}
|
|
1841
|
-
this.
|
|
2396
|
+
this.stream.reset();
|
|
1842
2397
|
// "is equals"
|
|
2398
|
+
this.stream.mark();
|
|
1843
2399
|
if (this.matchWord('is') && this.matchWord('equals')) {
|
|
1844
|
-
|
|
2400
|
+
this.stream.commit();
|
|
2401
|
+
return { condition: AbilityCondition.equals, operator: TokenTypes.EQ };
|
|
1845
2402
|
}
|
|
1846
|
-
this.
|
|
2403
|
+
this.stream.reset();
|
|
1847
2404
|
// not equal
|
|
2405
|
+
this.stream.mark();
|
|
1848
2406
|
if (this.matchWord('not') && this.matchWord('equals')) {
|
|
1849
|
-
|
|
2407
|
+
this.stream.commit();
|
|
2408
|
+
return { condition: AbilityCondition.not_equals, operator: TokenTypes.NOT_EQ };
|
|
1850
2409
|
}
|
|
1851
|
-
this.
|
|
2410
|
+
this.stream.reset();
|
|
1852
2411
|
// is not equals
|
|
2412
|
+
this.stream.mark();
|
|
1853
2413
|
if (this.matchWord('is') && this.matchWord('not') && this.matchWord('equals')) {
|
|
1854
|
-
|
|
2414
|
+
this.stream.commit();
|
|
2415
|
+
return { condition: AbilityCondition.not_equals, operator: TokenTypes.NOT_EQ };
|
|
1855
2416
|
}
|
|
1856
|
-
this.
|
|
2417
|
+
this.stream.reset();
|
|
1857
2418
|
// is in
|
|
2419
|
+
this.stream.mark();
|
|
1858
2420
|
if (this.matchWord('is') && this.matchWord('in')) {
|
|
1859
|
-
|
|
2421
|
+
this.stream.commit();
|
|
2422
|
+
return { condition: AbilityCondition.in, operator: TokenTypes.IN };
|
|
1860
2423
|
}
|
|
1861
|
-
this.
|
|
2424
|
+
this.stream.reset();
|
|
1862
2425
|
// not in
|
|
2426
|
+
this.stream.mark();
|
|
1863
2427
|
if (this.matchWord('not') && this.matchWord('in')) {
|
|
1864
|
-
|
|
2428
|
+
this.stream.commit();
|
|
2429
|
+
return { condition: AbilityCondition.not_in, operator: TokenTypes.NOT_IN };
|
|
1865
2430
|
}
|
|
1866
|
-
this.
|
|
2431
|
+
this.stream.reset();
|
|
1867
2432
|
// is not null
|
|
2433
|
+
this.stream.mark();
|
|
1868
2434
|
if (this.matchWord('is') && this.matchWord('not')) {
|
|
1869
|
-
if (this.check(
|
|
1870
|
-
this.
|
|
2435
|
+
if (this.stream.check(TokenTypes.NULL)) {
|
|
2436
|
+
this.stream.next();
|
|
2437
|
+
this.stream.commit();
|
|
1871
2438
|
return {
|
|
1872
2439
|
condition: AbilityCondition.not_equals,
|
|
1873
|
-
operator:
|
|
2440
|
+
operator: TokenTypes.NOT_EQ_NULL,
|
|
1874
2441
|
};
|
|
1875
2442
|
}
|
|
1876
2443
|
}
|
|
1877
|
-
this.
|
|
2444
|
+
this.stream.reset();
|
|
1878
2445
|
// is null
|
|
2446
|
+
this.stream.mark();
|
|
1879
2447
|
if (this.matchWord('is') && this.matchWord('null')) {
|
|
1880
|
-
if (this.check(
|
|
1881
|
-
this.
|
|
2448
|
+
if (this.stream.check(TokenTypes.NULL)) {
|
|
2449
|
+
this.stream.commit();
|
|
2450
|
+
this.stream.next();
|
|
1882
2451
|
return {
|
|
1883
2452
|
condition: AbilityCondition.equals,
|
|
1884
|
-
operator:
|
|
2453
|
+
operator: TokenTypes.EQ_NULL,
|
|
1885
2454
|
};
|
|
1886
2455
|
}
|
|
1887
2456
|
}
|
|
1888
|
-
this.
|
|
2457
|
+
this.stream.reset();
|
|
1889
2458
|
// Single token (symbol or keyword)
|
|
1890
|
-
const token = this.peek();
|
|
1891
|
-
if (token.
|
|
1892
|
-
token.
|
|
1893
|
-
token.
|
|
1894
|
-
this.syntaxError(`Expected comparison operator, got \`${token.value}\``, token, [
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2459
|
+
const token = this.stream.peek();
|
|
2460
|
+
if (token.type !== TokenTypes.SYMBOL &&
|
|
2461
|
+
token.type !== TokenTypes.KEYWORD &&
|
|
2462
|
+
token.type !== TokenTypes.NULL) {
|
|
2463
|
+
this.stream.syntaxError(`Expected comparison operator, got \`${token.value}\``, token, [
|
|
2464
|
+
TokenTypes.SYMBOL,
|
|
2465
|
+
TokenTypes.KEYWORD,
|
|
2466
|
+
TokenTypes.NULL,
|
|
1898
2467
|
]);
|
|
1899
2468
|
}
|
|
1900
|
-
this.
|
|
1901
|
-
switch (token.
|
|
1902
|
-
case
|
|
2469
|
+
this.stream.next();
|
|
2470
|
+
switch (token.type) {
|
|
2471
|
+
case TokenTypes.SYMBOL:
|
|
1903
2472
|
if (token.value === '=' || token.value === '==')
|
|
1904
|
-
return { condition: AbilityCondition.equals, operator:
|
|
2473
|
+
return { condition: AbilityCondition.equals, operator: TokenTypes.EQ };
|
|
1905
2474
|
if (token.value === '!=' || token.value === '<>')
|
|
1906
|
-
return { condition: AbilityCondition.not_equals, operator:
|
|
2475
|
+
return { condition: AbilityCondition.not_equals, operator: TokenTypes.NOT_EQ };
|
|
1907
2476
|
if (token.value === '>')
|
|
1908
|
-
return { condition: AbilityCondition.greater_than, operator:
|
|
2477
|
+
return { condition: AbilityCondition.greater_than, operator: TokenTypes.GT };
|
|
1909
2478
|
if (token.value === '<')
|
|
1910
|
-
return { condition: AbilityCondition.less_than, operator:
|
|
2479
|
+
return { condition: AbilityCondition.less_than, operator: TokenTypes.LT };
|
|
1911
2480
|
if (token.value === '>=')
|
|
1912
|
-
return { condition: AbilityCondition.greater_or_equal, operator:
|
|
2481
|
+
return { condition: AbilityCondition.greater_or_equal, operator: TokenTypes.GTE };
|
|
1913
2482
|
if (token.value === '<=')
|
|
1914
|
-
return { condition: AbilityCondition.less_or_equal, operator:
|
|
2483
|
+
return { condition: AbilityCondition.less_or_equal, operator: TokenTypes.LTE };
|
|
1915
2484
|
break;
|
|
1916
|
-
case
|
|
2485
|
+
case TokenTypes.KEYWORD:
|
|
1917
2486
|
if (token.value === 'contains' || token.value === 'includes' || token.value === 'has')
|
|
1918
|
-
return { condition: AbilityCondition.contains, operator:
|
|
2487
|
+
return { condition: AbilityCondition.contains, operator: TokenTypes.CONTAINS };
|
|
1919
2488
|
if (token.value === 'in')
|
|
1920
|
-
return { condition: AbilityCondition.in, operator:
|
|
2489
|
+
return { condition: AbilityCondition.in, operator: TokenTypes.IN };
|
|
1921
2490
|
if (token.value === 'equals')
|
|
1922
|
-
return { condition: AbilityCondition.equals, operator:
|
|
2491
|
+
return { condition: AbilityCondition.equals, operator: TokenTypes.EQ };
|
|
1923
2492
|
if (token.value === 'gte') {
|
|
1924
|
-
return { condition: AbilityCondition.greater_or_equal, operator:
|
|
2493
|
+
return { condition: AbilityCondition.greater_or_equal, operator: TokenTypes.GTE };
|
|
1925
2494
|
}
|
|
1926
2495
|
if (token.value === 'greater' || token.value === 'gt') {
|
|
1927
2496
|
// If we come here, it means "greater" without "than" – treat as '>'
|
|
1928
|
-
return { condition: AbilityCondition.greater_than, operator:
|
|
2497
|
+
return { condition: AbilityCondition.greater_than, operator: TokenTypes.GT };
|
|
1929
2498
|
}
|
|
1930
2499
|
if (token.value === 'less' || token.value === 'lt') {
|
|
1931
|
-
return { condition: AbilityCondition.less_than, operator:
|
|
2500
|
+
return { condition: AbilityCondition.less_than, operator: TokenTypes.LT };
|
|
1932
2501
|
}
|
|
1933
2502
|
if (token.value === 'lte') {
|
|
1934
|
-
return { condition: AbilityCondition.less_or_equal, operator:
|
|
2503
|
+
return { condition: AbilityCondition.less_or_equal, operator: TokenTypes.LTE };
|
|
1935
2504
|
}
|
|
1936
2505
|
if (token.value === 'is') {
|
|
1937
2506
|
// "is" alone -> equals
|
|
1938
|
-
return { condition: AbilityCondition.equals, operator:
|
|
2507
|
+
return { condition: AbilityCondition.equals, operator: TokenTypes.EQ };
|
|
1939
2508
|
}
|
|
1940
2509
|
break;
|
|
1941
2510
|
}
|
|
1942
|
-
return this.syntaxError(`Unexpected operator token \`${token.value}\``, token, [
|
|
1943
|
-
|
|
1944
|
-
|
|
2511
|
+
return this.stream.syntaxError(`Unexpected operator token \`${token.value}\``, token, [
|
|
2512
|
+
TokenTypes.SYMBOL,
|
|
2513
|
+
TokenTypes.KEYWORD,
|
|
1945
2514
|
]);
|
|
1946
2515
|
}
|
|
1947
2516
|
/**
|
|
@@ -1950,26 +2519,26 @@ class AbilityDSLParser {
|
|
|
1950
2519
|
* @returns True if the next token has that value.
|
|
1951
2520
|
*/
|
|
1952
2521
|
matchWord(word) {
|
|
1953
|
-
if (this.
|
|
2522
|
+
if (this.stream.eof()) {
|
|
1954
2523
|
return false;
|
|
1955
2524
|
}
|
|
1956
|
-
const token = this.peek();
|
|
1957
|
-
if ((token.
|
|
1958
|
-
token.
|
|
1959
|
-
token.
|
|
1960
|
-
token.
|
|
2525
|
+
const token = this.stream.peek();
|
|
2526
|
+
if ((token.type === TokenTypes.KEYWORD ||
|
|
2527
|
+
token.type === TokenTypes.IDENTIFIER ||
|
|
2528
|
+
token.type === TokenTypes.ALWAYS ||
|
|
2529
|
+
token.type === TokenTypes.NEVER) &&
|
|
1961
2530
|
token.value === word) {
|
|
1962
|
-
this.
|
|
2531
|
+
this.stream.next();
|
|
1963
2532
|
return true;
|
|
1964
2533
|
}
|
|
1965
2534
|
return false;
|
|
1966
2535
|
}
|
|
1967
2536
|
matchSymbol(symbol) {
|
|
1968
|
-
if (this.
|
|
2537
|
+
if (this.stream.eof())
|
|
1969
2538
|
return false;
|
|
1970
|
-
const token = this.peek();
|
|
1971
|
-
if (token.
|
|
1972
|
-
this.
|
|
2539
|
+
const token = this.stream.peek();
|
|
2540
|
+
if (token.type === TokenTypes.SYMBOL && token.value === symbol) {
|
|
2541
|
+
this.stream.next();
|
|
1973
2542
|
return true;
|
|
1974
2543
|
}
|
|
1975
2544
|
return false;
|
|
@@ -1983,32 +2552,34 @@ class AbilityDSLParser {
|
|
|
1983
2552
|
*/
|
|
1984
2553
|
parseValue() {
|
|
1985
2554
|
// Arrays start with a left bracket
|
|
1986
|
-
if (this.check(
|
|
1987
|
-
this.
|
|
2555
|
+
if (this.stream.check(TokenTypes.LBRACKET)) {
|
|
2556
|
+
this.stream.next();
|
|
1988
2557
|
return this.parseArray();
|
|
1989
2558
|
}
|
|
1990
2559
|
// Ensure we are not about to read a structural token as a value.
|
|
1991
|
-
const token = this.peek();
|
|
1992
|
-
if (token.
|
|
1993
|
-
token.
|
|
1994
|
-
token.
|
|
1995
|
-
this.syntaxError(`Unexpected ${token.
|
|
2560
|
+
const token = this.stream.peek();
|
|
2561
|
+
if (token.type === TokenTypes.ALL ||
|
|
2562
|
+
token.type === TokenTypes.ANY ||
|
|
2563
|
+
token.type === TokenTypes.EFFECT) {
|
|
2564
|
+
this.stream.syntaxError(`Unexpected ${token.type} in value position`, token);
|
|
1996
2565
|
}
|
|
1997
|
-
this.
|
|
2566
|
+
this.stream.next();
|
|
1998
2567
|
// CHECK THIS SWITCH COMPARE
|
|
1999
|
-
switch (token.
|
|
2000
|
-
case
|
|
2568
|
+
switch (token.type) {
|
|
2569
|
+
case TokenTypes.STRING:
|
|
2001
2570
|
return token.value;
|
|
2002
|
-
case
|
|
2571
|
+
case TokenTypes.NUMBER:
|
|
2003
2572
|
return Number(token.value);
|
|
2004
|
-
case
|
|
2573
|
+
case TokenTypes.BOOLEAN:
|
|
2005
2574
|
return token.value === 'true';
|
|
2006
|
-
case
|
|
2575
|
+
case TokenTypes.NULL:
|
|
2007
2576
|
return null;
|
|
2008
|
-
case
|
|
2577
|
+
case TokenTypes.IDENTIFIER:
|
|
2009
2578
|
return token.value;
|
|
2010
2579
|
default: {
|
|
2011
|
-
this.syntaxError(`Unexpected value token "${token.value}"`, token
|
|
2580
|
+
this.stream.syntaxError(`Unexpected value token "${token.value}"`, token, [
|
|
2581
|
+
TokenTypes.KEYWORD,
|
|
2582
|
+
]);
|
|
2012
2583
|
}
|
|
2013
2584
|
}
|
|
2014
2585
|
}
|
|
@@ -2019,11 +2590,11 @@ class AbilityDSLParser {
|
|
|
2019
2590
|
parseArray() {
|
|
2020
2591
|
const arr = [];
|
|
2021
2592
|
// Handle empty array
|
|
2022
|
-
if (this.check(
|
|
2023
|
-
this.
|
|
2593
|
+
if (this.stream.check(TokenTypes.RBRACKET)) {
|
|
2594
|
+
this.stream.next();
|
|
2024
2595
|
return arr;
|
|
2025
2596
|
}
|
|
2026
|
-
while (!this.
|
|
2597
|
+
while (!this.stream.eof() && !this.stream.check(TokenTypes.RBRACKET)) {
|
|
2027
2598
|
const value = this.parseValue();
|
|
2028
2599
|
// Flatten nested arrays if they appear (though grammar doesn't currently allow nesting).
|
|
2029
2600
|
if (Array.isArray(value)) {
|
|
@@ -2036,155 +2607,359 @@ class AbilityDSLParser {
|
|
|
2036
2607
|
}
|
|
2037
2608
|
else if (value === null) {
|
|
2038
2609
|
// Null is allowed in arrays? Currently, we throw.
|
|
2039
|
-
this.syntaxError('Unexpected null in array', this.peek());
|
|
2610
|
+
this.stream.syntaxError('Unexpected null in array', this.stream.peek());
|
|
2040
2611
|
}
|
|
2041
2612
|
// Optional comma between elements
|
|
2042
|
-
if (this.check(
|
|
2043
|
-
this.
|
|
2613
|
+
if (this.stream.check(TokenTypes.COMMA)) {
|
|
2614
|
+
this.stream.next();
|
|
2044
2615
|
}
|
|
2045
2616
|
}
|
|
2046
|
-
this.
|
|
2617
|
+
this.stream.expect(TokenTypes.RBRACKET, 'Expected "]"');
|
|
2047
2618
|
return arr;
|
|
2048
2619
|
}
|
|
2049
2620
|
// -------------------------------------------------------------------------
|
|
2050
|
-
// #region
|
|
2621
|
+
// #region comments
|
|
2051
2622
|
// -------------------------------------------------------------------------
|
|
2052
2623
|
consumeLeadingComments() {
|
|
2053
|
-
while (this.check(
|
|
2054
|
-
|
|
2055
|
-
this.processCommentToken(token);
|
|
2056
|
-
}
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
const lineIdx = token.line - 1;
|
|
2081
|
-
const lineBefore = lineIdx > 0 ? lines[lineIdx - 1] : '';
|
|
2082
|
-
const current = lines[lineIdx];
|
|
2083
|
-
const linesAfter = lineIdx + 1 < lines.length ? lines[lineIdx + 1] : '';
|
|
2084
|
-
const wave = ' '.repeat(Math.max(0, token.column - 1)) + '~'.repeat(token.value.length);
|
|
2085
|
-
const lineNumWidth = String(token.line + 1).length;
|
|
2086
|
-
const num = (n) => String(n).padStart(lineNumWidth, ' ');
|
|
2087
|
-
let context = '';
|
|
2088
|
-
if (lineBefore.trim() !== '') {
|
|
2089
|
-
context += `${num(token.line - 1)} | ${lineBefore}\n`;
|
|
2090
|
-
}
|
|
2091
|
-
context += `${num(token.line)} | ${current}\n`;
|
|
2092
|
-
context += `${' '.repeat(lineNumWidth)} | ${wave}\n`;
|
|
2093
|
-
if (linesAfter.trim() !== '') {
|
|
2094
|
-
context += `${num(token.line + 1)} | ${linesAfter}`;
|
|
2095
|
-
}
|
|
2096
|
-
let finalDetails = details;
|
|
2097
|
-
if (expected && expected?.length > 0) {
|
|
2098
|
-
const actual = token.value;
|
|
2099
|
-
const suggestion = this.suggest(actual, expected);
|
|
2100
|
-
const detailsMsg = `${details}\nDetails: Unexpected value token \`${actual}\``;
|
|
2101
|
-
finalDetails = suggestion ? `${detailsMsg} Did you mean \`${suggestion}\`?` : detailsMsg;
|
|
2624
|
+
while (this.stream.check(TokenTypes.COMMENT)) {
|
|
2625
|
+
this.stream.next();
|
|
2626
|
+
// this.processCommentToken(token);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
// private _consumeLeadingAnnotations() {
|
|
2630
|
+
// while (this.stream.check(TokenTypes.ANNOTATION)) {
|
|
2631
|
+
// const token = this.stream.next();
|
|
2632
|
+
// this.processAnnotationToken(token);
|
|
2633
|
+
// }
|
|
2634
|
+
// }
|
|
2635
|
+
consumeLeadingAliases() {
|
|
2636
|
+
while (this.stream.check(TokenTypes.ALIAS)) {
|
|
2637
|
+
this.stream.next(); // consume "alias"
|
|
2638
|
+
const nameToken = this.stream.expect(TokenTypes.IDENTIFIER, `Expected alias name`);
|
|
2639
|
+
const aliasKey = nameToken.value;
|
|
2640
|
+
this.stream.expect(TokenTypes.COLON, `Expected colon after an alias`);
|
|
2641
|
+
const annotations = this.takeAnnotations('alias');
|
|
2642
|
+
while (!this.stream.eof() && !this.isStartOfAlias() && !this.isStartOfPolicy()) {
|
|
2643
|
+
const rule = this.parseRule();
|
|
2644
|
+
rule.name = annotations.get('name')?.value || aliasKey;
|
|
2645
|
+
rule.description = annotations.get('description')?.value;
|
|
2646
|
+
if (annotations.get('disabled')?.value === true) {
|
|
2647
|
+
rule.disabled = true;
|
|
2648
|
+
}
|
|
2649
|
+
this.aliasBuffer.set(aliasKey, rule);
|
|
2650
|
+
}
|
|
2102
2651
|
}
|
|
2103
|
-
throw new AbilityDSLSyntaxError(token.line, token.column, context + '\n', finalDetails);
|
|
2104
2652
|
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
if (
|
|
2116
|
-
|
|
2117
|
-
|
|
2653
|
+
consumeLeadingAnnotations() {
|
|
2654
|
+
while (this.stream.check(TokenTypes.ANNOTATION)) {
|
|
2655
|
+
const token = this.stream.next();
|
|
2656
|
+
const text = token.value.trim();
|
|
2657
|
+
if (text.startsWith('@id ')) {
|
|
2658
|
+
this.annBuffer.setID(text.slice(4).trim(), token);
|
|
2659
|
+
}
|
|
2660
|
+
if (text.startsWith('@name ')) {
|
|
2661
|
+
this.annBuffer.setName(text.slice(6).trim(), token);
|
|
2662
|
+
}
|
|
2663
|
+
if (text.startsWith('@description ')) {
|
|
2664
|
+
this.annBuffer.setDescription(text.slice(13).trim(), token);
|
|
2665
|
+
}
|
|
2666
|
+
if (text.startsWith('@priority ')) {
|
|
2667
|
+
this.annBuffer.setPriority(parseInt(text.slice(10).trim(), 10), token);
|
|
2668
|
+
}
|
|
2669
|
+
if (text.startsWith('@disabled')) {
|
|
2670
|
+
const value = text.slice(9).trim();
|
|
2671
|
+
this.annBuffer.setDisabled(value.length === 0 ? true : text.slice(9).trim() === 'true', token);
|
|
2672
|
+
}
|
|
2673
|
+
if (text.startsWith('@tags ')) {
|
|
2674
|
+
const value = text
|
|
2675
|
+
.slice(6)
|
|
2676
|
+
.trim()
|
|
2677
|
+
.split(',')
|
|
2678
|
+
.map(tag => tag.trim());
|
|
2679
|
+
this.annBuffer.setTags(value, token);
|
|
2118
2680
|
}
|
|
2119
2681
|
}
|
|
2120
|
-
return best;
|
|
2121
2682
|
}
|
|
2122
|
-
|
|
2123
|
-
const
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
for (
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2683
|
+
takeAnnotations(owner) {
|
|
2684
|
+
const ann = this.annBuffer.clone();
|
|
2685
|
+
this.annBuffer.clear();
|
|
2686
|
+
const allowed = AnnotationAllowed[owner];
|
|
2687
|
+
for (const key of Object.keys(ann['store'])) {
|
|
2688
|
+
const entry = ann.get(key);
|
|
2689
|
+
if (!entry)
|
|
2690
|
+
continue;
|
|
2691
|
+
if (!allowed.has(key)) {
|
|
2692
|
+
this.stream.syntaxError(`Annotation @${key} is not allowed on ${owner}. Allowed: ${[...allowed]
|
|
2693
|
+
.map(a => '@' + a)
|
|
2694
|
+
.join(', ')}`, entry.token ?? this.stream.peek());
|
|
2132
2695
|
}
|
|
2133
2696
|
}
|
|
2134
|
-
return
|
|
2697
|
+
return ann;
|
|
2135
2698
|
}
|
|
2136
2699
|
// -------------------------------------------------------------------------
|
|
2137
|
-
// #region
|
|
2700
|
+
// #region Helpers
|
|
2138
2701
|
// -------------------------------------------------------------------------
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
for (const t of types) {
|
|
2142
|
-
if (token && token.code === t) {
|
|
2143
|
-
return this.advance();
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
const expected = types.map(t => t).join(', ');
|
|
2147
|
-
const actual = token ? token.value : AbilityDSLToken.EOF;
|
|
2148
|
-
const suggestion = this.suggest(actual, types);
|
|
2149
|
-
const details = `${message}\nDetails: Unexpected token \`${actual}\`, expected one of: ${expected}.`;
|
|
2150
|
-
const finalMsg = suggestion ? `${details} Did you mean \`${suggestion}\`?` : details;
|
|
2151
|
-
this.syntaxError(finalMsg, token ?? this.tokens[this.tokens.length - 1]);
|
|
2702
|
+
isStartOfPolicy() {
|
|
2703
|
+
return this.stream.check(TokenTypes.EFFECT);
|
|
2152
2704
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2705
|
+
isStartOfGroup() {
|
|
2706
|
+
return this.stream.check(TokenTypes.ALL) || this.stream.check(TokenTypes.ANY);
|
|
2707
|
+
}
|
|
2708
|
+
isStartOfExcept() {
|
|
2709
|
+
return this.stream.check(TokenTypes.EXCEPT);
|
|
2710
|
+
}
|
|
2711
|
+
isStartOfAlias() {
|
|
2712
|
+
return this.stream.check(TokenTypes.ALIAS);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
function ability(strings, ...expr) {
|
|
2717
|
+
const dsl = strings.reduce((acc, s, i) => acc + s + (expr[i] ?? ''), '');
|
|
2718
|
+
return new AbilityDSLParser(dsl).parse();
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
class AbilityStrategy {
|
|
2722
|
+
policies;
|
|
2723
|
+
matched;
|
|
2724
|
+
constructor(policies) {
|
|
2725
|
+
this.policies = policies;
|
|
2726
|
+
this.matched = policies.filter(p => p.matchState === AbilityMatch.match);
|
|
2727
|
+
}
|
|
2728
|
+
matchedPolicies() {
|
|
2729
|
+
return this.matched;
|
|
2730
|
+
}
|
|
2731
|
+
firstMatched() {
|
|
2732
|
+
return this.matched[0] ?? null;
|
|
2733
|
+
}
|
|
2734
|
+
lastMatched() {
|
|
2735
|
+
return this.matched.length > 0 ? this.matched[this.matched.length - 1] : null;
|
|
2736
|
+
}
|
|
2737
|
+
hasPermit() {
|
|
2738
|
+
return this.matched.some(p => p.effect === AbilityPolicyEffect.permit);
|
|
2739
|
+
}
|
|
2740
|
+
hasDeny() {
|
|
2741
|
+
return this.matched.some(p => p.effect === AbilityPolicyEffect.deny);
|
|
2742
|
+
}
|
|
2743
|
+
isAllowed() {
|
|
2744
|
+
return this.evaluate() === AbilityPolicyEffect.permit;
|
|
2745
|
+
}
|
|
2746
|
+
isDenied() {
|
|
2747
|
+
return this.evaluate() === AbilityPolicyEffect.deny;
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
/**
|
|
2752
|
+
* AllMustPermitStrategy
|
|
2753
|
+
*
|
|
2754
|
+
* This strategy requires *every applicable policy* to return "permit".
|
|
2755
|
+
* If at least one policy returns "deny" or "not applicable", the final result is "deny".
|
|
2756
|
+
*
|
|
2757
|
+
* Use this strategy when:
|
|
2758
|
+
* - You want strict, conservative access control.
|
|
2759
|
+
* - All rules must explicitly allow the action.
|
|
2760
|
+
*
|
|
2761
|
+
* Example:
|
|
2762
|
+
* Policies:
|
|
2763
|
+
* P1 → permit
|
|
2764
|
+
* P2 → permit
|
|
2765
|
+
* P3 → deny
|
|
2766
|
+
* Result: deny (because not all policies permitted)
|
|
2767
|
+
*/
|
|
2768
|
+
class AllMustPermitStrategy extends AbilityStrategy {
|
|
2769
|
+
evaluate() {
|
|
2770
|
+
const matched = this.matchedPolicies();
|
|
2771
|
+
if (matched.length === 0) {
|
|
2772
|
+
return AbilityPolicyEffect.deny;
|
|
2157
2773
|
}
|
|
2158
|
-
const
|
|
2159
|
-
|
|
2160
|
-
const suggestion = this.suggest(actual, [type]);
|
|
2161
|
-
const details = `${message}\nDetails: Unexpected token \`${actual}\`, expected "${expected}".`;
|
|
2162
|
-
const finalMsg = suggestion ? `${details} Did you mean \`${suggestion}\`?` : details;
|
|
2163
|
-
this.syntaxError(finalMsg, token ?? this.tokens[this.tokens.length - 1]);
|
|
2774
|
+
const allPermit = matched.every(p => p.effect === AbilityPolicyEffect.permit);
|
|
2775
|
+
return allPermit ? AbilityPolicyEffect.permit : AbilityPolicyEffect.deny;
|
|
2164
2776
|
}
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
/**
|
|
2780
|
+
* AnyPermitStrategy
|
|
2781
|
+
*
|
|
2782
|
+
* This strategy returns "permit" as soon as *any* applicable policy permits the action.
|
|
2783
|
+
* If no policy permits, the result is "deny".
|
|
2784
|
+
*
|
|
2785
|
+
* Use this strategy when:
|
|
2786
|
+
* - You want optimistic access control.
|
|
2787
|
+
* - A single positive rule should be enough to grant access.
|
|
2788
|
+
*
|
|
2789
|
+
* Example:
|
|
2790
|
+
* Policies:
|
|
2791
|
+
* P1 → deny
|
|
2792
|
+
* P2 → permit
|
|
2793
|
+
* P3 → deny
|
|
2794
|
+
* Result: permit (because at least one policy permitted)
|
|
2795
|
+
*/
|
|
2796
|
+
class AnyPermitStrategy extends AbilityStrategy {
|
|
2797
|
+
evaluate() {
|
|
2798
|
+
return this.hasPermit() ? AbilityPolicyEffect.permit : AbilityPolicyEffect.deny;
|
|
2169
2799
|
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
/**
|
|
2803
|
+
* DenyOverridesStrategy
|
|
2804
|
+
*
|
|
2805
|
+
* This strategy gives absolute priority to "deny".
|
|
2806
|
+
* If any applicable policy returns "deny", the final result is "deny".
|
|
2807
|
+
* Otherwise, if at least one policy permits, the result is "permit".
|
|
2808
|
+
*
|
|
2809
|
+
* Use this strategy when:
|
|
2810
|
+
* - Security is critical.
|
|
2811
|
+
* - A single denial must block access.
|
|
2812
|
+
*
|
|
2813
|
+
* Example:
|
|
2814
|
+
* Policies:
|
|
2815
|
+
* P1 → permit
|
|
2816
|
+
* P2 → deny
|
|
2817
|
+
* P3 → permit
|
|
2818
|
+
* Result: deny (because deny overrides everything)
|
|
2819
|
+
*/
|
|
2820
|
+
class DenyOverridesStrategy extends AbilityStrategy {
|
|
2821
|
+
evaluate() {
|
|
2822
|
+
if (this.hasDeny()) {
|
|
2823
|
+
return AbilityPolicyEffect.deny;
|
|
2824
|
+
}
|
|
2825
|
+
if (this.hasPermit()) {
|
|
2826
|
+
return AbilityPolicyEffect.permit;
|
|
2827
|
+
}
|
|
2828
|
+
return AbilityPolicyEffect.deny;
|
|
2172
2829
|
}
|
|
2173
|
-
|
|
2174
|
-
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
/**
|
|
2833
|
+
* FirstMatchStrategy
|
|
2834
|
+
*
|
|
2835
|
+
* This strategy evaluates policies in order and returns the result of the *first applicable* policy.
|
|
2836
|
+
* Remaining policies are ignored.
|
|
2837
|
+
*
|
|
2838
|
+
* Use this strategy when:
|
|
2839
|
+
* - Policy order matters.
|
|
2840
|
+
* - You want predictable, sequential rule evaluation.
|
|
2841
|
+
*
|
|
2842
|
+
* Example:
|
|
2843
|
+
* Policies:
|
|
2844
|
+
* P1 → not applicable
|
|
2845
|
+
* P2 → permit
|
|
2846
|
+
* P3 → deny
|
|
2847
|
+
* Result: permit (P2 is the first applicable)
|
|
2848
|
+
*/
|
|
2849
|
+
class FirstMatchStrategy extends AbilityStrategy {
|
|
2850
|
+
evaluate() {
|
|
2851
|
+
const first = this.firstMatched();
|
|
2852
|
+
return first?.effect ?? AbilityPolicyEffect.deny;
|
|
2175
2853
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
/**
|
|
2857
|
+
* OnlyOneApplicableStrategy
|
|
2858
|
+
*
|
|
2859
|
+
* This strategy requires that *exactly one* policy is applicable.
|
|
2860
|
+
* If zero or more than one policy applies, the result is "deny".
|
|
2861
|
+
*
|
|
2862
|
+
* Use this strategy when:
|
|
2863
|
+
* - Policies must be mutually exclusive.
|
|
2864
|
+
* - You want to detect ambiguous or conflicting rules.
|
|
2865
|
+
*
|
|
2866
|
+
* Example:
|
|
2867
|
+
* Policies:
|
|
2868
|
+
* P1 → applicable
|
|
2869
|
+
* P2 → applicable
|
|
2870
|
+
* Result: deny (more than one applicable policy)
|
|
2871
|
+
*/
|
|
2872
|
+
class OnlyOneApplicableStrategy extends AbilityStrategy {
|
|
2873
|
+
evaluate() {
|
|
2874
|
+
const matched = this.matchedPolicies();
|
|
2875
|
+
if (matched.length === 1) {
|
|
2876
|
+
return matched[0].effect;
|
|
2877
|
+
}
|
|
2878
|
+
return AbilityPolicyEffect.deny;
|
|
2178
2879
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
/**
|
|
2883
|
+
* PermitOverridesStrategy
|
|
2884
|
+
*
|
|
2885
|
+
* This strategy gives priority to "permit".
|
|
2886
|
+
* If any applicable policy permits, the final result is "permit".
|
|
2887
|
+
* Deny is returned only if no policy permits.
|
|
2888
|
+
*
|
|
2889
|
+
* Use this strategy when:
|
|
2890
|
+
* - You want permissive behavior.
|
|
2891
|
+
* - A single positive rule should override denials.
|
|
2892
|
+
*
|
|
2893
|
+
* Example:
|
|
2894
|
+
* Policies:
|
|
2895
|
+
* P1 → deny
|
|
2896
|
+
* P2 → permit
|
|
2897
|
+
* P3 → deny
|
|
2898
|
+
* Result: permit (permit overrides deny)
|
|
2899
|
+
*/
|
|
2900
|
+
class PermitOverridesStrategy extends AbilityStrategy {
|
|
2901
|
+
evaluate() {
|
|
2902
|
+
if (this.hasPermit()) {
|
|
2903
|
+
return AbilityPolicyEffect.permit;
|
|
2904
|
+
}
|
|
2905
|
+
if (this.hasDeny()) {
|
|
2906
|
+
return AbilityPolicyEffect.deny;
|
|
2907
|
+
}
|
|
2908
|
+
return AbilityPolicyEffect.deny;
|
|
2181
2909
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
/**
|
|
2913
|
+
* SequentialLastMatchStrategy
|
|
2914
|
+
*
|
|
2915
|
+
* This strategy evaluates all applicable policies in order and returns the result of the *last* applicable one.
|
|
2916
|
+
*
|
|
2917
|
+
* Use this strategy when:
|
|
2918
|
+
* - Later policies should override earlier ones.
|
|
2919
|
+
* - You want a "last rule wins" behavior.
|
|
2920
|
+
*
|
|
2921
|
+
* Example:
|
|
2922
|
+
* Policies:
|
|
2923
|
+
* P1 → permit
|
|
2924
|
+
* P2 → deny
|
|
2925
|
+
* P3 → permit
|
|
2926
|
+
* Result: permit (P3 is the last applicable)
|
|
2927
|
+
*/
|
|
2928
|
+
class SequentialLastMatchStrategy extends AbilityStrategy {
|
|
2929
|
+
evaluate() {
|
|
2930
|
+
const last = this.lastMatched();
|
|
2931
|
+
return last?.effect ?? AbilityPolicyEffect.deny;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
/**
|
|
2936
|
+
* PriorityStrategy
|
|
2937
|
+
*
|
|
2938
|
+
* This strategy evaluates policies based on their numeric priority.
|
|
2939
|
+
* The policy with the highest priority (lowest number or highest number depending on implementation)
|
|
2940
|
+
* determines the final result.
|
|
2941
|
+
*
|
|
2942
|
+
* Use this strategy when:
|
|
2943
|
+
* - Policies have explicit priority levels.
|
|
2944
|
+
* - You want deterministic resolution based on ranking.
|
|
2945
|
+
*
|
|
2946
|
+
* Example:
|
|
2947
|
+
* Policies:
|
|
2948
|
+
* P1 (priority 10) → deny
|
|
2949
|
+
* P2 (priority 1) → permit
|
|
2950
|
+
* Result: permit (P2 has higher priority)
|
|
2951
|
+
*/
|
|
2952
|
+
class PriorityStrategy extends AbilityStrategy {
|
|
2953
|
+
evaluate() {
|
|
2954
|
+
const matched = this.matchedPolicies();
|
|
2955
|
+
if (matched.length === 0) {
|
|
2956
|
+
return AbilityPolicyEffect.deny;
|
|
2957
|
+
}
|
|
2958
|
+
const sorted = [...matched].sort((a, b) => b.priority - a.priority);
|
|
2959
|
+
return sorted[0].effect;
|
|
2184
2960
|
}
|
|
2185
2961
|
}
|
|
2186
2962
|
|
|
2187
|
-
exports.AbilityCode = AbilityCode;
|
|
2188
2963
|
exports.AbilityCompare = AbilityCompare;
|
|
2189
2964
|
exports.AbilityCondition = AbilityCondition;
|
|
2190
2965
|
exports.AbilityDSLLexer = AbilityDSLLexer;
|
|
@@ -2202,7 +2977,21 @@ exports.AbilityPolicy = AbilityPolicy;
|
|
|
2202
2977
|
exports.AbilityPolicyEffect = AbilityPolicyEffect;
|
|
2203
2978
|
exports.AbilityResolver = AbilityResolver;
|
|
2204
2979
|
exports.AbilityResult = AbilityResult;
|
|
2205
|
-
exports.AbilityResultState = AbilityResultState;
|
|
2206
2980
|
exports.AbilityRule = AbilityRule;
|
|
2207
2981
|
exports.AbilityRuleSet = AbilityRuleSet;
|
|
2982
|
+
exports.AbilityStrategy = AbilityStrategy;
|
|
2208
2983
|
exports.AbilityTypeGenerator = AbilityTypeGenerator;
|
|
2984
|
+
exports.AllMustPermitStrategy = AllMustPermitStrategy;
|
|
2985
|
+
exports.AnyPermitStrategy = AnyPermitStrategy;
|
|
2986
|
+
exports.DenyOverridesStrategy = DenyOverridesStrategy;
|
|
2987
|
+
exports.FirstMatchStrategy = FirstMatchStrategy;
|
|
2988
|
+
exports.OnlyOneApplicableStrategy = OnlyOneApplicableStrategy;
|
|
2989
|
+
exports.PermitOverridesStrategy = PermitOverridesStrategy;
|
|
2990
|
+
exports.PriorityStrategy = PriorityStrategy;
|
|
2991
|
+
exports.SequentialLastMatchStrategy = SequentialLastMatchStrategy;
|
|
2992
|
+
exports.TokenTypes = TokenTypes;
|
|
2993
|
+
exports.ability = ability;
|
|
2994
|
+
exports.fromLiteral = fromLiteral;
|
|
2995
|
+
exports.isConditionEqual = isConditionEqual;
|
|
2996
|
+
exports.isConditionNotEqual = isConditionNotEqual;
|
|
2997
|
+
exports.toLiteral = toLiteral;
|