@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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Condition combinators for composing authorization policies.
|
|
3
3
|
*
|
|
4
|
+
* Conditions are evaluated by the rule engine: attach one to a
|
|
5
|
+
* `PermissionRule` and the rule applies only when it returns `true`.
|
|
6
|
+
*
|
|
4
7
|
* @module conditions/conditions
|
|
5
8
|
*/
|
|
6
9
|
import type { PermissionConditionFn } from "../permissionTypes/index.js";
|
|
@@ -33,8 +36,21 @@ export declare function isOwner(ownerField?: string): PermissionConditionFn;
|
|
|
33
36
|
/**
|
|
34
37
|
* Enforce tenant isolation — actor and resource must share the same tenant.
|
|
35
38
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
39
|
+
* The actor's tenant is read from `context.metadata`, which is supplied per
|
|
40
|
+
* check through `AuthorizationOptions.metadata`:
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* engine.check(actor, "invoice:read", invoice, {
|
|
44
|
+
* metadata: { tenantId: request.tenantId },
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @param actorTenantField - Key in the context metadata holding the actor's tenant. Defaults to "tenantId".
|
|
49
|
+
* @param resourceTenantField - Field on the resource holding the tenant ID. Defaults to "tenantId".
|
|
38
50
|
*/
|
|
39
51
|
export declare function tenantIsolation(actorTenantField?: string, resourceTenantField?: string): PermissionConditionFn;
|
|
52
|
+
/** Check that a value in the context metadata equals an expected value. */
|
|
53
|
+
export declare function metadataEquals(key: string, expected: unknown): PermissionConditionFn;
|
|
54
|
+
/** Check that the resource field matches the expected value. */
|
|
55
|
+
export declare function resourceEquals(field: string, expected: unknown): PermissionConditionFn;
|
|
40
56
|
//# sourceMappingURL=conditions.core.d.ts.map
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Condition combinators for composing authorization policies.
|
|
3
3
|
*
|
|
4
|
+
* Conditions are evaluated by the rule engine: attach one to a
|
|
5
|
+
* `PermissionRule` and the rule applies only when it returns `true`.
|
|
6
|
+
*
|
|
4
7
|
* @module conditions/conditions
|
|
5
8
|
*/
|
|
6
9
|
/**
|
|
@@ -45,6 +48,22 @@ export function always() {
|
|
|
45
48
|
export function never() {
|
|
46
49
|
return () => false;
|
|
47
50
|
}
|
|
51
|
+
/** Compares two identifiers that may differ in type across a boundary. */
|
|
52
|
+
function sameId(left, right) {
|
|
53
|
+
if (left === undefined || left === null)
|
|
54
|
+
return false;
|
|
55
|
+
if (right === undefined || right === null)
|
|
56
|
+
return false;
|
|
57
|
+
if (typeof left === typeof right)
|
|
58
|
+
return left === right;
|
|
59
|
+
// A numeric id from a database row against a string id from a token is the
|
|
60
|
+
// common case, and a strict comparison silently denies.
|
|
61
|
+
if ((typeof left === "string" || typeof left === "number") &&
|
|
62
|
+
(typeof right === "string" || typeof right === "number")) {
|
|
63
|
+
return String(left) === String(right);
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
48
67
|
/**
|
|
49
68
|
* Check that the actor owns the resource.
|
|
50
69
|
*
|
|
@@ -55,24 +74,45 @@ export function isOwner(ownerField = "ownerId") {
|
|
|
55
74
|
if (!context.resource || typeof context.resource !== "object")
|
|
56
75
|
return false;
|
|
57
76
|
const resource = context.resource;
|
|
58
|
-
|
|
59
|
-
return ownerId === context.actor.id;
|
|
77
|
+
return sameId(resource[ownerField], context.actor.id);
|
|
60
78
|
};
|
|
61
79
|
}
|
|
62
80
|
/**
|
|
63
81
|
* Enforce tenant isolation — actor and resource must share the same tenant.
|
|
64
82
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
83
|
+
* The actor's tenant is read from `context.metadata`, which is supplied per
|
|
84
|
+
* check through `AuthorizationOptions.metadata`:
|
|
85
|
+
*
|
|
86
|
+
* ```ts
|
|
87
|
+
* engine.check(actor, "invoice:read", invoice, {
|
|
88
|
+
* metadata: { tenantId: request.tenantId },
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @param actorTenantField - Key in the context metadata holding the actor's tenant. Defaults to "tenantId".
|
|
93
|
+
* @param resourceTenantField - Field on the resource holding the tenant ID. Defaults to "tenantId".
|
|
67
94
|
*/
|
|
68
95
|
export function tenantIsolation(actorTenantField = "tenantId", resourceTenantField = "tenantId") {
|
|
69
96
|
return (context) => {
|
|
70
97
|
const actorTenant = context.metadata?.get(actorTenantField);
|
|
98
|
+
if (actorTenant === undefined || actorTenant === null)
|
|
99
|
+
return false;
|
|
71
100
|
if (!context.resource || typeof context.resource !== "object")
|
|
72
101
|
return false;
|
|
73
102
|
const resource = context.resource;
|
|
74
|
-
|
|
75
|
-
|
|
103
|
+
return sameId(actorTenant, resource[resourceTenantField]);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Check that a value in the context metadata equals an expected value. */
|
|
107
|
+
export function metadataEquals(key, expected) {
|
|
108
|
+
return (context) => context.metadata?.get(key) === expected;
|
|
109
|
+
}
|
|
110
|
+
/** Check that the resource field matches the expected value. */
|
|
111
|
+
export function resourceEquals(field, expected) {
|
|
112
|
+
return (context) => {
|
|
113
|
+
if (!context.resource || typeof context.resource !== "object")
|
|
114
|
+
return false;
|
|
115
|
+
return context.resource[field] === expected;
|
|
76
116
|
};
|
|
77
117
|
}
|
|
78
118
|
//# sourceMappingURL=conditions.core.js.map
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module conditions
|
|
5
5
|
*/
|
|
6
|
-
export { allOf, anyOf, not, always, never, isOwner, tenantIsolation, } from "./conditions.core.js";
|
|
6
|
+
export { allOf, anyOf, not, always, never, isOwner, tenantIsolation, metadataEquals, resourceEquals, } from "./conditions.core.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/conditions/index.js
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module conditions
|
|
5
5
|
*/
|
|
6
|
-
export { allOf, anyOf, not, always, never, isOwner, tenantIsolation, } from "./conditions.core.js";
|
|
6
|
+
export { allOf, anyOf, not, always, never, isOwner, tenantIsolation, metadataEquals, resourceEquals, } from "./conditions.core.js";
|
|
7
7
|
//# sourceMappingURL=index.js.map
|
|
@@ -3,17 +3,55 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module evaluator/authorizationEngine
|
|
5
5
|
*/
|
|
6
|
-
import type { PermissionActor, PermissionDecision, ExplainResult, PermissionPolicyDefinition, AuthorizationOptions } from "../permissionTypes/index.js";
|
|
7
|
-
import type { RoleDefinition } from "../permissionTypes/index.js";
|
|
6
|
+
import type { PermissionActor, PermissionDecision, ExplainResult, PermissionRule, PermissionPolicyDefinition, PermissionCache, PermissionResolver, RoleResolver, RoleDefinition, RuleCombiningAlgorithm, AuthorizationOptions } from "../permissionTypes/index.js";
|
|
8
7
|
import { type Ability } from "../ability/ability.core.js";
|
|
8
|
+
import type { PermissionEventEmitter } from "../observability/observability.core.js";
|
|
9
|
+
/** Anything the engine will accept as its source of roles. */
|
|
10
|
+
export interface RoleSource {
|
|
11
|
+
get(name: string): RoleDefinition | undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Anything the engine will accept as its source of policies. */
|
|
14
|
+
export interface PolicySource {
|
|
15
|
+
names(): readonly string[];
|
|
16
|
+
get(name: string): PermissionPolicyDefinition | undefined;
|
|
17
|
+
}
|
|
9
18
|
/** Configuration for the permission engine. */
|
|
10
19
|
export interface PermissionEngineOptions {
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Roles available to the engine — an array, or a registry created with
|
|
22
|
+
* `createRoleRegistry()`.
|
|
23
|
+
*/
|
|
24
|
+
readonly roles?: readonly RoleDefinition[] | RoleSource;
|
|
25
|
+
/**
|
|
26
|
+
* Policies to evaluate during authorization — an array, or a registry
|
|
27
|
+
* created with `createPolicyRegistry()`.
|
|
28
|
+
*/
|
|
29
|
+
readonly policies?: readonly PermissionPolicyDefinition[] | PolicySource;
|
|
30
|
+
/** Static rules evaluated alongside the actor's permissions. */
|
|
31
|
+
readonly rules?: readonly PermissionRule[];
|
|
15
32
|
/** Default timeout for async policy evaluation (ms). */
|
|
16
33
|
readonly policyTimeout?: number;
|
|
34
|
+
/** How competing rules combine. Default: `"deny-overrides"`. */
|
|
35
|
+
readonly algorithm?: RuleCombiningAlgorithm;
|
|
36
|
+
/** Caches decisions. Create one with `createMemoryPermissionCache()`. */
|
|
37
|
+
readonly cache?: PermissionCache;
|
|
38
|
+
/** Time-to-live for cached decisions, in ms. */
|
|
39
|
+
readonly cacheTtlMs?: number;
|
|
40
|
+
/** Loads additional rules for an actor from an external source. */
|
|
41
|
+
readonly permissionResolver?: PermissionResolver;
|
|
42
|
+
/** Loads additional roles for an actor from an external source. */
|
|
43
|
+
readonly roleResolver?: RoleResolver;
|
|
44
|
+
/** Expands a permission into the permissions it implies. */
|
|
45
|
+
readonly expandImplied?: (permission: string) => readonly string[];
|
|
46
|
+
/** Emits an event for every completed check, including failures. */
|
|
47
|
+
readonly emitter?: PermissionEventEmitter;
|
|
48
|
+
/** Reports a failure authorization swallowed to stay fail-closed. */
|
|
49
|
+
readonly onError?: (error: unknown, source: string) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Rejects malformed configuration at construction instead of letting a
|
|
52
|
+
* grant that can never match sit in the role table. Default: `true`.
|
|
53
|
+
*/
|
|
54
|
+
readonly validateConfiguration?: boolean;
|
|
17
55
|
}
|
|
18
56
|
/**
|
|
19
57
|
* The public permission engine API.
|
|
@@ -39,6 +77,10 @@ export interface PermissionEngine {
|
|
|
39
77
|
* Create a pre-resolved Ability for an actor.
|
|
40
78
|
*/
|
|
41
79
|
createAbility(actor: PermissionActor): Ability;
|
|
80
|
+
/** Drop cached decisions for one actor. */
|
|
81
|
+
invalidateActor(actorId: string): Promise<void>;
|
|
82
|
+
/** Re-read the role source, discarding the memoized lookups. */
|
|
83
|
+
invalidateRoles(): void;
|
|
42
84
|
}
|
|
43
85
|
/**
|
|
44
86
|
* Create a permission engine.
|
|
@@ -3,47 +3,187 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module evaluator/authorizationEngine
|
|
5
5
|
*/
|
|
6
|
-
import { evaluate,
|
|
6
|
+
import { evaluate, evaluateWithTrace } from "./evaluator.core.js";
|
|
7
7
|
import { createAbility } from "../ability/ability.core.js";
|
|
8
|
-
import { PermissionDeniedError } from "../permissionErrors/index.js";
|
|
8
|
+
import { PermissionDeniedError, InvalidRoleError, } from "../permissionErrors/index.js";
|
|
9
|
+
import { isValidPermission } from "../permission/permission.core.js";
|
|
10
|
+
import { memoizeRoleLookup } from "../role/roleHierarchy.js";
|
|
11
|
+
import { freezeRoleDefinition } from "../role/roleRegistry.js";
|
|
12
|
+
function isRoleSource(roles) {
|
|
13
|
+
return (roles !== undefined &&
|
|
14
|
+
!Array.isArray(roles) &&
|
|
15
|
+
typeof roles.get === "function");
|
|
16
|
+
}
|
|
17
|
+
function isPolicySource(policies) {
|
|
18
|
+
return (policies !== undefined &&
|
|
19
|
+
!Array.isArray(policies) &&
|
|
20
|
+
typeof policies.names === "function");
|
|
21
|
+
}
|
|
22
|
+
/** Rejects a role whose grants could never match. */
|
|
23
|
+
function validateRole(role) {
|
|
24
|
+
if (!role.name || role.name.trim() === "") {
|
|
25
|
+
throw new InvalidRoleError("Role name cannot be empty");
|
|
26
|
+
}
|
|
27
|
+
for (const permission of role.permissions) {
|
|
28
|
+
if (!isValidPermission(permission)) {
|
|
29
|
+
// A malformed grant can never match, so it is a silent no-op unless
|
|
30
|
+
// it is rejected here.
|
|
31
|
+
throw new InvalidRoleError(`Role "${role.name}" grants "${permission}", which is not a valid ` +
|
|
32
|
+
`"resource:action" permission`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function validateRoles(roles) {
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
for (const role of roles) {
|
|
39
|
+
validateRole(role);
|
|
40
|
+
if (seen.has(role.name)) {
|
|
41
|
+
throw new InvalidRoleError(`Role "${role.name}" is defined more than once`);
|
|
42
|
+
}
|
|
43
|
+
seen.add(role.name);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
9
46
|
/**
|
|
10
47
|
* Create a permission engine.
|
|
11
48
|
*/
|
|
12
49
|
export function createPermissionEngine(options) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
50
|
+
const validateConfiguration = options?.validateConfiguration ?? true;
|
|
51
|
+
let roleLookup;
|
|
52
|
+
let invalidateRoleCache;
|
|
53
|
+
if (isRoleSource(options?.roles)) {
|
|
54
|
+
const source = options.roles;
|
|
55
|
+
// A role source is looked up lazily, so `validateConfiguration` has to be
|
|
56
|
+
// applied lazily too. Checking only the array form left a registry-backed
|
|
57
|
+
// engine accepting grants that can never match — the exact silent no-op
|
|
58
|
+
// the option exists to prevent.
|
|
59
|
+
const memo = memoizeRoleLookup((name) => {
|
|
60
|
+
const role = source.get(name);
|
|
61
|
+
if (role && validateConfiguration)
|
|
62
|
+
validateRole(role);
|
|
63
|
+
return role;
|
|
64
|
+
});
|
|
65
|
+
roleLookup = memo.lookup;
|
|
66
|
+
invalidateRoleCache = memo.invalidate;
|
|
18
67
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
68
|
+
else {
|
|
69
|
+
const list = (options?.roles ?? []);
|
|
70
|
+
if (validateConfiguration)
|
|
71
|
+
validateRoles(list);
|
|
72
|
+
// Copy each role, so a caller still holding the arrays it passed in
|
|
73
|
+
// cannot widen a grant after validation has run.
|
|
74
|
+
const roleMap = new Map(list.map((role) => [role.name, freezeRoleDefinition(role)]));
|
|
75
|
+
roleLookup = (name) => roleMap.get(name);
|
|
76
|
+
invalidateRoleCache = () => { };
|
|
77
|
+
}
|
|
78
|
+
const policySource = isPolicySource(options?.policies)
|
|
79
|
+
? options.policies
|
|
80
|
+
: undefined;
|
|
81
|
+
const policyList = policySource
|
|
82
|
+
? undefined
|
|
83
|
+
: (options?.policies ?? []);
|
|
84
|
+
const resolvePolicies = () => {
|
|
85
|
+
if (!policySource)
|
|
86
|
+
return policyList ?? [];
|
|
87
|
+
return policySource
|
|
88
|
+
.names()
|
|
89
|
+
.map((name) => policySource.get(name))
|
|
90
|
+
.filter((policy) => policy !== undefined);
|
|
91
|
+
};
|
|
92
|
+
// One live view over the configuration. `policies` is a getter so a
|
|
93
|
+
// registry-backed engine re-reads the registry on every evaluation — an
|
|
94
|
+
// Ability used to capture a snapshot of the policy list when it was
|
|
95
|
+
// created, so a policy defined afterwards was enforced by `engine.can()`
|
|
96
|
+
// and ignored by `ability.can()` for the same actor.
|
|
97
|
+
const liveOptions = {
|
|
98
|
+
getRole: (name) => roleLookup(name),
|
|
99
|
+
get policies() {
|
|
100
|
+
return resolvePolicies();
|
|
101
|
+
},
|
|
102
|
+
rules: options?.rules,
|
|
22
103
|
policyTimeout: options?.policyTimeout,
|
|
104
|
+
algorithm: options?.algorithm,
|
|
105
|
+
cache: options?.cache,
|
|
106
|
+
cacheTtlMs: options?.cacheTtlMs,
|
|
107
|
+
permissionResolver: options?.permissionResolver,
|
|
108
|
+
roleResolver: options?.roleResolver,
|
|
109
|
+
expandImplied: options?.expandImplied,
|
|
110
|
+
onError: options?.onError,
|
|
23
111
|
};
|
|
112
|
+
const evaluatorOptions = () => liveOptions;
|
|
113
|
+
const emitter = options?.emitter;
|
|
114
|
+
/**
|
|
115
|
+
* Runs a check and emits an audit event whichever way it ends — including
|
|
116
|
+
* when it throws. An authorization trail that records only the successful
|
|
117
|
+
* paths is not a trail.
|
|
118
|
+
*/
|
|
119
|
+
async function runCheck(actor, permission, resource, authOptions) {
|
|
120
|
+
const start = performance.now();
|
|
121
|
+
let decision;
|
|
122
|
+
let failure;
|
|
123
|
+
try {
|
|
124
|
+
decision = await evaluate(actor, permission, resource, evaluatorOptions(), authOptions);
|
|
125
|
+
return decision;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
failure = error;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
emitter?.emit({
|
|
133
|
+
actorId: actor.id,
|
|
134
|
+
permission,
|
|
135
|
+
resourceType: resourceTypeOf(resource),
|
|
136
|
+
allowed: decision?.allowed ?? false,
|
|
137
|
+
reason: decision?.reason ??
|
|
138
|
+
(failure instanceof Error ? `error:${failure.name}` : undefined),
|
|
139
|
+
durationMs: performance.now() - start,
|
|
140
|
+
errored: failure !== undefined,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
24
144
|
return {
|
|
25
145
|
async can(actor, permission, resource, authOptions) {
|
|
26
|
-
const decision = await
|
|
146
|
+
const decision = await runCheck(actor, permission, resource, authOptions);
|
|
27
147
|
return decision.allowed;
|
|
28
148
|
},
|
|
29
149
|
async check(actor, permission, resource, authOptions) {
|
|
30
|
-
return
|
|
150
|
+
return runCheck(actor, permission, resource, authOptions);
|
|
31
151
|
},
|
|
32
152
|
async authorize(actor, permission, resource, authOptions) {
|
|
33
|
-
const decision = await
|
|
153
|
+
const decision = await runCheck(actor, permission, resource, authOptions);
|
|
34
154
|
if (!decision.allowed) {
|
|
35
|
-
|
|
155
|
+
// The public message names nothing internal; the reason and the
|
|
156
|
+
// policy travel in metadata for the log.
|
|
157
|
+
throw new PermissionDeniedError(decision.publicReason ?? "Access denied", {
|
|
36
158
|
actorId: actor.id,
|
|
37
159
|
permission,
|
|
160
|
+
reason: decision.reason,
|
|
161
|
+
policy: decision.policy,
|
|
38
162
|
});
|
|
39
163
|
}
|
|
40
164
|
},
|
|
41
165
|
async explain(actor, permission, resource, authOptions) {
|
|
42
|
-
return
|
|
166
|
+
return evaluateWithTrace(actor, permission, resource, evaluatorOptions(), authOptions);
|
|
43
167
|
},
|
|
44
168
|
createAbility(actor) {
|
|
45
|
-
return createAbility(actor, evaluatorOptions);
|
|
169
|
+
return createAbility(actor, evaluatorOptions(), emitter);
|
|
170
|
+
},
|
|
171
|
+
async invalidateActor(actorId) {
|
|
172
|
+
await options?.cache?.invalidateActor(actorId);
|
|
173
|
+
},
|
|
174
|
+
invalidateRoles() {
|
|
175
|
+
invalidateRoleCache();
|
|
46
176
|
},
|
|
47
177
|
};
|
|
48
178
|
}
|
|
179
|
+
/** Best-effort resource type for an audit event. */
|
|
180
|
+
function resourceTypeOf(resource) {
|
|
181
|
+
if (typeof resource !== "object" || resource === null)
|
|
182
|
+
return undefined;
|
|
183
|
+
const record = resource;
|
|
184
|
+
if (typeof record.type === "string")
|
|
185
|
+
return record.type;
|
|
186
|
+
const name = record.constructor?.name;
|
|
187
|
+
return name && name !== "Object" ? name : undefined;
|
|
188
|
+
}
|
|
49
189
|
//# sourceMappingURL=authorizationEngine.js.map
|
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Core permission evaluator
|
|
2
|
+
* Core permission evaluator.
|
|
3
|
+
*
|
|
4
|
+
* One evaluation path, with an optional trace collector. `check` and
|
|
5
|
+
* `explain` used to be ninety duplicated lines kept in sync by hand — in the
|
|
6
|
+
* one pair of functions that must agree, since `explain` exists to say why
|
|
7
|
+
* `check` decided what it did.
|
|
8
|
+
*
|
|
9
|
+
* @module evaluator/evaluator
|
|
3
10
|
*/
|
|
4
|
-
import type { PermissionActor, PermissionDecision, AuthorizationOptions } from "../permissionTypes/index.js";
|
|
5
|
-
import type
|
|
11
|
+
import type { PermissionActor, PermissionDecision, ExplainStep, ExplainResult, AuthorizationOptions } from "../permissionTypes/index.js";
|
|
12
|
+
import { type EvaluatorOptions } from "./evaluator.pipeline.js";
|
|
6
13
|
export { evaluateWithExplain } from "./evaluator.explain.js";
|
|
14
|
+
/** Collects the steps that explain a decision. */
|
|
15
|
+
export interface TraceCollector {
|
|
16
|
+
push(step: ExplainStep): void;
|
|
17
|
+
}
|
|
7
18
|
/**
|
|
8
19
|
* Evaluate a single permission check and return a decision.
|
|
20
|
+
*
|
|
21
|
+
* Every failure path denies. An unparsable permission, an unknown role, a
|
|
22
|
+
* policy that throws or times out — all of them produce a denial with a
|
|
23
|
+
* reason, never an exception that some outer handler might turn into a pass.
|
|
24
|
+
* The one exception is cancellation, which throws `AuthorizationAbortedError`
|
|
25
|
+
* because the caller asked for the work to stop.
|
|
9
26
|
*/
|
|
10
|
-
export declare function evaluate(actor: PermissionActor, permissionStr: string, resource: unknown, options: EvaluatorOptions, authOptions?: AuthorizationOptions): Promise<PermissionDecision>;
|
|
27
|
+
export declare function evaluate(actor: PermissionActor, permissionStr: string, resource: unknown, options: EvaluatorOptions, authOptions?: AuthorizationOptions, trace?: TraceCollector): Promise<PermissionDecision>;
|
|
28
|
+
/**
|
|
29
|
+
* Evaluate and collect the trace, in one pass.
|
|
30
|
+
*
|
|
31
|
+
* `check` and `explain` share this, so a trace can never describe a decision
|
|
32
|
+
* other than the one that was made.
|
|
33
|
+
*/
|
|
34
|
+
export declare function evaluateWithTrace(actor: PermissionActor, permissionStr: string, resource: unknown, options: EvaluatorOptions, authOptions?: AuthorizationOptions): Promise<ExplainResult>;
|
|
11
35
|
//# sourceMappingURL=evaluator.core.d.ts.map
|