@objectstack/plugin-security 9.7.0 → 9.9.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
@@ -2293,17 +2293,29 @@ var defaultPermissionSets = [
2293
2293
  operation: "all",
2294
2294
  using: "organization_id = current_user.organization_id"
2295
2295
  },
2296
+ // Owner-scoped writes/deletes for rank-and-file members: you may modify
2297
+ // and delete the records you created, not other users'. Keyed on
2298
+ // `created_by` — the column the engine stamps on EVERY record — rather
2299
+ // than `owner_id`, which author-defined objects almost never declare. The
2300
+ // old `owner_id` key referenced a missing column on real objects, so
2301
+ // `computeRlsFilter` dropped the policy and the scoping silently no-op'd
2302
+ // (any member could edit/delete any record — #1985). These policies are
2303
+ // ENFORCED on writes via the security middleware's pre-image check (a
2304
+ // by-id update/delete never builds an RLS `where`, so the predicate is
2305
+ // verified against the target row before the mutation). Objects that
2306
+ // model transferable ownership with a dedicated owner field should
2307
+ // override these with a per-object policy.
2296
2308
  {
2297
2309
  name: "owner_only_writes",
2298
2310
  object: "*",
2299
2311
  operation: "update",
2300
- using: "owner_id = current_user.id"
2312
+ using: "created_by = current_user.id"
2301
2313
  },
2302
2314
  {
2303
2315
  name: "owner_only_deletes",
2304
2316
  object: "*",
2305
2317
  operation: "delete",
2306
- using: "owner_id = current_user.id"
2318
+ using: "created_by = current_user.id"
2307
2319
  },
2308
2320
  // ── better-auth system tables that lack `organization_id` and would
2309
2321
  // otherwise be left unprotected by the wildcard rule above. ────
@@ -2735,6 +2747,40 @@ var SecurityPlugin = class {
2735
2747
  );
2736
2748
  }
2737
2749
  }
2750
+ if ((opCtx.operation === "update" || opCtx.operation === "delete") && permissionSets.length > 0 && !!opCtx.context?.userId && this.ql) {
2751
+ const targetId = this.extractSingleId(opCtx);
2752
+ if (targetId != null) {
2753
+ const writeFilter = await this.computeRlsFilter(
2754
+ permissionSets,
2755
+ opCtx.object,
2756
+ opCtx.operation,
2757
+ opCtx.context
2758
+ );
2759
+ if (writeFilter) {
2760
+ let visible = null;
2761
+ try {
2762
+ visible = await this.ql.findOne(opCtx.object, {
2763
+ where: { $and: [{ id: targetId }, writeFilter] },
2764
+ context: opCtx.context
2765
+ });
2766
+ } catch {
2767
+ visible = null;
2768
+ }
2769
+ if (!visible) {
2770
+ throw new PermissionDeniedError(
2771
+ `[Security] Access denied: not permitted to ${opCtx.operation} this '${opCtx.object}' record (row-level security)`,
2772
+ {
2773
+ operation: opCtx.operation,
2774
+ object: opCtx.object,
2775
+ roles,
2776
+ permissionSets: explicitPermissionSets,
2777
+ recordId: targetId
2778
+ }
2779
+ );
2780
+ }
2781
+ }
2782
+ }
2783
+ }
2738
2784
  if ((opCtx.operation === "insert" || opCtx.operation === "update") && opCtx.data && permissionSets.length > 0) {
2739
2785
  const fieldPerms = this.permissionEvaluator.getFieldPermissions(
2740
2786
  opCtx.object,
@@ -2926,6 +2972,26 @@ var SecurityPlugin = class {
2926
2972
  }
2927
2973
  return permissionSets;
2928
2974
  }
2975
+ /**
2976
+ * Resolve a single scalar primary-key id from an update/delete operation
2977
+ * context, mirroring the engine's "single-id vs predicate" rule
2978
+ * (`engine.ts` update/delete): only a scalar `data.id` or `where.id`
2979
+ * identifies one row. An operator object (`{ $in: [...] }`, …) is a
2980
+ * multi-row predicate and returns `null` (multi-row writes route through the
2981
+ * `*Many` paths, out of scope for the by-id pre-image check).
2982
+ */
2983
+ extractSingleId(opCtx) {
2984
+ const isScalar = (v) => v !== null && (typeof v === "string" || typeof v === "number" || typeof v === "bigint");
2985
+ const data = opCtx?.data;
2986
+ if (data && typeof data === "object" && !Array.isArray(data) && isScalar(data.id)) {
2987
+ return data.id;
2988
+ }
2989
+ const where = opCtx?.options?.where;
2990
+ if (where && typeof where === "object" && "id" in where && isScalar(where.id)) {
2991
+ return where.id;
2992
+ }
2993
+ return null;
2994
+ }
2929
2995
  /**
2930
2996
  * Compile the applicable RLS policies for (object, operation) into a single
2931
2997
  * `FilterCondition`, applying the field-existence safety net (wildcard