@via-profit/ability 3.6.0 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +20 -13
- package/dist/index.js +443 -213
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -134,6 +134,7 @@ declare class AbilityRule<Resources extends object = object, Environment extends
|
|
|
134
134
|
resource: AbilityRuleConfig['resource'];
|
|
135
135
|
condition: AbilityConditionType;
|
|
136
136
|
}>): AbilityRule<Resources, Environment>;
|
|
137
|
+
hash(): string;
|
|
137
138
|
static equals<Resources extends object = object, Environment extends object = object>(subject: string, resource: AbilityRuleConfig['resource']): AbilityRule<Resources, Environment>;
|
|
138
139
|
static notEquals<Resources extends object = object, Environment extends object = object>(subject: string, resource: AbilityRuleConfig['resource']): AbilityRule<Resources, Environment>;
|
|
139
140
|
static contains<Resources extends object = object, Environment extends object = object>(subject: string, resource: AbilityRuleConfig['resource']): AbilityRule<Resources, Environment>;
|
|
@@ -199,6 +200,7 @@ declare class AbilityRuleSet<R extends ResourceObject = Record<string, unknown>,
|
|
|
199
200
|
compareMethod: AbilityCompareType;
|
|
200
201
|
rules: AbilityRule<R, E>[];
|
|
201
202
|
}>): AbilityRuleSet<R, E>;
|
|
203
|
+
hash(): string;
|
|
202
204
|
static and(rules: AbilityRule[]): AbilityRuleSet<Record<string, unknown>, Record<string, unknown>>;
|
|
203
205
|
static or(rules: AbilityRule[]): AbilityRuleSet<Record<string, unknown>, Record<string, unknown>>;
|
|
204
206
|
}
|
|
@@ -276,15 +278,15 @@ declare class AbilityPolicy<R extends ResourceObject = Record<string, unknown>,
|
|
|
276
278
|
* one of the rules returns as «permit»
|
|
277
279
|
*/
|
|
278
280
|
compareMethod: AbilityCompareType;
|
|
281
|
+
/**
|
|
282
|
+
* Policy ID
|
|
283
|
+
*/
|
|
284
|
+
id: string;
|
|
279
285
|
/**
|
|
280
286
|
* Policy name
|
|
281
287
|
*/
|
|
282
288
|
name: string;
|
|
283
289
|
description?: string | null;
|
|
284
|
-
/**
|
|
285
|
-
* Policy ID
|
|
286
|
-
*/
|
|
287
|
-
id: string;
|
|
288
290
|
/**
|
|
289
291
|
* Running the `enforce` or `resolve` method
|
|
290
292
|
* will select only those from all passed policies that fall under the specified permission key.
|
|
@@ -325,6 +327,7 @@ declare class AbilityPolicy<R extends ResourceObject = Record<string, unknown>,
|
|
|
325
327
|
compareMethod: AbilityCompareType;
|
|
326
328
|
ruleSet: AbilityRuleSet<R, E>[];
|
|
327
329
|
}>): AbilityPolicy<R, E>;
|
|
330
|
+
hash(): string;
|
|
328
331
|
}
|
|
329
332
|
|
|
330
333
|
type Primitive = string | number | boolean | null | undefined;
|
|
@@ -336,6 +339,7 @@ type EnvironmentObject = Record<string, unknown>;
|
|
|
336
339
|
type ResourcesMap = Record<string, ResourceObject>;
|
|
337
340
|
declare class AbilityTypeGenerator {
|
|
338
341
|
readonly policies: readonly AbilityPolicy[];
|
|
342
|
+
private readonly policyEntries;
|
|
339
343
|
constructor(policies: readonly AbilityPolicy[]);
|
|
340
344
|
/**
|
|
341
345
|
* Generates TypeScript type definitions based on the provided policies.
|
|
@@ -411,8 +415,8 @@ declare class AbilityResult<R extends ResourceObject = Record<string, unknown>,
|
|
|
411
415
|
* Useful for debugging, logging, or building UI tools that visualize permission logic.
|
|
412
416
|
*/
|
|
413
417
|
explain(): readonly AbilityExplain[];
|
|
414
|
-
isAllowed()
|
|
415
|
-
isDenied()
|
|
418
|
+
isAllowed: () => boolean;
|
|
419
|
+
isDenied: () => boolean;
|
|
416
420
|
}
|
|
417
421
|
|
|
418
422
|
interface AbilityResolverOptions<TTags extends string> {
|
|
@@ -420,6 +424,7 @@ interface AbilityResolverOptions<TTags extends string> {
|
|
|
420
424
|
}
|
|
421
425
|
type ExtractResources<P> = P extends AbilityPolicy<infer R, any, any> ? R : never;
|
|
422
426
|
type ExtractEnvironment<P> = P extends AbilityPolicy<any, infer E, any> ? E : never;
|
|
427
|
+
type ExtractPermission<R> = R extends AbilityResolver<infer P, any, any> ? keyof ExtractResources<P> & string : never;
|
|
423
428
|
type ExtractResourceByPermission<P, Perm extends string> = P extends AbilityPolicy<infer R, any, any> ? (Perm extends keyof R ? R[Perm] : never) : never;
|
|
424
429
|
type ExtractEnvironmentByPermission<P, Perm extends string> = P extends AbilityPolicy<any, infer E, any> ? (Perm extends keyof E ? E[Perm] : never) : never;
|
|
425
430
|
declare class AbilityResolver<P extends AbilityPolicy<any, any, any>, S extends AbilityStrategy<P extends AbilityPolicy<infer R, infer E, any> ? R : never, P extends AbilityPolicy<any, infer E, any> ? E : never>, TTags extends string = P extends AbilityPolicy<any, any, infer T> ? T : never> {
|
|
@@ -440,14 +445,16 @@ declare class AbilityResolver<P extends AbilityPolicy<any, any, any>, S extends
|
|
|
440
445
|
resolve<Permission extends keyof ExtractResources<P> & string>(permission: Permission, resource: ExtractResourceByPermission<P, Permission>, environment?: ExtractEnvironmentByPermission<P, Permission>): AbilityResult<ExtractResourceByPermission<P, Permission>, ExtractEnvironment<P>>;
|
|
441
446
|
enforce<Permission extends keyof ExtractResources<P> & string>(permission: Permission, resource: ExtractResourceByPermission<P, Permission>, environment?: ExtractEnvironmentByPermission<P, Permission>): void | never;
|
|
442
447
|
/**
|
|
448
|
+
* @deprecated - will be removed
|
|
449
|
+
*
|
|
443
450
|
* Check if the permission key is contained in another permission key
|
|
444
451
|
* @param permissionA - The first permission to check
|
|
445
452
|
* @param permissionB - The second permission to check
|
|
446
453
|
*/
|
|
447
454
|
static isInPermissionContain(permissionA: string, permissionB: string): boolean;
|
|
448
455
|
private toArray;
|
|
449
|
-
|
|
450
|
-
|
|
456
|
+
static normalizePermission(permission: string): string;
|
|
457
|
+
static matchPermissions(policySegments: string[], inputSegments: string[]): boolean;
|
|
451
458
|
}
|
|
452
459
|
|
|
453
460
|
declare class AbilityJSONParser {
|
|
@@ -456,13 +463,13 @@ declare class AbilityJSONParser {
|
|
|
456
463
|
* @param configs - Array of policy configurations
|
|
457
464
|
* @returns Array of AbilityPolicy instances
|
|
458
465
|
*/
|
|
459
|
-
static parse<
|
|
460
|
-
static parsePolicy<
|
|
461
|
-
static parseRule<
|
|
466
|
+
static parse<R extends ResourceObject, E extends EnvironmentObject, T extends string = string>(configs: readonly AbilityPolicyConfig[]): AbilityPolicy<R, E, T>[];
|
|
467
|
+
static parsePolicy<R extends ResourceObject, E extends EnvironmentObject, T extends string = string>(config: AbilityPolicyConfig): AbilityPolicy<R, E, T>;
|
|
468
|
+
static parseRule<R extends ResourceObject, E extends EnvironmentObject>(config: AbilityRuleConfig): AbilityRule<R, E>;
|
|
462
469
|
/**
|
|
463
470
|
* Parse the config JSON format to Group class instance
|
|
464
471
|
*/
|
|
465
|
-
static parseRuleSet<
|
|
472
|
+
static parseRuleSet<R extends ResourceObject, E extends EnvironmentObject>(config: AbilityRuleSetConfig): AbilityRuleSet<R, E>;
|
|
466
473
|
static ruleToJSON(rule: AbilityRule): AbilityRuleConfig;
|
|
467
474
|
static ruleSetToJSON(ruleSet: AbilityRuleSet): AbilityRuleSetConfig;
|
|
468
475
|
static policyToJSON(policy: AbilityPolicy): AbilityPolicyConfig;
|
|
@@ -800,4 +807,4 @@ declare class PriorityStrategy<R extends ResourceObject, E extends EnvironmentOb
|
|
|
800
807
|
}
|
|
801
808
|
|
|
802
809
|
export { AbilityCompare, AbilityCondition, AbilityDSLLexer, AbilityDSLParser, AbilityDSLToken, AbilityError, AbilityExplain, AbilityExplainPolicy, AbilityExplainRule, AbilityExplainRuleSet, AbilityJSONParser, AbilityMatch, AbilityParserError, AbilityPolicy, AbilityPolicyEffect, AbilityResolver, AbilityResult, AbilityRule, AbilityRuleSet, AbilityStrategy, AbilityTypeGenerator, AllMustPermitStrategy, AnyPermitStrategy, DenyOverridesStrategy, FirstMatchStrategy, OnlyOneApplicableStrategy, PermitOverridesStrategy, PriorityStrategy, SequentialLastMatchStrategy, TokenTypes, ability, fromLiteral, isConditionEqual, isConditionNotEqual, toLiteral };
|
|
803
|
-
export type { AbilityCompareType, AbilityConditionCode, AbilityConditionLiteral, AbilityConditionType, AbilityExplainConfig, AbilityExplainType, AbilityMatchType, AbilityPolicyConfig, AbilityPolicyConstructorProps, AbilityPolicyEffectType, AbilityResolverOptions, AbilityRuleConfig, AbilityRuleConstructorProps, AbilityRuleSetConfig, AbilityRuleSetConstructorProps, EnvironmentObject, NestedDict, Primitive, ResourceObject, ResourcesMap, TokenType, TokenTypeCode };
|
|
810
|
+
export type { AbilityCompareType, AbilityConditionCode, AbilityConditionLiteral, AbilityConditionType, AbilityExplainConfig, AbilityExplainType, AbilityMatchType, AbilityPolicyConfig, AbilityPolicyConstructorProps, AbilityPolicyEffectType, AbilityResolverOptions, AbilityRuleConfig, AbilityRuleConstructorProps, AbilityRuleSetConfig, AbilityRuleSetConstructorProps, EnvironmentObject, ExtractEnvironment, ExtractEnvironmentByPermission, ExtractPermission, ExtractResourceByPermission, ExtractResources, NestedDict, Primitive, ResourceObject, ResourcesMap, TokenType, TokenTypeCode };
|
package/dist/index.js
CHANGED
|
@@ -126,10 +126,198 @@ const AbilityMatch = {
|
|
|
126
126
|
disabled: brand$2('disabled'),
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
+
class AbilityExplain {
|
|
130
|
+
type;
|
|
131
|
+
children;
|
|
132
|
+
name;
|
|
133
|
+
match;
|
|
134
|
+
constructor(config, children = []) {
|
|
135
|
+
this.type = config.type;
|
|
136
|
+
this.children = children;
|
|
137
|
+
this.name = config.name;
|
|
138
|
+
this.match = config.match;
|
|
139
|
+
}
|
|
140
|
+
toString(indent = 0) {
|
|
141
|
+
const pad = ' '.repeat(indent);
|
|
142
|
+
const mark = this.match === AbilityMatch.match ? '✓' : '✗';
|
|
143
|
+
let out = '';
|
|
144
|
+
if (this.type === 'policy') {
|
|
145
|
+
out += '\n';
|
|
146
|
+
}
|
|
147
|
+
out += `${pad}${mark} ${this.type} «${this.name}» is ${this.match}`;
|
|
148
|
+
this.children.forEach(child => {
|
|
149
|
+
out += '\n' + child.toString(indent + 1);
|
|
150
|
+
});
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
class AbilityExplainRule extends AbilityExplain {
|
|
155
|
+
constructor(rule) {
|
|
156
|
+
super({
|
|
157
|
+
type: 'rule',
|
|
158
|
+
match: rule.state,
|
|
159
|
+
name: rule.name,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
class AbilityExplainRuleSet extends AbilityExplain {
|
|
164
|
+
constructor(ruleSet) {
|
|
165
|
+
const children = ruleSet.rules.map(rule => new AbilityExplainRule(rule));
|
|
166
|
+
super({
|
|
167
|
+
type: 'ruleSet',
|
|
168
|
+
match: ruleSet.state,
|
|
169
|
+
name: ruleSet.name,
|
|
170
|
+
}, children);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
class AbilityExplainPolicy extends AbilityExplain {
|
|
174
|
+
constructor(policy) {
|
|
175
|
+
const children = policy.ruleSet.map(ruleSet => new AbilityExplainRuleSet(ruleSet));
|
|
176
|
+
super({
|
|
177
|
+
type: 'policy',
|
|
178
|
+
name: policy.priority > -1 ? `@priority ${policy.priority} ${policy.name}` : policy.name,
|
|
179
|
+
match: policy.matchState,
|
|
180
|
+
}, children);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
class AbilityResult {
|
|
185
|
+
effect;
|
|
186
|
+
strategy;
|
|
187
|
+
constructor(effect, strategy) {
|
|
188
|
+
this.effect = effect;
|
|
189
|
+
this.strategy = strategy;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Returns a list of explanations for each policy involved in the ability evaluation.
|
|
193
|
+
* Each item describes how a specific policy contributed to the final permission result.
|
|
194
|
+
*
|
|
195
|
+
* Useful for debugging, logging, or building UI tools that visualize permission logic.
|
|
196
|
+
*/
|
|
197
|
+
explain() {
|
|
198
|
+
return this.strategy.policies.map(policy => {
|
|
199
|
+
return new AbilityExplainPolicy(policy);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
isAllowed = () => {
|
|
203
|
+
return this.strategy.isAllowed();
|
|
204
|
+
};
|
|
205
|
+
isDenied = () => {
|
|
206
|
+
return this.strategy.isDenied();
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
class AbilityResolver {
|
|
211
|
+
StrategyClass;
|
|
212
|
+
policyEntries;
|
|
213
|
+
constructor(
|
|
214
|
+
/**
|
|
215
|
+
* `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.
|
|
216
|
+
*/
|
|
217
|
+
policyOrListOfPolicies, strategy, options = {}) {
|
|
218
|
+
const policies = this.toArray(policyOrListOfPolicies);
|
|
219
|
+
const filtered = options.tags
|
|
220
|
+
? policies.filter(p => p.tags.some(tag => options.tags.includes(tag)))
|
|
221
|
+
: policies;
|
|
222
|
+
const sorted = [...filtered].sort((a, b) => b.priority - a.priority);
|
|
223
|
+
this.policyEntries = sorted.map(policy => ({
|
|
224
|
+
policy,
|
|
225
|
+
normalizedPermission: AbilityResolver.normalizePermission(policy.permission),
|
|
226
|
+
segments: AbilityResolver.normalizePermission(policy.permission).split('.'),
|
|
227
|
+
}));
|
|
228
|
+
this.StrategyClass = strategy;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Resolve policy for the resource and permission key
|
|
232
|
+
*
|
|
233
|
+
* @param permission - Permission key
|
|
234
|
+
* @param resource - Resource
|
|
235
|
+
* @param environment
|
|
236
|
+
*/
|
|
237
|
+
resolve(permission, resource, environment) {
|
|
238
|
+
const inputNormalized = AbilityResolver.normalizePermission(String(permission));
|
|
239
|
+
const inputSegments = inputNormalized.split('.');
|
|
240
|
+
const filteredPolicies = this.policyEntries
|
|
241
|
+
.filter(entry => AbilityResolver.matchPermissions(entry.segments, inputSegments))
|
|
242
|
+
.map(entry => entry.policy);
|
|
243
|
+
// 2. check the policies
|
|
244
|
+
for (const policy of filteredPolicies) {
|
|
245
|
+
if (policy.disabled) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const policyMatchState = policy.check(resource, environment);
|
|
249
|
+
if (policyMatchState === AbilityMatch.pending) {
|
|
250
|
+
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.`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// 3. Use strategy
|
|
254
|
+
const strategy = new this.StrategyClass(filteredPolicies);
|
|
255
|
+
const effect = strategy.evaluate();
|
|
256
|
+
return new AbilityResult(effect, strategy);
|
|
257
|
+
}
|
|
258
|
+
enforce(permission, resource, environment) {
|
|
259
|
+
const result = this.resolve(permission, resource, environment);
|
|
260
|
+
if (result.isDenied()) {
|
|
261
|
+
throw new AbilityError(`Permission denied`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* @deprecated - will be removed
|
|
266
|
+
*
|
|
267
|
+
* Check if the permission key is contained in another permission key
|
|
268
|
+
* @param permissionA - The first permission to check
|
|
269
|
+
* @param permissionB - The second permission to check
|
|
270
|
+
*/
|
|
271
|
+
static isInPermissionContain(permissionA, permissionB) {
|
|
272
|
+
const A = permissionA.split('.');
|
|
273
|
+
const B = permissionB.split('.');
|
|
274
|
+
const [longer, shorter] = A.length >= B.length ? [A, B] : [B, A];
|
|
275
|
+
return shorter.every((chunk, i) => {
|
|
276
|
+
return chunk === '*' || longer[i] === '*' || chunk === longer[i];
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
toArray(value) {
|
|
280
|
+
return [...(Array.isArray(value) ? value : [value])];
|
|
281
|
+
}
|
|
282
|
+
static normalizePermission(permission) {
|
|
283
|
+
return permission
|
|
284
|
+
.trim()
|
|
285
|
+
.replace(/^permission\./, '') // remove prefix
|
|
286
|
+
.replace(/\.+/g, '.') // collapse multiple dots
|
|
287
|
+
.toLowerCase(); // optional: make case-insensitive
|
|
288
|
+
}
|
|
289
|
+
static matchPermissions(policySegments, inputSegments) {
|
|
290
|
+
const maxLen = Math.max(policySegments.length, inputSegments.length);
|
|
291
|
+
for (let i = 0; i < maxLen; i++) {
|
|
292
|
+
const pSeg = policySegments[i];
|
|
293
|
+
const iSeg = inputSegments[i];
|
|
294
|
+
if (pSeg === undefined) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
if (pSeg === '*') {
|
|
298
|
+
continue; // '*'
|
|
299
|
+
}
|
|
300
|
+
if (iSeg === undefined) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
if (pSeg !== iSeg) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
129
311
|
class AbilityTypeGenerator {
|
|
130
312
|
policies;
|
|
313
|
+
policyEntries;
|
|
131
314
|
constructor(policies) {
|
|
132
315
|
this.policies = policies;
|
|
316
|
+
this.policyEntries = policies.map(policy => ({
|
|
317
|
+
policy,
|
|
318
|
+
normalizedPermission: AbilityResolver.normalizePermission(policy.permission),
|
|
319
|
+
segments: AbilityResolver.normalizePermission(policy.permission).split('.'),
|
|
320
|
+
}));
|
|
133
321
|
}
|
|
134
322
|
/**
|
|
135
323
|
* Generates TypeScript type definitions based on the provided policies.
|
|
@@ -277,21 +465,6 @@ class AbilityTypeGenerator {
|
|
|
277
465
|
* @returns TypeScript array type as string
|
|
278
466
|
*/
|
|
279
467
|
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
468
|
const elementType = this.getInArrayType(resource);
|
|
296
469
|
return `readonly ${elementType}[]`;
|
|
297
470
|
}
|
|
@@ -387,18 +560,43 @@ class AbilityTypeGenerator {
|
|
|
387
560
|
output += '// Do not edit manually\n';
|
|
388
561
|
output += 'export type Resources = {\n';
|
|
389
562
|
const sortedActions = Object.keys(structure).sort();
|
|
390
|
-
sortedActions.forEach(
|
|
391
|
-
const actionObj = structure[
|
|
563
|
+
sortedActions.forEach(permission => {
|
|
564
|
+
const actionObj = structure[permission];
|
|
392
565
|
const isEmpty = Object.keys(actionObj).length === 0;
|
|
566
|
+
const inputNormalized = AbilityResolver.normalizePermission(permission);
|
|
567
|
+
const inputSegments = inputNormalized.split('.');
|
|
568
|
+
const filteredPolicies = this.policyEntries
|
|
569
|
+
.filter(entry => AbilityResolver.matchPermissions(entry.segments, inputSegments))
|
|
570
|
+
.map(entry => entry.policy);
|
|
571
|
+
// Effects
|
|
572
|
+
const effects = [...new Set(filteredPolicies.map(p => p.effect))].sort();
|
|
573
|
+
// Policies list
|
|
574
|
+
const items = filteredPolicies
|
|
575
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
576
|
+
.map(p => {
|
|
577
|
+
const effect = p.effect.padEnd(6, ' '); // permit / deny / audit
|
|
578
|
+
const displayName = p.name === p.id ? 'Unnamed policy' : p.name;
|
|
579
|
+
return ` * - ${effect} ${p.id} "${displayName}"`;
|
|
580
|
+
})
|
|
581
|
+
.join('\n');
|
|
582
|
+
//
|
|
583
|
+
output += `
|
|
584
|
+
/**
|
|
585
|
+
* Permission: ${permission}
|
|
586
|
+
* Effects: ${effects.join(', ')}
|
|
587
|
+
* Policies:
|
|
588
|
+
${items}
|
|
589
|
+
*/
|
|
590
|
+
`;
|
|
393
591
|
if (isEmpty) {
|
|
394
|
-
//
|
|
395
|
-
output += ` ['${
|
|
592
|
+
// empty object → undefined
|
|
593
|
+
output += ` ['${permission}']: undefined;\n`;
|
|
396
594
|
}
|
|
397
595
|
else {
|
|
398
|
-
//
|
|
399
|
-
output += ` ['${
|
|
596
|
+
// not empty object
|
|
597
|
+
output += ` ['${permission}']: {\n`;
|
|
400
598
|
output += this.formatNestedObject(actionObj, 4);
|
|
401
|
-
output += ' };\n';
|
|
599
|
+
output += ' } | null | undefined;\n';
|
|
402
600
|
}
|
|
403
601
|
});
|
|
404
602
|
output += '}\n';
|
|
@@ -412,15 +610,37 @@ class AbilityTypeGenerator {
|
|
|
412
610
|
output += `\n\nexport type PolicyTags = ${tagsUnion};\n`;
|
|
413
611
|
// environments
|
|
414
612
|
output += '\n\nexport type Environment = {\n';
|
|
415
|
-
Object.entries(environment).forEach(([
|
|
613
|
+
Object.entries(environment).forEach(([permission, envObj]) => {
|
|
416
614
|
const isEmpty = Object.keys(envObj).length === 0;
|
|
615
|
+
const inputNormalized = AbilityResolver.normalizePermission(permission);
|
|
616
|
+
const inputSegments = inputNormalized.split('.');
|
|
617
|
+
const filteredPolicies = this.policyEntries
|
|
618
|
+
.filter(entry => AbilityResolver.matchPermissions(entry.segments, inputSegments))
|
|
619
|
+
.map(entry => entry.policy);
|
|
620
|
+
const effects = [...new Set(filteredPolicies.map(p => p.effect))].sort();
|
|
621
|
+
const items = filteredPolicies
|
|
622
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
623
|
+
.map(p => {
|
|
624
|
+
const effect = p.effect.padEnd(6, ' ');
|
|
625
|
+
const displayName = p.name === p.id ? 'Unnamed policy' : p.name;
|
|
626
|
+
return ` * - ${effect} ${p.id} "${displayName}"`;
|
|
627
|
+
})
|
|
628
|
+
.join('\n');
|
|
629
|
+
output += `
|
|
630
|
+
/**
|
|
631
|
+
* Permission: ${permission}
|
|
632
|
+
* Effects: ${effects.join(', ')}
|
|
633
|
+
* Policies:
|
|
634
|
+
${items}
|
|
635
|
+
*/
|
|
636
|
+
`;
|
|
417
637
|
if (isEmpty) {
|
|
418
|
-
output += ` ['${
|
|
638
|
+
output += ` ['${permission}']: undefined;\n`;
|
|
419
639
|
}
|
|
420
640
|
else {
|
|
421
|
-
output += ` ['${
|
|
641
|
+
output += ` ['${permission}']: {\n`;
|
|
422
642
|
output += this.formatNestedObject(envObj, 4);
|
|
423
|
-
output += ' };\n';
|
|
643
|
+
output += ' } | null | undefined;\n';
|
|
424
644
|
}
|
|
425
645
|
});
|
|
426
646
|
output += '}\n';
|
|
@@ -444,69 +664,126 @@ class AbilityTypeGenerator {
|
|
|
444
664
|
// Nested object
|
|
445
665
|
output += `${spaces}readonly ${key}: {\n`;
|
|
446
666
|
output += this.formatNestedObject(value, indent + 2);
|
|
447
|
-
output += `${spaces}};\n`;
|
|
667
|
+
output += `${spaces}} | null | undefined;\n`;
|
|
448
668
|
}
|
|
449
669
|
else {
|
|
450
670
|
// Primitive type
|
|
451
|
-
|
|
671
|
+
const va = [String(value)];
|
|
672
|
+
let v = String(value);
|
|
673
|
+
if (!v.match(/unknown/)) {
|
|
674
|
+
if (!v.match(/null/)) {
|
|
675
|
+
va.push('null');
|
|
676
|
+
}
|
|
677
|
+
if (!v.match(/undefined/)) {
|
|
678
|
+
va.push('undefined');
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
output += `${spaces}readonly ${key}: ${va.join(' | ')} \n`;
|
|
452
682
|
}
|
|
453
683
|
});
|
|
454
684
|
return output;
|
|
455
685
|
}
|
|
456
686
|
}
|
|
457
687
|
|
|
458
|
-
class
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
688
|
+
class AbilityHash {
|
|
689
|
+
static sha1(message) {
|
|
690
|
+
const msgBytes = AbilityHash.stringToBytes(message);
|
|
691
|
+
const msgBitLength = msgBytes.length * 8;
|
|
692
|
+
const withOne = new Uint8Array(msgBytes.length + 1);
|
|
693
|
+
withOne.set(msgBytes, 0);
|
|
694
|
+
withOne[msgBytes.length] = 0x80;
|
|
695
|
+
let zeroBytes = (56 - (withOne.length % 64) + 64) % 64;
|
|
696
|
+
const padded = new Uint8Array(withOne.length + zeroBytes + 8);
|
|
697
|
+
padded.set(withOne, 0);
|
|
698
|
+
const bitLenHigh = Math.floor(msgBitLength / 0x100000000);
|
|
699
|
+
const bitLenLow = msgBitLength >>> 0;
|
|
700
|
+
padded[padded.length - 8] = (bitLenHigh >>> 24) & 0xff;
|
|
701
|
+
padded[padded.length - 7] = (bitLenHigh >>> 16) & 0xff;
|
|
702
|
+
padded[padded.length - 6] = (bitLenHigh >>> 8) & 0xff;
|
|
703
|
+
padded[padded.length - 5] = bitLenHigh & 0xff;
|
|
704
|
+
padded[padded.length - 4] = (bitLenLow >>> 24) & 0xff;
|
|
705
|
+
padded[padded.length - 3] = (bitLenLow >>> 16) & 0xff;
|
|
706
|
+
padded[padded.length - 2] = (bitLenLow >>> 8) & 0xff;
|
|
707
|
+
padded[padded.length - 1] = bitLenLow & 0xff;
|
|
708
|
+
let h0 = 0x67452301;
|
|
709
|
+
let h1 = 0xefcdab89;
|
|
710
|
+
let h2 = 0x98badcfe;
|
|
711
|
+
let h3 = 0x10325476;
|
|
712
|
+
let h4 = 0xc3d2e1f0;
|
|
713
|
+
const w = new Array(80);
|
|
714
|
+
for (let i = 0; i < padded.length; i += 64) {
|
|
715
|
+
for (let j = 0; j < 16; j++) {
|
|
716
|
+
const idx = i + j * 4;
|
|
717
|
+
w[j] =
|
|
718
|
+
(padded[idx] << 24) | (padded[idx + 1] << 16) | (padded[idx + 2] << 8) | padded[idx + 3];
|
|
719
|
+
}
|
|
720
|
+
for (let j = 16; j < 80; j++) {
|
|
721
|
+
w[j] = AbilityHash.leftRotate(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
|
|
722
|
+
}
|
|
723
|
+
let a = h0;
|
|
724
|
+
let b = h1;
|
|
725
|
+
let c = h2;
|
|
726
|
+
let d = h3;
|
|
727
|
+
let e = h4;
|
|
728
|
+
for (let j = 0; j < 80; j++) {
|
|
729
|
+
let f;
|
|
730
|
+
let k;
|
|
731
|
+
if (j < 20) {
|
|
732
|
+
f = (b & c) | (~b & d);
|
|
733
|
+
k = 0x5a827999;
|
|
734
|
+
}
|
|
735
|
+
else {
|
|
736
|
+
if (j < 40) {
|
|
737
|
+
f = b ^ c ^ d;
|
|
738
|
+
k = 0x6ed9eba1;
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
if (j < 60) {
|
|
742
|
+
f = (b & c) | (b & d) | (c & d);
|
|
743
|
+
k = 0x8f1bbcdc;
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
f = b ^ c ^ d;
|
|
747
|
+
k = 0xca62c1d6;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const temp = (AbilityHash.leftRotate(a, 5) + f + e + k + (w[j] | 0)) | 0;
|
|
752
|
+
e = d;
|
|
753
|
+
d = c;
|
|
754
|
+
c = AbilityHash.leftRotate(b, 30);
|
|
755
|
+
b = a;
|
|
756
|
+
a = temp;
|
|
757
|
+
}
|
|
758
|
+
h0 = (h0 + a) | 0;
|
|
759
|
+
h1 = (h1 + b) | 0;
|
|
760
|
+
h2 = (h2 + c) | 0;
|
|
761
|
+
h3 = (h3 + d) | 0;
|
|
762
|
+
h4 = (h4 + e) | 0;
|
|
763
|
+
}
|
|
764
|
+
return [
|
|
765
|
+
AbilityHash.toHex32(h0),
|
|
766
|
+
AbilityHash.toHex32(h1),
|
|
767
|
+
AbilityHash.toHex32(h2),
|
|
768
|
+
AbilityHash.toHex32(h3),
|
|
769
|
+
AbilityHash.toHex32(h4),
|
|
770
|
+
].join('');
|
|
771
|
+
}
|
|
772
|
+
static leftRotate(value, bits) {
|
|
773
|
+
return ((value << bits) | (value >>> (32 - bits))) >>> 0;
|
|
774
|
+
}
|
|
775
|
+
static toHex32(num) {
|
|
776
|
+
return (num >>> 0).toString(16).padStart(8, '0');
|
|
777
|
+
}
|
|
778
|
+
static stringToBytes(str) {
|
|
779
|
+
if (typeof TextEncoder !== 'undefined') {
|
|
780
|
+
const encoder = new TextEncoder();
|
|
781
|
+
return encoder.encode(str);
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
const buf = Buffer.from(str, 'utf8');
|
|
785
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
475
786
|
}
|
|
476
|
-
out += `${pad}${mark} ${this.type} «${this.name}» is ${this.match}`;
|
|
477
|
-
this.children.forEach(child => {
|
|
478
|
-
out += '\n' + child.toString(indent + 1);
|
|
479
|
-
});
|
|
480
|
-
return out;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
class AbilityExplainRule extends AbilityExplain {
|
|
484
|
-
constructor(rule) {
|
|
485
|
-
super({
|
|
486
|
-
type: 'rule',
|
|
487
|
-
match: rule.state,
|
|
488
|
-
name: rule.name,
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
class AbilityExplainRuleSet extends AbilityExplain {
|
|
493
|
-
constructor(ruleSet) {
|
|
494
|
-
const children = ruleSet.rules.map(rule => new AbilityExplainRule(rule));
|
|
495
|
-
super({
|
|
496
|
-
type: 'ruleSet',
|
|
497
|
-
match: ruleSet.state,
|
|
498
|
-
name: ruleSet.name,
|
|
499
|
-
}, children);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
class AbilityExplainPolicy extends AbilityExplain {
|
|
503
|
-
constructor(policy) {
|
|
504
|
-
const children = policy.ruleSet.map(ruleSet => new AbilityExplainRuleSet(ruleSet));
|
|
505
|
-
super({
|
|
506
|
-
type: 'policy',
|
|
507
|
-
name: policy.priority > -1 ? `@priority ${policy.priority} ${policy.name}` : policy.name,
|
|
508
|
-
match: policy.matchState,
|
|
509
|
-
}, children);
|
|
510
787
|
}
|
|
511
788
|
}
|
|
512
789
|
|
|
@@ -527,15 +804,15 @@ class AbilityPolicy {
|
|
|
527
804
|
* one of the rules returns as «permit»
|
|
528
805
|
*/
|
|
529
806
|
compareMethod = AbilityCompare.and;
|
|
807
|
+
/**
|
|
808
|
+
* Policy ID
|
|
809
|
+
*/
|
|
810
|
+
id;
|
|
530
811
|
/**
|
|
531
812
|
* Policy name
|
|
532
813
|
*/
|
|
533
814
|
name;
|
|
534
815
|
description;
|
|
535
|
-
/**
|
|
536
|
-
* Policy ID
|
|
537
|
-
*/
|
|
538
|
-
id;
|
|
539
816
|
/**
|
|
540
817
|
* Running the `enforce` or `resolve` method
|
|
541
818
|
* will select only those from all passed policies that fall under the specified permission key.
|
|
@@ -546,8 +823,6 @@ class AbilityPolicy {
|
|
|
546
823
|
tags;
|
|
547
824
|
constructor(params) {
|
|
548
825
|
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;
|
|
551
826
|
this.permission = permission;
|
|
552
827
|
this.description = description;
|
|
553
828
|
this.effect = effect;
|
|
@@ -555,6 +830,8 @@ class AbilityPolicy {
|
|
|
555
830
|
this.priority = typeof priority === 'number' ? priority : -1;
|
|
556
831
|
this.disabled = typeof disabled === 'boolean' ? disabled : false;
|
|
557
832
|
this.tags = (tags || []);
|
|
833
|
+
this.id = id || `p_${this.hash().slice(0, 10)}`;
|
|
834
|
+
this.name = name || this.id;
|
|
558
835
|
}
|
|
559
836
|
/**
|
|
560
837
|
* Add rule set to the policy
|
|
@@ -668,6 +945,24 @@ class AbilityPolicy {
|
|
|
668
945
|
}
|
|
669
946
|
return policy;
|
|
670
947
|
}
|
|
948
|
+
hash() {
|
|
949
|
+
const parts = [
|
|
950
|
+
`permission:${this.permission}`,
|
|
951
|
+
`effect:${this.effect}`,
|
|
952
|
+
`compareMethod:${this.compareMethod}`,
|
|
953
|
+
`priority:${this.priority}`,
|
|
954
|
+
`disabled:${this.disabled}`,
|
|
955
|
+
];
|
|
956
|
+
if (this.tags && this.tags.length > 0) {
|
|
957
|
+
parts.push(`tags:${[...this.tags].sort().join(',')}`);
|
|
958
|
+
}
|
|
959
|
+
if (this.ruleSet && this.ruleSet.length > 0) {
|
|
960
|
+
const ruleHashes = this.ruleSet.map(r => r.hash());
|
|
961
|
+
parts.push(`rules:${ruleHashes.sort().join('|')}`);
|
|
962
|
+
}
|
|
963
|
+
const str = parts.join(';');
|
|
964
|
+
return AbilityHash.sha1(str);
|
|
965
|
+
}
|
|
671
966
|
}
|
|
672
967
|
|
|
673
968
|
function brand$1(code) {
|
|
@@ -678,127 +973,6 @@ const AbilityPolicyEffect = {
|
|
|
678
973
|
permit: brand$1('permit'),
|
|
679
974
|
};
|
|
680
975
|
|
|
681
|
-
class AbilityResult {
|
|
682
|
-
effect;
|
|
683
|
-
strategy;
|
|
684
|
-
constructor(effect, strategy) {
|
|
685
|
-
this.effect = effect;
|
|
686
|
-
this.strategy = strategy;
|
|
687
|
-
}
|
|
688
|
-
/**
|
|
689
|
-
* Returns a list of explanations for each policy involved in the ability evaluation.
|
|
690
|
-
* Each item describes how a specific policy contributed to the final permission result.
|
|
691
|
-
*
|
|
692
|
-
* Useful for debugging, logging, or building UI tools that visualize permission logic.
|
|
693
|
-
*/
|
|
694
|
-
explain() {
|
|
695
|
-
return this.strategy.policies.map(policy => {
|
|
696
|
-
return new AbilityExplainPolicy(policy);
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
isAllowed() {
|
|
700
|
-
return this.strategy.isAllowed();
|
|
701
|
-
}
|
|
702
|
-
isDenied() {
|
|
703
|
-
return this.strategy.isDenied();
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
class AbilityResolver {
|
|
708
|
-
StrategyClass;
|
|
709
|
-
policyEntries;
|
|
710
|
-
constructor(
|
|
711
|
-
/**
|
|
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.
|
|
713
|
-
*/
|
|
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;
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Resolve policy for the resource and permission key
|
|
729
|
-
*
|
|
730
|
-
* @param permission - Permission key
|
|
731
|
-
* @param resource - Resource
|
|
732
|
-
* @param environment
|
|
733
|
-
*/
|
|
734
|
-
resolve(permission, resource, environment) {
|
|
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
|
|
741
|
-
for (const policy of filteredPolicies) {
|
|
742
|
-
if (policy.disabled) {
|
|
743
|
-
continue;
|
|
744
|
-
}
|
|
745
|
-
const policyMatchState = policy.check(resource, environment);
|
|
746
|
-
if (policyMatchState === AbilityMatch.pending) {
|
|
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.`);
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
// 3. Use strategy
|
|
751
|
-
const strategy = new this.StrategyClass(filteredPolicies);
|
|
752
|
-
const effect = strategy.evaluate();
|
|
753
|
-
return new AbilityResult(effect, strategy);
|
|
754
|
-
}
|
|
755
|
-
enforce(permission, resource, environment) {
|
|
756
|
-
const result = this.resolve(permission, resource, environment);
|
|
757
|
-
if (result.isDenied()) {
|
|
758
|
-
throw new AbilityError(`Permission denied`);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
/**
|
|
762
|
-
* Check if the permission key is contained in another permission key
|
|
763
|
-
* @param permissionA - The first permission to check
|
|
764
|
-
* @param permissionB - The second permission to check
|
|
765
|
-
*/
|
|
766
|
-
static isInPermissionContain(permissionA, permissionB) {
|
|
767
|
-
const A = permissionA.split('.');
|
|
768
|
-
const B = permissionB.split('.');
|
|
769
|
-
const [longer, shorter] = A.length >= B.length ? [A, B] : [B, A];
|
|
770
|
-
return shorter.every((chunk, i) => {
|
|
771
|
-
return chunk === '*' || longer[i] === '*' || chunk === longer[i];
|
|
772
|
-
});
|
|
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
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
976
|
/**
|
|
803
977
|
* Represents a rule that defines a condition to be checked against a subject and resource.
|
|
804
978
|
*/
|
|
@@ -829,14 +1003,14 @@ class AbilityRule {
|
|
|
829
1003
|
*/
|
|
830
1004
|
constructor(params) {
|
|
831
1005
|
const { id, name, subject, resource, condition, disabled, description } = params;
|
|
832
|
-
this.name = name || `rule:${JSON.stringify(subject)}:${condition}:${JSON.stringify(resource)}`;
|
|
833
|
-
this.id = id || this.name;
|
|
834
1006
|
this.description = description;
|
|
835
1007
|
this.disabled = typeof disabled === 'boolean' ? disabled : false;
|
|
836
1008
|
this.subject = subject;
|
|
837
1009
|
this.resource = resource;
|
|
838
1010
|
this.condition = condition;
|
|
839
1011
|
this.state = this.disabled ? AbilityMatch.disabled : this.state;
|
|
1012
|
+
this.id = id || `r_${this.hash().slice(0, 10)}`;
|
|
1013
|
+
this.name = name || this.id;
|
|
840
1014
|
}
|
|
841
1015
|
static isPrimitive(v) {
|
|
842
1016
|
return typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null;
|
|
@@ -1063,6 +1237,14 @@ class AbilityRule {
|
|
|
1063
1237
|
condition: props.condition ?? this.condition,
|
|
1064
1238
|
});
|
|
1065
1239
|
}
|
|
1240
|
+
hash() {
|
|
1241
|
+
const parts = [];
|
|
1242
|
+
parts.push(`subject:${this.subject}`);
|
|
1243
|
+
parts.push(`resource:${JSON.stringify(this.resource)}`);
|
|
1244
|
+
parts.push(`condition:${this.condition}`);
|
|
1245
|
+
parts.push(`disabled:${this.disabled}`);
|
|
1246
|
+
return AbilityHash.sha1(parts.join(';'));
|
|
1247
|
+
}
|
|
1066
1248
|
static equals(subject, resource) {
|
|
1067
1249
|
return new AbilityRule({
|
|
1068
1250
|
condition: AbilityCondition.equals,
|
|
@@ -1168,13 +1350,13 @@ class AbilityRuleSet {
|
|
|
1168
1350
|
disabled;
|
|
1169
1351
|
constructor(params) {
|
|
1170
1352
|
const { name, id, compareMethod, isExcept, disabled, description } = params;
|
|
1171
|
-
this.name = name || `ruleset:${compareMethod}`;
|
|
1172
|
-
this.id = id || this.name;
|
|
1173
1353
|
this.description = description;
|
|
1174
1354
|
this.compareMethod = compareMethod;
|
|
1175
1355
|
this.isExcept = isExcept;
|
|
1176
1356
|
this.disabled = typeof disabled === 'boolean' ? disabled : false;
|
|
1177
1357
|
this.state = this.disabled ? AbilityMatch.disabled : this.state;
|
|
1358
|
+
this.id = id || `g_${this.hash().slice(0, 10)}`;
|
|
1359
|
+
this.name = name || this.id;
|
|
1178
1360
|
}
|
|
1179
1361
|
addRule(rule) {
|
|
1180
1362
|
this.rules.push(rule);
|
|
@@ -1236,6 +1418,16 @@ class AbilityRuleSet {
|
|
|
1236
1418
|
}
|
|
1237
1419
|
return next;
|
|
1238
1420
|
}
|
|
1421
|
+
hash() {
|
|
1422
|
+
const ruleHashes = this.rules.map(r => r.hash()).sort();
|
|
1423
|
+
const parts = [
|
|
1424
|
+
`compareMethod:${this.compareMethod}`,
|
|
1425
|
+
`isExcept:${this.isExcept}`,
|
|
1426
|
+
`disabled:${this.disabled}`,
|
|
1427
|
+
`rules:${ruleHashes.join('|')}`,
|
|
1428
|
+
];
|
|
1429
|
+
return AbilityHash.sha1(parts.join(';'));
|
|
1430
|
+
}
|
|
1239
1431
|
static and(rules) {
|
|
1240
1432
|
return new AbilityRuleSet({
|
|
1241
1433
|
compareMethod: AbilityCompare.and,
|
|
@@ -1508,11 +1700,49 @@ class AbilityDSLLexer {
|
|
|
1508
1700
|
readAnnotation() {
|
|
1509
1701
|
const startLine = this.line;
|
|
1510
1702
|
const startColumn = this.column;
|
|
1511
|
-
let
|
|
1703
|
+
let raw = '';
|
|
1704
|
+
// читаем всю строку после @
|
|
1512
1705
|
while (!this.isAtEnd() && !this.isNewline()) {
|
|
1513
|
-
|
|
1706
|
+
raw += this.advance();
|
|
1707
|
+
}
|
|
1708
|
+
raw = raw.trim();
|
|
1709
|
+
// parse literals
|
|
1710
|
+
let result = '';
|
|
1711
|
+
let i = 0;
|
|
1712
|
+
let inString = false;
|
|
1713
|
+
let quote = null;
|
|
1714
|
+
let escaped = false;
|
|
1715
|
+
while (i < raw.length) {
|
|
1716
|
+
const ch = raw[i];
|
|
1717
|
+
if (inString) {
|
|
1718
|
+
if (escaped) {
|
|
1719
|
+
result += ch;
|
|
1720
|
+
escaped = false;
|
|
1721
|
+
}
|
|
1722
|
+
else if (ch === '\\') {
|
|
1723
|
+
escaped = true;
|
|
1724
|
+
}
|
|
1725
|
+
else if (ch === quote) {
|
|
1726
|
+
inString = false;
|
|
1727
|
+
quote = null;
|
|
1728
|
+
}
|
|
1729
|
+
else {
|
|
1730
|
+
result += ch;
|
|
1731
|
+
}
|
|
1732
|
+
i++;
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
// start of string
|
|
1736
|
+
if (ch === '"' || ch === "'") {
|
|
1737
|
+
inString = true;
|
|
1738
|
+
quote = ch;
|
|
1739
|
+
i++;
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
result += ch;
|
|
1743
|
+
i++;
|
|
1514
1744
|
}
|
|
1515
|
-
return new AbilityDSLToken(TokenTypes.ANNOTATION,
|
|
1745
|
+
return new AbilityDSLToken(TokenTypes.ANNOTATION, result.trim(), startLine, startColumn);
|
|
1516
1746
|
}
|
|
1517
1747
|
readString() {
|
|
1518
1748
|
const startLine = this.line;
|