@zudojs/permissions 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -20
- package/dist/ability/ability.core.js +4 -24
- package/dist/cache/cache.actorDigest.d.ts +26 -0
- package/dist/cache/cache.actorDigest.js +90 -0
- package/dist/cache/cache.core.d.ts +7 -1
- package/dist/cache/cache.core.js +9 -2
- package/dist/cache/index.d.ts +1 -0
- package/dist/cache/index.js +1 -0
- package/dist/evaluator/authorizationEngine.d.ts +20 -3
- package/dist/evaluator/authorizationEngine.js +43 -68
- package/dist/evaluator/engineSupport/authorizationEngine.validation.d.ts +37 -0
- package/dist/evaluator/engineSupport/authorizationEngine.validation.js +90 -0
- package/dist/evaluator/engineSupport/evaluator.cacheKey.d.ts +24 -0
- package/dist/evaluator/engineSupport/evaluator.cacheKey.js +56 -0
- package/dist/evaluator/engineSupport/evaluator.observed.d.ts +18 -0
- package/dist/evaluator/engineSupport/evaluator.observed.js +49 -0
- package/dist/evaluator/engineSupport/index.d.ts +10 -0
- package/dist/evaluator/engineSupport/index.js +10 -0
- package/dist/evaluator/evaluator.core.js +19 -37
- package/dist/evaluator/evaluator.pipeline.d.ts +6 -0
- package/dist/evaluator/evaluator.pipeline.js +25 -3
- package/dist/http/httpMiddleware.core.d.ts +15 -5
- package/dist/http/httpMiddleware.core.js +18 -4
- package/dist/http/httpResource.helper.d.ts +30 -0
- package/dist/http/httpResource.helper.js +32 -0
- package/dist/http/httpTypes.d.ts +17 -4
- package/dist/http/index.d.ts +7 -5
- package/dist/http/index.js +6 -4
- package/dist/permission/index.d.ts +1 -1
- package/dist/permission/index.js +1 -1
- package/dist/permission/permission.core.d.ts +12 -0
- package/dist/permission/permission.core.js +20 -1
- package/dist/policy/policyRegistry.d.ts +12 -0
- package/dist/policy/policyRegistry.js +17 -1
- package/dist/role/roleRegistry.d.ts +9 -0
- package/dist/role/roleRegistry.js +19 -1
- package/dist/rule/rule.core.d.ts +14 -22
- package/dist/rule/rule.core.js +49 -86
- package/dist/rule/rule.pattern.d.ts +44 -0
- package/dist/rule/rule.pattern.js +72 -0
- package/dist/utils/utils.notifier.d.ts +19 -0
- package/dist/utils/utils.notifier.js +31 -0
- package/package.json +2 -10
package/dist/http/index.d.ts
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
* HTTP middleware adapter for @zudojs/permissions.
|
|
3
3
|
*
|
|
4
4
|
* The HTTP types are mirrored locally in `httpTypes.ts` so this package has no
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* dependency on @zudojs/http at all — http sits in a higher architecture tier,
|
|
6
|
+
* so it cannot be a dependency or a peer. The mirror is structural: anything
|
|
7
|
+
* satisfying the real `HttpMiddlewareContext` satisfies the local one, so the
|
|
8
|
+
* middleware composes with the real pipeline. A test runs the guard inside
|
|
9
|
+
* the real `HttpMiddlewarePipeline` to keep the two in step.
|
|
9
10
|
*
|
|
10
11
|
* @module http
|
|
11
12
|
*/
|
|
12
13
|
export { createActorMiddleware, createRequirePermissionMiddleware, authorize, createRequirePermissionsMiddleware, ACTOR_STATE_KEY, DECISION_STATE_KEY, DECISIONS_STATE_KEY, type AuthorizeMiddlewareOptions, type ActorMiddlewareOptions, type RequirePermissionMiddlewareOptions, type RequirePermissionsMiddlewareOptions, } from "./httpMiddleware.core.js";
|
|
13
14
|
export { createForbiddenResponse, createUnauthorizedResponse, createJsonResponse, type DeniedResponseOptions, type PermissionHttpResponse, } from "./httpHelpers.js";
|
|
14
|
-
export
|
|
15
|
+
export { loadResource, RESOURCE_ERROR_DECISION, type ResourceExtractor, type ResourceOutcome, } from "./httpResource.helper.js";
|
|
16
|
+
export type { HttpRequestBag, HttpMiddleware, HttpMiddlewareContext, HttpRequestContext, HttpResponseContext, HttpMiddlewareState, } from "./httpTypes.js";
|
|
15
17
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/http/index.js
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
* HTTP middleware adapter for @zudojs/permissions.
|
|
3
3
|
*
|
|
4
4
|
* The HTTP types are mirrored locally in `httpTypes.ts` so this package has no
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* dependency on @zudojs/http at all — http sits in a higher architecture tier,
|
|
6
|
+
* so it cannot be a dependency or a peer. The mirror is structural: anything
|
|
7
|
+
* satisfying the real `HttpMiddlewareContext` satisfies the local one, so the
|
|
8
|
+
* middleware composes with the real pipeline. A test runs the guard inside
|
|
9
|
+
* the real `HttpMiddlewarePipeline` to keep the two in step.
|
|
9
10
|
*
|
|
10
11
|
* @module http
|
|
11
12
|
*/
|
|
12
13
|
export { createActorMiddleware, createRequirePermissionMiddleware, authorize, createRequirePermissionsMiddleware, ACTOR_STATE_KEY, DECISION_STATE_KEY, DECISIONS_STATE_KEY, } from "./httpMiddleware.core.js";
|
|
13
14
|
export { createForbiddenResponse, createUnauthorizedResponse, createJsonResponse, } from "./httpHelpers.js";
|
|
15
|
+
export { loadResource, RESOURCE_ERROR_DECISION, } from "./httpResource.helper.js";
|
|
14
16
|
//# sourceMappingURL=index.js.map
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module permission
|
|
5
5
|
*/
|
|
6
|
-
export { parsePermission, parsePermissionSafe, isValidPermission, matches, matchesPermission, formatPermission, } from "./permission.core.js";
|
|
6
|
+
export { parsePermission, parsePermissionSafe, isValidPermission, matches, matchesPermission, permissionsOverlap, formatPermission, } from "./permission.core.js";
|
|
7
7
|
export { createPermissionRegistry, type PermissionRegistry, type PermissionRegistryOptions, type RegisteredPermission, } from "./permissionRegistry.js";
|
|
8
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/permission/index.js
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module permission
|
|
5
5
|
*/
|
|
6
|
-
export { parsePermission, parsePermissionSafe, isValidPermission, matches, matchesPermission, formatPermission, } from "./permission.core.js";
|
|
6
|
+
export { parsePermission, parsePermissionSafe, isValidPermission, matches, matchesPermission, permissionsOverlap, formatPermission, } from "./permission.core.js";
|
|
7
7
|
export { createPermissionRegistry, } from "./permissionRegistry.js";
|
|
8
8
|
//# sourceMappingURL=index.js.map
|
|
@@ -32,6 +32,18 @@ export declare function matches(pattern: string, target: string): boolean;
|
|
|
32
32
|
* Check if a permission pattern matches a structured Permission object.
|
|
33
33
|
*/
|
|
34
34
|
export declare function matchesPermission(pattern: string, permission: Permission): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Check if two permission patterns can name a common permission.
|
|
37
|
+
*
|
|
38
|
+
* This is the test a *deny* needs. For a concrete target it is exactly
|
|
39
|
+
* {@link matches}; for a wildcard target such as `post:*` it also holds when
|
|
40
|
+
* the deny is narrower (`post:delete`), which `matches` does not see.
|
|
41
|
+
*
|
|
42
|
+
* @example permissionsOverlap("post:delete", "post:*") → true
|
|
43
|
+
* @example permissionsOverlap("*:delete", "post:*") → true
|
|
44
|
+
* @example permissionsOverlap("post:delete", "post:read") → false
|
|
45
|
+
*/
|
|
46
|
+
export declare function permissionsOverlap(a: string, b: string): boolean;
|
|
35
47
|
/**
|
|
36
48
|
* Parse a permission string, returning null on invalid format instead of throwing.
|
|
37
49
|
*/
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module permission/permission
|
|
5
5
|
*/
|
|
6
6
|
import { InvalidPermissionError } from "../permissionErrors/index.js";
|
|
7
|
-
import { patternStrMatches } from "../rule/rule.
|
|
7
|
+
import { patternStrMatches, patternsOverlap } from "../rule/rule.pattern.js";
|
|
8
8
|
/**
|
|
9
9
|
* Valid permission format: `resource:action`.
|
|
10
10
|
*
|
|
@@ -62,6 +62,25 @@ export function matchesPermission(pattern, permission) {
|
|
|
62
62
|
return (patternStrMatches(parsed.resource, permission.resource) &&
|
|
63
63
|
patternStrMatches(parsed.action, permission.action));
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Check if two permission patterns can name a common permission.
|
|
67
|
+
*
|
|
68
|
+
* This is the test a *deny* needs. For a concrete target it is exactly
|
|
69
|
+
* {@link matches}; for a wildcard target such as `post:*` it also holds when
|
|
70
|
+
* the deny is narrower (`post:delete`), which `matches` does not see.
|
|
71
|
+
*
|
|
72
|
+
* @example permissionsOverlap("post:delete", "post:*") → true
|
|
73
|
+
* @example permissionsOverlap("*:delete", "post:*") → true
|
|
74
|
+
* @example permissionsOverlap("post:delete", "post:read") → false
|
|
75
|
+
*/
|
|
76
|
+
export function permissionsOverlap(a, b) {
|
|
77
|
+
const left = parsePermissionSafe(a);
|
|
78
|
+
const right = parsePermissionSafe(b);
|
|
79
|
+
if (!left || !right)
|
|
80
|
+
return false;
|
|
81
|
+
return (patternsOverlap(left.resource, right.resource) &&
|
|
82
|
+
patternsOverlap(left.action, right.action));
|
|
83
|
+
}
|
|
65
84
|
/**
|
|
66
85
|
* Parse a permission string, returning null on invalid format instead of throwing.
|
|
67
86
|
*/
|
|
@@ -8,6 +8,12 @@ import type { PermissionPolicyDefinition } from "../permissionTypes/index.js";
|
|
|
8
8
|
export interface PolicyRegistryOptions {
|
|
9
9
|
/** Allow overwriting an existing policy. Defaults to false. */
|
|
10
10
|
readonly allowOverride?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Reject permission patterns that are not `resource:action`. Defaults to
|
|
13
|
+
* true — a policy scoped to a pattern that never matches never runs, so a
|
|
14
|
+
* mistyped lockdown would deny nothing.
|
|
15
|
+
*/
|
|
16
|
+
readonly validatePermissions?: boolean;
|
|
11
17
|
}
|
|
12
18
|
/** A policy registry, usable directly as the engine's `policies` source. */
|
|
13
19
|
export interface PolicyRegistry {
|
|
@@ -19,6 +25,12 @@ export interface PolicyRegistry {
|
|
|
19
25
|
all(): readonly PermissionPolicyDefinition[];
|
|
20
26
|
remove(name: string): boolean;
|
|
21
27
|
clear(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Be told whenever the policy set changes. Returns an unsubscribe function.
|
|
30
|
+
* An engine built on this registry subscribes itself, so a policy change
|
|
31
|
+
* is not bypassed by a cached decision.
|
|
32
|
+
*/
|
|
33
|
+
subscribe(listener: () => void): () => void;
|
|
22
34
|
}
|
|
23
35
|
/**
|
|
24
36
|
* Create a policy registry.
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { DuplicatePolicyError } from "../permissionErrors/index.js";
|
|
7
7
|
import { selectPolicies } from "../evaluator/evaluator.pipeline.js";
|
|
8
|
+
import { validatePolicy } from "../evaluator/engineSupport/index.js";
|
|
9
|
+
import { createChangeNotifier } from "../utils/utils.notifier.js";
|
|
8
10
|
/**
|
|
9
11
|
* Create a policy registry.
|
|
10
12
|
*
|
|
@@ -13,6 +15,8 @@ import { selectPolicies } from "../evaluator/evaluator.pipeline.js";
|
|
|
13
15
|
export function createPolicyRegistry(options) {
|
|
14
16
|
const policies = new Map();
|
|
15
17
|
const allowOverride = options?.allowOverride ?? false;
|
|
18
|
+
const validatePermissions = options?.validatePermissions ?? true;
|
|
19
|
+
const changes = createChangeNotifier();
|
|
16
20
|
return {
|
|
17
21
|
/**
|
|
18
22
|
* Register a policy.
|
|
@@ -20,8 +24,12 @@ export function createPolicyRegistry(options) {
|
|
|
20
24
|
* Re-registering a name is rejected unless `allowOverride` was set: a
|
|
21
25
|
* second `define("owner-only", …)` used to replace the first in silence,
|
|
22
26
|
* which is an authorization rule vanishing without a trace.
|
|
27
|
+
*
|
|
28
|
+
* @throws {InvalidPermissionError} when a permission pattern is malformed.
|
|
23
29
|
*/
|
|
24
30
|
define(definition) {
|
|
31
|
+
if (validatePermissions)
|
|
32
|
+
validatePolicy(definition);
|
|
25
33
|
if (policies.has(definition.name) && !allowOverride) {
|
|
26
34
|
throw new DuplicatePolicyError(definition.name);
|
|
27
35
|
}
|
|
@@ -31,6 +39,7 @@ export function createPolicyRegistry(options) {
|
|
|
31
39
|
...definition,
|
|
32
40
|
permissions: Object.freeze([...definition.permissions]),
|
|
33
41
|
}));
|
|
42
|
+
changes.notify();
|
|
34
43
|
},
|
|
35
44
|
get(name) {
|
|
36
45
|
return policies.get(name);
|
|
@@ -55,10 +64,17 @@ export function createPolicyRegistry(options) {
|
|
|
55
64
|
return [...policies.values()];
|
|
56
65
|
},
|
|
57
66
|
remove(name) {
|
|
58
|
-
|
|
67
|
+
const removed = policies.delete(name);
|
|
68
|
+
if (removed)
|
|
69
|
+
changes.notify();
|
|
70
|
+
return removed;
|
|
59
71
|
},
|
|
60
72
|
clear() {
|
|
61
73
|
policies.clear();
|
|
74
|
+
changes.notify();
|
|
75
|
+
},
|
|
76
|
+
subscribe(listener) {
|
|
77
|
+
return changes.subscribe(listener);
|
|
62
78
|
},
|
|
63
79
|
};
|
|
64
80
|
}
|
|
@@ -26,6 +26,15 @@ export interface RoleRegistry {
|
|
|
26
26
|
all(): readonly RoleDefinition[];
|
|
27
27
|
remove(name: string): boolean;
|
|
28
28
|
clear(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Be told whenever the role set changes (`define`, a `remove` that removed
|
|
31
|
+
* something, `clear`). Returns an unsubscribe function.
|
|
32
|
+
*
|
|
33
|
+
* An engine built on this registry subscribes itself, which is what makes
|
|
34
|
+
* revoking a role take effect on the next check instead of whenever
|
|
35
|
+
* somebody remembers to call `engine.invalidateRoles()`.
|
|
36
|
+
*/
|
|
37
|
+
subscribe(listener: () => void): () => void;
|
|
29
38
|
}
|
|
30
39
|
/**
|
|
31
40
|
* Copy a role definition so it no longer shares arrays with the caller.
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { DuplicateRoleError, InvalidRoleError, RoleNotFoundError, } from "../permissionErrors/index.js";
|
|
7
7
|
import { isValidPermission } from "../permission/permission.core.js";
|
|
8
|
+
import { invalidRulePattern } from "../evaluator/engineSupport/index.js";
|
|
9
|
+
import { createChangeNotifier } from "../utils/utils.notifier.js";
|
|
8
10
|
/**
|
|
9
11
|
* Copy a role definition so it no longer shares arrays with the caller.
|
|
10
12
|
*
|
|
@@ -32,6 +34,7 @@ export function createRoleRegistry(options) {
|
|
|
32
34
|
const roles = new Map();
|
|
33
35
|
const allowOverride = options?.allowOverride ?? false;
|
|
34
36
|
const validatePermissions = options?.validatePermissions ?? true;
|
|
37
|
+
const changes = createChangeNotifier();
|
|
35
38
|
return {
|
|
36
39
|
/**
|
|
37
40
|
* Register a role definition.
|
|
@@ -54,11 +57,19 @@ export function createRoleRegistry(options) {
|
|
|
54
57
|
`valid "resource:action" permission`);
|
|
55
58
|
}
|
|
56
59
|
}
|
|
60
|
+
for (const rule of definition.rules ?? []) {
|
|
61
|
+
const invalid = invalidRulePattern(rule);
|
|
62
|
+
if (invalid !== undefined) {
|
|
63
|
+
throw new InvalidRoleError(`Role "${definition.name}" has a rule on "${invalid}", which is ` +
|
|
64
|
+
`not a valid "resource:action" pattern`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
57
67
|
}
|
|
58
68
|
if (roles.has(definition.name) && !allowOverride) {
|
|
59
69
|
throw new DuplicateRoleError(definition.name);
|
|
60
70
|
}
|
|
61
71
|
roles.set(definition.name, freezeRoleDefinition(definition));
|
|
72
|
+
changes.notify();
|
|
62
73
|
},
|
|
63
74
|
get(name) {
|
|
64
75
|
return roles.get(name);
|
|
@@ -79,10 +90,17 @@ export function createRoleRegistry(options) {
|
|
|
79
90
|
return [...roles.values()];
|
|
80
91
|
},
|
|
81
92
|
remove(name) {
|
|
82
|
-
|
|
93
|
+
const removed = roles.delete(name);
|
|
94
|
+
if (removed)
|
|
95
|
+
changes.notify();
|
|
96
|
+
return removed;
|
|
83
97
|
},
|
|
84
98
|
clear() {
|
|
85
99
|
roles.clear();
|
|
100
|
+
changes.notify();
|
|
101
|
+
},
|
|
102
|
+
subscribe(listener) {
|
|
103
|
+
return changes.subscribe(listener);
|
|
86
104
|
},
|
|
87
105
|
};
|
|
88
106
|
}
|
package/dist/rule/rule.core.d.ts
CHANGED
|
@@ -4,23 +4,7 @@
|
|
|
4
4
|
* @module rule/rule
|
|
5
5
|
*/
|
|
6
6
|
import type { PermissionRule, Permission, PermissionContext, RuleCombiningAlgorithm, RuleEvaluation } from "../permissionTypes/index.js";
|
|
7
|
-
|
|
8
|
-
* Check if a rule matches a target permission.
|
|
9
|
-
*
|
|
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}.
|
|
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;
|
|
7
|
+
export { ruleMatches, patternStrMatches } from "./rule.pattern.js";
|
|
24
8
|
/**
|
|
25
9
|
* Evaluate a set of rules against a target permission.
|
|
26
10
|
*
|
|
@@ -29,9 +13,15 @@ export declare function patternStrMatches(pattern: string, target: string): bool
|
|
|
29
13
|
* has to mean "owners may", not "anyone may". Conditions are async, so this
|
|
30
14
|
* function is too.
|
|
31
15
|
*
|
|
32
|
-
* A condition that
|
|
33
|
-
*
|
|
34
|
-
*
|
|
16
|
+
* A condition that cannot be evaluated fails closed, and what "closed" means
|
|
17
|
+
* depends on the rule's effect. An allow whose condition throws, or that has
|
|
18
|
+
* a condition but no context, does not apply. A *deny* in the same position
|
|
19
|
+
* does apply: treating it as unmet dropped the deny and let the role's allow
|
|
20
|
+
* win, so a missing resource or a blocklist service that was down granted
|
|
21
|
+
* the very access the deny existed to refuse.
|
|
22
|
+
*
|
|
23
|
+
* A deny also applies when its patterns merely overlap a wildcard target —
|
|
24
|
+
* `can(actor, "post:*")` is refused by a deny on `post:delete`.
|
|
35
25
|
*
|
|
36
26
|
* Default combining algorithm: `deny-overrides`. Any applicable deny wins,
|
|
37
27
|
* whatever its priority.
|
|
@@ -44,8 +34,10 @@ export declare function evaluateRules(rules: readonly PermissionRule[], target:
|
|
|
44
34
|
* Evaluate rules that carry no conditions.
|
|
45
35
|
*
|
|
46
36
|
* Synchronous, for callers that build their own condition-free rule sets.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
37
|
+
* A conditional rule cannot be evaluated without awaiting it — use
|
|
38
|
+
* {@link evaluateRules} for those — so it fails closed the same way an
|
|
39
|
+
* unevaluable condition does there: a conditional allow is skipped, and a
|
|
40
|
+
* conditional deny applies.
|
|
49
41
|
*/
|
|
50
42
|
export declare function evaluateRulesSync(rules: readonly PermissionRule[], target: Permission, options?: {
|
|
51
43
|
readonly algorithm?: RuleCombiningAlgorithm;
|
package/dist/rule/rule.core.js
CHANGED
|
@@ -3,42 +3,34 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module rule/rule
|
|
5
5
|
*/
|
|
6
|
+
import { ruleBearsOn } from "./rule.pattern.js";
|
|
7
|
+
export { ruleMatches, patternStrMatches } from "./rule.pattern.js";
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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}.
|
|
9
|
+
* Sort key for `priority` combining: higher priority first, and at equal
|
|
10
|
+
* priority a deny before an allow.
|
|
12
11
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return
|
|
12
|
+
function byPriority(a, b) {
|
|
13
|
+
const priorityA = a.priority ?? 0;
|
|
14
|
+
const priorityB = b.priority ?? 0;
|
|
15
|
+
if (priorityA !== priorityB)
|
|
16
|
+
return priorityB - priorityA;
|
|
17
|
+
if (a.effect === "deny" && b.effect !== "deny")
|
|
18
|
+
return -1;
|
|
19
|
+
if (b.effect === "deny" && a.effect !== "deny")
|
|
20
|
+
return 1;
|
|
21
|
+
return 0;
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
export function patternStrMatches(pattern, target) {
|
|
33
|
-
if (pattern === "*")
|
|
34
|
-
return true;
|
|
35
|
-
if (pattern === target)
|
|
36
|
-
return true;
|
|
37
|
-
if (pattern.endsWith(".*")) {
|
|
38
|
-
const prefix = pattern.slice(0, -2);
|
|
39
|
-
return target === prefix || target.startsWith(`${prefix}.`);
|
|
23
|
+
function combineApplicable(applicable, algorithm) {
|
|
24
|
+
if (applicable.length === 0)
|
|
25
|
+
return { allowed: false, applicable: [] };
|
|
26
|
+
if (algorithm === "deny-overrides") {
|
|
27
|
+
const deny = applicable.find((rule) => rule.effect === "deny");
|
|
28
|
+
if (deny)
|
|
29
|
+
return { allowed: false, matchedRule: deny, applicable };
|
|
30
|
+
return { allowed: true, matchedRule: applicable[0], applicable };
|
|
40
31
|
}
|
|
41
|
-
|
|
32
|
+
const top = [...applicable].sort(byPriority)[0];
|
|
33
|
+
return { allowed: top.effect === "allow", matchedRule: top, applicable };
|
|
42
34
|
}
|
|
43
35
|
/**
|
|
44
36
|
* Evaluate a set of rules against a target permission.
|
|
@@ -48,9 +40,15 @@ export function patternStrMatches(pattern, target) {
|
|
|
48
40
|
* has to mean "owners may", not "anyone may". Conditions are async, so this
|
|
49
41
|
* function is too.
|
|
50
42
|
*
|
|
51
|
-
* A condition that
|
|
52
|
-
*
|
|
53
|
-
*
|
|
43
|
+
* A condition that cannot be evaluated fails closed, and what "closed" means
|
|
44
|
+
* depends on the rule's effect. An allow whose condition throws, or that has
|
|
45
|
+
* a condition but no context, does not apply. A *deny* in the same position
|
|
46
|
+
* does apply: treating it as unmet dropped the deny and let the role's allow
|
|
47
|
+
* win, so a missing resource or a blocklist service that was down granted
|
|
48
|
+
* the very access the deny existed to refuse.
|
|
49
|
+
*
|
|
50
|
+
* A deny also applies when its patterns merely overlap a wildcard target —
|
|
51
|
+
* `can(actor, "post:*")` is refused by a deny on `post:delete`.
|
|
54
52
|
*
|
|
55
53
|
* Default combining algorithm: `deny-overrides`. Any applicable deny wins,
|
|
56
54
|
* whatever its priority.
|
|
@@ -59,78 +57,43 @@ export async function evaluateRules(rules, target, context, options) {
|
|
|
59
57
|
const algorithm = options?.algorithm ?? "deny-overrides";
|
|
60
58
|
const applicable = [];
|
|
61
59
|
for (const rule of rules) {
|
|
62
|
-
if (!
|
|
60
|
+
if (!ruleBearsOn(rule, target))
|
|
63
61
|
continue;
|
|
64
62
|
if (rule.condition) {
|
|
65
|
-
|
|
63
|
+
const failClosed = rule.effect === "deny";
|
|
64
|
+
if (!context) {
|
|
65
|
+
if (failClosed)
|
|
66
|
+
applicable.push(rule);
|
|
66
67
|
continue;
|
|
67
|
-
|
|
68
|
+
}
|
|
69
|
+
let met;
|
|
68
70
|
try {
|
|
69
71
|
met = await rule.condition(context);
|
|
70
72
|
}
|
|
71
73
|
catch (error) {
|
|
72
74
|
options?.onConditionError?.(rule, error);
|
|
73
|
-
met =
|
|
75
|
+
met = failClosed;
|
|
74
76
|
}
|
|
75
77
|
if (!met)
|
|
76
78
|
continue;
|
|
77
79
|
}
|
|
78
80
|
applicable.push(rule);
|
|
79
81
|
}
|
|
80
|
-
|
|
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 };
|
|
82
|
+
return combineApplicable(applicable, algorithm);
|
|
103
83
|
}
|
|
104
84
|
/**
|
|
105
85
|
* Evaluate rules that carry no conditions.
|
|
106
86
|
*
|
|
107
87
|
* Synchronous, for callers that build their own condition-free rule sets.
|
|
108
|
-
*
|
|
109
|
-
*
|
|
88
|
+
* A conditional rule cannot be evaluated without awaiting it — use
|
|
89
|
+
* {@link evaluateRules} for those — so it fails closed the same way an
|
|
90
|
+
* unevaluable condition does there: a conditional allow is skipped, and a
|
|
91
|
+
* conditional deny applies.
|
|
110
92
|
*/
|
|
111
93
|
export function evaluateRulesSync(rules, target, options) {
|
|
112
94
|
const algorithm = options?.algorithm ?? "deny-overrides";
|
|
113
|
-
const applicable = rules.filter((rule) =>
|
|
114
|
-
|
|
115
|
-
|
|
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 };
|
|
121
|
-
}
|
|
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;
|
|
127
|
-
if (a.effect === "deny" && b.effect !== "deny")
|
|
128
|
-
return -1;
|
|
129
|
-
if (b.effect === "deny" && a.effect !== "deny")
|
|
130
|
-
return 1;
|
|
131
|
-
return 0;
|
|
132
|
-
});
|
|
133
|
-
const top = sorted[0];
|
|
134
|
-
return { allowed: top.effect === "allow", matchedRule: top, applicable };
|
|
95
|
+
const applicable = rules.filter((rule) => ruleBearsOn(rule, target) &&
|
|
96
|
+
(rule.condition === undefined || rule.effect === "deny"));
|
|
97
|
+
return combineApplicable(applicable, algorithm);
|
|
135
98
|
}
|
|
136
99
|
//# sourceMappingURL=rule.core.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern matching shared by rules, grants, denies and policies.
|
|
3
|
+
*
|
|
4
|
+
* @module rule/rule.pattern
|
|
5
|
+
*/
|
|
6
|
+
import type { Permission, PermissionRule } from "../permissionTypes/index.js";
|
|
7
|
+
/**
|
|
8
|
+
* Check if a single pattern string matches a target, supporting wildcards.
|
|
9
|
+
*
|
|
10
|
+
* Two forms are supported, and they are the same two the permission matcher
|
|
11
|
+
* supports, so a pattern means the same thing wherever it is written:
|
|
12
|
+
* `*` — matches anything
|
|
13
|
+
* `billing.*` — matches `billing` and any `billing.…` namespace
|
|
14
|
+
*/
|
|
15
|
+
export declare function patternStrMatches(pattern: string, target: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Whether two segment patterns can name a common segment.
|
|
18
|
+
*
|
|
19
|
+
* For a concrete `target` this is exactly {@link patternStrMatches}. For a
|
|
20
|
+
* wildcard target it is what a *deny* has to test: `can(actor, "post:*")`
|
|
21
|
+
* asks about everything under `post`, so a deny of `post:delete` bears on it
|
|
22
|
+
* although `post:delete` does not match `post:*` as a pattern.
|
|
23
|
+
*/
|
|
24
|
+
export declare function patternsOverlap(a: string, b: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Check if a rule matches a target permission.
|
|
27
|
+
*
|
|
28
|
+
* Matching is about resource and action only. Whether the rule *applies* also
|
|
29
|
+
* depends on its condition, which needs a context and is therefore evaluated
|
|
30
|
+
* by `evaluateRules`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function ruleMatches(rule: PermissionRule, target: Permission): boolean;
|
|
33
|
+
/** Check if a rule's patterns overlap a (possibly wildcard) target. */
|
|
34
|
+
export declare function ruleOverlaps(rule: PermissionRule, target: Permission): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Whether a rule bears on a target: allows by matching, denies by overlap.
|
|
37
|
+
*
|
|
38
|
+
* An allow must cover the whole target to grant it; a deny needs only to
|
|
39
|
+
* touch it. For a concrete target the two are the same test.
|
|
40
|
+
*/
|
|
41
|
+
export declare function ruleBearsOn(rule: PermissionRule, target: Permission): boolean;
|
|
42
|
+
/** Whether a permission names more than one concrete permission. */
|
|
43
|
+
export declare function isWildcardTarget(target: Permission): boolean;
|
|
44
|
+
//# sourceMappingURL=rule.pattern.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern matching shared by rules, grants, denies and policies.
|
|
3
|
+
*
|
|
4
|
+
* @module rule/rule.pattern
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Check if a single pattern string matches a target, supporting wildcards.
|
|
8
|
+
*
|
|
9
|
+
* Two forms are supported, and they are the same two the permission matcher
|
|
10
|
+
* supports, so a pattern means the same thing wherever it is written:
|
|
11
|
+
* `*` — matches anything
|
|
12
|
+
* `billing.*` — matches `billing` and any `billing.…` namespace
|
|
13
|
+
*/
|
|
14
|
+
export function patternStrMatches(pattern, target) {
|
|
15
|
+
if (pattern === "*")
|
|
16
|
+
return true;
|
|
17
|
+
if (pattern === target)
|
|
18
|
+
return true;
|
|
19
|
+
if (pattern.endsWith(".*")) {
|
|
20
|
+
const prefix = pattern.slice(0, -2);
|
|
21
|
+
return target === prefix || target.startsWith(`${prefix}.`);
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Whether two segment patterns can name a common segment.
|
|
27
|
+
*
|
|
28
|
+
* For a concrete `target` this is exactly {@link patternStrMatches}. For a
|
|
29
|
+
* wildcard target it is what a *deny* has to test: `can(actor, "post:*")`
|
|
30
|
+
* asks about everything under `post`, so a deny of `post:delete` bears on it
|
|
31
|
+
* although `post:delete` does not match `post:*` as a pattern.
|
|
32
|
+
*/
|
|
33
|
+
export function patternsOverlap(a, b) {
|
|
34
|
+
return patternStrMatches(a, b) || patternStrMatches(b, a);
|
|
35
|
+
}
|
|
36
|
+
function patternList(pattern) {
|
|
37
|
+
return Array.isArray(pattern)
|
|
38
|
+
? pattern
|
|
39
|
+
: [pattern];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Check if a rule matches a target permission.
|
|
43
|
+
*
|
|
44
|
+
* Matching is about resource and action only. Whether the rule *applies* also
|
|
45
|
+
* depends on its condition, which needs a context and is therefore evaluated
|
|
46
|
+
* by `evaluateRules`.
|
|
47
|
+
*/
|
|
48
|
+
export function ruleMatches(rule, target) {
|
|
49
|
+
return (patternList(rule.resource).some((entry) => patternStrMatches(entry, target.resource)) &&
|
|
50
|
+
patternList(rule.action).some((entry) => patternStrMatches(entry, target.action)));
|
|
51
|
+
}
|
|
52
|
+
/** Check if a rule's patterns overlap a (possibly wildcard) target. */
|
|
53
|
+
export function ruleOverlaps(rule, target) {
|
|
54
|
+
return (patternList(rule.resource).some((entry) => patternsOverlap(entry, target.resource)) &&
|
|
55
|
+
patternList(rule.action).some((entry) => patternsOverlap(entry, target.action)));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Whether a rule bears on a target: allows by matching, denies by overlap.
|
|
59
|
+
*
|
|
60
|
+
* An allow must cover the whole target to grant it; a deny needs only to
|
|
61
|
+
* touch it. For a concrete target the two are the same test.
|
|
62
|
+
*/
|
|
63
|
+
export function ruleBearsOn(rule, target) {
|
|
64
|
+
return rule.effect === "deny"
|
|
65
|
+
? ruleOverlaps(rule, target)
|
|
66
|
+
: ruleMatches(rule, target);
|
|
67
|
+
}
|
|
68
|
+
/** Whether a permission names more than one concrete permission. */
|
|
69
|
+
export function isWildcardTarget(target) {
|
|
70
|
+
return target.resource.includes("*") || target.action.includes("*");
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=rule.pattern.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal change notifier for the live registries.
|
|
3
|
+
*
|
|
4
|
+
* @module utils/utils.notifier
|
|
5
|
+
*/
|
|
6
|
+
/** A set of change listeners. */
|
|
7
|
+
export interface ChangeNotifier {
|
|
8
|
+
/** Register a listener. Returns an unsubscribe function. */
|
|
9
|
+
subscribe(listener: () => void): () => void;
|
|
10
|
+
/**
|
|
11
|
+
* Call every listener. All of them run even if one throws; the first
|
|
12
|
+
* error is rethrown afterwards, so one broken subscriber cannot stop an
|
|
13
|
+
* engine further down the list from dropping its stale decisions.
|
|
14
|
+
*/
|
|
15
|
+
notify(): void;
|
|
16
|
+
}
|
|
17
|
+
/** Create a {@link ChangeNotifier}. */
|
|
18
|
+
export declare function createChangeNotifier(): ChangeNotifier;
|
|
19
|
+
//# sourceMappingURL=utils.notifier.d.ts.map
|