@zudojs/permissions 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -10
- package/dist/evaluator/authorizationEngine.d.ts +11 -1
- package/dist/evaluator/authorizationEngine.js +1 -0
- package/dist/evaluator/evaluator.core.js +13 -10
- package/dist/evaluator/evaluator.pipeline.d.ts +10 -1
- package/dist/evaluator/evaluator.pipeline.js +9 -4
- package/dist/http/httpHelpers.d.ts +7 -6
- package/dist/http/httpHelpers.js +6 -13
- package/dist/http/httpTypes.d.ts +17 -2
- package/dist/http/index.d.ts +1 -1
- package/dist/permissionTypes/index.d.ts +1 -1
- package/dist/permissionTypes/policyTypes.d.ts +18 -0
- package/dist/policy/index.d.ts +1 -0
- package/dist/policy/index.js +1 -0
- package/dist/policy/policyEffect.core.d.ts +21 -0
- package/dist/policy/policyEffect.core.js +22 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -153,33 +153,95 @@ denies when either is missing. It is only as good as the value you pass: fill
|
|
|
153
153
|
the tenant `@zudojs/tenancy` resolved), **never from a request header** — a
|
|
154
154
|
caller would set the header to the resource's tenant and pass.
|
|
155
155
|
|
|
156
|
+
Enforce it as a **deny** rule, `not(tenantIsolation())`:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { createPermissionEngine, not, tenantIsolation } from "@zudojs/permissions";
|
|
160
|
+
|
|
161
|
+
const engine = createPermissionEngine({
|
|
162
|
+
roles: [{ name: "accountant", permissions: ["invoice:read"] }],
|
|
163
|
+
rules: [
|
|
164
|
+
{
|
|
165
|
+
name: "tenant-isolation",
|
|
166
|
+
effect: "deny",
|
|
167
|
+
resource: "invoice",
|
|
168
|
+
action: "*",
|
|
169
|
+
condition: not(tenantIsolation()), // other tenant, or no tenant → deny
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A conditional **allow** rule with `tenantIsolation()` only adds a way in: a
|
|
176
|
+
role that grants `invoice:read` allows the check whatever the condition says,
|
|
177
|
+
so that role reads every tenant's invoices. The deny form refuses cross-tenant
|
|
178
|
+
access even for roles that hold the permission, and no policy can override it.
|
|
179
|
+
|
|
156
180
|
## Policies
|
|
157
181
|
|
|
158
|
-
A policy is a named, prioritised hook that runs alongside the rules.
|
|
182
|
+
A policy is a named, prioritised hook that runs alongside the rules. By
|
|
183
|
+
default it is an **extra condition on top of RBAC/ABAC**: it can take access
|
|
184
|
+
away, never hand it out.
|
|
159
185
|
|
|
160
186
|
```typescript
|
|
161
187
|
const engine = createPermissionEngine({
|
|
162
|
-
roles,
|
|
188
|
+
roles: [{ name: "staff", permissions: ["task:*"] }],
|
|
163
189
|
policyTimeout: 250,
|
|
164
190
|
policies: [
|
|
165
191
|
{
|
|
166
192
|
name: "business-hours",
|
|
167
|
-
permissions: ["
|
|
193
|
+
permissions: ["task:*"], // wildcards work here too
|
|
168
194
|
priority: 10, // higher runs first
|
|
169
195
|
cacheable: false, // depends on the clock, so never cache it
|
|
170
|
-
evaluate: (
|
|
196
|
+
evaluate: () =>
|
|
171
197
|
isBusinessHours()
|
|
172
|
-
? { allowed: true }
|
|
198
|
+
? { allowed: true } // "no objection" — the role still has to grant
|
|
173
199
|
: { allowed: false, reason: "outside_business_hours" },
|
|
174
200
|
},
|
|
175
201
|
],
|
|
176
202
|
});
|
|
203
|
+
|
|
204
|
+
await engine.can(staff, "task:delete"); // true in hours, false after
|
|
205
|
+
await engine.can(guest, "task:delete"); // false: no role grants it
|
|
177
206
|
```
|
|
178
207
|
|
|
208
|
+
> **Warning — a policy's `allowed: true` is not a grant.** Before 1.4 it was:
|
|
209
|
+
> an allowing policy granted the permission even to an actor with no roles, so
|
|
210
|
+
> the "business-hours" policy above handed `task:delete` to everyone during
|
|
211
|
+
> office hours. A policy now only constrains, unless it opts in with
|
|
212
|
+
> `effect: "grant"`.
|
|
213
|
+
|
|
179
214
|
Policies run highest priority first and stop at the first denial. A policy
|
|
180
|
-
that throws, or exceeds `policyTimeout`, denies. A denying policy always wins
|
|
181
|
-
|
|
182
|
-
|
|
215
|
+
that throws, or exceeds `policyTimeout`, denies. A denying policy always wins.
|
|
216
|
+
An allowing policy grants nothing by itself: the actor's roles, direct
|
|
217
|
+
permissions or rules must still grant the permission.
|
|
218
|
+
|
|
219
|
+
### Policies that grant
|
|
220
|
+
|
|
221
|
+
A policy that establishes the right on its own — ownership is the usual one —
|
|
222
|
+
says so with `effect: "grant"`:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
const engine = createPermissionEngine({
|
|
226
|
+
roles,
|
|
227
|
+
policies: [
|
|
228
|
+
{
|
|
229
|
+
name: "author-can-edit",
|
|
230
|
+
permissions: ["post:update"],
|
|
231
|
+
effect: "grant", // an allow here grants, even with no role
|
|
232
|
+
evaluate: ({ actor, resource }) => ({
|
|
233
|
+
allowed: (resource as { authorId?: string })?.authorId === actor.id,
|
|
234
|
+
}),
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
A granting policy still never overrides a denial — from another policy, an
|
|
241
|
+
explicit deny, or a deny rule that applied. Only the exact value `"grant"`
|
|
242
|
+
grants; a typo constrains. `createPermissionEngine({ defaultPolicyEffect:
|
|
243
|
+
"grant" })` restores the pre-1.4 behaviour for every policy that sets no
|
|
244
|
+
`effect` — prefer marking the individual policies.
|
|
183
245
|
|
|
184
246
|
`policyTimeout: 0` means "expire immediately", not "no timeout" — omit it to
|
|
185
247
|
disable.
|
|
@@ -428,7 +490,11 @@ const guard = authorize(engine, "post:update", {
|
|
|
428
490
|
- The guard extracts the actor itself when one is not already in state, so it
|
|
429
491
|
works without a separate actor middleware.
|
|
430
492
|
- No actor → **401** with `WWW-Authenticate`. Actor but not permitted →
|
|
431
|
-
**403**.
|
|
493
|
+
**403**. The refusal is a `GuardResponse` (`createGuardResponse` from
|
|
494
|
+
`@zudojs/middleware`), which `@zudojs/http` sends with that status, body and
|
|
495
|
+
headers. It used to be a plain `{ status, body, headers }` object, which a
|
|
496
|
+
route middleware's return ignored: the handler did not run, but the client
|
|
497
|
+
got `200`.
|
|
432
498
|
- The 403 body carries `decision.publicReason`, never the internal reason:
|
|
433
499
|
`policy_error:<name>` names your policies and does not belong in a
|
|
434
500
|
response. Pass `deniedResponse` to shape the body — it receives the real
|
|
@@ -444,7 +510,18 @@ const guard = authorize(engine, "post:update", {
|
|
|
444
510
|
|
|
445
511
|
The middleware composes with the real `@zudojs/http` pipeline without
|
|
446
512
|
depending on it: the HTTP types are mirrored structurally (headers, params and
|
|
447
|
-
query may be plain objects, as `@zudojs/http` provides them, or maps)
|
|
513
|
+
query may be plain objects, as `@zudojs/http` provides them, or maps), and
|
|
514
|
+
`HttpMiddleware` is assignable to `@zudojs/http`'s own `HttpMiddleware` — pass
|
|
515
|
+
a guard straight to a route, no `as never`:
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
router.put("/posts/:id", updatePost, { middleware: [guard] });
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
A middleware of your own typed as this package's `HttpMiddleware` answers a
|
|
522
|
+
refusal with `createGuardResponse({ status, body })`; returning a plain
|
|
523
|
+
`{ status, body, headers }` object is no longer typed, because
|
|
524
|
+
`@zudojs/http` never sent it as a response.
|
|
448
525
|
|
|
449
526
|
## Errors
|
|
450
527
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module evaluator/authorizationEngine
|
|
5
5
|
*/
|
|
6
|
-
import type { PermissionActor, PermissionDecision, ExplainResult, PermissionRule, PermissionPolicyDefinition, PermissionCache, PermissionResolver, RoleResolver, RoleDefinition, RuleCombiningAlgorithm, AuthorizationOptions } from "../permissionTypes/index.js";
|
|
6
|
+
import type { PermissionActor, PermissionDecision, ExplainResult, PermissionRule, PermissionPolicyDefinition, PolicyEffect, PermissionCache, PermissionResolver, RoleResolver, RoleDefinition, RuleCombiningAlgorithm, AuthorizationOptions } from "../permissionTypes/index.js";
|
|
7
7
|
import { type Ability } from "../ability/ability.core.js";
|
|
8
8
|
import type { PermissionEventEmitter } from "../observability/observability.core.js";
|
|
9
9
|
/**
|
|
@@ -57,6 +57,16 @@ export interface PermissionEngineOptions {
|
|
|
57
57
|
readonly rules?: readonly PermissionRule[];
|
|
58
58
|
/** Default timeout for async policy evaluation (ms). */
|
|
59
59
|
readonly policyTimeout?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Effect of a policy that sets no `effect` of its own. Default:
|
|
62
|
+
* `"constrain"` — an allowing policy is an extra condition and cannot
|
|
63
|
+
* grant a permission the actor's roles, permissions or rules do not.
|
|
64
|
+
*
|
|
65
|
+
* `"grant"` restores the behaviour before 1.4, where every allowing policy
|
|
66
|
+
* was an independent grant. Prefer `effect: "grant"` on the individual
|
|
67
|
+
* policies that really establish the right on their own.
|
|
68
|
+
*/
|
|
69
|
+
readonly defaultPolicyEffect?: PolicyEffect;
|
|
60
70
|
/** How competing rules combine. Default: `"deny-overrides"`. */
|
|
61
71
|
readonly algorithm?: RuleCombiningAlgorithm;
|
|
62
72
|
/** Caches decisions. Create one with `createMemoryPermissionCache()`. */
|
|
@@ -144,6 +144,7 @@ export function createPermissionEngine(options) {
|
|
|
144
144
|
},
|
|
145
145
|
rules: options?.rules,
|
|
146
146
|
policyTimeout: options?.policyTimeout,
|
|
147
|
+
defaultPolicyEffect: options?.defaultPolicyEffect,
|
|
147
148
|
algorithm: options?.algorithm,
|
|
148
149
|
cache: options?.cache,
|
|
149
150
|
cacheTtlMs: options?.cacheTtlMs,
|
|
@@ -185,7 +185,7 @@ export async function evaluate(actor, permissionStr, resource, options, authOpti
|
|
|
185
185
|
});
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
|
-
const decision = combine(ruleResult, outcome.decision, permissionStr);
|
|
188
|
+
const decision = combine(ruleResult, outcome.decision, outcome.grants, permissionStr);
|
|
189
189
|
/* ── Cache write ─────────────────────────────────────────────────────── */
|
|
190
190
|
// A decision forced by a condition that threw describes the failure, not
|
|
191
191
|
// the actor, and must not outlive it.
|
|
@@ -204,15 +204,18 @@ export async function evaluate(actor, permissionStr, resource, options, authOpti
|
|
|
204
204
|
/**
|
|
205
205
|
* Combine the rule outcome with the policy outcome.
|
|
206
206
|
*
|
|
207
|
-
* A denying policy always wins. An allowing policy
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
207
|
+
* A denying policy always wins. An allowing policy is, by default, only an
|
|
208
|
+
* extra condition: the actor's roles, permissions or rules must still grant
|
|
209
|
+
* the permission. It used to grant on its own, so a "business-hours" policy
|
|
210
|
+
* handed `task:delete` to an actor with no roles at all. Only a policy with
|
|
211
|
+
* `effect: "grant"` (or an engine with `defaultPolicyEffect: "grant"`) can
|
|
212
|
+
* grant access the rules did not — and even then it never overrides a
|
|
213
|
+
* denial, including one the *rules* produced. "The rules did not allow"
|
|
214
|
+
* covers two cases: no rule matched, and a deny rule matched. Treating them
|
|
215
|
+
* alike let an allowing policy for `post:*` cancel a `deny post:update` rule
|
|
216
|
+
* — the exact inversion of `deny-overrides`.
|
|
214
217
|
*/
|
|
215
|
-
function combine(ruleResult, policyDecision, permissionStr) {
|
|
218
|
+
function combine(ruleResult, policyDecision, policyGrants, permissionStr) {
|
|
216
219
|
if (policyDecision && !policyDecision.allowed)
|
|
217
220
|
return policyDecision;
|
|
218
221
|
const denyRule = !ruleResult.allowed && ruleResult.matchedRule?.effect === "deny"
|
|
@@ -232,7 +235,7 @@ function combine(ruleResult, policyDecision, permissionStr) {
|
|
|
232
235
|
...(policyDecision?.policy ? { policy: policyDecision.policy } : {}),
|
|
233
236
|
});
|
|
234
237
|
}
|
|
235
|
-
if (policyDecision?.allowed)
|
|
238
|
+
if (policyDecision?.allowed && policyGrants)
|
|
236
239
|
return policyDecision;
|
|
237
240
|
return denied("no_matching_rule");
|
|
238
241
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module evaluator/evaluator.pipeline
|
|
5
5
|
*/
|
|
6
|
-
import type { PermissionActor, PermissionContext, PermissionDecision, PermissionPolicyDefinition, PermissionRule, PermissionCache, PermissionResolver, RoleDefinition, RoleResolver, RuleCombiningAlgorithm, AuthorizationOptions } from "../permissionTypes/index.js";
|
|
6
|
+
import type { PermissionActor, PermissionContext, PermissionDecision, PermissionPolicyDefinition, PermissionRule, PermissionCache, PermissionResolver, RoleDefinition, RoleResolver, RuleCombiningAlgorithm, AuthorizationOptions, PolicyEffect } 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. */
|
|
@@ -14,6 +14,8 @@ export interface EvaluatorOptions {
|
|
|
14
14
|
readonly rules?: readonly PermissionRule[];
|
|
15
15
|
/** Default timeout for async policy evaluation (ms). */
|
|
16
16
|
readonly policyTimeout?: number;
|
|
17
|
+
/** Effect of a policy that sets none. Default: `"constrain"`. */
|
|
18
|
+
readonly defaultPolicyEffect?: PolicyEffect;
|
|
17
19
|
/** How competing rules combine. Default: `"deny-overrides"`. */
|
|
18
20
|
readonly algorithm?: RuleCombiningAlgorithm;
|
|
19
21
|
/** Decision cache. */
|
|
@@ -80,6 +82,13 @@ export interface PolicyOutcome {
|
|
|
80
82
|
/** Whether every policy that ran allows the result to be cached. */
|
|
81
83
|
readonly cacheable: boolean;
|
|
82
84
|
readonly evaluated: readonly string[];
|
|
85
|
+
/**
|
|
86
|
+
* Whether the allow may grant on its own: every applicable policy allowed
|
|
87
|
+
* and at least one of them has the `"grant"` effect. `false` for a denial,
|
|
88
|
+
* and for an allow from constraining policies only, which then needs a
|
|
89
|
+
* role, permission or rule to grant.
|
|
90
|
+
*/
|
|
91
|
+
readonly grants: boolean;
|
|
83
92
|
}
|
|
84
93
|
/**
|
|
85
94
|
* Evaluate policies for a context.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module evaluator/evaluator.pipeline
|
|
5
5
|
*/
|
|
6
|
+
import { policyGrants } from "../policy/policyEffect.core.js";
|
|
6
7
|
import { resolveRolePermissions } from "../role/roleHierarchy.js";
|
|
7
8
|
import { matches, permissionsOverlap, parsePermissionSafe, } from "../permission/permission.core.js";
|
|
8
9
|
import { isWildcardTarget } from "../rule/rule.pattern.js";
|
|
@@ -153,13 +154,13 @@ function narrowerPolicies(policies, applicable, permissionStr) {
|
|
|
153
154
|
*/
|
|
154
155
|
export async function evaluatePolicies(context, policies, options, authOptions) {
|
|
155
156
|
if (policies.length === 0) {
|
|
156
|
-
return { decision: null, cacheable: true, evaluated: [] };
|
|
157
|
+
return { decision: null, cacheable: true, evaluated: [], grants: false };
|
|
157
158
|
}
|
|
158
159
|
const permissionStr = `${context.permission.resource}:${context.permission.action}`;
|
|
159
160
|
const applicable = selectPolicies(policies, permissionStr);
|
|
160
161
|
const denyOnly = narrowerPolicies(policies, applicable, permissionStr);
|
|
161
162
|
if (applicable.length === 0 && denyOnly.length === 0) {
|
|
162
|
-
return { decision: null, cacheable: true, evaluated: [] };
|
|
163
|
+
return { decision: null, cacheable: true, evaluated: [], grants: false };
|
|
163
164
|
}
|
|
164
165
|
// The per-call timeout wins, but the engine-level default is what makes a
|
|
165
166
|
// configured timeout mean anything at all.
|
|
@@ -183,6 +184,7 @@ export async function evaluatePolicies(context, policies, options, authOptions)
|
|
|
183
184
|
}),
|
|
184
185
|
cacheable,
|
|
185
186
|
evaluated,
|
|
187
|
+
grants: false,
|
|
186
188
|
};
|
|
187
189
|
}
|
|
188
190
|
}
|
|
@@ -200,20 +202,23 @@ export async function evaluatePolicies(context, policies, options, authOptions)
|
|
|
200
202
|
}),
|
|
201
203
|
cacheable: false,
|
|
202
204
|
evaluated,
|
|
205
|
+
grants: false,
|
|
203
206
|
};
|
|
204
207
|
}
|
|
205
208
|
}
|
|
206
209
|
if (applicable.length === 0) {
|
|
207
|
-
return { decision: null, cacheable, evaluated };
|
|
210
|
+
return { decision: null, cacheable, evaluated, grants: false };
|
|
208
211
|
}
|
|
212
|
+
const grants = applicable.some((policy) => policyGrants(policy, options.defaultPolicyEffect));
|
|
209
213
|
return {
|
|
210
214
|
decision: Object.freeze({
|
|
211
215
|
allowed: true,
|
|
212
|
-
reason: "policy_allow",
|
|
216
|
+
reason: grants ? "policy_allow" : "policy_pass",
|
|
213
217
|
policy: applicable.map((policy) => policy.name).join(","),
|
|
214
218
|
}),
|
|
215
219
|
cacheable,
|
|
216
220
|
evaluated,
|
|
221
|
+
grants,
|
|
217
222
|
};
|
|
218
223
|
}
|
|
219
224
|
/**
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module http/httpHelpers
|
|
5
5
|
*/
|
|
6
|
+
import { type GuardResponse } from "@zudojs/middleware";
|
|
6
7
|
import type { PermissionDecision } from "../permissionTypes/index.js";
|
|
7
8
|
/** Options for creating denied responses. */
|
|
8
9
|
export interface DeniedResponseOptions {
|
|
@@ -16,12 +17,12 @@ export interface DeniedResponseOptions {
|
|
|
16
17
|
/** Value for the `WWW-Authenticate` header on a 401. */
|
|
17
18
|
readonly authenticateChallenge?: string;
|
|
18
19
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
/**
|
|
21
|
+
* A framework-agnostic response: a `GuardResponse` from `@zudojs/middleware`,
|
|
22
|
+
* which `@zudojs/http` sends with its own status. It still has the
|
|
23
|
+
* `status`, `body` and `headers` fields this type always had.
|
|
24
|
+
*/
|
|
25
|
+
export type PermissionHttpResponse = GuardResponse;
|
|
25
26
|
/**
|
|
26
27
|
* Create a 403 Forbidden JSON response for a decision.
|
|
27
28
|
*
|
package/dist/http/httpHelpers.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module http/httpHelpers
|
|
5
5
|
*/
|
|
6
|
+
import { createGuardResponse } from "@zudojs/middleware";
|
|
6
7
|
const JSON_HEADERS = { "content-type": "application/json" };
|
|
7
8
|
/**
|
|
8
9
|
* Create a 403 Forbidden JSON response for a decision.
|
|
@@ -18,11 +19,7 @@ export function createForbiddenResponse(decision, options) {
|
|
|
18
19
|
error: "Forbidden",
|
|
19
20
|
message: decision.publicReason ?? "Access denied",
|
|
20
21
|
};
|
|
21
|
-
return
|
|
22
|
-
status: 403,
|
|
23
|
-
body,
|
|
24
|
-
headers: JSON_HEADERS,
|
|
25
|
-
});
|
|
22
|
+
return createGuardResponse({ status: 403, body, headers: JSON_HEADERS });
|
|
26
23
|
}
|
|
27
24
|
/**
|
|
28
25
|
* Create a 401 Unauthorized JSON response.
|
|
@@ -34,23 +31,19 @@ export function createUnauthorizedResponse(options) {
|
|
|
34
31
|
const body = options?.unauthenticatedResponse
|
|
35
32
|
? options.unauthenticatedResponse()
|
|
36
33
|
: { error: "Unauthorized", message: "Authentication required" };
|
|
37
|
-
return
|
|
34
|
+
return createGuardResponse({
|
|
38
35
|
status: 401,
|
|
39
36
|
body,
|
|
40
|
-
headers:
|
|
37
|
+
headers: {
|
|
41
38
|
...JSON_HEADERS,
|
|
42
39
|
"www-authenticate": options?.authenticateChallenge ?? "Bearer",
|
|
43
|
-
}
|
|
40
|
+
},
|
|
44
41
|
});
|
|
45
42
|
}
|
|
46
43
|
/**
|
|
47
44
|
* Create a JSON response.
|
|
48
45
|
*/
|
|
49
46
|
export function createJsonResponse(status, body) {
|
|
50
|
-
return
|
|
51
|
-
status,
|
|
52
|
-
body,
|
|
53
|
-
headers: JSON_HEADERS,
|
|
54
|
-
});
|
|
47
|
+
return createGuardResponse({ status, body, headers: JSON_HEADERS });
|
|
55
48
|
}
|
|
56
49
|
//# sourceMappingURL=httpHelpers.js.map
|
package/dist/http/httpTypes.d.ts
CHANGED
|
@@ -6,8 +6,23 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module http/httpTypes
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import type { GuardResponse } from "@zudojs/middleware";
|
|
10
|
+
/**
|
|
11
|
+
* What a permission middleware returns: nothing, a web `Response`, a
|
|
12
|
+
* `GuardResponse` refusing the request, or whatever `next()` produced.
|
|
13
|
+
*/
|
|
14
|
+
export type HttpMiddlewareOutcome<Downstream> = void | Response | GuardResponse | Downstream;
|
|
15
|
+
/**
|
|
16
|
+
* HTTP middleware signature, structurally assignable to `@zudojs/http`'s
|
|
17
|
+
* `HttpMiddleware` without a cast.
|
|
18
|
+
*
|
|
19
|
+
* Generic over what `next()` resolves to, so a middleware hands back the
|
|
20
|
+
* real pipeline's response unchanged. A refusal is a `GuardResponse`
|
|
21
|
+
* (`createGuardResponse` from `@zudojs/middleware`), which `@zudojs/http`
|
|
22
|
+
* sends with its own status; a plain `{ status, body, headers }` object is
|
|
23
|
+
* not a response.
|
|
24
|
+
*/
|
|
25
|
+
export type HttpMiddleware = <Downstream extends HttpResponseContext>(context: HttpMiddlewareContext, next: () => Promise<Downstream>) => HttpMiddlewareOutcome<Downstream> | Promise<HttpMiddlewareOutcome<Downstream>>;
|
|
11
26
|
/** HTTP middleware context from @zudojs/http. */
|
|
12
27
|
export interface HttpMiddlewareContext {
|
|
13
28
|
readonly request: HttpRequestContext;
|
package/dist/http/index.d.ts
CHANGED
|
@@ -13,5 +13,5 @@
|
|
|
13
13
|
export { createActorMiddleware, createRequirePermissionMiddleware, authorize, createRequirePermissionsMiddleware, ACTOR_STATE_KEY, DECISION_STATE_KEY, DECISIONS_STATE_KEY, type AuthorizeMiddlewareOptions, type ActorMiddlewareOptions, type RequirePermissionMiddlewareOptions, type RequirePermissionsMiddlewareOptions, } from "./httpMiddleware.core.js";
|
|
14
14
|
export { createForbiddenResponse, createUnauthorizedResponse, createJsonResponse, type DeniedResponseOptions, type PermissionHttpResponse, } from "./httpHelpers.js";
|
|
15
15
|
export { loadResource, RESOURCE_ERROR_DECISION, type ResourceExtractor, type ResourceOutcome, } from "./httpResource.helper.js";
|
|
16
|
-
export type { HttpRequestBag, HttpMiddleware, HttpMiddlewareContext, HttpRequestContext, HttpResponseContext, HttpMiddlewareState, } from "./httpTypes.js";
|
|
16
|
+
export type { HttpRequestBag, HttpMiddleware, HttpMiddlewareOutcome, HttpMiddlewareContext, HttpRequestContext, HttpResponseContext, HttpMiddlewareState, } from "./httpTypes.js";
|
|
17
17
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -5,5 +5,5 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { type PermissionActor, type Permission, type PermissionString, } from "./permissionActor.js";
|
|
7
7
|
export { type RuleEffect, type RuleCombiningAlgorithm, type RuleEvaluation, type PermissionRule, type PermissionConditionFn, type PermissionContext, type PermissionDecision, } from "./ruleTypes.js";
|
|
8
|
-
export { type RoleDefinition, type PermissionResolver, type RoleResolver, type PermissionCache, type PermissionPolicyDefinition, type ExplainStep, type ExplainResult, type AuthorizationOptions, } from "./policyTypes.js";
|
|
8
|
+
export { type RoleDefinition, type PermissionResolver, type RoleResolver, type PermissionCache, type PermissionPolicyDefinition, type PolicyEffect, type ExplainStep, type ExplainResult, type AuthorizationOptions, } from "./policyTypes.js";
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -42,9 +42,27 @@ export interface PermissionCache {
|
|
|
42
42
|
/** Drops every entry. */
|
|
43
43
|
clear?(): Promise<void>;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* What an allowing policy means.
|
|
47
|
+
*
|
|
48
|
+
* - `"constrain"` (the default): the policy is an extra condition on top of
|
|
49
|
+
* RBAC/ABAC. Its allow means "no objection"; the actor's roles, direct
|
|
50
|
+
* permissions or rules must still grant the permission. Its deny denies.
|
|
51
|
+
* - `"grant"`: the policy is an independent grant. Its allow grants the
|
|
52
|
+
* permission even when no role or rule does — an ownership check, say.
|
|
53
|
+
* Use it only for a policy that establishes the right on its own.
|
|
54
|
+
*/
|
|
55
|
+
export type PolicyEffect = "constrain" | "grant";
|
|
45
56
|
/** A named authorization policy. */
|
|
46
57
|
export interface PermissionPolicyDefinition {
|
|
47
58
|
readonly name: string;
|
|
59
|
+
/**
|
|
60
|
+
* Whether this policy's allow can grant a permission the actor's roles do
|
|
61
|
+
* not include. Default: the engine's `defaultPolicyEffect`, which is
|
|
62
|
+
* `"constrain"` — a policy only ever narrows access. Any value other than
|
|
63
|
+
* `"grant"` constrains.
|
|
64
|
+
*/
|
|
65
|
+
readonly effect?: PolicyEffect;
|
|
48
66
|
/**
|
|
49
67
|
* Permissions this policy applies to. Wildcards are honoured, so
|
|
50
68
|
* `["post:*"]` covers `post:update` — the same matching grants use.
|
package/dist/policy/index.d.ts
CHANGED
package/dist/policy/index.js
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy effect resolution: whether an allowing policy may grant access on
|
|
3
|
+
* its own or only constrains what RBAC/ABAC already granted.
|
|
4
|
+
*
|
|
5
|
+
* @module policy/policyEffect.core
|
|
6
|
+
*/
|
|
7
|
+
import type { PermissionPolicyDefinition, PolicyEffect } from "../permissionTypes/index.js";
|
|
8
|
+
/** The effect a policy without its own `effect` has. */
|
|
9
|
+
export declare const DEFAULT_POLICY_EFFECT: PolicyEffect;
|
|
10
|
+
/**
|
|
11
|
+
* Whether a policy's allow is an independent grant.
|
|
12
|
+
*
|
|
13
|
+
* Only the exact value `"grant"` grants. A typo, a missing value or anything
|
|
14
|
+
* else falls back to constraining, so a mistake narrows access instead of
|
|
15
|
+
* widening it.
|
|
16
|
+
*
|
|
17
|
+
* @param policy - The policy that allowed.
|
|
18
|
+
* @param defaultEffect - The engine's `defaultPolicyEffect`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function policyGrants(policy: PermissionPolicyDefinition, defaultEffect?: PolicyEffect): boolean;
|
|
21
|
+
//# sourceMappingURL=policyEffect.core.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Policy effect resolution: whether an allowing policy may grant access on
|
|
3
|
+
* its own or only constrains what RBAC/ABAC already granted.
|
|
4
|
+
*
|
|
5
|
+
* @module policy/policyEffect.core
|
|
6
|
+
*/
|
|
7
|
+
/** The effect a policy without its own `effect` has. */
|
|
8
|
+
export const DEFAULT_POLICY_EFFECT = "constrain";
|
|
9
|
+
/**
|
|
10
|
+
* Whether a policy's allow is an independent grant.
|
|
11
|
+
*
|
|
12
|
+
* Only the exact value `"grant"` grants. A typo, a missing value or anything
|
|
13
|
+
* else falls back to constraining, so a mistake narrows access instead of
|
|
14
|
+
* widening it.
|
|
15
|
+
*
|
|
16
|
+
* @param policy - The policy that allowed.
|
|
17
|
+
* @param defaultEffect - The engine's `defaultPolicyEffect`.
|
|
18
|
+
*/
|
|
19
|
+
export function policyGrants(policy, defaultEffect = DEFAULT_POLICY_EFFECT) {
|
|
20
|
+
return (policy.effect ?? defaultEffect) === "grant";
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=policyEffect.core.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/permissions",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Generic authorization engine with RBAC, ABAC, resource authorization, wildcards, role hierarchy, policies, and abilities.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -26,14 +26,15 @@
|
|
|
26
26
|
"!dist/.tsbuildinfo"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@zudojs/errors": "1.
|
|
29
|
+
"@zudojs/errors": "1.3.0",
|
|
30
|
+
"@zudojs/middleware": "1.1.0"
|
|
30
31
|
},
|
|
31
32
|
"engines": {
|
|
32
33
|
"node": ">=24.0.0"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
36
|
"typescript": "7.0.2",
|
|
36
|
-
"vitest": "^
|
|
37
|
+
"vitest": "^5.0.1"
|
|
37
38
|
},
|
|
38
39
|
"publishConfig": {
|
|
39
40
|
"access": "public"
|
|
@@ -45,7 +46,7 @@
|
|
|
45
46
|
"authorization",
|
|
46
47
|
"permissions"
|
|
47
48
|
],
|
|
48
|
-
"homepage": "https://
|
|
49
|
+
"homepage": "https://zudojs.oyinlola.site/docs/packages-permissions",
|
|
49
50
|
"bugs": {
|
|
50
51
|
"url": "https://github.com/oyinlola-tech/zudo/issues"
|
|
51
52
|
},
|