@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
|
@@ -4,80 +4,225 @@
|
|
|
4
4
|
* @module evaluator/evaluator.pipeline
|
|
5
5
|
*/
|
|
6
6
|
import { resolveRolePermissions } from "../role/roleHierarchy.js";
|
|
7
|
-
import {
|
|
7
|
+
import { matches } from "../permission/permission.core.js";
|
|
8
|
+
import { AuthorizationAbortedError, PermissionResolverError, PolicyError, PolicyTimeoutError, } from "../permissionErrors/index.js";
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* Normalize a thrown value into an `Error` before it reaches `onError`.
|
|
11
|
+
*
|
|
12
|
+
* A resolver or a policy is caller code and may throw anything at all — a
|
|
13
|
+
* string, `undefined`, a plain object. An `onError` handler that reads
|
|
14
|
+
* `error.message` then fails inside the failure path, so anything that is not
|
|
15
|
+
* already an `Error` is wrapped, keeping the original as `cause`.
|
|
10
16
|
*/
|
|
11
|
-
|
|
17
|
+
function reportable(value, wrap) {
|
|
18
|
+
return value instanceof Error ? value : wrap(value);
|
|
19
|
+
}
|
|
20
|
+
/** Throws if the caller has cancelled the check. */
|
|
21
|
+
export function assertNotAborted(signal) {
|
|
22
|
+
if (signal?.aborted)
|
|
23
|
+
throw new AuthorizationAbortedError();
|
|
24
|
+
}
|
|
25
|
+
/** Normalizes caller-supplied metadata into the map conditions read. */
|
|
26
|
+
export function toMetadataMap(metadata) {
|
|
27
|
+
if (!metadata)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (metadata instanceof Map)
|
|
30
|
+
return metadata;
|
|
31
|
+
return new Map(Object.entries(metadata));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve everything an actor holds: direct permissions, role permissions
|
|
35
|
+
* (following inheritance), resolver-supplied roles and rules.
|
|
36
|
+
*
|
|
37
|
+
* A role the lookup cannot find is reported rather than thrown. An unknown
|
|
38
|
+
* role in a token is a data problem, and turning every request from that
|
|
39
|
+
* actor into an exception hands the decision to whichever error handler
|
|
40
|
+
* happens to be installed — which may well be more permissive than a denial.
|
|
41
|
+
*/
|
|
42
|
+
export async function resolveActorGrants(actor, options, signal) {
|
|
12
43
|
const permissions = new Set();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
44
|
+
const rules = [...(options.rules ?? [])];
|
|
45
|
+
const unknownRoles = [];
|
|
46
|
+
for (const permission of actor.permissions ?? []) {
|
|
47
|
+
permissions.add(permission);
|
|
48
|
+
}
|
|
49
|
+
const roleNames = new Set(actor.roles ?? []);
|
|
50
|
+
if (options.roleResolver) {
|
|
51
|
+
assertNotAborted(signal);
|
|
52
|
+
try {
|
|
53
|
+
for (const role of await options.roleResolver.resolveRoles(actor)) {
|
|
54
|
+
roleNames.add(role);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
options.onError?.(reportable(error, (cause) => new PermissionResolverError("RoleResolver.resolveRoles failed", cause)), "RoleResolver.resolveRoles");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (roleNames.size > 0 && options.getRole) {
|
|
62
|
+
try {
|
|
63
|
+
const resolution = resolveRolePermissions([...roleNames], options.getRole, { onUnknownRole: (name) => unknownRoles.push(name) });
|
|
64
|
+
for (const permission of resolution.permissions)
|
|
65
|
+
permissions.add(permission);
|
|
66
|
+
rules.push(...resolution.rules);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
// A cycle in the role graph, or a role source that throws, is a
|
|
70
|
+
// configuration failure. Throwing here would hand the outcome to
|
|
71
|
+
// whichever error handler the caller installed, which may well be more
|
|
72
|
+
// permissive than a denial — so every role is treated as unresolved and
|
|
73
|
+
// the check falls through to deny.
|
|
74
|
+
options.onError?.(error, "RoleHierarchy");
|
|
75
|
+
for (const name of roleNames) {
|
|
76
|
+
if (!unknownRoles.includes(name))
|
|
77
|
+
unknownRoles.push(name);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
17
80
|
}
|
|
18
|
-
|
|
81
|
+
if (options.permissionResolver) {
|
|
82
|
+
assertNotAborted(signal);
|
|
83
|
+
try {
|
|
84
|
+
rules.push(...(await options.permissionResolver.resolvePermissions(actor)));
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
options.onError?.(reportable(error, (cause) => new PermissionResolverError("PermissionResolver.resolvePermissions failed", cause)), "PermissionResolver.resolvePermissions");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (options.expandImplied) {
|
|
91
|
+
for (const permission of [...permissions]) {
|
|
92
|
+
for (const implied of options.expandImplied(permission)) {
|
|
93
|
+
permissions.add(implied);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
permissions: [...permissions],
|
|
99
|
+
rules,
|
|
100
|
+
unknownRoles,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Resolve all permission strings for an actor (direct + role-based).
|
|
105
|
+
*
|
|
106
|
+
* Kept for callers that only need the strings; the engine uses
|
|
107
|
+
* {@link resolveActorGrants}, which also carries rules and unknown roles.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveActorPermissions(actor, options) {
|
|
110
|
+
const permissions = new Set(actor.permissions ?? []);
|
|
19
111
|
if (actor.roles && options.getRole) {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
112
|
+
const resolution = resolveRolePermissions(actor.roles, options.getRole, {
|
|
113
|
+
onUnknownRole: () => { },
|
|
114
|
+
});
|
|
115
|
+
for (const permission of resolution.permissions)
|
|
116
|
+
permissions.add(permission);
|
|
23
117
|
}
|
|
24
|
-
return
|
|
118
|
+
return [...permissions];
|
|
119
|
+
}
|
|
120
|
+
/** Policies that apply to a permission, most important first. */
|
|
121
|
+
export function selectPolicies(policies, permissionStr) {
|
|
122
|
+
return policies
|
|
123
|
+
.filter((policy) =>
|
|
124
|
+
// Wildcards work here for the same reason they work for grants: a
|
|
125
|
+
// policy registered for "post:*" is meant to cover "post:update".
|
|
126
|
+
policy.permissions.some((pattern) => matches(pattern, permissionStr)))
|
|
127
|
+
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
25
128
|
}
|
|
26
129
|
/**
|
|
27
|
-
* Evaluate policies for a context.
|
|
130
|
+
* Evaluate policies for a context.
|
|
131
|
+
*
|
|
132
|
+
* Policies are evaluated highest priority first and short-circuit on the
|
|
133
|
+
* first denial. A policy that throws or times out denies — an authorization
|
|
134
|
+
* check that cannot complete must not fall through to "allowed".
|
|
28
135
|
*/
|
|
29
|
-
export async function evaluatePolicies(context, policies, authOptions) {
|
|
30
|
-
if (policies.length === 0)
|
|
31
|
-
return null;
|
|
136
|
+
export async function evaluatePolicies(context, policies, options, authOptions) {
|
|
137
|
+
if (policies.length === 0) {
|
|
138
|
+
return { decision: null, cacheable: true, evaluated: [] };
|
|
139
|
+
}
|
|
32
140
|
const permissionStr = `${context.permission.resource}:${context.permission.action}`;
|
|
33
|
-
const applicable = policies
|
|
34
|
-
if (applicable.length === 0)
|
|
35
|
-
return null;
|
|
141
|
+
const applicable = selectPolicies(policies, permissionStr);
|
|
142
|
+
if (applicable.length === 0) {
|
|
143
|
+
return { decision: null, cacheable: true, evaluated: [] };
|
|
144
|
+
}
|
|
145
|
+
// The per-call timeout wins, but the engine-level default is what makes a
|
|
146
|
+
// configured timeout mean anything at all.
|
|
147
|
+
const timeoutMs = authOptions?.policyTimeout ?? options.policyTimeout;
|
|
148
|
+
const evaluated = [];
|
|
149
|
+
let cacheable = true;
|
|
36
150
|
for (const policy of applicable) {
|
|
151
|
+
assertNotAborted(context.signal ?? authOptions?.signal);
|
|
152
|
+
evaluated.push(policy.name);
|
|
153
|
+
if (policy.cacheable === false)
|
|
154
|
+
cacheable = false;
|
|
37
155
|
try {
|
|
38
|
-
const result = await withTimeout(Promise.resolve(policy.evaluate(context)),
|
|
156
|
+
const result = await withTimeout(Promise.resolve(policy.evaluate(context)), timeoutMs, policy.name);
|
|
39
157
|
if (!result.allowed) {
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
158
|
+
return {
|
|
159
|
+
decision: Object.freeze({
|
|
160
|
+
allowed: false,
|
|
161
|
+
reason: result.reason ?? `policy:${policy.name}`,
|
|
162
|
+
policy: policy.name,
|
|
163
|
+
publicReason: result.publicReason ?? "Access denied",
|
|
164
|
+
}),
|
|
165
|
+
cacheable,
|
|
166
|
+
evaluated,
|
|
167
|
+
};
|
|
45
168
|
}
|
|
46
169
|
}
|
|
47
170
|
catch (error) {
|
|
48
171
|
if (error instanceof AuthorizationAbortedError)
|
|
49
172
|
throw error;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
173
|
+
options.onError?.(reportable(error, (cause) => new PolicyError(policy.name, cause)), `Policy.${policy.name}`);
|
|
174
|
+
// Policy error — fail closed.
|
|
175
|
+
return {
|
|
176
|
+
decision: Object.freeze({
|
|
177
|
+
allowed: false,
|
|
178
|
+
reason: `policy_error:${policy.name}`,
|
|
179
|
+
policy: policy.name,
|
|
180
|
+
publicReason: "Access denied",
|
|
181
|
+
}),
|
|
182
|
+
cacheable: false,
|
|
183
|
+
evaluated,
|
|
184
|
+
};
|
|
56
185
|
}
|
|
57
186
|
}
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
187
|
+
return {
|
|
188
|
+
decision: Object.freeze({
|
|
189
|
+
allowed: true,
|
|
190
|
+
reason: "policy_allow",
|
|
191
|
+
policy: applicable.map((policy) => policy.name).join(","),
|
|
192
|
+
}),
|
|
193
|
+
cacheable,
|
|
194
|
+
evaluated,
|
|
195
|
+
};
|
|
63
196
|
}
|
|
64
197
|
/**
|
|
65
198
|
* Run a promise with an optional timeout.
|
|
199
|
+
*
|
|
200
|
+
* `0` means "expire immediately", not "no timeout" — treating a falsy value
|
|
201
|
+
* as "disabled" silently turned `policyTimeout: 0` into no timeout at all.
|
|
202
|
+
* Pass `undefined` to disable.
|
|
66
203
|
*/
|
|
67
|
-
async function withTimeout(promise, timeoutMs) {
|
|
68
|
-
if (
|
|
204
|
+
export async function withTimeout(promise, timeoutMs, policyName = "policy") {
|
|
205
|
+
if (timeoutMs === undefined)
|
|
69
206
|
return promise;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
207
|
+
let timer;
|
|
208
|
+
// Keep the loser handled so a late rejection cannot escape as an unhandled
|
|
209
|
+
// rejection once the race is over.
|
|
210
|
+
void promise.catch(() => { });
|
|
211
|
+
try {
|
|
212
|
+
return await Promise.race([
|
|
213
|
+
promise,
|
|
214
|
+
new Promise((_resolve, reject) => {
|
|
215
|
+
timer = setTimeout(() => {
|
|
216
|
+
reject(new PolicyTimeoutError(policyName, timeoutMs));
|
|
217
|
+
}, Math.max(0, timeoutMs));
|
|
218
|
+
if (typeof timer === "object" && "unref" in timer)
|
|
219
|
+
timer.unref();
|
|
220
|
+
}),
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
if (timer !== undefined)
|
|
78
225
|
clearTimeout(timer);
|
|
79
|
-
|
|
80
|
-
});
|
|
81
|
-
});
|
|
226
|
+
}
|
|
82
227
|
}
|
|
83
228
|
//# sourceMappingURL=evaluator.pipeline.js.map
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module evaluator
|
|
5
5
|
*/
|
|
6
|
-
export { evaluate,
|
|
7
|
-
export
|
|
6
|
+
export { evaluate, evaluateWithTrace } from "./evaluator.core.js";
|
|
7
|
+
export { evaluateWithExplain } from "./evaluator.explain.js";
|
|
8
|
+
export { resolveActorGrants, resolveActorPermissions, evaluatePolicies, selectPolicies, withTimeout, toMetadataMap, assertNotAborted, type EvaluatorOptions, type ResolvedGrants, type PolicyOutcome, } from "./evaluator.pipeline.js";
|
|
9
|
+
export { createPermissionEngine, type PermissionEngine, type PermissionEngineOptions, } from "./authorizationEngine.js";
|
|
8
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/evaluator/index.js
CHANGED
|
@@ -3,5 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module evaluator
|
|
5
5
|
*/
|
|
6
|
-
export { evaluate,
|
|
6
|
+
export { evaluate, evaluateWithTrace } from "./evaluator.core.js";
|
|
7
|
+
export { evaluateWithExplain } from "./evaluator.explain.js";
|
|
8
|
+
export { resolveActorGrants, resolveActorPermissions, evaluatePolicies, selectPolicies, withTimeout, toMetadataMap, assertNotAborted, } from "./evaluator.pipeline.js";
|
|
9
|
+
export { createPermissionEngine, } from "./authorizationEngine.js";
|
|
7
10
|
//# sourceMappingURL=index.js.map
|
|
@@ -6,23 +6,39 @@
|
|
|
6
6
|
import type { PermissionDecision } from "../permissionTypes/index.js";
|
|
7
7
|
/** Options for creating denied responses. */
|
|
8
8
|
export interface DeniedResponseOptions {
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Builds the 403 body. Receives the real decision, not a reconstruction of
|
|
11
|
+
* it, so `policy`, `matchedPermission` and `metadata` are all available.
|
|
12
|
+
*/
|
|
10
13
|
readonly deniedResponse?: (decision: PermissionDecision) => unknown;
|
|
14
|
+
/** Builds the 401 body for an unauthenticated request. */
|
|
15
|
+
readonly unauthenticatedResponse?: () => unknown;
|
|
16
|
+
/** Value for the `WWW-Authenticate` header on a 401. */
|
|
17
|
+
readonly authenticateChallenge?: string;
|
|
18
|
+
}
|
|
19
|
+
/** A framework-agnostic response. */
|
|
20
|
+
export interface PermissionHttpResponse {
|
|
21
|
+
readonly status: number;
|
|
22
|
+
readonly body: unknown;
|
|
23
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
11
24
|
}
|
|
12
25
|
/**
|
|
13
|
-
* Create a 403 Forbidden JSON response.
|
|
26
|
+
* Create a 403 Forbidden JSON response for a decision.
|
|
27
|
+
*
|
|
28
|
+
* The default body carries `decision.publicReason`, never `decision.reason`:
|
|
29
|
+
* the internal reason names policies and rules (`policy_error:billingOwner`),
|
|
30
|
+
* which tells a prober about the authorization model.
|
|
14
31
|
*/
|
|
15
|
-
export declare function createForbiddenResponse(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
export declare function createForbiddenResponse(decision: PermissionDecision, options?: DeniedResponseOptions): PermissionHttpResponse;
|
|
33
|
+
/**
|
|
34
|
+
* Create a 401 Unauthorized JSON response.
|
|
35
|
+
*
|
|
36
|
+
* A missing actor is not the same as a denied one: answering both with 403
|
|
37
|
+
* leaves a client unable to tell "log in" from "you may not do this".
|
|
38
|
+
*/
|
|
39
|
+
export declare function createUnauthorizedResponse(options?: DeniedResponseOptions): PermissionHttpResponse;
|
|
20
40
|
/**
|
|
21
41
|
* Create a JSON response.
|
|
22
42
|
*/
|
|
23
|
-
export declare function createJsonResponse(status: number, body: unknown):
|
|
24
|
-
readonly status: number;
|
|
25
|
-
readonly body: unknown;
|
|
26
|
-
readonly headers: Record<string, string>;
|
|
27
|
-
};
|
|
43
|
+
export declare function createJsonResponse(status: number, body: unknown): PermissionHttpResponse;
|
|
28
44
|
//# sourceMappingURL=httpHelpers.d.ts.map
|
package/dist/http/httpHelpers.js
CHANGED
|
@@ -3,17 +3,44 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module http/httpHelpers
|
|
5
5
|
*/
|
|
6
|
+
const JSON_HEADERS = { "content-type": "application/json" };
|
|
6
7
|
/**
|
|
7
|
-
* Create a 403 Forbidden JSON response.
|
|
8
|
+
* Create a 403 Forbidden JSON response for a decision.
|
|
9
|
+
*
|
|
10
|
+
* The default body carries `decision.publicReason`, never `decision.reason`:
|
|
11
|
+
* the internal reason names policies and rules (`policy_error:billingOwner`),
|
|
12
|
+
* which tells a prober about the authorization model.
|
|
8
13
|
*/
|
|
9
|
-
export function createForbiddenResponse(
|
|
14
|
+
export function createForbiddenResponse(decision, options) {
|
|
10
15
|
const body = options?.deniedResponse
|
|
11
|
-
? options.deniedResponse(
|
|
12
|
-
: {
|
|
16
|
+
? options.deniedResponse(decision)
|
|
17
|
+
: {
|
|
18
|
+
error: "Forbidden",
|
|
19
|
+
message: decision.publicReason ?? "Access denied",
|
|
20
|
+
};
|
|
13
21
|
return Object.freeze({
|
|
14
22
|
status: 403,
|
|
15
23
|
body,
|
|
16
|
-
headers:
|
|
24
|
+
headers: JSON_HEADERS,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create a 401 Unauthorized JSON response.
|
|
29
|
+
*
|
|
30
|
+
* A missing actor is not the same as a denied one: answering both with 403
|
|
31
|
+
* leaves a client unable to tell "log in" from "you may not do this".
|
|
32
|
+
*/
|
|
33
|
+
export function createUnauthorizedResponse(options) {
|
|
34
|
+
const body = options?.unauthenticatedResponse
|
|
35
|
+
? options.unauthenticatedResponse()
|
|
36
|
+
: { error: "Unauthorized", message: "Authentication required" };
|
|
37
|
+
return Object.freeze({
|
|
38
|
+
status: 401,
|
|
39
|
+
body,
|
|
40
|
+
headers: Object.freeze({
|
|
41
|
+
...JSON_HEADERS,
|
|
42
|
+
"www-authenticate": options?.authenticateChallenge ?? "Bearer",
|
|
43
|
+
}),
|
|
17
44
|
});
|
|
18
45
|
}
|
|
19
46
|
/**
|
|
@@ -23,7 +50,7 @@ export function createJsonResponse(status, body) {
|
|
|
23
50
|
return Object.freeze({
|
|
24
51
|
status,
|
|
25
52
|
body,
|
|
26
|
-
headers:
|
|
53
|
+
headers: JSON_HEADERS,
|
|
27
54
|
});
|
|
28
55
|
}
|
|
29
56
|
//# sourceMappingURL=httpHelpers.js.map
|
|
@@ -6,43 +6,84 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module http/httpMiddleware
|
|
8
8
|
*/
|
|
9
|
-
import type { PermissionActor,
|
|
9
|
+
import type { PermissionActor, AuthorizationOptions } from "../permissionTypes/index.js";
|
|
10
10
|
import type { PermissionEngine } from "../evaluator/authorizationEngine.js";
|
|
11
11
|
import type { HttpMiddleware, HttpMiddlewareContext } from "./httpTypes.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
import { type DeniedResponseOptions } from "./httpHelpers.js";
|
|
13
|
+
/** Options shared by the permission middleware. */
|
|
14
|
+
export interface AuthorizeMiddlewareOptions extends DeniedResponseOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Extracts an actor from the request context.
|
|
17
|
+
*
|
|
18
|
+
* The guard middleware calls this itself when no actor is already in
|
|
19
|
+
* state, so `authorize(engine, permission, { extractActor })` works on its
|
|
20
|
+
* own — installing `createActorMiddleware` first is an optimisation, not a
|
|
21
|
+
* requirement.
|
|
22
|
+
*/
|
|
23
|
+
readonly extractActor?: (context: HttpMiddlewareContext) => PermissionActor | Promise<PermissionActor> | undefined;
|
|
24
|
+
/** Authorization options (policyTimeout, metadata). */
|
|
17
25
|
readonly authorization?: AuthorizationOptions;
|
|
18
|
-
/**
|
|
19
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Forwards `context.signal` to the engine so a client disconnect stops
|
|
28
|
+
* policy evaluation. Default: `true`.
|
|
29
|
+
*/
|
|
30
|
+
readonly forwardSignal?: boolean;
|
|
31
|
+
/** Builds per-request metadata for conditions and policies. */
|
|
32
|
+
readonly extractMetadata?: (context: HttpMiddlewareContext) => Record<string, unknown> | undefined;
|
|
20
33
|
}
|
|
21
34
|
/** Options for the requirePermission middleware. */
|
|
22
35
|
export interface RequirePermissionMiddlewareOptions extends AuthorizeMiddlewareOptions {
|
|
23
36
|
/** The permission to check (e.g. "post:update"). */
|
|
24
37
|
readonly permission: string;
|
|
25
|
-
/**
|
|
38
|
+
/** Extracts the resource from the request (optional). */
|
|
26
39
|
readonly extractResource?: (context: HttpMiddlewareContext) => unknown;
|
|
27
40
|
}
|
|
41
|
+
/** Options for {@link createActorMiddleware}. */
|
|
42
|
+
export interface ActorMiddlewareOptions extends AuthorizeMiddlewareOptions {
|
|
43
|
+
/** Required: this middleware exists to run it. */
|
|
44
|
+
readonly extractActor: (context: HttpMiddlewareContext) => PermissionActor | Promise<PermissionActor> | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Answer 401 immediately when no actor could be extracted, instead of
|
|
47
|
+
* letting the request continue to whatever comes next. Default: `false`,
|
|
48
|
+
* preserving the "authenticate here, authorize later" split.
|
|
49
|
+
*/
|
|
50
|
+
readonly requireActor?: boolean;
|
|
51
|
+
}
|
|
28
52
|
/** State key for the current actor. */
|
|
29
53
|
export declare const ACTOR_STATE_KEY = "permissions:actor";
|
|
30
54
|
/** State key for the authorization decision. */
|
|
31
55
|
export declare const DECISION_STATE_KEY = "permissions:decision";
|
|
56
|
+
/** State key for the batch decision map. */
|
|
57
|
+
export declare const DECISIONS_STATE_KEY = "permissions:decisions";
|
|
32
58
|
/**
|
|
33
59
|
* Create middleware that extracts the actor from the request and stores it in state.
|
|
34
60
|
*/
|
|
35
|
-
export declare function createActorMiddleware(options:
|
|
61
|
+
export declare function createActorMiddleware(options: ActorMiddlewareOptions): HttpMiddleware;
|
|
36
62
|
/**
|
|
37
63
|
* Create middleware that checks a permission and returns 403 if denied.
|
|
64
|
+
*
|
|
65
|
+
* An unauthenticated request gets 401; an authenticated one that is not
|
|
66
|
+
* permitted gets 403.
|
|
38
67
|
*/
|
|
39
68
|
export declare function createRequirePermissionMiddleware(engine: PermissionEngine, options: RequirePermissionMiddlewareOptions): HttpMiddleware;
|
|
40
69
|
/**
|
|
41
70
|
* Create middleware that checks a permission and short-circuits on denial.
|
|
42
71
|
*/
|
|
43
|
-
export declare function authorize(engine: PermissionEngine, permission: string, options
|
|
72
|
+
export declare function authorize(engine: PermissionEngine, permission: string, options?: Omit<RequirePermissionMiddlewareOptions, "permission">): HttpMiddleware;
|
|
73
|
+
/** Options for {@link createRequirePermissionsMiddleware}. */
|
|
74
|
+
export interface RequirePermissionsMiddlewareOptions extends AuthorizeMiddlewareOptions {
|
|
75
|
+
/** Extracts the resource checked for every permission (optional). */
|
|
76
|
+
readonly extractResource?: (context: HttpMiddlewareContext) => unknown;
|
|
77
|
+
/**
|
|
78
|
+
* `"all"` (default) requires every permission; `"any"` requires one.
|
|
79
|
+
*/
|
|
80
|
+
readonly mode?: "all" | "any";
|
|
81
|
+
}
|
|
44
82
|
/**
|
|
45
|
-
* Create middleware that checks multiple permissions
|
|
83
|
+
* Create middleware that checks multiple permissions.
|
|
84
|
+
*
|
|
85
|
+
* Under `"all"` the first denial short-circuits: evaluating the rest costs
|
|
86
|
+
* policy calls and timeouts for an answer that is already decided.
|
|
46
87
|
*/
|
|
47
|
-
export declare function createRequirePermissionsMiddleware(engine: PermissionEngine, permissions: readonly string[], options
|
|
88
|
+
export declare function createRequirePermissionsMiddleware(engine: PermissionEngine, permissions: readonly string[], options?: RequirePermissionsMiddlewareOptions): HttpMiddleware;
|
|
48
89
|
//# sourceMappingURL=httpMiddleware.core.d.ts.map
|
|
@@ -6,12 +6,37 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module http/httpMiddleware
|
|
8
8
|
*/
|
|
9
|
-
import { createForbiddenResponse,
|
|
9
|
+
import { createForbiddenResponse, createUnauthorizedResponse, } from "./httpHelpers.js";
|
|
10
10
|
// ─── State Keys ───────────────────────────────────────────────────────────
|
|
11
11
|
/** State key for the current actor. */
|
|
12
12
|
export const ACTOR_STATE_KEY = "permissions:actor";
|
|
13
13
|
/** State key for the authorization decision. */
|
|
14
14
|
export const DECISION_STATE_KEY = "permissions:decision";
|
|
15
|
+
/** State key for the batch decision map. */
|
|
16
|
+
export const DECISIONS_STATE_KEY = "permissions:decisions";
|
|
17
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* Finds the actor for a request: from state first, then by extracting it.
|
|
20
|
+
*/
|
|
21
|
+
async function resolveActor(context, options) {
|
|
22
|
+
const existing = context.state.get(ACTOR_STATE_KEY);
|
|
23
|
+
if (existing)
|
|
24
|
+
return existing;
|
|
25
|
+
if (!options.extractActor)
|
|
26
|
+
return undefined;
|
|
27
|
+
const actor = await options.extractActor(context);
|
|
28
|
+
if (actor)
|
|
29
|
+
context.state.set(ACTOR_STATE_KEY, actor);
|
|
30
|
+
return actor;
|
|
31
|
+
}
|
|
32
|
+
function buildAuthorization(context, options) {
|
|
33
|
+
const metadata = options.extractMetadata?.(context);
|
|
34
|
+
return {
|
|
35
|
+
...options.authorization,
|
|
36
|
+
...(options.forwardSignal !== false ? { signal: context.signal } : {}),
|
|
37
|
+
...(metadata ? { metadata } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
15
40
|
// ─── Middleware Factories ──────────────────────────────────────────────────
|
|
16
41
|
/**
|
|
17
42
|
* Create middleware that extracts the actor from the request and stores it in state.
|
|
@@ -22,59 +47,78 @@ export function createActorMiddleware(options) {
|
|
|
22
47
|
if (actor) {
|
|
23
48
|
context.state.set(ACTOR_STATE_KEY, actor);
|
|
24
49
|
}
|
|
50
|
+
else if (options.requireActor === true) {
|
|
51
|
+
return createUnauthorizedResponse(options);
|
|
52
|
+
}
|
|
25
53
|
return next();
|
|
26
54
|
};
|
|
27
55
|
}
|
|
28
56
|
/**
|
|
29
57
|
* Create middleware that checks a permission and returns 403 if denied.
|
|
58
|
+
*
|
|
59
|
+
* An unauthenticated request gets 401; an authenticated one that is not
|
|
60
|
+
* permitted gets 403.
|
|
30
61
|
*/
|
|
31
62
|
export function createRequirePermissionMiddleware(engine, options) {
|
|
32
63
|
return async (context, next) => {
|
|
33
|
-
const actor = context
|
|
34
|
-
if (!actor)
|
|
35
|
-
return
|
|
36
|
-
}
|
|
64
|
+
const actor = await resolveActor(context, options);
|
|
65
|
+
if (!actor)
|
|
66
|
+
return createUnauthorizedResponse(options);
|
|
37
67
|
const resource = options.extractResource?.(context);
|
|
38
|
-
const decision = await engine.check(actor, options.permission, resource, options
|
|
68
|
+
const decision = await engine.check(actor, options.permission, resource, buildAuthorization(context, options));
|
|
39
69
|
context.state.set(DECISION_STATE_KEY, decision);
|
|
40
|
-
if (!decision.allowed)
|
|
41
|
-
return createForbiddenResponse(decision
|
|
42
|
-
}
|
|
70
|
+
if (!decision.allowed)
|
|
71
|
+
return createForbiddenResponse(decision, options);
|
|
43
72
|
return next();
|
|
44
73
|
};
|
|
45
74
|
}
|
|
46
75
|
/**
|
|
47
76
|
* Create middleware that checks a permission and short-circuits on denial.
|
|
48
77
|
*/
|
|
49
|
-
export function authorize(engine, permission, options) {
|
|
78
|
+
export function authorize(engine, permission, options = {}) {
|
|
50
79
|
return createRequirePermissionMiddleware(engine, {
|
|
80
|
+
...options,
|
|
51
81
|
permission,
|
|
52
|
-
extractActor: options.extractActor,
|
|
53
|
-
authorization: options.authorization,
|
|
54
|
-
deniedResponse: options.deniedResponse,
|
|
55
|
-
extractResource: options.extractResource,
|
|
56
82
|
});
|
|
57
83
|
}
|
|
58
84
|
/**
|
|
59
|
-
* Create middleware that checks multiple permissions
|
|
85
|
+
* Create middleware that checks multiple permissions.
|
|
86
|
+
*
|
|
87
|
+
* Under `"all"` the first denial short-circuits: evaluating the rest costs
|
|
88
|
+
* policy calls and timeouts for an answer that is already decided.
|
|
60
89
|
*/
|
|
61
|
-
export function createRequirePermissionsMiddleware(engine, permissions, options) {
|
|
90
|
+
export function createRequirePermissionsMiddleware(engine, permissions, options = {}) {
|
|
91
|
+
const mode = options.mode ?? "all";
|
|
62
92
|
return async (context, next) => {
|
|
63
|
-
const actor = context
|
|
64
|
-
if (!actor)
|
|
65
|
-
return
|
|
66
|
-
|
|
93
|
+
const actor = await resolveActor(context, options);
|
|
94
|
+
if (!actor)
|
|
95
|
+
return createUnauthorizedResponse(options);
|
|
96
|
+
const resource = options.extractResource?.(context);
|
|
97
|
+
const authorization = buildAuthorization(context, options);
|
|
67
98
|
const results = new Map();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
99
|
+
let lastDenial;
|
|
100
|
+
for (const permission of permissions) {
|
|
101
|
+
const decision = await engine.check(actor, permission, resource, authorization);
|
|
102
|
+
results.set(permission, decision);
|
|
103
|
+
if (decision.allowed) {
|
|
104
|
+
if (mode === "any") {
|
|
105
|
+
context.state.set(DECISIONS_STATE_KEY, results);
|
|
106
|
+
return next();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
lastDenial = decision;
|
|
111
|
+
if (mode === "all")
|
|
112
|
+
break;
|
|
76
113
|
}
|
|
77
114
|
}
|
|
115
|
+
context.state.set(DECISIONS_STATE_KEY, results);
|
|
116
|
+
if (mode === "all" && lastDenial) {
|
|
117
|
+
return createForbiddenResponse(lastDenial, options);
|
|
118
|
+
}
|
|
119
|
+
if (mode === "any") {
|
|
120
|
+
return createForbiddenResponse(lastDenial ?? { allowed: false, publicReason: "Access denied" }, options);
|
|
121
|
+
}
|
|
78
122
|
return next();
|
|
79
123
|
};
|
|
80
124
|
}
|