@via-profit/ability 3.5.3 → 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 +1490 -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,37 +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
|
});
|
|
218
|
+
const filteredStructure = {};
|
|
219
|
+
Object.entries(resorceStructure).forEach(([action, fields]) => {
|
|
220
|
+
if (!action.endsWith('.*')) {
|
|
221
|
+
filteredStructure[action] = fields;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
183
224
|
// Transform flat structure into nested structure for easier use
|
|
184
|
-
const nestedStructure = this.buildNestedStructure(
|
|
185
|
-
|
|
225
|
+
const nestedStructure = this.buildNestedStructure(filteredStructure);
|
|
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('.');
|
|
186
237
|
}
|
|
187
238
|
/**
|
|
188
239
|
* Determines TypeScript type based on the rule
|
|
@@ -190,25 +241,32 @@ class AbilityTypeGenerator {
|
|
|
190
241
|
* @returns TypeScript type as string
|
|
191
242
|
*/
|
|
192
243
|
determineTypeFromRule(rule) {
|
|
193
|
-
if (rule.condition.
|
|
194
|
-
rule.condition.isEqual(AbilityCondition.always)) {
|
|
244
|
+
if (rule.condition === AbilityCondition.never || rule.condition === AbilityCondition.always) {
|
|
195
245
|
return null;
|
|
196
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
|
+
}
|
|
197
256
|
// Numeric comparisons - always number
|
|
198
|
-
if (rule.condition
|
|
199
|
-
rule.condition
|
|
200
|
-
rule.condition
|
|
201
|
-
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) {
|
|
202
261
|
return 'number';
|
|
203
262
|
}
|
|
204
263
|
// Array operations
|
|
205
|
-
if (rule.condition.
|
|
206
|
-
|
|
207
|
-
return this.getArrayType(rule.resource);
|
|
264
|
+
if (rule.condition === AbilityCondition.in || rule.condition === AbilityCondition.not_in) {
|
|
265
|
+
return this.getInArrayType(rule.resource);
|
|
208
266
|
}
|
|
209
267
|
// Equality/Inequality operations
|
|
210
|
-
if (rule.condition
|
|
211
|
-
rule.condition
|
|
268
|
+
if (rule.condition === AbilityCondition.equals ||
|
|
269
|
+
rule.condition === AbilityCondition.not_equals) {
|
|
212
270
|
return this.getPrimitiveType(rule.resource);
|
|
213
271
|
}
|
|
214
272
|
return 'any';
|
|
@@ -219,20 +277,38 @@ class AbilityTypeGenerator {
|
|
|
219
277
|
* @returns TypeScript array type as string
|
|
220
278
|
*/
|
|
221
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) {
|
|
222
299
|
if (Array.isArray(resource)) {
|
|
223
300
|
if (resource.length === 0) {
|
|
224
|
-
return '
|
|
301
|
+
return 'unknown';
|
|
225
302
|
}
|
|
226
303
|
// Determine types of array elements
|
|
227
304
|
const elementTypes = new Set(resource.map(item => this.getPrimitiveType(item)));
|
|
228
|
-
|
|
305
|
+
return elementTypes.size === 1
|
|
229
306
|
? Array.from(elementTypes)[0]
|
|
230
307
|
: `(${Array.from(elementTypes).join(' | ')})`;
|
|
231
|
-
return `readonly ${elementType}[]`;
|
|
232
308
|
}
|
|
233
309
|
// If resource is not an array but condition is in/not_in,
|
|
234
310
|
// it expects an array of such elements
|
|
235
|
-
return
|
|
311
|
+
return this.getPrimitiveType(resource);
|
|
236
312
|
}
|
|
237
313
|
/**
|
|
238
314
|
* Gets primitive TypeScript type for a value
|
|
@@ -241,11 +317,15 @@ class AbilityTypeGenerator {
|
|
|
241
317
|
*/
|
|
242
318
|
getPrimitiveType(value) {
|
|
243
319
|
if (value === null) {
|
|
244
|
-
return 'null';
|
|
320
|
+
return 'null | unknown';
|
|
245
321
|
}
|
|
246
322
|
if (value === undefined) {
|
|
247
323
|
return 'undefined';
|
|
248
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
|
+
}
|
|
249
329
|
switch (typeof value) {
|
|
250
330
|
case 'string':
|
|
251
331
|
return 'string';
|
|
@@ -298,9 +378,11 @@ class AbilityTypeGenerator {
|
|
|
298
378
|
/**
|
|
299
379
|
* Formats type structure into a string
|
|
300
380
|
* @param structure - Nested type structure
|
|
381
|
+
* @param environment
|
|
382
|
+
* @param allTags
|
|
301
383
|
* @returns Formatted TypeScript type definition string
|
|
302
384
|
*/
|
|
303
|
-
formatTypeDefinitions(structure) {
|
|
385
|
+
formatTypeDefinitions(structure, environment, allTags) {
|
|
304
386
|
let output = '// Automatically generated by via-profit/ability\n';
|
|
305
387
|
output += '// Do not edit manually\n';
|
|
306
388
|
output += 'export type Resources = {\n';
|
|
@@ -320,6 +402,29 @@ class AbilityTypeGenerator {
|
|
|
320
402
|
}
|
|
321
403
|
});
|
|
322
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
|
|
323
428
|
return output;
|
|
324
429
|
}
|
|
325
430
|
/**
|
|
@@ -363,12 +468,12 @@ class AbilityExplain {
|
|
|
363
468
|
}
|
|
364
469
|
toString(indent = 0) {
|
|
365
470
|
const pad = ' '.repeat(indent);
|
|
366
|
-
const mark = this.match
|
|
471
|
+
const mark = this.match === AbilityMatch.match ? '✓' : '✗';
|
|
367
472
|
let out = '';
|
|
368
473
|
if (this.type === 'policy') {
|
|
369
474
|
out += '\n';
|
|
370
475
|
}
|
|
371
|
-
out += `${pad}${mark} ${this.type} «${this.name}» is ${this.match
|
|
476
|
+
out += `${pad}${mark} ${this.type} «${this.name}» is ${this.match}`;
|
|
372
477
|
this.children.forEach(child => {
|
|
373
478
|
out += '\n' + child.toString(indent + 1);
|
|
374
479
|
});
|
|
@@ -399,7 +504,7 @@ class AbilityExplainPolicy extends AbilityExplain {
|
|
|
399
504
|
const children = policy.ruleSet.map(ruleSet => new AbilityExplainRuleSet(ruleSet));
|
|
400
505
|
super({
|
|
401
506
|
type: 'policy',
|
|
402
|
-
name: policy.name,
|
|
507
|
+
name: policy.priority > -1 ? `@priority ${policy.priority} ${policy.name}` : policy.name,
|
|
403
508
|
match: policy.matchState,
|
|
404
509
|
}, children);
|
|
405
510
|
}
|
|
@@ -426,6 +531,7 @@ class AbilityPolicy {
|
|
|
426
531
|
* Policy name
|
|
427
532
|
*/
|
|
428
533
|
name;
|
|
534
|
+
description;
|
|
429
535
|
/**
|
|
430
536
|
* Policy ID
|
|
431
537
|
*/
|
|
@@ -435,13 +541,20 @@ class AbilityPolicy {
|
|
|
435
541
|
* will select only those from all passed policies that fall under the specified permission key.
|
|
436
542
|
*/
|
|
437
543
|
permission;
|
|
544
|
+
priority = -1;
|
|
545
|
+
disabled;
|
|
546
|
+
tags;
|
|
438
547
|
constructor(params) {
|
|
439
|
-
const { name, id, permission, effect, compareMethod = AbilityCompare.and } = params;
|
|
440
|
-
this.
|
|
441
|
-
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;
|
|
442
551
|
this.permission = permission;
|
|
552
|
+
this.description = description;
|
|
443
553
|
this.effect = effect;
|
|
444
554
|
this.compareMethod = compareMethod;
|
|
555
|
+
this.priority = typeof priority === 'number' ? priority : -1;
|
|
556
|
+
this.disabled = typeof disabled === 'boolean' ? disabled : false;
|
|
557
|
+
this.tags = (tags || []);
|
|
445
558
|
}
|
|
446
559
|
/**
|
|
447
560
|
* Add rule set to the policy
|
|
@@ -461,6 +574,18 @@ class AbilityPolicy {
|
|
|
461
574
|
}
|
|
462
575
|
return this;
|
|
463
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
|
+
}
|
|
464
589
|
/**
|
|
465
590
|
* Check if the policy is matched
|
|
466
591
|
* @param resource - The resource to check
|
|
@@ -468,31 +593,57 @@ class AbilityPolicy {
|
|
|
468
593
|
*/
|
|
469
594
|
check(resource, environment) {
|
|
470
595
|
this.matchState = AbilityMatch.mismatch;
|
|
596
|
+
if (this.disabled) {
|
|
597
|
+
this.matchState = AbilityMatch.disabled;
|
|
598
|
+
return this.matchState;
|
|
599
|
+
}
|
|
471
600
|
if (!this.ruleSet.length) {
|
|
472
601
|
return this.matchState;
|
|
473
602
|
}
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (
|
|
479
|
-
|
|
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;
|
|
480
609
|
}
|
|
481
|
-
|
|
482
|
-
|
|
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;
|
|
483
614
|
return this.matchState;
|
|
484
615
|
}
|
|
485
|
-
|
|
486
|
-
if (AbilityCompare.and.isEqual(this.compareMethod)) {
|
|
487
|
-
if (rulesetCheckStates.every(s => AbilityMatch.match.isEqual(s))) {
|
|
616
|
+
if (AbilityCompare.or === this.compareMethod && AbilityMatch.match === state) {
|
|
488
617
|
this.matchState = AbilityMatch.match;
|
|
618
|
+
// break to check except-rule sets
|
|
619
|
+
break;
|
|
489
620
|
}
|
|
490
621
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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;
|
|
494
643
|
}
|
|
495
644
|
}
|
|
645
|
+
// 5. match
|
|
646
|
+
this.matchState = AbilityMatch.match;
|
|
496
647
|
return this.matchState;
|
|
497
648
|
}
|
|
498
649
|
explain() {
|
|
@@ -505,6 +656,8 @@ class AbilityPolicy {
|
|
|
505
656
|
const policy = new AbilityPolicy({
|
|
506
657
|
id: props.id ?? this.id,
|
|
507
658
|
name: props.name ?? this.name,
|
|
659
|
+
description: props.description ?? this.description,
|
|
660
|
+
priority: typeof props.priority !== 'undefined' ? props.priority : this.priority,
|
|
508
661
|
permission: props.permission ?? this.permission,
|
|
509
662
|
effect: props.effect ?? this.effect,
|
|
510
663
|
compareMethod: props.compareMethod ?? this.compareMethod,
|
|
@@ -517,24 +670,20 @@ class AbilityPolicy {
|
|
|
517
670
|
}
|
|
518
671
|
}
|
|
519
672
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
static permit = new AbilityPolicyEffect('permit');
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
class AbilityResultState extends AbilityCode {
|
|
526
|
-
static allow = new AbilityResultState('allow');
|
|
527
|
-
static deny = new AbilityResultState('deny');
|
|
528
|
-
static neutral = new AbilityResultState('neutral');
|
|
673
|
+
function brand$1(code) {
|
|
674
|
+
return code;
|
|
529
675
|
}
|
|
676
|
+
const AbilityPolicyEffect = {
|
|
677
|
+
deny: brand$1('deny'),
|
|
678
|
+
permit: brand$1('permit'),
|
|
679
|
+
};
|
|
530
680
|
|
|
531
681
|
class AbilityResult {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
this.policies = policies;
|
|
682
|
+
effect;
|
|
683
|
+
strategy;
|
|
684
|
+
constructor(effect, strategy) {
|
|
685
|
+
this.effect = effect;
|
|
686
|
+
this.strategy = strategy;
|
|
538
687
|
}
|
|
539
688
|
/**
|
|
540
689
|
* Returns a list of explanations for each policy involved in the ability evaluation.
|
|
@@ -543,53 +692,37 @@ class AbilityResult {
|
|
|
543
692
|
* Useful for debugging, logging, or building UI tools that visualize permission logic.
|
|
544
693
|
*/
|
|
545
694
|
explain() {
|
|
546
|
-
return this.policies.map(policy => {
|
|
695
|
+
return this.strategy.policies.map(policy => {
|
|
547
696
|
return new AbilityExplainPolicy(policy);
|
|
548
697
|
});
|
|
549
698
|
}
|
|
550
|
-
getLastMatchedPolicy() {
|
|
551
|
-
for (let i = this.policies.length - 1; i >= 0; i--) {
|
|
552
|
-
if (this.policies[i].matchState.isEqual(AbilityMatch.match)) {
|
|
553
|
-
return this.policies[i];
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
return null;
|
|
557
|
-
}
|
|
558
|
-
getFinalState() {
|
|
559
|
-
let state = AbilityResultState.neutral;
|
|
560
|
-
for (const p of this.policies) {
|
|
561
|
-
if (p.matchState.isEqual(AbilityMatch.match)) {
|
|
562
|
-
if (p.effect.isEqual(AbilityPolicyEffect.permit)) {
|
|
563
|
-
state = AbilityResultState.allow;
|
|
564
|
-
}
|
|
565
|
-
else if (p.effect.isEqual(AbilityPolicyEffect.deny)) {
|
|
566
|
-
state = AbilityResultState.deny;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
else if (p.matchState.isEqual(AbilityMatch.mismatch)) {
|
|
570
|
-
state = AbilityResultState.neutral;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
return state;
|
|
574
|
-
}
|
|
575
699
|
isAllowed() {
|
|
576
|
-
return this.
|
|
700
|
+
return this.strategy.isAllowed();
|
|
577
701
|
}
|
|
578
702
|
isDenied() {
|
|
579
|
-
return this.
|
|
703
|
+
return this.strategy.isDenied();
|
|
580
704
|
}
|
|
581
705
|
}
|
|
582
706
|
|
|
583
707
|
class AbilityResolver {
|
|
584
|
-
|
|
708
|
+
StrategyClass;
|
|
709
|
+
policyEntries;
|
|
585
710
|
constructor(
|
|
586
711
|
/**
|
|
587
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.
|
|
588
713
|
*/
|
|
589
|
-
policyOrListOfPolicies) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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;
|
|
593
726
|
}
|
|
594
727
|
/**
|
|
595
728
|
* Resolve policy for the resource and permission key
|
|
@@ -599,24 +732,30 @@ class AbilityResolver {
|
|
|
599
732
|
* @param environment
|
|
600
733
|
*/
|
|
601
734
|
resolve(permission, resource, environment) {
|
|
602
|
-
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
|
|
603
741
|
for (const policy of filteredPolicies) {
|
|
742
|
+
if (policy.disabled) {
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
604
745
|
const policyMatchState = policy.check(resource, environment);
|
|
605
746
|
if (policyMatchState === AbilityMatch.pending) {
|
|
606
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.`);
|
|
607
748
|
}
|
|
608
749
|
}
|
|
609
|
-
|
|
750
|
+
// 3. Use strategy
|
|
751
|
+
const strategy = new this.StrategyClass(filteredPolicies);
|
|
752
|
+
const effect = strategy.evaluate();
|
|
753
|
+
return new AbilityResult(effect, strategy);
|
|
610
754
|
}
|
|
611
755
|
enforce(permission, resource, environment) {
|
|
612
756
|
const result = this.resolve(permission, resource, environment);
|
|
613
757
|
if (result.isDenied()) {
|
|
614
|
-
|
|
615
|
-
if (lastPolicy) {
|
|
616
|
-
throw new AbilityError(`Permission denied by policy "${lastPolicy.name.toString()}"`);
|
|
617
|
-
}
|
|
618
|
-
// No policy matched → implicit deny
|
|
619
|
-
throw new AbilityError(`Permission denied: no matching policy found (implicit deny)`);
|
|
758
|
+
throw new AbilityError(`Permission denied`);
|
|
620
759
|
}
|
|
621
760
|
}
|
|
622
761
|
/**
|
|
@@ -632,6 +771,32 @@ class AbilityResolver {
|
|
|
632
771
|
return chunk === '*' || longer[i] === '*' || chunk === longer[i];
|
|
633
772
|
});
|
|
634
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
|
+
}
|
|
635
800
|
}
|
|
636
801
|
|
|
637
802
|
/**
|
|
@@ -648,8 +813,10 @@ class AbilityRule {
|
|
|
648
813
|
resource;
|
|
649
814
|
condition;
|
|
650
815
|
name;
|
|
816
|
+
description;
|
|
651
817
|
id;
|
|
652
818
|
state = AbilityMatch.pending;
|
|
819
|
+
disabled;
|
|
653
820
|
/**
|
|
654
821
|
* Creates an instance of AbilityRule.
|
|
655
822
|
* @param {string} params.id - The unique identifier of the rule.
|
|
@@ -657,33 +824,37 @@ class AbilityRule {
|
|
|
657
824
|
* @param {AbilityCondition} params.condition - The condition to evaluate.
|
|
658
825
|
* @param {string} params.subject - The subject of the rule.
|
|
659
826
|
* @param {string} params.resource - The resource to compare against.
|
|
827
|
+
* @param {boolean} params.disabled - Disabling flag.
|
|
660
828
|
* @param params
|
|
661
829
|
*/
|
|
662
830
|
constructor(params) {
|
|
663
|
-
const { id, name, subject, resource, condition } = params;
|
|
664
|
-
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)}`;
|
|
665
833
|
this.id = id || this.name;
|
|
834
|
+
this.description = description;
|
|
835
|
+
this.disabled = typeof disabled === 'boolean' ? disabled : false;
|
|
666
836
|
this.subject = subject;
|
|
667
837
|
this.resource = resource;
|
|
668
838
|
this.condition = condition;
|
|
839
|
+
this.state = this.disabled ? AbilityMatch.disabled : this.state;
|
|
669
840
|
}
|
|
670
|
-
isPrimitive(v) {
|
|
841
|
+
static isPrimitive(v) {
|
|
671
842
|
return typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null;
|
|
672
843
|
}
|
|
673
|
-
isNumber(v) {
|
|
844
|
+
static isNumber(v) {
|
|
674
845
|
return typeof v === 'number';
|
|
675
846
|
}
|
|
676
|
-
isString(v) {
|
|
847
|
+
static isString(v) {
|
|
677
848
|
return typeof v === 'string';
|
|
678
849
|
}
|
|
679
|
-
valueLen = (v) => this.isString(v) || Array.isArray(v) ? v.length : null;
|
|
680
|
-
operatorHandlers = {
|
|
681
|
-
[AbilityCondition.always
|
|
682
|
-
[AbilityCondition.never
|
|
683
|
-
[AbilityCondition.equals
|
|
684
|
-
[AbilityCondition.not_equals
|
|
685
|
-
[AbilityCondition.contains
|
|
686
|
-
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)) {
|
|
687
858
|
return a.includes(b);
|
|
688
859
|
}
|
|
689
860
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
@@ -691,8 +862,8 @@ class AbilityRule {
|
|
|
691
862
|
}
|
|
692
863
|
return false;
|
|
693
864
|
},
|
|
694
|
-
[AbilityCondition.not_contains
|
|
695
|
-
if (Array.isArray(a) &&
|
|
865
|
+
[toLiteral(AbilityCondition.not_contains)]: (a, b) => {
|
|
866
|
+
if (Array.isArray(a) && AbilityRule.isPrimitive(b)) {
|
|
696
867
|
return !a.includes(b);
|
|
697
868
|
}
|
|
698
869
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
@@ -700,8 +871,8 @@ class AbilityRule {
|
|
|
700
871
|
}
|
|
701
872
|
return false;
|
|
702
873
|
},
|
|
703
|
-
[AbilityCondition.in
|
|
704
|
-
if (
|
|
874
|
+
[toLiteral(AbilityCondition.in)]: (a, b) => {
|
|
875
|
+
if (AbilityRule.isPrimitive(a) && Array.isArray(b)) {
|
|
705
876
|
return b.includes(a);
|
|
706
877
|
}
|
|
707
878
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
@@ -709,8 +880,8 @@ class AbilityRule {
|
|
|
709
880
|
}
|
|
710
881
|
return false;
|
|
711
882
|
},
|
|
712
|
-
[AbilityCondition.not_in
|
|
713
|
-
if (
|
|
883
|
+
[toLiteral(AbilityCondition.not_in)]: (a, b) => {
|
|
884
|
+
if (AbilityRule.isPrimitive(a) && Array.isArray(b)) {
|
|
714
885
|
return !b.includes(a);
|
|
715
886
|
}
|
|
716
887
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
@@ -718,55 +889,55 @@ class AbilityRule {
|
|
|
718
889
|
}
|
|
719
890
|
return false;
|
|
720
891
|
},
|
|
721
|
-
[AbilityCondition.greater_than
|
|
722
|
-
return
|
|
892
|
+
[toLiteral(AbilityCondition.greater_than)]: (a, b) => {
|
|
893
|
+
return AbilityRule.isNumber(a) && AbilityRule.isNumber(b) ? a > b : false;
|
|
723
894
|
},
|
|
724
|
-
[AbilityCondition.less_than
|
|
725
|
-
return
|
|
895
|
+
[toLiteral(AbilityCondition.less_than)]: (a, b) => {
|
|
896
|
+
return AbilityRule.isNumber(a) && AbilityRule.isNumber(b) ? a < b : false;
|
|
726
897
|
},
|
|
727
|
-
[AbilityCondition.greater_or_equal
|
|
728
|
-
return
|
|
898
|
+
[toLiteral(AbilityCondition.greater_or_equal)]: (a, b) => {
|
|
899
|
+
return AbilityRule.isNumber(a) && AbilityRule.isNumber(b) ? a >= b : false;
|
|
729
900
|
},
|
|
730
|
-
[AbilityCondition.less_or_equal
|
|
731
|
-
return
|
|
901
|
+
[toLiteral(AbilityCondition.less_or_equal)]: (a, b) => {
|
|
902
|
+
return AbilityRule.isNumber(a) && AbilityRule.isNumber(b) ? a <= b : false;
|
|
732
903
|
},
|
|
733
|
-
[AbilityCondition.length_greater_than
|
|
734
|
-
const alen =
|
|
904
|
+
[toLiteral(AbilityCondition.length_greater_than)]: (a, b) => {
|
|
905
|
+
const alen = AbilityRule.valueLen(a);
|
|
735
906
|
if (alen === null) {
|
|
736
907
|
return false;
|
|
737
908
|
}
|
|
738
|
-
if (
|
|
909
|
+
if (AbilityRule.isNumber(b)) {
|
|
739
910
|
return alen > b;
|
|
740
911
|
}
|
|
741
|
-
const bLen =
|
|
912
|
+
const bLen = AbilityRule.valueLen(b);
|
|
742
913
|
if (bLen !== null) {
|
|
743
914
|
return alen > bLen;
|
|
744
915
|
}
|
|
745
916
|
return false;
|
|
746
917
|
},
|
|
747
|
-
[AbilityCondition.length_less_than
|
|
748
|
-
const alen =
|
|
918
|
+
[toLiteral(AbilityCondition.length_less_than)]: (a, b) => {
|
|
919
|
+
const alen = AbilityRule.valueLen(a);
|
|
749
920
|
if (alen === null) {
|
|
750
921
|
return false;
|
|
751
922
|
}
|
|
752
|
-
if (
|
|
923
|
+
if (AbilityRule.isNumber(b)) {
|
|
753
924
|
return alen < b;
|
|
754
925
|
}
|
|
755
|
-
const bLen =
|
|
926
|
+
const bLen = AbilityRule.valueLen(b);
|
|
756
927
|
if (bLen !== null) {
|
|
757
928
|
return alen < bLen;
|
|
758
929
|
}
|
|
759
930
|
return false;
|
|
760
931
|
},
|
|
761
|
-
[AbilityCondition.length_equals
|
|
762
|
-
const alen =
|
|
932
|
+
[toLiteral(AbilityCondition.length_equals)]: (a, b) => {
|
|
933
|
+
const alen = AbilityRule.valueLen(a);
|
|
763
934
|
if (alen === null) {
|
|
764
935
|
return false;
|
|
765
936
|
}
|
|
766
|
-
if (
|
|
937
|
+
if (AbilityRule.isNumber(b)) {
|
|
767
938
|
return alen === b;
|
|
768
939
|
}
|
|
769
|
-
const bLen =
|
|
940
|
+
const bLen = AbilityRule.valueLen(b);
|
|
770
941
|
if (bLen !== null) {
|
|
771
942
|
return alen === bLen;
|
|
772
943
|
}
|
|
@@ -779,8 +950,12 @@ class AbilityRule {
|
|
|
779
950
|
* @param environment
|
|
780
951
|
*/
|
|
781
952
|
check(resource, environment) {
|
|
953
|
+
if (this.disabled) {
|
|
954
|
+
this.state = AbilityMatch.disabled;
|
|
955
|
+
return this.state;
|
|
956
|
+
}
|
|
782
957
|
const [subjectValue, resourceValue] = this.extractValues(resource, environment);
|
|
783
|
-
const handler =
|
|
958
|
+
const handler = AbilityRule.operatorHandlers[toLiteral(this.condition)];
|
|
784
959
|
const result = handler(subjectValue, resourceValue);
|
|
785
960
|
this.state = result ? AbilityMatch.match : AbilityMatch.mismatch;
|
|
786
961
|
return this.state;
|
|
@@ -827,41 +1002,62 @@ class AbilityRule {
|
|
|
827
1002
|
}
|
|
828
1003
|
return [subjectValue, resourceValue];
|
|
829
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
|
+
}
|
|
830
1028
|
/**
|
|
831
1029
|
* Get the value of the object by dot notation
|
|
832
1030
|
* @param resource - The object to get the value from
|
|
833
1031
|
* @param desc - The dot notation string
|
|
834
1032
|
*/
|
|
835
1033
|
getDotNotationValue(resource, desc) {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
}
|
|
848
|
-
else {
|
|
849
|
-
resource = undefined;
|
|
850
|
-
}
|
|
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];
|
|
851
1045
|
}
|
|
852
1046
|
else {
|
|
853
|
-
|
|
1047
|
+
const arr = current[seg.prop];
|
|
1048
|
+
current = Array.isArray(arr) ? arr[seg.index] : undefined;
|
|
854
1049
|
}
|
|
855
1050
|
}
|
|
856
|
-
return
|
|
1051
|
+
return current;
|
|
857
1052
|
}
|
|
858
1053
|
toString() {
|
|
859
|
-
return `AbilityRule: ${this.name} condition: ${this.condition
|
|
1054
|
+
return `AbilityRule: ${this.name} condition: ${toLiteral(this.condition)} subject: "${this.subject?.toString()}" resource: "${this.resource?.toString()}"`;
|
|
860
1055
|
}
|
|
861
1056
|
copyWith(props) {
|
|
862
1057
|
return new AbilityRule({
|
|
863
1058
|
id: props.id ?? this.id,
|
|
864
1059
|
name: props.name ?? this.name,
|
|
1060
|
+
description: props.description ?? this.description,
|
|
865
1061
|
subject: props.subject ?? this.subject,
|
|
866
1062
|
resource: props.resource ?? this.resource,
|
|
867
1063
|
condition: props.condition ?? this.condition,
|
|
@@ -963,15 +1159,22 @@ class AbilityRuleSet {
|
|
|
963
1159
|
* Group name
|
|
964
1160
|
*/
|
|
965
1161
|
name;
|
|
1162
|
+
description;
|
|
966
1163
|
/**
|
|
967
1164
|
* Group ID
|
|
968
1165
|
*/
|
|
969
1166
|
id;
|
|
1167
|
+
isExcept = false;
|
|
1168
|
+
disabled;
|
|
970
1169
|
constructor(params) {
|
|
971
|
-
const { name, id, compareMethod } = params;
|
|
972
|
-
this.name = name ||
|
|
1170
|
+
const { name, id, compareMethod, isExcept, disabled, description } = params;
|
|
1171
|
+
this.name = name || `ruleset:${compareMethod}`;
|
|
973
1172
|
this.id = id || this.name;
|
|
1173
|
+
this.description = description;
|
|
974
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;
|
|
975
1178
|
}
|
|
976
1179
|
addRule(rule) {
|
|
977
1180
|
this.rules.push(rule);
|
|
@@ -983,40 +1186,48 @@ class AbilityRuleSet {
|
|
|
983
1186
|
}
|
|
984
1187
|
check(resources, environment) {
|
|
985
1188
|
this.state = AbilityMatch.mismatch;
|
|
1189
|
+
if (this.disabled) {
|
|
1190
|
+
this.state = AbilityMatch.disabled;
|
|
1191
|
+
return this.state;
|
|
1192
|
+
}
|
|
986
1193
|
if (!this.rules.length) {
|
|
987
1194
|
return this.state;
|
|
988
1195
|
}
|
|
989
1196
|
const ruleCheckStates = [];
|
|
990
1197
|
for (const rule of this.rules) {
|
|
1198
|
+
if (rule.disabled) {
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
991
1201
|
const state = rule.check(resources, environment);
|
|
992
1202
|
ruleCheckStates.push(state);
|
|
993
|
-
if (AbilityCompare.and
|
|
1203
|
+
if (AbilityCompare.and === this.compareMethod && AbilityMatch.mismatch === state) {
|
|
994
1204
|
return this.state; // mismatch
|
|
995
1205
|
}
|
|
996
|
-
if (AbilityCompare.or
|
|
1206
|
+
if (AbilityCompare.or === this.compareMethod && AbilityMatch.match === state) {
|
|
997
1207
|
this.state = AbilityMatch.match;
|
|
998
1208
|
return this.state;
|
|
999
1209
|
}
|
|
1000
1210
|
}
|
|
1001
|
-
if (AbilityCompare.and
|
|
1002
|
-
if (ruleCheckStates.every(s => AbilityMatch.match
|
|
1211
|
+
if (AbilityCompare.and === this.compareMethod) {
|
|
1212
|
+
if (ruleCheckStates.every(s => AbilityMatch.match === s)) {
|
|
1003
1213
|
this.state = AbilityMatch.match;
|
|
1004
1214
|
}
|
|
1005
1215
|
}
|
|
1006
|
-
if (AbilityCompare.or
|
|
1007
|
-
if (ruleCheckStates.some(s => AbilityMatch.match
|
|
1216
|
+
if (AbilityCompare.or === this.compareMethod) {
|
|
1217
|
+
if (ruleCheckStates.some(s => AbilityMatch.match === s)) {
|
|
1008
1218
|
this.state = AbilityMatch.match;
|
|
1009
1219
|
}
|
|
1010
1220
|
}
|
|
1011
1221
|
return this.state;
|
|
1012
1222
|
}
|
|
1013
1223
|
toString() {
|
|
1014
|
-
return `AbilityRuleSet: ${this.name} compareMethod: ${this.compareMethod
|
|
1224
|
+
return `AbilityRuleSet: ${this.name} compareMethod: ${this.compareMethod}, rules: ${this.rules.map(rule => rule.toString()).join('\n')}`;
|
|
1015
1225
|
}
|
|
1016
1226
|
copyWith(props) {
|
|
1017
1227
|
const next = new AbilityRuleSet({
|
|
1018
1228
|
id: props.id ?? this.id,
|
|
1019
1229
|
name: props.name ?? this.name,
|
|
1230
|
+
description: props.description ?? this.description,
|
|
1020
1231
|
compareMethod: props.compareMethod ?? this.compareMethod,
|
|
1021
1232
|
});
|
|
1022
1233
|
const nextRules = props.rules ?? this.rules;
|
|
@@ -1047,37 +1258,42 @@ class AbilityJSONParser {
|
|
|
1047
1258
|
return configs.map(config => AbilityJSONParser.parsePolicy(config));
|
|
1048
1259
|
}
|
|
1049
1260
|
static parsePolicy(config) {
|
|
1050
|
-
const { id, name, ruleSet, compareMethod, permission, effect } = config;
|
|
1261
|
+
const { id, name, ruleSet, compareMethod, permission, effect, priority, disabled, tags } = config;
|
|
1051
1262
|
// Create the empty policy
|
|
1052
1263
|
const policy = new AbilityPolicy({
|
|
1053
1264
|
name,
|
|
1054
1265
|
id,
|
|
1055
1266
|
permission: permission,
|
|
1056
|
-
|
|
1267
|
+
priority: priority,
|
|
1268
|
+
effect: effect,
|
|
1269
|
+
disabled,
|
|
1270
|
+
tags,
|
|
1057
1271
|
});
|
|
1058
|
-
policy.compareMethod =
|
|
1272
|
+
policy.compareMethod = compareMethod;
|
|
1059
1273
|
ruleSet.forEach(ruleSetConfig => {
|
|
1060
1274
|
policy.addRuleSet(AbilityJSONParser.parseRuleSet(ruleSetConfig));
|
|
1061
1275
|
});
|
|
1062
1276
|
return policy;
|
|
1063
1277
|
}
|
|
1064
1278
|
static parseRule(config) {
|
|
1065
|
-
const { id, name, subject, resource, condition } = config;
|
|
1279
|
+
const { id, name, subject, resource, condition, disabled } = config;
|
|
1066
1280
|
return new AbilityRule({
|
|
1067
1281
|
id,
|
|
1068
1282
|
name,
|
|
1069
1283
|
subject,
|
|
1070
1284
|
resource,
|
|
1071
|
-
|
|
1285
|
+
disabled,
|
|
1286
|
+
condition,
|
|
1072
1287
|
});
|
|
1073
1288
|
}
|
|
1074
1289
|
/**
|
|
1075
1290
|
* Parse the config JSON format to Group class instance
|
|
1076
1291
|
*/
|
|
1077
1292
|
static parseRuleSet(config) {
|
|
1078
|
-
const { id, name, rules, compareMethod } = config;
|
|
1293
|
+
const { id, name, rules, compareMethod, disabled } = config;
|
|
1079
1294
|
const ruleSet = new AbilityRuleSet({
|
|
1080
|
-
|
|
1295
|
+
disabled,
|
|
1296
|
+
compareMethod: compareMethod,
|
|
1081
1297
|
name,
|
|
1082
1298
|
id,
|
|
1083
1299
|
});
|
|
@@ -1094,25 +1310,30 @@ class AbilityJSONParser {
|
|
|
1094
1310
|
name: rule.name,
|
|
1095
1311
|
subject: rule.subject,
|
|
1096
1312
|
resource: rule.resource,
|
|
1097
|
-
condition: rule.condition
|
|
1313
|
+
condition: rule.condition,
|
|
1314
|
+
disabled: rule.disabled,
|
|
1098
1315
|
};
|
|
1099
1316
|
}
|
|
1100
1317
|
static ruleSetToJSON(ruleSet) {
|
|
1101
1318
|
return {
|
|
1102
1319
|
id: ruleSet.id.toString(),
|
|
1103
1320
|
name: ruleSet.name.toString(),
|
|
1104
|
-
compareMethod: ruleSet.compareMethod
|
|
1321
|
+
compareMethod: ruleSet.compareMethod,
|
|
1105
1322
|
rules: ruleSet.rules.map(rule => AbilityJSONParser.ruleToJSON(rule)),
|
|
1323
|
+
disabled: ruleSet.disabled,
|
|
1106
1324
|
};
|
|
1107
1325
|
}
|
|
1108
1326
|
static policyToJSON(policy) {
|
|
1109
1327
|
return {
|
|
1110
1328
|
id: policy.id.toString(),
|
|
1111
1329
|
name: policy.name.toString(),
|
|
1112
|
-
compareMethod: policy.compareMethod
|
|
1330
|
+
compareMethod: policy.compareMethod,
|
|
1113
1331
|
ruleSet: policy.ruleSet.map(ruleSet => AbilityJSONParser.ruleSetToJSON(ruleSet)),
|
|
1114
1332
|
permission: policy.permission,
|
|
1115
|
-
effect: policy.effect
|
|
1333
|
+
effect: policy.effect,
|
|
1334
|
+
priority: policy.priority,
|
|
1335
|
+
disabled: policy.disabled,
|
|
1336
|
+
tags: policy.tags,
|
|
1116
1337
|
};
|
|
1117
1338
|
}
|
|
1118
1339
|
static toJSON(policies) {
|
|
@@ -1120,67 +1341,66 @@ class AbilityJSONParser {
|
|
|
1120
1341
|
}
|
|
1121
1342
|
}
|
|
1122
1343
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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;
|
|
1134
1395
|
constructor(type, value, line, column) {
|
|
1135
|
-
|
|
1396
|
+
this.type = type;
|
|
1136
1397
|
this.value = value;
|
|
1137
1398
|
this.line = line;
|
|
1138
1399
|
this.column = column;
|
|
1139
1400
|
}
|
|
1140
|
-
/**
|
|
1141
|
-
* Returns a human-readable representation of the token, useful for debugging.
|
|
1142
|
-
* Example output: "AbilityDSLToken([EFFECT] permit"
|
|
1143
|
-
*/
|
|
1144
1401
|
toString() {
|
|
1145
|
-
return `AbilityDSLToken([${this.
|
|
1146
|
-
}
|
|
1147
|
-
static EFFECT = 'EFFECT';
|
|
1148
|
-
static IF = 'IF';
|
|
1149
|
-
static PERMISSION = 'PERMISSION';
|
|
1150
|
-
static IDENTIFIER = 'IDENTIFIER';
|
|
1151
|
-
static COLON = 'COLON';
|
|
1152
|
-
static COMMA = 'COMMA';
|
|
1153
|
-
static DOT = 'DOT';
|
|
1154
|
-
static LBRACKET = 'LBRACKET';
|
|
1155
|
-
static RBRACKET = 'RBRACKET';
|
|
1156
|
-
static ALL = 'ALL';
|
|
1157
|
-
static ANY = 'ANY';
|
|
1158
|
-
static OF = 'OF';
|
|
1159
|
-
static EOF = 'EOF';
|
|
1160
|
-
static COMMENT = 'COMMENT';
|
|
1161
|
-
static EQ = 'EQ';
|
|
1162
|
-
static CONTAINS = 'CONTAINS';
|
|
1163
|
-
static IN = 'IN';
|
|
1164
|
-
static NOT_IN = 'NOT_IN';
|
|
1165
|
-
static NOT_CONTAINS = 'NOT_CONTAINS';
|
|
1166
|
-
static GT = 'GT';
|
|
1167
|
-
static GTE = 'GTE';
|
|
1168
|
-
static LT = 'LT';
|
|
1169
|
-
static LTE = 'LTE';
|
|
1170
|
-
static NULL = 'NULL';
|
|
1171
|
-
static EQ_NULL = 'EQ_NULL';
|
|
1172
|
-
static NOT_EQ_NULL = 'NOT_EQ_NULL';
|
|
1173
|
-
static LEN_GT = 'LEN_GT';
|
|
1174
|
-
static LEN_LT = 'LEN_LT';
|
|
1175
|
-
static LEN_EQ = 'LEN_EQ';
|
|
1176
|
-
static NOT_EQ = 'NOT_EQ';
|
|
1177
|
-
static ALWAYS = 'ALWAYS';
|
|
1178
|
-
static NEVER = 'NEVER';
|
|
1179
|
-
static STRING = 'STRING';
|
|
1180
|
-
static NUMBER = 'NUMBER';
|
|
1181
|
-
static BOOLEAN = 'BOOLEAN';
|
|
1182
|
-
static SYMBOL = 'SYMBOL';
|
|
1183
|
-
static KEYWORD = 'KEYWORD';
|
|
1402
|
+
return `AbilityDSLToken([${this.type}] "${this.value}" at ${this.line}:${this.column})`;
|
|
1403
|
+
}
|
|
1184
1404
|
}
|
|
1185
1405
|
|
|
1186
1406
|
class AbilityDSLLexer {
|
|
@@ -1189,7 +1409,6 @@ class AbilityDSLLexer {
|
|
|
1189
1409
|
tokens = [];
|
|
1190
1410
|
line = 1;
|
|
1191
1411
|
column = 1;
|
|
1192
|
-
// Список ключевых слов
|
|
1193
1412
|
keywords = new Set([
|
|
1194
1413
|
'if',
|
|
1195
1414
|
'all',
|
|
@@ -1220,6 +1439,8 @@ class AbilityDSLLexer {
|
|
|
1220
1439
|
'than',
|
|
1221
1440
|
'always',
|
|
1222
1441
|
'never',
|
|
1442
|
+
'except',
|
|
1443
|
+
'alias',
|
|
1223
1444
|
]);
|
|
1224
1445
|
constructor(input) {
|
|
1225
1446
|
this.input = input;
|
|
@@ -1230,6 +1451,10 @@ class AbilityDSLLexer {
|
|
|
1230
1451
|
if (this.isAtEnd())
|
|
1231
1452
|
break;
|
|
1232
1453
|
const char = this.peek();
|
|
1454
|
+
if (char === '@') {
|
|
1455
|
+
this.tokens.push(this.readAnnotation());
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1233
1458
|
if (char === '#') {
|
|
1234
1459
|
this.tokens.push(this.readComment());
|
|
1235
1460
|
continue;
|
|
@@ -1252,7 +1477,7 @@ class AbilityDSLLexer {
|
|
|
1252
1477
|
}
|
|
1253
1478
|
throw new Error(`Unexpected character '${char}' at ${this.line}:${this.column}`);
|
|
1254
1479
|
}
|
|
1255
|
-
this.tokens.push(new AbilityDSLToken(
|
|
1480
|
+
this.tokens.push(new AbilityDSLToken(TokenTypes.EOF, '', this.line, this.column));
|
|
1256
1481
|
return this.tokens;
|
|
1257
1482
|
}
|
|
1258
1483
|
readComment() {
|
|
@@ -1263,7 +1488,31 @@ class AbilityDSLLexer {
|
|
|
1263
1488
|
while (!this.isAtEnd() && !this.isNewline()) {
|
|
1264
1489
|
value += this.advance();
|
|
1265
1490
|
}
|
|
1266
|
-
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);
|
|
1267
1516
|
}
|
|
1268
1517
|
readString() {
|
|
1269
1518
|
const startLine = this.line;
|
|
@@ -1283,7 +1532,7 @@ class AbilityDSLLexer {
|
|
|
1283
1532
|
continue;
|
|
1284
1533
|
}
|
|
1285
1534
|
if (char === quote) {
|
|
1286
|
-
return new AbilityDSLToken(
|
|
1535
|
+
return new AbilityDSLToken(TokenTypes.STRING, value, startLine, startColumn);
|
|
1287
1536
|
}
|
|
1288
1537
|
value += char;
|
|
1289
1538
|
}
|
|
@@ -1297,7 +1546,7 @@ class AbilityDSLLexer {
|
|
|
1297
1546
|
this.advance();
|
|
1298
1547
|
}
|
|
1299
1548
|
const value = this.input.slice(start, this.pos);
|
|
1300
|
-
return new AbilityDSLToken(
|
|
1549
|
+
return new AbilityDSLToken(TokenTypes.NUMBER, value, startLine, startColumn);
|
|
1301
1550
|
}
|
|
1302
1551
|
readSymbol() {
|
|
1303
1552
|
const startLine = this.line;
|
|
@@ -1305,41 +1554,41 @@ class AbilityDSLLexer {
|
|
|
1305
1554
|
const char = this.advance();
|
|
1306
1555
|
switch (char) {
|
|
1307
1556
|
case '.':
|
|
1308
|
-
return new AbilityDSLToken(
|
|
1557
|
+
return new AbilityDSLToken(TokenTypes.DOT, char, startLine, startColumn);
|
|
1309
1558
|
case ':':
|
|
1310
|
-
return new AbilityDSLToken(
|
|
1559
|
+
return new AbilityDSLToken(TokenTypes.COLON, char, startLine, startColumn);
|
|
1311
1560
|
case ',':
|
|
1312
|
-
return new AbilityDSLToken(
|
|
1561
|
+
return new AbilityDSLToken(TokenTypes.COMMA, char, startLine, startColumn);
|
|
1313
1562
|
case '[':
|
|
1314
|
-
return new AbilityDSLToken(
|
|
1563
|
+
return new AbilityDSLToken(TokenTypes.LBRACKET, char, startLine, startColumn);
|
|
1315
1564
|
case ']':
|
|
1316
|
-
return new AbilityDSLToken(
|
|
1565
|
+
return new AbilityDSLToken(TokenTypes.RBRACKET, char, startLine, startColumn);
|
|
1317
1566
|
case '>':
|
|
1318
1567
|
if (this.peek() === '=') {
|
|
1319
1568
|
this.advance();
|
|
1320
|
-
return new AbilityDSLToken(
|
|
1569
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '>=', startLine, startColumn);
|
|
1321
1570
|
}
|
|
1322
|
-
return new AbilityDSLToken(
|
|
1571
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '>', startLine, startColumn);
|
|
1323
1572
|
case '<':
|
|
1324
1573
|
if (this.peek() === '=') {
|
|
1325
1574
|
this.advance();
|
|
1326
|
-
return new AbilityDSLToken(
|
|
1575
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '<=', startLine, startColumn);
|
|
1327
1576
|
}
|
|
1328
1577
|
if (this.peek() === '>') {
|
|
1329
1578
|
this.advance();
|
|
1330
|
-
return new AbilityDSLToken(
|
|
1579
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '<>', startLine, startColumn);
|
|
1331
1580
|
}
|
|
1332
|
-
return new AbilityDSLToken(
|
|
1581
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '<', startLine, startColumn);
|
|
1333
1582
|
case '=':
|
|
1334
1583
|
if (this.peek() === '=') {
|
|
1335
1584
|
this.advance();
|
|
1336
|
-
return new AbilityDSLToken(
|
|
1585
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '==', startLine, startColumn);
|
|
1337
1586
|
}
|
|
1338
|
-
return new AbilityDSLToken(
|
|
1587
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '=', startLine, startColumn);
|
|
1339
1588
|
case '!':
|
|
1340
1589
|
if (this.peek() === '=') {
|
|
1341
1590
|
this.advance();
|
|
1342
|
-
return new AbilityDSLToken(
|
|
1591
|
+
return new AbilityDSLToken(TokenTypes.SYMBOL, '!=', startLine, startColumn);
|
|
1343
1592
|
}
|
|
1344
1593
|
throw new Error(`Unexpected symbol '!' at ${this.line}:${this.column}`);
|
|
1345
1594
|
default:
|
|
@@ -1366,60 +1615,66 @@ class AbilityDSLLexer {
|
|
|
1366
1615
|
}
|
|
1367
1616
|
const word = this.input.slice(start, this.pos);
|
|
1368
1617
|
if (word === 'always') {
|
|
1369
|
-
return new AbilityDSLToken(
|
|
1618
|
+
return new AbilityDSLToken(TokenTypes.ALWAYS, word, startLine, startColumn);
|
|
1370
1619
|
}
|
|
1371
1620
|
if (word === 'never') {
|
|
1372
|
-
return new AbilityDSLToken(
|
|
1621
|
+
return new AbilityDSLToken(TokenTypes.NEVER, word, startLine, startColumn);
|
|
1373
1622
|
}
|
|
1374
1623
|
// Если есть точка — это путь (identifier или permission)
|
|
1375
1624
|
if (word.includes('.')) {
|
|
1376
1625
|
const last = this.tokens[this.tokens.length - 1];
|
|
1377
|
-
if (last?.
|
|
1626
|
+
if (last?.type === TokenTypes.EFFECT) {
|
|
1378
1627
|
if (word.startsWith('permission.')) {
|
|
1379
|
-
return new AbilityDSLToken(
|
|
1628
|
+
return new AbilityDSLToken(TokenTypes.PERMISSION, word, startLine, startColumn);
|
|
1380
1629
|
}
|
|
1381
1630
|
}
|
|
1382
|
-
return new AbilityDSLToken(
|
|
1631
|
+
return new AbilityDSLToken(TokenTypes.IDENTIFIER, word, startLine, startColumn);
|
|
1383
1632
|
}
|
|
1384
1633
|
// Ключевые слова
|
|
1385
1634
|
if (this.keywords.has(word)) {
|
|
1386
1635
|
// Эффекты
|
|
1387
1636
|
if (word === 'permit' || word === 'allow') {
|
|
1388
|
-
return new AbilityDSLToken(
|
|
1637
|
+
return new AbilityDSLToken(TokenTypes.EFFECT, 'permit', startLine, startColumn);
|
|
1389
1638
|
}
|
|
1390
1639
|
if (word === 'deny' || word === 'forbidden') {
|
|
1391
|
-
return new AbilityDSLToken(
|
|
1640
|
+
return new AbilityDSLToken(TokenTypes.EFFECT, 'deny', startLine, startColumn);
|
|
1392
1641
|
}
|
|
1393
1642
|
// Групповые ключевые слова
|
|
1394
1643
|
if (word === 'all') {
|
|
1395
|
-
return new AbilityDSLToken(
|
|
1644
|
+
return new AbilityDSLToken(TokenTypes.ALL, word, startLine, startColumn);
|
|
1396
1645
|
}
|
|
1397
1646
|
if (word === 'any') {
|
|
1398
|
-
return new AbilityDSLToken(
|
|
1647
|
+
return new AbilityDSLToken(TokenTypes.ANY, word, startLine, startColumn);
|
|
1399
1648
|
}
|
|
1400
1649
|
if (word === 'of') {
|
|
1401
|
-
return new AbilityDSLToken(
|
|
1650
|
+
return new AbilityDSLToken(TokenTypes.OF, word, startLine, startColumn);
|
|
1402
1651
|
}
|
|
1403
1652
|
if (word === 'if') {
|
|
1404
|
-
return new AbilityDSLToken(
|
|
1653
|
+
return new AbilityDSLToken(TokenTypes.IF, word, startLine, startColumn);
|
|
1405
1654
|
}
|
|
1406
1655
|
// Булевы и null
|
|
1407
1656
|
if (word === 'true' || word === 'false') {
|
|
1408
|
-
return new AbilityDSLToken(
|
|
1657
|
+
return new AbilityDSLToken(TokenTypes.BOOLEAN, word, startLine, startColumn);
|
|
1409
1658
|
}
|
|
1410
1659
|
if (word === 'null') {
|
|
1411
|
-
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);
|
|
1412
1667
|
}
|
|
1413
1668
|
// Остальные ключевые слова (contains, in, equals, greater, less, not, is, or, than, equal)
|
|
1414
|
-
return new AbilityDSLToken(
|
|
1669
|
+
return new AbilityDSLToken(TokenTypes.KEYWORD, word, startLine, startColumn);
|
|
1415
1670
|
}
|
|
1416
1671
|
// Если после EFFECT и нет точки — действие (например, "create")
|
|
1417
1672
|
const lastToken = this.tokens[this.tokens.length - 1];
|
|
1418
|
-
if (lastToken?.
|
|
1419
|
-
return new AbilityDSLToken(
|
|
1673
|
+
if (lastToken?.type === TokenTypes.EFFECT) {
|
|
1674
|
+
return new AbilityDSLToken(TokenTypes.PERMISSION, word, startLine, startColumn);
|
|
1420
1675
|
}
|
|
1421
1676
|
// Обычный идентификатор
|
|
1422
|
-
return new AbilityDSLToken(
|
|
1677
|
+
return new AbilityDSLToken(TokenTypes.IDENTIFIER, word, startLine, startColumn);
|
|
1423
1678
|
}
|
|
1424
1679
|
skipWhitespace() {
|
|
1425
1680
|
while (!this.isAtEnd() && /\s/.test(this.peek())) {
|
|
@@ -1519,6 +1774,215 @@ class AbilityDSLSyntaxError extends Error {
|
|
|
1519
1774
|
}
|
|
1520
1775
|
}
|
|
1521
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
|
+
|
|
1522
1986
|
/**
|
|
1523
1987
|
* Parser for the Ability DSL.
|
|
1524
1988
|
*
|
|
@@ -1537,12 +2001,9 @@ class AbilityDSLSyntaxError extends Error {
|
|
|
1537
2001
|
*/
|
|
1538
2002
|
class AbilityDSLParser {
|
|
1539
2003
|
dsl;
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
name: null,
|
|
1544
|
-
description: null,
|
|
1545
|
-
};
|
|
2004
|
+
stream;
|
|
2005
|
+
annBuffer = new AbilityDSLAnnotations();
|
|
2006
|
+
aliasBuffer = new AbilityDSLAliases();
|
|
1546
2007
|
constructor(dsl) {
|
|
1547
2008
|
this.dsl = dsl;
|
|
1548
2009
|
}
|
|
@@ -1551,17 +2012,19 @@ class AbilityDSLParser {
|
|
|
1551
2012
|
* @returns Array of AbilityPolicy instances.
|
|
1552
2013
|
*/
|
|
1553
2014
|
parse() {
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
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);
|
|
1557
2020
|
const policies = [];
|
|
1558
|
-
|
|
1559
|
-
while (!this.isAtEnd()) {
|
|
2021
|
+
while (!this.stream.eof()) {
|
|
1560
2022
|
this.consumeLeadingComments();
|
|
1561
|
-
|
|
2023
|
+
this.consumeLeadingAnnotations();
|
|
2024
|
+
this.consumeLeadingAliases();
|
|
1562
2025
|
if (!this.isStartOfPolicy()) {
|
|
1563
|
-
const token = this.peek();
|
|
1564
|
-
this.syntaxError(`Expected policy, got ${token.
|
|
2026
|
+
const token = this.stream.peek();
|
|
2027
|
+
this.stream.syntaxError(`Expected policy, got ${token.type}.`, token, [TokenTypes.EFFECT]);
|
|
1565
2028
|
}
|
|
1566
2029
|
policies.push(this.parsePolicy());
|
|
1567
2030
|
}
|
|
@@ -1578,31 +2041,37 @@ class AbilityDSLParser {
|
|
|
1578
2041
|
*/
|
|
1579
2042
|
parsePolicy() {
|
|
1580
2043
|
this.consumeLeadingComments();
|
|
1581
|
-
|
|
2044
|
+
this.consumeLeadingAnnotations();
|
|
2045
|
+
this.consumeLeadingAliases();
|
|
2046
|
+
const annotations = this.takeAnnotations('policy');
|
|
1582
2047
|
// Effect: "permit" or "deny"
|
|
1583
|
-
const effectToken = this.
|
|
2048
|
+
const effectToken = this.stream.expect(TokenTypes.EFFECT, 'Expected effect');
|
|
1584
2049
|
const effect = effectToken.value;
|
|
1585
2050
|
// Permission: e.g. "order.update"
|
|
1586
|
-
const permissionToken = this.
|
|
2051
|
+
const permissionToken = this.stream.expect(TokenTypes.PERMISSION, 'Expected permission');
|
|
1587
2052
|
const permission = permissionToken.value;
|
|
1588
2053
|
if (!permission.startsWith('permission.')) {
|
|
1589
|
-
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);
|
|
1590
2055
|
}
|
|
1591
2056
|
// "if" keyword
|
|
1592
|
-
this.
|
|
2057
|
+
this.stream.expect(TokenTypes.IF, 'Expected "if"');
|
|
1593
2058
|
// Group selector: "all" or "any" – determines how the top‑level rule sets are combined.
|
|
1594
|
-
const compareToken = this.
|
|
1595
|
-
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;
|
|
1596
2061
|
// Colon after the group keyword
|
|
1597
|
-
this.
|
|
2062
|
+
this.stream.expect(TokenTypes.COLON, 'Expected ":"');
|
|
1598
2063
|
// Parse the list of rule sets (each "all of:" or "any of:" block)
|
|
1599
2064
|
const ruleSets = this.parseRuleSets(compareMethod);
|
|
1600
2065
|
// Construct the policy instance.
|
|
1601
2066
|
return new AbilityPolicy({
|
|
1602
|
-
id:
|
|
1603
|
-
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,
|
|
1604
2071
|
permission: permission.replace(/^permission\./, ''),
|
|
1605
2072
|
effect: effect === 'permit' ? AbilityPolicyEffect.permit : AbilityPolicyEffect.deny,
|
|
2073
|
+
disabled: annotations.disabled?.value ?? undefined,
|
|
2074
|
+
tags: annotations.tags?.value ?? undefined,
|
|
1606
2075
|
compareMethod,
|
|
1607
2076
|
}).addRuleSets(ruleSets);
|
|
1608
2077
|
}
|
|
@@ -1614,32 +2083,41 @@ class AbilityDSLParser {
|
|
|
1614
2083
|
*/
|
|
1615
2084
|
parseRuleSets(policyCompareMethod) {
|
|
1616
2085
|
const sets = [];
|
|
1617
|
-
while (!this.
|
|
2086
|
+
while (!this.stream.eof() && !this.isStartOfPolicy()) {
|
|
1618
2087
|
this.consumeLeadingComments();
|
|
2088
|
+
this.consumeLeadingAnnotations();
|
|
2089
|
+
// Если начинается новая except группа — парсим её
|
|
2090
|
+
if (this.isStartOfExcept()) {
|
|
2091
|
+
sets.push(this.parseExceptGroup(policyCompareMethod));
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
1619
2094
|
// Если начинается новая группа — парсим её
|
|
1620
2095
|
if (this.isStartOfGroup()) {
|
|
1621
2096
|
sets.push(this.parseGroup());
|
|
1622
2097
|
continue;
|
|
1623
2098
|
}
|
|
1624
|
-
|
|
1625
|
-
const meta = this.takeAnnotations();
|
|
2099
|
+
const annotation = this.takeAnnotations('ruleSet');
|
|
1626
2100
|
const group = new AbilityRuleSet({
|
|
2101
|
+
id: annotation.id?.value || null,
|
|
1627
2102
|
compareMethod: policyCompareMethod,
|
|
1628
|
-
name:
|
|
2103
|
+
name: annotation.name?.value ?? null,
|
|
2104
|
+
description: annotation.description?.value || null,
|
|
2105
|
+
disabled: annotation.disabled?.value ?? undefined,
|
|
1629
2106
|
});
|
|
1630
2107
|
// Читаем правила implicit-группы
|
|
1631
|
-
while (!this.
|
|
2108
|
+
while (!this.stream.eof()) {
|
|
1632
2109
|
this.consumeLeadingComments();
|
|
1633
|
-
|
|
2110
|
+
this.consumeLeadingAnnotations();
|
|
2111
|
+
if (this.isStartOfGroup() || this.isStartOfPolicy() || this.isStartOfExcept()) {
|
|
1634
2112
|
break;
|
|
1635
2113
|
}
|
|
1636
|
-
if (this.check(
|
|
1637
|
-
this.check(
|
|
1638
|
-
this.check(
|
|
2114
|
+
if (this.stream.check(TokenTypes.IDENTIFIER) ||
|
|
2115
|
+
this.stream.check(TokenTypes.ALWAYS) ||
|
|
2116
|
+
this.stream.check(TokenTypes.NEVER)) {
|
|
1639
2117
|
group.addRule(this.parseRule());
|
|
1640
2118
|
}
|
|
1641
2119
|
else {
|
|
1642
|
-
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());
|
|
1643
2121
|
}
|
|
1644
2122
|
}
|
|
1645
2123
|
sets.push(group);
|
|
@@ -1651,24 +2129,83 @@ class AbilityDSLParser {
|
|
|
1651
2129
|
*/
|
|
1652
2130
|
parseGroup() {
|
|
1653
2131
|
this.consumeLeadingComments();
|
|
1654
|
-
|
|
1655
|
-
const
|
|
1656
|
-
const
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
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()) {
|
|
1663
2148
|
this.consumeLeadingComments();
|
|
2149
|
+
this.consumeLeadingAnnotations();
|
|
2150
|
+
if (this.isStartOfExcept()) {
|
|
2151
|
+
break;
|
|
2152
|
+
}
|
|
1664
2153
|
if (this.isStartOfGroup() || this.isStartOfPolicy()) {
|
|
1665
2154
|
break;
|
|
1666
2155
|
}
|
|
1667
|
-
if (this.check(
|
|
2156
|
+
if (this.stream.check(TokenTypes.IDENTIFIER)) {
|
|
1668
2157
|
group.addRule(this.parseRule());
|
|
1669
2158
|
}
|
|
1670
2159
|
else {
|
|
1671
|
-
this.syntaxError(`Unexpected token in group: ${this.peek().
|
|
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)) {
|
|
2205
|
+
group.addRule(this.parseRule());
|
|
2206
|
+
}
|
|
2207
|
+
else {
|
|
2208
|
+
this.stream.syntaxError(`Unexpected token in except group: ${this.stream.peek().type}`, this.stream.peek());
|
|
1672
2209
|
}
|
|
1673
2210
|
}
|
|
1674
2211
|
return group;
|
|
@@ -1681,57 +2218,54 @@ class AbilityDSLParser {
|
|
|
1681
2218
|
*/
|
|
1682
2219
|
parseRule() {
|
|
1683
2220
|
this.consumeLeadingComments();
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
// const specOperator = this.consume();
|
|
1690
|
-
// // return new AbilityRule({
|
|
1691
|
-
// // subject: '',
|
|
1692
|
-
// // resource,
|
|
1693
|
-
// // condition,
|
|
1694
|
-
// // name: meta.name,
|
|
1695
|
-
// // });
|
|
1696
|
-
// }
|
|
1697
|
-
const isNeverAlways = this.check(AbilityDSLToken.ALWAYS) || this.check(AbilityDSLToken.NEVER);
|
|
1698
|
-
if (!isNeverAlways && !this.check(AbilityDSLToken.IDENTIFIER)) {
|
|
1699
|
-
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());
|
|
1700
2226
|
}
|
|
1701
|
-
//
|
|
2227
|
+
// subject
|
|
1702
2228
|
const subject = isNeverAlways
|
|
1703
2229
|
? ''
|
|
1704
|
-
: this.
|
|
1705
|
-
//
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
let beforePos = this.pos;
|
|
1709
|
-
// Special operators that don't consume a value token.
|
|
1710
|
-
if (operator === AbilityDSLToken.EQ_NULL ||
|
|
1711
|
-
operator === AbilityDSLToken.NOT_EQ_NULL ||
|
|
1712
|
-
operator === AbilityDSLToken.NULL ||
|
|
1713
|
-
operator === AbilityDSLToken.ALWAYS ||
|
|
1714
|
-
operator === AbilityDSLToken.NEVER) {
|
|
1715
|
-
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);
|
|
1716
2234
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
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();
|
|
1719
2247
|
resource = this.parseValue();
|
|
2248
|
+
valueToken = this.stream.lookPrev();
|
|
2249
|
+
this.stream.commit();
|
|
1720
2250
|
}
|
|
1721
|
-
// Checking that there are no extra tokens after the value
|
|
1722
|
-
// (skip comments)
|
|
1723
2251
|
this.consumeLeadingComments();
|
|
1724
|
-
|
|
2252
|
+
this.consumeLeadingAnnotations();
|
|
2253
|
+
this.consumeLeadingAliases();
|
|
2254
|
+
// validation: identifier without dot → error
|
|
1725
2255
|
if (typeof resource === 'string' &&
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
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]);
|
|
1729
2260
|
}
|
|
1730
2261
|
return new AbilityRule({
|
|
2262
|
+
id: annotations.id?.value || null,
|
|
1731
2263
|
subject,
|
|
1732
2264
|
resource,
|
|
1733
2265
|
condition,
|
|
1734
|
-
name:
|
|
2266
|
+
name: annotations.name?.value || null,
|
|
2267
|
+
description: annotations.description?.value || null,
|
|
2268
|
+
disabled: annotations.disabled?.value ?? undefined,
|
|
1735
2269
|
});
|
|
1736
2270
|
}
|
|
1737
2271
|
// -------------------------------------------------------------------------
|
|
@@ -1742,200 +2276,241 @@ class AbilityDSLParser {
|
|
|
1742
2276
|
* Returns both the resulting AbilityCondition and the token type that was consumed.
|
|
1743
2277
|
*/
|
|
1744
2278
|
parseConditionOperator() {
|
|
1745
|
-
const savedPos = this.pos;
|
|
1746
2279
|
// "always"
|
|
2280
|
+
this.stream.mark();
|
|
1747
2281
|
if (this.matchWord('always')) {
|
|
1748
|
-
|
|
2282
|
+
this.stream.commit();
|
|
2283
|
+
return { condition: AbilityCondition.always, operator: TokenTypes.ALWAYS };
|
|
1749
2284
|
}
|
|
1750
|
-
this.
|
|
2285
|
+
this.stream.reset();
|
|
1751
2286
|
// "never"
|
|
2287
|
+
this.stream.mark();
|
|
1752
2288
|
if (this.matchWord('never')) {
|
|
1753
|
-
|
|
2289
|
+
this.stream.commit();
|
|
2290
|
+
return { condition: AbilityCondition.never, operator: TokenTypes.NEVER };
|
|
1754
2291
|
}
|
|
1755
|
-
this.
|
|
2292
|
+
this.stream.reset();
|
|
1756
2293
|
// "length equals"
|
|
2294
|
+
this.stream.mark();
|
|
1757
2295
|
if (this.matchWord('length') && this.matchWord('equals')) {
|
|
1758
|
-
|
|
2296
|
+
this.stream.commit();
|
|
2297
|
+
return { condition: AbilityCondition.length_equals, operator: TokenTypes.LEN_EQ };
|
|
1759
2298
|
}
|
|
1760
|
-
this.
|
|
2299
|
+
this.stream.reset();
|
|
1761
2300
|
// "length ="
|
|
2301
|
+
this.stream.mark();
|
|
1762
2302
|
if (this.matchWord('length') && this.matchSymbol('=')) {
|
|
1763
|
-
|
|
2303
|
+
this.stream.commit();
|
|
2304
|
+
return { condition: AbilityCondition.length_equals, operator: TokenTypes.LEN_EQ };
|
|
1764
2305
|
}
|
|
1765
|
-
this.
|
|
2306
|
+
this.stream.reset();
|
|
1766
2307
|
// "length greater than"
|
|
2308
|
+
this.stream.mark();
|
|
1767
2309
|
if (this.matchWord('length') && this.matchWord('greater') && this.matchWord('than')) {
|
|
1768
|
-
|
|
2310
|
+
this.stream.commit();
|
|
2311
|
+
return { condition: AbilityCondition.length_greater_than, operator: TokenTypes.LEN_GT };
|
|
1769
2312
|
}
|
|
1770
|
-
this.
|
|
2313
|
+
this.stream.reset();
|
|
1771
2314
|
// "length >"
|
|
2315
|
+
this.stream.mark();
|
|
1772
2316
|
if (this.matchWord('length') && this.matchSymbol('>')) {
|
|
1773
|
-
|
|
2317
|
+
this.stream.commit();
|
|
2318
|
+
return { condition: AbilityCondition.length_greater_than, operator: TokenTypes.LEN_GT };
|
|
1774
2319
|
}
|
|
1775
|
-
this.
|
|
2320
|
+
this.stream.reset();
|
|
1776
2321
|
// "length less than"
|
|
2322
|
+
this.stream.mark();
|
|
1777
2323
|
if (this.matchWord('length') && this.matchWord('less') && this.matchWord('than')) {
|
|
1778
|
-
|
|
2324
|
+
this.stream.commit();
|
|
2325
|
+
return { condition: AbilityCondition.length_less_than, operator: TokenTypes.LEN_LT };
|
|
1779
2326
|
}
|
|
1780
|
-
this.
|
|
2327
|
+
this.stream.reset();
|
|
1781
2328
|
// "length <"
|
|
2329
|
+
this.stream.mark();
|
|
1782
2330
|
if (this.matchWord('length') && this.matchSymbol('<')) {
|
|
1783
|
-
|
|
2331
|
+
this.stream.commit();
|
|
2332
|
+
return { condition: AbilityCondition.length_less_than, operator: TokenTypes.LEN_LT };
|
|
1784
2333
|
}
|
|
1785
|
-
this.
|
|
2334
|
+
this.stream.reset();
|
|
1786
2335
|
// "greater than or equal"
|
|
2336
|
+
this.stream.mark();
|
|
1787
2337
|
if (this.matchWord('greater') &&
|
|
1788
2338
|
this.matchWord('than') &&
|
|
1789
2339
|
this.matchWord('or') &&
|
|
1790
2340
|
this.matchWord('equal')) {
|
|
1791
|
-
|
|
2341
|
+
this.stream.commit();
|
|
2342
|
+
return { condition: AbilityCondition.greater_or_equal, operator: TokenTypes.GTE };
|
|
1792
2343
|
}
|
|
1793
|
-
this.
|
|
2344
|
+
this.stream.reset();
|
|
1794
2345
|
// greater than
|
|
2346
|
+
this.stream.mark();
|
|
1795
2347
|
if (this.matchWord('greater') && this.matchWord('than')) {
|
|
1796
|
-
|
|
2348
|
+
this.stream.commit();
|
|
2349
|
+
return { condition: AbilityCondition.greater_than, operator: TokenTypes.GT };
|
|
1797
2350
|
}
|
|
1798
|
-
this.
|
|
2351
|
+
this.stream.reset();
|
|
1799
2352
|
// less than or equal
|
|
2353
|
+
this.stream.mark();
|
|
1800
2354
|
if (this.matchWord('less') &&
|
|
1801
2355
|
this.matchWord('than') &&
|
|
1802
2356
|
this.matchWord('or') &&
|
|
1803
2357
|
this.matchWord('equal')) {
|
|
1804
|
-
|
|
2358
|
+
this.stream.commit();
|
|
2359
|
+
return { condition: AbilityCondition.less_or_equal, operator: TokenTypes.LTE };
|
|
1805
2360
|
}
|
|
1806
|
-
this.
|
|
2361
|
+
this.stream.reset();
|
|
1807
2362
|
// less than
|
|
1808
2363
|
if (this.matchWord('less') && this.matchWord('than')) {
|
|
1809
|
-
return { condition: AbilityCondition.less_than, operator:
|
|
2364
|
+
return { condition: AbilityCondition.less_than, operator: TokenTypes.LT };
|
|
1810
2365
|
}
|
|
1811
|
-
this.
|
|
2366
|
+
this.stream.reset();
|
|
1812
2367
|
// "not contains"
|
|
2368
|
+
this.stream.mark();
|
|
1813
2369
|
if (this.matchWord('not') && this.matchWord('contains')) {
|
|
2370
|
+
this.stream.commit();
|
|
1814
2371
|
return {
|
|
1815
2372
|
condition: AbilityCondition.not_contains,
|
|
1816
|
-
operator:
|
|
2373
|
+
operator: TokenTypes.NOT_CONTAINS,
|
|
1817
2374
|
};
|
|
1818
2375
|
}
|
|
1819
|
-
this.
|
|
2376
|
+
this.stream.reset();
|
|
1820
2377
|
// "not includes"
|
|
2378
|
+
this.stream.mark();
|
|
1821
2379
|
if (this.matchWord('not') && this.matchWord('includes')) {
|
|
2380
|
+
this.stream.commit();
|
|
1822
2381
|
return {
|
|
1823
2382
|
condition: AbilityCondition.not_contains,
|
|
1824
|
-
operator:
|
|
2383
|
+
operator: TokenTypes.NOT_CONTAINS,
|
|
1825
2384
|
};
|
|
1826
2385
|
}
|
|
1827
|
-
this.
|
|
2386
|
+
this.stream.reset();
|
|
1828
2387
|
// "not includes"
|
|
2388
|
+
this.stream.mark();
|
|
1829
2389
|
if (this.matchWord('not') && this.matchWord('has')) {
|
|
2390
|
+
this.stream.commit();
|
|
1830
2391
|
return {
|
|
1831
2392
|
condition: AbilityCondition.not_contains,
|
|
1832
|
-
operator:
|
|
2393
|
+
operator: TokenTypes.NOT_CONTAINS,
|
|
1833
2394
|
};
|
|
1834
2395
|
}
|
|
1835
|
-
this.
|
|
2396
|
+
this.stream.reset();
|
|
1836
2397
|
// "is equals"
|
|
2398
|
+
this.stream.mark();
|
|
1837
2399
|
if (this.matchWord('is') && this.matchWord('equals')) {
|
|
1838
|
-
|
|
2400
|
+
this.stream.commit();
|
|
2401
|
+
return { condition: AbilityCondition.equals, operator: TokenTypes.EQ };
|
|
1839
2402
|
}
|
|
1840
|
-
this.
|
|
2403
|
+
this.stream.reset();
|
|
1841
2404
|
// not equal
|
|
2405
|
+
this.stream.mark();
|
|
1842
2406
|
if (this.matchWord('not') && this.matchWord('equals')) {
|
|
1843
|
-
|
|
2407
|
+
this.stream.commit();
|
|
2408
|
+
return { condition: AbilityCondition.not_equals, operator: TokenTypes.NOT_EQ };
|
|
1844
2409
|
}
|
|
1845
|
-
this.
|
|
2410
|
+
this.stream.reset();
|
|
1846
2411
|
// is not equals
|
|
2412
|
+
this.stream.mark();
|
|
1847
2413
|
if (this.matchWord('is') && this.matchWord('not') && this.matchWord('equals')) {
|
|
1848
|
-
|
|
2414
|
+
this.stream.commit();
|
|
2415
|
+
return { condition: AbilityCondition.not_equals, operator: TokenTypes.NOT_EQ };
|
|
1849
2416
|
}
|
|
1850
|
-
this.
|
|
2417
|
+
this.stream.reset();
|
|
1851
2418
|
// is in
|
|
2419
|
+
this.stream.mark();
|
|
1852
2420
|
if (this.matchWord('is') && this.matchWord('in')) {
|
|
1853
|
-
|
|
2421
|
+
this.stream.commit();
|
|
2422
|
+
return { condition: AbilityCondition.in, operator: TokenTypes.IN };
|
|
1854
2423
|
}
|
|
1855
|
-
this.
|
|
2424
|
+
this.stream.reset();
|
|
1856
2425
|
// not in
|
|
2426
|
+
this.stream.mark();
|
|
1857
2427
|
if (this.matchWord('not') && this.matchWord('in')) {
|
|
1858
|
-
|
|
2428
|
+
this.stream.commit();
|
|
2429
|
+
return { condition: AbilityCondition.not_in, operator: TokenTypes.NOT_IN };
|
|
1859
2430
|
}
|
|
1860
|
-
this.
|
|
2431
|
+
this.stream.reset();
|
|
1861
2432
|
// is not null
|
|
2433
|
+
this.stream.mark();
|
|
1862
2434
|
if (this.matchWord('is') && this.matchWord('not')) {
|
|
1863
|
-
if (this.check(
|
|
1864
|
-
this.
|
|
2435
|
+
if (this.stream.check(TokenTypes.NULL)) {
|
|
2436
|
+
this.stream.next();
|
|
2437
|
+
this.stream.commit();
|
|
1865
2438
|
return {
|
|
1866
2439
|
condition: AbilityCondition.not_equals,
|
|
1867
|
-
operator:
|
|
2440
|
+
operator: TokenTypes.NOT_EQ_NULL,
|
|
1868
2441
|
};
|
|
1869
2442
|
}
|
|
1870
2443
|
}
|
|
1871
|
-
this.
|
|
2444
|
+
this.stream.reset();
|
|
1872
2445
|
// is null
|
|
2446
|
+
this.stream.mark();
|
|
1873
2447
|
if (this.matchWord('is') && this.matchWord('null')) {
|
|
1874
|
-
if (this.check(
|
|
1875
|
-
this.
|
|
2448
|
+
if (this.stream.check(TokenTypes.NULL)) {
|
|
2449
|
+
this.stream.commit();
|
|
2450
|
+
this.stream.next();
|
|
1876
2451
|
return {
|
|
1877
2452
|
condition: AbilityCondition.equals,
|
|
1878
|
-
operator:
|
|
2453
|
+
operator: TokenTypes.EQ_NULL,
|
|
1879
2454
|
};
|
|
1880
2455
|
}
|
|
1881
2456
|
}
|
|
1882
|
-
this.
|
|
2457
|
+
this.stream.reset();
|
|
1883
2458
|
// Single token (symbol or keyword)
|
|
1884
|
-
const token = this.peek();
|
|
1885
|
-
if (token.
|
|
1886
|
-
token.
|
|
1887
|
-
token.
|
|
1888
|
-
this.syntaxError(`Expected comparison operator, got \`${token.value}\``, token, [
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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,
|
|
1892
2467
|
]);
|
|
1893
2468
|
}
|
|
1894
|
-
this.
|
|
1895
|
-
switch (token.
|
|
1896
|
-
case
|
|
2469
|
+
this.stream.next();
|
|
2470
|
+
switch (token.type) {
|
|
2471
|
+
case TokenTypes.SYMBOL:
|
|
1897
2472
|
if (token.value === '=' || token.value === '==')
|
|
1898
|
-
return { condition: AbilityCondition.equals, operator:
|
|
2473
|
+
return { condition: AbilityCondition.equals, operator: TokenTypes.EQ };
|
|
1899
2474
|
if (token.value === '!=' || token.value === '<>')
|
|
1900
|
-
return { condition: AbilityCondition.not_equals, operator:
|
|
2475
|
+
return { condition: AbilityCondition.not_equals, operator: TokenTypes.NOT_EQ };
|
|
1901
2476
|
if (token.value === '>')
|
|
1902
|
-
return { condition: AbilityCondition.greater_than, operator:
|
|
2477
|
+
return { condition: AbilityCondition.greater_than, operator: TokenTypes.GT };
|
|
1903
2478
|
if (token.value === '<')
|
|
1904
|
-
return { condition: AbilityCondition.less_than, operator:
|
|
2479
|
+
return { condition: AbilityCondition.less_than, operator: TokenTypes.LT };
|
|
1905
2480
|
if (token.value === '>=')
|
|
1906
|
-
return { condition: AbilityCondition.greater_or_equal, operator:
|
|
2481
|
+
return { condition: AbilityCondition.greater_or_equal, operator: TokenTypes.GTE };
|
|
1907
2482
|
if (token.value === '<=')
|
|
1908
|
-
return { condition: AbilityCondition.less_or_equal, operator:
|
|
2483
|
+
return { condition: AbilityCondition.less_or_equal, operator: TokenTypes.LTE };
|
|
1909
2484
|
break;
|
|
1910
|
-
case
|
|
2485
|
+
case TokenTypes.KEYWORD:
|
|
1911
2486
|
if (token.value === 'contains' || token.value === 'includes' || token.value === 'has')
|
|
1912
|
-
return { condition: AbilityCondition.contains, operator:
|
|
2487
|
+
return { condition: AbilityCondition.contains, operator: TokenTypes.CONTAINS };
|
|
1913
2488
|
if (token.value === 'in')
|
|
1914
|
-
return { condition: AbilityCondition.in, operator:
|
|
2489
|
+
return { condition: AbilityCondition.in, operator: TokenTypes.IN };
|
|
1915
2490
|
if (token.value === 'equals')
|
|
1916
|
-
return { condition: AbilityCondition.equals, operator:
|
|
2491
|
+
return { condition: AbilityCondition.equals, operator: TokenTypes.EQ };
|
|
1917
2492
|
if (token.value === 'gte') {
|
|
1918
|
-
return { condition: AbilityCondition.greater_or_equal, operator:
|
|
2493
|
+
return { condition: AbilityCondition.greater_or_equal, operator: TokenTypes.GTE };
|
|
1919
2494
|
}
|
|
1920
2495
|
if (token.value === 'greater' || token.value === 'gt') {
|
|
1921
2496
|
// If we come here, it means "greater" without "than" – treat as '>'
|
|
1922
|
-
return { condition: AbilityCondition.greater_than, operator:
|
|
2497
|
+
return { condition: AbilityCondition.greater_than, operator: TokenTypes.GT };
|
|
1923
2498
|
}
|
|
1924
2499
|
if (token.value === 'less' || token.value === 'lt') {
|
|
1925
|
-
return { condition: AbilityCondition.less_than, operator:
|
|
2500
|
+
return { condition: AbilityCondition.less_than, operator: TokenTypes.LT };
|
|
1926
2501
|
}
|
|
1927
2502
|
if (token.value === 'lte') {
|
|
1928
|
-
return { condition: AbilityCondition.less_or_equal, operator:
|
|
2503
|
+
return { condition: AbilityCondition.less_or_equal, operator: TokenTypes.LTE };
|
|
1929
2504
|
}
|
|
1930
2505
|
if (token.value === 'is') {
|
|
1931
2506
|
// "is" alone -> equals
|
|
1932
|
-
return { condition: AbilityCondition.equals, operator:
|
|
2507
|
+
return { condition: AbilityCondition.equals, operator: TokenTypes.EQ };
|
|
1933
2508
|
}
|
|
1934
2509
|
break;
|
|
1935
2510
|
}
|
|
1936
|
-
return this.syntaxError(`Unexpected operator token \`${token.value}\``, token, [
|
|
1937
|
-
|
|
1938
|
-
|
|
2511
|
+
return this.stream.syntaxError(`Unexpected operator token \`${token.value}\``, token, [
|
|
2512
|
+
TokenTypes.SYMBOL,
|
|
2513
|
+
TokenTypes.KEYWORD,
|
|
1939
2514
|
]);
|
|
1940
2515
|
}
|
|
1941
2516
|
/**
|
|
@@ -1944,26 +2519,26 @@ class AbilityDSLParser {
|
|
|
1944
2519
|
* @returns True if the next token has that value.
|
|
1945
2520
|
*/
|
|
1946
2521
|
matchWord(word) {
|
|
1947
|
-
if (this.
|
|
2522
|
+
if (this.stream.eof()) {
|
|
1948
2523
|
return false;
|
|
1949
2524
|
}
|
|
1950
|
-
const token = this.peek();
|
|
1951
|
-
if ((token.
|
|
1952
|
-
token.
|
|
1953
|
-
token.
|
|
1954
|
-
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) &&
|
|
1955
2530
|
token.value === word) {
|
|
1956
|
-
this.
|
|
2531
|
+
this.stream.next();
|
|
1957
2532
|
return true;
|
|
1958
2533
|
}
|
|
1959
2534
|
return false;
|
|
1960
2535
|
}
|
|
1961
2536
|
matchSymbol(symbol) {
|
|
1962
|
-
if (this.
|
|
2537
|
+
if (this.stream.eof())
|
|
1963
2538
|
return false;
|
|
1964
|
-
const token = this.peek();
|
|
1965
|
-
if (token.
|
|
1966
|
-
this.
|
|
2539
|
+
const token = this.stream.peek();
|
|
2540
|
+
if (token.type === TokenTypes.SYMBOL && token.value === symbol) {
|
|
2541
|
+
this.stream.next();
|
|
1967
2542
|
return true;
|
|
1968
2543
|
}
|
|
1969
2544
|
return false;
|
|
@@ -1977,32 +2552,34 @@ class AbilityDSLParser {
|
|
|
1977
2552
|
*/
|
|
1978
2553
|
parseValue() {
|
|
1979
2554
|
// Arrays start with a left bracket
|
|
1980
|
-
if (this.check(
|
|
1981
|
-
this.
|
|
2555
|
+
if (this.stream.check(TokenTypes.LBRACKET)) {
|
|
2556
|
+
this.stream.next();
|
|
1982
2557
|
return this.parseArray();
|
|
1983
2558
|
}
|
|
1984
2559
|
// Ensure we are not about to read a structural token as a value.
|
|
1985
|
-
const token = this.peek();
|
|
1986
|
-
if (token.
|
|
1987
|
-
token.
|
|
1988
|
-
token.
|
|
1989
|
-
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);
|
|
1990
2565
|
}
|
|
1991
|
-
this.
|
|
2566
|
+
this.stream.next();
|
|
1992
2567
|
// CHECK THIS SWITCH COMPARE
|
|
1993
|
-
switch (token.
|
|
1994
|
-
case
|
|
2568
|
+
switch (token.type) {
|
|
2569
|
+
case TokenTypes.STRING:
|
|
1995
2570
|
return token.value;
|
|
1996
|
-
case
|
|
2571
|
+
case TokenTypes.NUMBER:
|
|
1997
2572
|
return Number(token.value);
|
|
1998
|
-
case
|
|
2573
|
+
case TokenTypes.BOOLEAN:
|
|
1999
2574
|
return token.value === 'true';
|
|
2000
|
-
case
|
|
2575
|
+
case TokenTypes.NULL:
|
|
2001
2576
|
return null;
|
|
2002
|
-
case
|
|
2577
|
+
case TokenTypes.IDENTIFIER:
|
|
2003
2578
|
return token.value;
|
|
2004
2579
|
default: {
|
|
2005
|
-
this.syntaxError(`Unexpected value token "${token.value}"`, token
|
|
2580
|
+
this.stream.syntaxError(`Unexpected value token "${token.value}"`, token, [
|
|
2581
|
+
TokenTypes.KEYWORD,
|
|
2582
|
+
]);
|
|
2006
2583
|
}
|
|
2007
2584
|
}
|
|
2008
2585
|
}
|
|
@@ -2013,11 +2590,11 @@ class AbilityDSLParser {
|
|
|
2013
2590
|
parseArray() {
|
|
2014
2591
|
const arr = [];
|
|
2015
2592
|
// Handle empty array
|
|
2016
|
-
if (this.check(
|
|
2017
|
-
this.
|
|
2593
|
+
if (this.stream.check(TokenTypes.RBRACKET)) {
|
|
2594
|
+
this.stream.next();
|
|
2018
2595
|
return arr;
|
|
2019
2596
|
}
|
|
2020
|
-
while (!this.
|
|
2597
|
+
while (!this.stream.eof() && !this.stream.check(TokenTypes.RBRACKET)) {
|
|
2021
2598
|
const value = this.parseValue();
|
|
2022
2599
|
// Flatten nested arrays if they appear (though grammar doesn't currently allow nesting).
|
|
2023
2600
|
if (Array.isArray(value)) {
|
|
@@ -2030,155 +2607,359 @@ class AbilityDSLParser {
|
|
|
2030
2607
|
}
|
|
2031
2608
|
else if (value === null) {
|
|
2032
2609
|
// Null is allowed in arrays? Currently, we throw.
|
|
2033
|
-
this.syntaxError('Unexpected null in array', this.peek());
|
|
2610
|
+
this.stream.syntaxError('Unexpected null in array', this.stream.peek());
|
|
2034
2611
|
}
|
|
2035
2612
|
// Optional comma between elements
|
|
2036
|
-
if (this.check(
|
|
2037
|
-
this.
|
|
2613
|
+
if (this.stream.check(TokenTypes.COMMA)) {
|
|
2614
|
+
this.stream.next();
|
|
2038
2615
|
}
|
|
2039
2616
|
}
|
|
2040
|
-
this.
|
|
2617
|
+
this.stream.expect(TokenTypes.RBRACKET, 'Expected "]"');
|
|
2041
2618
|
return arr;
|
|
2042
2619
|
}
|
|
2043
2620
|
// -------------------------------------------------------------------------
|
|
2044
|
-
// #region
|
|
2621
|
+
// #region comments
|
|
2045
2622
|
// -------------------------------------------------------------------------
|
|
2046
2623
|
consumeLeadingComments() {
|
|
2047
|
-
while (this.check(
|
|
2048
|
-
|
|
2049
|
-
this.processCommentToken(token);
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
const lineIdx = token.line - 1;
|
|
2075
|
-
const lineBefore = lineIdx > 0 ? lines[lineIdx - 1] : '';
|
|
2076
|
-
const current = lines[lineIdx];
|
|
2077
|
-
const linesAfter = lineIdx + 1 < lines.length ? lines[lineIdx + 1] : '';
|
|
2078
|
-
const wave = ' '.repeat(Math.max(0, token.column - 1)) + '~'.repeat(token.value.length);
|
|
2079
|
-
const lineNumWidth = String(token.line + 1).length;
|
|
2080
|
-
const num = (n) => String(n).padStart(lineNumWidth, ' ');
|
|
2081
|
-
let context = '';
|
|
2082
|
-
if (lineBefore.trim() !== '') {
|
|
2083
|
-
context += `${num(token.line - 1)} | ${lineBefore}\n`;
|
|
2084
|
-
}
|
|
2085
|
-
context += `${num(token.line)} | ${current}\n`;
|
|
2086
|
-
context += `${' '.repeat(lineNumWidth)} | ${wave}\n`;
|
|
2087
|
-
if (linesAfter.trim() !== '') {
|
|
2088
|
-
context += `${num(token.line + 1)} | ${linesAfter}`;
|
|
2089
|
-
}
|
|
2090
|
-
let finalDetails = details;
|
|
2091
|
-
if (expected && expected?.length > 0) {
|
|
2092
|
-
const actual = token.value;
|
|
2093
|
-
const suggestion = this.suggest(actual, expected);
|
|
2094
|
-
const detailsMsg = `${details}\nDetails: Unexpected value token \`${actual}\``;
|
|
2095
|
-
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
|
+
}
|
|
2096
2651
|
}
|
|
2097
|
-
throw new AbilityDSLSyntaxError(token.line, token.column, context + '\n', finalDetails);
|
|
2098
2652
|
}
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
if (
|
|
2110
|
-
|
|
2111
|
-
|
|
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);
|
|
2112
2680
|
}
|
|
2113
2681
|
}
|
|
2114
|
-
return best;
|
|
2115
2682
|
}
|
|
2116
|
-
|
|
2117
|
-
const
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
for (
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
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());
|
|
2126
2695
|
}
|
|
2127
2696
|
}
|
|
2128
|
-
return
|
|
2697
|
+
return ann;
|
|
2129
2698
|
}
|
|
2130
2699
|
// -------------------------------------------------------------------------
|
|
2131
|
-
// #region
|
|
2700
|
+
// #region Helpers
|
|
2132
2701
|
// -------------------------------------------------------------------------
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
for (const t of types) {
|
|
2136
|
-
if (token && token.code === t) {
|
|
2137
|
-
return this.advance();
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
const expected = types.map(t => t).join(', ');
|
|
2141
|
-
const actual = token ? token.value : AbilityDSLToken.EOF;
|
|
2142
|
-
const suggestion = this.suggest(actual, types);
|
|
2143
|
-
const details = `${message}\nDetails: Unexpected token \`${actual}\`, expected one of: ${expected}.`;
|
|
2144
|
-
const finalMsg = suggestion ? `${details} Did you mean \`${suggestion}\`?` : details;
|
|
2145
|
-
this.syntaxError(finalMsg, token ?? this.tokens[this.tokens.length - 1]);
|
|
2702
|
+
isStartOfPolicy() {
|
|
2703
|
+
return this.stream.check(TokenTypes.EFFECT);
|
|
2146
2704
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
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;
|
|
2151
2773
|
}
|
|
2152
|
-
const
|
|
2153
|
-
|
|
2154
|
-
const suggestion = this.suggest(actual, [type]);
|
|
2155
|
-
const details = `${message}\nDetails: Unexpected token \`${actual}\`, expected "${expected}".`;
|
|
2156
|
-
const finalMsg = suggestion ? `${details} Did you mean \`${suggestion}\`?` : details;
|
|
2157
|
-
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;
|
|
2158
2776
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
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;
|
|
2163
2799
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
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;
|
|
2166
2829
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
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;
|
|
2169
2853
|
}
|
|
2170
|
-
|
|
2171
|
-
|
|
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;
|
|
2172
2879
|
}
|
|
2173
|
-
|
|
2174
|
-
|
|
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;
|
|
2175
2909
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
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;
|
|
2178
2960
|
}
|
|
2179
2961
|
}
|
|
2180
2962
|
|
|
2181
|
-
exports.AbilityCode = AbilityCode;
|
|
2182
2963
|
exports.AbilityCompare = AbilityCompare;
|
|
2183
2964
|
exports.AbilityCondition = AbilityCondition;
|
|
2184
2965
|
exports.AbilityDSLLexer = AbilityDSLLexer;
|
|
@@ -2196,7 +2977,21 @@ exports.AbilityPolicy = AbilityPolicy;
|
|
|
2196
2977
|
exports.AbilityPolicyEffect = AbilityPolicyEffect;
|
|
2197
2978
|
exports.AbilityResolver = AbilityResolver;
|
|
2198
2979
|
exports.AbilityResult = AbilityResult;
|
|
2199
|
-
exports.AbilityResultState = AbilityResultState;
|
|
2200
2980
|
exports.AbilityRule = AbilityRule;
|
|
2201
2981
|
exports.AbilityRuleSet = AbilityRuleSet;
|
|
2982
|
+
exports.AbilityStrategy = AbilityStrategy;
|
|
2202
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;
|