@via-profit/ability 1.0.3 → 2.0.0-rc.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.js CHANGED
@@ -2,6 +2,254 @@
2
2
  /******/ "use strict";
3
3
  /******/ var __webpack_modules__ = ({
4
4
 
5
+ /***/ 19:
6
+ /***/ ((__unused_webpack_module, exports) => {
7
+
8
+
9
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
10
+ exports.AbilityCode = void 0;
11
+ class AbilityCode {
12
+ code;
13
+ constructor(code) {
14
+ this.code = code;
15
+ }
16
+ isEqual(compareWith) {
17
+ return compareWith !== null && this.code === compareWith.code;
18
+ }
19
+ isNotEqual(compareWith) {
20
+ return !this.isEqual(compareWith);
21
+ }
22
+ }
23
+ exports.AbilityCode = AbilityCode;
24
+ exports["default"] = AbilityCode;
25
+
26
+
27
+ /***/ }),
28
+
29
+ /***/ 923:
30
+ /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
31
+
32
+
33
+ var __importDefault = (this && this.__importDefault) || function (mod) {
34
+ return (mod && mod.__esModule) ? mod : { "default": mod };
35
+ };
36
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
37
+ exports.AbilityCompare = void 0;
38
+ const AbilityCode_1 = __importDefault(__webpack_require__(19));
39
+ class AbilityCompare extends AbilityCode_1.default {
40
+ static OR = new AbilityCompare(0);
41
+ static AND = new AbilityCompare(1);
42
+ }
43
+ exports.AbilityCompare = AbilityCompare;
44
+ exports["default"] = AbilityCompare;
45
+
46
+
47
+ /***/ }),
48
+
49
+ /***/ 261:
50
+ /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
51
+
52
+
53
+ var __importDefault = (this && this.__importDefault) || function (mod) {
54
+ return (mod && mod.__esModule) ? mod : { "default": mod };
55
+ };
56
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
57
+ exports.AbilityCondition = void 0;
58
+ const AbilityCode_1 = __importDefault(__webpack_require__(19));
59
+ class AbilityCondition extends AbilityCode_1.default {
60
+ static EQUAL = new AbilityCondition('=');
61
+ static NOT_EQUAL = new AbilityCondition('<>');
62
+ static MORE_THAN = new AbilityCondition('>');
63
+ static LESS_THAN = new AbilityCondition('<');
64
+ static LESS_OR_EQUAL = new AbilityCondition('<=');
65
+ static MORE_OR_EQUAL = new AbilityCondition('>=');
66
+ static IN = new AbilityCondition('in');
67
+ static NOT_IN = new AbilityCondition('not in');
68
+ }
69
+ exports.AbilityCondition = AbilityCondition;
70
+ exports["default"] = AbilityCondition;
71
+
72
+
73
+ /***/ }),
74
+
75
+ /***/ 122:
76
+ /***/ ((__unused_webpack_module, exports) => {
77
+
78
+
79
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
80
+ exports.PermissionError = exports.AbilityParserError = exports.AbilityError = void 0;
81
+ class AbilityError extends Error {
82
+ constructor(message) {
83
+ super(message);
84
+ }
85
+ }
86
+ exports.AbilityError = AbilityError;
87
+ class AbilityParserError extends Error {
88
+ constructor(message) {
89
+ super(message);
90
+ }
91
+ }
92
+ exports.AbilityParserError = AbilityParserError;
93
+ class PermissionError extends Error {
94
+ constructor(message) {
95
+ super(message);
96
+ }
97
+ }
98
+ exports.PermissionError = PermissionError;
99
+
100
+
101
+ /***/ }),
102
+
103
+ /***/ 909:
104
+ /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
105
+
106
+
107
+ var __importDefault = (this && this.__importDefault) || function (mod) {
108
+ return (mod && mod.__esModule) ? mod : { "default": mod };
109
+ };
110
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
111
+ exports.AbilityMatch = void 0;
112
+ const AbilityCode_1 = __importDefault(__webpack_require__(19));
113
+ class AbilityMatch extends AbilityCode_1.default {
114
+ static PENDING = new AbilityMatch(2);
115
+ static MATCH = new AbilityMatch(1);
116
+ static MISMATCH = new AbilityMatch(0);
117
+ }
118
+ exports.AbilityMatch = AbilityMatch;
119
+ exports["default"] = AbilityMatch;
120
+
121
+
122
+ /***/ }),
123
+
124
+ /***/ 189:
125
+ /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
126
+
127
+
128
+ var __importDefault = (this && this.__importDefault) || function (mod) {
129
+ return (mod && mod.__esModule) ? mod : { "default": mod };
130
+ };
131
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
132
+ exports.AbilityParser = void 0;
133
+ const AbilityError_1 = __webpack_require__(122);
134
+ const AbilityCondition_1 = __importDefault(__webpack_require__(261));
135
+ class AbilityParser {
136
+ /**
137
+ * Validates the configuration object based on the provided field validation configurations.
138
+ * @param config - The configuration object to validate.
139
+ * @param fields - An array of field validation configurations.
140
+ * @throws {AbilityParserError} If a required field is missing or if a field has an incorrect type.
141
+ */
142
+ static validateConfig(config, fields) {
143
+ fields.forEach(([field, type, isRequired]) => {
144
+ const value = config[field];
145
+ if (isRequired) {
146
+ if (typeof value === 'undefined') {
147
+ throw new AbilityError_1.AbilityParserError(`Missing required field [${field}]`);
148
+ }
149
+ }
150
+ switch (type) {
151
+ case 'array':
152
+ if (typeof value !== 'object' || !Array.isArray(value)) {
153
+ throw new AbilityError_1.AbilityParserError(`Field [${field}] must be an type of [${type}], bit got [${typeof value}]`);
154
+ }
155
+ break;
156
+ default:
157
+ if (typeof value !== type && typeof value !== 'undefined') {
158
+ throw new AbilityError_1.AbilityParserError(`Field [${field}] must be a type of [${type}], bit got [${typeof value}]`);
159
+ }
160
+ break;
161
+ }
162
+ });
163
+ }
164
+ /**
165
+ * Prepares and validates the configuration object or JSON string.
166
+ * @param configOrJson - The configuration object or JSON string to validate.
167
+ * @param fields - An array of field validation configurations.
168
+ * @returns The validated configuration object.
169
+ */
170
+ static prepareAndValidateConfig(configOrJson, fields) {
171
+ const config = typeof configOrJson === 'string'
172
+ ? (JSON.parse(configOrJson))
173
+ : configOrJson;
174
+ AbilityParser.validateConfig(config, fields);
175
+ return config;
176
+ }
177
+ /*
178
+ *
179
+ * readonly ['order.update']: {
180
+ * readonly user: {
181
+ * readonly roles: readonly string[];
182
+ * readonly department: string;
183
+ * };
184
+ * readonly order: {
185
+ * readonly estimatedArrivalAt: number;
186
+ * readonly status: string;
187
+ * }
188
+ * }
189
+ *
190
+ * */
191
+ /**
192
+ * Sets a value in a nested object structure based on a dot/bracket notation path.
193
+ * @param object - The target object to modify.
194
+ * @param path - The path to the property in dot/bracket notation.
195
+ * @param value - The value to set at the specified path.
196
+ */
197
+ static setValueDotValue(object, path, value) {
198
+ const way = path.replace(/\[/g, '.').replace(/\]/g, '').split('.');
199
+ const last = way.pop();
200
+ if (!last) {
201
+ throw new AbilityError_1.AbilityParserError(`Invalid path provided on a [${path}]`);
202
+ }
203
+ way.reduce((o, k, i, kk) => {
204
+ if (!o[k]) {
205
+ o[k] = isFinite(Number(kk[i + 1])) ? [] : {};
206
+ }
207
+ return o[k];
208
+ }, object)[last] = value;
209
+ }
210
+ /**
211
+ * Generates TypeScript type definitions based on the provided policies.
212
+ * @param policies - An array of AbilityPolicy instances.
213
+ * @param outPath - The output path for the generated type definitions.
214
+ * @returns A record containing the generated type definitions.
215
+ */
216
+ static generateTypeDefs(policies, outPath) {
217
+ const record = {};
218
+ policies.forEach(policy => {
219
+ policy.ruleSet.forEach(ruleSet => {
220
+ ruleSet.rules.forEach(rule => {
221
+ const [leftFieldPath, condition, rightFiledPath] = rule.matches;
222
+ let value = 'any';
223
+ switch (true) {
224
+ case condition.isEqual(AbilityCondition_1.default.NOT_EQUAL):
225
+ case condition.isEqual(AbilityCondition_1.default.EQUAL):
226
+ value = typeof rightFiledPath;
227
+ break;
228
+ case condition.isEqual(AbilityCondition_1.default.IN):
229
+ case condition.isEqual(AbilityCondition_1.default.NOT_IN):
230
+ value = `${typeof rightFiledPath}[]`;
231
+ break;
232
+ case condition.isEqual(AbilityCondition_1.default.MORE_OR_EQUAL):
233
+ case condition.isEqual(AbilityCondition_1.default.MORE_THAN):
234
+ case condition.isEqual(AbilityCondition_1.default.LESS_OR_EQUAL):
235
+ case condition.isEqual(AbilityCondition_1.default.LESS_THAN):
236
+ value = 'number';
237
+ break;
238
+ }
239
+ AbilityParser.setValueDotValue(record, leftFieldPath, value);
240
+ });
241
+ });
242
+ });
243
+ console.log(JSON.stringify(record));
244
+ return record;
245
+ }
246
+ }
247
+ exports.AbilityParser = AbilityParser;
248
+ exports["default"] = AbilityParser;
249
+
250
+
251
+ /***/ }),
252
+
5
253
  /***/ 844:
