@objectstack/plugin-security 9.9.0 → 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.js
CHANGED
|
@@ -915,6 +915,8 @@ __export(index_exports, {
|
|
|
915
915
|
SECURITY_PLUGIN_VERSION: () => SECURITY_PLUGIN_VERSION,
|
|
916
916
|
SecurityPlugin: () => SecurityPlugin,
|
|
917
917
|
backfillOrgAdminGrants: () => backfillOrgAdminGrants,
|
|
918
|
+
bootstrapPlatformAdmin: () => bootstrapPlatformAdmin,
|
|
919
|
+
claimSeedOwnership: () => claimSeedOwnership,
|
|
918
920
|
isPermissionDeniedError: () => isPermissionDeniedError,
|
|
919
921
|
reconcileOrgAdminGrant: () => reconcileOrgAdminGrant,
|
|
920
922
|
securityDefaultPermissionSets: () => securityDefaultPermissionSets,
|
|
@@ -1073,6 +1075,14 @@ var RLSCompiler = class {
|
|
|
1073
1075
|
roles: executionContext?.roles,
|
|
1074
1076
|
org_user_ids: executionContext?.org_user_ids
|
|
1075
1077
|
};
|
|
1078
|
+
const membership = executionContext?.rlsMembership;
|
|
1079
|
+
if (membership && typeof membership === "object") {
|
|
1080
|
+
for (const [key, value] of Object.entries(membership)) {
|
|
1081
|
+
if (Array.isArray(value) && userCtx[key] === void 0) {
|
|
1082
|
+
userCtx[key] = value;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1076
1086
|
const filters = [];
|
|
1077
1087
|
for (const policy of policies) {
|
|
1078
1088
|
if (!policy.using) continue;
|
|
@@ -1089,14 +1099,24 @@ var RLSCompiler = class {
|
|
|
1089
1099
|
}
|
|
1090
1100
|
/**
|
|
1091
1101
|
* Compile a single RLS expression into a query filter.
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
* -
|
|
1096
|
-
* -
|
|
1102
|
+
*
|
|
1103
|
+
* This reference compiler recognizes exactly four forms — anything else
|
|
1104
|
+
* returns `null` and (via {@link compileFilter}) fails closed:
|
|
1105
|
+
* - `field = current_user.property` → `{ field: <value> }`
|
|
1106
|
+
* - `field = 'literal_value'` → `{ field: 'literal_value' }`
|
|
1107
|
+
* - `field IN (current_user.array)` → `{ field: { $in: [...] } }`
|
|
1108
|
+
* (the array may be a §7.3.1 pre-resolved membership set)
|
|
1109
|
+
* - `1 = 1` → `{}` (always-true / no restriction)
|
|
1110
|
+
*
|
|
1111
|
+
* There is intentionally no support for subqueries, `LIKE`/`ILIKE`,
|
|
1112
|
+
* regex, `ANY`/`ALL`, `AND`/`OR`/`NOT`, or `NULL` checks — express those
|
|
1113
|
+
* needs as a `current_user.*` property the runtime pre-resolves instead.
|
|
1097
1114
|
*/
|
|
1098
1115
|
compileExpression(expression, userCtx) {
|
|
1099
1116
|
if (!expression) return null;
|
|
1117
|
+
if (/^\s*1\s*=\s*1\s*$/.test(expression)) {
|
|
1118
|
+
return {};
|
|
1119
|
+
}
|
|
1100
1120
|
const eqMatch = expression.match(/^\s*(\w+)\s*=\s*current_user\.(\w+)\s*$/);
|
|
1101
1121
|
if (eqMatch) {
|
|
1102
1122
|
const [, field, prop] = eqMatch;
|
|
@@ -1244,11 +1264,91 @@ function isPermissionDeniedError(e) {
|
|
|
1244
1264
|
}
|
|
1245
1265
|
|
|
1246
1266
|
// src/bootstrap-platform-admin.ts
|
|
1267
|
+
var import_system2 = require("@objectstack/spec/system");
|
|
1268
|
+
|
|
1269
|
+
// src/claim-seed-ownership.ts
|
|
1247
1270
|
var import_system = require("@objectstack/spec/system");
|
|
1248
1271
|
var SYSTEM_CTX = { isSystem: true };
|
|
1272
|
+
function hasOwnerField(schema) {
|
|
1273
|
+
const fields = schema?.fields;
|
|
1274
|
+
if (!fields) return false;
|
|
1275
|
+
if (Array.isArray(fields)) {
|
|
1276
|
+
return fields.some((f) => f?.name === "owner_id");
|
|
1277
|
+
}
|
|
1278
|
+
return Object.prototype.hasOwnProperty.call(fields, "owner_id");
|
|
1279
|
+
}
|
|
1280
|
+
async function claimSeedOwnership(ql, adminUserId, options = {}) {
|
|
1281
|
+
const logger = options.logger;
|
|
1282
|
+
if (!adminUserId || adminUserId === import_system.SystemUserId.SYSTEM) return [];
|
|
1283
|
+
if (!ql || typeof ql.update !== "function" || typeof ql.find !== "function") {
|
|
1284
|
+
return [];
|
|
1285
|
+
}
|
|
1286
|
+
const registry = ql.registry;
|
|
1287
|
+
if (!registry || typeof registry.getAllObjects !== "function") {
|
|
1288
|
+
logger?.warn?.("[security] claimSeedOwnership: registry unavailable");
|
|
1289
|
+
return [];
|
|
1290
|
+
}
|
|
1291
|
+
const schemas = registry.getAllObjects();
|
|
1292
|
+
const results = [];
|
|
1293
|
+
for (const schema of schemas) {
|
|
1294
|
+
if (!schema?.name) continue;
|
|
1295
|
+
if (schema.managedBy) continue;
|
|
1296
|
+
if (schema.name.startsWith("sys_")) continue;
|
|
1297
|
+
if (!hasOwnerField(schema)) continue;
|
|
1298
|
+
try {
|
|
1299
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1300
|
+
const ids = [];
|
|
1301
|
+
for (const where of [{ owner_id: null }, { owner_id: import_system.SystemUserId.SYSTEM }]) {
|
|
1302
|
+
const rows = await ql.find(
|
|
1303
|
+
schema.name,
|
|
1304
|
+
{ where, limit: 1e4, fields: ["id"] },
|
|
1305
|
+
{ context: SYSTEM_CTX }
|
|
1306
|
+
);
|
|
1307
|
+
const list = Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
|
|
1308
|
+
for (const r of list) {
|
|
1309
|
+
if (r?.id && !seen.has(r.id)) {
|
|
1310
|
+
seen.add(r.id);
|
|
1311
|
+
ids.push(r.id);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
if (ids.length === 0) continue;
|
|
1316
|
+
let updated = 0;
|
|
1317
|
+
for (const id of ids) {
|
|
1318
|
+
try {
|
|
1319
|
+
await ql.update(
|
|
1320
|
+
schema.name,
|
|
1321
|
+
{ id, owner_id: adminUserId },
|
|
1322
|
+
{ context: SYSTEM_CTX }
|
|
1323
|
+
);
|
|
1324
|
+
updated += 1;
|
|
1325
|
+
} catch (e) {
|
|
1326
|
+
logger?.warn?.(`[security] claimSeedOwnership failed for ${schema.name}:${id}`, {
|
|
1327
|
+
error: e.message
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (updated > 0) results.push({ object: schema.name, count: updated });
|
|
1332
|
+
} catch (e) {
|
|
1333
|
+
logger?.warn?.(`[security] claimSeedOwnership scan failed for ${schema.name}`, {
|
|
1334
|
+
error: e.message
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
if (results.length > 0) {
|
|
1339
|
+
const total = results.reduce((s, r) => s + r.count, 0);
|
|
1340
|
+
logger?.info?.(`[security] handed ${total} seeded record(s) to first admin ${adminUserId}`, {
|
|
1341
|
+
breakdown: results
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
return results;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/bootstrap-platform-admin.ts
|
|
1348
|
+
var SYSTEM_CTX2 = { isSystem: true };
|
|
1249
1349
|
async function tryFind(ql, object, where, limit = 100) {
|
|
1250
1350
|
try {
|
|
1251
|
-
const rows = await ql.find(object, { where, limit }, { context:
|
|
1351
|
+
const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX2 });
|
|
1252
1352
|
return Array.isArray(rows) ? rows : [];
|
|
1253
1353
|
} catch {
|
|
1254
1354
|
return [];
|
|
@@ -1256,7 +1356,7 @@ async function tryFind(ql, object, where, limit = 100) {
|
|
|
1256
1356
|
}
|
|
1257
1357
|
async function tryInsert(ql, object, data) {
|
|
1258
1358
|
try {
|
|
1259
|
-
return await ql.insert(object, data, { context:
|
|
1359
|
+
return await ql.insert(object, data, { context: SYSTEM_CTX2 });
|
|
1260
1360
|
} catch {
|
|
1261
1361
|
return null;
|
|
1262
1362
|
}
|
|
@@ -1284,6 +1384,9 @@ async function bootstrapPlatformAdmin(ql, bootstrapPermissionSets, options = {})
|
|
|
1284
1384
|
id,
|
|
1285
1385
|
name: ps.name,
|
|
1286
1386
|
label: ps.label ?? ps.name,
|
|
1387
|
+
// `description` is not part of the typed PermissionSet shape (name/label
|
|
1388
|
+
// only); read it defensively so a runtime-provided description still
|
|
1389
|
+
// persists without tripping the dts typecheck.
|
|
1287
1390
|
description: ps.description ?? null,
|
|
1288
1391
|
object_permissions: JSON.stringify(ps.objects ?? {}),
|
|
1289
1392
|
field_permissions: JSON.stringify(ps.fields ?? {}),
|
|
@@ -1313,12 +1416,12 @@ async function bootstrapPlatformAdmin(ql, bootstrapPermissionSets, options = {})
|
|
|
1313
1416
|
{ permission_set_id: adminPsId },
|
|
1314
1417
|
50
|
|
1315
1418
|
);
|
|
1316
|
-
if (existingAdminLinks.some((r) => !r.organization_id && r.user_id !==
|
|
1419
|
+
if (existingAdminLinks.some((r) => !r.organization_id && r.user_id !== import_system2.SystemUserId.SYSTEM)) {
|
|
1317
1420
|
return { seeded: seededCount, adminPromoted: false, reason: "already_have_admin" };
|
|
1318
1421
|
}
|
|
1319
1422
|
const allUsers = await tryFind(ql, "sys_user", {}, 50);
|
|
1320
1423
|
const humanUsers = allUsers.filter(
|
|
1321
|
-
(u) => u.id !==
|
|
1424
|
+
(u) => u.id !== import_system2.SystemUserId.SYSTEM && u.role !== "system"
|
|
1322
1425
|
);
|
|
1323
1426
|
if (humanUsers.length === 0) {
|
|
1324
1427
|
logger?.info?.("[security] no human users yet \u2014 first sign-up will be promoted to platform admin");
|
|
@@ -1342,11 +1445,18 @@ async function bootstrapPlatformAdmin(ql, bootstrapPermissionSets, options = {})
|
|
|
1342
1445
|
return { seeded: seededCount, adminPromoted: false, reason: "insert_failed" };
|
|
1343
1446
|
}
|
|
1344
1447
|
logger?.info?.(`[security] first user promoted to platform admin: ${target.email ?? target.id}`);
|
|
1345
|
-
|
|
1448
|
+
let ownershipClaimed = 0;
|
|
1449
|
+
try {
|
|
1450
|
+
const claims = await claimSeedOwnership(ql, target.id, { logger });
|
|
1451
|
+
ownershipClaimed = claims.reduce((s, c) => s + c.count, 0);
|
|
1452
|
+
} catch (e) {
|
|
1453
|
+
logger?.warn?.("[security] seed ownership handoff failed", { error: e.message });
|
|
1454
|
+
}
|
|
1455
|
+
return { seeded: seededCount, adminPromoted: true, ownershipClaimed };
|
|
1346
1456
|
}
|
|
1347
1457
|
|
|
1348
1458
|
// src/auto-org-admin-grant.ts
|
|
1349
|
-
var
|
|
1459
|
+
var SYSTEM_CTX3 = { isSystem: true };
|
|
1350
1460
|
var PERMISSION_SET_NAME = "organization_admin";
|
|
1351
1461
|
function genId2(prefix) {
|
|
1352
1462
|
const rand = Math.random().toString(36).slice(2, 10);
|
|
@@ -1355,7 +1465,7 @@ function genId2(prefix) {
|
|
|
1355
1465
|
}
|
|
1356
1466
|
async function tryFind2(ql, object, where, limit = 50) {
|
|
1357
1467
|
try {
|
|
1358
|
-
const rows = await ql.find(object, { where, limit }, { context:
|
|
1468
|
+
const rows = await ql.find(object, { where, limit }, { context: SYSTEM_CTX3 });
|
|
1359
1469
|
return Array.isArray(rows) ? rows : Array.isArray(rows?.records) ? rows.records : [];
|
|
1360
1470
|
} catch {
|
|
1361
1471
|
return [];
|
|
@@ -1363,14 +1473,14 @@ async function tryFind2(ql, object, where, limit = 50) {
|
|
|
1363
1473
|
}
|
|
1364
1474
|
async function tryInsert2(ql, object, data) {
|
|
1365
1475
|
try {
|
|
1366
|
-
return await ql.insert(object, data, { context:
|
|
1476
|
+
return await ql.insert(object, data, { context: SYSTEM_CTX3 });
|
|
1367
1477
|
} catch {
|
|
1368
1478
|
return null;
|
|
1369
1479
|
}
|
|
1370
1480
|
}
|
|
1371
1481
|
async function tryDelete(ql, object, id) {
|
|
1372
1482
|
try {
|
|
1373
|
-
await ql.delete(object, id, { context:
|
|
1483
|
+
await ql.delete(object, id, { context: SYSTEM_CTX3 });
|
|
1374
1484
|
return true;
|
|
1375
1485
|
} catch {
|
|
1376
1486
|
return false;
|
|
@@ -3103,6 +3213,8 @@ var SecurityPlugin = class {
|
|
|
3103
3213
|
SECURITY_PLUGIN_VERSION,
|
|
3104
3214
|
SecurityPlugin,
|
|
3105
3215
|
backfillOrgAdminGrants,
|
|
3216
|
+
bootstrapPlatformAdmin,
|
|
3217
|
+
claimSeedOwnership,
|
|
3106
3218
|
isPermissionDeniedError,
|
|
3107
3219
|
reconcileOrgAdminGrant,
|
|
3108
3220
|
securityDefaultPermissionSets,
|