@zudojs/permissions 0.0.1 → 1.0.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 +345 -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 +24 -5
- package/dist/cache/cache.core.js +62 -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 +146 -17
- package/dist/evaluator/evaluator.core.d.ts +28 -4
- package/dist/evaluator/evaluator.core.js +246 -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 +26 -23
- 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 +16 -25
- package/dist/role/roleRegistry.js +27 -23
- 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 +23 -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,12 +4,27 @@
|
|
|
4
4
|
import { PermissionError } from "./permissionError.base.js";
|
|
5
5
|
/**
|
|
6
6
|
* Access denied — the actor is not authorized.
|
|
7
|
+
*
|
|
8
|
+
* `AuthorizationError` exposes its body to the client, so the identifying
|
|
9
|
+
* details go in `internal` rather than `metadata`: `actorId` is an internal
|
|
10
|
+
* identifier and a policy name describes the shape of the authorization
|
|
11
|
+
* model, neither of which belongs in a 403 body.
|
|
7
12
|
*/
|
|
8
13
|
export declare class PermissionDeniedError extends PermissionError {
|
|
14
|
+
/** Detail for logs and audit trails. Never serialized to a client. */
|
|
15
|
+
readonly details: {
|
|
16
|
+
readonly actorId?: string;
|
|
17
|
+
readonly permission?: string;
|
|
18
|
+
readonly resourceType?: string;
|
|
19
|
+
readonly reason?: string;
|
|
20
|
+
readonly policy?: string;
|
|
21
|
+
};
|
|
9
22
|
constructor(message?: string, options?: {
|
|
10
23
|
readonly actorId?: string;
|
|
11
24
|
readonly permission?: string;
|
|
12
25
|
readonly resourceType?: string;
|
|
26
|
+
readonly reason?: string;
|
|
27
|
+
readonly policy?: string;
|
|
13
28
|
});
|
|
14
29
|
}
|
|
15
30
|
/**
|
|
@@ -36,4 +51,13 @@ export declare class RoleNotFoundError extends PermissionError {
|
|
|
36
51
|
export declare class DuplicateRoleError extends PermissionError {
|
|
37
52
|
constructor(role: string);
|
|
38
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* A duplicate policy was registered.
|
|
56
|
+
*
|
|
57
|
+
* Silently replacing a policy is how an authorization rule disappears without
|
|
58
|
+
* anyone noticing, so the registry rejects it unless override is asked for.
|
|
59
|
+
*/
|
|
60
|
+
export declare class DuplicatePolicyError extends PermissionError {
|
|
61
|
+
constructor(policy: string);
|
|
62
|
+
}
|
|
39
63
|
//# sourceMappingURL=permissionError.access.d.ts.map
|
|
@@ -5,17 +5,23 @@ import { ErrorCode } from "@zudojs/errors";
|
|
|
5
5
|
import { PermissionError } from "./permissionError.base.js";
|
|
6
6
|
/**
|
|
7
7
|
* Access denied — the actor is not authorized.
|
|
8
|
+
*
|
|
9
|
+
* `AuthorizationError` exposes its body to the client, so the identifying
|
|
10
|
+
* details go in `internal` rather than `metadata`: `actorId` is an internal
|
|
11
|
+
* identifier and a policy name describes the shape of the authorization
|
|
12
|
+
* model, neither of which belongs in a 403 body.
|
|
8
13
|
*/
|
|
9
14
|
export class PermissionDeniedError extends PermissionError {
|
|
15
|
+
/** Detail for logs and audit trails. Never serialized to a client. */
|
|
16
|
+
details;
|
|
10
17
|
constructor(message = "Access denied", options) {
|
|
11
18
|
super(message, {
|
|
12
19
|
code: ErrorCode.ACCESS_DENIED,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
resourceType: options?.resourceType,
|
|
17
|
-
},
|
|
20
|
+
// Only the permission name, which the caller already knows because
|
|
21
|
+
// they attempted it.
|
|
22
|
+
metadata: { permission: options?.permission },
|
|
18
23
|
});
|
|
24
|
+
this.details = Object.freeze({ ...options });
|
|
19
25
|
}
|
|
20
26
|
}
|
|
21
27
|
/**
|
|
@@ -62,4 +68,18 @@ export class DuplicateRoleError extends PermissionError {
|
|
|
62
68
|
});
|
|
63
69
|
}
|
|
64
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* A duplicate policy was registered.
|
|
73
|
+
*
|
|
74
|
+
* Silently replacing a policy is how an authorization rule disappears without
|
|
75
|
+
* anyone noticing, so the registry rejects it unless override is asked for.
|
|
76
|
+
*/
|
|
77
|
+
export class DuplicatePolicyError extends PermissionError {
|
|
78
|
+
constructor(policy) {
|
|
79
|
+
super(`Duplicate policy: ${policy}`, {
|
|
80
|
+
code: ErrorCode.CONFLICT,
|
|
81
|
+
metadata: { policy },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
65
85
|
//# sourceMappingURL=permissionError.access.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission error types — re-exports from focused files.
|
|
3
3
|
*/
|
|
4
|
-
export { PermissionDeniedError, PermissionNotFoundError, DuplicatePermissionError, RoleNotFoundError, DuplicateRoleError, } from "./permissionError.access.js";
|
|
4
|
+
export { PermissionDeniedError, PermissionNotFoundError, DuplicatePermissionError, RoleNotFoundError, DuplicateRoleError, DuplicatePolicyError, } from "./permissionError.access.js";
|
|
5
5
|
export { InvalidPermissionError, InvalidRoleError, CircularRoleInheritanceError, PolicyError, PolicyTimeoutError, PermissionResolverError, AuthorizationAbortedError, } from "./permissionError.validation.js";
|
|
6
6
|
//# sourceMappingURL=permissionError.types.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permission error types — re-exports from focused files.
|
|
3
3
|
*/
|
|
4
|
-
export { PermissionDeniedError, PermissionNotFoundError, DuplicatePermissionError, RoleNotFoundError, DuplicateRoleError, } from "./permissionError.access.js";
|
|
4
|
+
export { PermissionDeniedError, PermissionNotFoundError, DuplicatePermissionError, RoleNotFoundError, DuplicateRoleError, DuplicatePolicyError, } from "./permissionError.access.js";
|
|
5
5
|
export { InvalidPermissionError, InvalidRoleError, CircularRoleInheritanceError, PolicyError, PolicyTimeoutError, PermissionResolverError, AuthorizationAbortedError, } from "./permissionError.validation.js";
|
|
6
6
|
//# sourceMappingURL=permissionError.types.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Core
|
|
2
|
+
* Core permission type definitions.
|
|
3
3
|
*
|
|
4
4
|
* @module permissionTypes
|
|
5
5
|
*/
|
|
6
6
|
export { type PermissionActor, type Permission, type PermissionString, } from "./permissionActor.js";
|
|
7
|
-
export { type RuleEffect, type PermissionRule, type PermissionConditionFn, type PermissionContext, type PermissionDecision, } from "./ruleTypes.js";
|
|
7
|
+
export { type RuleEffect, type RuleCombiningAlgorithm, type RuleEvaluation, type PermissionRule, type PermissionConditionFn, type PermissionContext, type PermissionDecision, } from "./ruleTypes.js";
|
|
8
8
|
export { type RoleDefinition, type PermissionResolver, type RoleResolver, type PermissionCache, type PermissionPolicyDefinition, type ExplainStep, type ExplainResult, type AuthorizationOptions, } from "./policyTypes.js";
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -9,10 +9,15 @@ import type { PermissionRule, PermissionContext, PermissionDecision } from "./ru
|
|
|
9
9
|
export interface RoleDefinition {
|
|
10
10
|
/** Role name (e.g. "admin", "editor"). */
|
|
11
11
|
readonly name: string;
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Permissions granted by this role. May be empty for a role that exists
|
|
14
|
+
* only to combine others through {@link RoleDefinition.inherits}.
|
|
15
|
+
*/
|
|
13
16
|
readonly permissions: readonly string[];
|
|
14
17
|
/** Roles this role inherits from. */
|
|
15
18
|
readonly inherits?: readonly string[];
|
|
19
|
+
/** Rules granted by this role, for ABAC beyond a permission string. */
|
|
20
|
+
readonly rules?: readonly PermissionRule[];
|
|
16
21
|
/** Role description. */
|
|
17
22
|
readonly description?: string;
|
|
18
23
|
/** Whether this is a system role (cannot be deleted). */
|
|
@@ -34,18 +39,30 @@ export interface PermissionCache {
|
|
|
34
39
|
}): Promise<void>;
|
|
35
40
|
delete(key: string): Promise<void>;
|
|
36
41
|
invalidateActor(actorId: string): Promise<void>;
|
|
42
|
+
/** Drops every entry. */
|
|
43
|
+
clear?(): Promise<void>;
|
|
37
44
|
}
|
|
38
45
|
/** A named authorization policy. */
|
|
39
46
|
export interface PermissionPolicyDefinition {
|
|
40
47
|
readonly name: string;
|
|
48
|
+
/**
|
|
49
|
+
* Permissions this policy applies to. Wildcards are honoured, so
|
|
50
|
+
* `["post:*"]` covers `post:update` — the same matching grants use.
|
|
51
|
+
*/
|
|
41
52
|
readonly permissions: readonly string[];
|
|
53
|
+
/**
|
|
54
|
+
* Whether a decision from this policy may be cached. A policy that reads
|
|
55
|
+
* mutable state outside the actor and resource should set this to `false`.
|
|
56
|
+
* Default: `true`.
|
|
57
|
+
*/
|
|
42
58
|
readonly cacheable?: boolean;
|
|
59
|
+
/** Higher priority policies are evaluated first. Default: 0. */
|
|
43
60
|
readonly priority?: number;
|
|
44
61
|
evaluate(context: PermissionContext): PermissionDecision | Promise<PermissionDecision>;
|
|
45
62
|
}
|
|
46
63
|
/** A step in an explain trace. */
|
|
47
64
|
export interface ExplainStep {
|
|
48
|
-
readonly type: "role" | "permission" | "rule" | "policy" | "deny";
|
|
65
|
+
readonly type: "role" | "permission" | "rule" | "policy" | "condition" | "cache" | "deny";
|
|
49
66
|
readonly detail: string;
|
|
50
67
|
readonly matched: boolean;
|
|
51
68
|
}
|
|
@@ -53,10 +70,26 @@ export interface ExplainStep {
|
|
|
53
70
|
export interface ExplainResult {
|
|
54
71
|
readonly allowed: boolean;
|
|
55
72
|
readonly steps: readonly ExplainStep[];
|
|
73
|
+
/** The decision the steps explain. */
|
|
74
|
+
readonly decision: PermissionDecision;
|
|
56
75
|
}
|
|
57
76
|
/** Options for authorization checks. */
|
|
58
77
|
export interface AuthorizationOptions {
|
|
78
|
+
/**
|
|
79
|
+
* Cancels the check. Evaluation aborts with an
|
|
80
|
+
* `AuthorizationAbortedError`; it does not silently allow.
|
|
81
|
+
*/
|
|
59
82
|
readonly signal?: AbortSignal;
|
|
83
|
+
/** Overrides the engine's policy timeout, in milliseconds. */
|
|
60
84
|
readonly policyTimeout?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Request-scoped metadata made available to conditions and policies as
|
|
87
|
+
* `PermissionContext.metadata` — tenant, IP, time of day.
|
|
88
|
+
*/
|
|
89
|
+
readonly metadata?: ReadonlyMap<string, unknown> | Record<string, unknown>;
|
|
90
|
+
/** Skips the decision cache for this check. */
|
|
91
|
+
readonly skipCache?: boolean;
|
|
92
|
+
/** Identifies the resource for caching. Falls back to `resource.id`. */
|
|
93
|
+
readonly resourceId?: string;
|
|
61
94
|
}
|
|
62
95
|
//# sourceMappingURL=policyTypes.d.ts.map
|
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
import type { PermissionActor, Permission } from "./permissionActor.js";
|
|
7
7
|
/** Whether a rule allows or denies access. */
|
|
8
8
|
export type RuleEffect = "allow" | "deny";
|
|
9
|
+
/**
|
|
10
|
+
* How competing rules are combined into one decision.
|
|
11
|
+
*
|
|
12
|
+
* - `"deny-overrides"` (the default) — any applicable deny wins, whatever its
|
|
13
|
+
* priority. This is the safe reading, and the one the documentation has
|
|
14
|
+
* always described.
|
|
15
|
+
* - `"priority"` — the highest-priority rule wins; deny breaks a tie.
|
|
16
|
+
*/
|
|
17
|
+
export type RuleCombiningAlgorithm = "deny-overrides" | "priority";
|
|
9
18
|
/** A permission rule that grants or denies access under conditions. */
|
|
10
19
|
export interface PermissionRule {
|
|
11
20
|
/** Allow or deny. */
|
|
@@ -14,9 +23,12 @@ export interface PermissionRule {
|
|
|
14
23
|
readonly action: string | readonly string[];
|
|
15
24
|
/** Resource(s) this rule applies to. */
|
|
16
25
|
readonly resource: string | readonly string[];
|
|
17
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Optional condition for ABAC. A rule whose condition returns `false` does
|
|
28
|
+
* not apply — neither to allow nor to deny.
|
|
29
|
+
*/
|
|
18
30
|
readonly condition?: PermissionConditionFn;
|
|
19
|
-
/** Rule priority — higher wins
|
|
31
|
+
/** Rule priority — higher wins under the `"priority"` algorithm. */
|
|
20
32
|
readonly priority?: number;
|
|
21
33
|
/** Human-readable rule name for debugging. */
|
|
22
34
|
readonly name?: string;
|
|
@@ -27,24 +39,45 @@ export type PermissionConditionFn = (context: PermissionContext) => boolean | Pr
|
|
|
27
39
|
export interface PermissionContext {
|
|
28
40
|
/** The actor requesting access. */
|
|
29
41
|
readonly actor: PermissionActor;
|
|
30
|
-
/** The permission
|
|
42
|
+
/** The permission being checked, parsed into resource and action. */
|
|
31
43
|
readonly permission: Permission;
|
|
32
44
|
/** The target resource (optional). */
|
|
33
45
|
readonly resource?: unknown;
|
|
34
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Arbitrary metadata (IP, tenant, timestamp, etc.), supplied through
|
|
48
|
+
* `AuthorizationOptions.metadata`. Conditions such as `tenantIsolation`
|
|
49
|
+
* read the actor's tenant from here.
|
|
50
|
+
*/
|
|
35
51
|
readonly metadata?: ReadonlyMap<string, unknown>;
|
|
52
|
+
/** Cancels long-running conditions and policies. */
|
|
53
|
+
readonly signal?: AbortSignal;
|
|
36
54
|
}
|
|
37
55
|
/** Result of an authorization check. */
|
|
38
56
|
export interface PermissionDecision {
|
|
39
57
|
/** Whether the action is allowed. */
|
|
40
58
|
readonly allowed: boolean;
|
|
41
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Why the decision came out this way, as a stable machine-readable code.
|
|
61
|
+
*
|
|
62
|
+
* Internal by design: it names policies and rules, so it belongs in a log,
|
|
63
|
+
* not in a response body. {@link PermissionDecision.publicReason} is the
|
|
64
|
+
* one safe to return to a caller.
|
|
65
|
+
*/
|
|
42
66
|
readonly reason?: string;
|
|
43
67
|
/** Name of the policy/rule that produced the decision. */
|
|
44
68
|
readonly policy?: string;
|
|
45
69
|
/** The matched permission string. */
|
|
46
70
|
readonly matchedPermission?: string;
|
|
71
|
+
/** A caller-safe message, free of policy and rule names. */
|
|
72
|
+
readonly publicReason?: string;
|
|
47
73
|
/** Additional decision metadata. */
|
|
48
74
|
readonly metadata?: unknown;
|
|
49
75
|
}
|
|
76
|
+
/** The outcome of evaluating a rule set. */
|
|
77
|
+
export interface RuleEvaluation {
|
|
78
|
+
readonly allowed: boolean;
|
|
79
|
+
readonly matchedRule?: PermissionRule;
|
|
80
|
+
/** Rules that matched the target and whose condition passed. */
|
|
81
|
+
readonly applicable: readonly PermissionRule[];
|
|
82
|
+
}
|
|
50
83
|
//# sourceMappingURL=ruleTypes.d.ts.map
|
package/dist/policy/index.d.ts
CHANGED
package/dist/policy/index.js
CHANGED
|
@@ -4,33 +4,26 @@
|
|
|
4
4
|
* @module policy/policyRegistry
|
|
5
5
|
*/
|
|
6
6
|
import type { PermissionPolicyDefinition } from "../permissionTypes/index.js";
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
/** Options for the policy registry. */
|
|
8
|
+
export interface PolicyRegistryOptions {
|
|
9
|
+
/** Allow overwriting an existing policy. Defaults to false. */
|
|
10
|
+
readonly allowOverride?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** A policy registry, usable directly as the engine's `policies` source. */
|
|
13
|
+
export interface PolicyRegistry {
|
|
14
14
|
define(definition: PermissionPolicyDefinition): void;
|
|
15
|
-
/**
|
|
16
|
-
* Get a policy by name.
|
|
17
|
-
*/
|
|
18
15
|
get(name: string): PermissionPolicyDefinition | undefined;
|
|
19
|
-
|
|
20
|
-
* Get all policies that apply to a given permission string.
|
|
21
|
-
*/
|
|
16
|
+
has(name: string): boolean;
|
|
22
17
|
forPermission(permission: string): readonly PermissionPolicyDefinition[];
|
|
23
|
-
/**
|
|
24
|
-
* Get all registered policy names.
|
|
25
|
-
*/
|
|
26
18
|
names(): readonly string[];
|
|
27
|
-
|
|
28
|
-
* Remove a policy.
|
|
29
|
-
*/
|
|
19
|
+
all(): readonly PermissionPolicyDefinition[];
|
|
30
20
|
remove(name: string): boolean;
|
|
31
|
-
/**
|
|
32
|
-
* Clear all policies.
|
|
33
|
-
*/
|
|
34
21
|
clear(): void;
|
|
35
|
-
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a policy registry.
|
|
25
|
+
*
|
|
26
|
+
* Pass it straight to `createPermissionEngine({ policies: registry })`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createPolicyRegistry(options?: PolicyRegistryOptions): PolicyRegistry;
|
|
36
29
|
//# sourceMappingURL=policyRegistry.d.ts.map
|
|
@@ -3,52 +3,55 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module policy/policyRegistry
|
|
5
5
|
*/
|
|
6
|
+
import { DuplicatePolicyError } from "../permissionErrors/index.js";
|
|
7
|
+
import { selectPolicies } from "../evaluator/evaluator.pipeline.js";
|
|
6
8
|
/**
|
|
7
9
|
* Create a policy registry.
|
|
10
|
+
*
|
|
11
|
+
* Pass it straight to `createPermissionEngine({ policies: registry })`.
|
|
8
12
|
*/
|
|
9
|
-
export function createPolicyRegistry() {
|
|
13
|
+
export function createPolicyRegistry(options) {
|
|
10
14
|
const policies = new Map();
|
|
15
|
+
const allowOverride = options?.allowOverride ?? false;
|
|
11
16
|
return {
|
|
12
17
|
/**
|
|
13
18
|
* Register a policy.
|
|
19
|
+
*
|
|
20
|
+
* Re-registering a name is rejected unless `allowOverride` was set: a
|
|
21
|
+
* second `define("owner-only", …)` used to replace the first in silence,
|
|
22
|
+
* which is an authorization rule vanishing without a trace.
|
|
14
23
|
*/
|
|
15
24
|
define(definition) {
|
|
25
|
+
if (policies.has(definition.name) && !allowOverride) {
|
|
26
|
+
throw new DuplicatePolicyError(definition.name);
|
|
27
|
+
}
|
|
16
28
|
policies.set(definition.name, Object.freeze({ ...definition }));
|
|
17
29
|
},
|
|
18
|
-
/**
|
|
19
|
-
* Get a policy by name.
|
|
20
|
-
*/
|
|
21
30
|
get(name) {
|
|
22
31
|
return policies.get(name);
|
|
23
32
|
},
|
|
33
|
+
has(name) {
|
|
34
|
+
return policies.has(name);
|
|
35
|
+
},
|
|
24
36
|
/**
|
|
25
|
-
*
|
|
37
|
+
* Policies that apply to a permission, highest priority first.
|
|
38
|
+
*
|
|
39
|
+
* Delegates to the evaluator's own selection, so this and the engine can
|
|
40
|
+
* never disagree about which policies cover a permission — they were two
|
|
41
|
+
* copies of the same filter-and-sort before.
|
|
26
42
|
*/
|
|
27
43
|
forPermission(permission) {
|
|
28
|
-
|
|
29
|
-
for (const [, policy] of policies) {
|
|
30
|
-
if (policy.permissions.includes(permission)) {
|
|
31
|
-
results.push(policy);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
// Sort by priority descending
|
|
35
|
-
return results.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
44
|
+
return selectPolicies([...policies.values()], permission);
|
|
36
45
|
},
|
|
37
|
-
/**
|
|
38
|
-
* Get all registered policy names.
|
|
39
|
-
*/
|
|
40
46
|
names() {
|
|
41
|
-
return
|
|
47
|
+
return [...policies.keys()];
|
|
48
|
+
},
|
|
49
|
+
all() {
|
|
50
|
+
return [...policies.values()];
|
|
42
51
|
},
|
|
43
|
-
/**
|
|
44
|
-
* Remove a policy.
|
|
45
|
-
*/
|
|
46
52
|
remove(name) {
|
|
47
53
|
return policies.delete(name);
|
|
48
54
|
},
|
|
49
|
-
/**
|
|
50
|
-
* Clear all policies.
|
|
51
|
-
*/
|
|
52
55
|
clear() {
|
|
53
56
|
policies.clear();
|
|
54
57
|
},
|
package/dist/role/index.d.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module role
|
|
5
5
|
*/
|
|
6
|
-
export { createRoleRegistry, type RoleRegistryOptions, } from "./roleRegistry.js";
|
|
7
|
-
export { resolveRolePermissions } from "./roleHierarchy.js";
|
|
6
|
+
export { createRoleRegistry, type RoleRegistry, type RoleRegistryOptions, } from "./roleRegistry.js";
|
|
7
|
+
export { resolveRolePermissions, memoizeRoleLookup, type RoleResolution, type RoleResolutionOptions, } from "./roleHierarchy.js";
|
|
8
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/role/index.js
CHANGED
|
@@ -3,13 +3,43 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module role/roleHierarchy
|
|
5
5
|
*/
|
|
6
|
-
import type { RoleDefinition } from "../permissionTypes/index.js";
|
|
6
|
+
import type { PermissionRule, RoleDefinition } from "../permissionTypes/index.js";
|
|
7
|
+
/** What a set of roles grants, once inheritance is followed. */
|
|
8
|
+
export interface RoleResolution {
|
|
9
|
+
readonly permissions: readonly string[];
|
|
10
|
+
readonly rules: readonly PermissionRule[];
|
|
11
|
+
/** Role names the lookup could not find. */
|
|
12
|
+
readonly unknownRoles: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
/** Options for {@link resolveRolePermissions}. */
|
|
15
|
+
export interface RoleResolutionOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Called for each role the lookup cannot find.
|
|
18
|
+
*
|
|
19
|
+
* Reporting rather than throwing is deliberate: a stale role name in a
|
|
20
|
+
* token is a data problem, and an authorization check that throws hands the
|
|
21
|
+
* outcome to an outer error handler instead of denying.
|
|
22
|
+
*/
|
|
23
|
+
readonly onUnknownRole?: (name: string) => void;
|
|
24
|
+
}
|
|
7
25
|
/**
|
|
8
|
-
* Resolve
|
|
26
|
+
* Resolve everything a set of roles grants, following inheritance chains.
|
|
9
27
|
*
|
|
10
28
|
* @param roleNames - Direct role names assigned to the actor.
|
|
11
29
|
* @param getRole - Function to look up a role definition by name.
|
|
12
|
-
* @returns Deduplicated
|
|
30
|
+
* @returns Deduplicated permissions and rules, plus any unresolved names.
|
|
31
|
+
* @throws CircularRoleInheritanceError when the graph contains a cycle
|
|
13
32
|
*/
|
|
14
|
-
export declare function resolveRolePermissions(roleNames: readonly string[], getRole: (name: string) => RoleDefinition | undefined):
|
|
33
|
+
export declare function resolveRolePermissions(roleNames: readonly string[], getRole: (name: string) => RoleDefinition | undefined, options?: RoleResolutionOptions): RoleResolution;
|
|
34
|
+
/**
|
|
35
|
+
* Wrap a role lookup with a memo, so a deep hierarchy is walked once per
|
|
36
|
+
* distinct role rather than on every authorization check.
|
|
37
|
+
*
|
|
38
|
+
* The cache is invalidated by `invalidate()`; a registry that changes at
|
|
39
|
+
* runtime should call it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function memoizeRoleLookup(getRole: (name: string) => RoleDefinition | undefined): {
|
|
42
|
+
readonly lookup: (name: string) => RoleDefinition | undefined;
|
|
43
|
+
readonly invalidate: () => void;
|
|
44
|
+
};
|
|
15
45
|
//# sourceMappingURL=roleHierarchy.d.ts.map
|
|
@@ -3,46 +3,71 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module role/roleHierarchy
|
|
5
5
|
*/
|
|
6
|
-
import { CircularRoleInheritanceError
|
|
6
|
+
import { CircularRoleInheritanceError } from "../permissionErrors/index.js";
|
|
7
7
|
/**
|
|
8
|
-
* Resolve
|
|
8
|
+
* Resolve everything a set of roles grants, following inheritance chains.
|
|
9
9
|
*
|
|
10
10
|
* @param roleNames - Direct role names assigned to the actor.
|
|
11
11
|
* @param getRole - Function to look up a role definition by name.
|
|
12
|
-
* @returns Deduplicated
|
|
12
|
+
* @returns Deduplicated permissions and rules, plus any unresolved names.
|
|
13
|
+
* @throws CircularRoleInheritanceError when the graph contains a cycle
|
|
13
14
|
*/
|
|
14
|
-
export function resolveRolePermissions(roleNames, getRole) {
|
|
15
|
+
export function resolveRolePermissions(roleNames, getRole, options) {
|
|
15
16
|
const permissions = new Set();
|
|
17
|
+
const rules = [];
|
|
18
|
+
const unknownRoles = [];
|
|
16
19
|
const visited = new Set();
|
|
17
20
|
for (const name of roleNames) {
|
|
18
|
-
|
|
21
|
+
collect(name, getRole, permissions, rules, unknownRoles, visited, []);
|
|
19
22
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* Collect permissions from a role and its ancestors, detecting cycles.
|
|
24
|
-
*/
|
|
25
|
-
function collectPermissions(roleName, getRole, permissions, visited, chain) {
|
|
26
|
-
const role = getRole(roleName);
|
|
27
|
-
if (!role) {
|
|
28
|
-
throw new RoleNotFoundError(roleName);
|
|
23
|
+
if (options?.onUnknownRole) {
|
|
24
|
+
for (const name of unknownRoles)
|
|
25
|
+
options.onUnknownRole(name);
|
|
29
26
|
}
|
|
30
|
-
|
|
27
|
+
return { permissions: [...permissions], rules, unknownRoles };
|
|
28
|
+
}
|
|
29
|
+
function collect(roleName, getRole, permissions, rules, unknownRoles, visited, chain) {
|
|
30
|
+
// Cycle detection comes first: a role that appears twice in the current
|
|
31
|
+
// chain is a cycle even if it has already been visited on another branch.
|
|
31
32
|
if (chain.includes(roleName)) {
|
|
32
33
|
throw new CircularRoleInheritanceError([...chain, roleName]);
|
|
33
34
|
}
|
|
34
|
-
// Skip if already fully processed (not in current chain)
|
|
35
35
|
if (visited.has(roleName))
|
|
36
36
|
return;
|
|
37
|
+
const role = getRole(roleName);
|
|
38
|
+
if (!role) {
|
|
39
|
+
if (!unknownRoles.includes(roleName))
|
|
40
|
+
unknownRoles.push(roleName);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
37
43
|
visited.add(roleName);
|
|
38
44
|
const newChain = [...chain, roleName];
|
|
39
|
-
for (const
|
|
40
|
-
permissions.add(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
45
|
+
for (const permission of role.permissions)
|
|
46
|
+
permissions.add(permission);
|
|
47
|
+
if (role.rules)
|
|
48
|
+
rules.push(...role.rules);
|
|
49
|
+
for (const parent of role.inherits ?? []) {
|
|
50
|
+
collect(parent, getRole, permissions, rules, unknownRoles, visited, newChain);
|
|
46
51
|
}
|
|
47
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Wrap a role lookup with a memo, so a deep hierarchy is walked once per
|
|
55
|
+
* distinct role rather than on every authorization check.
|
|
56
|
+
*
|
|
57
|
+
* The cache is invalidated by `invalidate()`; a registry that changes at
|
|
58
|
+
* runtime should call it.
|
|
59
|
+
*/
|
|
60
|
+
export function memoizeRoleLookup(getRole) {
|
|
61
|
+
const cache = new Map();
|
|
62
|
+
return {
|
|
63
|
+
lookup: (name) => {
|
|
64
|
+
if (cache.has(name))
|
|
65
|
+
return cache.get(name);
|
|
66
|
+
const role = getRole(name);
|
|
67
|
+
cache.set(name, role);
|
|
68
|
+
return role;
|
|
69
|
+
},
|
|
70
|
+
invalidate: () => cache.clear(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
48
73
|
//# sourceMappingURL=roleHierarchy.js.map
|
|
@@ -8,38 +8,29 @@ import type { RoleDefinition } from "../permissionTypes/index.js";
|
|
|
8
8
|
export interface RoleRegistryOptions {
|
|
9
9
|
/** Allow overwriting existing roles. Defaults to false. */
|
|
10
10
|
readonly allowOverride?: boolean;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Create a role registry.
|
|
14
|
-
*/
|
|
15
|
-
export declare function createRoleRegistry(options?: RoleRegistryOptions): {
|
|
16
11
|
/**
|
|
17
|
-
*
|
|
12
|
+
* Reject permission strings that are not `resource:action`. Defaults to
|
|
13
|
+
* true — a malformed grant can never match, so accepting one registers a
|
|
14
|
+
* permission that silently does nothing.
|
|
18
15
|
*/
|
|
16
|
+
readonly validatePermissions?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** A role registry, usable directly as the engine's `roles` source. */
|
|
19
|
+
export interface RoleRegistry {
|
|
19
20
|
define(definition: RoleDefinition): void;
|
|
20
|
-
/**
|
|
21
|
-
* Get a role by name.
|
|
22
|
-
*/
|
|
23
21
|
get(name: string): RoleDefinition | undefined;
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
*/
|
|
22
|
+
/** Like {@link RoleRegistry.get}, but throws when the role is unregistered. */
|
|
23
|
+
require(name: string): RoleDefinition;
|
|
27
24
|
has(name: string): boolean;
|
|
28
|
-
/**
|
|
29
|
-
* Get all registered role names.
|
|
30
|
-
*/
|
|
31
25
|
names(): readonly string[];
|
|
32
|
-
/**
|
|
33
|
-
* Get all registered role definitions.
|
|
34
|
-
*/
|
|
35
26
|
all(): readonly RoleDefinition[];
|
|
36
|
-
/**
|
|
37
|
-
* Remove a role from the registry.
|
|
38
|
-
*/
|
|
39
27
|
remove(name: string): boolean;
|
|
40
|
-
/**
|
|
41
|
-
* Clear all registered roles.
|
|
42
|
-
*/
|
|
43
28
|
clear(): void;
|
|
44
|
-
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a role registry.
|
|
32
|
+
*
|
|
33
|
+
* Pass it straight to `createPermissionEngine({ roles: registry })`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createRoleRegistry(options?: RoleRegistryOptions): RoleRegistry;
|
|
45
36
|
//# sourceMappingURL=roleRegistry.d.ts.map
|