@cullet/erp-core 1.2.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/KIT_CONTEXT.md +5 -3
- package/README.md +160 -0
- package/dist/abac/index.cjs +7 -0
- package/dist/abac/index.d.cts +2 -0
- package/dist/abac/index.d.ts +2 -0
- package/dist/abac/index.js +2 -0
- package/dist/app-error.cjs +193 -0
- package/dist/app-error.cjs.map +1 -0
- package/dist/app-error.d.cts +108 -0
- package/dist/app-error.js +170 -0
- package/dist/app-error.js.map +1 -0
- package/dist/application/index.cjs +27 -0
- package/dist/application/index.d.cts +3 -0
- package/dist/application/index.d.ts +2 -1
- package/dist/application/index.js +2 -1
- package/dist/authorization-error.cjs +157 -0
- package/dist/authorization-error.cjs.map +1 -0
- package/dist/authorization-error.d.cts +113 -0
- package/dist/authorization-error.d.ts +113 -0
- package/dist/authorization-error.js +152 -0
- package/dist/authorization-error.js.map +1 -0
- package/dist/authorizer.port.d.cts +130 -0
- package/dist/authorizer.port.d.ts +130 -0
- package/dist/composite-authorizer.d.cts +216 -0
- package/dist/composite-authorizer.d.ts +216 -0
- package/dist/condition-evaluator.cjs +565 -0
- package/dist/condition-evaluator.cjs.map +1 -0
- package/dist/condition-evaluator.js +536 -0
- package/dist/condition-evaluator.js.map +1 -0
- package/dist/core-config.d.cts +172 -0
- package/dist/{gate-types.d.ts → core-config.d.ts} +78 -77
- package/dist/domain/index.cjs +17 -0
- package/dist/domain/index.d.cts +4 -0
- package/dist/domain/index.d.ts +2 -1
- package/dist/domain/index.js +2 -1
- package/dist/domain-event-contracts.cjs +34 -0
- package/dist/domain-event-contracts.cjs.map +1 -0
- package/dist/domain-event-contracts.d.cts +27 -0
- package/dist/domain-event-contracts.js +2 -1
- package/dist/domain-event-contracts.js.map +1 -1
- package/dist/domain-exception.cjs +17 -0
- package/dist/domain-exception.cjs.map +1 -0
- package/dist/domain-exception.d.cts +7 -0
- package/dist/entity.cjs +126 -0
- package/dist/entity.cjs.map +1 -0
- package/dist/entity.js +115 -0
- package/dist/entity.js.map +1 -0
- package/dist/errors/index.cjs +43 -0
- package/dist/errors/index.d.cts +4 -0
- package/dist/errors/index.d.ts +2 -1
- package/dist/errors/index.js +4 -2
- package/dist/exceptions/index.cjs +17 -0
- package/dist/exceptions/index.d.cts +5 -0
- package/dist/exceptions/validation-field.cjs +3 -0
- package/dist/exceptions/validation-field.d.cts +2 -0
- package/dist/gate-engine-registry.cjs +309 -0
- package/dist/gate-engine-registry.cjs.map +1 -0
- package/dist/gate-engine-registry.d.cts +82 -0
- package/dist/gate-engine-registry.d.ts +3 -2
- package/dist/gate-engine-registry.js +2 -1
- package/dist/gate-engine-registry.js.map +1 -1
- package/dist/gate-v1-payload.schema.cjs +76 -0
- package/dist/gate-v1-payload.schema.cjs.map +1 -0
- package/dist/gate-v1-payload.schema.js +1 -533
- package/dist/gate-v1-payload.schema.js.map +1 -1
- package/dist/hashing.cjs +66 -0
- package/dist/hashing.cjs.map +1 -0
- package/dist/immutable.cjs +77 -0
- package/dist/immutable.cjs.map +1 -0
- package/dist/immutable.d.cts +6 -0
- package/dist/index.cjs +181 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +37 -0
- package/dist/index.d.ts +19 -12
- package/dist/index.js +15 -9
- package/dist/index.js.map +1 -1
- package/dist/invalid-state-transition-exception.cjs +47 -0
- package/dist/invalid-state-transition-exception.cjs.map +1 -0
- package/dist/invariant-violation-exception.cjs +16 -0
- package/dist/invariant-violation-exception.cjs.map +1 -0
- package/dist/not-found-error.cjs +65 -0
- package/dist/not-found-error.cjs.map +1 -0
- package/dist/not-found-error.js +1 -1
- package/dist/outcome.cjs +97 -0
- package/dist/outcome.cjs.map +1 -0
- package/dist/outcome.d.cts +58 -0
- package/dist/outcome.d.ts +1 -83
- package/dist/parse-gate-payload.d.cts +62 -0
- package/dist/parse-gate-payload.d.ts +2 -2
- package/dist/path.d.cts +90 -0
- package/dist/path.d.ts +2 -2
- package/dist/plugin.cjs +79 -0
- package/dist/plugin.cjs.map +1 -0
- package/dist/plugin.d.cts +85 -0
- package/dist/plugins/index.cjs +3 -0
- package/dist/plugins/index.d.cts +2 -0
- package/dist/policies/engines/index.cjs +10 -0
- package/dist/policies/engines/index.d.cts +4 -0
- package/dist/policies/engines/index.d.ts +1 -1
- package/dist/policies/engines/v1/gate/index.cjs +92 -0
- package/dist/policies/engines/v1/gate/index.cjs.map +1 -0
- package/dist/policies/engines/v1/gate/index.d.cts +121 -0
- package/dist/policies/engines/v1/gate/index.d.ts +2 -2
- package/dist/policies/engines/v1/gate/index.js +2 -1
- package/dist/policies/engines/v1/gate/index.js.map +1 -1
- package/dist/policies/index.cjs +41 -0
- package/dist/policies/index.d.cts +8 -0
- package/dist/policies/index.d.ts +3 -2
- package/dist/policies/index.js +2 -2
- package/dist/policy-bridge.cjs +437 -0
- package/dist/policy-bridge.cjs.map +1 -0
- package/dist/policy-bridge.d.cts +185 -0
- package/dist/policy-bridge.d.ts +185 -0
- package/dist/policy-bridge.js +396 -0
- package/dist/policy-bridge.js.map +1 -0
- package/dist/policy-service.cjs +1190 -0
- package/dist/policy-service.cjs.map +1 -0
- package/dist/policy-service.d.cts +559 -0
- package/dist/policy-service.d.ts +2 -2
- package/dist/policy-service.js +239 -239
- package/dist/policy-service.js.map +1 -1
- package/dist/rbac/index.cjs +9 -0
- package/dist/rbac/index.d.cts +3 -0
- package/dist/rbac/index.d.ts +3 -0
- package/dist/rbac/index.js +2 -0
- package/dist/requested-by.cjs +54 -0
- package/dist/requested-by.cjs.map +1 -0
- package/dist/requested-by.d.cts +25 -0
- package/dist/requested-by.d.ts +25 -0
- package/dist/requested-by.js +49 -0
- package/dist/requested-by.js.map +1 -0
- package/dist/result/index.cjs +7 -0
- package/dist/result/index.d.cts +3 -0
- package/dist/result/index.d.ts +2 -1
- package/dist/result.cjs +135 -0
- package/dist/result.cjs.map +1 -0
- package/dist/result.d.cts +84 -0
- package/dist/result.d.ts +84 -0
- package/dist/rule.cjs +327 -0
- package/dist/rule.cjs.map +1 -0
- package/dist/rule.js +298 -0
- package/dist/rule.js.map +1 -0
- package/dist/ruleset-registry.cjs +47 -0
- package/dist/ruleset-registry.cjs.map +1 -0
- package/dist/rulesets/index.cjs +3 -0
- package/dist/rulesets/index.d.cts +2 -0
- package/dist/temporal-guards.cjs +32 -0
- package/dist/temporal-guards.cjs.map +1 -0
- package/dist/temporal-use-case.cjs +139 -0
- package/dist/temporal-use-case.cjs.map +1 -0
- package/dist/temporal-use-case.d.cts +282 -0
- package/dist/temporal-use-case.d.ts +5 -27
- package/dist/temporal-use-case.js +2 -48
- package/dist/temporal-use-case.js.map +1 -1
- package/dist/unexpected-error.cjs +19 -0
- package/dist/unexpected-error.cjs.map +1 -0
- package/dist/unexpected-error.js +2 -167
- package/dist/unexpected-error.js.map +1 -1
- package/dist/use-case.cjs +96 -0
- package/dist/use-case.cjs.map +1 -0
- package/dist/uuid-identifier.cjs +65 -0
- package/dist/uuid-identifier.cjs.map +1 -0
- package/dist/uuid-identifier.d.cts +147 -0
- package/dist/uuid-identifier.d.ts +2 -85
- package/dist/validation-code.cjs +60 -0
- package/dist/validation-code.cjs.map +1 -0
- package/dist/validation-code.d.cts +23 -0
- package/dist/validation-error.cjs +887 -0
- package/dist/validation-error.cjs.map +1 -0
- package/dist/validation-error.d.cts +681 -0
- package/dist/validation-error.d.ts +2 -98
- package/dist/validation-error.js +7 -134
- package/dist/validation-error.js.map +1 -1
- package/dist/validation-exception.cjs +41 -0
- package/dist/validation-exception.cjs.map +1 -0
- package/dist/validation-exception.d.cts +50 -0
- package/dist/validation-field.cjs +28 -0
- package/dist/validation-field.cjs.map +1 -0
- package/dist/validation-field.d.cts +12 -0
- package/dist/value-object-ruleset.contracts.d.cts +36 -0
- package/dist/value-object.cjs +85 -0
- package/dist/value-object.cjs.map +1 -0
- package/dist/value-object.d.cts +89 -0
- package/dist/value-object.d.ts +89 -0
- package/dist/value-object.js +1 -112
- package/dist/value-object.js.map +1 -1
- package/dist/version.d.cts +10 -0
- package/dist/versioning/index.cjs +7 -0
- package/dist/versioning/index.d.cts +3 -0
- package/meta.json +19 -4
- package/package.json +173 -28
- package/src/abac/index.ts +1 -0
- package/src/core/abac/abac-request.ts +29 -0
- package/src/core/abac/attributes.ts +39 -0
- package/src/core/abac/authorizer.ts +108 -0
- package/src/core/abac/combining.ts +63 -0
- package/src/core/abac/composite-authorizer.ts +45 -0
- package/src/core/abac/domain/policy-set.ts +52 -0
- package/src/core/abac/domain/rule.ts +197 -0
- package/src/core/abac/index.ts +21 -0
- package/src/core/application/ports/abac-authorizer.port.ts +20 -0
- package/src/core/application/ports/authorizer.port.ts +22 -0
- package/src/core/errors/authorization-error.ts +26 -0
- package/src/core/errors/error-codes.ts +1 -0
- package/src/core/index.ts +7 -0
- package/src/core/rbac/access-request.ts +32 -0
- package/src/core/rbac/authorizer.ts +91 -0
- package/src/core/rbac/domain/grant.ts +96 -0
- package/src/core/rbac/domain/permission-set.ts +67 -0
- package/src/core/rbac/domain/permission.ts +119 -0
- package/src/core/rbac/domain/role.ts +101 -0
- package/src/core/rbac/domain/scope.ts +84 -0
- package/src/core/rbac/index.ts +15 -0
- package/src/core/rbac/policy-bridge.ts +44 -0
- package/src/examples/application/authorize-cancel-order-abac.example.ts +107 -0
- package/src/examples/application/authorize-cancel-order.example.ts +101 -0
- package/src/rbac/index.ts +1 -0
- package/src/version.ts +1 -1
package/dist/rule.cjs
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
const require_authorization_error = require("./authorization-error.cjs");
|
|
2
|
+
const require_validation_code = require("./validation-code.cjs");
|
|
3
|
+
const require_validation_field = require("./validation-field.cjs");
|
|
4
|
+
const require_validation_exception = require("./validation-exception.cjs");
|
|
5
|
+
const require_value_object = require("./value-object.cjs");
|
|
6
|
+
const require_result = require("./result.cjs");
|
|
7
|
+
const require_condition_evaluator = require("./condition-evaluator.cjs");
|
|
8
|
+
//#region src/core/abac/attributes.ts
|
|
9
|
+
/**
|
|
10
|
+
* Flattens an {@link AbacRequest} into the nested {@link PolicyContext} the
|
|
11
|
+
* condition evaluator reads (it resolves dotted fields like `resource.status` by
|
|
12
|
+
* walking nested objects). The actor lands as `subject.id`; the four attribute
|
|
13
|
+
* bags nest under `subject` / `resource` / `action` / `env`; a `resource`
|
|
14
|
+
* reference adds `resource.type` / `resource.id`. The actor id and the resource
|
|
15
|
+
* reference win over any same-named keys in the attribute bags.
|
|
16
|
+
*
|
|
17
|
+
* Consumers fold dynamic facts (e.g. RBAC roles/permissions derived from grants,
|
|
18
|
+
* or a computed `ownerIsActor` flag) into `attributes.subject` / `resource` so
|
|
19
|
+
* rules can reference `subject.roles`, `resource.ownerIsActor`, and the like.
|
|
20
|
+
*/
|
|
21
|
+
function abacContext(request) {
|
|
22
|
+
const attributes = request.attributes;
|
|
23
|
+
const subject = {
|
|
24
|
+
...attributes.subject,
|
|
25
|
+
id: request.actor.raw
|
|
26
|
+
};
|
|
27
|
+
const resource = { ...attributes.resource };
|
|
28
|
+
if (request.resource) {
|
|
29
|
+
resource.type = request.resource.type;
|
|
30
|
+
if (request.resource.id !== void 0) resource.id = request.resource.id;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
subject,
|
|
34
|
+
resource,
|
|
35
|
+
action: { ...attributes.action },
|
|
36
|
+
env: { ...attributes.environment }
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/core/abac/combining.ts
|
|
41
|
+
function firstWithEffect(rules, effect) {
|
|
42
|
+
return rules.find((rule) => rule.effect === effect);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Applies `algorithm` to the already-matched `applicable` rules, falling back to
|
|
46
|
+
* `defaultEffect` when the algorithm reaches no rule-backed decision. Pure and
|
|
47
|
+
* total; the returned `decidingRule` is absent exactly when `defaultEffect` was
|
|
48
|
+
* used.
|
|
49
|
+
*/
|
|
50
|
+
function combine(algorithm, applicable, defaultEffect) {
|
|
51
|
+
switch (algorithm) {
|
|
52
|
+
case "deny-overrides": {
|
|
53
|
+
const deny = firstWithEffect(applicable, "DENY");
|
|
54
|
+
if (deny) return {
|
|
55
|
+
effect: "DENY",
|
|
56
|
+
decidingRule: deny
|
|
57
|
+
};
|
|
58
|
+
const permit = firstWithEffect(applicable, "PERMIT");
|
|
59
|
+
if (permit) return {
|
|
60
|
+
effect: "PERMIT",
|
|
61
|
+
decidingRule: permit
|
|
62
|
+
};
|
|
63
|
+
return { effect: defaultEffect };
|
|
64
|
+
}
|
|
65
|
+
case "permit-overrides": {
|
|
66
|
+
const permit = firstWithEffect(applicable, "PERMIT");
|
|
67
|
+
if (permit) return {
|
|
68
|
+
effect: "PERMIT",
|
|
69
|
+
decidingRule: permit
|
|
70
|
+
};
|
|
71
|
+
const deny = firstWithEffect(applicable, "DENY");
|
|
72
|
+
if (deny) return {
|
|
73
|
+
effect: "DENY",
|
|
74
|
+
decidingRule: deny
|
|
75
|
+
};
|
|
76
|
+
return { effect: defaultEffect };
|
|
77
|
+
}
|
|
78
|
+
case "first-applicable": {
|
|
79
|
+
const first = applicable[0];
|
|
80
|
+
if (first) return {
|
|
81
|
+
effect: first.effect,
|
|
82
|
+
decidingRule: first
|
|
83
|
+
};
|
|
84
|
+
return { effect: defaultEffect };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/core/abac/authorizer.ts
|
|
90
|
+
const ENGINE_VERSION = 1;
|
|
91
|
+
/**
|
|
92
|
+
* The pure ABAC decisor. Given an {@link AbacRequest} and an
|
|
93
|
+
* {@link AbacPolicySet}, it flattens the request's attributes into a context,
|
|
94
|
+
* matches each rule's condition with the gate engine's pure condition evaluator
|
|
95
|
+
* (reused, not reimplemented), resolves the effects through the set's combining
|
|
96
|
+
* algorithm, and returns a {@link Result} — never throwing, mirroring the
|
|
97
|
+
* "errors as values" contract of `RbacAuthorizer`/`GateEngineV1`.
|
|
98
|
+
*
|
|
99
|
+
* A denial maps to {@link AuthorizationError.policyDenied}, attributed to the
|
|
100
|
+
* deciding rule's `id`/`version`; a condition-evaluation failure (a missing
|
|
101
|
+
* attribute, a wrong-typed operand) fails closed as
|
|
102
|
+
* {@link AuthorizationError.forbidden}. Loading the dynamic attributes is the
|
|
103
|
+
* consumer's job (behind an `AbacAuthorizerPort` adapter); this class only decides.
|
|
104
|
+
*/
|
|
105
|
+
var AbacAuthorizer = class {
|
|
106
|
+
constructor(params = {}) {
|
|
107
|
+
this.coreConfig = params.coreConfig ?? require_condition_evaluator.coreConfig;
|
|
108
|
+
}
|
|
109
|
+
authorize(request, policies) {
|
|
110
|
+
const evaluator = new require_condition_evaluator.ConditionEvaluatorV1(abacContext(request), this.coreConfig.getConditionEvaluationOptions(ENGINE_VERSION));
|
|
111
|
+
const applicable = [];
|
|
112
|
+
for (const rule of policies.rules) {
|
|
113
|
+
const matched = evaluator.evaluate(rule.condition);
|
|
114
|
+
if (matched.isErr()) return require_result.Result.err(require_authorization_error.AuthorizationError.forbidden({
|
|
115
|
+
action: request.action,
|
|
116
|
+
resource: request.resource,
|
|
117
|
+
actor: { userId: request.actor.raw },
|
|
118
|
+
details: matched.errorOrNull() ?? void 0
|
|
119
|
+
}));
|
|
120
|
+
if (matched.getOrThrow()) applicable.push(rule);
|
|
121
|
+
}
|
|
122
|
+
const { effect, decidingRule } = combine(policies.algorithm, applicable, policies.defaultEffect);
|
|
123
|
+
if (effect === "PERMIT") return require_result.Result.ok({
|
|
124
|
+
action: request.action,
|
|
125
|
+
effect: "PERMIT",
|
|
126
|
+
matchedRule: decidingRule?.id ?? "<default>",
|
|
127
|
+
algorithm: policies.algorithm,
|
|
128
|
+
grantedAtIso: (/* @__PURE__ */ new Date()).toISOString()
|
|
129
|
+
});
|
|
130
|
+
return require_result.Result.err(require_authorization_error.AuthorizationError.policyDenied({
|
|
131
|
+
policyId: decidingRule?.id ?? "<default-deny>",
|
|
132
|
+
policyVersion: decidingRule?.version ?? 0,
|
|
133
|
+
evaluatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
134
|
+
action: request.action,
|
|
135
|
+
resource: request.resource,
|
|
136
|
+
actor: { userId: request.actor.raw },
|
|
137
|
+
reasonCode: decidingRule?.id
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/core/abac/composite-authorizer.ts
|
|
143
|
+
/**
|
|
144
|
+
* Runs a coarse RBAC check first, then refines with ABAC — the standard hybrid
|
|
145
|
+
* "does the actor hold the capability, *and* do the attributes allow it here?"
|
|
146
|
+
* flow. Short-circuits with the RBAC denial when the actor is not even capable,
|
|
147
|
+
* so the boundary sees the most specific 403.
|
|
148
|
+
*
|
|
149
|
+
* Only the two port interfaces are referenced (both erased at runtime), so this
|
|
150
|
+
* pulls in no RBAC/ABAC engine code of its own — it just sequences the seams the
|
|
151
|
+
* consumer wires up.
|
|
152
|
+
*/
|
|
153
|
+
var CompositeAuthorizer = class {
|
|
154
|
+
constructor(rbac, abac) {
|
|
155
|
+
this.rbac = rbac;
|
|
156
|
+
this.abac = abac;
|
|
157
|
+
}
|
|
158
|
+
async authorize(request) {
|
|
159
|
+
const coarse = await this.rbac.authorize(request);
|
|
160
|
+
if (coarse.isErr()) return coarse;
|
|
161
|
+
return this.abac.authorize(request);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/core/abac/domain/policy-set.ts
|
|
166
|
+
/**
|
|
167
|
+
* An ordered collection of {@link AbacRule}s plus how to combine them
|
|
168
|
+
* ({@link CombiningAlgorithm}) and what to decide when no rule applies
|
|
169
|
+
* ({@link defaultEffect}). This is where ABAC goes beyond a single gate
|
|
170
|
+
* condition: several PERMIT/DENY rules resolved by an explicit algorithm.
|
|
171
|
+
*
|
|
172
|
+
* Closed by default — the combining algorithm defaults to `"deny-overrides"` and
|
|
173
|
+
* the default effect to `"DENY"`, so a request matching nothing is denied. A
|
|
174
|
+
* plain holder (not a value object); build through {@link of}.
|
|
175
|
+
*/
|
|
176
|
+
var AbacPolicySet = class AbacPolicySet {
|
|
177
|
+
constructor(_rules, _algorithm, _defaultEffect) {
|
|
178
|
+
this._rules = _rules;
|
|
179
|
+
this._algorithm = _algorithm;
|
|
180
|
+
this._defaultEffect = _defaultEffect;
|
|
181
|
+
Object.freeze(this._rules);
|
|
182
|
+
Object.freeze(this);
|
|
183
|
+
}
|
|
184
|
+
static of(rules, options = {}) {
|
|
185
|
+
return new AbacPolicySet([...rules], options.algorithm ?? "deny-overrides", options.defaultEffect ?? "DENY");
|
|
186
|
+
}
|
|
187
|
+
get rules() {
|
|
188
|
+
return this._rules;
|
|
189
|
+
}
|
|
190
|
+
get algorithm() {
|
|
191
|
+
return this._algorithm;
|
|
192
|
+
}
|
|
193
|
+
get defaultEffect() {
|
|
194
|
+
return this._defaultEffect;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/core/abac/domain/rule.ts
|
|
199
|
+
const RULE_FIELD = require_validation_field.ValidationField.of("abacRule");
|
|
200
|
+
const CONDITION_OPS = new Set([
|
|
201
|
+
"eq",
|
|
202
|
+
"neq",
|
|
203
|
+
"gt",
|
|
204
|
+
"gte",
|
|
205
|
+
"lt",
|
|
206
|
+
"lte",
|
|
207
|
+
"in",
|
|
208
|
+
"notIn",
|
|
209
|
+
"isNull",
|
|
210
|
+
"isNotNull"
|
|
211
|
+
]);
|
|
212
|
+
const RULE_EFFECTS = new Set(["PERMIT", "DENY"]);
|
|
213
|
+
function isPlainObject(value) {
|
|
214
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* A single attribute-based rule: an {@link RuleEffect} that takes hold when its
|
|
218
|
+
* {@link ConditionNode} matches the request's flattened attributes. The
|
|
219
|
+
* condition reuses the gate/compute condition DSL (`{ field, op, value }` leaves
|
|
220
|
+
* combined with `and`/`or`/`not`), so a consumer learns one condition language
|
|
221
|
+
* across the whole kit.
|
|
222
|
+
*
|
|
223
|
+
* A zod-free value object: it validates its own shape on construction (throwing
|
|
224
|
+
* {@link InvalidValueException}) and is deep-frozen thereafter. Because rules are
|
|
225
|
+
* authored in TypeScript — trusted data, not untrusted JSON — the structural
|
|
226
|
+
* check is a hand-rolled walk of the condition tree rather than a schema parse.
|
|
227
|
+
* Build one through {@link of}; the constructor is private.
|
|
228
|
+
*/
|
|
229
|
+
var AbacRule = class AbacRule extends require_value_object.ValueObject {
|
|
230
|
+
constructor(props) {
|
|
231
|
+
super(props);
|
|
232
|
+
this.finalize();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Builds a rule, validating that `id` is non-blank, `version` is an integer
|
|
236
|
+
* ≥ 1, `effect` is `"PERMIT"`/`"DENY"`, and `condition` is a well-formed
|
|
237
|
+
* condition node. Throws {@link InvalidValueException} otherwise.
|
|
238
|
+
*/
|
|
239
|
+
static of(props) {
|
|
240
|
+
if (typeof props.id !== "string" || props.id.trim().length === 0) throw new require_validation_exception.InvalidValueException(RULE_FIELD.nested("id"), require_validation_code.ValidationCode.BLANK, `abac rule id must be a non-empty string, got "${props.id}"`);
|
|
241
|
+
if (!Number.isInteger(props.version) || props.version < 1) throw new require_validation_exception.InvalidValueException(RULE_FIELD.nested("version"), require_validation_code.ValidationCode.OUT_OF_RANGE, `abac rule version must be an integer >= 1, got "${props.version}"`);
|
|
242
|
+
if (!RULE_EFFECTS.has(props.effect)) throw new require_validation_exception.InvalidValueException(RULE_FIELD.nested("effect"), require_validation_code.ValidationCode.INVALID_FORMAT, `abac rule effect must be "PERMIT" or "DENY", got "${String(props.effect)}"`);
|
|
243
|
+
AbacRule.assertConditionShape(props.condition);
|
|
244
|
+
return new AbacRule(props);
|
|
245
|
+
}
|
|
246
|
+
static assertConditionShape(node) {
|
|
247
|
+
if (!isPlainObject(node)) AbacRule.rejectCondition("abac rule condition must be a leaf { field, op } or an { and | or | not } node");
|
|
248
|
+
if ("and" in node || "or" in node) {
|
|
249
|
+
const key = "and" in node ? "and" : "or";
|
|
250
|
+
const children = node[key];
|
|
251
|
+
if (!Array.isArray(children) || children.length === 0) AbacRule.rejectCondition(`abac rule "${key}" node must hold a non-empty array of child conditions`);
|
|
252
|
+
for (const child of children) AbacRule.assertConditionShape(child);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if ("not" in node) {
|
|
256
|
+
AbacRule.assertConditionShape(node.not);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if ("field" in node && "op" in node) {
|
|
260
|
+
if (typeof node.field !== "string" || node.field.trim().length === 0) AbacRule.rejectCondition(`abac rule condition leaf "field" must be a non-empty string, got "${String(node.field)}"`);
|
|
261
|
+
if (typeof node.op !== "string" || !CONDITION_OPS.has(node.op)) AbacRule.rejectCondition(`abac rule condition leaf "op" is not a supported operator, got "${String(node.op)}"`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
AbacRule.rejectCondition("abac rule condition must be a leaf { field, op } or an { and | or | not } node");
|
|
265
|
+
}
|
|
266
|
+
static rejectCondition(message) {
|
|
267
|
+
throw new require_validation_exception.InvalidValueException(RULE_FIELD.nested("condition"), require_validation_code.ValidationCode.INVALID_FORMAT, message);
|
|
268
|
+
}
|
|
269
|
+
get id() {
|
|
270
|
+
return this.value.id;
|
|
271
|
+
}
|
|
272
|
+
get version() {
|
|
273
|
+
return this.value.version;
|
|
274
|
+
}
|
|
275
|
+
get effect() {
|
|
276
|
+
return this.value.effect;
|
|
277
|
+
}
|
|
278
|
+
/** The condition tree evaluated against a request's flattened attributes. */
|
|
279
|
+
get condition() {
|
|
280
|
+
return this.value.condition;
|
|
281
|
+
}
|
|
282
|
+
get description() {
|
|
283
|
+
return this.value.description;
|
|
284
|
+
}
|
|
285
|
+
toPrimitive() {
|
|
286
|
+
return {
|
|
287
|
+
id: this.value.id,
|
|
288
|
+
version: this.value.version,
|
|
289
|
+
effect: this.value.effect,
|
|
290
|
+
condition: this.value.condition,
|
|
291
|
+
...this.value.description !== void 0 ? { description: this.value.description } : {}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
//#endregion
|
|
296
|
+
Object.defineProperty(exports, "AbacAuthorizer", {
|
|
297
|
+
enumerable: true,
|
|
298
|
+
get: function() {
|
|
299
|
+
return AbacAuthorizer;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
Object.defineProperty(exports, "AbacPolicySet", {
|
|
303
|
+
enumerable: true,
|
|
304
|
+
get: function() {
|
|
305
|
+
return AbacPolicySet;
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
Object.defineProperty(exports, "AbacRule", {
|
|
309
|
+
enumerable: true,
|
|
310
|
+
get: function() {
|
|
311
|
+
return AbacRule;
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
Object.defineProperty(exports, "CompositeAuthorizer", {
|
|
315
|
+
enumerable: true,
|
|
316
|
+
get: function() {
|
|
317
|
+
return CompositeAuthorizer;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
Object.defineProperty(exports, "abacContext", {
|
|
321
|
+
enumerable: true,
|
|
322
|
+
get: function() {
|
|
323
|
+
return abacContext;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
//# sourceMappingURL=rule.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule.cjs","names":["coreConfig","ConditionEvaluatorV1","Result","AuthorizationError","ValidationField","ValueObject","InvalidValueException","ValidationCode"],"sources":["../src/core/abac/attributes.ts","../src/core/abac/combining.ts","../src/core/abac/authorizer.ts","../src/core/abac/composite-authorizer.ts","../src/core/abac/domain/policy-set.ts","../src/core/abac/domain/rule.ts"],"sourcesContent":["import type { PolicyContext } from \"../policies/engines/gate-types.js\";\n\nimport type { AbacRequest } from \"./abac-request.js\";\n\n/**\n * Flattens an {@link AbacRequest} into the nested {@link PolicyContext} the\n * condition evaluator reads (it resolves dotted fields like `resource.status` by\n * walking nested objects). The actor lands as `subject.id`; the four attribute\n * bags nest under `subject` / `resource` / `action` / `env`; a `resource`\n * reference adds `resource.type` / `resource.id`. The actor id and the resource\n * reference win over any same-named keys in the attribute bags.\n *\n * Consumers fold dynamic facts (e.g. RBAC roles/permissions derived from grants,\n * or a computed `ownerIsActor` flag) into `attributes.subject` / `resource` so\n * rules can reference `subject.roles`, `resource.ownerIsActor`, and the like.\n */\nexport function abacContext(request: AbacRequest): PolicyContext {\n const attributes = request.attributes;\n\n const subject: Record<string, unknown> = {\n ...attributes.subject,\n id: request.actor.raw,\n };\n\n const resource: Record<string, unknown> = { ...attributes.resource };\n if (request.resource) {\n resource.type = request.resource.type;\n if (request.resource.id !== undefined) {\n resource.id = request.resource.id;\n }\n }\n\n return {\n subject,\n resource,\n action: { ...attributes.action },\n env: { ...attributes.environment },\n };\n}\n","import type { AbacRule, RuleEffect } from \"./domain/rule.js\";\n\n/**\n * How an {@link AbacPolicySet} resolves several applicable rules into one effect.\n *\n * - `deny-overrides` (default, secure): any applicable DENY wins; otherwise the\n * first PERMIT; otherwise the set's default effect.\n * - `permit-overrides`: any applicable PERMIT wins; otherwise the first DENY;\n * otherwise the default effect.\n * - `first-applicable`: the effect of the first rule that matches; otherwise the\n * default effect.\n */\nexport type CombiningAlgorithm =\n | \"deny-overrides\"\n | \"permit-overrides\"\n | \"first-applicable\";\n\n/** The resolved decision plus the rule (if any) that produced it. */\nexport interface CombineResult {\n readonly effect: RuleEffect;\n readonly decidingRule?: AbacRule;\n}\n\nfunction firstWithEffect(\n rules: readonly AbacRule[],\n effect: RuleEffect,\n): AbacRule | undefined {\n return rules.find((rule) => rule.effect === effect);\n}\n\n/**\n * Applies `algorithm` to the already-matched `applicable` rules, falling back to\n * `defaultEffect` when the algorithm reaches no rule-backed decision. Pure and\n * total; the returned `decidingRule` is absent exactly when `defaultEffect` was\n * used.\n */\nexport function combine(\n algorithm: CombiningAlgorithm,\n applicable: readonly AbacRule[],\n defaultEffect: RuleEffect,\n): CombineResult {\n switch (algorithm) {\n case \"deny-overrides\": {\n const deny = firstWithEffect(applicable, \"DENY\");\n if (deny) return { effect: \"DENY\", decidingRule: deny };\n const permit = firstWithEffect(applicable, \"PERMIT\");\n if (permit) return { effect: \"PERMIT\", decidingRule: permit };\n return { effect: defaultEffect };\n }\n case \"permit-overrides\": {\n const permit = firstWithEffect(applicable, \"PERMIT\");\n if (permit) return { effect: \"PERMIT\", decidingRule: permit };\n const deny = firstWithEffect(applicable, \"DENY\");\n if (deny) return { effect: \"DENY\", decidingRule: deny };\n return { effect: defaultEffect };\n }\n case \"first-applicable\": {\n const first = applicable[0];\n if (first) return { effect: first.effect, decidingRule: first };\n return { effect: defaultEffect };\n }\n }\n}\n","import { type CoreConfig, coreConfig } from \"../config/index.js\";\nimport { AuthorizationError } from \"../errors/authorization-error.js\";\nimport { ConditionEvaluatorV1 } from \"../policies/engines/v1/condition-evaluator.js\";\nimport { Result } from \"../result/result.js\";\n\nimport type { AbacRequest } from \"./abac-request.js\";\nimport { abacContext } from \"./attributes.js\";\nimport { combine, type CombiningAlgorithm } from \"./combining.js\";\nimport type { AbacPolicySet } from \"./domain/policy-set.js\";\nimport type { AbacRule } from \"./domain/rule.js\";\n\n// ABAC reuses the gate engine's v1 condition matcher; the version stamps the\n// evaluation reports the shared reporter emits.\nconst ENGINE_VERSION = 1;\n\n/** The successful decision returned by {@link AbacAuthorizer.authorize}. */\ninterface AbacDecision {\n /** The audited business action that was allowed. */\n readonly action: string;\n readonly effect: \"PERMIT\";\n /** The id of the rule that permitted, or `\"<default>\"` when the set's default did. */\n readonly matchedRule: string;\n readonly algorithm: CombiningAlgorithm;\n /** When the decision was made, ISO-8601. */\n readonly grantedAtIso: string;\n}\n\n/**\n * The pure ABAC decisor. Given an {@link AbacRequest} and an\n * {@link AbacPolicySet}, it flattens the request's attributes into a context,\n * matches each rule's condition with the gate engine's pure condition evaluator\n * (reused, not reimplemented), resolves the effects through the set's combining\n * algorithm, and returns a {@link Result} — never throwing, mirroring the\n * \"errors as values\" contract of `RbacAuthorizer`/`GateEngineV1`.\n *\n * A denial maps to {@link AuthorizationError.policyDenied}, attributed to the\n * deciding rule's `id`/`version`; a condition-evaluation failure (a missing\n * attribute, a wrong-typed operand) fails closed as\n * {@link AuthorizationError.forbidden}. Loading the dynamic attributes is the\n * consumer's job (behind an `AbacAuthorizerPort` adapter); this class only decides.\n */\nclass AbacAuthorizer {\n private readonly coreConfig: CoreConfig;\n\n constructor(params: { readonly coreConfig?: CoreConfig } = {}) {\n this.coreConfig = params.coreConfig ?? coreConfig;\n }\n\n authorize(\n request: AbacRequest,\n policies: AbacPolicySet,\n ): Result<AbacDecision, AuthorizationError> {\n const context = abacContext(request);\n const evaluator = new ConditionEvaluatorV1(\n context,\n this.coreConfig.getConditionEvaluationOptions(ENGINE_VERSION),\n );\n\n const applicable: AbacRule[] = [];\n for (const rule of policies.rules) {\n const matched = evaluator.evaluate(rule.condition);\n if (matched.isErr()) {\n // Fail closed: a technical evaluation error is never a silent PERMIT.\n return Result.err(\n AuthorizationError.forbidden({\n action: request.action,\n resource: request.resource,\n actor: { userId: request.actor.raw },\n details: matched.errorOrNull() ?? undefined,\n }),\n );\n }\n if (matched.getOrThrow()) {\n applicable.push(rule);\n }\n }\n\n const { effect, decidingRule } = combine(\n policies.algorithm,\n applicable,\n policies.defaultEffect,\n );\n\n if (effect === \"PERMIT\") {\n return Result.ok({\n action: request.action,\n effect: \"PERMIT\",\n matchedRule: decidingRule?.id ?? \"<default>\",\n algorithm: policies.algorithm,\n grantedAtIso: new Date().toISOString(),\n });\n }\n\n return Result.err(\n AuthorizationError.policyDenied({\n policyId: decidingRule?.id ?? \"<default-deny>\",\n policyVersion: decidingRule?.version ?? 0,\n evaluatedAtIso: new Date().toISOString(),\n action: request.action,\n resource: request.resource,\n actor: { userId: request.actor.raw },\n reasonCode: decidingRule?.id,\n }),\n );\n }\n}\n\nexport { AbacAuthorizer, type AbacDecision };\n","import { Result } from \"../result/result.js\";\nimport type { AbacAuthorizerPort } from \"../application/ports/abac-authorizer.port.js\";\nimport type { AuthorizerPort } from \"../application/ports/authorizer.port.js\";\nimport type { AuthorizationError } from \"../errors/authorization-error.js\";\nimport type { AccessRequest } from \"../rbac/access-request.js\";\n\nimport type { AbacAttributes } from \"./abac-request.js\";\n\n/**\n * The request a {@link CompositeAuthorizer} accepts — one object that satisfies\n * both the RBAC {@link AccessRequest} (its `required`/`scope`) and the ABAC\n * request (its `attributes`).\n */\ntype CompositeAccessRequest = AccessRequest & {\n readonly attributes: AbacAttributes;\n};\n\n/**\n * Runs a coarse RBAC check first, then refines with ABAC — the standard hybrid\n * \"does the actor hold the capability, *and* do the attributes allow it here?\"\n * flow. Short-circuits with the RBAC denial when the actor is not even capable,\n * so the boundary sees the most specific 403.\n *\n * Only the two port interfaces are referenced (both erased at runtime), so this\n * pulls in no RBAC/ABAC engine code of its own — it just sequences the seams the\n * consumer wires up.\n */\nclass CompositeAuthorizer {\n constructor(\n private readonly rbac: AuthorizerPort,\n private readonly abac: AbacAuthorizerPort,\n ) {}\n\n async authorize(\n request: CompositeAccessRequest,\n ): Promise<Result<void, AuthorizationError>> {\n const coarse = await this.rbac.authorize(request);\n if (coarse.isErr()) {\n return coarse;\n }\n return this.abac.authorize(request);\n }\n}\n\nexport { CompositeAuthorizer, type CompositeAccessRequest };\n","import type { CombiningAlgorithm } from \"../combining.js\";\n\nimport type { AbacRule, RuleEffect } from \"./rule.js\";\n\n/**\n * An ordered collection of {@link AbacRule}s plus how to combine them\n * ({@link CombiningAlgorithm}) and what to decide when no rule applies\n * ({@link defaultEffect}). This is where ABAC goes beyond a single gate\n * condition: several PERMIT/DENY rules resolved by an explicit algorithm.\n *\n * Closed by default — the combining algorithm defaults to `\"deny-overrides\"` and\n * the default effect to `\"DENY\"`, so a request matching nothing is denied. A\n * plain holder (not a value object); build through {@link of}.\n */\nclass AbacPolicySet {\n private constructor(\n private readonly _rules: readonly AbacRule[],\n private readonly _algorithm: CombiningAlgorithm,\n private readonly _defaultEffect: RuleEffect,\n ) {\n Object.freeze(this._rules);\n Object.freeze(this);\n }\n\n static of(\n rules: readonly AbacRule[],\n options: {\n readonly algorithm?: CombiningAlgorithm;\n readonly defaultEffect?: RuleEffect;\n } = {},\n ): AbacPolicySet {\n return new AbacPolicySet(\n [...rules],\n options.algorithm ?? \"deny-overrides\",\n options.defaultEffect ?? \"DENY\",\n );\n }\n\n get rules(): readonly AbacRule[] {\n return this._rules;\n }\n\n get algorithm(): CombiningAlgorithm {\n return this._algorithm;\n }\n\n get defaultEffect(): RuleEffect {\n return this._defaultEffect;\n }\n}\n\nexport { AbacPolicySet };\n","import { ValidationCode } from \"../../exceptions/validation-code.js\";\nimport { InvalidValueException } from \"../../exceptions/validation-exception.js\";\nimport { ValidationField } from \"../../exceptions/validation-field.js\";\nimport { ValueObject } from \"../../domain/value-object.js\";\nimport type {\n ConditionNode,\n ConditionOp,\n} from \"../../policies/engines/v1/condition-types.js\";\n\n/** Whether a matching {@link AbacRule} permits or denies the action. */\ntype RuleEffect = \"PERMIT\" | \"DENY\";\n\n/**\n * The serializable shape of a rule: a stable `id`/`version` (surfaced on a\n * denial as `policyId`/`policyVersion`), the {@link RuleEffect}, and the\n * {@link ConditionNode} that decides whether the rule applies to a request's\n * flattened attributes.\n */\ntype AbacRuleProps = {\n readonly id: string;\n readonly version: number;\n readonly effect: RuleEffect;\n readonly condition: ConditionNode;\n readonly description?: string;\n};\n\nconst RULE_FIELD = ValidationField.of(\"abacRule\");\n\nconst CONDITION_OPS = new Set<ConditionOp>([\n \"eq\",\n \"neq\",\n \"gt\",\n \"gte\",\n \"lt\",\n \"lte\",\n \"in\",\n \"notIn\",\n \"isNull\",\n \"isNotNull\",\n]);\n\nconst RULE_EFFECTS = new Set<RuleEffect>([\"PERMIT\", \"DENY\"]);\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * A single attribute-based rule: an {@link RuleEffect} that takes hold when its\n * {@link ConditionNode} matches the request's flattened attributes. The\n * condition reuses the gate/compute condition DSL (`{ field, op, value }` leaves\n * combined with `and`/`or`/`not`), so a consumer learns one condition language\n * across the whole kit.\n *\n * A zod-free value object: it validates its own shape on construction (throwing\n * {@link InvalidValueException}) and is deep-frozen thereafter. Because rules are\n * authored in TypeScript — trusted data, not untrusted JSON — the structural\n * check is a hand-rolled walk of the condition tree rather than a schema parse.\n * Build one through {@link of}; the constructor is private.\n */\nclass AbacRule extends ValueObject<AbacRuleProps, AbacRuleProps> {\n private constructor(props: AbacRuleProps) {\n super(props);\n this.finalize();\n }\n\n /**\n * Builds a rule, validating that `id` is non-blank, `version` is an integer\n * ≥ 1, `effect` is `\"PERMIT\"`/`\"DENY\"`, and `condition` is a well-formed\n * condition node. Throws {@link InvalidValueException} otherwise.\n */\n static of(props: AbacRuleProps): AbacRule {\n if (typeof props.id !== \"string\" || props.id.trim().length === 0) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"id\"),\n ValidationCode.BLANK,\n `abac rule id must be a non-empty string, got \"${props.id}\"`,\n );\n }\n\n if (!Number.isInteger(props.version) || props.version < 1) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"version\"),\n ValidationCode.OUT_OF_RANGE,\n `abac rule version must be an integer >= 1, got \"${props.version}\"`,\n );\n }\n\n if (!RULE_EFFECTS.has(props.effect)) {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"effect\"),\n ValidationCode.INVALID_FORMAT,\n `abac rule effect must be \"PERMIT\" or \"DENY\", got \"${String(props.effect)}\"`,\n );\n }\n\n AbacRule.assertConditionShape(props.condition);\n\n return new AbacRule(props);\n }\n\n // Mirrors what the shared condition evaluator would reject at eval time\n // (empty and/or nodes, malformed leaves) but fails fast at construction.\n private static assertConditionShape(node: unknown): void {\n if (!isPlainObject(node)) {\n AbacRule.rejectCondition(\n \"abac rule condition must be a leaf { field, op } or an { and | or | not } node\",\n );\n }\n\n if (\"and\" in node || \"or\" in node) {\n const key = \"and\" in node ? \"and\" : \"or\";\n const children = node[key];\n if (!Array.isArray(children) || children.length === 0) {\n AbacRule.rejectCondition(\n `abac rule \"${key}\" node must hold a non-empty array of child conditions`,\n );\n }\n for (const child of children) {\n AbacRule.assertConditionShape(child);\n }\n return;\n }\n\n if (\"not\" in node) {\n AbacRule.assertConditionShape(node.not);\n return;\n }\n\n if (\"field\" in node && \"op\" in node) {\n if (\n typeof node.field !== \"string\" ||\n node.field.trim().length === 0\n ) {\n AbacRule.rejectCondition(\n `abac rule condition leaf \"field\" must be a non-empty string, got \"${String(node.field)}\"`,\n );\n }\n if (\n typeof node.op !== \"string\" ||\n !CONDITION_OPS.has(node.op as ConditionOp)\n ) {\n AbacRule.rejectCondition(\n `abac rule condition leaf \"op\" is not a supported operator, got \"${String(node.op)}\"`,\n );\n }\n return;\n }\n\n AbacRule.rejectCondition(\n \"abac rule condition must be a leaf { field, op } or an { and | or | not } node\",\n );\n }\n\n private static rejectCondition(message: string): never {\n throw new InvalidValueException(\n RULE_FIELD.nested(\"condition\"),\n ValidationCode.INVALID_FORMAT,\n message,\n );\n }\n\n get id(): string {\n return this.value.id;\n }\n\n get version(): number {\n return this.value.version;\n }\n\n get effect(): RuleEffect {\n return this.value.effect;\n }\n\n /** The condition tree evaluated against a request's flattened attributes. */\n get condition(): ConditionNode {\n return this.value.condition as ConditionNode;\n }\n\n get description(): string | undefined {\n return this.value.description;\n }\n\n toPrimitive(): AbacRuleProps {\n return {\n id: this.value.id,\n version: this.value.version,\n effect: this.value.effect,\n condition: this.value.condition as ConditionNode,\n ...(this.value.description !== undefined\n ? { description: this.value.description }\n : {}),\n };\n }\n}\n\nexport { AbacRule, type AbacRuleProps, type RuleEffect };\n"],"mappings":"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"}
|
package/dist/rule.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { t as AuthorizationError } from "./authorization-error.js";
|
|
2
|
+
import { t as ValidationCode } from "./validation-code.js";
|
|
3
|
+
import { t as ValidationField } from "./validation-field.js";
|
|
4
|
+
import { t as InvalidValueException } from "./validation-exception.js";
|
|
5
|
+
import { t as ValueObject } from "./value-object.js";
|
|
6
|
+
import { r as Result } from "./result.js";
|
|
7
|
+
import { r as coreConfig, t as ConditionEvaluatorV1 } from "./condition-evaluator.js";
|
|
8
|
+
//#region src/core/abac/attributes.ts
|
|
9
|
+
/**
|
|
10
|
+
* Flattens an {@link AbacRequest} into the nested {@link PolicyContext} the
|
|
11
|
+
* condition evaluator reads (it resolves dotted fields like `resource.status` by
|
|
12
|
+
* walking nested objects). The actor lands as `subject.id`; the four attribute
|
|
13
|
+
* bags nest under `subject` / `resource` / `action` / `env`; a `resource`
|
|
14
|
+
* reference adds `resource.type` / `resource.id`. The actor id and the resource
|
|
15
|
+
* reference win over any same-named keys in the attribute bags.
|
|
16
|
+
*
|
|
17
|
+
* Consumers fold dynamic facts (e.g. RBAC roles/permissions derived from grants,
|
|
18
|
+
* or a computed `ownerIsActor` flag) into `attributes.subject` / `resource` so
|
|
19
|
+
* rules can reference `subject.roles`, `resource.ownerIsActor`, and the like.
|
|
20
|
+
*/
|
|
21
|
+
function abacContext(request) {
|
|
22
|
+
const attributes = request.attributes;
|
|
23
|
+
const subject = {
|
|
24
|
+
...attributes.subject,
|
|
25
|
+
id: request.actor.raw
|
|
26
|
+
};
|
|
27
|
+
const resource = { ...attributes.resource };
|
|
28
|
+
if (request.resource) {
|
|
29
|
+
resource.type = request.resource.type;
|
|
30
|
+
if (request.resource.id !== void 0) resource.id = request.resource.id;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
subject,
|
|
34
|
+
resource,
|
|
35
|
+
action: { ...attributes.action },
|
|
36
|
+
env: { ...attributes.environment }
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/core/abac/combining.ts
|
|
41
|
+
function firstWithEffect(rules, effect) {
|
|
42
|
+
return rules.find((rule) => rule.effect === effect);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Applies `algorithm` to the already-matched `applicable` rules, falling back to
|
|
46
|
+
* `defaultEffect` when the algorithm reaches no rule-backed decision. Pure and
|
|
47
|
+
* total; the returned `decidingRule` is absent exactly when `defaultEffect` was
|
|
48
|
+
* used.
|
|
49
|
+
*/
|
|
50
|
+
function combine(algorithm, applicable, defaultEffect) {
|
|
51
|
+
switch (algorithm) {
|
|
52
|
+
case "deny-overrides": {
|
|
53
|
+
const deny = firstWithEffect(applicable, "DENY");
|
|
54
|
+
if (deny) return {
|
|
55
|
+
effect: "DENY",
|
|
56
|
+
decidingRule: deny
|
|
57
|
+
};
|
|
58
|
+
const permit = firstWithEffect(applicable, "PERMIT");
|
|
59
|
+
if (permit) return {
|
|
60
|
+
effect: "PERMIT",
|
|
61
|
+
decidingRule: permit
|
|
62
|
+
};
|
|
63
|
+
return { effect: defaultEffect };
|
|
64
|
+
}
|
|
65
|
+
case "permit-overrides": {
|
|
66
|
+
const permit = firstWithEffect(applicable, "PERMIT");
|
|
67
|
+
if (permit) return {
|
|
68
|
+
effect: "PERMIT",
|
|
69
|
+
decidingRule: permit
|
|
70
|
+
};
|
|
71
|
+
const deny = firstWithEffect(applicable, "DENY");
|
|
72
|
+
if (deny) return {
|
|
73
|
+
effect: "DENY",
|
|
74
|
+
decidingRule: deny
|
|
75
|
+
};
|
|
76
|
+
return { effect: defaultEffect };
|
|
77
|
+
}
|
|
78
|
+
case "first-applicable": {
|
|
79
|
+
const first = applicable[0];
|
|
80
|
+
if (first) return {
|
|
81
|
+
effect: first.effect,
|
|
82
|
+
decidingRule: first
|
|
83
|
+
};
|
|
84
|
+
return { effect: defaultEffect };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/core/abac/authorizer.ts
|
|
90
|
+
const ENGINE_VERSION = 1;
|
|
91
|
+
/**
|
|
92
|
+
* The pure ABAC decisor. Given an {@link AbacRequest} and an
|
|
93
|
+
* {@link AbacPolicySet}, it flattens the request's attributes into a context,
|
|
94
|
+
* matches each rule's condition with the gate engine's pure condition evaluator
|
|
95
|
+
* (reused, not reimplemented), resolves the effects through the set's combining
|
|
96
|
+
* algorithm, and returns a {@link Result} — never throwing, mirroring the
|
|
97
|
+
* "errors as values" contract of `RbacAuthorizer`/`GateEngineV1`.
|
|
98
|
+
*
|
|
99
|
+
* A denial maps to {@link AuthorizationError.policyDenied}, attributed to the
|
|
100
|
+
* deciding rule's `id`/`version`; a condition-evaluation failure (a missing
|
|
101
|
+
* attribute, a wrong-typed operand) fails closed as
|
|
102
|
+
* {@link AuthorizationError.forbidden}. Loading the dynamic attributes is the
|
|
103
|
+
* consumer's job (behind an `AbacAuthorizerPort` adapter); this class only decides.
|
|
104
|
+
*/
|
|
105
|
+
var AbacAuthorizer = class {
|
|
106
|
+
constructor(params = {}) {
|
|
107
|
+
this.coreConfig = params.coreConfig ?? coreConfig;
|
|
108
|
+
}
|
|
109
|
+
authorize(request, policies) {
|
|
110
|
+
const evaluator = new ConditionEvaluatorV1(abacContext(request), this.coreConfig.getConditionEvaluationOptions(ENGINE_VERSION));
|
|
111
|
+
const applicable = [];
|
|
112
|
+
for (const rule of policies.rules) {
|
|
113
|
+
const matched = evaluator.evaluate(rule.condition);
|
|
114
|
+
if (matched.isErr()) return Result.err(AuthorizationError.forbidden({
|
|
115
|
+
action: request.action,
|
|
116
|
+
resource: request.resource,
|
|
117
|
+
actor: { userId: request.actor.raw },
|
|
118
|
+
details: matched.errorOrNull() ?? void 0
|
|
119
|
+
}));
|
|
120
|
+
if (matched.getOrThrow()) applicable.push(rule);
|
|
121
|
+
}
|
|
122
|
+
const { effect, decidingRule } = combine(policies.algorithm, applicable, policies.defaultEffect);
|
|
123
|
+
if (effect === "PERMIT") return Result.ok({
|
|
124
|
+
action: request.action,
|
|
125
|
+
effect: "PERMIT",
|
|
126
|
+
matchedRule: decidingRule?.id ?? "<default>",
|
|
127
|
+
algorithm: policies.algorithm,
|
|
128
|
+
grantedAtIso: (/* @__PURE__ */ new Date()).toISOString()
|
|
129
|
+
});
|
|
130
|
+
return Result.err(AuthorizationError.policyDenied({
|
|
131
|
+
policyId: decidingRule?.id ?? "<default-deny>",
|
|
132
|
+
policyVersion: decidingRule?.version ?? 0,
|
|
133
|
+
evaluatedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
|
|
134
|
+
action: request.action,
|
|
135
|
+
resource: request.resource,
|
|
136
|
+
actor: { userId: request.actor.raw },
|
|
137
|
+
reasonCode: decidingRule?.id
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/core/abac/composite-authorizer.ts
|
|
143
|
+
/**
|
|
144
|
+
* Runs a coarse RBAC check first, then refines with ABAC — the standard hybrid
|
|
145
|
+
* "does the actor hold the capability, *and* do the attributes allow it here?"
|
|
146
|
+
* flow. Short-circuits with the RBAC denial when the actor is not even capable,
|
|
147
|
+
* so the boundary sees the most specific 403.
|
|
148
|
+
*
|
|
149
|
+
* Only the two port interfaces are referenced (both erased at runtime), so this
|
|
150
|
+
* pulls in no RBAC/ABAC engine code of its own — it just sequences the seams the
|
|
151
|
+
* consumer wires up.
|
|
152
|
+
*/
|
|
153
|
+
var CompositeAuthorizer = class {
|
|
154
|
+
constructor(rbac, abac) {
|
|
155
|
+
this.rbac = rbac;
|
|
156
|
+
this.abac = abac;
|
|
157
|
+
}
|
|
158
|
+
async authorize(request) {
|
|
159
|
+
const coarse = await this.rbac.authorize(request);
|
|
160
|
+
if (coarse.isErr()) return coarse;
|
|
161
|
+
return this.abac.authorize(request);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/core/abac/domain/policy-set.ts
|
|
166
|
+
/**
|
|
167
|
+
* An ordered collection of {@link AbacRule}s plus how to combine them
|
|
168
|
+
* ({@link CombiningAlgorithm}) and what to decide when no rule applies
|
|
169
|
+
* ({@link defaultEffect}). This is where ABAC goes beyond a single gate
|
|
170
|
+
* condition: several PERMIT/DENY rules resolved by an explicit algorithm.
|
|
171
|
+
*
|
|
172
|
+
* Closed by default — the combining algorithm defaults to `"deny-overrides"` and
|
|
173
|
+
* the default effect to `"DENY"`, so a request matching nothing is denied. A
|
|
174
|
+
* plain holder (not a value object); build through {@link of}.
|
|
175
|
+
*/
|
|
176
|
+
var AbacPolicySet = class AbacPolicySet {
|
|
177
|
+
constructor(_rules, _algorithm, _defaultEffect) {
|
|
178
|
+
this._rules = _rules;
|
|
179
|
+
this._algorithm = _algorithm;
|
|
180
|
+
this._defaultEffect = _defaultEffect;
|
|
181
|
+
Object.freeze(this._rules);
|
|
182
|
+
Object.freeze(this);
|
|
183
|
+
}
|
|
184
|
+
static of(rules, options = {}) {
|
|
185
|
+
return new AbacPolicySet([...rules], options.algorithm ?? "deny-overrides", options.defaultEffect ?? "DENY");
|
|
186
|
+
}
|
|
187
|
+
get rules() {
|
|
188
|
+
return this._rules;
|
|
189
|
+
}
|
|
190
|
+
get algorithm() {
|
|
191
|
+
return this._algorithm;
|
|
192
|
+
}
|
|
193
|
+
get defaultEffect() {
|
|
194
|
+
return this._defaultEffect;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/core/abac/domain/rule.ts
|
|
199
|
+
const RULE_FIELD = ValidationField.of("abacRule");
|
|
200
|
+
const CONDITION_OPS = new Set([
|
|
201
|
+
"eq",
|
|
202
|
+
"neq",
|
|
203
|
+
"gt",
|
|
204
|
+
"gte",
|
|
205
|
+
"lt",
|
|
206
|
+
"lte",
|
|
207
|
+
"in",
|
|
208
|
+
"notIn",
|
|
209
|
+
"isNull",
|
|
210
|
+
"isNotNull"
|
|
211
|
+
]);
|
|
212
|
+
const RULE_EFFECTS = new Set(["PERMIT", "DENY"]);
|
|
213
|
+
function isPlainObject(value) {
|
|
214
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* A single attribute-based rule: an {@link RuleEffect} that takes hold when its
|
|
218
|
+
* {@link ConditionNode} matches the request's flattened attributes. The
|
|
219
|
+
* condition reuses the gate/compute condition DSL (`{ field, op, value }` leaves
|
|
220
|
+
* combined with `and`/`or`/`not`), so a consumer learns one condition language
|
|
221
|
+
* across the whole kit.
|
|
222
|
+
*
|
|
223
|
+
* A zod-free value object: it validates its own shape on construction (throwing
|
|
224
|
+
* {@link InvalidValueException}) and is deep-frozen thereafter. Because rules are
|
|
225
|
+
* authored in TypeScript — trusted data, not untrusted JSON — the structural
|
|
226
|
+
* check is a hand-rolled walk of the condition tree rather than a schema parse.
|
|
227
|
+
* Build one through {@link of}; the constructor is private.
|
|
228
|
+
*/
|
|
229
|
+
var AbacRule = class AbacRule extends ValueObject {
|
|
230
|
+
constructor(props) {
|
|
231
|
+
super(props);
|
|
232
|
+
this.finalize();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Builds a rule, validating that `id` is non-blank, `version` is an integer
|
|
236
|
+
* ≥ 1, `effect` is `"PERMIT"`/`"DENY"`, and `condition` is a well-formed
|
|
237
|
+
* condition node. Throws {@link InvalidValueException} otherwise.
|
|
238
|
+
*/
|
|
239
|
+
static of(props) {
|
|
240
|
+
if (typeof props.id !== "string" || props.id.trim().length === 0) throw new InvalidValueException(RULE_FIELD.nested("id"), ValidationCode.BLANK, `abac rule id must be a non-empty string, got "${props.id}"`);
|
|
241
|
+
if (!Number.isInteger(props.version) || props.version < 1) throw new InvalidValueException(RULE_FIELD.nested("version"), ValidationCode.OUT_OF_RANGE, `abac rule version must be an integer >= 1, got "${props.version}"`);
|
|
242
|
+
if (!RULE_EFFECTS.has(props.effect)) throw new InvalidValueException(RULE_FIELD.nested("effect"), ValidationCode.INVALID_FORMAT, `abac rule effect must be "PERMIT" or "DENY", got "${String(props.effect)}"`);
|
|
243
|
+
AbacRule.assertConditionShape(props.condition);
|
|
244
|
+
return new AbacRule(props);
|
|
245
|
+
}
|
|
246
|
+
static assertConditionShape(node) {
|
|
247
|
+
if (!isPlainObject(node)) AbacRule.rejectCondition("abac rule condition must be a leaf { field, op } or an { and | or | not } node");
|
|
248
|
+
if ("and" in node || "or" in node) {
|
|
249
|
+
const key = "and" in node ? "and" : "or";
|
|
250
|
+
const children = node[key];
|
|
251
|
+
if (!Array.isArray(children) || children.length === 0) AbacRule.rejectCondition(`abac rule "${key}" node must hold a non-empty array of child conditions`);
|
|
252
|
+
for (const child of children) AbacRule.assertConditionShape(child);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if ("not" in node) {
|
|
256
|
+
AbacRule.assertConditionShape(node.not);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if ("field" in node && "op" in node) {
|
|
260
|
+
if (typeof node.field !== "string" || node.field.trim().length === 0) AbacRule.rejectCondition(`abac rule condition leaf "field" must be a non-empty string, got "${String(node.field)}"`);
|
|
261
|
+
if (typeof node.op !== "string" || !CONDITION_OPS.has(node.op)) AbacRule.rejectCondition(`abac rule condition leaf "op" is not a supported operator, got "${String(node.op)}"`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
AbacRule.rejectCondition("abac rule condition must be a leaf { field, op } or an { and | or | not } node");
|
|
265
|
+
}
|
|
266
|
+
static rejectCondition(message) {
|
|
267
|
+
throw new InvalidValueException(RULE_FIELD.nested("condition"), ValidationCode.INVALID_FORMAT, message);
|
|
268
|
+
}
|
|
269
|
+
get id() {
|
|
270
|
+
return this.value.id;
|
|
271
|
+
}
|
|
272
|
+
get version() {
|
|
273
|
+
return this.value.version;
|
|
274
|
+
}
|
|
275
|
+
get effect() {
|
|
276
|
+
return this.value.effect;
|
|
277
|
+
}
|
|
278
|
+
/** The condition tree evaluated against a request's flattened attributes. */
|
|
279
|
+
get condition() {
|
|
280
|
+
return this.value.condition;
|
|
281
|
+
}
|
|
282
|
+
get description() {
|
|
283
|
+
return this.value.description;
|
|
284
|
+
}
|
|
285
|
+
toPrimitive() {
|
|
286
|
+
return {
|
|
287
|
+
id: this.value.id,
|
|
288
|
+
version: this.value.version,
|
|
289
|
+
effect: this.value.effect,
|
|
290
|
+
condition: this.value.condition,
|
|
291
|
+
...this.value.description !== void 0 ? { description: this.value.description } : {}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
//#endregion
|
|
296
|
+
export { abacContext as a, AbacAuthorizer as i, AbacPolicySet as n, CompositeAuthorizer as r, AbacRule as t };
|
|
297
|
+
|
|
298
|
+
//# sourceMappingURL=rule.js.map
|