@zudojs/permissions 0.1.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +350 -14
- package/dist/ability/ability.core.d.ts +2 -1
- package/dist/ability/ability.core.js +34 -9
- package/dist/cache/cache.core.d.ts +25 -5
- package/dist/cache/cache.core.js +77 -20
- package/dist/cache/index.d.ts +1 -1
- package/dist/conditions/conditions.core.d.ts +18 -2
- package/dist/conditions/conditions.core.js +46 -6
- package/dist/conditions/index.d.ts +1 -1
- package/dist/conditions/index.js +1 -1
- package/dist/evaluator/authorizationEngine.d.ts +48 -6
- package/dist/evaluator/authorizationEngine.js +156 -16
- package/dist/evaluator/evaluator.core.d.ts +28 -4
- package/dist/evaluator/evaluator.core.js +259 -39
- package/dist/evaluator/evaluator.explain.d.ts +6 -3
- package/dist/evaluator/evaluator.explain.js +8 -71
- package/dist/evaluator/evaluator.pipeline.d.ts +70 -9
- package/dist/evaluator/evaluator.pipeline.js +194 -49
- package/dist/evaluator/index.d.ts +4 -2
- package/dist/evaluator/index.js +4 -1
- package/dist/http/httpHelpers.d.ts +28 -12
- package/dist/http/httpHelpers.js +33 -6
- package/dist/http/httpMiddleware.core.d.ts +54 -13
- package/dist/http/httpMiddleware.core.js +72 -28
- package/dist/http/index.d.ts +7 -3
- package/dist/http/index.js +7 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/observability.core.d.ts +32 -12
- package/dist/observability/observability.core.js +41 -20
- package/dist/permission/index.d.ts +2 -2
- package/dist/permission/index.js +1 -1
- package/dist/permission/permission.core.d.ts +13 -2
- package/dist/permission/permission.core.js +29 -21
- package/dist/permission/permissionRegistry.d.ts +27 -33
- package/dist/permission/permissionRegistry.js +59 -41
- package/dist/permissionErrors/index.d.ts +1 -1
- package/dist/permissionErrors/index.js +1 -1
- package/dist/permissionErrors/permissionError.access.d.ts +24 -0
- package/dist/permissionErrors/permissionError.access.js +25 -5
- package/dist/permissionErrors/permissionError.types.d.ts +1 -1
- package/dist/permissionErrors/permissionError.types.js +1 -1
- package/dist/permissionTypes/index.d.ts +2 -2
- package/dist/permissionTypes/index.js +1 -1
- package/dist/permissionTypes/policyTypes.d.ts +35 -2
- package/dist/permissionTypes/ruleTypes.d.ts +38 -5
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/index.js +1 -1
- package/dist/policy/policyRegistry.d.ts +16 -23
- package/dist/policy/policyRegistry.js +32 -24
- package/dist/role/index.d.ts +2 -2
- package/dist/role/index.js +1 -1
- package/dist/role/roleHierarchy.d.ts +34 -4
- package/dist/role/roleHierarchy.js +48 -23
- package/dist/role/roleRegistry.d.ts +25 -25
- package/dist/role/roleRegistry.js +46 -24
- package/dist/rule/index.d.ts +2 -2
- package/dist/rule/index.js +2 -2
- package/dist/rule/rule.core.d.ts +39 -10
- package/dist/rule/rule.core.js +95 -38
- package/dist/rule/ruleCompiler.d.ts +14 -5
- package/dist/rule/ruleCompiler.js +46 -30
- package/dist/utils/utils.helper.d.ts +8 -0
- package/dist/utils/utils.helper.js +16 -14
- package/package.json +27 -16
- package/dist/.tsbuildinfo +0 -1
- package/dist/ability/ability.core.d.ts.map +0 -1
- package/dist/ability/ability.core.js.map +0 -1
- package/dist/ability/index.d.ts.map +0 -1
- package/dist/ability/index.js.map +0 -1
- package/dist/actor/actor.core.d.ts.map +0 -1
- package/dist/actor/actor.core.js.map +0 -1
- package/dist/actor/index.d.ts.map +0 -1
- package/dist/actor/index.js.map +0 -1
- package/dist/cache/cache.core.d.ts.map +0 -1
- package/dist/cache/cache.core.js.map +0 -1
- package/dist/cache/index.d.ts.map +0 -1
- package/dist/cache/index.js.map +0 -1
- package/dist/conditions/conditions.core.d.ts.map +0 -1
- package/dist/conditions/conditions.core.js.map +0 -1
- package/dist/conditions/index.d.ts.map +0 -1
- package/dist/conditions/index.js.map +0 -1
- package/dist/evaluator/authorizationEngine.d.ts.map +0 -1
- package/dist/evaluator/authorizationEngine.js.map +0 -1
- package/dist/evaluator/evaluator.core.d.ts.map +0 -1
- package/dist/evaluator/evaluator.core.js.map +0 -1
- package/dist/evaluator/evaluator.explain.d.ts.map +0 -1
- package/dist/evaluator/evaluator.explain.js.map +0 -1
- package/dist/evaluator/evaluator.pipeline.d.ts.map +0 -1
- package/dist/evaluator/evaluator.pipeline.js.map +0 -1
- package/dist/evaluator/index.d.ts.map +0 -1
- package/dist/evaluator/index.js.map +0 -1
- package/dist/http/httpHelpers.d.ts.map +0 -1
- package/dist/http/httpHelpers.js.map +0 -1
- package/dist/http/httpMiddleware.core.d.ts.map +0 -1
- package/dist/http/httpMiddleware.core.js.map +0 -1
- package/dist/http/httpTypes.d.ts.map +0 -1
- package/dist/http/httpTypes.js.map +0 -1
- package/dist/http/index.d.ts.map +0 -1
- package/dist/http/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/observability/index.d.ts.map +0 -1
- package/dist/observability/index.js.map +0 -1
- package/dist/observability/observability.core.d.ts.map +0 -1
- package/dist/observability/observability.core.js.map +0 -1
- package/dist/permission/index.d.ts.map +0 -1
- package/dist/permission/index.js.map +0 -1
- package/dist/permission/permission.core.d.ts.map +0 -1
- package/dist/permission/permission.core.js.map +0 -1
- package/dist/permission/permissionRegistry.d.ts.map +0 -1
- package/dist/permission/permissionRegistry.js.map +0 -1
- package/dist/permissionErrors/index.d.ts.map +0 -1
- package/dist/permissionErrors/index.js.map +0 -1
- package/dist/permissionErrors/permissionError.access.d.ts.map +0 -1
- package/dist/permissionErrors/permissionError.access.js.map +0 -1
- package/dist/permissionErrors/permissionError.base.d.ts.map +0 -1
- package/dist/permissionErrors/permissionError.base.js.map +0 -1
- package/dist/permissionErrors/permissionError.types.d.ts.map +0 -1
- package/dist/permissionErrors/permissionError.types.js.map +0 -1
- package/dist/permissionErrors/permissionError.validation.d.ts.map +0 -1
- package/dist/permissionErrors/permissionError.validation.js.map +0 -1
- package/dist/permissionTypes/index.d.ts.map +0 -1
- package/dist/permissionTypes/index.js.map +0 -1
- package/dist/permissionTypes/permissionActor.d.ts.map +0 -1
- package/dist/permissionTypes/permissionActor.js.map +0 -1
- package/dist/permissionTypes/policyTypes.d.ts.map +0 -1
- package/dist/permissionTypes/policyTypes.js.map +0 -1
- package/dist/permissionTypes/ruleTypes.d.ts.map +0 -1
- package/dist/permissionTypes/ruleTypes.js.map +0 -1
- package/dist/policy/index.d.ts.map +0 -1
- package/dist/policy/index.js.map +0 -1
- package/dist/policy/policyRegistry.d.ts.map +0 -1
- package/dist/policy/policyRegistry.js.map +0 -1
- package/dist/resolvers/index.d.ts.map +0 -1
- package/dist/resolvers/index.js.map +0 -1
- package/dist/resolvers/resolvers.core.d.ts.map +0 -1
- package/dist/resolvers/resolvers.core.js.map +0 -1
- package/dist/role/index.d.ts.map +0 -1
- package/dist/role/index.js.map +0 -1
- package/dist/role/roleHierarchy.d.ts.map +0 -1
- package/dist/role/roleHierarchy.js.map +0 -1
- package/dist/role/roleRegistry.d.ts.map +0 -1
- package/dist/role/roleRegistry.js.map +0 -1
- package/dist/rule/index.d.ts.map +0 -1
- package/dist/rule/index.js.map +0 -1
- package/dist/rule/rule.core.d.ts.map +0 -1
- package/dist/rule/rule.core.js.map +0 -1
- package/dist/rule/ruleCompiler.d.ts.map +0 -1
- package/dist/rule/ruleCompiler.js.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/utils.helper.d.ts.map +0 -1
- package/dist/utils/utils.helper.js.map +0 -1
|
@@ -3,62 +3,84 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module role/roleRegistry
|
|
5
5
|
*/
|
|
6
|
-
import { DuplicateRoleError, InvalidRoleError, } from "../permissionErrors/index.js";
|
|
6
|
+
import { DuplicateRoleError, InvalidRoleError, RoleNotFoundError, } from "../permissionErrors/index.js";
|
|
7
|
+
import { isValidPermission } from "../permission/permission.core.js";
|
|
8
|
+
/**
|
|
9
|
+
* Copy a role definition so it no longer shares arrays with the caller.
|
|
10
|
+
*
|
|
11
|
+
* `Object.freeze({ ...definition })` froze the wrapper and kept the caller's
|
|
12
|
+
* `permissions`, `inherits` and `rules` arrays by reference — so pushing
|
|
13
|
+
* `"*:*"` onto an array *after* `define()` had validated it widened the role
|
|
14
|
+
* in silence.
|
|
15
|
+
*/
|
|
16
|
+
export function freezeRoleDefinition(definition) {
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
...definition,
|
|
19
|
+
permissions: Object.freeze([...definition.permissions]),
|
|
20
|
+
...(definition.inherits
|
|
21
|
+
? { inherits: Object.freeze([...definition.inherits]) }
|
|
22
|
+
: {}),
|
|
23
|
+
...(definition.rules ? { rules: Object.freeze([...definition.rules]) } : {}),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
7
26
|
/**
|
|
8
27
|
* Create a role registry.
|
|
28
|
+
*
|
|
29
|
+
* Pass it straight to `createPermissionEngine({ roles: registry })`.
|
|
9
30
|
*/
|
|
10
31
|
export function createRoleRegistry(options) {
|
|
11
32
|
const roles = new Map();
|
|
12
33
|
const allowOverride = options?.allowOverride ?? false;
|
|
34
|
+
const validatePermissions = options?.validatePermissions ?? true;
|
|
13
35
|
return {
|
|
14
36
|
/**
|
|
15
37
|
* Register a role definition.
|
|
38
|
+
*
|
|
39
|
+
* A role with no permissions of its own is valid: a role that exists only
|
|
40
|
+
* to combine others through `inherits` is the normal way to build a
|
|
41
|
+
* hierarchy.
|
|
16
42
|
*/
|
|
17
43
|
define(definition) {
|
|
18
44
|
if (!definition.name || definition.name.trim() === "") {
|
|
19
45
|
throw new InvalidRoleError("Role name cannot be empty");
|
|
20
46
|
}
|
|
21
|
-
if (!
|
|
22
|
-
throw new InvalidRoleError(`Role "${definition.name}" must
|
|
47
|
+
if (!Array.isArray(definition.permissions)) {
|
|
48
|
+
throw new InvalidRoleError(`Role "${definition.name}" must declare a permissions array`);
|
|
49
|
+
}
|
|
50
|
+
if (validatePermissions) {
|
|
51
|
+
for (const permission of definition.permissions) {
|
|
52
|
+
if (!isValidPermission(permission)) {
|
|
53
|
+
throw new InvalidRoleError(`Role "${definition.name}" grants "${permission}", which is not a ` +
|
|
54
|
+
`valid "resource:action" permission`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
23
57
|
}
|
|
24
58
|
if (roles.has(definition.name) && !allowOverride) {
|
|
25
59
|
throw new DuplicateRoleError(definition.name);
|
|
26
60
|
}
|
|
27
|
-
roles.set(definition.name,
|
|
61
|
+
roles.set(definition.name, freezeRoleDefinition(definition));
|
|
28
62
|
},
|
|
29
|
-
/**
|
|
30
|
-
* Get a role by name.
|
|
31
|
-
*/
|
|
32
63
|
get(name) {
|
|
33
64
|
return roles.get(name);
|
|
34
65
|
},
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
66
|
+
require(name) {
|
|
67
|
+
const role = roles.get(name);
|
|
68
|
+
if (!role)
|
|
69
|
+
throw new RoleNotFoundError(name);
|
|
70
|
+
return role;
|
|
71
|
+
},
|
|
38
72
|
has(name) {
|
|
39
73
|
return roles.has(name);
|
|
40
74
|
},
|
|
41
|
-
/**
|
|
42
|
-
* Get all registered role names.
|
|
43
|
-
*/
|
|
44
75
|
names() {
|
|
45
|
-
return
|
|
76
|
+
return [...roles.keys()];
|
|
46
77
|
},
|
|
47
|
-
/**
|
|
48
|
-
* Get all registered role definitions.
|
|
49
|
-
*/
|
|
50
78
|
all() {
|
|
51
|
-
return
|
|
79
|
+
return [...roles.values()];
|
|
52
80
|
},
|
|
53
|
-
/**
|
|
54
|
-
* Remove a role from the registry.
|
|
55
|
-
*/
|
|
56
81
|
remove(name) {
|
|
57
82
|
return roles.delete(name);
|
|
58
83
|
},
|
|
59
|
-
/**
|
|
60
|
-
* Clear all registered roles.
|
|
61
|
-
*/
|
|
62
84
|
clear() {
|
|
63
85
|
roles.clear();
|
|
64
86
|
},
|
package/dist/rule/index.d.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module rule
|
|
5
5
|
*/
|
|
6
|
-
export { ruleMatches, evaluateRules } from "./rule.core.js";
|
|
7
|
-
export { compileRules, findMatchingRules } from "./ruleCompiler.js";
|
|
6
|
+
export { ruleMatches, patternStrMatches, evaluateRules, evaluateRulesSync, } from "./rule.core.js";
|
|
7
|
+
export { compileRules, findMatchingRules, type RuleIndex, } from "./ruleCompiler.js";
|
|
8
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/rule/index.js
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module rule
|
|
5
5
|
*/
|
|
6
|
-
export { ruleMatches, evaluateRules } from "./rule.core.js";
|
|
7
|
-
export { compileRules, findMatchingRules } from "./ruleCompiler.js";
|
|
6
|
+
export { ruleMatches, patternStrMatches, evaluateRules, evaluateRulesSync, } from "./rule.core.js";
|
|
7
|
+
export { compileRules, findMatchingRules, } from "./ruleCompiler.js";
|
|
8
8
|
//# sourceMappingURL=index.js.map
|
package/dist/rule/rule.core.d.ts
CHANGED
|
@@ -1,24 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rule matching and
|
|
2
|
+
* Rule matching and evaluation for permission decisions.
|
|
3
3
|
*
|
|
4
4
|
* @module rule/rule
|
|
5
5
|
*/
|
|
6
|
-
import type { PermissionRule, Permission } from "../permissionTypes/index.js";
|
|
6
|
+
import type { PermissionRule, Permission, PermissionContext, RuleCombiningAlgorithm, RuleEvaluation } from "../permissionTypes/index.js";
|
|
7
7
|
/**
|
|
8
8
|
* Check if a rule matches a target permission.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Matching is about resource and action only. Whether the rule *applies* also
|
|
11
|
+
* depends on its condition, which needs a context and is therefore evaluated
|
|
12
|
+
* by {@link evaluateRules}.
|
|
12
13
|
*/
|
|
13
14
|
export declare function ruleMatches(rule: PermissionRule, target: Permission): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Check if a single pattern string matches a target, supporting wildcards.
|
|
17
|
+
*
|
|
18
|
+
* Two forms are supported, and they are the same two the permission matcher
|
|
19
|
+
* supports, so a pattern means the same thing wherever it is written:
|
|
20
|
+
* `*` — matches anything
|
|
21
|
+
* `billing.*` — matches `billing` and any `billing.…` namespace
|
|
22
|
+
*/
|
|
23
|
+
export declare function patternStrMatches(pattern: string, target: string): boolean;
|
|
14
24
|
/**
|
|
15
25
|
* Evaluate a set of rules against a target permission.
|
|
16
26
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
27
|
+
* A rule with a condition applies only when that condition returns `true`,
|
|
28
|
+
* which is what makes ABAC work: `{ effect: "allow", condition: isOwner() }`
|
|
29
|
+
* has to mean "owners may", not "anyone may". Conditions are async, so this
|
|
30
|
+
* function is too.
|
|
31
|
+
*
|
|
32
|
+
* A condition that throws is treated as unmet — an authorization check fails
|
|
33
|
+
* closed. A rule that carries a condition when no context was supplied is
|
|
34
|
+
* skipped for the same reason.
|
|
35
|
+
*
|
|
36
|
+
* Default combining algorithm: `deny-overrides`. Any applicable deny wins,
|
|
37
|
+
* whatever its priority.
|
|
38
|
+
*/
|
|
39
|
+
export declare function evaluateRules(rules: readonly PermissionRule[], target: Permission, context?: PermissionContext, options?: {
|
|
40
|
+
readonly algorithm?: RuleCombiningAlgorithm;
|
|
41
|
+
readonly onConditionError?: (rule: PermissionRule, error: unknown) => void;
|
|
42
|
+
}): Promise<RuleEvaluation>;
|
|
43
|
+
/**
|
|
44
|
+
* Evaluate rules that carry no conditions.
|
|
45
|
+
*
|
|
46
|
+
* Synchronous, for callers that build their own condition-free rule sets.
|
|
47
|
+
* Any rule with a condition is skipped, because there is no way to evaluate
|
|
48
|
+
* one without awaiting it — use {@link evaluateRules} for those.
|
|
19
49
|
*/
|
|
20
|
-
export declare function
|
|
21
|
-
readonly
|
|
22
|
-
|
|
23
|
-
};
|
|
50
|
+
export declare function evaluateRulesSync(rules: readonly PermissionRule[], target: Permission, options?: {
|
|
51
|
+
readonly algorithm?: RuleCombiningAlgorithm;
|
|
52
|
+
}): RuleEvaluation;
|
|
24
53
|
//# sourceMappingURL=rule.core.d.ts.map
|
package/dist/rule/rule.core.js
CHANGED
|
@@ -1,45 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rule matching and
|
|
2
|
+
* Rule matching and evaluation for permission decisions.
|
|
3
3
|
*
|
|
4
4
|
* @module rule/rule
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* Check if a rule matches a target permission.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Matching is about resource and action only. Whether the rule *applies* also
|
|
10
|
+
* depends on its condition, which needs a context and is therefore evaluated
|
|
11
|
+
* by {@link evaluateRules}.
|
|
11
12
|
*/
|
|
12
13
|
export function ruleMatches(rule, target) {
|
|
13
|
-
return (
|
|
14
|
-
|
|
14
|
+
return (patternListMatches(rule.resource, target.resource) &&
|
|
15
|
+
patternListMatches(rule.action, target.action));
|
|
15
16
|
}
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
*/
|
|
19
|
-
function resourceMatches(pattern, target) {
|
|
17
|
+
/** Check if any pattern in a rule field matches a target segment. */
|
|
18
|
+
function patternListMatches(pattern, target) {
|
|
20
19
|
if (Array.isArray(pattern)) {
|
|
21
|
-
return pattern.some((
|
|
22
|
-
}
|
|
23
|
-
return patternStrMatches(pattern, target);
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Check if a rule's action pattern matches a target action.
|
|
27
|
-
*/
|
|
28
|
-
function actionMatches(pattern, target) {
|
|
29
|
-
if (Array.isArray(pattern)) {
|
|
30
|
-
return pattern.some((p) => patternStrMatches(p, target));
|
|
20
|
+
return pattern.some((entry) => patternStrMatches(entry, target));
|
|
31
21
|
}
|
|
32
22
|
return patternStrMatches(pattern, target);
|
|
33
23
|
}
|
|
34
24
|
/**
|
|
35
25
|
* Check if a single pattern string matches a target, supporting wildcards.
|
|
26
|
+
*
|
|
27
|
+
* Two forms are supported, and they are the same two the permission matcher
|
|
28
|
+
* supports, so a pattern means the same thing wherever it is written:
|
|
29
|
+
* `*` — matches anything
|
|
30
|
+
* `billing.*` — matches `billing` and any `billing.…` namespace
|
|
36
31
|
*/
|
|
37
|
-
function patternStrMatches(pattern, target) {
|
|
32
|
+
export function patternStrMatches(pattern, target) {
|
|
38
33
|
if (pattern === "*")
|
|
39
34
|
return true;
|
|
40
35
|
if (pattern === target)
|
|
41
36
|
return true;
|
|
42
|
-
// Namespace wildcard: "billing.*" matches "billing.invoice"
|
|
43
37
|
if (pattern.endsWith(".*")) {
|
|
44
38
|
const prefix = pattern.slice(0, -2);
|
|
45
39
|
return target === prefix || target.startsWith(`${prefix}.`);
|
|
@@ -49,21 +43,87 @@ function patternStrMatches(pattern, target) {
|
|
|
49
43
|
/**
|
|
50
44
|
* Evaluate a set of rules against a target permission.
|
|
51
45
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
46
|
+
* A rule with a condition applies only when that condition returns `true`,
|
|
47
|
+
* which is what makes ABAC work: `{ effect: "allow", condition: isOwner() }`
|
|
48
|
+
* has to mean "owners may", not "anyone may". Conditions are async, so this
|
|
49
|
+
* function is too.
|
|
50
|
+
*
|
|
51
|
+
* A condition that throws is treated as unmet — an authorization check fails
|
|
52
|
+
* closed. A rule that carries a condition when no context was supplied is
|
|
53
|
+
* skipped for the same reason.
|
|
54
|
+
*
|
|
55
|
+
* Default combining algorithm: `deny-overrides`. Any applicable deny wins,
|
|
56
|
+
* whatever its priority.
|
|
57
|
+
*/
|
|
58
|
+
export async function evaluateRules(rules, target, context, options) {
|
|
59
|
+
const algorithm = options?.algorithm ?? "deny-overrides";
|
|
60
|
+
const applicable = [];
|
|
61
|
+
for (const rule of rules) {
|
|
62
|
+
if (!ruleMatches(rule, target))
|
|
63
|
+
continue;
|
|
64
|
+
if (rule.condition) {
|
|
65
|
+
if (!context)
|
|
66
|
+
continue;
|
|
67
|
+
let met = false;
|
|
68
|
+
try {
|
|
69
|
+
met = await rule.condition(context);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
options?.onConditionError?.(rule, error);
|
|
73
|
+
met = false;
|
|
74
|
+
}
|
|
75
|
+
if (!met)
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
applicable.push(rule);
|
|
79
|
+
}
|
|
80
|
+
if (applicable.length === 0) {
|
|
81
|
+
return { allowed: false, applicable: [] };
|
|
82
|
+
}
|
|
83
|
+
if (algorithm === "deny-overrides") {
|
|
84
|
+
const deny = applicable.find((rule) => rule.effect === "deny");
|
|
85
|
+
if (deny)
|
|
86
|
+
return { allowed: false, matchedRule: deny, applicable };
|
|
87
|
+
return { allowed: true, matchedRule: applicable[0], applicable };
|
|
88
|
+
}
|
|
89
|
+
const sorted = [...applicable].sort((a, b) => {
|
|
90
|
+
const priorityA = a.priority ?? 0;
|
|
91
|
+
const priorityB = b.priority ?? 0;
|
|
92
|
+
if (priorityA !== priorityB)
|
|
93
|
+
return priorityB - priorityA;
|
|
94
|
+
// Same priority: deny wins.
|
|
95
|
+
if (a.effect === "deny" && b.effect !== "deny")
|
|
96
|
+
return -1;
|
|
97
|
+
if (b.effect === "deny" && a.effect !== "deny")
|
|
98
|
+
return 1;
|
|
99
|
+
return 0;
|
|
100
|
+
});
|
|
101
|
+
const top = sorted[0];
|
|
102
|
+
return { allowed: top.effect === "allow", matchedRule: top, applicable };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Evaluate rules that carry no conditions.
|
|
106
|
+
*
|
|
107
|
+
* Synchronous, for callers that build their own condition-free rule sets.
|
|
108
|
+
* Any rule with a condition is skipped, because there is no way to evaluate
|
|
109
|
+
* one without awaiting it — use {@link evaluateRules} for those.
|
|
54
110
|
*/
|
|
55
|
-
export function
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
111
|
+
export function evaluateRulesSync(rules, target, options) {
|
|
112
|
+
const algorithm = options?.algorithm ?? "deny-overrides";
|
|
113
|
+
const applicable = rules.filter((rule) => rule.condition === undefined && ruleMatches(rule, target));
|
|
114
|
+
if (applicable.length === 0)
|
|
115
|
+
return { allowed: false, applicable: [] };
|
|
116
|
+
if (algorithm === "deny-overrides") {
|
|
117
|
+
const deny = applicable.find((rule) => rule.effect === "deny");
|
|
118
|
+
if (deny)
|
|
119
|
+
return { allowed: false, matchedRule: deny, applicable };
|
|
120
|
+
return { allowed: true, matchedRule: applicable[0], applicable };
|
|
59
121
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return priB - priA;
|
|
66
|
-
// Same priority: deny wins
|
|
122
|
+
const sorted = [...applicable].sort((a, b) => {
|
|
123
|
+
const priorityA = a.priority ?? 0;
|
|
124
|
+
const priorityB = b.priority ?? 0;
|
|
125
|
+
if (priorityA !== priorityB)
|
|
126
|
+
return priorityB - priorityA;
|
|
67
127
|
if (a.effect === "deny" && b.effect !== "deny")
|
|
68
128
|
return -1;
|
|
69
129
|
if (b.effect === "deny" && a.effect !== "deny")
|
|
@@ -71,9 +131,6 @@ export function evaluateRules(rules, target) {
|
|
|
71
131
|
return 0;
|
|
72
132
|
});
|
|
73
133
|
const top = sorted[0];
|
|
74
|
-
return {
|
|
75
|
-
allowed: top.effect === "allow",
|
|
76
|
-
matchedRule: top,
|
|
77
|
-
};
|
|
134
|
+
return { allowed: top.effect === "allow", matchedRule: top, applicable };
|
|
78
135
|
}
|
|
79
136
|
//# sourceMappingURL=rule.core.js.map
|
|
@@ -4,16 +4,22 @@
|
|
|
4
4
|
* @module rule/ruleCompiler
|
|
5
5
|
*/
|
|
6
6
|
import type { PermissionRule, Permission } from "../permissionTypes/index.js";
|
|
7
|
-
/** Compiled rule index for
|
|
8
|
-
interface RuleIndex {
|
|
7
|
+
/** Compiled rule index for fast lookup by resource:action. */
|
|
8
|
+
export interface RuleIndex {
|
|
9
9
|
/** Exact matches: "post:update" → rules. */
|
|
10
10
|
readonly exact: ReadonlyMap<string, readonly PermissionRule[]>;
|
|
11
|
-
/** Resource wildcards: "post:*" → rules. */
|
|
11
|
+
/** Resource wildcards: "post:*" → rules, keyed by resource. */
|
|
12
12
|
readonly resourceWildcard: ReadonlyMap<string, readonly PermissionRule[]>;
|
|
13
|
-
/** Action wildcards: "*:read" → rules. */
|
|
13
|
+
/** Action wildcards: "*:read" → rules, keyed by action. */
|
|
14
14
|
readonly actionWildcard: ReadonlyMap<string, readonly PermissionRule[]>;
|
|
15
15
|
/** Global wildcards: "*:*" → rules. */
|
|
16
16
|
readonly globalWildcard: readonly PermissionRule[];
|
|
17
|
+
/**
|
|
18
|
+
* Rules using a namespace wildcard (`billing.*`), which no exact key can
|
|
19
|
+
* index. Scanned linearly — small in practice, and leaving them out of the
|
|
20
|
+
* index entirely is what made them silently unmatchable.
|
|
21
|
+
*/
|
|
22
|
+
readonly patterned: readonly PermissionRule[];
|
|
17
23
|
}
|
|
18
24
|
/**
|
|
19
25
|
* Compile a set of rules into an optimized index.
|
|
@@ -21,7 +27,10 @@ interface RuleIndex {
|
|
|
21
27
|
export declare function compileRules(rules: readonly PermissionRule[]): RuleIndex;
|
|
22
28
|
/**
|
|
23
29
|
* Find all rules from the compiled index that match a target permission.
|
|
30
|
+
*
|
|
31
|
+
* The result is de-duplicated: a rule listing several resources or actions is
|
|
32
|
+
* indexed under each, and returning it more than once would let one rule
|
|
33
|
+
* count twice when the decision is combined.
|
|
24
34
|
*/
|
|
25
35
|
export declare function findMatchingRules(index: RuleIndex, target: Permission): readonly PermissionRule[];
|
|
26
|
-
export {};
|
|
27
36
|
//# sourceMappingURL=ruleCompiler.d.ts.map
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module rule/ruleCompiler
|
|
5
5
|
*/
|
|
6
|
+
import { ruleMatches } from "./rule.core.js";
|
|
7
|
+
/** True when a pattern needs a linear scan rather than a map lookup. */
|
|
8
|
+
function isPatterned(pattern) {
|
|
9
|
+
return pattern !== "*" && pattern.endsWith(".*");
|
|
10
|
+
}
|
|
6
11
|
/**
|
|
7
12
|
* Compile a set of rules into an optimized index.
|
|
8
13
|
*/
|
|
@@ -11,63 +16,74 @@ export function compileRules(rules) {
|
|
|
11
16
|
const resourceWildcard = new Map();
|
|
12
17
|
const actionWildcard = new Map();
|
|
13
18
|
const globalWildcard = [];
|
|
19
|
+
const patterned = [];
|
|
20
|
+
const push = (map, key, rule) => {
|
|
21
|
+
const bucket = map.get(key);
|
|
22
|
+
if (bucket)
|
|
23
|
+
bucket.push(rule);
|
|
24
|
+
else
|
|
25
|
+
map.set(key, [rule]);
|
|
26
|
+
};
|
|
14
27
|
for (const rule of rules) {
|
|
15
28
|
const resources = normalizeToArray(rule.resource);
|
|
16
29
|
const actions = normalizeToArray(rule.action);
|
|
30
|
+
let indexed = false;
|
|
17
31
|
for (const resource of resources) {
|
|
18
32
|
for (const action of actions) {
|
|
33
|
+
if (isPatterned(resource) || isPatterned(action)) {
|
|
34
|
+
if (!indexed) {
|
|
35
|
+
patterned.push(rule);
|
|
36
|
+
indexed = true;
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
19
40
|
if (resource === "*" && action === "*") {
|
|
20
41
|
globalWildcard.push(rule);
|
|
21
42
|
}
|
|
22
43
|
else if (resource === "*") {
|
|
23
|
-
|
|
24
|
-
if (!actionWildcard.has(key))
|
|
25
|
-
actionWildcard.set(key, []);
|
|
26
|
-
actionWildcard.get(key).push(rule);
|
|
44
|
+
push(actionWildcard, action, rule);
|
|
27
45
|
}
|
|
28
46
|
else if (action === "*") {
|
|
29
|
-
|
|
30
|
-
if (!resourceWildcard.has(key))
|
|
31
|
-
resourceWildcard.set(key, []);
|
|
32
|
-
resourceWildcard.get(key).push(rule);
|
|
47
|
+
push(resourceWildcard, resource, rule);
|
|
33
48
|
}
|
|
34
49
|
else {
|
|
35
|
-
|
|
36
|
-
if (!exact.has(key))
|
|
37
|
-
exact.set(key, []);
|
|
38
|
-
exact.get(key).push(rule);
|
|
50
|
+
push(exact, `${resource}:${action}`, rule);
|
|
39
51
|
}
|
|
40
52
|
}
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
|
-
|
|
44
|
-
exact,
|
|
45
|
-
resourceWildcard,
|
|
46
|
-
actionWildcard,
|
|
47
|
-
globalWildcard,
|
|
48
|
-
};
|
|
49
|
-
return index;
|
|
55
|
+
return { exact, resourceWildcard, actionWildcard, globalWildcard, patterned };
|
|
50
56
|
}
|
|
51
57
|
/**
|
|
52
58
|
* Find all rules from the compiled index that match a target permission.
|
|
59
|
+
*
|
|
60
|
+
* The result is de-duplicated: a rule listing several resources or actions is
|
|
61
|
+
* indexed under each, and returning it more than once would let one rule
|
|
62
|
+
* count twice when the decision is combined.
|
|
53
63
|
*/
|
|
54
64
|
export function findMatchingRules(index, target) {
|
|
55
65
|
const results = [];
|
|
56
|
-
const
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
const add = (rules) => {
|
|
68
|
+
if (!rules)
|
|
69
|
+
return;
|
|
70
|
+
for (const rule of rules) {
|
|
71
|
+
if (seen.has(rule))
|
|
72
|
+
continue;
|
|
73
|
+
seen.add(rule);
|
|
74
|
+
results.push(rule);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
57
77
|
// 1. Exact matches
|
|
58
|
-
|
|
59
|
-
if (exactRules)
|
|
60
|
-
results.push(...exactRules);
|
|
78
|
+
add(index.exact.get(`${target.resource}:${target.action}`));
|
|
61
79
|
// 2. Resource wildcard: "post:*" matches "post:update"
|
|
62
|
-
|
|
63
|
-
if (resWildcard)
|
|
64
|
-
results.push(...resWildcard);
|
|
80
|
+
add(index.resourceWildcard.get(target.resource));
|
|
65
81
|
// 3. Action wildcard: "*:read" matches "post:read"
|
|
66
|
-
|
|
67
|
-
if (actWildcard)
|
|
68
|
-
results.push(...actWildcard);
|
|
82
|
+
add(index.actionWildcard.get(target.action));
|
|
69
83
|
// 4. Global wildcard: "*:*"
|
|
70
|
-
|
|
84
|
+
add(index.globalWildcard);
|
|
85
|
+
// 5. Namespace wildcards, which need a real match test
|
|
86
|
+
add(index.patterned.filter((rule) => ruleMatches(rule, target)));
|
|
71
87
|
return results;
|
|
72
88
|
}
|
|
73
89
|
function normalizeToArray(value) {
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import type { PermissionActor } from "../permissionTypes/index.js";
|
|
7
7
|
/**
|
|
8
8
|
* Create a permission check cache key.
|
|
9
|
+
*
|
|
10
|
+
* @deprecated Use `permissionCacheKey` from `@zudojs/permissions`. Two key
|
|
11
|
+
* formats meant entries written with one were invisible to the invalidation
|
|
12
|
+
* that understood the other; this now delegates to the canonical builder.
|
|
9
13
|
*/
|
|
10
14
|
export declare function createCacheKey(actorId: string, permission: string, resourceId?: string): string;
|
|
11
15
|
/**
|
|
@@ -27,6 +31,10 @@ export declare function extractAction(permission: string): string;
|
|
|
27
31
|
export declare function buildPermission(resource: string, action: string): string;
|
|
28
32
|
/**
|
|
29
33
|
* Create a quick actor object.
|
|
34
|
+
*
|
|
35
|
+
* @deprecated Use `createPermissionActor`. This used to build an actor whose
|
|
36
|
+
* role and permission arrays were not frozen, so two functions with the same
|
|
37
|
+
* job produced objects with different mutability.
|
|
30
38
|
*/
|
|
31
39
|
export declare function createActor(id: string, options?: {
|
|
32
40
|
readonly type?: string;
|
|
@@ -3,12 +3,18 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module utils/utils
|
|
5
5
|
*/
|
|
6
|
+
import { permissionCacheKey } from "../cache/cache.core.js";
|
|
7
|
+
import { createPermissionActor } from "../actor/actor.core.js";
|
|
8
|
+
import { formatPermission, parsePermissionSafe, } from "../permission/permission.core.js";
|
|
6
9
|
/**
|
|
7
10
|
* Create a permission check cache key.
|
|
11
|
+
*
|
|
12
|
+
* @deprecated Use `permissionCacheKey` from `@zudojs/permissions`. Two key
|
|
13
|
+
* formats meant entries written with one were invisible to the invalidation
|
|
14
|
+
* that understood the other; this now delegates to the canonical builder.
|
|
8
15
|
*/
|
|
9
16
|
export function createCacheKey(actorId, permission, resourceId) {
|
|
10
|
-
|
|
11
|
-
return resourceId ? `${base}:${resourceId}` : base;
|
|
17
|
+
return permissionCacheKey(actorId, permission, resourceId);
|
|
12
18
|
}
|
|
13
19
|
/**
|
|
14
20
|
* Extract the resource type from a permission string.
|
|
@@ -17,8 +23,7 @@ export function createCacheKey(actorId, permission, resourceId) {
|
|
|
17
23
|
* @example extractResource("billing.invoice:refund") → "billing.invoice"
|
|
18
24
|
*/
|
|
19
25
|
export function extractResource(permission) {
|
|
20
|
-
|
|
21
|
-
return lastColon > 0 ? permission.slice(0, lastColon) : permission;
|
|
26
|
+
return parsePermissionSafe(permission)?.resource ?? permission;
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
29
|
* Extract the action from a permission string.
|
|
@@ -26,25 +31,22 @@ export function extractResource(permission) {
|
|
|
26
31
|
* @example extractAction("post:update") → "update"
|
|
27
32
|
*/
|
|
28
33
|
export function extractAction(permission) {
|
|
29
|
-
|
|
30
|
-
return lastColon > 0 ? permission.slice(lastColon + 1) : "";
|
|
34
|
+
return parsePermissionSafe(permission)?.action ?? "";
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
37
|
* Build a permission string from resource and action.
|
|
34
38
|
*/
|
|
35
39
|
export function buildPermission(resource, action) {
|
|
36
|
-
return
|
|
40
|
+
return formatPermission(resource, action);
|
|
37
41
|
}
|
|
38
42
|
/**
|
|
39
43
|
* Create a quick actor object.
|
|
44
|
+
*
|
|
45
|
+
* @deprecated Use `createPermissionActor`. This used to build an actor whose
|
|
46
|
+
* role and permission arrays were not frozen, so two functions with the same
|
|
47
|
+
* job produced objects with different mutability.
|
|
40
48
|
*/
|
|
41
49
|
export function createActor(id, options) {
|
|
42
|
-
return
|
|
43
|
-
id,
|
|
44
|
-
type: options?.type,
|
|
45
|
-
roles: options?.roles,
|
|
46
|
-
permissions: options?.permissions,
|
|
47
|
-
deniedPermissions: options?.deniedPermissions,
|
|
48
|
-
});
|
|
50
|
+
return createPermissionActor(id, options);
|
|
49
51
|
}
|
|
50
52
|
//# sourceMappingURL=utils.helper.js.map
|