@authup/access 1.0.0-beta.65 → 1.0.0-beta.67
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 +557 -65
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +838 -30
- package/dist/index.mjs.map +1 -1
- package/dist/permission/authorization/check-schema.d.ts +23 -0
- package/dist/permission/authorization/check-schema.d.ts.map +1 -0
- package/dist/permission/authorization/check.d.ts +7 -0
- package/dist/permission/authorization/check.d.ts.map +1 -0
- package/dist/permission/authorization/constants.d.ts +14 -0
- package/dist/permission/authorization/constants.d.ts.map +1 -0
- package/dist/permission/authorization/error.d.ts +18 -0
- package/dist/permission/authorization/error.d.ts.map +1 -0
- package/dist/permission/authorization/index.d.ts +9 -0
- package/dist/permission/authorization/index.d.ts.map +1 -0
- package/dist/permission/authorization/module.d.ts +77 -0
- package/dist/permission/authorization/module.d.ts.map +1 -0
- package/dist/permission/authorization/policy.d.ts +31 -0
- package/dist/permission/authorization/policy.d.ts.map +1 -0
- package/dist/permission/authorization/schema.d.ts +107 -0
- package/dist/permission/authorization/schema.d.ts.map +1 -0
- package/dist/permission/authorization/types.d.ts +136 -0
- package/dist/permission/authorization/types.d.ts.map +1 -0
- package/dist/permission/evaluator/module.d.ts.map +1 -1
- package/dist/permission/evaluator/types.d.ts +16 -1
- package/dist/permission/evaluator/types.d.ts.map +1 -1
- package/dist/permission/index.d.ts +1 -0
- package/dist/permission/index.d.ts.map +1 -1
- package/dist/permission/realm-scope/helpers.d.ts +1 -1
- package/dist/permission/realm-scope/helpers.d.ts.map +1 -1
- package/dist/policy/built-in/date/evaluator.d.ts.map +1 -1
- package/dist/policy/built-in/permission-binding/identity-evaluator.d.ts +21 -0
- package/dist/policy/built-in/permission-binding/identity-evaluator.d.ts.map +1 -0
- package/dist/policy/built-in/permission-binding/index.d.ts +1 -0
- package/dist/policy/built-in/permission-binding/index.d.ts.map +1 -1
- package/dist/policy/built-in/permission-binding/validator.d.ts.map +1 -1
- package/dist/policy/built-in/time/evaluator.d.ts.map +1 -1
- package/dist/policy/constants.d.ts +11 -0
- package/dist/policy/constants.d.ts.map +1 -1
- package/dist/policy/data.d.ts +2 -0
- package/dist/policy/data.d.ts.map +1 -1
- package/dist/policy/evaluation/context/define.d.ts.map +1 -1
- package/dist/policy/evaluation/context/index.d.ts +1 -0
- package/dist/policy/evaluation/context/index.d.ts.map +1 -1
- package/dist/policy/evaluation/context/transition.d.ts +13 -0
- package/dist/policy/evaluation/context/transition.d.ts.map +1 -0
- package/dist/policy/evaluation/context/types.d.ts +27 -0
- package/dist/policy/evaluation/context/types.d.ts.map +1 -1
- package/dist/policy/types.d.ts +5 -0
- package/dist/policy/types.d.ts.map +1 -1
- package/package.json +8 -8
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { AuthupError, ErrorCode, isAuthupError, markInstanceof, matchesInstanceof } from "@authup/errors";
|
|
1
|
+
import { AuthupError, ErrorCode, InternalError, isAuthupError, markInstanceof, matchesInstanceof } from "@authup/errors";
|
|
2
2
|
import { DecisionStrategy, flattenObject, hasOwnProperty, isObject } from "@authup/kit";
|
|
3
3
|
import { isEqual } from "smob";
|
|
4
4
|
import { and, eq, inArray, nin, not, or } from "@rapiq/core";
|
|
5
|
-
import { defineIssueGroup, defineIssueItem } from "@ebec/core";
|
|
6
|
-
import { createValidator } from "@validup/zod";
|
|
7
|
-
import { Container } from "validup";
|
|
5
|
+
import { defineIssueGroup, defineIssueItem, prefixIssuePath } from "@ebec/core";
|
|
6
|
+
import { buildIssuesForZodError, createValidator } from "@validup/zod";
|
|
7
|
+
import { Container, ValidupError, isValidupError } from "validup";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { compileFilters } from "@rapiq/adapter-memory";
|
|
10
10
|
import { MongoFiltersParser } from "@rapiq/parser-mongo";
|
|
@@ -515,6 +515,10 @@ var PolicyData = class PolicyData {
|
|
|
515
515
|
has(key) {
|
|
516
516
|
return key in this.data;
|
|
517
517
|
}
|
|
518
|
+
delete(key) {
|
|
519
|
+
delete this.data[key];
|
|
520
|
+
this.validated.delete(key);
|
|
521
|
+
}
|
|
518
522
|
setValidated(key) {
|
|
519
523
|
this.validated.add(key);
|
|
520
524
|
}
|
|
@@ -534,7 +538,26 @@ function definePolicyEvaluationContext(input = {}) {
|
|
|
534
538
|
data: input.data || new PolicyData(),
|
|
535
539
|
include: input.include || [],
|
|
536
540
|
exclude: input.exclude || [],
|
|
537
|
-
withConditions: input.withConditions ?? false
|
|
541
|
+
withConditions: input.withConditions ?? false,
|
|
542
|
+
realmAttributeName: input.realmAttributeName,
|
|
543
|
+
transitions: input.transitions
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/policy/evaluation/context/transition.ts
|
|
548
|
+
/**
|
|
549
|
+
* A sink keeping the earliest reported instant, which is the moment an answer
|
|
550
|
+
* built from every evaluation it was handed to stops being current.
|
|
551
|
+
*/
|
|
552
|
+
function createPolicyTransitionCollector() {
|
|
553
|
+
let next;
|
|
554
|
+
return {
|
|
555
|
+
report(at) {
|
|
556
|
+
if (!next || at.getTime() < next.getTime()) next = at;
|
|
557
|
+
},
|
|
558
|
+
get next() {
|
|
559
|
+
return next;
|
|
560
|
+
}
|
|
538
561
|
};
|
|
539
562
|
}
|
|
540
563
|
//#endregion
|
|
@@ -757,7 +780,7 @@ var CompositePolicyValidator = class extends Container {
|
|
|
757
780
|
initialize() {
|
|
758
781
|
super.initialize();
|
|
759
782
|
this.mount("decisionStrategy", createValidator(z.enum(DecisionStrategy).or(z.null()).or(z.undefined()).optional()));
|
|
760
|
-
this.mount("children", createValidator(z.array(z.looseObject({ type: z.string().min(
|
|
783
|
+
this.mount("children", createValidator(z.array(z.looseObject({ type: z.string().min(1) }))));
|
|
761
784
|
this.mount("invert", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
|
|
762
785
|
}
|
|
763
786
|
};
|
|
@@ -895,6 +918,22 @@ function toDate$1(input) {
|
|
|
895
918
|
if (typeof input === "number") return new Date(input);
|
|
896
919
|
return input;
|
|
897
920
|
}
|
|
921
|
+
/**
|
|
922
|
+
* The next instant at which a date policy's verdict could change, seen from the
|
|
923
|
+
* (day-normalized) `now`: the start day while it is still ahead, else the
|
|
924
|
+
* midnight after the end day while that day has not passed. Nothing once the
|
|
925
|
+
* end day is behind, since the verdict is final from then on.
|
|
926
|
+
*/
|
|
927
|
+
function nextDateTransition(policy, now) {
|
|
928
|
+
if (policy.start) {
|
|
929
|
+
const start = normalizeDate$1(toDate$1(policy.start));
|
|
930
|
+
if (now < start) return start;
|
|
931
|
+
}
|
|
932
|
+
if (policy.end) {
|
|
933
|
+
const end = normalizeDate$1(toDate$1(policy.end));
|
|
934
|
+
if (now <= end) return new Date(end.getFullYear(), end.getMonth(), end.getDate() + 1);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
898
937
|
var DatePolicyEvaluator = class {
|
|
899
938
|
validator;
|
|
900
939
|
constructor() {
|
|
@@ -910,7 +949,13 @@ var DatePolicyEvaluator = class {
|
|
|
910
949
|
ctx.data.set("date", now);
|
|
911
950
|
ctx.data.setValidated("date");
|
|
912
951
|
}
|
|
913
|
-
} else
|
|
952
|
+
} else {
|
|
953
|
+
now = normalizeDate$1(/* @__PURE__ */ new Date());
|
|
954
|
+
if (ctx.transitions) {
|
|
955
|
+
const next = nextDateTransition(policy, now);
|
|
956
|
+
if (next) ctx.transitions.report(next);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
914
959
|
if (policy.start) {
|
|
915
960
|
const start = normalizeDate$1(toDate$1(policy.start));
|
|
916
961
|
if (now < start) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
|
|
@@ -987,25 +1032,6 @@ var IdentityPolicyEvaluator = class {
|
|
|
987
1032
|
}
|
|
988
1033
|
};
|
|
989
1034
|
//#endregion
|
|
990
|
-
//#region src/policy/built-in/permission-binding/validator.ts
|
|
991
|
-
var PermissionBindingPolicyValidator = class extends Container {
|
|
992
|
-
initialize() {}
|
|
993
|
-
};
|
|
994
|
-
//#endregion
|
|
995
|
-
//#region src/policy/built-in/permission-binding/module.ts
|
|
996
|
-
var PermissionBindingPolicyEvaluator = class {
|
|
997
|
-
validator;
|
|
998
|
-
constructor() {
|
|
999
|
-
this.validator = new PermissionBindingPolicyValidator();
|
|
1000
|
-
}
|
|
1001
|
-
requires() {
|
|
1002
|
-
return ["permissionBinding"];
|
|
1003
|
-
}
|
|
1004
|
-
async evaluate(value) {
|
|
1005
|
-
return { success: maybeInvertPolicyOutcome(true, (await this.validator.run(value)).invert) };
|
|
1006
|
-
}
|
|
1007
|
-
};
|
|
1008
|
-
//#endregion
|
|
1009
1035
|
//#region src/policy/built-in/realm-match/validator.ts
|
|
1010
1036
|
var RealmMatchPolicyValidator = class extends Container {
|
|
1011
1037
|
initialize() {
|
|
@@ -1062,7 +1088,7 @@ var RealmMatchPolicyEvaluator = class {
|
|
|
1062
1088
|
else {
|
|
1063
1089
|
const terms = [];
|
|
1064
1090
|
if (realmId !== null && scope !== "none") terms.push(eq(field, realmId));
|
|
1065
|
-
if (scope === "ownOrNull") terms.push(eq(field, null));
|
|
1091
|
+
if (scope === "ownOrNull" && (realmId !== null || identity.realmName != null)) terms.push(eq(field, null));
|
|
1066
1092
|
if (terms.length === 0) condition = inArray(field, []);
|
|
1067
1093
|
else if (terms.length === 1) condition = terms[0];
|
|
1068
1094
|
else condition = or(...terms);
|
|
@@ -1161,6 +1187,159 @@ var RealmMatchPolicyEvaluator = class {
|
|
|
1161
1187
|
}
|
|
1162
1188
|
};
|
|
1163
1189
|
//#endregion
|
|
1190
|
+
//#region src/policy/built-in/permission-binding/validator.ts
|
|
1191
|
+
var PermissionBindingPolicyValidator = class extends Container {
|
|
1192
|
+
initialize() {
|
|
1193
|
+
this.mount("invert", createValidator(z.boolean().or(z.null()).or(z.undefined()).optional()));
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
//#endregion
|
|
1197
|
+
//#region src/policy/built-in/permission-binding/identity-evaluator.ts
|
|
1198
|
+
function containsPermissionBinding(policy) {
|
|
1199
|
+
if (policy.type === "permissionBinding") return true;
|
|
1200
|
+
if (policy.type !== "composite") return false;
|
|
1201
|
+
const children = policy.children;
|
|
1202
|
+
return Array.isArray(children) && children.some((child) => !!child && typeof child === "object" && containsPermissionBinding(child));
|
|
1203
|
+
}
|
|
1204
|
+
var IdentityPermissionBindingPolicyEvaluator = class {
|
|
1205
|
+
validator;
|
|
1206
|
+
identityEvaluator;
|
|
1207
|
+
realmMatchEvaluator;
|
|
1208
|
+
identityPermissionProvider;
|
|
1209
|
+
constructor(identityPermissionProvider) {
|
|
1210
|
+
this.validator = new PermissionBindingPolicyValidator();
|
|
1211
|
+
this.identityEvaluator = new IdentityPolicyEvaluator();
|
|
1212
|
+
this.realmMatchEvaluator = new RealmMatchPolicyEvaluator();
|
|
1213
|
+
this.identityPermissionProvider = identityPermissionProvider;
|
|
1214
|
+
}
|
|
1215
|
+
requires() {
|
|
1216
|
+
return ["permissionBinding"];
|
|
1217
|
+
}
|
|
1218
|
+
async accessData(ctx) {
|
|
1219
|
+
if (!ctx.data.has("permissionBinding")) return null;
|
|
1220
|
+
if (ctx.data.isValidated("permissionBinding")) return ctx.data.get("permissionBinding");
|
|
1221
|
+
const data = ctx.data.get("permissionBinding");
|
|
1222
|
+
ctx.data.set("permissionBinding", data);
|
|
1223
|
+
ctx.data.setValidated("permissionBinding");
|
|
1224
|
+
return data;
|
|
1225
|
+
}
|
|
1226
|
+
async evaluate(value, ctx) {
|
|
1227
|
+
const policy = await this.validator.run(value);
|
|
1228
|
+
const identity = await this.identityEvaluator.accessData(ctx);
|
|
1229
|
+
if (!identity) return {
|
|
1230
|
+
success: false,
|
|
1231
|
+
issues: [definePolicyIssueItem({
|
|
1232
|
+
code: "DATA_MISSING",
|
|
1233
|
+
message: "The data property identity is missing",
|
|
1234
|
+
path: ctx.path
|
|
1235
|
+
})]
|
|
1236
|
+
};
|
|
1237
|
+
const binding = await this.accessData(ctx);
|
|
1238
|
+
if (!binding) return {
|
|
1239
|
+
success: false,
|
|
1240
|
+
issues: [definePolicyIssueItem({
|
|
1241
|
+
code: "DATA_MISSING",
|
|
1242
|
+
message: "The data property permission is missing",
|
|
1243
|
+
path: ctx.path
|
|
1244
|
+
})]
|
|
1245
|
+
};
|
|
1246
|
+
const identityBindings = await this.identityPermissionProvider.getFor(identity).then((bindings) => bindings.filter((item) => {
|
|
1247
|
+
if (item.permission.name !== binding.permission.name) return false;
|
|
1248
|
+
return (binding.permission.realmId ?? null) === (item.permission.realmId ?? null) && (binding.permission.clientId ?? null) === (item.permission.clientId ?? null);
|
|
1249
|
+
}));
|
|
1250
|
+
if (identityBindings.length === 0) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
|
|
1251
|
+
const [aggr] = aggregatePermissionPolicyBindings(identityBindings);
|
|
1252
|
+
if (!aggr) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
|
|
1253
|
+
const issues = [];
|
|
1254
|
+
const pendingIssues = [];
|
|
1255
|
+
const conditions = [];
|
|
1256
|
+
let pending = false;
|
|
1257
|
+
let lowerable = true;
|
|
1258
|
+
for (const grant of aggr.grants) {
|
|
1259
|
+
let reachCondition = null;
|
|
1260
|
+
const realmOutcome = await this.realmMatchEvaluator.evaluate({
|
|
1261
|
+
scope: grant.realmScope,
|
|
1262
|
+
attributeName: ctx.realmAttributeName
|
|
1263
|
+
}, ctx);
|
|
1264
|
+
if (realmOutcome.pending) {
|
|
1265
|
+
const scope = normalizeRealmScope(grant.realmScope);
|
|
1266
|
+
if (scope === "none") continue;
|
|
1267
|
+
if (scope !== "any") {
|
|
1268
|
+
if (!realmOutcome.condition) {
|
|
1269
|
+
pending = true;
|
|
1270
|
+
lowerable = false;
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
reachCondition = realmOutcome.condition;
|
|
1274
|
+
}
|
|
1275
|
+
} else if (!realmOutcome.success) continue;
|
|
1276
|
+
if (!grant.policy) {
|
|
1277
|
+
if (!reachCondition) return { success: maybeInvertPolicyOutcome(true, policy.invert) };
|
|
1278
|
+
conditions.push(reachCondition);
|
|
1279
|
+
pending = true;
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
if (containsPermissionBinding(grant.policy)) {
|
|
1283
|
+
issues.push(definePolicyIssueItem({
|
|
1284
|
+
code: "INVALID",
|
|
1285
|
+
message: "A grant policy must not evaluate the permission binding.",
|
|
1286
|
+
path: ctx.path
|
|
1287
|
+
}));
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
const outcome = await new PolicyEngine(ctx.evaluators).evaluate(grant.policy, {
|
|
1291
|
+
...ctx,
|
|
1292
|
+
path: [...ctx.path || []]
|
|
1293
|
+
});
|
|
1294
|
+
if (outcome.success) {
|
|
1295
|
+
if (!reachCondition) return { success: maybeInvertPolicyOutcome(true, policy.invert) };
|
|
1296
|
+
conditions.push(reachCondition);
|
|
1297
|
+
pending = true;
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
if (outcome.pending) {
|
|
1301
|
+
pending = true;
|
|
1302
|
+
if (ctx.withConditions && outcome.condition && !ctx.realmAttributeName) conditions.push(reachCondition ? and(reachCondition, outcome.condition) : outcome.condition);
|
|
1303
|
+
else lowerable = false;
|
|
1304
|
+
if (outcome.issues) pendingIssues.push(...outcome.issues);
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
if (outcome.issues) issues.push(...outcome.issues);
|
|
1308
|
+
}
|
|
1309
|
+
if (pending) {
|
|
1310
|
+
const result = {
|
|
1311
|
+
success: false,
|
|
1312
|
+
pending: true,
|
|
1313
|
+
issues: pendingIssues
|
|
1314
|
+
};
|
|
1315
|
+
if (lowerable && ctx.withConditions && conditions.length > 0) {
|
|
1316
|
+
const condition = conditions.length === 1 ? conditions[0] : or(...conditions);
|
|
1317
|
+
result.condition = policy.invert ? not(condition) : condition;
|
|
1318
|
+
}
|
|
1319
|
+
return result;
|
|
1320
|
+
}
|
|
1321
|
+
const success = maybeInvertPolicyOutcome(false, policy.invert);
|
|
1322
|
+
return {
|
|
1323
|
+
success,
|
|
1324
|
+
issues: success ? [] : issues
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
//#endregion
|
|
1329
|
+
//#region src/policy/built-in/permission-binding/module.ts
|
|
1330
|
+
var PermissionBindingPolicyEvaluator = class {
|
|
1331
|
+
validator;
|
|
1332
|
+
constructor() {
|
|
1333
|
+
this.validator = new PermissionBindingPolicyValidator();
|
|
1334
|
+
}
|
|
1335
|
+
requires() {
|
|
1336
|
+
return ["permissionBinding"];
|
|
1337
|
+
}
|
|
1338
|
+
async evaluate(value) {
|
|
1339
|
+
return { success: maybeInvertPolicyOutcome(true, (await this.validator.run(value)).invert) };
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
//#endregion
|
|
1164
1343
|
//#region src/policy/built-in/time/constants.ts
|
|
1165
1344
|
let TimePolicyInterval = /* @__PURE__ */ function(TimePolicyInterval) {
|
|
1166
1345
|
TimePolicyInterval["DAILY"] = "daily";
|
|
@@ -1220,6 +1399,30 @@ function toDate(input, dateRef) {
|
|
|
1220
1399
|
if (typeof input === "number") return new Date(input);
|
|
1221
1400
|
return input;
|
|
1222
1401
|
}
|
|
1402
|
+
/**
|
|
1403
|
+
* The next instant at which a time policy's verdict could change, seen from
|
|
1404
|
+
* `now`: today's start while it is ahead, the minute after today's end while
|
|
1405
|
+
* that is ahead, and the next local midnight, where the daily window moves on
|
|
1406
|
+
* and the day-of-week/month/year checks roll over. The earliest of those; a
|
|
1407
|
+
* policy with no constraint never changes.
|
|
1408
|
+
*
|
|
1409
|
+
* `normalizeDate` keeps the seconds and milliseconds of the instant it is
|
|
1410
|
+
* evaluated at and drops the policy's own, so the comparisons below are made
|
|
1411
|
+
* at minute precision: `now < start` stops holding at HH:MM:00.000 of the
|
|
1412
|
+
* start, and `now > end` starts holding at the following minute. Reading the
|
|
1413
|
+
* candidates off `normalizeDate` instead would report the seconds of this
|
|
1414
|
+
* evaluation, up to a minute after the verdict already flipped.
|
|
1415
|
+
*/
|
|
1416
|
+
function nextTimeTransition(policy, now) {
|
|
1417
|
+
const candidates = [];
|
|
1418
|
+
const atMinute = (input) => new Date(now.getFullYear(), now.getMonth(), now.getDate(), input.getHours(), input.getMinutes());
|
|
1419
|
+
if (policy.start) candidates.push(atMinute(toDate(policy.start, now)));
|
|
1420
|
+
if (policy.end) candidates.push(new Date(atMinute(toDate(policy.end, now)).getTime() + 6e4));
|
|
1421
|
+
if (policy.start || policy.end || policy.interval) candidates.push(new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1));
|
|
1422
|
+
let next;
|
|
1423
|
+
for (const candidate of candidates) if (candidate.getTime() > now.getTime() && (!next || candidate.getTime() < next.getTime())) next = candidate;
|
|
1424
|
+
return next;
|
|
1425
|
+
}
|
|
1223
1426
|
var TimePolicyEvaluator = class {
|
|
1224
1427
|
validator;
|
|
1225
1428
|
constructor() {
|
|
@@ -1235,7 +1438,13 @@ var TimePolicyEvaluator = class {
|
|
|
1235
1438
|
ctx.data.set("time", now);
|
|
1236
1439
|
ctx.data.setValidated("time");
|
|
1237
1440
|
}
|
|
1238
|
-
} else
|
|
1441
|
+
} else {
|
|
1442
|
+
now = /* @__PURE__ */ new Date();
|
|
1443
|
+
if (ctx.transitions) {
|
|
1444
|
+
const next = nextTimeTransition(policy, now);
|
|
1445
|
+
if (next) ctx.transitions.report(next);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1239
1448
|
if (policy.start) {
|
|
1240
1449
|
const start = normalizeDate(toDate(policy.start, now), now);
|
|
1241
1450
|
if (now < start) return { success: maybeInvertPolicyOutcome(false, policy.invert) };
|
|
@@ -1280,6 +1489,25 @@ const PolicyDefaultEvaluators = {
|
|
|
1280
1489
|
["realmMatch"]: new RealmMatchPolicyEvaluator(),
|
|
1281
1490
|
["time"]: new TimePolicyEvaluator()
|
|
1282
1491
|
};
|
|
1492
|
+
/**
|
|
1493
|
+
* The configuration validator of every built-in policy type, keyed like
|
|
1494
|
+
* `PolicyDefaultEvaluators`. It is what `projectAuthorizationPolicy` runs a
|
|
1495
|
+
* node through, so a type absent here cannot travel in the authorization
|
|
1496
|
+
* catalog. The registry is open like the evaluator one (#3635): spread it and
|
|
1497
|
+
* add a custom type's validator to let that type travel. It is frozen, since
|
|
1498
|
+
* a write into it would make the type projectable for every caller in the
|
|
1499
|
+
* process.
|
|
1500
|
+
*/
|
|
1501
|
+
const PolicyDefaultValidators = Object.freeze({
|
|
1502
|
+
["composite"]: new CompositePolicyValidator(),
|
|
1503
|
+
["attributes"]: new AttributesPolicyValidator(),
|
|
1504
|
+
["attributeNames"]: new AttributeNamesPolicyValidator(),
|
|
1505
|
+
["date"]: new DatePolicyValidator(),
|
|
1506
|
+
["identity"]: new IdentityPolicyValidator(),
|
|
1507
|
+
["permissionBinding"]: new PermissionBindingPolicyValidator(),
|
|
1508
|
+
["realmMatch"]: new RealmMatchPolicyValidator(),
|
|
1509
|
+
["time"]: new TimePolicyValidator()
|
|
1510
|
+
});
|
|
1283
1511
|
//#endregion
|
|
1284
1512
|
//#region src/policy/input.ts
|
|
1285
1513
|
/**
|
|
@@ -1356,6 +1584,7 @@ var PermissionEvaluator = class {
|
|
|
1356
1584
|
const evaluationResult = await this.policyEngine.evaluate(compositePolicy, definePolicyEvaluationContext({
|
|
1357
1585
|
include: options.policiesIncluded,
|
|
1358
1586
|
exclude: options.policiesExcluded,
|
|
1587
|
+
transitions: options.transitions,
|
|
1359
1588
|
data
|
|
1360
1589
|
}));
|
|
1361
1590
|
if (evaluationResult.success || options.pendingPolicies === "permit" && !!evaluationResult.pending) {
|
|
@@ -1435,7 +1664,8 @@ var PermissionEvaluator = class {
|
|
|
1435
1664
|
};
|
|
1436
1665
|
const evaluationResult = await this.policyEngine.evaluate(compositePolicy, definePolicyEvaluationContext({
|
|
1437
1666
|
data,
|
|
1438
|
-
withConditions: true
|
|
1667
|
+
withConditions: true,
|
|
1668
|
+
realmAttributeName: ctx.realmAttributeName
|
|
1439
1669
|
}));
|
|
1440
1670
|
if (evaluationResult.success) return { verdict: "allow" };
|
|
1441
1671
|
if (evaluationResult.pending) {
|
|
@@ -1459,6 +1689,584 @@ var PermissionEvaluator = class {
|
|
|
1459
1689
|
}
|
|
1460
1690
|
};
|
|
1461
1691
|
//#endregion
|
|
1462
|
-
|
|
1692
|
+
//#region src/permission/authorization/error.ts
|
|
1693
|
+
const AUTHORIZATION_CATALOG_STALE_ERROR_INSTANCE = Symbol.for("@authup/access/AuthorizationCatalogStaleError");
|
|
1694
|
+
/**
|
|
1695
|
+
* A grant references a definition or a policy the supplied catalog does not
|
|
1696
|
+
* carry: the caller's cached catalog predates the definition or the junction
|
|
1697
|
+
* row and must be refetched. A definition the catalog carries with
|
|
1698
|
+
* `policies: null`, and one whose tree this copy cannot project, are not
|
|
1699
|
+
* stale but unevaluable, and a grant of either is dropped.
|
|
1700
|
+
*
|
|
1701
|
+
* Recognize it with `isAuthorizationCatalogStaleError`, never `instanceof`:
|
|
1702
|
+
* this package is a plain caret dependency of several workspaces, so a
|
|
1703
|
+
* consumer tree may resolve two copies of it and turn the one recoverable
|
|
1704
|
+
* state into an unrecoverable one.
|
|
1705
|
+
*/
|
|
1706
|
+
var AuthorizationCatalogStaleError = class extends AuthupError {
|
|
1707
|
+
constructor(message) {
|
|
1708
|
+
super({ message });
|
|
1709
|
+
this.name = "AuthorizationCatalogStaleError";
|
|
1710
|
+
markInstanceof(this, AUTHORIZATION_CATALOG_STALE_ERROR_INSTANCE);
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
//#endregion
|
|
1714
|
+
//#region src/permission/authorization/check.ts
|
|
1715
|
+
/**
|
|
1716
|
+
* Marker-only: the error carries no code of its own, and it never crosses a
|
|
1717
|
+
* wire boundary, so there is no shape to fall back to.
|
|
1718
|
+
*/
|
|
1719
|
+
function isAuthorizationCatalogStaleError(input) {
|
|
1720
|
+
return matchesInstanceof(input, AUTHORIZATION_CATALOG_STALE_ERROR_INSTANCE);
|
|
1721
|
+
}
|
|
1722
|
+
//#endregion
|
|
1723
|
+
//#region src/permission/authorization/schema.ts
|
|
1724
|
+
const id = z.string().min(1);
|
|
1725
|
+
const namespaceId = id.nullable();
|
|
1726
|
+
/**
|
|
1727
|
+
* A policy node as it travels. Deliberately LOOSE: every key beyond `type` is
|
|
1728
|
+
* that type's own configuration, and `projectAuthorizationPolicy` is what
|
|
1729
|
+
* validates it, by running the type's own validator. Mounting a node in a
|
|
1730
|
+
* validup container would strip every key the container does not mount, and
|
|
1731
|
+
* the whole configuration would go with it.
|
|
1732
|
+
*/
|
|
1733
|
+
const authorizationPolicySchema = z.looseObject({ type: z.string().min(1) });
|
|
1734
|
+
/**
|
|
1735
|
+
* One permission definition. `policies` carries the ids of its
|
|
1736
|
+
* definition-layer trees, or `null` when the server could not project one.
|
|
1737
|
+
*/
|
|
1738
|
+
const authorizationDefinitionSchema = z.object({
|
|
1739
|
+
name: z.string().min(1),
|
|
1740
|
+
realm_id: namespaceId,
|
|
1741
|
+
client_id: namespaceId,
|
|
1742
|
+
decision_strategy: z.enum(DecisionStrategy).nullable(),
|
|
1743
|
+
policies: z.array(id).nullable()
|
|
1744
|
+
});
|
|
1745
|
+
const authorizationCatalogSchema = z.object({
|
|
1746
|
+
policies: z.record(id, authorizationPolicySchema),
|
|
1747
|
+
permissions: z.array(authorizationDefinitionSchema)
|
|
1748
|
+
});
|
|
1749
|
+
/**
|
|
1750
|
+
* One grant of the identity, as the introspection endpoints report it.
|
|
1751
|
+
* Whether a grant's policy ids are declared is checked against the catalog
|
|
1752
|
+
* the consumer holds, since the two travel separately.
|
|
1753
|
+
*/
|
|
1754
|
+
const authorizationGrantSchema = z.object({
|
|
1755
|
+
name: z.string().min(1),
|
|
1756
|
+
realm_id: namespaceId.optional(),
|
|
1757
|
+
client_id: namespaceId.optional(),
|
|
1758
|
+
realm_scope: z.enum(RealmScope).nullish(),
|
|
1759
|
+
policies: z.array(id).nullish()
|
|
1760
|
+
});
|
|
1761
|
+
const authorizationIdentitySchema = z.object({
|
|
1762
|
+
id: z.string().min(1),
|
|
1763
|
+
type: z.enum(["user", "client"]),
|
|
1764
|
+
realmId: z.string().nullish(),
|
|
1765
|
+
realmName: z.string().nullish(),
|
|
1766
|
+
clientId: z.string().nullish()
|
|
1767
|
+
});
|
|
1768
|
+
/**
|
|
1769
|
+
* The whole input `createAuthorizationEvaluator` takes: the catalog
|
|
1770
|
+
* `GET /authorization` serves, the grants an introspection reports and the
|
|
1771
|
+
* identity it names. A validup container over zod mounts, the shape every
|
|
1772
|
+
* validator in this package uses, so a consumer can reuse or override one
|
|
1773
|
+
* member's rules instead of restating the document.
|
|
1774
|
+
*/
|
|
1775
|
+
var AuthorizationEvaluatorInputValidator = class extends Container {
|
|
1776
|
+
initialize() {
|
|
1777
|
+
super.initialize();
|
|
1778
|
+
this.mount("catalog", createValidator(authorizationCatalogSchema));
|
|
1779
|
+
this.mount("grants", createValidator(z.array(authorizationGrantSchema).optional()));
|
|
1780
|
+
this.mount("identity", createValidator(authorizationIdentitySchema.optional()));
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
const validator = new AuthorizationEvaluatorInputValidator();
|
|
1784
|
+
/**
|
|
1785
|
+
* Validate the evaluator's input, plus the two rules per-key validators
|
|
1786
|
+
* cannot express.
|
|
1787
|
+
*
|
|
1788
|
+
* Every id a definition names must be a key of `policies`: a dangling id
|
|
1789
|
+
* resolves to `undefined`, which reads as "this layer carries no policy", so
|
|
1790
|
+
* a catalog that merely omitted a tree would grant unrestricted access rather
|
|
1791
|
+
* than fail. The lookup is own-property only, so an id such as `constructor`
|
|
1792
|
+
* cannot be answered by a member of `Object.prototype`.
|
|
1793
|
+
*
|
|
1794
|
+
* And the identity and its grants travel together: grants without an identity
|
|
1795
|
+
* belong to nobody, while an identity without a grant list is the shape an
|
|
1796
|
+
* INACTIVE introspection produces (`permissions` is absent unless the
|
|
1797
|
+
* credential is active), which read as "holds nothing" would authorize every
|
|
1798
|
+
* definition that carries no binding check. An identity holding no grant says
|
|
1799
|
+
* so with an empty array.
|
|
1800
|
+
*/
|
|
1801
|
+
async function parseAuthorizationEvaluatorInput(input) {
|
|
1802
|
+
const identityGiven = typeof input.identity !== "undefined";
|
|
1803
|
+
const grantsGiven = typeof input.grants !== "undefined";
|
|
1804
|
+
if (!identityGiven && grantsGiven) throw new ValidupError([defineIssueItem({
|
|
1805
|
+
message: "Grants require the identity they belong to.",
|
|
1806
|
+
path: ["identity"]
|
|
1807
|
+
})]);
|
|
1808
|
+
if (identityGiven && !grantsGiven) throw new ValidupError([defineIssueItem({
|
|
1809
|
+
message: "An identity requires its grant list; pass an empty array for an identity holding none.",
|
|
1810
|
+
path: ["grants"]
|
|
1811
|
+
})]);
|
|
1812
|
+
const { catalog, grants, identity } = await validator.run(structuredClone(input));
|
|
1813
|
+
for (const [i, definition] of catalog.permissions.entries()) for (const [j, policyId] of (definition.policies ?? []).entries()) {
|
|
1814
|
+
if (Object.hasOwn(catalog.policies, policyId)) continue;
|
|
1815
|
+
throw new ValidupError([defineIssueItem({
|
|
1816
|
+
message: `The policy ${policyId} is not declared by the catalog.`,
|
|
1817
|
+
path: [
|
|
1818
|
+
"catalog",
|
|
1819
|
+
"permissions",
|
|
1820
|
+
i,
|
|
1821
|
+
"policies",
|
|
1822
|
+
j
|
|
1823
|
+
]
|
|
1824
|
+
})]);
|
|
1825
|
+
}
|
|
1826
|
+
return {
|
|
1827
|
+
catalog,
|
|
1828
|
+
grants: grants ?? [],
|
|
1829
|
+
identity
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* One verdict: the permission namespace and the requested realms it holds in.
|
|
1834
|
+
* A realm value is a realm key the caller itself supplied, or `null` for the
|
|
1835
|
+
* global rows, so nothing here is resolved or looked up.
|
|
1836
|
+
*/
|
|
1837
|
+
const authorizationCheckPermissionSchema = z.object({
|
|
1838
|
+
name: z.string().min(1),
|
|
1839
|
+
realms: z.array(z.string().min(1).nullable()).min(1)
|
|
1840
|
+
});
|
|
1841
|
+
const authorizationCheckPermissionsSchema = z.array(authorizationCheckPermissionSchema);
|
|
1842
|
+
//#endregion
|
|
1843
|
+
//#region src/permission/authorization/check-schema.ts
|
|
1844
|
+
/**
|
|
1845
|
+
* The whole input `createAuthorizationCheckEvaluator` takes: the answer
|
|
1846
|
+
* `POST /authorization/check` served and the identity it belongs to. Same
|
|
1847
|
+
* validup-over-zod shape as the catalog's input, so a consumer can override
|
|
1848
|
+
* one member's rules instead of restating the answer.
|
|
1849
|
+
*/
|
|
1850
|
+
var AuthorizationCheckEvaluatorInputValidator = class extends Container {
|
|
1851
|
+
initialize() {
|
|
1852
|
+
super.initialize();
|
|
1853
|
+
this.mount("permissions", createValidator(authorizationCheckPermissionsSchema));
|
|
1854
|
+
this.mount("identity", createValidator(authorizationIdentitySchema.optional()));
|
|
1855
|
+
}
|
|
1856
|
+
};
|
|
1857
|
+
const checkValidator = new AuthorizationCheckEvaluatorInputValidator();
|
|
1858
|
+
/**
|
|
1859
|
+
* Validate the check evaluator's input, plus the one rule a per-key validator
|
|
1860
|
+
* cannot express: a permission namespace appears at most once. Two entries for
|
|
1861
|
+
* one name would make the answer depend on which the consumer looked up first,
|
|
1862
|
+
* and the server emits one entry per name by construction.
|
|
1863
|
+
*/
|
|
1864
|
+
async function parseAuthorizationCheckEvaluatorInput(input) {
|
|
1865
|
+
const { permissions, identity } = await checkValidator.run(structuredClone(input));
|
|
1866
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1867
|
+
for (const [i, permission] of permissions.entries()) {
|
|
1868
|
+
if (seen.has(permission.name)) throw new ValidupError([defineIssueItem({
|
|
1869
|
+
message: `The permission ${permission.name} is answered more than once.`,
|
|
1870
|
+
path: [
|
|
1871
|
+
"permissions",
|
|
1872
|
+
i,
|
|
1873
|
+
"name"
|
|
1874
|
+
]
|
|
1875
|
+
})]);
|
|
1876
|
+
seen.add(permission.name);
|
|
1877
|
+
}
|
|
1878
|
+
return {
|
|
1879
|
+
permissions,
|
|
1880
|
+
identity
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
//#endregion
|
|
1884
|
+
//#region src/permission/authorization/constants.ts
|
|
1885
|
+
/**
|
|
1886
|
+
* The node a policy tree travels as when the server withholds it, because the
|
|
1887
|
+
* caller's realm reach does not cover the row it was read from.
|
|
1888
|
+
*
|
|
1889
|
+
* It is deliberately a type no consumer can project: `projectAuthorizationPolicy`
|
|
1890
|
+
* refuses it, which is already the state a definition referencing it denies
|
|
1891
|
+
* through and a grant naming it is dropped through, so withholding a body needs
|
|
1892
|
+
* no rule of its own on the consumer side. That is a contract between the two
|
|
1893
|
+
* halves rather than a server detail, which is why it is declared HERE, next to
|
|
1894
|
+
* the projection that has to keep refusing it, and pinned by this package's own
|
|
1895
|
+
* test.
|
|
1896
|
+
*/
|
|
1897
|
+
const AUTHORIZATION_POLICY_WITHHELD_TYPE = "withheld";
|
|
1898
|
+
//#endregion
|
|
1899
|
+
//#region src/permission/authorization/policy.ts
|
|
1900
|
+
/**
|
|
1901
|
+
* The two keys a node carries whatever its type is. Deliberately NOT the whole
|
|
1902
|
+
* node: every other key is that type's own configuration, which the type's own
|
|
1903
|
+
* validator reads, and a container strips every key it does not mount.
|
|
1904
|
+
*/
|
|
1905
|
+
var AuthorizationPolicyHeadValidator = class extends Container {
|
|
1906
|
+
initialize() {
|
|
1907
|
+
super.initialize();
|
|
1908
|
+
this.mount("type", createValidator(z.string().min(1)));
|
|
1909
|
+
this.mount("children", createValidator(z.array(z.unknown()).optional()));
|
|
1910
|
+
}
|
|
1911
|
+
};
|
|
1912
|
+
/**
|
|
1913
|
+
* The one definition of what a policy looks like on the wire, used by the server
|
|
1914
|
+
* to build a document and by the consumer to validate one. A node is the OUTPUT
|
|
1915
|
+
* of its type's validator (validup strips every unmounted key, so entity columns
|
|
1916
|
+
* such as `id`, `name`, `builtIn` or `createdAt` never travel), plus its type
|
|
1917
|
+
* and, for a composite, its projected children.
|
|
1918
|
+
*
|
|
1919
|
+
* The type set is the `validators` registry, `PolicyDefaultValidators` unless
|
|
1920
|
+
* the caller hands another (#3635): a type the registry lacks is refused, so
|
|
1921
|
+
* the server and a consumer that register the same custom type let it travel,
|
|
1922
|
+
* and one that does not denies it. The withheld node is refused whatever the
|
|
1923
|
+
* registry says, since projecting it would evaluate a tree the caller was not
|
|
1924
|
+
* allowed to see.
|
|
1925
|
+
*
|
|
1926
|
+
* Every refusal is a `ValidupError`, the node's head and the type's own
|
|
1927
|
+
* validator alike, and a child's issues are rebased onto the position it sits
|
|
1928
|
+
* at, so one message names the node in the tree that is wrong.
|
|
1929
|
+
*/
|
|
1930
|
+
async function projectAuthorizationPolicy(input, validators = PolicyDefaultValidators) {
|
|
1931
|
+
const headValidator = new AuthorizationPolicyHeadValidator();
|
|
1932
|
+
const project = async (value) => {
|
|
1933
|
+
const head = await headValidator.run(value);
|
|
1934
|
+
const validator = head.type !== "withheld" && Object.hasOwn(validators, head.type) ? validators[head.type] : void 0;
|
|
1935
|
+
if (!validator) throw new ValidupError([defineIssueItem({
|
|
1936
|
+
message: `The policy type ${head.type} is not supported.`,
|
|
1937
|
+
path: ["type"]
|
|
1938
|
+
})]);
|
|
1939
|
+
const output = {
|
|
1940
|
+
...await validator.run(value),
|
|
1941
|
+
type: head.type
|
|
1942
|
+
};
|
|
1943
|
+
for (const key of Object.keys(output)) if (typeof output[key] === "undefined") delete output[key];
|
|
1944
|
+
if (head.type === "composite") output.children = await Promise.all((head.children ?? []).map((child, index) => projectChild(child, index)));
|
|
1945
|
+
else delete output.children;
|
|
1946
|
+
return output;
|
|
1947
|
+
};
|
|
1948
|
+
const projectChild = async (child, index) => {
|
|
1949
|
+
try {
|
|
1950
|
+
return await project(child);
|
|
1951
|
+
} catch (e) {
|
|
1952
|
+
if (isValidupError(e)) throw new ValidupError(e.issues.map((issue) => prefixIssuePath(issue, ["children", index])));
|
|
1953
|
+
throw e;
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
return project(input);
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Whether a projected tree evaluates the permission binding at any depth. A
|
|
1960
|
+
* grant policy carrying one would re-enter its own permission's grants, so
|
|
1961
|
+
* `createAuthorizationEvaluator` drops the grant that names it and evaluates
|
|
1962
|
+
* the permission from the rest. Its one caller is that consumer: the tree is
|
|
1963
|
+
* legal in a definition, where the binding check is what reach is enforced
|
|
1964
|
+
* by, so the producer carries it and only the grant side is refused.
|
|
1965
|
+
*/
|
|
1966
|
+
function containsBindingCheck(policy) {
|
|
1967
|
+
if (policy.type === "permissionBinding") return true;
|
|
1968
|
+
return (policy.children ?? []).some((child) => containsBindingCheck(child));
|
|
1969
|
+
}
|
|
1970
|
+
//#endregion
|
|
1971
|
+
//#region src/permission/authorization/module.ts
|
|
1972
|
+
/**
|
|
1973
|
+
* Every value `realmScopeMatches` decides, and nothing it would have to guess
|
|
1974
|
+
* at (#3636): a realm key, `null` for a global row, or a list of either for a
|
|
1975
|
+
* row spanning several realms. An empty key and an empty list are legal and
|
|
1976
|
+
* DENY there (no identity's realm is the empty string, and the empty set is
|
|
1977
|
+
* fail-closed), so refusing them here would make the consumer the one side
|
|
1978
|
+
* that errors where the server answers. `undefined` stays refused: the server
|
|
1979
|
+
* coerces it to `null`, which reads as a global row, and a caller passing an
|
|
1980
|
+
* absent column should hear about it rather than reach the global rows.
|
|
1981
|
+
*/
|
|
1982
|
+
const realmMatchSchema = z.union([
|
|
1983
|
+
z.string(),
|
|
1984
|
+
z.array(z.string().nullable()),
|
|
1985
|
+
z.null()
|
|
1986
|
+
]);
|
|
1987
|
+
/**
|
|
1988
|
+
* Every refusal of the DOCUMENT is a `ValidupError` carrying the path of the
|
|
1989
|
+
* member at fault, the shape `parseAuthorizationEvaluatorInput` raises and the
|
|
1990
|
+
* one a consumer can render. A bare `Error` carries neither a code nor a path,
|
|
1991
|
+
* so nothing downstream can normalize it.
|
|
1992
|
+
*/
|
|
1993
|
+
function refuseDocument(message, path) {
|
|
1994
|
+
throw new ValidupError([defineIssueItem({
|
|
1995
|
+
message,
|
|
1996
|
+
path
|
|
1997
|
+
})]);
|
|
1998
|
+
}
|
|
1999
|
+
function refuseDocumentWithIssues(issues) {
|
|
2000
|
+
throw new ValidupError(issues);
|
|
2001
|
+
}
|
|
2002
|
+
function toPolicy(tree) {
|
|
2003
|
+
const { invert, children, ...policy } = tree;
|
|
2004
|
+
return {
|
|
2005
|
+
...policy,
|
|
2006
|
+
...typeof invert === "boolean" ? { invert } : {},
|
|
2007
|
+
...children ? { children: children.map((child) => toPolicy(child)) } : {}
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Build a resource-server (or console) evaluator from the catalog
|
|
2012
|
+
* `GET /authorization` serves, the identity's grants as the introspection
|
|
2013
|
+
* endpoints report them, and the identity itself. Rejects legacy, incomplete
|
|
2014
|
+
* and unsupported input. A grant referencing a definition or a policy the
|
|
2015
|
+
* catalog lacks throws `AuthorizationCatalogStaleError`, the signal to
|
|
2016
|
+
* refetch the catalog: the grants come from a fresh introspection while the
|
|
2017
|
+
* catalog may predate the definition or the junction row. A definition the
|
|
2018
|
+
* server could not project travels with `policies: null` instead of being
|
|
2019
|
+
* left out, so a grant of it is dropped rather than reported stale: the
|
|
2020
|
+
* definition layer cannot be evaluated here, a refetch changes nothing, and
|
|
2021
|
+
* the permission denies until the policy is fixed.
|
|
2022
|
+
*
|
|
2023
|
+
* A tree THIS copy cannot project is the same condition read from the other
|
|
2024
|
+
* side, and it is per tree: a policy type the validator registry lacks (a
|
|
2025
|
+
* type newer than this package, or a custom one the caller did not register)
|
|
2026
|
+
* or a configuration its validator refuses denies the definitions that reference
|
|
2027
|
+
* it, exactly as `policies: null` does, and drops a grant that names it,
|
|
2028
|
+
* exactly as a binding check does. One such tree never takes the evaluator
|
|
2029
|
+
* down for the permissions that do not use it. A malformed catalog still
|
|
2030
|
+
* throws: a duplicate permission namespace is not a data condition.
|
|
2031
|
+
*
|
|
2032
|
+
* Both the identity and the grants are optional. Without an identity no grant
|
|
2033
|
+
* is bound and any IDENTITY key the caller supplied is REMOVED, so the
|
|
2034
|
+
* binding evaluator answers with missing data exactly as the server does for
|
|
2035
|
+
* an anonymous request, only a definition whose policies need no identity can
|
|
2036
|
+
* pass, and a caller can never inject an identity the document did not prove.
|
|
2037
|
+
* Grants without the identity they belong to are refused.
|
|
2038
|
+
*
|
|
2039
|
+
* The REALM_MATCH data key follows the server's own three-way rule: a
|
|
2040
|
+
* resource that carries a realm passes it (null for a global row, which
|
|
2041
|
+
* `own` denies), a resource with no realm dimension passes nothing and reach
|
|
2042
|
+
* neutral-passes. `preEvaluate` / `preEvaluateOneOf` are the pre-gate and
|
|
2043
|
+
* enforce reach only when that key is present. `compile` returns the
|
|
2044
|
+
* allow/deny/conditional/post contract; a `post` collection query must be
|
|
2045
|
+
* rejected or evaluated over every candidate before paging. The supplied
|
|
2046
|
+
* identity is authoritative; `options.decisionStrategy` is forwarded and the
|
|
2047
|
+
* policy include, exclude and pending options are refused.
|
|
2048
|
+
*
|
|
2049
|
+
* The policy type set is open (#3635): `options.validators` decides which
|
|
2050
|
+
* trees project and `options.evaluators` how they evaluate, the built-in
|
|
2051
|
+
* registries by default. A custom type needs both, the validator the server
|
|
2052
|
+
* projected it with and the evaluator the server decides it with; given only
|
|
2053
|
+
* the first it projects and then denies. The binding evaluator is always this
|
|
2054
|
+
* function's own, since it is what binds the introspected grants.
|
|
2055
|
+
*/
|
|
2056
|
+
async function createAuthorizationEvaluator(input, options = {}) {
|
|
2057
|
+
const { catalog, grants, identity } = await parseAuthorizationEvaluatorInput(input);
|
|
2058
|
+
const trees = /* @__PURE__ */ new Map();
|
|
2059
|
+
const unevaluable = /* @__PURE__ */ new Set();
|
|
2060
|
+
for (const [id, raw] of Object.entries(catalog.policies)) try {
|
|
2061
|
+
trees.set(id, await projectAuthorizationPolicy(raw, options.validators));
|
|
2062
|
+
} catch {
|
|
2063
|
+
unevaluable.add(id);
|
|
2064
|
+
}
|
|
2065
|
+
const definitions = [];
|
|
2066
|
+
const permissions = /* @__PURE__ */ new Map();
|
|
2067
|
+
for (const [index, entry] of catalog.permissions.entries()) {
|
|
2068
|
+
const permission = {
|
|
2069
|
+
name: entry.name,
|
|
2070
|
+
realmId: entry.realm_id,
|
|
2071
|
+
clientId: entry.client_id,
|
|
2072
|
+
decisionStrategy: entry.decision_strategy ?? void 0
|
|
2073
|
+
};
|
|
2074
|
+
const key = buildPermissionKey(permission);
|
|
2075
|
+
if (permissions.has(key)) refuseDocument(`The catalog defines the permission ${key} more than once.`, [
|
|
2076
|
+
"catalog",
|
|
2077
|
+
"permissions",
|
|
2078
|
+
index
|
|
2079
|
+
]);
|
|
2080
|
+
if (entry.policies === null || entry.policies.some((id) => unevaluable.has(id))) {
|
|
2081
|
+
permissions.set(key, null);
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
permissions.set(key, permission);
|
|
2085
|
+
const policies = [];
|
|
2086
|
+
for (const [i, id] of entry.policies.entries()) {
|
|
2087
|
+
const tree = trees.get(id);
|
|
2088
|
+
if (!tree) refuseDocument(`The catalog does not declare the policy ${id}.`, [
|
|
2089
|
+
"catalog",
|
|
2090
|
+
"permissions",
|
|
2091
|
+
index,
|
|
2092
|
+
"policies",
|
|
2093
|
+
i
|
|
2094
|
+
]);
|
|
2095
|
+
policies.push(toPolicy(tree));
|
|
2096
|
+
}
|
|
2097
|
+
definitions.push({
|
|
2098
|
+
permission,
|
|
2099
|
+
policies: policies.length > 0 ? policies : void 0
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
const bindings = [];
|
|
2103
|
+
for (const grant of grants) {
|
|
2104
|
+
const key = buildPermissionKey({
|
|
2105
|
+
name: grant.name,
|
|
2106
|
+
realmId: grant.realm_id ?? null,
|
|
2107
|
+
clientId: grant.client_id ?? null
|
|
2108
|
+
});
|
|
2109
|
+
const permission = permissions.get(key);
|
|
2110
|
+
if (typeof permission === "undefined") throw new AuthorizationCatalogStaleError(`The catalog does not define the permission ${key}.`);
|
|
2111
|
+
if (permission === null) continue;
|
|
2112
|
+
const policies = [];
|
|
2113
|
+
let evaluable = true;
|
|
2114
|
+
for (const id of grant.policies ?? []) {
|
|
2115
|
+
if (unevaluable.has(id)) {
|
|
2116
|
+
evaluable = false;
|
|
2117
|
+
break;
|
|
2118
|
+
}
|
|
2119
|
+
const tree = trees.get(id);
|
|
2120
|
+
if (!tree) throw new AuthorizationCatalogStaleError(`The catalog does not declare the policy ${id}.`);
|
|
2121
|
+
if (containsBindingCheck(tree)) {
|
|
2122
|
+
evaluable = false;
|
|
2123
|
+
break;
|
|
2124
|
+
}
|
|
2125
|
+
policies.push(toPolicy(tree));
|
|
2126
|
+
}
|
|
2127
|
+
if (!evaluable) continue;
|
|
2128
|
+
bindings.push({
|
|
2129
|
+
permission,
|
|
2130
|
+
realmScope: normalizeRealmScope(grant.realm_scope),
|
|
2131
|
+
policies: policies.length > 0 ? policies : void 0
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
const engine = new PolicyEngine(options.evaluators ?? PolicyDefaultEvaluators);
|
|
2135
|
+
engine.registerEvaluator("permissionBinding", new IdentityPermissionBindingPolicyEvaluator({ getFor: async () => bindings }));
|
|
2136
|
+
const evaluator = new PermissionEvaluator({
|
|
2137
|
+
provider: new PermissionMemoryProvider(definitions),
|
|
2138
|
+
policyEngine: engine
|
|
2139
|
+
});
|
|
2140
|
+
const withIdentity = (input) => {
|
|
2141
|
+
const data = input?.clone() ?? new PolicyData();
|
|
2142
|
+
if (!identity) {
|
|
2143
|
+
data.delete("identity");
|
|
2144
|
+
return data;
|
|
2145
|
+
}
|
|
2146
|
+
data.set("identity", {
|
|
2147
|
+
id: identity.id,
|
|
2148
|
+
type: identity.type,
|
|
2149
|
+
realmId: identity.realmId ?? void 0,
|
|
2150
|
+
realmName: identity.realmName ?? void 0,
|
|
2151
|
+
clientId: identity.clientId ?? null
|
|
2152
|
+
});
|
|
2153
|
+
return data;
|
|
2154
|
+
};
|
|
2155
|
+
const forGate = (ctx) => {
|
|
2156
|
+
if (ctx.options?.policiesIncluded || ctx.options?.policiesExcluded || ctx.options?.pendingPolicies) throw new InternalError("The authorization evaluator does not accept policy bypass options.");
|
|
2157
|
+
return {
|
|
2158
|
+
name: ctx.name,
|
|
2159
|
+
realmId: ctx.realmId,
|
|
2160
|
+
clientId: ctx.clientId,
|
|
2161
|
+
data: withIdentity(ctx.data),
|
|
2162
|
+
...ctx.options?.decisionStrategy ? { options: { decisionStrategy: ctx.options.decisionStrategy } } : {}
|
|
2163
|
+
};
|
|
2164
|
+
};
|
|
2165
|
+
const forResource = (ctx) => {
|
|
2166
|
+
const next = forGate(ctx);
|
|
2167
|
+
if (next.data.has("realmMatch")) {
|
|
2168
|
+
const value = next.data.get("realmMatch");
|
|
2169
|
+
const outcome = realmMatchSchema.safeParse(value);
|
|
2170
|
+
if (!outcome.success) refuseDocumentWithIssues(buildIssuesForZodError(outcome.error, value).map((issue) => prefixIssuePath(issue, ["data", "realmMatch"])));
|
|
2171
|
+
}
|
|
2172
|
+
return next;
|
|
2173
|
+
};
|
|
2174
|
+
return {
|
|
2175
|
+
async evaluate(ctx) {
|
|
2176
|
+
return evaluator.evaluate(forResource(ctx));
|
|
2177
|
+
},
|
|
2178
|
+
async evaluateOneOf(ctx) {
|
|
2179
|
+
return evaluator.evaluateOneOf(forResource(ctx));
|
|
2180
|
+
},
|
|
2181
|
+
async preEvaluate(ctx) {
|
|
2182
|
+
return evaluator.preEvaluate(forGate(ctx));
|
|
2183
|
+
},
|
|
2184
|
+
async preEvaluateOneOf(ctx) {
|
|
2185
|
+
return evaluator.preEvaluateOneOf(forGate(ctx));
|
|
2186
|
+
},
|
|
2187
|
+
async compile(ctx) {
|
|
2188
|
+
if (ctx.data?.has("realmMatch") || ctx.data?.has("attributes")) throw new InternalError("Compile authorization without resource realm or row attributes.");
|
|
2189
|
+
return evaluator.compile({
|
|
2190
|
+
name: ctx.name,
|
|
2191
|
+
realmId: ctx.realmId,
|
|
2192
|
+
clientId: ctx.clientId,
|
|
2193
|
+
data: withIdentity(ctx.data),
|
|
2194
|
+
realmAttributeName: ctx.realmAttributeName
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* An `IPermissionEvaluator` over the answer `POST /authorization/check`
|
|
2201
|
+
* served: the caller's own verdicts, paired with the realms they hold in.
|
|
2202
|
+
*
|
|
2203
|
+
* It is what a PUBLIC client gets where the catalog is out of reach. A public
|
|
2204
|
+
* client holds no secret, so it can obtain no `client_credentials` token and
|
|
2205
|
+
* has no credential of its own for the catalog's gate to be satisfied by;
|
|
2206
|
+
* publishing every policy predicate to it instead would be the wrong trade.
|
|
2207
|
+
* The verdicts are computed by the server from the same grants and the same
|
|
2208
|
+
* evaluators a request runs, so this is authoritative where the name-only
|
|
2209
|
+
* fallback it replaces is merely coarse.
|
|
2210
|
+
*
|
|
2211
|
+
* Two properties follow from the answer being a pre-gate:
|
|
2212
|
+
*
|
|
2213
|
+
* It is an UPPER BOUND, so `evaluate` and `evaluateOneOf` answer exactly what
|
|
2214
|
+
* the pre-gate pair answers. A check that depends on a resource row belongs to
|
|
2215
|
+
* whoever holds the row, and the server decides it there; answering such a
|
|
2216
|
+
* check here would mean inventing a verdict. That is the same bound the
|
|
2217
|
+
* name-only fallback already had, so nothing that gates on it loosens.
|
|
2218
|
+
*
|
|
2219
|
+
* And a resource realm is matched against what the REQUEST asked about: a
|
|
2220
|
+
* `realmMatch` naming a realm the request never carried is in no entry's list
|
|
2221
|
+
* and therefore denies. That is fail-closed by construction rather than by a
|
|
2222
|
+
* rule, and it is why a caller must ask about the realms its UI will ask
|
|
2223
|
+
* about.
|
|
2224
|
+
*/
|
|
2225
|
+
async function createAuthorizationCheckEvaluator(input) {
|
|
2226
|
+
const { permissions, identity } = await parseAuthorizationCheckEvaluatorInput(input);
|
|
2227
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
2228
|
+
for (const permission of permissions) verdicts.set(permission.name, new Set(permission.realms));
|
|
2229
|
+
const ownKeys = /* @__PURE__ */ new Set();
|
|
2230
|
+
if (identity?.realmId) ownKeys.add(identity.realmId);
|
|
2231
|
+
if (identity?.realmName) ownKeys.add(identity.realmName);
|
|
2232
|
+
const matchesRealm = (realms, value) => {
|
|
2233
|
+
if (realms.has(value)) return true;
|
|
2234
|
+
if (value === null || !ownKeys.has(value)) return false;
|
|
2235
|
+
for (const key of ownKeys) if (realms.has(key)) return true;
|
|
2236
|
+
return false;
|
|
2237
|
+
};
|
|
2238
|
+
const holds = (name, data) => {
|
|
2239
|
+
const realms = verdicts.get(name);
|
|
2240
|
+
if (!realms) return false;
|
|
2241
|
+
if (!data || !data.has("realmMatch")) return true;
|
|
2242
|
+
const value = data.get("realmMatch");
|
|
2243
|
+
if (Array.isArray(value)) {
|
|
2244
|
+
if (value.length === 0) return false;
|
|
2245
|
+
return value.every((entry) => matchesRealm(realms, entry ?? null));
|
|
2246
|
+
}
|
|
2247
|
+
return matchesRealm(realms, value ?? null);
|
|
2248
|
+
};
|
|
2249
|
+
const names = (ctx) => Array.isArray(ctx.name) ? ctx.name : [ctx.name];
|
|
2250
|
+
const assertEvery = async (ctx) => {
|
|
2251
|
+
for (const name of names(ctx)) if (!holds(name, ctx.data)) throw PermissionError.evaluationFailed(ctx.name);
|
|
2252
|
+
};
|
|
2253
|
+
const assertSome = async (ctx) => {
|
|
2254
|
+
const list = names(ctx);
|
|
2255
|
+
for (const name of list) if (holds(name, ctx.data)) return;
|
|
2256
|
+
throw PermissionError.evaluationFailed(ctx.name);
|
|
2257
|
+
};
|
|
2258
|
+
return {
|
|
2259
|
+
evaluate: assertEvery,
|
|
2260
|
+
evaluateOneOf: assertSome,
|
|
2261
|
+
preEvaluate: assertEvery,
|
|
2262
|
+
preEvaluateOneOf: assertSome,
|
|
2263
|
+
async compile(ctx) {
|
|
2264
|
+
if ((Array.isArray(ctx.name) ? ctx.name : [ctx.name]).some((name) => holds(name, ctx.data))) return { verdict: "post" };
|
|
2265
|
+
return { verdict: "deny" };
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
//#endregion
|
|
2270
|
+
export { AUTHORIZATION_CATALOG_STALE_ERROR_INSTANCE, AUTHORIZATION_POLICY_WITHHELD_TYPE, AttributeNamesPolicyEvaluator, AttributeNamesPolicyValidator, AttributesPolicyEvaluator, AttributesPolicyValidator, AuthorizationCatalogStaleError, AuthorizationCheckEvaluatorInputValidator, AuthorizationEvaluatorInputValidator, BuiltInPolicyType, CompositePolicyEvaluator, CompositePolicyValidator, DatePolicyEvaluator, DatePolicyValidator, IdentityPermissionBindingPolicyEvaluator, IdentityPolicyEvaluator, IdentityPolicyValidator, PERMISSION_ERROR_INSTANCE, POLICY_ERROR_INSTANCE, PermissionBindingPolicyEvaluator, PermissionBindingPolicyValidator, PermissionError, PermissionEvaluator, PermissionMemoryProvider, PolicyData, PolicyDefaultEvaluators, PolicyDefaultValidators, PolicyEngine, PolicyError, PolicyIssueCode, PolicyIssueSeverity, REALM_SCOPE_ORDER, RealmMatchPolicyEvaluator, RealmMatchPolicyValidator, RealmScope, SystemPolicyName, TimePolicyEvaluator, TimePolicyInterval, TimePolicyValidator, aggregatePermissionPolicyBindings, authorizationCatalogSchema, authorizationCheckPermissionSchema, authorizationCheckPermissionsSchema, authorizationDefinitionSchema, authorizationGrantSchema, authorizationIdentitySchema, authorizationPolicySchema, buildPermissionKey, compareRealmScope, containsBindingCheck, createAuthorizationCheckEvaluator, createAuthorizationEvaluator, createPolicyTransitionCollector, defineAttributesPolicy, definePolicyData, definePolicyEvaluationContext, definePolicyIssueGroup, definePolicyIssueItem, definePolicyWithType, grantDominates, isAuthorizationCatalogStaleError, isCompositePolicy, isIntervalForDayOfMonth, isIntervalForDayOfWeek, isIntervalForDayOfYear, isPermissionError, isPermissionPolicyBindingEqual, isPolicyError, maxRealmScope, maybeInvertPolicyOutcome, minRealmScope, normalizeRealmScope, parseAuthorizationCheckEvaluatorInput, parseAuthorizationEvaluatorInput, projectAuthorizationPolicy, realmScopeMatches };
|
|
1463
2271
|
|
|
1464
2272
|
//# sourceMappingURL=index.mjs.map
|