@objectstack/plugin-reports 16.0.0 → 17.0.0-rc.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.
@@ -1,22 +1,22 @@
1
1
 
2
- > @objectstack/plugin-reports@16.0.0 build /home/runner/work/framework/framework/packages/plugins/plugin-reports
2
+ > @objectstack/plugin-reports@17.0.0-rc.0 build /home/runner/work/objectstack/objectstack/packages/plugins/plugin-reports
3
3
  > tsup --config ../../../tsup.config.ts
4
4
 
5
5
  CLI Building entry: src/index.ts
6
6
  CLI Using tsconfig: tsconfig.json
7
7
  CLI tsup v8.5.1
8
- CLI Using tsup config: /home/runner/work/framework/framework/tsup.config.ts
8
+ CLI Using tsup config: /home/runner/work/objectstack/objectstack/tsup.config.ts
9
9
  CLI Target: es2020
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- ESM dist/index.mjs 22.17 KB
14
- ESM dist/index.mjs.map 46.00 KB
15
- ESM ⚡️ Build success in 64ms
16
- CJS dist/index.js 23.30 KB
17
- CJS dist/index.js.map 46.00 KB
18
- CJS ⚡️ Build success in 64ms
13
+ ESM dist/index.mjs 24.38 KB
14
+ ESM dist/index.mjs.map 51.63 KB
15
+ ESM ⚡️ Build success in 92ms
16
+ CJS dist/index.js 25.50 KB
17
+ CJS dist/index.js.map 51.63 KB
18
+ CJS ⚡️ Build success in 95ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 17003ms
21
- DTS dist/index.d.mts 7.17 KB
22
- DTS dist/index.d.ts 7.17 KB
20
+ DTS ⚡️ Build success in 13639ms
21
+ DTS dist/index.d.mts 9.09 KB
22
+ DTS dist/index.d.ts 9.09 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,223 @@
1
1
  # @objectstack/plugin-reports
2
2
 