6
254
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
7
255
 
@@ -12,166 +260,135 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
260
  Object.defineProperty(exports, "__esModule", ({ value: true }));
13
261
  exports.AbilityPolicy = void 0;
14
262
  const AbilityRule_1 = __importDefault(__webpack_require__(476));
263
+ const AbilityRuleSet_1 = __importDefault(__webpack_require__(402));
264
+ const AbilityMatch_1 = __importDefault(__webpack_require__(909));
265
+ const AbilityCompare_1 = __importDefault(__webpack_require__(923));
266
+ const AbilityPolicyEffect_1 = __importDefault(__webpack_require__(277));
267
+ const AbilityParser_1 = __importDefault(__webpack_require__(189));
15
268
  class AbilityPolicy {
269
+ matchState = AbilityMatch_1.default.PENDING;
16
270
  /**
17
271
  * List of rules
18
272
  */
19
- rules = [];
273
+ ruleSet = [];
20
274
  /**
21
- * Nested policies
275
+ * Policy effect
22
276
  */
23
- policies = [];
277
+ effect;
24
278
  /**
25
279
  * Rules compare method.\
26
280
  * For the «and» method the rule will be permitted if all\
27
281
  * rules will be returns «permit» status and for the «or» - if\
28
282
  * one of the rules returns as «permit»
29
283
  */
30
- rulesCompareMethod = 'and';
31
- /**
32
- * Policies compare method.\
33
- * For the «and» method the policy will be permitted if all\
34
- * policies will be returns «permit» status and for the «or» - if\
35
- * one of the policies returns as «permit»
36
- */
37
- policiesCompareMethod = 'and';
284
+ compareMethod = AbilityCompare_1.default.AND;
38
285
  /**
39
286
  * Policy name
40
287
  */
41
288
  name;
42
- /**
43
- * Policy description
44
- */
45
- description = null;
46
289
  /**
47
290
  * Policy ID
48
291
  */
49
292
  id;
50
- constructor(policyName, policyID, description) {
51
- this.name = policyName || Symbol('name');
52
- this.id = policyID || Symbol('id');
53
- this.description = typeof description === 'string' ? description : null;
54
- }
55
- addRule(rule, compareMethod = 'and') {
56
- this.rules.push(rule);
57
- this.rulesCompareMethod = compareMethod;
58
- return this;
59
- }
60
- addRules(rules, compareMethod = 'and') {
61
- rules.forEach(rule => this.addRule(rule, compareMethod));
62
- return this;
63
- }
64
- addPolicy(policy, compareMethod = 'and') {
65
- this.policies.push(policy);
66
- this.policiesCompareMethod = compareMethod;
67
- return this;
293
+ /**
294
+ * Soon
295
+ */
296
+ action;
297
+ constructor(params) {
298
+ const { name, id, action, effect } = params;
299
+ this.name = name || Symbol('name');
300
+ this.id = id || Symbol('id');
301
+ this.action = action;
302
+ this.effect = effect;
68
303
  }
69
- addPolicies(policies, compareMethod = 'and') {
70
- policies.forEach(policy => this.addPolicy(policy, compareMethod));
304
+ /**
305
+ * Add rule set to the policy
306
+ * @param ruleSet - The rule set to add
307
+ */
308
+ addRuleSet(ruleSet) {
309
+ this.ruleSet.push(ruleSet);
71
310
  return this;
72
311
  }
73
- getName() {
74
- return this.name;
75
- }
76
- getID() {
77
- return this.id;
78
- }
79
- getPolicies() {
80
- return this.policies;
81
- }
82
- getRules() {
83
- return this.rules;
84
- }
85
- setDescription(description) {
86
- this.description = description;
312
+ /**
313
+ * Add rule to the policy
314
+ * @param rule - The rule to add
315
+ */
316
+ addRule(rule) {
317
+ this.addRuleSet(new AbilityRuleSet_1.default({
318
+ name: rule.name,
319
+ }).addRule(rule, AbilityCompare_1.default.AND));
87
320
  return this;
88
321
  }
89
- enforce(subject, resource, environment) {
90
- const { permission, deniedPolicies } = this.check(subject, resource, environment);
91
- if (permission === 'deny') {
92
- throw new Error(`Permission denied. ${deniedPolicies[0].getName().toString()}`);
93
- }
94
- }
95
- isPermit(subject, resource, environment) {
96
- const { permission } = this.check(subject, resource, environment);
97
- return permission === 'permit';
98
- }
99
- isDeny(subject, resource, environment) {
100
- const { permission } = this.check(subject, resource, environment);
101
- return permission === 'deny';
102
- }
103
- check(subject, resource, environment) {
104
- const deniedRules = [];
105
- const deniedPolicies = [];
106
- const ruleStatuses = [];
107
- const policyStatuses = [];
108
- AbilityPolicy.validatePolicy(this);
109
- this.policies.forEach(policy => {
110
- const policyResult = policy.check(subject, resource, environment);
111
- policyStatuses.push(policyResult.permission);
112
- if (policyResult.permission === 'deny') {
113
- deniedPolicies.push(policy);
114
- }
115
- policyResult.deniedRules.forEach(rule => {
116
- deniedRules.push(rule);
322
+ /**
323
+ * Check if the policy is matched
324
+ * @param resource - The resource to check
325
+ */
326
+ check(resource) {
327
+ this.matchState = AbilityMatch_1.default.MISMATCH;
328
+ /**
329
+ * If policy contain a rules
330
+ */
331
+ if (this.ruleSet.length) {
332
+ const ruleCheckStates = [];
333
+ this.ruleSet.forEach(rule => {
334
+ const ruleCheckState = rule.check(resource);
335
+ ruleCheckStates.push(ruleCheckState);
117
336
  });
118
- });
119
- this.rules.forEach(rule => {
120
- const permission = rule.check(subject, resource, environment);
121
- ruleStatuses.push(permission);
122
- if (permission === 'deny') {
123
- deniedRules.push(rule);
124
- deniedPolicies.push(this);
337
+ if (AbilityCompare_1.default.AND.isEqual(this.compareMethod)) {
338
+ if (ruleCheckStates.every(ruleState => AbilityMatch_1.default.MATCH.isEqual(ruleState))) {
339
+ this.matchState = AbilityMatch_1.default.MATCH;
340
+ }
341
+ }
342
+ if (AbilityCompare_1.default.OR.isEqual(this.compareMethod)) {
343
+ if (ruleCheckStates.some(ruleState => AbilityMatch_1.default.MATCH.isEqual(ruleState))) {
344
+ this.matchState = AbilityMatch_1.default.MATCH;
345
+ }
125
346
  }
126
- });
127
- let res = 'deny';
128
- if (policyStatuses.length) {
129
- res = policyStatuses[this.policiesCompareMethod === 'and' ? 'every' : 'some'](status => status === 'permit')
130
- ? 'permit'
131
- : 'deny';
132
- }
133
- if (ruleStatuses.length) {
134
- res = ruleStatuses[this.rulesCompareMethod === 'and' ? 'every' : 'some'](status => status === 'permit')
135
- ? 'permit'
136
- : 'deny';
137
347
  }
138
- return {
139
- permission: res,
140
- deniedRules,
141
- deniedPolicies,
142
- };
348
+ return this.matchState;
143
349
  }
144
350
  /**
145
351
  * Parse the config JSON format to Policy class instance
146
352
  */
147
353
  static parse(configOrJson) {
148
- const { id, name, description, rules, policies, rulesCompareMethod, policiesCompareMethod } = typeof configOrJson === 'string'
149
- ? JSON.parse(configOrJson)
150
- : configOrJson;
354
+ const config = AbilityParser_1.default.prepareAndValidateConfig(configOrJson, [
355
+ ['id', 'string', false],
356
+ ['name', 'string', true],
357
+ ['action', 'string', true],
358
+ ['effect', 'number', true],
359
+ ['compareMethod', 'number', true],
360
+ ['ruleSet', 'array', true],
361
+ ]);
362
+ const { id, name, ruleSet, compareMethod, action, effect } = config;
151
363
  // Create the empty policy
152
- const policy = new AbilityPolicy(name, id, description);
153
- if (description) {
154
- policy.setDescription(description);
155
- }
156
- // Adding rules if exists
157
- if (rules && rules.length > 0) {
158
- const abilityRules = rules.map(ruleConfig => AbilityRule_1.default.parse(ruleConfig));
159
- policy.addRules(abilityRules, rulesCompareMethod);
160
- }
161
- // Adding policies if exixts
162
- if (policies && policies.length > 0) {
163
- const nestedPolicies = policies.map(nestedConfig => AbilityPolicy.parse(nestedConfig));
164
- policy.addPolicies(nestedPolicies, policiesCompareMethod);
165
- }
364
+ const policy = new AbilityPolicy({
365
+ name,
366
+ id,
367
+ action,
368
+ effect: new AbilityPolicyEffect_1.default(effect),
369
+ });
370
+ policy.compareMethod = new AbilityCompare_1.default(compareMethod);
371
+ ruleSet.forEach(ruleOrRuleSet => {
372
+ // is ruleset
373
+ if ('rules' in ruleOrRuleSet) {
374
+ policy.addRuleSet(AbilityRuleSet_1.default.parse(ruleOrRuleSet));
375
+ }
376
+ // is simple rule
377
+ if (!('rules' in ruleOrRuleSet)) {
378
+ policy.addRule(AbilityRule_1.default.parse(ruleOrRuleSet));
379
+ }
380
+ });
166
381
  return policy;
167
382
  }
168
- static validatePolicy(policy) {
169
- if (policy.policies.length > 0 && policy.rules.length > 0) {
170
- throw new Error("The policy can't have a policies and rules at the same time");
171
- }
172
- if (policy.policies.length === 0 && policy.rules.length === 0) {
173
- throw new Error('The policy must have a nested policies or rules');
174
- }
383
+ export() {
384
+ return {
385
+ id: this.id.toString(),
386
+ name: this.name.toString(),
387
+ compareMethod: this.compareMethod.code,
388
+ ruleSet: this.ruleSet.map(rule => rule.export()),
389
+ action: this.action,
390
+ effect: this.effect.code,
391
+ };
175
392
  }
176
393
  }
177
394
  exports.AbilityPolicy = AbilityPolicy;
@@ -180,129 +397,166 @@ exports["default"] = AbilityPolicy;
180
397
 
181
398
  /***/ }),
182
399
 
183
- /***/ 476:
184
- /***/ ((__unused_webpack_module, exports) => {
400
+ /***/ 277:
401
+ /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
185
402
 
186
403
 
404
+ var __importDefault = (this && this.__importDefault) || function (mod) {
405
+ return (mod && mod.__esModule) ? mod : { "default": mod };
406
+ };
187
407
  Object.defineProperty(exports, "__esModule", ({ value: true }));
188
- exports.AbilityRule = void 0;
189
- class AbilityRule {
190
- matches;
191
- name;
192
- effect;
408
+ exports.AbilityPolicyEffect = void 0;
409
+ const AbilityCode_1 = __importDefault(__webpack_require__(19));
410
+ class AbilityPolicyEffect extends AbilityCode_1.default {
411
+ static DENY = new AbilityPolicyEffect(0);
412
+ static PERMIT = new AbilityPolicyEffect(1);
413
+ }
414
+ exports.AbilityPolicyEffect = AbilityPolicyEffect;
415
+ exports["default"] = AbilityPolicyEffect;
416
+
417
+
418
+ /***/ }),
419
+
420
+ /***/ 668:
421
+ /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
422
+
423
+
424
+ var __importDefault = (this && this.__importDefault) || function (mod) {
425
+ return (mod && mod.__esModule) ? mod : { "default": mod };
426
+ };
427
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
428
+ exports.AbilityResolver = void 0;
429
+ const AbilityPolicyEffect_1 = __importDefault(__webpack_require__(277));
430
+ const AbilityMatch_1 = __importDefault(__webpack_require__(909));
431
+ const AbilityError_1 = __webpack_require__(122);
432
+ class AbilityResolver {
433
+ policies;
434
+ constructor(policies) {
435
+ this.policies = policies;
436
+ }
193
437
  /**
194
- * Create the rule to compare
438
+ * Resolve policy for the resource and action
195
439
  *
196
- * @param ruleName {string} - The rule name
197
- * @param effect {AbilityRuleStatus} - Return value
198
- * @param matches {AbilityRuleMatches} - The matching rule he matching rule can be on of the format:
199
- * \
200
- * For example, be compared two's data\
201
- * \
202
- * _The subject_
203
- * ```json
204
- * {"userID": "1", "userDepartament": "NBC"}
205
- * ```
206
- * and _The resource_
207
- * ```json
208
- * {"departamentID": "154", "departamentName": "NBC"}
209
- * ```
210
- * \
211
- * Now we can make the matching rule:
212
- * ```json
213
- * ["subject.userDepartament", "=", "resource.departamentName"]
214
- * ```
215
- *
216
- * \
217
- * **Example 2.**\
218
- * In this case will be compared resource and string:
219
- * \
220
- * _The subject_
221
- * ```json
222
- * {"userID": "1", "userDepartament": "NBC"}
223
- * ```
224
- * and _The resource_ will be «undefined».\
225
- * Now we can make the matching rule:
226
- * ```json
227
- * ["subject.userDepartament", "=", "NBC"]
228
- * ```
229
- * \
230
- * **Example 3.**\
231
- * In this case will be compared resource and array of string:\
232
- * \
233
- * _The subject_
234
- * ```json
235
- * {"userID": "1", "userDepartament": "NBC"}
236
- * ```
237
- * and _The resource_
238
- * ```json
239
- * ["FOX", "NBC", "AONE"]
240
- * ```
241
- * \
242
- * Now we can make the matching rule:
243
- * ```json
244
- * ["subject.userDepartament", "=", "resource"]
245
- * ```
246
- * **Note: In this rule whe set the resource field as the «resource» string.\
247
- * This means that we will compare the entire resource as a whole,\
248
- * and not search for it by field name.**
249
- * \
250
- * **Example 4.**\
251
- * In this case will be compared resource and array of string:\
252
- * \
253
- * _The subject_
254
- * ```json
255
- * {"user": {"account": {"roles": ["admin", "viewer"]}}}
256
- * ```
257
- * and _The resource_
258
- * ```json
259
- * undefined
260
- * ```
261
- * \
262
- * Now we can make the matching rule:
263
- * ```json
264
- * ["subject.user.account.roles", "in", "admin"]
440
+ @param action - Action
441
+ * @param resource - Resource
265
442
  */
266
- constructor(matches, effect = 'permit', ruleName) {
267
- this.name = ruleName || Symbol('name');
268
- this.effect = effect;
269
- this.matches = matches;
443
+ resolve(action, resource) {
444
+ const filteredPolicies = this.policies.filter(policy => {
445
+ return AbilityResolver.isInActionContain(policy.action, String(action));
446
+ });
447
+ filteredPolicies.map(policy => policy.check(resource));
448
+ this.policies = filteredPolicies;
449
+ return this;
270
450
  }
271
- getName() {
272
- return this.name;
451
+ enforce(action, resource) {
452
+ const resolver = this.resolve(action, resource);
453
+ if (resolver) {
454
+ if (resolver.isDeny()) {
455
+ throw new AbilityError_1.PermissionError(resolver.getPolicy()?.name?.toString() || 'Unknown permission error');
456
+ }
457
+ }
273
458
  }
459
+ /**
460
+ * Get the last effect of the policy
461
+ *
462
+ * @returns {AbilityPolicyEffect | null}
463
+ */
274
464
  getEffect() {
275
- return this.effect;
465
+ const effects = this.policies.reduce((collect, policy, _index) => {
466
+ if (policy.matchState.isEqual(AbilityMatch_1.default.MATCH)) {
467
+ return collect.concat(policy.effect);
468
+ }
469
+ return collect;
470
+ }, []);
471
+ if (effects.length) {
472
+ return effects[effects.length - 1];
473
+ }
474
+ return null;
475
+ }
476
+ isPermit() {
477
+ const effect = this.getEffect();
478
+ return effect !== null && effect.isEqual(AbilityPolicyEffect_1.default.PERMIT);
276
479
  }
277
- isPermit(subject, resource, environment) {
278
- return 'permit' === this.check(subject, resource, environment);
480
+ isDeny() {
481
+ const effect = this.getEffect();
482
+ return effect !== null && effect.isEqual(AbilityPolicyEffect_1.default.DENY);
279
483
  }
280
- isDeny(subject, resource, environment) {
281
- return 'deny' === this.check(subject, resource, environment);
484
+ getPolicy() {
485
+ const lastPolicy = this.policies.length ? this.policies[this.policies.length - 1] : null;
486
+ return lastPolicy && lastPolicy.matchState.isEqual(AbilityMatch_1.default.MATCH) ? lastPolicy : null;
282
487
  }
283
- check(subject, resource, environment) {
284
- const [_subjectFieldName, condition, _resourceFieldName] = this.matches;
488
+ /**
489
+ * Check if the action is contained in another action
490
+ * @param actionA - The first action to check
491
+ * @param actionB - The second action to check
492
+ */
493
+ static isInActionContain(actionA, actionB) {
494
+ const actionAArray = String(actionA).split('.');
495
+ const actionBArray = String(actionB).split('.');
496
+ const a = actionAArray.length >= actionBArray.length ? actionAArray : actionBArray;
497
+ const b = actionBArray.length >= actionAArray.length ? actionBArray : actionAArray;
498
+ return a
499
+ .reduce((acc, chunk, index) => {
500
+ const iterationRes = chunk === b[index] || b[index] === '*' || chunk === '*';
501
+ return acc.concat(iterationRes);
502
+ }, [])
503
+ .every(Boolean);
504
+ }
505
+ }
506
+ exports.AbilityResolver = AbilityResolver;
507
+ exports["default"] = AbilityResolver;
508
+
509
+
510
+ /***/ }),
511
+
512
+ /***/ 476:
513
+ /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
514
+
515
+
516
+ var __importDefault = (this && this.__importDefault) || function (mod) {
517
+ return (mod && mod.__esModule) ? mod : { "default": mod };
518
+ };
519
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
520
+ exports.AbilityRule = void 0;
521
+ const AbilityMatch_1 = __importDefault(__webpack_require__(909));
522
+ const AbilityCondition_1 = __importDefault(__webpack_require__(261));
523
+ const AbilityParser_1 = __importDefault(__webpack_require__(189));
524
+ class AbilityRule {
525
+ matches;
526
+ name;
527
+ state = AbilityMatch_1.default.PENDING;
528
+ constructor(params) {
529
+ const { name, matches } = params;
530
+ this.name = name || Symbol('name');
531
+ this.matches = matches;
532
+ }
533
+ /**
534
+ * Check if the rule is matched
535
+ * @param resource - The resource to check
536
+ */
537
+ check(resource) {
538
+ const [_subjectPathName, condition, _staticValueOrPathName] = this.matches;
285
539
  let is = false;
286
- const [valueS, valueO] = this.extractValues(subject, resource, environment);
287
- if (condition === '<') {
540
+ const [valueS, valueO] = this.extractValues(resource);
541
+ if (AbilityCondition_1.default.LESS_THAN.isEqual(condition)) {
288
542
  is = Number(valueS) < Number(valueO);
289
543
  }
290
- if (condition === '<=') {
544
+ if (AbilityCondition_1.default.LESS_OR_EQUAL.isEqual(condition)) {
291
545
  is = Number(valueS) <= Number(valueO);
292
546
  }
293
- if (condition === '>') {
547
+ if (AbilityCondition_1.default.MORE_THAN.isEqual(condition)) {
294
548
  is = Number(valueS) > Number(valueO);
295
549
  }
296
- if (condition === '>=') {
550
+ if (AbilityCondition_1.default.MORE_OR_EQUAL.isEqual(condition)) {
297
551
  is = Number(valueS) >= Number(valueO);
298
552
  }
299
- if (condition === '=') {
553
+ if (AbilityCondition_1.default.EQUAL.isEqual(condition)) {
300
554
  is = valueS === valueO;
301
555
  }
302
- if (condition === '<>') {
556
+ if (AbilityCondition_1.default.NOT_EQUAL.isEqual(condition)) {
303
557
  is = valueS !== valueO;
304
558
  }
305
- if (condition === 'in') {
559
+ if (AbilityCondition_1.default.IN.isEqual(condition)) {
306
560
  // [<some>] and [<some>]
307
561
  if (Array.isArray(valueS) && Array.isArray(valueO)) {
308
562
  is = valueS.some(v => valueO.find(v1 => v1 === v));
@@ -316,41 +570,53 @@ class AbilityRule {
316
570
  is = valueS.includes(valueO);
317
571
  }
318
572
  }
319
- return is ? this.effect : this.effect === 'permit' ? 'deny' : 'permit';
320
- }
321
- extractValues(sub, res, env) {
322
- const [subjectFieldName, _condition, resourceFieldName] = this.matches;
323
- const REGEXP = /^(subject|resource|environment)\./;
324
- // The subject field must be named at «subject.<field-name>»
325
- if (!subjectFieldName.match(/^(subject|environment)\./)) {
326
- throw new Error(`Matches error. The subject field must be named at «subject.<field-name>», but got ${subjectFieldName}`);
573
+ if (AbilityCondition_1.default.NOT_IN.isEqual(condition)) {
574
+ // [<some>] and [<some>]
575
+ if (Array.isArray(valueS) && Array.isArray(valueO)) {
576
+ is = !valueS.some(v => valueO.find(v1 => v1 === v));
577
+ }
578
+ // <some> and [<some>]
579
+ if ((typeof valueS === 'string' || typeof valueS === 'number') && Array.isArray(valueO)) {
580
+ is = !valueO.includes(valueS);
581
+ }
582
+ // [<some>] and <some>
583
+ if ((typeof valueO === 'string' || typeof valueO === 'number') && Array.isArray(valueS)) {
584
+ is = !valueS.includes(valueO);
585
+ }
586
+ }
587
+ this.state = is ? AbilityMatch_1.default.MATCH : AbilityMatch_1.default.MISMATCH;
588
+ return this.state;
589
+ }
590
+ /**
591
+ * Extract values from the resource
592
+ * @param resource - The resource to extract values from
593
+ */
594
+ extractValues(resource) {
595
+ const [subjectPathName, _condition, staticValueOrPathName] = this.matches;
596
+ let leftSideValue;
597
+ let rightSideValue;
598
+ if (resource === null || typeof resource === 'undefined') {
599
+ return [NaN, NaN];
327
600
  }
328
- const sFieldName = subjectFieldName.replace(/^(subject|environment)\./, '');
329
- const subject = typeof sub === 'undefined' || sub === null ? {} : sub;
330
- const resource = typeof res === 'undefined' || res === null ? {} : res;
331
- const sValue = subject
332
- ? this.getDotNotationValue(subjectFieldName.match(/^subject\./)
333
- ? subject
334
- : subjectFieldName.match(/^environment\./)
335
- ? env
336
- : {}, sFieldName)
337
- : subject;
338
- // The resource field name can be «resource».
339
- // In this case the resource be compare as is
340
- if (resourceFieldName === 'resource') {
341
- return [sValue, resource];
601
+ const isPath = (str) => {
602
+ return typeof str === 'string' && str.match(/\./g) !== null;
603
+ };
604
+ if (isPath(subjectPathName)) {
605
+ leftSideValue = this.getDotNotationValue(resource, subjectPathName);
342
606
  }
343
- // Object field name - is a «resource.<field-name>»
344
- if (resource && String(resourceFieldName).match(REGEXP)) {
345
- const oFieldName = String(resourceFieldName).replace(REGEXP, '');
346
- return [sValue, this.getDotNotationValue(resource, oFieldName)];
607
+ if (isPath(staticValueOrPathName)) {
608
+ rightSideValue = this.getDotNotationValue(resource, staticValueOrPathName);
347
609
  }
348
- // The resource field abne can be «<some-value>» only
349
- if (String(resourceFieldName).match(REGEXP) === null) {
350
- return [sValue, resourceFieldName];
610
+ else {
611
+ rightSideValue = staticValueOrPathName;
351
612
  }
352
- return [NaN, NaN];
613
+ return [leftSideValue, rightSideValue];
353
614
  }
615
+ /**
616
+ * Get the value of the object by dot notation
617
+ * @param resource - The object to get the value from
618
+ * @param desc - The dot notation string
619
+ */
354
620
  getDotNotationValue(resource, desc) {
355
621
  const arr = desc.split('.');
356
622
  while (arr.length && resource) {
@@ -374,15 +640,28 @@ class AbilityRule {
374
640
  }
375
641
  return resource;
376
642
  }
643
+ static parse(configOrJson) {
644
+ const config = AbilityParser_1.default.prepareAndValidateConfig(configOrJson, [
645
+ ['id', 'string', false],
646
+ ['name', 'string', true],
647
+ ['matches', 'array', true],
648
+ ]);
649
+ const { name, matches } = config;
650
+ const [leftField, condition, rightField] = matches;
651
+ return new AbilityRule({
652
+ name,
653
+ matches: [leftField, new AbilityCondition_1.default(condition), rightField],
654
+ });
655
+ }
377
656
  /**
378
- * Parsing the rule config object or JSON string\
379
- * of config and returns the AbilityRule class instance
657
+ * Export the rule to config object
380
658
  */
381
- static parse(configOrJson) {
382
- const { name, effect, matches } = typeof configOrJson === 'string'
383
- ? JSON.parse(configOrJson)
384
- : configOrJson;
385
- return new AbilityRule(matches, effect, name);
659
+ export() {
660
+ const [leftField, condition, rightField] = this.matches;
661
+ return {
662
+ name: this.name,
663
+ matches: [leftField, condition.code, rightField],
664
+ };
386
665
  }
387
666
  }
388
667
  exports.AbilityRule = AbilityRule;
@@ -391,7 +670,7 @@ exports["default"] = AbilityRule;
391
670
 
392
671
  /***/ }),
393
672
 
394
- /***/ 31:
673
+ /***/ 402:
395
674
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
396
675
 
397
676
 
@@ -399,107 +678,100 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
399
678
  return (mod && mod.__esModule) ? mod : { "default": mod };
400
679
  };
401
680
  Object.defineProperty(exports, "__esModule", ({ value: true }));
681
+ exports.AbilityRuleSet = void 0;
402
682
  const AbilityRule_1 = __importDefault(__webpack_require__(476));
403
- const AbilityPolicy_1 = __importDefault(__webpack_require__(844));
404
- class AbilityService {
683
+ const AbilityCompare_1 = __importDefault(__webpack_require__(923));
684
+ const AbilityMatch_1 = __importDefault(__webpack_require__(909));
685
+ const AbilityParser_1 = __importDefault(__webpack_require__(189));
686
+ class AbilityRuleSet {
687
+ state = AbilityMatch_1.default.PENDING;
405
688
  /**
406
- * Create the rule to compare
407
- *
408
- * @param ruleName {string} - The rule name
409
- * @param effect {AbilityRuleStatus} - Return value
410
- * @param matches {AbilityRuleMatches} - The matching rule he matching rule can be on of the format:
411
- * \
412
- * For example, be compared two's data\
413
- * \
414
- * _The subject_
415
- * ```json
416
- * {"userID": "1", "userDepartament": "NBC"}
417
- * ```
418
- * and _The resource_
419
- * ```json
420
- * {"departamentID": "154", "departamentName": "NBC"}
421
- * ```
422
- * \
423
- * Now we can make the matching rule:
424
- * ```json
425
- * ["subject.userDepartament", "=", "resource.departamentName"]
426
- * ```
427
- *
428
- * \
429
- * **Example 2.**\
430
- * In this case will be compared resource and string:
431
- * \
432
- * _The subject_
433
- * ```json
434
- * {"userID": "1", "userDepartament": "NBC"}
435
- * ```
436
- * and _The resource_ will be «undefined».\
437
- * Now we can make the matching rule:
438
- * ```json
439
- * ["subject.userDepartament", "=", "NBC"]
440
- * ```
441
- * \
442
- * **Example 3.**\
443
- * In this case will be compared resource and array of string:\
444
- * \
445
- * _The subject_
446
- * ```json
447
- * {"userID": "1", "userDepartament": "NBC"}
448
- * ```
449
- * and _The resource_
450
- * ```json
451
- * ["FOX", "NBC", "AONE"]
452
- * ```
453
- * \
454
- * Now we can make the matching rule:
455
- * ```json
456
- * ["subject.userDepartament", "=", "resource"]
457
- * ```
458
- * **Note: In this rule whe set the resource field as the «resource» string.\
459
- * This means that we will compare the entire resource as a whole,\
460
- * and not search for it by field name.**
689
+ * List of rules
461
690
  */
462
- createRule(...args) {
463
- return new AbilityRule_1.default(...args);
464
- }
691
+ rules = [];
692
+ /**
693
+ * Rules compare method.\
694
+ * For the «and» method the rule will be permitted if all\
695
+ * rules will be returns «permit» status and for the «or» - if\
696
+ * one of the rules returns as «permit»
697
+ */
698
+ compareMethod = AbilityCompare_1.default.AND;
465
699
  /**
466
- * Create the Policy class instance
700
+ * Group name
467
701
  */
468
- createPolicy(...args) {
469
- return new AbilityPolicy_1.default(...args);
470
- }
471
- checkPolicies(policiesResult, compareMethod) {
472
- const deniedRules = [];
473
- const deniedPolicies = [];
474
- const statuses = [];
475
- policiesResult.forEach(policyResult => {
476
- statuses.push(policyResult.permission);
477
- if (policyResult.permission === 'deny') {
478
- policyResult.deniedRules.forEach(rule => {
479
- deniedRules.push(rule);
480
- });
481
- policyResult.deniedRules.forEach(st => {
482
- deniedRules.push(st);
483
- });
702
+ name;
703
+ /**
704
+ * Group ID
705
+ */
706
+ id;
707
+ constructor(params) {
708
+ const { name, id } = params || {};
709
+ this.name = name || Symbol('name');
710
+ this.id = id || Symbol('id');
711
+ }
712
+ addRule(rule, compareMethod) {
713
+ this.rules.push(rule);
714
+ this.compareMethod = compareMethod;
715
+ return this;
716
+ }
717
+ addRules(rules, compareMethod) {
718
+ rules.forEach(rule => this.addRule(rule, compareMethod));
719
+ return this;
720
+ }
721
+ check(resources) {
722
+ this.state = AbilityMatch_1.default.MISMATCH;
723
+ if (!this.rules.length) {
724
+ return this.state;
725
+ }
726
+ const ruleCheckStates = this.rules.reduce((collect, rule) => {
727
+ return collect.concat(rule.check(resources));
728
+ }, []);
729
+ if (AbilityCompare_1.default.AND.isEqual(this.compareMethod)) {
730
+ if (ruleCheckStates.every(ruleState => AbilityMatch_1.default.MATCH.isEqual(ruleState))) {
731
+ this.state = AbilityMatch_1.default.MATCH;
484
732
  }
733
+ }
734
+ if (AbilityCompare_1.default.OR.isEqual(this.compareMethod)) {
735
+ if (ruleCheckStates.some(ruleState => AbilityMatch_1.default.MATCH.isEqual(ruleState))) {
736
+ this.state = AbilityMatch_1.default.MATCH;
737
+ }
738
+ }
739
+ return this.state;
740
+ }
741
+ /**
742
+ * Parse the config JSON format to Group class instance
743
+ */
744
+ static parse(configOrJson) {
745
+ const config = AbilityParser_1.default.prepareAndValidateConfig(configOrJson, [
746
+ ['id', 'string', false],
747
+ ['name', 'string', true],
748
+ ['compareMethod', 'number', true],
749
+ ['rules', 'array', true],
750
+ ]);
751
+ const { id, name, rules, compareMethod } = config;
752
+ const ruleSet = new AbilityRuleSet({
753
+ name,
754
+ id,
485
755
  });
486
- const permission = statuses[compareMethod === 'and' ? 'every' : 'some'](status => status === 'permit')
487
- ? 'permit'
488
- : 'deny';
756
+ ruleSet.compareMethod = new AbilityCompare_1.default(compareMethod);
757
+ // Adding rules if exists
758
+ if (rules && rules.length > 0) {
759
+ const abilityRules = rules.map(ruleConfig => AbilityRule_1.default.parse(ruleConfig));
760
+ ruleSet.addRules(abilityRules, ruleSet.compareMethod);
761
+ }
762
+ return ruleSet;
763
+ }
764
+ export() {
489
765
  return {
490
- permission,
491
- deniedRules,
492
- deniedPolicies,
766
+ id: this.id.toString(),
767
+ name: this.name.toString(),
768
+ compareMethod: this.compareMethod.code,
769
+ rules: this.rules.map(rule => rule.export()),
493
770
  };
494
771
  }
495
- enforcePolicies(policiesResult, compareMethod) {
496
- const { permission, deniedRules } = this.checkPolicies(policiesResult, compareMethod);
497
- if (permission === 'deny') {
498
- throw new Error(`Permission denied. ${deniedRules[0].getName().toString()}`);
499
- }
500
- }
501
772
  }
502
- exports["default"] = AbilityService;
773
+ exports.AbilityRuleSet = AbilityRuleSet;
774
+ exports["default"] = AbilityRuleSet;
503
775
 
504
776
 
505
777
  /***/ }),
@@ -522,15 +794,18 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
522
794
  var __exportStar = (this && this.__exportStar) || function(m, exports) {
523
795
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
524
796
  };
525
- var __importDefault = (this && this.__importDefault) || function (mod) {
526
- return (mod && mod.__esModule) ? mod : { "default": mod };
527
- };
528
797
  Object.defineProperty(exports, "__esModule", ({ value: true }));
529
- const AbilityService_1 = __importDefault(__webpack_require__(31));
798
+ __exportStar(__webpack_require__(19), exports);
799
+ __exportStar(__webpack_require__(923), exports);
800
+ __exportStar(__webpack_require__(261), exports);
801
+ __exportStar(__webpack_require__(122), exports);
802
+ __exportStar(__webpack_require__(909), exports);
803
+ __exportStar(__webpack_require__(189), exports);
530
804
  __exportStar(__webpack_require__(844), exports);
531
- __exportStar(__webpack_require__(31), exports);
805
+ __exportStar(__webpack_require__(277), exports);
806
+ __exportStar(__webpack_require__(668), exports);
532
807
  __exportStar(__webpack_require__(476), exports);
533
- exports["default"] = AbilityService_1.default;
808
+ __exportStar(__webpack_require__(402), exports);
534
809
 
535
810
 
536
811
  /***/ })