@objectstack/plugin-reports 16.1.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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +206 -0
- package/dist/index.d.mts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +51 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/report-export-axis.test.ts +220 -0
- package/src/report-service.ts +79 -0
- package/src/reports-plugin.ts +17 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/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
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[
|
|
14
|
-
[
|
|
15
|
-
[
|
|
16
|
-
[
|
|
17
|
-
[
|
|
18
|
-
[
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m24.38 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m51.63 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 92ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m25.50 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m51.63 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 95ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 13639ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m9.09 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m9.09 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,211 @@
|
|
|
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
|
+
|
|
3
209
|
## 16.1.0
|
|
4
210
|
|
|
5
211
|
### 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
|