@via-profit/ability 3.6.0 → 3.6.2
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 +24 -14
- package/dist/index.js +492 -229
- 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
|
}
|
|
@@ -216,14 +218,16 @@ type AbilityExplainConfig = {
|
|
|
216
218
|
readonly type: AbilityExplainType;
|
|
217
219
|
readonly name: string;
|
|
218
220
|
readonly match: AbilityMatchType;
|
|
221
|
+
readonly debugInfo?: boolean;
|
|
219
222
|
};
|
|
220
223
|
declare class AbilityExplain {
|
|
221
224
|
readonly type: AbilityExplainType;
|
|
222
225
|
readonly children: AbilityExplain[];
|
|
223
226
|
readonly name: string;
|
|
224
227
|
readonly match: AbilityMatchType;
|
|
228
|
+
readonly debugInfo?: boolean;
|
|
225
229
|
constructor(config: AbilityExplainConfig, children?: AbilityExplain[]);
|
|
226
|
-
toString(
|
|
230
|
+
toString(indentPrefix?: string, isLast?: boolean): string;
|
|
227
231
|
}
|
|
228
232
|
declare class AbilityExplainRule extends AbilityExplain {
|
|
229
233
|
constructor(rule: AbilityRule);
|
|
@@ -276,15 +280,15 @@ declare class AbilityPolicy<R extends ResourceObject = Record<string, unknown>,
|
|
|
276
280
|
* one of the rules returns as «permit»
|
|
277
281
|
*/
|
|
278
282
|
compareMethod: AbilityCompareType;
|
|
283
|
+
/**
|
|
284
|
+
* Policy ID
|
|
285
|
+
*/
|
|
286
|
+
id: string;
|
|
279
287
|
/**
|
|
280
288
|
* Policy name
|
|
281
289
|
*/
|
|
282
290
|
name: string;
|
|
283
291
|
description?: string | null;
|
|
284
|
-
/**
|
|
285
|
-
* Policy ID
|
|
286
|
-
*/
|
|
287
|
-
id: string;
|
|
288
292
|
/**
|
|
289
293
|
* Running the `enforce` or `resolve` method
|
|
290
294
|
* will select only those from all passed policies that fall under the specified permission key.
|
|
@@ -325,6 +329,7 @@ declare class AbilityPolicy<R extends ResourceObject = Record<string, unknown>,
|
|
|
325
329
|
compareMethod: AbilityCompareType;
|
|
326
330
|
ruleSet: AbilityRuleSet<R, E>[];
|
|
327
331
|
}>): AbilityPolicy<R, E>;
|
|
332
|
+
hash(): string;
|
|
328
333
|
}
|
|
329
334
|
|
|
330
335
|
type Primitive = string | number | boolean | null | undefined;
|
|
@@ -336,6 +341,7 @@ type EnvironmentObject = Record<string, unknown>;
|
|
|
336
341
|
type ResourcesMap = Record<string, ResourceObject>;
|
|
337
342
|
declare class AbilityTypeGenerator {
|
|
338
343
|
readonly policies: readonly AbilityPolicy[];
|
|
344
|
+
private readonly policyEntries;
|
|
339
345
|
constructor(policies: readonly AbilityPolicy[]);
|
|
340
346
|
/**
|
|
341
347
|
* Generates TypeScript type definitions based on the provided policies.
|
|
@@ -411,8 +417,8 @@ declare class AbilityResult<R extends ResourceObject = Record<string, unknown>,
|
|
|
411
417
|
* Useful for debugging, logging, or building UI tools that visualize permission logic.
|
|
412
418
|
*/
|
|
413
419
|
explain(): readonly AbilityExplain[];
|
|
414
|
-
isAllowed()
|
|
415
|
-
isDenied()
|
|
420
|
+
isAllowed: () => boolean;
|
|
421
|
+
isDenied: () => boolean;
|
|
416
422
|
}
|
|
417
423
|
|
|
418
424
|
interface AbilityResolverOptions<TTags extends string> {
|
|
@@ -420,6 +426,7 @@ interface AbilityResolverOptions<TTags extends string> {
|
|
|
420
426
|
}
|
|
421
427
|
type ExtractResources<P> = P extends AbilityPolicy<infer R, any, any> ? R : never;
|
|
422
428
|
type ExtractEnvironment<P> = P extends AbilityPolicy<any, infer E, any> ? E : never;
|
|
429
|
+
type ExtractPermission<R> = R extends AbilityResolver<infer P, any, any> ? keyof ExtractResources<P> & string : never;
|
|
423
430
|
type ExtractResourceByPermission<P, Perm extends string> = P extends AbilityPolicy<infer R, any, any> ? (Perm extends keyof R ? R[Perm] : never) : never;
|
|
424
431
|
type ExtractEnvironmentByPermission<P, Perm extends string> = P extends AbilityPolicy<any, infer E, any> ? (Perm extends keyof E ? E[Perm] : never) : never;
|
|
425
432
|
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 +447,16 @@ declare class AbilityResolver<P extends AbilityPolicy<any, any, any>, S extends
|
|
|
440
447
|
resolve<Permission extends keyof ExtractResources<P> & string>(permission: Permission, resource: ExtractResourceByPermission<P, Permission>, environment?: ExtractEnvironmentByPermission<P, Permission>): AbilityResult<ExtractResourceByPermission<P, Permission>, ExtractEnvironment<P>>;
|
|
441
448
|
enforce<Permission extends keyof ExtractResources<P> & string>(permission: Permission, resource: ExtractResourceByPermission<P, Permission>, environment?: ExtractEnvironmentByPermission<P, Permission>): void | never;
|
|
442
449
|
/**
|
|
450
|
+
* @deprecated - will be removed
|
|
451
|
+
*
|
|
443
452
|
* Check if the permission key is contained in another permission key
|
|
444
453
|
* @param permissionA - The first permission to check
|
|
445
454
|
* @param permissionB - The second permission to check
|
|
446
455
|
*/
|
|
447
456
|
static isInPermissionContain(permissionA: string, permissionB: string): boolean;
|
|
448
457
|
private toArray;
|
|
449
|
-
|
|
450
|
-
|
|
458
|
+
static normalizePermission(permission: string): string;
|
|
459
|
+
static matchPermissions(policySegments: string[], inputSegments: string[]): boolean;
|
|
451
460
|
}
|
|
452
461
|
|
|
453
462
|
declare class AbilityJSONParser {
|
|
@@ -456,13 +465,13 @@ declare class AbilityJSONParser {
|
|
|
456
465
|
* @param configs - Array of policy configurations
|
|
457
466
|
* @returns Array of AbilityPolicy instances
|
|
458
467
|
*/
|
|
459
|
-
static parse<
|
|
460
|
-
static parsePolicy<
|
|
461
|
-
static parseRule<
|
|
468
|
+
static parse<R extends ResourceObject, E extends EnvironmentObject, T extends string = string>(configs: readonly AbilityPolicyConfig[]): AbilityPolicy<R, E, T>[];
|
|
469
|
+
static parsePolicy<R extends ResourceObject, E extends EnvironmentObject, T extends string = string>(config: AbilityPolicyConfig): AbilityPolicy<R, E, T>;
|
|
470
|
+
static parseRule<R extends ResourceObject, E extends EnvironmentObject>(config: AbilityRuleConfig): AbilityRule<R, E>;
|
|
462
471
|
/**
|
|
463
472
|
* Parse the config JSON format to Group class instance
|
|
464
473
|
*/
|
|
465
|
-
static parseRuleSet<
|
|
474
|
+
static parseRuleSet<R extends ResourceObject, E extends EnvironmentObject>(config: AbilityRuleSetConfig): AbilityRuleSet<R, E>;
|
|
466
475
|
static ruleToJSON(rule: AbilityRule): AbilityRuleConfig;
|
|
467
476
|
static ruleSetToJSON(ruleSet: AbilityRuleSet): AbilityRuleSetConfig;
|
|
468
477
|
static policyToJSON(policy: AbilityPolicy): AbilityPolicyConfig;
|
|
@@ -544,6 +553,7 @@ declare class AbilityDSLParser<R extends ResourceObject = Record<string, unknown
|
|
|
544
553
|
private takeAnnotations;
|
|
545
554
|
private isStartOfPolicy;
|
|
546
555
|
private isStartOfGroup;
|
|
556
|
+
private isStartOfRule;
|
|
547
557
|
private isStartOfExcept;
|
|
548
558
|
private isStartOfAlias;
|
|
549
559
|
}
|
|
@@ -800,4 +810,4 @@ declare class PriorityStrategy<R extends ResourceObject, E extends EnvironmentOb
|
|
|
800
810
|
}
|
|
801
811
|
|
|
802
812
|
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 };
|
|
813
|
+
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,220 @@ const AbilityMatch = {
|
|
|
126
126
|
disabled: brand$2('disabled'),
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
+
const colors = {
|
|
130
|
+
reset: '\x1b[0m',
|
|
131
|
+
green: '\x1b[32m',
|
|
132
|
+
red: '\x1b[31m',
|
|
133
|
+
blue: '\x1b[34m',
|
|
134
|
+
yellow: '\x1b[33m',
|
|
135
|
+
white: '\x1b[37m',
|
|
136
|
+
gray: '\x1b[90m',
|
|
137
|
+
};
|
|
138
|
+
class AbilityExplain {
|
|
139
|
+
type;
|
|
140
|
+
children;
|
|
141
|
+
name;
|
|
142
|
+
match;
|
|
143
|
+
debugInfo;
|
|
144
|
+
constructor(config, children = []) {
|
|
145
|
+
this.type = config.type;
|
|
146
|
+
this.children = children;
|
|
147
|
+
this.name = config.name;
|
|
148
|
+
this.match = config.match;
|
|
149
|
+
this.debugInfo = config.debugInfo;
|
|
150
|
+
}
|
|
151
|
+
toString(indentPrefix = '', isLast = true) {
|
|
152
|
+
const isMatch = this.match === AbilityMatch.match;
|
|
153
|
+
const mark = isMatch ? `${colors.green}✓${colors.reset}` : `${colors.red}✗${colors.reset}`;
|
|
154
|
+
const label = this.type === 'policy'
|
|
155
|
+
? `${colors.blue}POLICY${colors.reset}`
|
|
156
|
+
: this.type === 'ruleSet'
|
|
157
|
+
? `${colors.yellow}RULESET${colors.reset}`
|
|
158
|
+
: `${colors.white}RULE${colors.reset}`;
|
|
159
|
+
const branch = indentPrefix.length === 0
|
|
160
|
+
? ''
|
|
161
|
+
: isLast
|
|
162
|
+
? `${colors.gray}└─${colors.reset} `
|
|
163
|
+
: `${colors.gray}├─${colors.reset} `;
|
|
164
|
+
let out = `${indentPrefix}${branch}${label} ${this.name} — ${mark}`;
|
|
165
|
+
if (this.debugInfo) {
|
|
166
|
+
out += ` ${colors.gray}(${this.debugInfo})${colors.reset}`;
|
|
167
|
+
}
|
|
168
|
+
const nextIndent = indentPrefix + (isLast ? ' ' : `${colors.gray}│ ${colors.reset}`);
|
|
169
|
+
this.children.forEach((child, index) => {
|
|
170
|
+
const last = index === this.children.length - 1;
|
|
171
|
+
out += '\n' + child.toString(nextIndent, last);
|
|
172
|
+
});
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
class AbilityExplainRule extends AbilityExplain {
|
|
177
|
+
constructor(rule) {
|
|
178
|
+
super({
|
|
179
|
+
type: 'rule',
|
|
180
|
+
match: rule.state,
|
|
181
|
+
name: rule.name,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
class AbilityExplainRuleSet extends AbilityExplain {
|
|
186
|
+
constructor(ruleSet) {
|
|
187
|
+
const children = ruleSet.rules.map(rule => new AbilityExplainRule(rule));
|
|
188
|
+
super({
|
|
189
|
+
type: 'ruleSet',
|
|
190
|
+
match: ruleSet.state,
|
|
191
|
+
name: ruleSet.name,
|
|
192
|
+
}, children);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
class AbilityExplainPolicy extends AbilityExplain {
|
|
196
|
+
constructor(policy) {
|
|
197
|
+
const children = policy.ruleSet.map(ruleSet => new AbilityExplainRuleSet(ruleSet));
|
|
198
|
+
super({
|
|
199
|
+
type: 'policy',
|
|
200
|
+
name: policy.priority > -1 ? `@priority ${policy.priority} ${policy.name}` : policy.name,
|
|
201
|
+
match: policy.matchState,
|
|
202
|
+
}, children);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
class AbilityResult {
|
|
207
|
+
effect;
|
|
208
|
+
strategy;
|
|
209
|
+
constructor(effect, strategy) {
|
|
210
|
+
this.effect = effect;
|
|
211
|
+
this.strategy = strategy;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Returns a list of explanations for each policy involved in the ability evaluation.
|
|
215
|
+
* Each item describes how a specific policy contributed to the final permission result.
|
|
216
|
+
*
|
|
217
|
+
* Useful for debugging, logging, or building UI tools that visualize permission logic.
|
|
218
|
+
*/
|
|
219
|
+
explain() {
|
|
220
|
+
return this.strategy.policies.map(policy => {
|
|
221
|
+
return new AbilityExplainPolicy(policy);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
isAllowed = () => {
|
|
225
|
+
return this.strategy.isAllowed();
|
|
226
|
+
};
|
|
227
|
+
isDenied = () => {
|
|
228
|
+
return this.strategy.isDenied();
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
class AbilityResolver {
|
|
233
|
+
StrategyClass;
|
|
234
|
+
policyEntries;
|
|
235
|
+
constructor(
|
|
236
|
+
/**
|
|
237
|
+
* `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.
|
|
238
|
+
*/
|
|
239
|
+
policyOrListOfPolicies, strategy, options = {}) {
|
|
240
|
+
const policies = this.toArray(policyOrListOfPolicies);
|
|
241
|
+
const filtered = options.tags
|
|
242
|
+
? policies.filter(p => p.tags.some(tag => options.tags.includes(tag)))
|
|
243
|
+
: policies;
|
|
244
|
+
const sorted = [...filtered].sort((a, b) => b.priority - a.priority);
|
|
245
|
+
this.policyEntries = sorted.map(policy => ({
|
|
246
|
+
policy,
|
|
247
|
+
normalizedPermission: AbilityResolver.normalizePermission(policy.permission),
|
|
248
|
+
segments: AbilityResolver.normalizePermission(policy.permission).split('.'),
|
|
249
|
+
}));
|
|
250
|
+
this.StrategyClass = strategy;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Resolve policy for the resource and permission key
|
|
254
|
+
*
|
|
255
|
+
* @param permission - Permission key
|
|
256
|
+
* @param resource - Resource
|
|
257
|
+
* @param environment
|
|
258
|
+
*/
|
|
259
|
+
resolve(permission, resource, environment) {
|
|
260
|
+
const inputNormalized = AbilityResolver.normalizePermission(String(permission));
|
|
261
|
+
const inputSegments = inputNormalized.split('.');
|
|
262
|
+
const filteredPolicies = this.policyEntries
|
|
263
|
+
.filter(entry => AbilityResolver.matchPermissions(entry.segments, inputSegments))
|
|
264
|
+
.map(entry => entry.policy);
|
|
265
|
+
// 2. check the policies
|
|
266
|
+
for (const policy of filteredPolicies) {
|
|
267
|
+
if (policy.disabled) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const policyMatchState = policy.check(resource, environment);
|
|
271
|
+
if (policyMatchState === AbilityMatch.pending) {
|
|
272
|
+
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.`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// 3. Use strategy
|
|
276
|
+
const strategy = new this.StrategyClass(filteredPolicies);
|
|
277
|
+
const effect = strategy.evaluate();
|
|
278
|
+
return new AbilityResult(effect, strategy);
|
|
279
|
+
}
|
|
280
|
+
enforce(permission, resource, environment) {
|
|
281
|
+
const result = this.resolve(permission, resource, environment);
|
|
282
|
+
if (result.isDenied()) {
|
|
283
|
+
throw new AbilityError(`Permission denied`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* @deprecated - will be removed
|
|
288
|
+
*
|
|
289
|
+
* Check if the permission key is contained in another permission key
|
|
290
|
+
* @param permissionA - The first permission to check
|
|
291
|
+
* @param permissionB - The second permission to check
|
|
292
|
+
*/
|
|
293
|
+
static isInPermissionContain(permissionA, permissionB) {
|
|
294
|
+
const A = permissionA.split('.');
|
|
295
|
+
const B = permissionB.split('.');
|
|
296
|
+
const [longer, shorter] = A.length >= B.length ? [A, B] : [B, A];
|
|
297
|
+
return shorter.every((chunk, i) => {
|
|
298
|
+
return chunk === '*' || longer[i] === '*' || chunk === longer[i];
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
toArray(value) {
|
|
302
|
+
return [...(Array.isArray(value) ? value : [value])];
|
|
303
|
+
}
|
|
304
|
+
static normalizePermission(permission) {
|
|
305
|
+
return permission
|
|
306
|
+
.trim()
|
|
307
|
+
.replace(/^permission\./, '') // remove prefix
|
|
308
|
+
.replace(/\.+/g, '.') // collapse multiple dots
|
|
309
|
+
.toLowerCase(); // optional: make case-insensitive
|
|
310
|
+
}
|
|
311
|
+
static matchPermissions(policySegments, inputSegments) {
|
|
312
|
+
const maxLen = Math.max(policySegments.length, inputSegments.length);
|
|
313
|
+
for (let i = 0; i < maxLen; i++) {
|
|
314
|
+
const pSeg = policySegments[i];
|
|
315
|
+
const iSeg = inputSegments[i];
|
|
316
|
+
if (pSeg === undefined) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
if (pSeg === '*') {
|
|
320
|
+
continue; // '*'
|
|
321
|
+
}
|
|
322
|
+
if (iSeg === undefined) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
if (pSeg !== iSeg) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
129
333
|
class AbilityTypeGenerator {
|
|
130
334
|
policies;
|
|
335
|
+
policyEntries;
|
|
131
336
|
constructor(policies) {
|
|
132
337
|
this.policies = policies;
|
|
338
|
+
this.policyEntries = policies.map(policy => ({
|
|
339
|
+
policy,
|
|
340
|
+
normalizedPermission: AbilityResolver.normalizePermission(policy.permission),
|
|
341
|
+
segments: AbilityResolver.normalizePermission(policy.permission).split('.'),
|
|
342
|
+
}));
|
|
133
343
|
}
|
|
134
344
|
/**
|
|
135
345
|
* Generates TypeScript type definitions based on the provided policies.
|
|
@@ -277,21 +487,6 @@ class AbilityTypeGenerator {
|
|
|
277
487
|
* @returns TypeScript array type as string
|
|
278
488
|
*/
|
|
279
489
|
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
490
|
const elementType = this.getInArrayType(resource);
|
|
296
491
|
return `readonly ${elementType}[]`;
|
|
297
492
|
}
|
|
@@ -387,18 +582,43 @@ class AbilityTypeGenerator {
|
|
|
387
582
|
output += '// Do not edit manually\n';
|
|
388
583
|
output += 'export type Resources = {\n';
|
|
389
584
|
const sortedActions = Object.keys(structure).sort();
|
|
390
|
-
sortedActions.forEach(
|
|
391
|
-
const actionObj = structure[
|
|
585
|
+
sortedActions.forEach(permission => {
|
|
586
|
+
const actionObj = structure[permission];
|
|
392
587
|
const isEmpty = Object.keys(actionObj).length === 0;
|
|
588
|
+
const inputNormalized = AbilityResolver.normalizePermission(permission);
|
|
589
|
+
const inputSegments = inputNormalized.split('.');
|
|
590
|
+
const filteredPolicies = this.policyEntries
|
|
591
|
+
.filter(entry => AbilityResolver.matchPermissions(entry.segments, inputSegments))
|
|
592
|
+
.map(entry => entry.policy);
|
|
593
|
+
// Effects
|
|
594
|
+
const effects = [...new Set(filteredPolicies.map(p => p.effect))].sort();
|
|
595
|
+
// Policies list
|
|
596
|
+
const items = filteredPolicies
|
|
597
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
598
|
+
.map(p => {
|
|
599
|
+
const effect = p.effect.padEnd(6, ' '); // permit / deny / audit
|
|
600
|
+
const displayName = p.name === p.id ? 'Unnamed policy' : p.name;
|
|
601
|
+
return ` * - ${effect} ${p.id} "${displayName}"`;
|
|
602
|
+
})
|
|
603
|
+
.join('\n');
|
|
604
|
+
//
|
|
605
|
+
output += `
|
|
606
|
+
/**
|
|
607
|
+
* Permission: ${permission}
|
|
608
|
+
* Effects: ${effects.join(', ')}
|
|
609
|
+
* Policies:
|
|
610
|
+
${items}
|
|
611
|
+
*/
|
|
612
|
+
`;
|
|
393
613
|
if (isEmpty) {
|
|
394
|
-
//
|
|
395
|
-
output += ` ['${
|
|
614
|
+
// empty object → undefined
|
|
615
|
+
output += ` ['${permission}']: undefined;\n`;
|
|
396
616
|
}
|
|
397
617
|
else {
|
|
398
|
-
//
|
|
399
|
-
output += ` ['${
|
|
618
|
+
// not empty object
|
|
619
|
+
output += ` ['${permission}']: {\n`;
|
|
400
620
|
output += this.formatNestedObject(actionObj, 4);
|
|
401
|
-
output += ' };\n';
|
|
621
|
+
output += ' } | null | undefined;\n';
|
|
402
622
|
}
|
|
403
623
|
});
|
|
404
624
|
output += '}\n';
|
|
@@ -412,15 +632,37 @@ class AbilityTypeGenerator {
|
|
|
412
632
|
output += `\n\nexport type PolicyTags = ${tagsUnion};\n`;
|
|
413
633
|
// environments
|
|
414
634
|
output += '\n\nexport type Environment = {\n';
|
|
415
|
-
Object.entries(environment).forEach(([
|
|
635
|
+
Object.entries(environment).forEach(([permission, envObj]) => {
|
|
416
636
|
const isEmpty = Object.keys(envObj).length === 0;
|
|
637
|
+
const inputNormalized = AbilityResolver.normalizePermission(permission);
|
|
638
|
+
const inputSegments = inputNormalized.split('.');
|
|
639
|
+
const filteredPolicies = this.policyEntries
|
|
640
|
+
.filter(entry => AbilityResolver.matchPermissions(entry.segments, inputSegments))
|
|
641
|
+
.map(entry => entry.policy);
|
|
642
|
+
const effects = [...new Set(filteredPolicies.map(p => p.effect))].sort();
|
|
643
|
+
const items = filteredPolicies
|
|
644
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
645
|
+
.map(p => {
|
|
646
|
+
const effect = p.effect.padEnd(6, ' ');
|
|
647
|
+
const displayName = p.name === p.id ? 'Unnamed policy' : p.name;
|
|
648
|
+
return ` * - ${effect} ${p.id} "${displayName}"`;
|
|
649
|
+
})
|
|
650
|
+
.join('\n');
|
|
651
|
+
output += `
|
|
652
|
+
/**
|
|
653
|
+
* Permission: ${permission}
|
|
654
|
+
* Effects: ${effects.join(', ')}
|
|
655
|
+
* Policies:
|
|
656
|
+
${items}
|
|
657
|
+
*/
|
|
658
|
+
`;
|
|
417
659
|
if (isEmpty) {
|
|
418
|
-
output += ` ['${
|
|
660
|
+
output += ` ['${permission}']: undefined;\n`;
|
|
419
661
|
}
|
|
420
662
|
else {
|
|
421
|
-
output += ` ['${
|
|
663
|
+
output += ` ['${permission}']: {\n`;
|
|
422
664
|
output += this.formatNestedObject(envObj, 4);
|
|
423
|
-
output += ' };\n';
|
|
665
|
+
output += ' } | null | undefined;\n';
|
|
424
666
|
}
|
|
425
667
|
});
|
|
426
668
|
output += '}\n';
|
|
@@ -444,69 +686,126 @@ class AbilityTypeGenerator {
|
|
|
444
686
|
// Nested object
|
|
445
687
|
output += `${spaces}readonly ${key}: {\n`;
|
|
446
688
|
output += this.formatNestedObject(value, indent + 2);
|
|
447
|
-
output += `${spaces}};\n`;
|
|
689
|
+
output += `${spaces}} | null | undefined;\n`;
|
|
448
690
|
}
|
|
449
691
|
else {
|
|
450
692
|
// Primitive type
|
|
451
|
-
|
|
693
|
+
const va = [String(value)];
|
|
694
|
+
let v = String(value);
|
|
695
|
+
if (!v.match(/unknown/)) {
|
|
696
|
+
if (!v.match(/null/)) {
|
|
697
|
+
va.push('null');
|
|
698
|
+
}
|
|
699
|
+
if (!v.match(/undefined/)) {
|
|
700
|
+
va.push('undefined');
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
output += `${spaces}readonly ${key}: ${va.join(' | ')} \n`;
|
|
452
704
|
}
|
|
453
705
|
});
|
|
454
706
|
return output;
|
|
455
707
|
}
|
|
456
708
|
}
|
|
457
709
|
|
|
458
|
-
class
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
710
|
+
class AbilityHash {
|
|
711
|
+
static sha1(message) {
|
|
712
|
+
const msgBytes = AbilityHash.stringToBytes(message);
|
|
713
|
+
const msgBitLength = msgBytes.length * 8;
|
|
714
|
+
const withOne = new Uint8Array(msgBytes.length + 1);
|
|
715
|
+
withOne.set(msgBytes, 0);
|
|
716
|
+
withOne[msgBytes.length] = 0x80;
|
|
717
|
+
let zeroBytes = (56 - (withOne.length % 64) + 64) % 64;
|
|
718
|
+
const padded = new Uint8Array(withOne.length + zeroBytes + 8);
|
|
719
|
+
padded.set(withOne, 0);
|
|
720
|
+
const bitLenHigh = Math.floor(msgBitLength / 0x100000000);
|
|
721
|
+
const bitLenLow = msgBitLength >>> 0;
|
|
722
|
+
padded[padded.length - 8] = (bitLenHigh >>> 24) & 0xff;
|
|
723
|
+
padded[padded.length - 7] = (bitLenHigh >>> 16) & 0xff;
|
|
724
|
+
padded[padded.length - 6] = (bitLenHigh >>> 8) & 0xff;
|
|
725
|
+
padded[padded.length - 5] = bitLenHigh & 0xff;
|
|
726
|
+
padded[padded.length - 4] = (bitLenLow >>> 24) & 0xff;
|
|
727
|
+
padded[padded.length - 3] = (bitLenLow >>> 16) & 0xff;
|
|
728
|
+
padded[padded.length - 2] = (bitLenLow >>> 8) & 0xff;
|
|
729
|
+
padded[padded.length - 1] = bitLenLow & 0xff;
|
|
730
|
+
let h0 = 0x67452301;
|
|
731
|
+
let h1 = 0xefcdab89;
|
|
732
|
+
let h2 = 0x98badcfe;
|
|
733
|
+
let h3 = 0x10325476;
|
|
734
|
+
let h4 = 0xc3d2e1f0;
|
|
735
|
+
const w = new Array(80);
|
|
736
|
+
for (let i = 0; i < padded.length; i += 64) {
|
|
737
|
+
for (let j = 0; j < 16; j++) {
|
|
738
|
+
const idx = i + j * 4;
|
|
739
|
+
w[j] =
|
|
740
|
+
(padded[idx] << 24) | (padded[idx + 1] << 16) | (padded[idx + 2] << 8) | padded[idx + 3];
|
|
741
|
+
}
|
|
742
|
+
for (let j = 16; j < 80; j++) {
|
|
743
|
+
w[j] = AbilityHash.leftRotate(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
|
|
744
|
+
}
|
|
745
|
+
let a = h0;
|
|
746
|
+
let b = h1;
|
|
747
|
+
let c = h2;
|
|
748
|
+
let d = h3;
|
|
749
|
+
let e = h4;
|
|
750
|
+
for (let j = 0; j < 80; j++) {
|
|
751
|
+
let f;
|
|
752
|
+
let k;
|
|
753
|
+
if (j < 20) {
|
|
754
|
+
f = (b & c) | (~b & d);
|
|
755
|
+
k = 0x5a827999;
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
if (j < 40) {
|
|
759
|
+
f = b ^ c ^ d;
|
|
760
|
+
k = 0x6ed9eba1;
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
if (j < 60) {
|
|
764
|
+
f = (b & c) | (b & d) | (c & d);
|
|
765
|
+
k = 0x8f1bbcdc;
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
f = b ^ c ^ d;
|
|
769
|
+
k = 0xca62c1d6;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const temp = (AbilityHash.leftRotate(a, 5) + f + e + k + (w[j] | 0)) | 0;
|
|
774
|
+
e = d;
|
|
775
|
+
d = c;
|
|
776
|
+
c = AbilityHash.leftRotate(b, 30);
|
|
777
|
+
b = a;
|
|
778
|
+
a = temp;
|
|
779
|
+
}
|
|
780
|
+
h0 = (h0 + a) | 0;
|
|
781
|
+
h1 = (h1 + b) | 0;
|
|
782
|
+
h2 = (h2 + c) | 0;
|
|
783
|
+
h3 = (h3 + d) | 0;
|
|
784
|
+
h4 = (h4 + e) | 0;
|
|
785
|
+
}
|
|
786
|
+
return [
|
|
787
|
+
AbilityHash.toHex32(h0),
|
|
788
|
+
AbilityHash.toHex32(h1),
|
|
789
|
+
AbilityHash.toHex32(h2),
|
|
790
|
+
AbilityHash.toHex32(h3),
|
|
791
|
+
AbilityHash.toHex32(h4),
|
|
792
|
+
].join('');
|
|
793
|
+
}
|
|
794
|
+
static leftRotate(value, bits) {
|
|
795
|
+
return ((value << bits) | (value >>> (32 - bits))) >>> 0;
|
|
796
|
+
}
|
|
797
|
+
static toHex32(num) {
|
|
798
|
+
return (num >>> 0).toString(16).padStart(8, '0');
|
|
799
|
+
}
|
|
800
|
+
static stringToBytes(str) {
|
|
801
|
+
if (typeof TextEncoder !== 'undefined') {
|
|
802
|
+
const encoder = new TextEncoder();
|
|
803
|
+
return encoder.encode(str);
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
const buf = Buffer.from(str, 'utf8');
|
|
807
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
475
808
|
}
|
|
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
809
|
}
|
|
511
810
|
}
|
|
512
811
|
|
|
@@ -527,15 +826,15 @@ class AbilityPolicy {
|
|
|
527
826
|
* one of the rules returns as «permit»
|
|
528
827
|
*/
|
|
529
828
|
compareMethod = AbilityCompare.and;
|
|
829
|
+
/**
|
|
830
|
+
* Policy ID
|
|
831
|
+
*/
|
|
832
|
+
id;
|
|
530
833
|
/**
|
|
531
834
|
* Policy name
|
|
532
835
|
*/
|
|
533
836
|
name;
|
|
534
837
|
description;
|
|
535
|
-
/**
|
|
536
|
-
* Policy ID
|
|
537
|
-
*/
|
|
538
|
-
id;
|
|
539
838
|
/**
|
|
540
839
|
* Running the `enforce` or `resolve` method
|
|
541
840
|
* will select only those from all passed policies that fall under the specified permission key.
|
|
@@ -546,8 +845,6 @@ class AbilityPolicy {
|
|
|
546
845
|
tags;
|
|
547
846
|
constructor(params) {
|
|
548
847
|
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
848
|
this.permission = permission;
|
|
552
849
|
this.description = description;
|
|
553
850
|
this.effect = effect;
|
|
@@ -555,6 +852,8 @@ class AbilityPolicy {
|
|
|
555
852
|
this.priority = typeof priority === 'number' ? priority : -1;
|
|
556
853
|
this.disabled = typeof disabled === 'boolean' ? disabled : false;
|
|
557
854
|
this.tags = (tags || []);
|
|
855
|
+
this.id = id || `p_${this.hash().slice(0, 10)}`;
|
|
856
|
+
this.name = name || this.id;
|
|
558
857
|
}
|
|
559
858
|
/**
|
|
560
859
|
* Add rule set to the policy
|
|
@@ -668,6 +967,24 @@ class AbilityPolicy {
|
|
|
668
967
|
}
|
|
669
968
|
return policy;
|
|
670
969
|
}
|
|
970
|
+
hash() {
|
|
971
|
+
const parts = [
|
|
972
|
+
`permission:${this.permission}`,
|
|
973
|
+
`effect:${this.effect}`,
|
|
974
|
+
`compareMethod:${this.compareMethod}`,
|
|
975
|
+
`priority:${this.priority}`,
|
|
976
|
+
`disabled:${this.disabled}`,
|
|
977
|
+
];
|
|
978
|
+
if (this.tags && this.tags.length > 0) {
|
|
979
|
+
parts.push(`tags:${[...this.tags].sort().join(',')}`);
|
|
980
|
+
}
|
|
981
|
+
if (this.ruleSet && this.ruleSet.length > 0) {
|
|
982
|
+
const ruleHashes = this.ruleSet.map(r => r.hash());
|
|
983
|
+
parts.push(`rules:${ruleHashes.sort().join('|')}`);
|
|
984
|
+
}
|
|
985
|
+
const str = parts.join(';');
|
|
986
|
+
return AbilityHash.sha1(str);
|
|
987
|
+
}
|
|
671
988
|
}
|
|
672
989
|
|
|
673
990
|
function brand$1(code) {
|
|
@@ -678,127 +995,6 @@ const AbilityPolicyEffect = {
|
|
|
678
995
|
permit: brand$1('permit'),
|
|
679
996
|
};
|
|
680
997
|
|
|
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
998
|
/**
|
|
803
999
|
* Represents a rule that defines a condition to be checked against a subject and resource.
|
|
804
1000
|
*/
|
|
@@ -829,14 +1025,14 @@ class AbilityRule {
|
|
|
829
1025
|
*/
|
|
830
1026
|
constructor(params) {
|
|
831
1027
|
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
1028
|
this.description = description;
|
|
835
1029
|
this.disabled = typeof disabled === 'boolean' ? disabled : false;
|
|
836
1030
|
this.subject = subject;
|
|
837
1031
|
this.resource = resource;
|
|
838
1032
|
this.condition = condition;
|
|
839
1033
|
this.state = this.disabled ? AbilityMatch.disabled : this.state;
|
|
1034
|
+
this.id = id || `r_${this.hash().slice(0, 10)}`;
|
|
1035
|
+
this.name = name || this.id;
|
|
840
1036
|
}
|
|
841
1037
|
static isPrimitive(v) {
|
|
842
1038
|
return typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null;
|
|
@@ -1063,6 +1259,14 @@ class AbilityRule {
|
|
|
1063
1259
|
condition: props.condition ?? this.condition,
|
|
1064
1260
|
});
|
|
1065
1261
|
}
|
|
1262
|
+
hash() {
|
|
1263
|
+
const parts = [];
|
|
1264
|
+
parts.push(`subject:${this.subject}`);
|
|
1265
|
+
parts.push(`resource:${JSON.stringify(this.resource)}`);
|
|
1266
|
+
parts.push(`condition:${this.condition}`);
|
|
1267
|
+
parts.push(`disabled:${this.disabled}`);
|
|
1268
|
+
return AbilityHash.sha1(parts.join(';'));
|
|
1269
|
+
}
|
|
1066
1270
|
static equals(subject, resource) {
|
|
1067
1271
|
return new AbilityRule({
|
|
1068
1272
|
condition: AbilityCondition.equals,
|
|
@@ -1168,13 +1372,13 @@ class AbilityRuleSet {
|
|
|
1168
1372
|
disabled;
|
|
1169
1373
|
constructor(params) {
|
|
1170
1374
|
const { name, id, compareMethod, isExcept, disabled, description } = params;
|
|
1171
|
-
this.name = name || `ruleset:${compareMethod}`;
|
|
1172
|
-
this.id = id || this.name;
|
|
1173
1375
|
this.description = description;
|
|
1174
1376
|
this.compareMethod = compareMethod;
|
|
1175
1377
|
this.isExcept = isExcept;
|
|
1176
1378
|
this.disabled = typeof disabled === 'boolean' ? disabled : false;
|
|
1177
1379
|
this.state = this.disabled ? AbilityMatch.disabled : this.state;
|
|
1380
|
+
this.id = id || `g_${this.hash().slice(0, 10)}`;
|
|
1381
|
+
this.name = name || this.id;
|
|
1178
1382
|
}
|
|
1179
1383
|
addRule(rule) {
|
|
1180
1384
|
this.rules.push(rule);
|
|
@@ -1236,6 +1440,16 @@ class AbilityRuleSet {
|
|
|
1236
1440
|
}
|
|
1237
1441
|
return next;
|
|
1238
1442
|
}
|
|
1443
|
+
hash() {
|
|
1444
|
+
const ruleHashes = this.rules.map(r => r.hash()).sort();
|
|
1445
|
+
const parts = [
|
|
1446
|
+
`compareMethod:${this.compareMethod}`,
|
|
1447
|
+
`isExcept:${this.isExcept}`,
|
|
1448
|
+
`disabled:${this.disabled}`,
|
|
1449
|
+
`rules:${ruleHashes.join('|')}`,
|
|
1450
|
+
];
|
|
1451
|
+
return AbilityHash.sha1(parts.join(';'));
|
|
1452
|
+
}
|
|
1239
1453
|
static and(rules) {
|
|
1240
1454
|
return new AbilityRuleSet({
|
|
1241
1455
|
compareMethod: AbilityCompare.and,
|
|
@@ -1308,32 +1522,32 @@ class AbilityJSONParser {
|
|
|
1308
1522
|
return {
|
|
1309
1523
|
id: rule.id,
|
|
1310
1524
|
name: rule.name,
|
|
1525
|
+
disabled: rule.disabled,
|
|
1311
1526
|
subject: rule.subject,
|
|
1312
1527
|
resource: rule.resource,
|
|
1313
1528
|
condition: rule.condition,
|
|
1314
|
-
disabled: rule.disabled,
|
|
1315
1529
|
};
|
|
1316
1530
|
}
|
|
1317
1531
|
static ruleSetToJSON(ruleSet) {
|
|
1318
1532
|
return {
|
|
1319
1533
|
id: ruleSet.id.toString(),
|
|
1320
1534
|
name: ruleSet.name.toString(),
|
|
1535
|
+
disabled: ruleSet.disabled,
|
|
1321
1536
|
compareMethod: ruleSet.compareMethod,
|
|
1322
1537
|
rules: ruleSet.rules.map(rule => AbilityJSONParser.ruleToJSON(rule)),
|
|
1323
|
-
disabled: ruleSet.disabled,
|
|
1324
1538
|
};
|
|
1325
1539
|
}
|
|
1326
1540
|
static policyToJSON(policy) {
|
|
1327
1541
|
return {
|
|
1328
1542
|
id: policy.id.toString(),
|
|
1329
1543
|
name: policy.name.toString(),
|
|
1330
|
-
|
|
1331
|
-
|
|
1544
|
+
disabled: policy.disabled,
|
|
1545
|
+
priority: policy.priority,
|
|
1332
1546
|
permission: policy.permission,
|
|
1333
1547
|
effect: policy.effect,
|
|
1334
|
-
|
|
1335
|
-
disabled: policy.disabled,
|
|
1548
|
+
compareMethod: policy.compareMethod,
|
|
1336
1549
|
tags: policy.tags,
|
|
1550
|
+
ruleSet: policy.ruleSet.map(ruleSet => AbilityJSONParser.ruleSetToJSON(ruleSet)),
|
|
1337
1551
|
};
|
|
1338
1552
|
}
|
|
1339
1553
|
static toJSON(policies) {
|
|
@@ -1508,11 +1722,49 @@ class AbilityDSLLexer {
|
|
|
1508
1722
|
readAnnotation() {
|
|
1509
1723
|
const startLine = this.line;
|
|
1510
1724
|
const startColumn = this.column;
|
|
1511
|
-
let
|
|
1725
|
+
let raw = '';
|
|
1726
|
+
// читаем всю строку после @
|
|
1512
1727
|
while (!this.isAtEnd() && !this.isNewline()) {
|
|
1513
|
-
|
|
1728
|
+
raw += this.advance();
|
|
1729
|
+
}
|
|
1730
|
+
raw = raw.trim();
|
|
1731
|
+
// parse literals
|
|
1732
|
+
let result = '';
|
|
1733
|
+
let i = 0;
|
|
1734
|
+
let inString = false;
|
|
1735
|
+
let quote = null;
|
|
1736
|
+
let escaped = false;
|
|
1737
|
+
while (i < raw.length) {
|
|
1738
|
+
const ch = raw[i];
|
|
1739
|
+
if (inString) {
|
|
1740
|
+
if (escaped) {
|
|
1741
|
+
result += ch;
|
|
1742
|
+
escaped = false;
|
|
1743
|
+
}
|
|
1744
|
+
else if (ch === '\\') {
|
|
1745
|
+
escaped = true;
|
|
1746
|
+
}
|
|
1747
|
+
else if (ch === quote) {
|
|
1748
|
+
inString = false;
|
|
1749
|
+
quote = null;
|
|
1750
|
+
}
|
|
1751
|
+
else {
|
|
1752
|
+
result += ch;
|
|
1753
|
+
}
|
|
1754
|
+
i++;
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
// start of string
|
|
1758
|
+
if (ch === '"' || ch === "'") {
|
|
1759
|
+
inString = true;
|
|
1760
|
+
quote = ch;
|
|
1761
|
+
i++;
|
|
1762
|
+
continue;
|
|
1763
|
+
}
|
|
1764
|
+
result += ch;
|
|
1765
|
+
i++;
|
|
1514
1766
|
}
|
|
1515
|
-
return new AbilityDSLToken(TokenTypes.ANNOTATION,
|
|
1767
|
+
return new AbilityDSLToken(TokenTypes.ANNOTATION, result.trim(), startLine, startColumn);
|
|
1516
1768
|
}
|
|
1517
1769
|
readString() {
|
|
1518
1770
|
const startLine = this.line;
|
|
@@ -2083,28 +2335,34 @@ class AbilityDSLParser {
|
|
|
2083
2335
|
*/
|
|
2084
2336
|
parseRuleSets(policyCompareMethod) {
|
|
2085
2337
|
const sets = [];
|
|
2338
|
+
this.consumeLeadingComments();
|
|
2339
|
+
this.consumeLeadingAnnotations();
|
|
2086
2340
|
while (!this.stream.eof() && !this.isStartOfPolicy()) {
|
|
2087
|
-
|
|
2088
|
-
this.consumeLeadingAnnotations();
|
|
2089
|
-
// Если начинается новая except группа — парсим её
|
|
2341
|
+
// maybe except ruleSet
|
|
2090
2342
|
if (this.isStartOfExcept()) {
|
|
2091
2343
|
sets.push(this.parseExceptGroup(policyCompareMethod));
|
|
2092
2344
|
continue;
|
|
2093
2345
|
}
|
|
2094
|
-
//
|
|
2346
|
+
// maybe ruleSet
|
|
2095
2347
|
if (this.isStartOfGroup()) {
|
|
2096
2348
|
sets.push(this.parseGroup());
|
|
2097
2349
|
continue;
|
|
2098
2350
|
}
|
|
2099
|
-
|
|
2351
|
+
// implicit ruleSet
|
|
2352
|
+
// if (!this.isStartOfRule()) {
|
|
2353
|
+
// this.consumeLeadingComments();
|
|
2354
|
+
// this.consumeLeadingAnnotations();
|
|
2355
|
+
// }
|
|
2356
|
+
// is implicit group
|
|
2357
|
+
// const annotation = this.takeAnnotations('ruleSet');
|
|
2100
2358
|
const group = new AbilityRuleSet({
|
|
2101
|
-
id: annotation.id?.value || null,
|
|
2359
|
+
// id: annotation.id?.value || null,
|
|
2102
2360
|
compareMethod: policyCompareMethod,
|
|
2103
|
-
name: annotation.name?.value ?? null,
|
|
2104
|
-
description: annotation.description?.value || null,
|
|
2105
|
-
disabled: annotation.disabled?.value ?? undefined,
|
|
2361
|
+
// name: annotation.name?.value ?? null,
|
|
2362
|
+
// description: annotation.description?.value || null,
|
|
2363
|
+
// disabled: annotation.disabled?.value ?? undefined,
|
|
2106
2364
|
});
|
|
2107
|
-
//
|
|
2365
|
+
// Read rules of implicit-группы
|
|
2108
2366
|
while (!this.stream.eof()) {
|
|
2109
2367
|
this.consumeLeadingComments();
|
|
2110
2368
|
this.consumeLeadingAnnotations();
|
|
@@ -2705,6 +2963,11 @@ class AbilityDSLParser {
|
|
|
2705
2963
|
isStartOfGroup() {
|
|
2706
2964
|
return this.stream.check(TokenTypes.ALL) || this.stream.check(TokenTypes.ANY);
|
|
2707
2965
|
}
|
|
2966
|
+
isStartOfRule() {
|
|
2967
|
+
return (this.stream.check(TokenTypes.IDENTIFIER) ||
|
|
2968
|
+
this.stream.check(TokenTypes.ALWAYS) ||
|
|
2969
|
+
this.stream.check(TokenTypes.NEVER));
|
|
2970
|
+
}
|
|
2708
2971
|
isStartOfExcept() {
|
|
2709
2972
|
return this.stream.check(TokenTypes.EXCEPT);
|
|
2710
2973
|
}
|