@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,57 +1,277 @@
|
|
|
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 {
|
|
11
|
+
import { parsePermissionSafe, matches } from "../permission/permission.core.js";
|
|
12
|
+
import { InvalidPermissionError } from "../permissionErrors/index.js";
|
|
5
13
|
import { compileRules, findMatchingRules } from "../rule/ruleCompiler.js";
|
|
6
14
|
import { evaluateRules } from "../rule/rule.core.js";
|
|
7
|
-
import {
|
|
15
|
+
import { assertNotAborted, evaluatePolicies, resolveActorGrants, toMetadataMap, } from "./evaluator.pipeline.js";
|
|
16
|
+
import { permissionCacheKey } from "../cache/cache.core.js";
|
|
8
17
|
export { evaluateWithExplain } from "./evaluator.explain.js";
|
|
18
|
+
/** A public message that names nothing internal. */
|
|
19
|
+
const PUBLIC_DENIED = "Access denied";
|
|
20
|
+
function denied(reason, extra) {
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
allowed: false,
|
|
23
|
+
reason,
|
|
24
|
+
publicReason: PUBLIC_DENIED,
|
|
25
|
+
...extra,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
9
28
|
/**
|
|
10
29
|
* Evaluate a single permission check and return a decision.
|
|
30
|
+
*
|
|
31
|
+
* Every failure path denies. An unparsable permission, an unknown role, a
|
|
32
|
+
* policy that throws or times out — all of them produce a denial with a
|
|
33
|
+
* reason, never an exception that some outer handler might turn into a pass.
|
|
34
|
+
* The one exception is cancellation, which throws `AuthorizationAbortedError`
|
|
35
|
+
* because the caller asked for the work to stop.
|
|
11
36
|
*/
|
|
12
|
-
export async function evaluate(actor, permissionStr, resource, options, authOptions) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
export async function evaluate(actor, permissionStr, resource, options, authOptions, trace) {
|
|
38
|
+
const signal = authOptions?.signal;
|
|
39
|
+
assertNotAborted(signal);
|
|
40
|
+
const permission = parsePermissionSafe(permissionStr);
|
|
41
|
+
if (!permission) {
|
|
42
|
+
trace?.push({
|
|
43
|
+
type: "deny",
|
|
44
|
+
detail: `Malformed permission: ${permissionStr}`,
|
|
45
|
+
matched: false,
|
|
46
|
+
});
|
|
47
|
+
return denied("invalid_permission");
|
|
48
|
+
}
|
|
49
|
+
/* ── Explicit denies ─────────────────────────────────────────────────── */
|
|
50
|
+
// Denies are checked before the cache is consulted. `deniedPermissions`
|
|
51
|
+
// travels on the actor object, which is per-request data the cache key
|
|
52
|
+
// knows nothing about, so a cached allow from an earlier call would
|
|
53
|
+
// otherwise be served to a caller who has since denied the permission.
|
|
54
|
+
for (const deny of actor.deniedPermissions ?? []) {
|
|
55
|
+
// Denies go through the same matcher as grants. Comparing them as exact
|
|
56
|
+
// strings meant `*:delete` was ignored while `*:delete` as a grant was
|
|
57
|
+
// honoured — an asymmetry a deny list cannot survive.
|
|
58
|
+
if (!parsePermissionSafe(deny)) {
|
|
59
|
+
// A malformed deny can never match, so it would silently grant what it
|
|
60
|
+
// was written to forbid. Report it rather than dropping it in silence.
|
|
61
|
+
options.onError?.(new InvalidPermissionError(deny), "Actor.deniedPermissions");
|
|
62
|
+
trace?.push({
|
|
63
|
+
type: "deny",
|
|
64
|
+
detail: `Malformed deny ignored: ${deny}`,
|
|
65
|
+
matched: false,
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (matches(deny, permissionStr)) {
|
|
70
|
+
trace?.push({
|
|
71
|
+
type: "deny",
|
|
72
|
+
detail: `Explicit deny: ${deny}`,
|
|
73
|
+
matched: true,
|
|
74
|
+
});
|
|
75
|
+
return denied("explicit_deny", { matchedPermission: deny });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/* ── Cache ───────────────────────────────────────────────────────────── */
|
|
79
|
+
const resourceId = authOptions?.resourceId ?? resourceIdOf(resource);
|
|
80
|
+
// Only a decision the key can fully describe may be cached.
|
|
81
|
+
//
|
|
82
|
+
// A resource with no derivable id is the sharp case: the key would collapse
|
|
83
|
+
// to `actor|permission`, so an allow for `{ ownerId: "ada" }` would answer
|
|
84
|
+
// for `{ ownerId: "bob" }` on the next call. Request metadata is the other:
|
|
85
|
+
// `tenantIsolation()` reads the tenant from it, and it is not part of the
|
|
86
|
+
// key, so a decision made for one tenant must not answer for another.
|
|
87
|
+
const keyable = (resource === undefined || resourceId !== undefined) &&
|
|
88
|
+
!hasMetadata(authOptions?.metadata);
|
|
89
|
+
const cacheKey = options.cache && authOptions?.skipCache !== true && keyable
|
|
90
|
+
? permissionCacheKey(actor.id, permissionStr, resourceId)
|
|
91
|
+
: undefined;
|
|
92
|
+
if (options.cache && cacheKey) {
|
|
93
|
+
try {
|
|
94
|
+
const cached = await options.cache.get(cacheKey);
|
|
95
|
+
if (cached) {
|
|
96
|
+
trace?.push({
|
|
97
|
+
type: "cache",
|
|
98
|
+
detail: `Cache hit: ${cached.allowed ? "allow" : "deny"}`,
|
|
99
|
+
matched: cached.allowed,
|
|
23
100
|
});
|
|
101
|
+
return cached;
|
|
24
102
|
}
|
|
25
103
|
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
options.onError?.(error, "PermissionCache.get");
|
|
106
|
+
}
|
|
26
107
|
}
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
108
|
+
/* ── Grants ──────────────────────────────────────────────────────────── */
|
|
109
|
+
const grants = await resolveActorGrants(actor, options, signal);
|
|
110
|
+
assertNotAborted(signal);
|
|
111
|
+
for (const role of grants.unknownRoles) {
|
|
112
|
+
options.onError?.(new Error(`Unknown role "${role}" for actor "${actor.id}"`), "RoleHierarchy");
|
|
113
|
+
trace?.push({
|
|
114
|
+
type: "role",
|
|
115
|
+
detail: `Unknown role: ${role}`,
|
|
116
|
+
matched: false,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (trace) {
|
|
120
|
+
for (const role of actor.roles ?? []) {
|
|
121
|
+
trace.push({
|
|
122
|
+
type: "role",
|
|
123
|
+
detail: `Role: ${role}`,
|
|
124
|
+
matched: !grants.unknownRoles.includes(role),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const matchedPermissions = grants.permissions.filter((granted) => matches(granted, permissionStr));
|
|
129
|
+
if (trace) {
|
|
130
|
+
// Only the grants that bear on this decision. Listing every permission
|
|
131
|
+
// the actor holds turns a trace into a dump of their whole grant set.
|
|
132
|
+
for (const granted of matchedPermissions) {
|
|
133
|
+
trace.push({
|
|
134
|
+
type: "permission",
|
|
135
|
+
detail: `Permission: ${granted}`,
|
|
136
|
+
matched: true,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (matchedPermissions.length === 0) {
|
|
140
|
+
trace.push({
|
|
141
|
+
type: "permission",
|
|
142
|
+
detail: `No permission matched "${permissionStr}" among ${grants.permissions.length} granted`,
|
|
143
|
+
matched: false,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/* ── Rules ───────────────────────────────────────────────────────────── */
|
|
148
|
+
const context = {
|
|
149
|
+
actor,
|
|
150
|
+
permission,
|
|
151
|
+
resource,
|
|
152
|
+
metadata: toMetadataMap(authOptions?.metadata),
|
|
153
|
+
signal,
|
|
154
|
+
};
|
|
155
|
+
const rules = [
|
|
156
|
+
...matchedPermissions.map((granted) => ({
|
|
32
157
|
effect: "allow",
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
158
|
+
resource: parsePermissionSafe(granted)?.resource ?? granted,
|
|
159
|
+
action: parsePermissionSafe(granted)?.action ?? "*",
|
|
160
|
+
})),
|
|
161
|
+
...grants.rules,
|
|
162
|
+
];
|
|
163
|
+
const ruleResult = await evaluateRules(findMatchingRules(compileRules(rules), permission), permission, context, {
|
|
164
|
+
algorithm: options.algorithm,
|
|
165
|
+
onConditionError: (rule, error) => {
|
|
166
|
+
options.onError?.(error, `RuleCondition.${rule.name ?? "unnamed"}`);
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
if (trace) {
|
|
170
|
+
for (const rule of ruleResult.applicable) {
|
|
171
|
+
trace.push({
|
|
172
|
+
type: "rule",
|
|
173
|
+
detail: `Rule ${rule.effect}: ${rule.name ?? `${String(rule.resource)}:${String(rule.action)}`}`,
|
|
174
|
+
matched: true,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/* ── Policies ────────────────────────────────────────────────────────── */
|
|
179
|
+
const outcome = await evaluatePolicies(context, options.policies ?? [], options, authOptions);
|
|
180
|
+
if (trace) {
|
|
181
|
+
for (const name of outcome.evaluated) {
|
|
182
|
+
trace.push({
|
|
183
|
+
type: "policy",
|
|
184
|
+
detail: `Policy: ${name}`,
|
|
185
|
+
matched: outcome.decision?.allowed ?? true,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const decision = combine(ruleResult, outcome.decision, permissionStr);
|
|
190
|
+
/* ── Cache write ─────────────────────────────────────────────────────── */
|
|
191
|
+
if (options.cache && cacheKey && outcome.cacheable) {
|
|
192
|
+
try {
|
|
193
|
+
await options.cache.set(cacheKey, decision, {
|
|
194
|
+
ttl: options.cacheTtlMs,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
options.onError?.(error, "PermissionCache.set");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return decision;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Combine the rule outcome with the policy outcome.
|
|
205
|
+
*
|
|
206
|
+
* A denying policy always wins. An allowing policy can grant access the rules
|
|
207
|
+
* did not, which is what makes a policy an ABAC escape hatch rather than a
|
|
208
|
+
* filter — but it can never override a denial, and that includes a denial
|
|
209
|
+
* the *rules* produced. "The rules did not allow" covers two cases: no rule
|
|
210
|
+
* matched, and a deny rule matched. Treating them alike let an allowing
|
|
211
|
+
* policy for `post:*` cancel a `deny post:update` rule — the exact inversion
|
|
212
|
+
* of `deny-overrides`.
|
|
213
|
+
*/
|
|
214
|
+
function combine(ruleResult, policyDecision, permissionStr) {
|
|
215
|
+
if (policyDecision && !policyDecision.allowed)
|
|
216
|
+
return policyDecision;
|
|
217
|
+
const denyRule = !ruleResult.allowed && ruleResult.matchedRule?.effect === "deny"
|
|
218
|
+
? ruleResult.matchedRule
|
|
219
|
+
: undefined;
|
|
220
|
+
if (denyRule) {
|
|
221
|
+
return denied("rule_deny", {
|
|
222
|
+
matchedPermission: permissionStr,
|
|
223
|
+
...(denyRule.name ? { policy: denyRule.name } : {}),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (ruleResult.allowed) {
|
|
227
|
+
return Object.freeze({
|
|
228
|
+
allowed: true,
|
|
229
|
+
reason: "role_permission",
|
|
230
|
+
matchedPermission: permissionStr,
|
|
231
|
+
...(policyDecision?.policy ? { policy: policyDecision.policy } : {}),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (policyDecision?.allowed)
|
|
235
|
+
return policyDecision;
|
|
236
|
+
return denied("no_matching_rule");
|
|
237
|
+
}
|
|
238
|
+
/** True when the caller supplied request metadata the cache key cannot carry. */
|
|
239
|
+
function hasMetadata(metadata) {
|
|
240
|
+
if (!metadata)
|
|
241
|
+
return false;
|
|
242
|
+
if (metadata instanceof Map)
|
|
243
|
+
return metadata.size > 0;
|
|
244
|
+
return Object.keys(metadata).length > 0;
|
|
245
|
+
}
|
|
246
|
+
/** Best-effort resource identity for the cache key. */
|
|
247
|
+
function resourceIdOf(resource) {
|
|
248
|
+
if (typeof resource !== "object" || resource === null)
|
|
249
|
+
return undefined;
|
|
250
|
+
const id = resource.id;
|
|
251
|
+
if (typeof id === "string")
|
|
252
|
+
return id;
|
|
253
|
+
if (typeof id === "number")
|
|
254
|
+
return String(id);
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Evaluate and collect the trace, in one pass.
|
|
259
|
+
*
|
|
260
|
+
* `check` and `explain` share this, so a trace can never describe a decision
|
|
261
|
+
* other than the one that was made.
|
|
262
|
+
*/
|
|
263
|
+
export async function evaluateWithTrace(actor, permissionStr, resource, options, authOptions) {
|
|
264
|
+
const steps = [];
|
|
265
|
+
const decision = await evaluate(actor, permissionStr, resource, options, authOptions, { push: (step) => steps.push(step) });
|
|
266
|
+
steps.push({
|
|
267
|
+
type: decision.allowed ? "permission" : "deny",
|
|
268
|
+
detail: `Decision: ${decision.allowed ? "allow" : "deny"} (${decision.reason ?? "unknown"})`,
|
|
269
|
+
matched: decision.allowed,
|
|
36
270
|
});
|
|
37
|
-
const compiled = compileRules(rules);
|
|
38
|
-
const matching = findMatchingRules(compiled, permission);
|
|
39
|
-
const ruleResult = evaluateRules(matching, permission);
|
|
40
|
-
if (!ruleResult.allowed) {
|
|
41
|
-
const ctx = { actor, permission, resource };
|
|
42
|
-
const policyResult = await evaluatePolicies(ctx, options.policies ?? [], authOptions);
|
|
43
|
-
if (policyResult !== null)
|
|
44
|
-
return policyResult;
|
|
45
|
-
return Object.freeze({ allowed: false, reason: "no_matching_rule" });
|
|
46
|
-
}
|
|
47
|
-
const ctx = { actor, permission, resource };
|
|
48
|
-
const policyResult = await evaluatePolicies(ctx, options.policies ?? [], authOptions);
|
|
49
|
-
if (policyResult !== null)
|
|
50
|
-
return policyResult;
|
|
51
271
|
return Object.freeze({
|
|
52
|
-
allowed:
|
|
53
|
-
|
|
54
|
-
|
|
272
|
+
allowed: decision.allowed,
|
|
273
|
+
steps: Object.freeze(steps),
|
|
274
|
+
decision,
|
|
55
275
|
});
|
|
56
276
|
}
|
|
57
277
|
//# sourceMappingURL=evaluator.core.js.map
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Permission evaluator with full explain trace.
|
|
2
|
+
* Permission evaluator with a full explain trace.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* A thin alias over {@link evaluateWithTrace}: the trace is produced by the
|
|
5
|
+
* same pass that produces the decision, so the two cannot disagree.
|
|
6
|
+
*
|
|
7
|
+
* @module evaluator/evaluator.explain
|
|
5
8
|
*/
|
|
6
9
|
import type { PermissionActor, ExplainResult, AuthorizationOptions } from "../permissionTypes/index.js";
|
|
7
10
|
import type { EvaluatorOptions } from "./evaluator.pipeline.js";
|
|
8
11
|
/**
|
|
9
|
-
* Evaluate with full explain trace.
|
|
12
|
+
* Evaluate with a full explain trace.
|
|
10
13
|
*/
|
|
11
14
|
export declare function evaluateWithExplain(actor: PermissionActor, permissionStr: string, resource: unknown, options: EvaluatorOptions, authOptions?: AuthorizationOptions): Promise<ExplainResult>;
|
|
12
15
|
//# sourceMappingURL=evaluator.explain.d.ts.map
|
|
@@ -1,79 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Permission evaluator with full explain trace.
|
|
2
|
+
* Permission evaluator with a full explain trace.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* A thin alias over {@link evaluateWithTrace}: the trace is produced by the
|
|
5
|
+
* same pass that produces the decision, so the two cannot disagree.
|
|
6
|
+
*
|
|
7
|
+
* @module evaluator/evaluator.explain
|
|
5
8
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import { compileRules, findMatchingRules } from "../rule/ruleCompiler.js";
|
|
8
|
-
import { evaluateRules } from "../rule/rule.core.js";
|
|
9
|
-
import { resolveActorPermissions, evaluatePolicies, } from "./evaluator.pipeline.js";
|
|
9
|
+
import { evaluateWithTrace } from "./evaluator.core.js";
|
|
10
10
|
/**
|
|
11
|
-
* Evaluate with full explain trace.
|
|
11
|
+
* Evaluate with a full explain trace.
|
|
12
12
|
*/
|
|
13
13
|
export async function evaluateWithExplain(actor, permissionStr, resource, options, authOptions) {
|
|
14
|
-
|
|
15
|
-
const steps = [];
|
|
16
|
-
if (actor.deniedPermissions) {
|
|
17
|
-
for (const denied of actor.deniedPermissions) {
|
|
18
|
-
if (denied === permissionStr ||
|
|
19
|
-
denied === `${permission.resource}:*` ||
|
|
20
|
-
denied === "*:*") {
|
|
21
|
-
steps.push({
|
|
22
|
-
type: "deny",
|
|
23
|
-
detail: `Explicit deny: ${denied}`,
|
|
24
|
-
matched: true,
|
|
25
|
-
});
|
|
26
|
-
return Object.freeze({ allowed: false, steps });
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
const allPermissions = resolveActorPermissions(actor, options);
|
|
31
|
-
if (actor.roles) {
|
|
32
|
-
for (const role of actor.roles) {
|
|
33
|
-
steps.push({ type: "role", detail: `Role: ${role}`, matched: true });
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
const matchingPermissions = allPermissions.filter((p) => matches(p, permissionStr));
|
|
37
|
-
const rules = matchingPermissions.map((p) => {
|
|
38
|
-
const parsed = parsePermission(p);
|
|
39
|
-
return {
|
|
40
|
-
effect: "allow",
|
|
41
|
-
action: parsed.action,
|
|
42
|
-
resource: parsed.resource,
|
|
43
|
-
};
|
|
44
|
-
});
|
|
45
|
-
const compiled = compileRules(rules);
|
|
46
|
-
const matching = findMatchingRules(compiled, permission);
|
|
47
|
-
const ruleResult = evaluateRules(matching, permission);
|
|
48
|
-
for (const perm of allPermissions) {
|
|
49
|
-
const matched = matches(perm, permissionStr);
|
|
50
|
-
steps.push({ type: "permission", detail: `Permission: ${perm}`, matched });
|
|
51
|
-
}
|
|
52
|
-
if (!ruleResult.allowed) {
|
|
53
|
-
const ctx = { actor, permission, resource };
|
|
54
|
-
const policyResult = await evaluatePolicies(ctx, options.policies ?? [], authOptions);
|
|
55
|
-
if (policyResult !== null) {
|
|
56
|
-
steps.push({
|
|
57
|
-
type: "policy",
|
|
58
|
-
detail: `Policy: ${policyResult.reason ?? "unknown"}`,
|
|
59
|
-
matched: policyResult.allowed,
|
|
60
|
-
});
|
|
61
|
-
return Object.freeze({ allowed: policyResult.allowed, steps });
|
|
62
|
-
}
|
|
63
|
-
steps.push({ type: "deny", detail: "No matching rule", matched: false });
|
|
64
|
-
return Object.freeze({ allowed: false, steps });
|
|
65
|
-
}
|
|
66
|
-
const ctx = { actor, permission, resource };
|
|
67
|
-
const policyResult = await evaluatePolicies(ctx, options.policies ?? [], authOptions);
|
|
68
|
-
if (policyResult !== null) {
|
|
69
|
-
steps.push({
|
|
70
|
-
type: "policy",
|
|
71
|
-
detail: `Policy: ${policyResult.reason ?? "unknown"}`,
|
|
72
|
-
matched: policyResult.allowed,
|
|
73
|
-
});
|
|
74
|
-
return Object.freeze({ allowed: policyResult.allowed, steps });
|
|
75
|
-
}
|
|
76
|
-
steps.push({ type: "permission", detail: "Rule matched", matched: true });
|
|
77
|
-
return Object.freeze({ allowed: true, steps });
|
|
14
|
+
return evaluateWithTrace(actor, permissionStr, resource, options, authOptions);
|
|
78
15
|
}
|
|
79
16
|
//# sourceMappingURL=evaluator.explain.js.map
|
|
@@ -3,26 +3,87 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module evaluator/evaluator.pipeline
|
|
5
5
|
*/
|
|
6
|
-
import type { PermissionActor, PermissionContext, PermissionDecision, PermissionPolicyDefinition, AuthorizationOptions } from "../permissionTypes/index.js";
|
|
6
|
+
import type { PermissionActor, PermissionContext, PermissionDecision, PermissionPolicyDefinition, PermissionRule, PermissionCache, PermissionResolver, RoleDefinition, RoleResolver, RuleCombiningAlgorithm, AuthorizationOptions } from "../permissionTypes/index.js";
|
|
7
7
|
/** Configuration for the evaluator. */
|
|
8
8
|
export interface EvaluatorOptions {
|
|
9
9
|
/** Function to look up a role definition by name. */
|
|
10
|
-
readonly getRole?: (name: string) =>
|
|
11
|
-
readonly name: string;
|
|
12
|
-
readonly permissions: readonly string[];
|
|
13
|
-
readonly inherits?: readonly string[];
|
|
14
|
-
} | undefined;
|
|
10
|
+
readonly getRole?: (name: string) => RoleDefinition | undefined;
|
|
15
11
|
/** Registered policies. */
|
|
16
12
|
readonly policies?: readonly PermissionPolicyDefinition[];
|
|
13
|
+
/** Static rules, evaluated alongside the actor's permissions. */
|
|
14
|
+
readonly rules?: readonly PermissionRule[];
|
|
17
15
|
/** Default timeout for async policy evaluation (ms). */
|
|
18
16
|
readonly policyTimeout?: number;
|
|
17
|
+
/** How competing rules combine. Default: `"deny-overrides"`. */
|
|
18
|
+
readonly algorithm?: RuleCombiningAlgorithm;
|
|
19
|
+
/** Decision cache. */
|
|
20
|
+
readonly cache?: PermissionCache;
|
|
21
|
+
/** Time-to-live for cached decisions, in ms. */
|
|
22
|
+
readonly cacheTtlMs?: number;
|
|
23
|
+
/** Loads additional rules for an actor from an external source. */
|
|
24
|
+
readonly permissionResolver?: PermissionResolver;
|
|
25
|
+
/** Loads additional roles for an actor from an external source. */
|
|
26
|
+
readonly roleResolver?: RoleResolver;
|
|
27
|
+
/**
|
|
28
|
+
* Expands a permission into the ones it implies, so that granting
|
|
29
|
+
* `post:admin` can carry `post:read` with it.
|
|
30
|
+
*/
|
|
31
|
+
readonly expandImplied?: (permission: string) => readonly string[];
|
|
32
|
+
/** Reports a failure that authorization swallowed to stay fail-closed. */
|
|
33
|
+
readonly onError?: (error: unknown, source: string) => void;
|
|
19
34
|
}
|
|
35
|
+
/** The permissions and rules an actor holds, once everything is resolved. */
|
|
36
|
+
export interface ResolvedGrants {
|
|
37
|
+
readonly permissions: readonly string[];
|
|
38
|
+
readonly rules: readonly PermissionRule[];
|
|
39
|
+
/** Roles that could not be resolved. Their permissions are absent. */
|
|
40
|
+
readonly unknownRoles: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
/** Throws if the caller has cancelled the check. */
|
|
43
|
+
export declare function assertNotAborted(signal?: AbortSignal): void;
|
|
44
|
+
/** Normalizes caller-supplied metadata into the map conditions read. */
|
|
45
|
+
export declare function toMetadataMap(metadata: AuthorizationOptions["metadata"]): ReadonlyMap<string, unknown> | undefined;
|
|
20
46
|
/**
|
|
21
|
-
* Resolve
|
|
47
|
+
* Resolve everything an actor holds: direct permissions, role permissions
|
|
48
|
+
* (following inheritance), resolver-supplied roles and rules.
|
|
49
|
+
*
|
|
50
|
+
* A role the lookup cannot find is reported rather than thrown. An unknown
|
|
51
|
+
* role in a token is a data problem, and turning every request from that
|
|
52
|
+
* actor into an exception hands the decision to whichever error handler
|
|
53
|
+
* happens to be installed — which may well be more permissive than a denial.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveActorGrants(actor: PermissionActor, options: EvaluatorOptions, signal?: AbortSignal): Promise<ResolvedGrants>;
|
|
56
|
+
/**
|
|
57
|
+
* Resolve all permission strings for an actor (direct + role-based).
|
|
58
|
+
*
|
|
59
|
+
* Kept for callers that only need the strings; the engine uses
|
|
60
|
+
* {@link resolveActorGrants}, which also carries rules and unknown roles.
|
|
22
61
|
*/
|
|
23
62
|
export declare function resolveActorPermissions(actor: PermissionActor, options: EvaluatorOptions): readonly string[];
|
|
63
|
+
/** Policies that apply to a permission, most important first. */
|
|
64
|
+
export declare function selectPolicies(policies: readonly PermissionPolicyDefinition[], permissionStr: string): readonly PermissionPolicyDefinition[];
|
|
65
|
+
/** The outcome of running the applicable policies. */
|
|
66
|
+
export interface PolicyOutcome {
|
|
67
|
+
/** `null` when no policy applied. */
|
|
68
|
+
readonly decision: PermissionDecision | null;
|
|
69
|
+
/** Whether every policy that ran allows the result to be cached. */
|
|
70
|
+
readonly cacheable: boolean;
|
|
71
|
+
readonly evaluated: readonly string[];
|
|
72
|
+
}
|
|
24
73
|
/**
|
|
25
|
-
* Evaluate policies for a context.
|
|
74
|
+
* Evaluate policies for a context.
|
|
75
|
+
*
|
|
76
|
+
* Policies are evaluated highest priority first and short-circuit on the
|
|
77
|
+
* first denial. A policy that throws or times out denies — an authorization
|
|
78
|
+
* check that cannot complete must not fall through to "allowed".
|
|
79
|
+
*/
|
|
80
|
+
export declare function evaluatePolicies(context: PermissionContext, policies: readonly PermissionPolicyDefinition[], options: EvaluatorOptions, authOptions?: AuthorizationOptions): Promise<PolicyOutcome>;
|
|
81
|
+
/**
|
|
82
|
+
* Run a promise with an optional timeout.
|
|
83
|
+
*
|
|
84
|
+
* `0` means "expire immediately", not "no timeout" — treating a falsy value
|
|
85
|
+
* as "disabled" silently turned `policyTimeout: 0` into no timeout at all.
|
|
86
|
+
* Pass `undefined` to disable.
|
|
26
87
|
*/
|
|
27
|
-
export declare function
|
|
88
|
+
export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number | undefined, policyName?: string): Promise<T>;
|
|
28
89
|
//# sourceMappingURL=evaluator.pipeline.d.ts.map
|