@authup/access 1.0.0-beta.53 → 1.0.0-beta.55
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/dist/index.d.mts +150 -24
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +309 -89
- package/dist/index.mjs.map +1 -1
- package/dist/permission/evaluator/module.d.ts +12 -1
- package/dist/permission/evaluator/module.d.ts.map +1 -1
- package/dist/permission/evaluator/types.d.ts +48 -0
- package/dist/permission/evaluator/types.d.ts.map +1 -1
- package/dist/permission/helpers/aggregate.d.ts +1 -1
- package/dist/permission/realm-scope/constants.d.ts +1 -1
- package/dist/permission/realm-scope/constants.d.ts.map +1 -1
- package/dist/permission/types.d.ts +7 -7
- package/dist/permission/types.d.ts.map +1 -1
- package/dist/policy/built-in/attribute-names/evaluator.d.ts +1 -0
- package/dist/policy/built-in/attribute-names/evaluator.d.ts.map +1 -1
- package/dist/policy/built-in/attributes/evaluator.d.ts +5 -0
- package/dist/policy/built-in/attributes/evaluator.d.ts.map +1 -1
- package/dist/policy/built-in/attributes/types.d.ts +2 -2
- package/dist/policy/built-in/attributes/types.d.ts.map +1 -1
- package/dist/policy/built-in/composite/evaluator.d.ts.map +1 -1
- package/dist/policy/built-in/composite/types.d.ts +1 -1
- package/dist/policy/built-in/composite/types.d.ts.map +1 -1
- package/dist/policy/built-in/constants.d.ts +0 -1
- package/dist/policy/built-in/constants.d.ts.map +1 -1
- package/dist/policy/built-in/identity/evaluator.d.ts +1 -0
- package/dist/policy/built-in/identity/evaluator.d.ts.map +1 -1
- package/dist/policy/built-in/identity/types.d.ts +1 -1
- package/dist/policy/built-in/permission-binding/module.d.ts +1 -0
- package/dist/policy/built-in/permission-binding/module.d.ts.map +1 -1
- package/dist/policy/built-in/realm-match/evaluator.d.ts +14 -0
- package/dist/policy/built-in/realm-match/evaluator.d.ts.map +1 -1
- package/dist/policy/built-in/realm-match/types.d.ts +6 -6
- package/dist/policy/built-in/realm-match/types.d.ts.map +1 -1
- package/dist/policy/built-in/time/types.d.ts +3 -3
- package/dist/policy/built-in/time/types.d.ts.map +1 -1
- package/dist/policy/built-in/types.d.ts +2 -0
- package/dist/policy/built-in/types.d.ts.map +1 -1
- package/dist/policy/engine/module.d.ts.map +1 -1
- package/dist/policy/evaluation/context/define.d.ts.map +1 -1
- package/dist/policy/evaluation/context/types.d.ts +7 -0
- package/dist/policy/evaluation/context/types.d.ts.map +1 -1
- package/dist/policy/evaluation/types.d.ts +42 -0
- package/dist/policy/evaluation/types.d.ts.map +1 -1
- package/package.json +6 -4
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { AuthupError, ErrorCode, hasInstanceof, isAuthupError, markInstanceof } from "@authup/errors";
|
|
2
2
|
import { DecisionStrategy, flattenObject, hasOwnProperty, isObject } from "@authup/kit";
|
|
3
3
|
import { isEqual } from "smob";
|
|
4
|
+
import { and, eq, inArray, nin, not, or } from "@rapiq/core";
|
|
4
5
|
import { Container, defineIssueGroup, defineIssueItem } from "validup";
|
|
5
6
|
import { createValidator } from "@validup/zod";
|
|
6
7
|
import { z } from "zod";
|
|
7
|
-
import {
|
|
8
|
+
import { compileFilters } from "@rapiq/memory";
|
|
9
|
+
import { MongoFiltersParser } from "@rapiq/parser-mongo";
|
|
8
10
|
//#region src/permission/error/module.ts
|
|
9
11
|
const PERMISSION_ERROR_INSTANCE = Symbol.for("@authup/access/PermissionError");
|
|
10
12
|
var PermissionError = class PermissionError extends AuthupError {
|
|
@@ -72,7 +74,7 @@ let RealmScope = /* @__PURE__ */ function(RealmScope) {
|
|
|
72
74
|
RealmScope["NONE"] = "none";
|
|
73
75
|
/** Own realm only — resource realm must equal the actor's realm. */
|
|
74
76
|
RealmScope["OWN"] = "own";
|
|
75
|
-
/** Own realm or null/global resources (
|
|
77
|
+
/** Own realm or null/global resources (realmId === null). */
|
|
76
78
|
RealmScope["OWN_OR_NULL"] = "ownOrNull";
|
|
77
79
|
/** Any realm, including null/global. */
|
|
78
80
|
RealmScope["ANY"] = "any";
|
|
@@ -162,7 +164,7 @@ function formatKeySegment(value) {
|
|
|
162
164
|
return value || "*";
|
|
163
165
|
}
|
|
164
166
|
function buildPermissionKey(input) {
|
|
165
|
-
return `${formatKeySegment(input.
|
|
167
|
+
return `${formatKeySegment(input.realmId)}/${formatKeySegment(input.clientId)}/${input.name}`;
|
|
166
168
|
}
|
|
167
169
|
//#endregion
|
|
168
170
|
//#region src/permission/helpers/aggregate.ts
|
|
@@ -170,24 +172,24 @@ function buildPermissionKey(input) {
|
|
|
170
172
|
* One disjunction term per raw binding: the grant's realm reach (fail-closed `own` default)
|
|
171
173
|
* paired with its policy — the raw single policy (its `id` preserved for junction propagation),
|
|
172
174
|
* a composite over multiple policies (Layer-1 permission policies, combined under the
|
|
173
|
-
* permission's
|
|
175
|
+
* permission's decisionStrategy), or `undefined` when the grant is unrestricted.
|
|
174
176
|
*/
|
|
175
177
|
function buildGrant(binding) {
|
|
176
178
|
let policy;
|
|
177
179
|
if (binding.policies && binding.policies.length === 1) policy = binding.policies[0];
|
|
178
180
|
else if (binding.policies && binding.policies.length > 1) policy = {
|
|
179
181
|
type: "composite",
|
|
180
|
-
|
|
182
|
+
decisionStrategy: binding.permission.decisionStrategy || DecisionStrategy.UNANIMOUS,
|
|
181
183
|
children: binding.policies
|
|
182
184
|
};
|
|
183
185
|
return {
|
|
184
|
-
|
|
186
|
+
realmScope: normalizeRealmScope(binding.realmScope),
|
|
185
187
|
policy
|
|
186
188
|
};
|
|
187
189
|
}
|
|
188
190
|
/**
|
|
189
191
|
* Group raw permission-policy bindings by permission key into the actor's disjunction of
|
|
190
|
-
* `(
|
|
192
|
+
* `(realmScope, policy)` grants per permission — with NO lossy collapse. Every consumer
|
|
191
193
|
* (the binding evaluator, isSuperset, junction propagation, the memory provider) evaluates the
|
|
192
194
|
* grant disjunction directly, so each grant's realm reach stays paired with the policy that
|
|
193
195
|
* gates it.
|
|
@@ -213,8 +215,8 @@ function aggregatePermissionPolicyBindings(input) {
|
|
|
213
215
|
//#region src/permission/helpers/equal.ts
|
|
214
216
|
function isPermissionPolicyBindingEqual(a, b) {
|
|
215
217
|
if (a.permission.name !== b.permission.name) return false;
|
|
216
|
-
if ((a.permission.
|
|
217
|
-
if ((a.permission.
|
|
218
|
+
if ((a.permission.realmId ?? null) !== (b.permission.realmId ?? null)) return false;
|
|
219
|
+
if ((a.permission.clientId ?? null) !== (b.permission.clientId ?? null)) return false;
|
|
218
220
|
return true;
|
|
219
221
|
}
|
|
220
222
|
//#endregion
|
|
@@ -223,9 +225,9 @@ function isPermissionPolicyBindingEqual(a, b) {
|
|
|
223
225
|
* Policy fields that do NOT affect evaluation — persisted identity, audit and label columns plus
|
|
224
226
|
* the loaded `parent` / `realm` relation objects. They are stripped before the structural compare
|
|
225
227
|
* in isPolicyEquivalent so two DISTINCT rows with identical configuration still match. Verified
|
|
226
|
-
* against every built-in evaluator, which read only `type`, `invert`, `
|
|
228
|
+
* against every built-in evaluator, which read only `type`, `invert`, `decisionStrategy`,
|
|
227
229
|
* `children` and the type-specific config (query / names / types / start / end / interval /
|
|
228
|
-
*
|
|
230
|
+
* dayOf* / attributeName* / scope). `parent` is additionally a circular back-reference and
|
|
229
231
|
* `realm` a nested object with its own id, so leaving either in would break the deep compare.
|
|
230
232
|
*
|
|
231
233
|
* SECURITY INVARIANT: every key here MUST stay non-evaluation-affecting. Adding an
|
|
@@ -234,16 +236,16 @@ function isPermissionPolicyBindingEqual(a, b) {
|
|
|
234
236
|
*/
|
|
235
237
|
const NON_SEMANTIC_POLICY_KEYS = /* @__PURE__ */ new Set([
|
|
236
238
|
"id",
|
|
237
|
-
"
|
|
239
|
+
"builtIn",
|
|
238
240
|
"name",
|
|
239
|
-
"
|
|
241
|
+
"displayName",
|
|
240
242
|
"description",
|
|
241
|
-
"
|
|
243
|
+
"parentId",
|
|
242
244
|
"parent",
|
|
243
|
-
"
|
|
245
|
+
"realmId",
|
|
244
246
|
"realm",
|
|
245
|
-
"
|
|
246
|
-
"
|
|
247
|
+
"createdAt",
|
|
248
|
+
"updatedAt"
|
|
247
249
|
]);
|
|
248
250
|
/**
|
|
249
251
|
* Project a policy node onto its evaluation-relevant configuration: drop the non-semantic keys
|
|
@@ -305,7 +307,7 @@ function policyDominates(parent, child) {
|
|
|
305
307
|
* every child grant must be dominated by SOME parent grant.
|
|
306
308
|
*/
|
|
307
309
|
function grantDominates(parent, child) {
|
|
308
|
-
if (compareRealmScope(parent.
|
|
310
|
+
if (compareRealmScope(parent.realmScope, child.realmScope) < 0) return false;
|
|
309
311
|
return policyDominates(parent.policy, child.policy);
|
|
310
312
|
}
|
|
311
313
|
//#endregion
|
|
@@ -318,8 +320,8 @@ var PermissionMemoryProvider = class {
|
|
|
318
320
|
async findOne(options) {
|
|
319
321
|
const key = buildPermissionKey({
|
|
320
322
|
name: options.name,
|
|
321
|
-
|
|
322
|
-
|
|
323
|
+
clientId: options.clientId,
|
|
324
|
+
realmId: options.realmId
|
|
323
325
|
});
|
|
324
326
|
const entry = this.items[key];
|
|
325
327
|
if (entry) return entry;
|
|
@@ -334,6 +336,38 @@ var PermissionMemoryProvider = class {
|
|
|
334
336
|
}
|
|
335
337
|
};
|
|
336
338
|
//#endregion
|
|
339
|
+
//#region src/policy/issue/constants.ts
|
|
340
|
+
let PolicyIssueSeverity = /* @__PURE__ */ function(PolicyIssueSeverity) {
|
|
341
|
+
PolicyIssueSeverity["WARNING"] = "warning";
|
|
342
|
+
PolicyIssueSeverity["ERROR"] = "error";
|
|
343
|
+
return PolicyIssueSeverity;
|
|
344
|
+
}({});
|
|
345
|
+
let PolicyIssueCode = /* @__PURE__ */ function(PolicyIssueCode) {
|
|
346
|
+
PolicyIssueCode["DATA_MISSING"] = "DATA_MISSING";
|
|
347
|
+
PolicyIssueCode["DATA_INVALID"] = "DATA_INVALID";
|
|
348
|
+
PolicyIssueCode["INVALID"] = "INVALID";
|
|
349
|
+
PolicyIssueCode["TYPE_UNKNOWN"] = "TYPE_UNKNOWN";
|
|
350
|
+
PolicyIssueCode["FIELD_MISSING"] = "FIELD_MISSING";
|
|
351
|
+
PolicyIssueCode["FIELD_INVALID"] = "FIELD_INVALID";
|
|
352
|
+
PolicyIssueCode["CHILD_INVALID"] = "CHILD_INVALID";
|
|
353
|
+
PolicyIssueCode["CHILD_EVALUATION_FAILED"] = "CHILD_EVALUATION_FAILED";
|
|
354
|
+
PolicyIssueCode["EVALUATION_DENIED"] = "EVALUATION_DENIED";
|
|
355
|
+
PolicyIssueCode["EVALUATOR_NOT_FOUND"] = "EVALUATOR_NOT_FOUND";
|
|
356
|
+
PolicyIssueCode["EVALUATOR_ERROR"] = "EVALUATOR_ERROR";
|
|
357
|
+
PolicyIssueCode["SKIPPED_EXCLUDED"] = "SKIPPED_EXCLUDED";
|
|
358
|
+
PolicyIssueCode["SKIPPED_NOT_INCLUDED"] = "SKIPPED_NOT_INCLUDED";
|
|
359
|
+
PolicyIssueCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
360
|
+
return PolicyIssueCode;
|
|
361
|
+
}({});
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/policy/issue/define.ts
|
|
364
|
+
function definePolicyIssueItem(input) {
|
|
365
|
+
return defineIssueItem(input);
|
|
366
|
+
}
|
|
367
|
+
function definePolicyIssueGroup(input) {
|
|
368
|
+
return defineIssueGroup(input);
|
|
369
|
+
}
|
|
370
|
+
//#endregion
|
|
337
371
|
//#region src/policy/error/module.ts
|
|
338
372
|
const POLICY_ERROR_INSTANCE = Symbol.for("@authup/access/PolicyError");
|
|
339
373
|
var PolicyError = class PolicyError extends AuthupError {
|
|
@@ -418,6 +452,25 @@ var PolicyEngine = class {
|
|
|
418
452
|
};
|
|
419
453
|
}
|
|
420
454
|
try {
|
|
455
|
+
if (evaluator.requires) {
|
|
456
|
+
const missing = evaluator.requires(policy).filter((key) => !ctx.data || !ctx.data.has(key));
|
|
457
|
+
if (missing.length > 0) {
|
|
458
|
+
const result = {
|
|
459
|
+
success: false,
|
|
460
|
+
pending: true,
|
|
461
|
+
issues: [definePolicyIssueItem({
|
|
462
|
+
code: "DATA_MISSING",
|
|
463
|
+
message: `The data propert${missing.length > 1 ? "ies" : "y"} ${missing.join(", ")} ${missing.length > 1 ? "are" : "is"} missing`,
|
|
464
|
+
path: ctx.path
|
|
465
|
+
})]
|
|
466
|
+
};
|
|
467
|
+
if (ctx.withConditions && evaluator.toCondition) try {
|
|
468
|
+
const condition = await evaluator.toCondition(policy, ctx);
|
|
469
|
+
if (condition) result.condition = condition;
|
|
470
|
+
} catch {}
|
|
471
|
+
return result;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
421
474
|
return await evaluator.evaluate(policy, {
|
|
422
475
|
...ctx,
|
|
423
476
|
evaluators: {
|
|
@@ -494,42 +547,11 @@ function definePolicyEvaluationContext(input = {}) {
|
|
|
494
547
|
path: input.path || [],
|
|
495
548
|
data: input.data || new PolicyData(),
|
|
496
549
|
include: input.include || [],
|
|
497
|
-
exclude: input.exclude || []
|
|
550
|
+
exclude: input.exclude || [],
|
|
551
|
+
withConditions: input.withConditions ?? false
|
|
498
552
|
};
|
|
499
553
|
}
|
|
500
554
|
//#endregion
|
|
501
|
-
//#region src/policy/issue/constants.ts
|
|
502
|
-
let PolicyIssueSeverity = /* @__PURE__ */ function(PolicyIssueSeverity) {
|
|
503
|
-
PolicyIssueSeverity["WARNING"] = "warning";
|
|
504
|
-
PolicyIssueSeverity["ERROR"] = "error";
|
|
505
|
-
return PolicyIssueSeverity;
|
|
506
|
-
}({});
|
|
507
|
-
let PolicyIssueCode = /* @__PURE__ */ function(PolicyIssueCode) {
|
|
508
|
-
PolicyIssueCode["DATA_MISSING"] = "DATA_MISSING";
|
|
509
|
-
PolicyIssueCode["DATA_INVALID"] = "DATA_INVALID";
|
|
510
|
-
PolicyIssueCode["INVALID"] = "INVALID";
|
|
511
|
-
PolicyIssueCode["TYPE_UNKNOWN"] = "TYPE_UNKNOWN";
|
|
512
|
-
PolicyIssueCode["FIELD_MISSING"] = "FIELD_MISSING";
|
|
513
|
-
PolicyIssueCode["FIELD_INVALID"] = "FIELD_INVALID";
|
|
514
|
-
PolicyIssueCode["CHILD_INVALID"] = "CHILD_INVALID";
|
|
515
|
-
PolicyIssueCode["CHILD_EVALUATION_FAILED"] = "CHILD_EVALUATION_FAILED";
|
|
516
|
-
PolicyIssueCode["EVALUATION_DENIED"] = "EVALUATION_DENIED";
|
|
517
|
-
PolicyIssueCode["EVALUATOR_NOT_FOUND"] = "EVALUATOR_NOT_FOUND";
|
|
518
|
-
PolicyIssueCode["EVALUATOR_ERROR"] = "EVALUATOR_ERROR";
|
|
519
|
-
PolicyIssueCode["SKIPPED_EXCLUDED"] = "SKIPPED_EXCLUDED";
|
|
520
|
-
PolicyIssueCode["SKIPPED_NOT_INCLUDED"] = "SKIPPED_NOT_INCLUDED";
|
|
521
|
-
PolicyIssueCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
|
|
522
|
-
return PolicyIssueCode;
|
|
523
|
-
}({});
|
|
524
|
-
//#endregion
|
|
525
|
-
//#region src/policy/issue/define.ts
|
|
526
|
-
function definePolicyIssueItem(input) {
|
|
527
|
-
return defineIssueItem(input);
|
|
528
|
-
}
|
|
529
|
-
function definePolicyIssueGroup(input) {
|
|
530
|
-
return defineIssueGroup(input);
|
|
531
|
-
}
|
|
532
|
-
//#endregion
|
|
533
555
|
//#region src/policy/helpers/invert.ts
|
|
534
556
|
function maybeInvertPolicyOutcome(outcome, invert) {
|
|
535
557
|
if (typeof invert === "undefined") return outcome;
|
|
@@ -594,7 +616,6 @@ const SystemPolicyName = {
|
|
|
594
616
|
PERMISSION_BINDING: "system.permission-binding",
|
|
595
617
|
REALM_MATCH: "system.realm-match",
|
|
596
618
|
CLIENT_NAMES_SELF_MANAGE: "system.client-names-self-manage",
|
|
597
|
-
ROBOT_NAMES_SELF_MANAGE: "system.robot-names-self-manage",
|
|
598
619
|
USER_NAMES_SELF_MANAGE: "system.user-names-self-manage"
|
|
599
620
|
};
|
|
600
621
|
//#endregion
|
|
@@ -610,8 +631,25 @@ var AttributesPolicyValidator = class extends Container {
|
|
|
610
631
|
//#region src/policy/built-in/attributes/evaluator.ts
|
|
611
632
|
var AttributesPolicyEvaluator = class {
|
|
612
633
|
validator;
|
|
634
|
+
parser;
|
|
613
635
|
constructor() {
|
|
614
636
|
this.validator = new AttributesPolicyValidator();
|
|
637
|
+
this.parser = new MongoFiltersParser();
|
|
638
|
+
}
|
|
639
|
+
requires() {
|
|
640
|
+
return ["attributes"];
|
|
641
|
+
}
|
|
642
|
+
async toCondition(value) {
|
|
643
|
+
let policy;
|
|
644
|
+
let condition;
|
|
645
|
+
try {
|
|
646
|
+
policy = await this.validator.run(value);
|
|
647
|
+
this.fixQuery(policy.query);
|
|
648
|
+
condition = this.parser.parse(policy.query);
|
|
649
|
+
} catch {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
return policy.invert ? not(condition) : condition;
|
|
615
653
|
}
|
|
616
654
|
async accessData(ctx) {
|
|
617
655
|
if (!ctx.data.has("attributes")) return null;
|
|
@@ -633,7 +671,20 @@ var AttributesPolicyEvaluator = class {
|
|
|
633
671
|
})]
|
|
634
672
|
};
|
|
635
673
|
this.fixQuery(policy.query);
|
|
636
|
-
|
|
674
|
+
let testIt;
|
|
675
|
+
try {
|
|
676
|
+
testIt = compileFilters(this.parser.parse(policy.query), { caseSensitive: true });
|
|
677
|
+
} catch (e) {
|
|
678
|
+
return {
|
|
679
|
+
success: false,
|
|
680
|
+
issues: [definePolicyIssueItem({
|
|
681
|
+
code: "FIELD_INVALID",
|
|
682
|
+
message: e instanceof Error ? e.message : "The query could not be parsed.",
|
|
683
|
+
path: ctx.path
|
|
684
|
+
})]
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
return { success: maybeInvertPolicyOutcome(testIt(data), policy.invert) };
|
|
637
688
|
}
|
|
638
689
|
fixQuery(query) {
|
|
639
690
|
if (Array.isArray(query)) {
|
|
@@ -665,6 +716,9 @@ var AttributeNamesPolicyEvaluator = class {
|
|
|
665
716
|
this.validator = new AttributeNamesPolicyValidator();
|
|
666
717
|
this.attributesEvaluator = new AttributesPolicyEvaluator();
|
|
667
718
|
}
|
|
719
|
+
requires() {
|
|
720
|
+
return ["attributes"];
|
|
721
|
+
}
|
|
668
722
|
async evaluate(value, ctx) {
|
|
669
723
|
const policy = await this.validator.run(value);
|
|
670
724
|
const data = await this.attributesEvaluator.accessData(ctx);
|
|
@@ -698,7 +752,7 @@ var AttributeNamesPolicyEvaluator = class {
|
|
|
698
752
|
var CompositePolicyValidator = class extends Container {
|
|
699
753
|
initialize() {
|
|
700
754
|
super.initialize();
|
|
701
|
-
this.mount("
|
|
755
|
+
this.mount("decisionStrategy", createValidator(z.enum(DecisionStrategy).or(z.null()).or(z.undefined()).optional()));
|
|
702
756
|
this.mount("children", createValidator(z.array(z.looseObject({ type: z.string().min(3) }))));
|
|
703
757
|
this.mount("invert", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
|
|
704
758
|
}
|
|
@@ -712,18 +766,41 @@ var CompositePolicyEvaluator = class {
|
|
|
712
766
|
}
|
|
713
767
|
async evaluate(value, ctx) {
|
|
714
768
|
const policy = await this.validator.run(value);
|
|
769
|
+
if (policy.children.length === 0) return {
|
|
770
|
+
success: false,
|
|
771
|
+
issues: [definePolicyIssueItem({
|
|
772
|
+
code: "INVALID",
|
|
773
|
+
message: "A composite policy must define at least one child policy.",
|
|
774
|
+
path: ctx.path
|
|
775
|
+
})]
|
|
776
|
+
};
|
|
715
777
|
let count = 0;
|
|
716
|
-
|
|
778
|
+
let pending = 0;
|
|
779
|
+
const decisionStrategy = policy.decisionStrategy ?? DecisionStrategy.UNANIMOUS;
|
|
717
780
|
const engine = new PolicyEngine(ctx.evaluators);
|
|
718
781
|
const issues = [];
|
|
782
|
+
const pendingIssues = [];
|
|
783
|
+
const pendingConditions = [];
|
|
784
|
+
let pendingWithoutCondition = 0;
|
|
719
785
|
for (const childPolicy of policy.children) {
|
|
720
786
|
const path = [...ctx.path || [], ...childPolicy.type ? [childPolicy.type] : []];
|
|
721
787
|
const outcome = await engine.evaluate(childPolicy, {
|
|
722
788
|
...ctx,
|
|
723
789
|
path
|
|
724
790
|
});
|
|
791
|
+
if (outcome.pending) {
|
|
792
|
+
pending++;
|
|
793
|
+
if (outcome.condition) pendingConditions.push(outcome.condition);
|
|
794
|
+
else pendingWithoutCondition++;
|
|
795
|
+
if (outcome.issues && outcome.issues.length > 0) pendingIssues.push(defineIssueGroup({
|
|
796
|
+
message: `The evaluation of child policy ${childPolicy.type} is pending`,
|
|
797
|
+
issues: outcome.issues,
|
|
798
|
+
path
|
|
799
|
+
}));
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
725
802
|
if (outcome.success) {
|
|
726
|
-
if (
|
|
803
|
+
if (decisionStrategy === DecisionStrategy.AFFIRMATIVE) return {
|
|
727
804
|
success: maybeInvertPolicyOutcome(true, policy.invert),
|
|
728
805
|
issues: []
|
|
729
806
|
};
|
|
@@ -734,7 +811,7 @@ var CompositePolicyEvaluator = class {
|
|
|
734
811
|
issues: outcome.issues || [],
|
|
735
812
|
path
|
|
736
813
|
}));
|
|
737
|
-
if (
|
|
814
|
+
if (decisionStrategy === DecisionStrategy.UNANIMOUS) {
|
|
738
815
|
const success = maybeInvertPolicyOutcome(false, policy.invert);
|
|
739
816
|
return {
|
|
740
817
|
success,
|
|
@@ -748,12 +825,41 @@ var CompositePolicyEvaluator = class {
|
|
|
748
825
|
count--;
|
|
749
826
|
}
|
|
750
827
|
}
|
|
751
|
-
|
|
828
|
+
let settled;
|
|
829
|
+
switch (decisionStrategy) {
|
|
830
|
+
case DecisionStrategy.AFFIRMATIVE:
|
|
831
|
+
settled = pending > 0 ? null : false;
|
|
832
|
+
break;
|
|
833
|
+
case DecisionStrategy.UNANIMOUS:
|
|
834
|
+
settled = pending > 0 ? null : count > 0;
|
|
835
|
+
break;
|
|
836
|
+
default: if (count - pending > 0) settled = true;
|
|
837
|
+
else if (count + pending <= 0) settled = false;
|
|
838
|
+
else settled = null;
|
|
839
|
+
}
|
|
840
|
+
if (settled === null) {
|
|
841
|
+
let condition = null;
|
|
842
|
+
if (ctx.withConditions && pendingWithoutCondition === 0 && pendingConditions.length > 0 && decisionStrategy !== DecisionStrategy.CONSENSUS) {
|
|
843
|
+
if (pendingConditions.length === 1) {
|
|
844
|
+
const [first] = pendingConditions;
|
|
845
|
+
condition = first ?? null;
|
|
846
|
+
} else if (decisionStrategy === DecisionStrategy.AFFIRMATIVE) condition = or(...pendingConditions);
|
|
847
|
+
else condition = and(...pendingConditions);
|
|
848
|
+
if (condition && policy.invert) condition = not(condition);
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
success: false,
|
|
852
|
+
pending: true,
|
|
853
|
+
...condition ? { condition } : {},
|
|
854
|
+
issues: pendingIssues
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
if (settled) return { success: maybeInvertPolicyOutcome(true, policy.invert) };
|
|
752
858
|
const success = maybeInvertPolicyOutcome(false, policy.invert);
|
|
753
859
|
return {
|
|
754
860
|
success,
|
|
755
861
|
issues: success ? [] : [defineIssueGroup({
|
|
756
|
-
message: `The evaluation of composite policy failed (${
|
|
862
|
+
message: `The evaluation of composite policy failed (${decisionStrategy})`,
|
|
757
863
|
issues: issues || [],
|
|
758
864
|
path: ctx.path
|
|
759
865
|
})]
|
|
@@ -840,6 +946,9 @@ var IdentityPolicyEvaluator = class {
|
|
|
840
946
|
this.validator = new IdentityPolicyValidator();
|
|
841
947
|
this.dataValidator = new PolicyIdentityDataValidator();
|
|
842
948
|
}
|
|
949
|
+
requires() {
|
|
950
|
+
return ["identity"];
|
|
951
|
+
}
|
|
843
952
|
async accessData(ctx) {
|
|
844
953
|
if (!ctx.data.has("identity")) return null;
|
|
845
954
|
if (ctx.data.isValidated("identity")) return ctx.data.get("identity");
|
|
@@ -884,6 +993,9 @@ var PermissionBindingPolicyEvaluator = class {
|
|
|
884
993
|
constructor() {
|
|
885
994
|
this.validator = new PermissionBindingPolicyValidator();
|
|
886
995
|
}
|
|
996
|
+
requires() {
|
|
997
|
+
return ["permissionBinding"];
|
|
998
|
+
}
|
|
887
999
|
async evaluate(value) {
|
|
888
1000
|
return { success: maybeInvertPolicyOutcome(true, (await this.validator.run(value)).invert) };
|
|
889
1001
|
}
|
|
@@ -893,10 +1005,10 @@ var PermissionBindingPolicyEvaluator = class {
|
|
|
893
1005
|
var RealmMatchPolicyValidator = class extends Container {
|
|
894
1006
|
initialize() {
|
|
895
1007
|
super.initialize();
|
|
896
|
-
this.mount("
|
|
897
|
-
this.mount("
|
|
898
|
-
this.mount("
|
|
899
|
-
this.mount("
|
|
1008
|
+
this.mount("decisionStrategy", createValidator(z.enum(DecisionStrategy).or(z.null()).or(z.undefined()).optional()));
|
|
1009
|
+
this.mount("attributeName", createValidator(z.string().min(3).or(z.array(z.string().min(3))).or(z.null()).or(z.undefined()).optional()));
|
|
1010
|
+
this.mount("attributeNameStrict", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
|
|
1011
|
+
this.mount("attributeNullMatchAll", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
|
|
900
1012
|
this.mount("scope", createValidator(z.enum(RealmScope).or(z.null()).or(z.undefined()).optional()));
|
|
901
1013
|
this.mount("invert", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
|
|
902
1014
|
}
|
|
@@ -912,6 +1024,56 @@ var RealmMatchPolicyEvaluator = class {
|
|
|
912
1024
|
this.identityEvaluator = new IdentityPolicyEvaluator();
|
|
913
1025
|
this.attributesEvaluator = new AttributesPolicyEvaluator();
|
|
914
1026
|
}
|
|
1027
|
+
requires(value) {
|
|
1028
|
+
if (value.scope) return ["identity"];
|
|
1029
|
+
return ["identity", "attributes"];
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Condition form over the row's realm column, with the actor realm baked in
|
|
1033
|
+
* (mirrors `realmScopeMatches` / the attribute-mode match). Exactness holds for
|
|
1034
|
+
* row-shaped data where the referenced field exists (DB rows) — the object-bag
|
|
1035
|
+
* "key absent → neutral-pass" case has no SQL counterpart. Lowerable configs:
|
|
1036
|
+
* scope mode (field = single-string `attributeName`, default `realmId` — scope-mode
|
|
1037
|
+
* EVALUATION ignores `attributeName`, lowering reuses it as the column override),
|
|
1038
|
+
* and strict attribute mode with a single explicit `attributeName`. Everything
|
|
1039
|
+
* else (multi-key / default-key CONSENSUS, non-strict key expansion) is
|
|
1040
|
+
* row-shape-dependent and stays a post-check.
|
|
1041
|
+
*/
|
|
1042
|
+
async toCondition(value, ctx) {
|
|
1043
|
+
let policy;
|
|
1044
|
+
try {
|
|
1045
|
+
policy = await this.validator.run(value);
|
|
1046
|
+
} catch {
|
|
1047
|
+
return null;
|
|
1048
|
+
}
|
|
1049
|
+
const identity = await this.identityEvaluator.accessData(ctx);
|
|
1050
|
+
if (!identity) return null;
|
|
1051
|
+
const realmId = identity.realmId ?? null;
|
|
1052
|
+
if (policy.scope) {
|
|
1053
|
+
const scope = normalizeRealmScope(policy.scope);
|
|
1054
|
+
const field = typeof policy.attributeName === "string" ? policy.attributeName : "realmId";
|
|
1055
|
+
let condition;
|
|
1056
|
+
if (scope === "any") condition = nin(field, []);
|
|
1057
|
+
else {
|
|
1058
|
+
const terms = [];
|
|
1059
|
+
if (scope !== "none" && realmId !== null) terms.push(eq(field, realmId));
|
|
1060
|
+
if (scope === "ownOrNull") terms.push(eq(field, null));
|
|
1061
|
+
if (terms.length === 0) condition = inArray(field, []);
|
|
1062
|
+
else if (terms.length === 1) condition = terms[0];
|
|
1063
|
+
else condition = or(...terms);
|
|
1064
|
+
}
|
|
1065
|
+
return policy.invert ? not(condition) : condition;
|
|
1066
|
+
}
|
|
1067
|
+
if (typeof policy.attributeName !== "string" || (policy.attributeNameStrict ?? true) === false) return null;
|
|
1068
|
+
const key = policy.attributeName;
|
|
1069
|
+
const terms = [];
|
|
1070
|
+
if (policy.attributeNullMatchAll) terms.push(eq(key, null));
|
|
1071
|
+
if (identity.realmId !== void 0) terms.push(eq(key, identity.realmId));
|
|
1072
|
+
if (identity.realmName !== void 0) terms.push(eq(key, identity.realmName));
|
|
1073
|
+
if (terms.length === 0) return policy.invert ? nin(key, []) : inArray(key, []);
|
|
1074
|
+
const condition = terms.length === 1 ? terms[0] : or(...terms);
|
|
1075
|
+
return policy.invert ? not(condition) : condition;
|
|
1076
|
+
}
|
|
915
1077
|
async evaluate(value, ctx) {
|
|
916
1078
|
const policy = await this.validator.run(value);
|
|
917
1079
|
const identity = await this.identityEvaluator.accessData(ctx);
|
|
@@ -924,7 +1086,17 @@ var RealmMatchPolicyEvaluator = class {
|
|
|
924
1086
|
})]
|
|
925
1087
|
};
|
|
926
1088
|
if (policy.scope) {
|
|
927
|
-
if (!ctx.data || !ctx.data.has("realmMatch"))
|
|
1089
|
+
if (!ctx.data || !ctx.data.has("realmMatch")) {
|
|
1090
|
+
if (ctx.withConditions) {
|
|
1091
|
+
const condition = await this.toCondition(value, ctx);
|
|
1092
|
+
return {
|
|
1093
|
+
success: false,
|
|
1094
|
+
pending: true,
|
|
1095
|
+
...condition ? { condition } : {}
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
return { success: true };
|
|
1099
|
+
}
|
|
928
1100
|
const resourceRealm = ctx.data.get("realmMatch");
|
|
929
1101
|
return { success: maybeInvertPolicyOutcome(realmScopeMatches(policy.scope, resourceRealm ?? null, identity.realmId, identity.realmName), policy.invert) };
|
|
930
1102
|
}
|
|
@@ -938,8 +1110,8 @@ var RealmMatchPolicyEvaluator = class {
|
|
|
938
1110
|
})]
|
|
939
1111
|
};
|
|
940
1112
|
let keys;
|
|
941
|
-
if (policy.
|
|
942
|
-
else keys = [policy.
|
|
1113
|
+
if (policy.attributeName) if (Array.isArray(policy.attributeName)) keys = policy.attributeName;
|
|
1114
|
+
else keys = [policy.attributeName];
|
|
943
1115
|
else {
|
|
944
1116
|
keys = [
|
|
945
1117
|
"realm_id",
|
|
@@ -947,9 +1119,9 @@ var RealmMatchPolicyEvaluator = class {
|
|
|
947
1119
|
"realmId",
|
|
948
1120
|
"realmName"
|
|
949
1121
|
];
|
|
950
|
-
policy.
|
|
1122
|
+
policy.decisionStrategy = DecisionStrategy.CONSENSUS;
|
|
951
1123
|
}
|
|
952
|
-
if (!(policy.
|
|
1124
|
+
if (!(policy.attributeNameStrict ?? true)) {
|
|
953
1125
|
const resourceKeys = Object.keys(attributes);
|
|
954
1126
|
const keysToAdd = [];
|
|
955
1127
|
for (const resourceKey of resourceKeys) {
|
|
@@ -969,13 +1141,13 @@ var RealmMatchPolicyEvaluator = class {
|
|
|
969
1141
|
evaluated = true;
|
|
970
1142
|
let outcome = false;
|
|
971
1143
|
const attributeValue = attributes[key];
|
|
972
|
-
if (attributeValue === null && policy.
|
|
1144
|
+
if (attributeValue === null && policy.attributeNullMatchAll) outcome = true;
|
|
973
1145
|
else if (attributeValue === identity.realmId || attributeValue === identity.realmName) outcome = true;
|
|
974
1146
|
if (outcome) {
|
|
975
|
-
if (policy.
|
|
1147
|
+
if (policy.decisionStrategy === DecisionStrategy.AFFIRMATIVE) return { success: maybeInvertPolicyOutcome(true, policy.invert) };
|
|
976
1148
|
count++;
|
|
977
1149
|
} else {
|
|
978
|
-
if (policy.
|
|
1150
|
+
if (policy.decisionStrategy === DecisionStrategy.UNANIMOUS) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
|
|
979
1151
|
count--;
|
|
980
1152
|
}
|
|
981
1153
|
}
|
|
@@ -1008,9 +1180,9 @@ function isIntervalForDayOfYear(interval) {
|
|
|
1008
1180
|
var TimePolicyValidator = class extends Container {
|
|
1009
1181
|
initialize() {
|
|
1010
1182
|
super.initialize();
|
|
1011
|
-
this.mount("
|
|
1012
|
-
this.mount("
|
|
1013
|
-
this.mount("
|
|
1183
|
+
this.mount("dayOfWeek", createValidator(z.number().min(0).max(6).or(z.null()).or(z.undefined()).optional()));
|
|
1184
|
+
this.mount("dayOfMonth", createValidator(z.number().min(1).max(31).or(z.null()).or(z.undefined()).optional()));
|
|
1185
|
+
this.mount("dayOfYear", createValidator(z.number().min(1).max(365).or(z.null()).or(z.undefined()).optional()));
|
|
1014
1186
|
this.mount("start", createValidator(z.date().or(z.iso.datetime()).or(z.iso.time()).or(z.number()).or(z.null()).or(z.undefined()).optional()));
|
|
1015
1187
|
this.mount("end", createValidator(z.date().or(z.iso.datetime()).or(z.iso.time()).or(z.number()).or(z.null()).or(z.undefined()).optional()));
|
|
1016
1188
|
this.mount("interval", createValidator(z.enum(TimePolicyInterval).or(z.null()).or(z.undefined()).optional()));
|
|
@@ -1067,16 +1239,16 @@ var TimePolicyEvaluator = class {
|
|
|
1067
1239
|
if (now > end) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
|
|
1068
1240
|
}
|
|
1069
1241
|
if (policy.interval) {
|
|
1070
|
-
if (isIntervalForDayOfWeek(policy.interval) && policy.
|
|
1071
|
-
if (now.getDay() !== policy.
|
|
1242
|
+
if (isIntervalForDayOfWeek(policy.interval) && policy.dayOfWeek) {
|
|
1243
|
+
if (now.getDay() !== policy.dayOfWeek) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
|
|
1072
1244
|
}
|
|
1073
|
-
if (isIntervalForDayOfMonth(policy.interval) && policy.
|
|
1074
|
-
if (now.getDate() !== policy.
|
|
1245
|
+
if (isIntervalForDayOfMonth(policy.interval) && policy.dayOfMonth) {
|
|
1246
|
+
if (now.getDate() !== policy.dayOfMonth) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
|
|
1075
1247
|
}
|
|
1076
|
-
if (isIntervalForDayOfYear(policy.interval) && policy.
|
|
1248
|
+
if (isIntervalForDayOfYear(policy.interval) && policy.dayOfYear) {
|
|
1077
1249
|
const start = new Date(now.getFullYear(), 0, 0);
|
|
1078
1250
|
const diff = now.getTime() - start.getTime() + (start.getTimezoneOffset() - now.getTimezoneOffset()) * 60 * 1e3;
|
|
1079
|
-
if (Math.floor(diff / (1e3 * 60 * 60 * 24)) !== policy.
|
|
1251
|
+
if (Math.floor(diff / (1e3 * 60 * 60 * 24)) !== policy.dayOfYear) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
|
|
1080
1252
|
}
|
|
1081
1253
|
}
|
|
1082
1254
|
return { success: maybeInvertPolicyOutcome(true, policy.invert) };
|
|
@@ -1176,7 +1348,7 @@ var PermissionEvaluator = class {
|
|
|
1176
1348
|
data.set("permissionBinding", binding);
|
|
1177
1349
|
const compositePolicy = {
|
|
1178
1350
|
type: "composite",
|
|
1179
|
-
|
|
1351
|
+
decisionStrategy: DecisionStrategy.AFFIRMATIVE,
|
|
1180
1352
|
children: grantPolicies
|
|
1181
1353
|
};
|
|
1182
1354
|
const evaluationResult = await this.policyEngine.evaluate(compositePolicy, definePolicyEvaluationContext({
|
|
@@ -1184,7 +1356,7 @@ var PermissionEvaluator = class {
|
|
|
1184
1356
|
exclude: options.policiesExcluded,
|
|
1185
1357
|
data
|
|
1186
1358
|
}));
|
|
1187
|
-
if (evaluationResult.success) {
|
|
1359
|
+
if (evaluationResult.success || options.pendingPolicies === "permit" && !!evaluationResult.pending) {
|
|
1188
1360
|
if (decisionStrategy === DecisionStrategy.AFFIRMATIVE) return;
|
|
1189
1361
|
count++;
|
|
1190
1362
|
} else {
|
|
@@ -1224,11 +1396,7 @@ var PermissionEvaluator = class {
|
|
|
1224
1396
|
...ctx,
|
|
1225
1397
|
options: {
|
|
1226
1398
|
...ctx.options || {},
|
|
1227
|
-
|
|
1228
|
-
"attributes",
|
|
1229
|
-
"attributeNames",
|
|
1230
|
-
"realmMatch"
|
|
1231
|
-
]
|
|
1399
|
+
pendingPolicies: "permit"
|
|
1232
1400
|
}
|
|
1233
1401
|
});
|
|
1234
1402
|
}
|
|
@@ -1241,6 +1409,58 @@ var PermissionEvaluator = class {
|
|
|
1241
1409
|
}
|
|
1242
1410
|
});
|
|
1243
1411
|
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Compile the permission's policy trees against the knowns bag into a
|
|
1414
|
+
* {@link PermissionCompileResult} — the query-build counterpart of
|
|
1415
|
+
* {@link evaluate} (issue #3286 phase 3). Runs the SAME evaluation walk with
|
|
1416
|
+
* `withConditions` enabled: policies that settle with the knowns settle here,
|
|
1417
|
+
* pending subtrees contribute their exact condition form. Multiple names are
|
|
1418
|
+
* a disjunction (any-of), so a single non-expressible name degrades the whole
|
|
1419
|
+
* compilation to `post` — pushing only some disjuncts would wrongly exclude
|
|
1420
|
+
* rows another name would admit.
|
|
1421
|
+
*/
|
|
1422
|
+
async compile(ctx) {
|
|
1423
|
+
const names = Array.isArray(ctx.name) ? ctx.name : [ctx.name];
|
|
1424
|
+
const dataBase = ctx.data || new PolicyData();
|
|
1425
|
+
const conditions = [];
|
|
1426
|
+
let post = false;
|
|
1427
|
+
for (const name of names) {
|
|
1428
|
+
const binding = await this.findOne(name, {
|
|
1429
|
+
realmId: ctx.realmId,
|
|
1430
|
+
clientId: ctx.clientId
|
|
1431
|
+
});
|
|
1432
|
+
if (!binding) continue;
|
|
1433
|
+
const grantPolicies = binding.grants.map((grant) => grant.policy).filter((grantPolicy) => !!grantPolicy);
|
|
1434
|
+
if (grantPolicies.length < binding.grants.length) return { verdict: "allow" };
|
|
1435
|
+
const data = dataBase.clone();
|
|
1436
|
+
data.set("permissionBinding", binding);
|
|
1437
|
+
const compositePolicy = {
|
|
1438
|
+
type: "composite",
|
|
1439
|
+
decisionStrategy: DecisionStrategy.AFFIRMATIVE,
|
|
1440
|
+
children: grantPolicies
|
|
1441
|
+
};
|
|
1442
|
+
const evaluationResult = await this.policyEngine.evaluate(compositePolicy, definePolicyEvaluationContext({
|
|
1443
|
+
data,
|
|
1444
|
+
withConditions: true
|
|
1445
|
+
}));
|
|
1446
|
+
if (evaluationResult.success) return { verdict: "allow" };
|
|
1447
|
+
if (evaluationResult.pending) if (evaluationResult.condition) conditions.push(evaluationResult.condition);
|
|
1448
|
+
else post = true;
|
|
1449
|
+
}
|
|
1450
|
+
if (post) return { verdict: "post" };
|
|
1451
|
+
if (conditions.length === 0) return { verdict: "deny" };
|
|
1452
|
+
if (conditions.length === 1) {
|
|
1453
|
+
const [condition] = conditions;
|
|
1454
|
+
return {
|
|
1455
|
+
verdict: "conditional",
|
|
1456
|
+
condition
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
return {
|
|
1460
|
+
verdict: "conditional",
|
|
1461
|
+
condition: or(...conditions)
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1244
1464
|
};
|
|
1245
1465
|
//#endregion
|
|
1246
1466
|
export { AttributeNamesPolicyEvaluator, AttributeNamesPolicyValidator, AttributesPolicyEvaluator, AttributesPolicyValidator, BuiltInPolicyType, CompositePolicyEvaluator, CompositePolicyValidator, DatePolicyEvaluator, DatePolicyValidator, IdentityPolicyEvaluator, IdentityPolicyValidator, PERMISSION_ERROR_INSTANCE, POLICY_ERROR_INSTANCE, PermissionBindingPolicyEvaluator, PermissionBindingPolicyValidator, PermissionError, PermissionEvaluator, PermissionMemoryProvider, PolicyData, PolicyDefaultEvaluators, PolicyEngine, PolicyError, PolicyIssueCode, PolicyIssueSeverity, REALM_SCOPE_ORDER, RealmMatchPolicyEvaluator, RealmMatchPolicyValidator, RealmScope, SystemPolicyName, TimePolicyEvaluator, TimePolicyInterval, TimePolicyValidator, aggregatePermissionPolicyBindings, buildPermissionKey, compareRealmScope, defineAttributesPolicy, definePolicyData, definePolicyEvaluationContext, definePolicyIssueGroup, definePolicyIssueItem, definePolicyWithType, grantDominates, isCompositePolicy, isIntervalForDayOfMonth, isIntervalForDayOfWeek, isIntervalForDayOfYear, isPermissionError, isPermissionPolicyBindingEqual, isPolicyError, maxRealmScope, maybeInvertPolicyOutcome, minRealmScope, normalizeRealmScope, realmScopeMatches };
|