@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.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
- * Supports simple expressions like:
1094
- * - "field_name = current_user.property"
1095
- * - "field_name IN (current_user.array_property)"
1096
- * - "field_name = 'literal_value'"
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: SYSTEM_CTX });
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: SYSTEM_CTX });
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 !== import_system.SystemUserId.SYSTEM)) {
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 !== import_system.SystemUserId.SYSTEM && u.role !== "system"
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
- return { seeded: seededCount, adminPromoted: true };
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 SYSTEM_CTX2 = { isSystem: true };
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: SYSTEM_CTX2 });
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: SYSTEM_CTX2 });
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: SYSTEM_CTX2 });
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,