3
+ ## 17.0.0-rc.0
4
+
5
+ ### Major Changes
6
+
7
+ - 4ed7ed4: feat(security)!: the export axis is now OPT-IN, explainable, and covers reports (#3544, #3710)
8
+
9
+ **BREAKING — `allowExport` unset no longer means "inherit read".** Reading a
10
+ record and taking a bulk machine-readable copy of the whole table are different
11
+ privileges (Salesforce "Export Reports", Dynamics "Export to Excel", NetSuite
12
+ "Export Lists", SAP `S_GUI` 61 all separate them). The axis now says so.
13
+
14
+ ### Migration — FROM → TO
15
+
16
+ | | before | after |
17
+ | -------------------- | ----------------------------------- | -------------------------- |
18
+ | `allowExport` unset | export **allowed** (inherited read) | export **denied** |
19
+ | `allowExport: false` | export denied | export denied (unchanged) |
20
+ | `allowExport: true` | export allowed | export allowed (unchanged) |
21
+
22
+ **The one-line fix:** add `allowExport: true` to the object entry (or the `'*'`
23
+ wildcard) of every permission set whose holders should keep exporting.
24
+
25
+ ```ts
26
+ objects: {
27
+ deal: { allowRead: true, allowExport: true }, // ← add the grant
28
+ }
29
+ ```
30
+
31
+ Nothing else changes: read, CRUD, RLS, FLS and sharing are untouched, and a set
32
+ that never exported is unaffected.
33
+
34
+ **Who is affected.** Package-shipped sets are re-seeded on upgrade, so the
35
+ built-ins are handled for you — `admin_full_access` and `organization_admin` now
36
+ carry `allowExport: true` explicitly. **Environment-authored sets are not**: any
37
+ custom set whose users export must be edited. `member_default` deliberately does
38
+ NOT carry the grant, so ordinary authenticated users lose export until an admin
39
+ grants it — that is the point of the flip, not an oversight.
40
+
41
+ **Merge semantics.** Most-permissive, exactly like the CRUD bits: any set
42
+ granting `true` grants export. `false` and unset are the same outcome; `false`
43
+ is authoring intent, not a veto, because permission sets are additive capability
44
+ containers (ADR-0090).
45
+
46
+ **Not implied by super-user bits.** `viewAllRecords` / `modifyAllRecords` no
47
+ longer confer export. Separating "may see all data" from "may take a bulk copy"
48
+ is the segregation-of-duties case the axis exists for.
49
+
50
+ ### Also in this change
51
+
52
+ - **spec** — a set carrying `allowExport` is now **high-privilege**
53
+ (`describeHighPrivilegeBits`), so it cannot be bound to the `everyone` /
54
+ `guest` audience anchors. Without this the opt-in was defeatable by binding an
55
+ export-granting set to `everyone`. One predicate, so the runtime anchor gate,
56
+ the `@objectstack/lint` security-posture rule and the install-time suggestion
57
+ surface all pick it up together.
58
+ - **spec / plugin-security** — `ExplainOperationSchema` gains `export`, so
59
+ `explain` can answer _why_ a caller got `403 EXPORT_NOT_PERMITTED`. It
60
+ explains as `read ∧ the export grant`: `object_crud` reports the conjunction
61
+ and attributes the granting set, while every data-shaped layer
62
+ (requiredPermissions, OWD/depth/sharing, RLS, record attribution) is computed
63
+ as the `find` the export actually performs — asking the RLS compiler about an
64
+ `export` operation would match no policy and wrongly report "no RLS applies".
65
+ `readFilter` is surfaced for `export` as it is for `read`.
66
+ - **plugin-reports** — closes the reports side door (#3710). A report rendered
67
+ as `csv`/`json` is the same bulk copy of the same object, so it is gated by
68
+ the same `ISecurityService.canExport`. Enforced in `executeReport`, which the
69
+ interactive run, the ad-hoc run and the scheduled dispatch all funnel through;
70
+ `scheduleReport` additionally refuses at create time so an author is not told
71
+ at 3am. A schedule created while granted stops delivering once the grant is
72
+ revoked. `html_table` stays a read — it is a rendered view, not a bulk copy.
73
+ Deployments without `plugin-security` are unaffected (no permission sets
74
+ exist, so the axis does not apply).
75
+
76
+ ### Patch Changes
77
+
78
+ - Updated dependencies [50616d9]
79
+ - Updated dependencies [08b5a3d]
80
+ - Updated dependencies [d99aeb3]
81
+ - Updated dependencies [4727eb8]
82
+ - Updated dependencies [f63cd09]
83
+ - Updated dependencies [fa3d0cf]
84
+ - Updated dependencies [af5a224]
85
+ - Updated dependencies [71f76e1]
86
+ - Updated dependencies [37b1346]
87
+ - Updated dependencies [99736a0]
88
+ - Updated dependencies [fe67e34]
89
+ - Updated dependencies [fdb4f50]
90
+ - Updated dependencies [1bd5652]
91
+ - Updated dependencies [14252d3]
92
+ - Updated dependencies [7fb436c]
93
+ - Updated dependencies [879ea13]
94
+ - Updated dependencies [201b31f]
95
+ - Updated dependencies [e2616e0]
96
+ - Updated dependencies [6fdc5c6]
97
+ - Updated dependencies [8b9d71e]
98
+ - Updated dependencies [33f5e23]
99
+ - Updated dependencies [259af21]
100
+ - Updated dependencies [587fc91]
101
+ - Updated dependencies [1986594]
102
+ - Updated dependencies [ad4af62]
103
+ - Updated dependencies [d44dbfa]
104
+ - Updated dependencies [474fe39]
105
+ - Updated dependencies [0bc685a]
106
+ - Updated dependencies [b949059]
107
+ - Updated dependencies [be1c52c]
108
+ - Updated dependencies [c5ff96d]
109
+ - Updated dependencies [84e7be9]
110
+ - Updated dependencies [a6c3f38]
111
+ - Updated dependencies [debc23a]
112
+ - Updated dependencies [0f8ad09]
113
+ - Updated dependencies [8f9689f]
114
+ - Updated dependencies [57a3bb3]
115
+ - Updated dependencies [5f9a987]
116
+ - Updated dependencies [9f060e5]
117
+ - Updated dependencies [bc17d39]
118
+ - Updated dependencies [db02d47]
119
+ - Updated dependencies [0bfdf46]
120
+ - Updated dependencies [376a061]
121
+ - Updated dependencies [7c7e246]
122
+ - Updated dependencies [f35cdc5]
123
+ - Updated dependencies [9ea2bc5]
124
+ - Updated dependencies [c2d9098]
125
+ - Updated dependencies [a227ed7]
126
+ - Updated dependencies [9613396]
127
+ - Updated dependencies [e47b342]
128
+ - Updated dependencies [4ed7ed4]
129
+ - Updated dependencies [2fa4ca1]
130
+ - Updated dependencies [f5a2320]
131
+ - Updated dependencies [deb538f]
132
+ - Updated dependencies [5b89711]
133
+ - Updated dependencies [0c8a22f]
134
+ - Updated dependencies [763931e]
135
+ - Updated dependencies [de9af8a]
136
+ - Updated dependencies [c4df271]
137
+ - Updated dependencies [a41ba5c]
138
+ - Updated dependencies [189854c]
139
+ - Updated dependencies [0e3a226]
140
+ - Updated dependencies [524151c]
141
+ - Updated dependencies [1d4756e]
142
+ - Updated dependencies [720c5ad]
143
+ - Updated dependencies [a8d1e24]
144
+ - Updated dependencies [d1cabaa]
145
+ - Updated dependencies [41642b0]
146
+ - Updated dependencies [4cca74c]
147
+ - Updated dependencies [88ef03e]
148
+ - Updated dependencies [9e2caf3]
149
+ - Updated dependencies [81ce41a]
150
+ - Updated dependencies [85e1e4e]
151
+ - Updated dependencies [dac6a08]
152
+ - Updated dependencies [394b7a1]
153
+ - Updated dependencies [677b591]
154
+ - Updated dependencies [d77d1b7]
155
+ - Updated dependencies [5b79a34]
156
+ - Updated dependencies [c757854]
157
+ - Updated dependencies [0045682]
158
+ - Updated dependencies [2a5f04a]
159
+ - Updated dependencies [4f740b0]
160
+ - Updated dependencies [67452d1]
161
+ - Updated dependencies [4921a95]
162
+ - Updated dependencies [0fc6219]
163
+ - Updated dependencies [605e190]
164
+ - Updated dependencies [c6c59f1]
165
+ - Updated dependencies [b0e78a8]
166
+ - Updated dependencies [f31cc8d]
167
+ - Updated dependencies [f343dc4]
168
+ - Updated dependencies [8269e32]
169
+ - Updated dependencies [74f7339]
170
+ - Updated dependencies [a6c35a2]
171
+ - Updated dependencies [c2f1002]
172
+ - Updated dependencies [f163028]
173
+ - Updated dependencies [f07808c]
174
+ - Updated dependencies [7ffc3d3]
175
+ - Updated dependencies [88346ba]
176
+ - Updated dependencies [4631592]
177
+ - Updated dependencies [32ff033]
178
+ - Updated dependencies [5ac93d4]
179
+ - Updated dependencies [93f267f]
180
+ - Updated dependencies [0024abf]
181
+ - Updated dependencies [acbf364]
182
+ - Updated dependencies [5487c20]
183
+ - Updated dependencies [aa8b847]
184
+ - Updated dependencies [7687f7b]
185
+ - Updated dependencies [1659072]
186
+ - Updated dependencies [abceb0d]
187
+ - Updated dependencies [0c302a7]
188
+ - Updated dependencies [6633337]
189
+ - Updated dependencies [f00d8d4]
190
+ - Updated dependencies [503be86]
191
+ - Updated dependencies [cde1975]
192
+ - Updated dependencies [0bc685a]
193
+ - Updated dependencies [11949fc]
194
+ - Updated dependencies [b098b0e]
195
+ - Updated dependencies [4d00b13]
196
+ - Updated dependencies [9aa5510]
197
+ - Updated dependencies [57bab76]
198
+ - Updated dependencies [b90086a]
199
+ - Updated dependencies [b95577a]
200
+ - Updated dependencies [83c161f]
201
+ - Updated dependencies [d8c4957]
202
+ - Updated dependencies [f24cb83]
203
+ - Updated dependencies [5dbbb92]
204
+ - Updated dependencies [69f1dfd]
205
+ - @objectstack/spec@17.0.0-rc.0
206
+ - @objectstack/platform-objects@17.0.0-rc.0
207
+ - @objectstack/core@17.0.0-rc.0
208
+
209
+ ## 16.1.0
210
+
211
+ ### Patch Changes
212
+
213
+ - Updated dependencies [212b66a]
214
+ - Updated dependencies [d10c4dc]
215
+ - Updated dependencies [9e45b63]
216
+ - Updated dependencies [b20201f]
217
+ - @objectstack/platform-objects@16.1.0
218
+ - @objectstack/spec@16.1.0
219
+ - @objectstack/core@16.1.0
220
+
3
221
  ## 16.0.0
4
222
 
5
223
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -68,6 +68,21 @@ interface ReportServiceOptions {
68
68
  * closed instead of running with RLS bypassed (#2980).
69
69
  */
70
70
  resolveOwnerContext?: OwnerContextResolver;
71
+ /**
72
+ * [#3544 / #3710] The user-level export axis —
73
+ * `ISecurityService.canExport(object, context)`, wired by the reports plugin
74
+ * from `getService('security')`.
75
+ *
76
+ * A report rendered as `csv`/`json` IS a bulk machine-readable copy of the
77
+ * object, so it is the same privilege `GET /data/:object/export` gates.
78
+ * Without this the axis had a side door: a caller refused at that route could
79
+ * save a report on the same object, run it as CSV — or schedule one to their
80
+ * own inbox — and receive the identical rows.
81
+ *
82
+ * Omitted (no `plugin-security`, so no permission sets exist anywhere) → the
83
+ * axis does not apply, matching the REST export route's own fail-open.
84
+ */
85
+ canExport?: (object: string, context: unknown) => Promise<boolean>;
71
86
  }
72
87
  declare class ReportService implements IReportService {
73
88
  private readonly engine;
@@ -76,7 +91,28 @@ declare class ReportService implements IReportService {
76
91
  private readonly logger;
77
92
  private readonly maxRows;
78
93
  private readonly resolveOwnerContext?;
94
+ private readonly canExportFn?;
79
95
  constructor(opts: ReportServiceOptions);
96
+ /**
97
+ * [#3544 / #3710] Gate a report rendering on the user-level export axis.
98
+ *
99
+ * Throws `EXPORT_NOT_PERMITTED` when the principal behind `context` may not
100
+ * take a bulk copy of `object`. A no-op for non-bulk formats (`html_table`),
101
+ * for a system context, and when no `canExport` is wired.
102
+ *
103
+ * Deliberately checked HERE — one place — rather than at each of the three
104
+ * callers (`runReport`, the ad-hoc run, and the scheduled dispatch): a gate
105
+ * per call site is how a fourth call site later ships ungated. `dispatchDue`
106
+ * routes through `executeReport` too, so the scheduled CSV is covered by the
107
+ * same line. (`scheduleReport` additionally pre-checks, so an author is
108
+ * refused when they create the schedule rather than silently at 3am — but
109
+ * that is UX, and THIS is the enforcement: a grant revoked after the schedule
110
+ * was created must still stop the delivery.)
111
+ *
112
+ * Fails CLOSED on a throw — it resolves permission sets to decide, and a
113
+ * resolution failure must never read as a grant (ADR-0049).
114
+ */
115
+ private assertExportAllowed;
80
116
  /**
81
117
  * Authorization for a saved-report row. `sys_saved_report` is a
82
118
  * protection-locked system object, so its rows are *read* with
package/dist/index.d.ts CHANGED
@@ -68,6 +68,21 @@ interface ReportServiceOptions {
68
68
  * closed instead of running with RLS bypassed (#2980).
69
69
  */
70
70
  resolveOwnerContext?: OwnerContextResolver;
71
+ /**
72
+ * [#3544 / #3710] The user-level export axis —
73
+ * `ISecurityService.canExport(object, context)`, wired by the reports plugin
74
+ * from `getService('security')`.
75
+ *
76
+ * A report rendered as `csv`/`json` IS a bulk machine-readable copy of the
77
+ * object, so it is the same privilege `GET /data/:object/export` gates.
78
+ * Without this the axis had a side door: a caller refused at that route could
79
+ * save a report on the same object, run it as CSV — or schedule one to their
80
+ * own inbox — and receive the identical rows.
81
+ *
82
+ * Omitted (no `plugin-security`, so no permission sets exist anywhere) → the
83
+ * axis does not apply, matching the REST export route's own fail-open.
84
+ */
85
+ canExport?: (object: string, context: unknown) => Promise<boolean>;
71
86
  }
72
87
  declare class ReportService implements IReportService {
73
88
  private readonly engine;
@@ -76,7 +91,28 @@ declare class ReportService implements IReportService {
76
91
  private readonly logger;
77
92
  private readonly maxRows;
78
93
  private readonly resolveOwnerContext?;
94
+ private readonly canExportFn?;
79
95
  constructor(opts: ReportServiceOptions);
96
+ /**
97
+ * [#3544 / #3710] Gate a report rendering on the user-level export axis.
98
+ *
99
+ * Throws `EXPORT_NOT_PERMITTED` when the principal behind `context` may not
100
+ * take a bulk copy of `object`. A no-op for non-bulk formats (`html_table`),
101
+ * for a system context, and when no `canExport` is wired.
102
+ *
103
+ * Deliberately checked HERE — one place — rather than at each of the three
104
+ * callers (`runReport`, the ad-hoc run, and the scheduled dispatch): a gate
105
+ * per call site is how a fourth call site later ships ungated. `dispatchDue`
106
+ * routes through `executeReport` too, so the scheduled CSV is covered by the
107
+ * same line. (`scheduleReport` additionally pre-checks, so an author is
108
+ * refused when they create the schedule rather than silently at 3am — but
109
+ * that is UX, and THIS is the enforcement: a grant revoked after the schedule
110
+ * was created must still stop the delivery.)
111
+ *
112
+ * Fails CLOSED on a throw — it resolves permission sets to decide, and a
113
+ * resolution failure must never read as a grant (ADR-0049).
114
+ */
115
+ private assertExportAllowed;
80
116
  /**
81
117
  * Authorization for a saved-report row. `sys_saved_report` is a
82
118
  * protection-locked system object, so its rows are *read* with
package/dist/index.js CHANGED
@@ -147,6 +147,7 @@ function renderSubject(template, vars) {
147
147
  const tpl = template ?? "{{name}} \u2014 {{date}}";
148
148
  return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_m, k) => vars[String(k)] ?? "");
149
149
  }
150
+ var BULK_EXPORT_FORMATS = /* @__PURE__ */ new Set(["csv", "json"]);
150
151
  var ReportService = class {
151
152
  constructor(opts) {
152
153
  this.engine = opts.engine;
@@ -155,6 +156,43 @@ var ReportService = class {
155
156
  this.logger = opts.logger ?? {};
156
157
  this.maxRows = Math.max(1, opts.maxRows ?? 5e3);
157
158
  this.resolveOwnerContext = opts.resolveOwnerContext;
159
+ this.canExportFn = opts.canExport;
160
+ }
161
+ /**
162
+ * [#3544 / #3710] Gate a report rendering on the user-level export axis.
163
+ *
164
+ * Throws `EXPORT_NOT_PERMITTED` when the principal behind `context` may not
165
+ * take a bulk copy of `object`. A no-op for non-bulk formats (`html_table`),
166
+ * for a system context, and when no `canExport` is wired.
167
+ *
168
+ * Deliberately checked HERE — one place — rather than at each of the three
169
+ * callers (`runReport`, the ad-hoc run, and the scheduled dispatch): a gate
170
+ * per call site is how a fourth call site later ships ungated. `dispatchDue`
171
+ * routes through `executeReport` too, so the scheduled CSV is covered by the
172
+ * same line. (`scheduleReport` additionally pre-checks, so an author is
173
+ * refused when they create the schedule rather than silently at 3am — but
174
+ * that is UX, and THIS is the enforcement: a grant revoked after the schedule
175
+ * was created must still stop the delivery.)
176
+ *
177
+ * Fails CLOSED on a throw — it resolves permission sets to decide, and a
178
+ * resolution failure must never read as a grant (ADR-0049).
179
+ */
180
+ async assertExportAllowed(object, format, context) {
181
+ if (!BULK_EXPORT_FORMATS.has(format)) return;
182
+ if (context?.isSystem) return;
183
+ if (!this.canExportFn) return;
184
+ let allowed;
185
+ try {
186
+ allowed = await this.canExportFn(object, context);
187
+ } catch (err) {
188
+ this.logger.warn?.("ReportService: canExport check failed \u2014 denying export", err);
189
+ allowed = false;
190
+ }
191
+ if (!allowed) {
192
+ throw new Error(
193
+ `EXPORT_NOT_PERMITTED: exporting '${object}' as ${format} is not permitted for this user`
194
+ );
195
+ }
158
196
  }
159
197
  // ── Access control ─────────────────────────────────────────────
160
198
  /**
@@ -277,6 +315,7 @@ var ReportService = class {
277
315
  );
278
316
  }
279
317
  async executeReport(report, context, stamp = true) {
318
+ await this.assertExportAllowed(report.object_name, report.format, context);
280
319
  const q = report.query ?? {};
281
320
  const limit = Math.min(q.limit ?? DEFAULT_LIMIT, this.maxRows);
282
321
  const rows = await this.engine.find(report.object_name, {
@@ -327,6 +366,7 @@ var ReportService = class {
327
366
  }
328
367
  const report = await this.getReport(input.reportId, context);
329
368
  if (!report) throw new Error(`REPORT_NOT_FOUND: ${input.reportId}`);
369
+ await this.assertExportAllowed(report.object_name, input.format ?? "html_table", context);
330
370
  const now = this.clock.now();
331
371
  const interval = input.intervalMinutes ?? DEFAULT_INTERVAL_MIN;
332
372
  const cron = input.cronExpression?.trim() || null;
@@ -555,11 +595,22 @@ var ReportsServicePlugin = class {
555
595
  if (!email) {
556
596
  ctx.logger.warn("ReportsServicePlugin: no email service \u2014 schedules will fire without delivery");
557
597
  }
598
+ const canExport = async (object, context) => {
599
+ let security;
600
+ try {
601
+ security = ctx.getService("security");
602
+ } catch {
603
+ return true;
604
+ }
605
+ if (!security || typeof security.canExport !== "function") return true;
606
+ return await security.canExport(object, context);
607
+ };
558
608
  this.service = new ReportService({
559
609
  engine,
560
610
  email,
561
611
  logger: ctx.logger,
562
612
  maxRows: this.options.maxRows,
613
+ canExport,
563
614
  // Scheduled reports run under the owner's resolved RLS context, not a
564
615
  // system bypass (#2980). No owner-context resolver is wired yet — that
565
616
  // is the reports-surface consumer of ADR-0073's user-less identity