@objectstack/plugin-security 9.9.1 → 9.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +173 -14
- package/dist/index.d.ts +173 -14
- package/dist/index.js +126 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +124 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1041,6 +1041,14 @@ var RLSCompiler = class {
|
|
|
1041
1041
|
roles: executionContext?.roles,
|
|
1042
1042
|
org_user_ids: executionContext?.org_user_ids
|
|
1043
1043
|
};
|
|
1044
|
+
const membership = executionContext?.rlsMembership;
|
|
1045
|
+
if (membership && typeof membership === "object") {
|
|
1046
|
+
for (const [key, value] of Object.entries(membership)) {
|
|
1047
|
+
if (Array.isArray(value) && userCtx[key] === void 0) {
|
|
1048
|
+
userCtx[key] = value;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1044
1052
|
const filters = [];
|
|
1045
1053
|
for (const policy of policies) {
|
|
1046
1054
|
if (!policy.using) continue;
|
|
@@ -1057,14 +1065,24 @@ var RLSCompiler = class {
|
|
|
1057
1065
|
}
|
|
1058
1066
|
/**
|
|
1059
1067
|
* Compile a single RLS expression into a query filter.
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
* -
|
|
1064
|
-
* -
|
|
1068
|
+
*
|
|
1069
|
+
* This reference compiler recognizes exactly four forms — anything else
|
|
1070
|
+
* returns `null` and (via {@link compileFilter}) fails closed:
|
|
1071
|
+
* - `field = current_user.property` → `{ field: <value> }`
|
|
1072
|
+
* - `field = 'literal_value'` → `{ field: 'literal_value' }`
|
|
1073
|
+
* - `field IN (current_user.array)` → `{ field: { $in: [...] } }`
|
|
1074
|
+
* (the array may be a §7.3.1 pre-resolved membership set)
|
|
1075
|
+
* - `1 = 1` → `{}` (always-true / no restriction)
|
|
1076
|
+
*
|
|
1077
|
+
* There is intentionally no support for subqueries, `LIKE`/`ILIKE`,
|
|
1078
|
+
* regex, `ANY`/`ALL`, `AND`/`OR`/`NOT`, or `NULL` checks — express those
|
|
1079
|
+
* needs as a `current_user.*` property the runtime pre-resolves instead.
|
|
1065
1080
|
*/
|
|
1066
1081
|
compileExpression(expression, userCtx) {
|
|
1067
1082
|
if (!expression) return null;
|
|
1083
|
+
if (/^\s*1\s*=\s*1\s*$/.test(expression)) {
|
|
1084
|
+
return {};
|
|
1085
|
+
}
|
|
1068
1086
|
const eqMatch = expression.match(/^\s*(\w+)\s*=\s*current_user\.(\w+)\s*$/);
|
|
1069
1087
|
if (eqMatch) {
|
|
1070
1088
|
const [, field, prop] = eqMatch;
|
|
@@ -1212,11 +1230,91 @@ function isPermissionDeniedError(e) {
|
|
|
1212
1230
|
}
|
|
1213
1231
|
|
|
1214
1232
|
// src/bootstrap-platform-admin.ts
|
|
1233
|
+
import { SystemUserId as SystemUserId2 } from "@objectstack/spec/system";
|
|
1234
|
+
|
|
1235
|
+
// src/claim-seed-ownership.ts
|
|
1215
1236
|
import { SystemUserId } from "@objectstack/spec/system";
|
|
1216
1237
|
var SYSTEM_CTX = { isSystem: true };
|
|
1238
|
+
function hasOwnerField(schema) {
|
|
1239
|
+
const fields = schema?.fields;
|
|
1240
|
+
if (!fields) return false;
|
|
1241
|
+
if (Array.isArray(fields)) {
|
|
1242
|
+
return fields.some((f) => f?.name === "owner_id");
|
|
1243
|
+
}
|
|
1244
|
+
return Object.prototype.hasOwnProperty.call(fields, "owner_id");
|
|
1245
|
+
}
|
|
1246
|
+
async function claimSeedOwnership(ql, adminUserId, options = {}) {
|
|
1247
|
+
const logger = options.logger;
|
|
1248
|
+
if (!adminUserId || adminUserId === SystemUserId.SYSTEM) return [];
|
|
1249
|
+
if (!ql || typeof ql.update !== "function" || typeof ql.find !== "function") {
|
|
1250
|
+
return [];
|
|
1251
|
+
}
|
|
1252
|
+
const registry = ql.registry;
|
|
1253
|
+
if (!registry || typeof registry.getAllObjects !== "function") {
|
|
1254
|
+
logger?.warn?.("[security] claimSeedOwnership: registry unavailable");
|
|
1255
|
+
return [];
|
|
1256
|
+
}
|
|
1257
|
+
const schemas = registry.getAllObjects();
|
|
1258
|
+
const results = [];
|
|
1259
|
+
for (const schema of schemas) {
|
|
1260
|
+
if (!schema?.name) continue;
|
|
1261
|
+
if (schema.managedBy) continue;
|
|
1262
|
+
if (schema.name.startsWith("sys_")) continue;
|
|
1263
|
+
if (!hasOwnerField(schema)) continue;
|
|
1264
|
+
try {
|
|
1265
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1266
|
+
const ids = [];
|
|
1267
|
+
for (const where of [{ owner_id: null }, { owner_id: SystemUserId.SYSTEM }]) {
|
|
1268
|
+
const rows = await ql.find(
|
|
1269
|
+
schema.name,
|
|
1270
|
+
{ where, limit: 1e4, fields: ["id"] },
|
|
1271
|
+
{ context: SYSTEM_CTX }
|
|
1272
|
+
);
|
|
1273
|
+
const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
|
|
1274
|
+
for (const r of list) {
|
|
1275
|
+
if (r?.id && !seen.has(r.id)) {
|
|
1276
|
+
seen.add(r.id);
|
|
1277
|
+
ids.push(r.id);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
if (ids.length === 0) continue;
|
|
1282
|
+
let updated = 0;
|
|
1283
|
+
for (const id of ids) {
|
|
1284
|
+
try {
|
|
1285
|
+
await ql.update(
|
|
1286
|
+
schema.name,
|
|
1287
|
+
{ id, owner_id: adminUserId },
|
|
1288
|
+
{ context: SYSTEM_CTX }
|
|
1289
|
+
);
|
|
1290
|
+
updated += 1;
|
|
1291
|
+
} catch (e) {
|
|
1292
|
+
logger?.warn?.(`[security] claimSeedOwnership failed for ${schema.name}:${id}`, {
|
|
1293
|
+
error: e.message
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
if (updated > 0) results.push({ object: schema.name, count: updated });
|
|
1298
|
+
} catch (e) {
|
|
1299
|
+
logger?.warn?.(`[security] claimSeedOwnership scan failed for ${schema.name}`, {
|
|
1300
|
+
error: e.message
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (results.length > 0) {
|
|
1305
|
+
const total = results.reduce((s, r) => s + r.count, 0);
|
|
1306
|
+
logger?.info?.(`[security] handed ${total} seeded record(s) to first admin ${adminUserId}`, {
|
|
1307
|
+
breakdown: results
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
return results;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/bootstrap-platform-admin.ts
|
|
1314
|
+
var SYSTEM_CTX2 = { isSystem: true };
|
|
1217
1315
|
async function tryFind(ql, object, where, limit = 100) {
|
|
1218
1316
|
try {
|
|
1219
|
-
const rows = await ql.find(object, { where, limit }, { context:
|
|
1317
|
+
const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX2 });
|
|
1220
1318
|
return Array.isArray(rows) ? rows : [];
|
|
1221
1319
|
} catch {
|
|
1222
1320
|
return [];
|
|
@@ -1224,7 +1322,7 @@ async function tryFind(ql, object, where, limit = 100) {
|
|
|
1224
1322
|
}
|
|
1225
1323
|
async function tryInsert(ql, object, data) {
|
|
1226
1324
|
try {
|
|
1227
|
-
return await ql.insert(object, data, { context:
|
|
1325
|
+
return await ql.insert(object, data, { context: SYSTEM_CTX2 });
|
|
1228
1326
|
} catch {
|
|
1229
1327
|
return null;
|
|
1230
1328
|
}
|
|
@@ -1252,6 +1350,9 @@ async function bootstrapPlatformAdmin(ql, bootstrapPermissionSets, options = {})
|
|
|
1252
1350
|
id,
|
|
1253
1351
|
name: ps.name,
|
|
1254
1352
|
label: ps.label ?? ps.name,
|
|
1353
|
+
// `description` is not part of the typed PermissionSet shape (name/label
|
|
1354
|
+
// only); read it defensively so a runtime-provided description still
|
|
1355
|
+
// persists without tripping the dts typecheck.
|
|
1255
1356
|
description: ps.description ?? null,
|
|
1256
1357
|
object_permissions: JSON.stringify(ps.objects ?? {}),
|
|
1257
1358
|
field_permissions: JSON.stringify(ps.fields ?? {}),
|
|
@@ -1281,12 +1382,12 @@ async function bootstrapPlatformAdmin(ql, bootstrapPermissionSets, options = {})
|
|
|
1281
1382
|
{ permission_set_id: adminPsId },
|
|
1282
1383
|
50
|
|
1283
1384
|
);
|
|
1284
|
-
if (existingAdminLinks.some((r) => !r.organization_id && r.user_id !==
|
|
1385
|
+
if (existingAdminLinks.some((r) => !r.organization_id && r.user_id !== SystemUserId2.SYSTEM)) {
|
|
1285
1386
|
return { seeded: seededCount, adminPromoted: false, reason: "already_have_admin" };
|
|
1286
1387
|
}
|
|
1287
1388
|
const allUsers = await tryFind(ql, "sys_user", {}, 50);
|
|
1288
1389
|
const humanUsers = allUsers.filter(
|
|
1289
|
-
(u) => u.id !==
|
|
1390
|
+
(u) => u.id !== SystemUserId2.SYSTEM && u.role !== "system"
|
|
1290
1391
|
);
|
|
1291
1392
|
if (humanUsers.length === 0) {
|
|
1292
1393
|
logger?.info?.("[security] no human users yet \u2014 first sign-up will be promoted to platform admin");
|
|
@@ -1310,11 +1411,18 @@ async function bootstrapPlatformAdmin(ql, bootstrapPermissionSets, options = {})
|
|
|
1310
1411
|
return { seeded: seededCount, adminPromoted: false, reason: "insert_failed" };
|
|
1311
1412
|
}
|
|
1312
1413
|
logger?.info?.(`[security] first user promoted to platform admin: ${target.email ?? target.id}`);
|
|
1313
|
-
|
|
1414
|
+
let ownershipClaimed = 0;
|
|
1415
|
+
try {
|
|
1416
|
+
const claims = await claimSeedOwnership(ql, target.id, { logger });
|
|
1417
|
+
ownershipClaimed = claims.reduce((s, c) => s + c.count, 0);
|
|
1418
|
+
} catch (e) {
|
|
1419
|
+
logger?.warn?.("[security] seed ownership handoff failed", { error: e.message });
|
|
1420
|
+
}
|
|
1421
|
+
return { seeded: seededCount, adminPromoted: true, ownershipClaimed };
|
|
1314
1422
|
}
|
|
1315
1423
|
|
|
1316
1424
|
// src/auto-org-admin-grant.ts
|
|
1317
|
-
var
|
|
1425
|
+
var SYSTEM_CTX3 = { isSystem: true };
|
|
1318
1426
|
var PERMISSION_SET_NAME = "organization_admin";
|
|
1319
1427
|
function genId2(prefix) {
|
|
1320
1428
|
const rand = Math.random().toString(36).slice(2, 10);
|
|
@@ -1323,7 +1431,7 @@ function genId2(prefix) {
|
|
|
1323
1431
|
}
|
|
1324
1432
|
async function tryFind2(ql, object, where, limit = 50) {
|
|
1325
1433
|
try {
|
|
1326
|
-
const rows = await ql.find(object, { where, limit }, { context:
|
|
1434
|
+
const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX3 });
|
|
1327
1435
|
return Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
|
|
1328
1436
|
} catch {
|
|
1329
1437
|
return [];
|
|
@@ -1331,14 +1439,14 @@ async function tryFind2(ql, object, where, limit = 50) {
|
|
|
1331
1439
|
}
|
|
1332
1440
|
async function tryInsert2(ql, object, data) {
|
|
1333
1441
|
try {
|
|
1334
|
-
return await ql.insert(object, data, { context:
|
|
1442
|
+
return await ql.insert(object, data, { context: SYSTEM_CTX3 });
|
|
1335
1443
|
} catch {
|
|
1336
1444
|
return null;
|
|
1337
1445
|
}
|
|
1338
1446
|
}
|
|
1339
1447
|
async function tryDelete(ql, object, id) {
|
|
1340
1448
|
try {
|
|
1341
|
-
await ql.delete(object, id, { context:
|
|
1449
|
+
await ql.delete(object, id, { context: SYSTEM_CTX3 });
|
|
1342
1450
|
return true;
|
|
1343
1451
|
} catch {
|
|
1344
1452
|
return false;
|
|
@@ -3070,6 +3178,8 @@ export {
|
|
|
3070
3178
|
SECURITY_PLUGIN_VERSION,
|
|
3071
3179
|
SecurityPlugin,
|
|
3072
3180
|
backfillOrgAdminGrants,
|
|
3181
|
+
bootstrapPlatformAdmin,
|
|
3182
|
+
claimSeedOwnership,
|
|
3073
3183
|
isPermissionDeniedError,
|
|
3074
3184
|
reconcileOrgAdminGrant,
|
|
3075
3185
|
securityDefaultPermissionSets,
|