@objectstack/plugin-reports 14.8.0 → 15.1.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 +93 -0
- package/dist/index.d.mts +34 -4
- package/dist/index.d.ts +34 -4
- package/dist/index.js +78 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +78 -22
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/report-service.test.ts +99 -4
- package/src/report-service.ts +118 -17
- package/src/reports-plugin.ts +7 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/plugin-reports@
|
|
2
|
+
> @objectstack/plugin-reports@15.1.0 build /home/runner/work/framework/framework/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[32m22.17 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m46.00 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 165ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m23.30 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m46.00 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 171ms
|
|
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 19372ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m7.17 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m7.17 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,98 @@
|
|
|
1
1
|
# @objectstack/plugin-reports
|
|
2
2
|
|
|
3
|
+
## 15.1.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f531a26: fix(security): close three execution-surface authz holes surfaced by the #2849 class sweep (#2980, #2981, #2982)
|
|
8
|
+
|
|
9
|
+
Three independent, confirmed-exploitable defects where an execution surface
|
|
10
|
+
ignored the caller's identity or fell open on a missing one. Each is fixed at
|
|
11
|
+
its own enforcement point; none change behaviour for correctly-scoped callers.
|
|
12
|
+
|
|
13
|
+
- **#2980 — reports IDOR + scheduled-report RLS bypass.** `ReportService`
|
|
14
|
+
discarded the caller's context and read/wrote `sys_saved_report` with a system
|
|
15
|
+
context, so any authenticated user could read, delete, or overwrite any saved
|
|
16
|
+
report by id (cross-owner / cross-tenant), and `listReports` enumerated all
|
|
17
|
+
owners. `getReport`/`deleteReport`/`saveReport`/`listReports` are now
|
|
18
|
+
owner-scoped (system read of the protection-locked metadata object, but
|
|
19
|
+
authorization enforced by owner match); create/overwrite can no longer spoof
|
|
20
|
+
ownership. Scheduled dispatch no longer runs `isSystem` (which emailed the
|
|
21
|
+
target object's entire table past the owner's RLS): it resolves the owner to a
|
|
22
|
+
real RLS-bearing context via a new `resolveOwnerContext` seam and **fails
|
|
23
|
+
closed** (skips + marks the schedule failed) when the owner can't be resolved,
|
|
24
|
+
rather than running elevated. Wiring that resolver is the reports-surface
|
|
25
|
+
consumer of ADR-0073's user-less identity resolution.
|
|
26
|
+
|
|
27
|
+
- **#2981 — knowledge/RAG retrieval fall-open.** `applyPermissionFilter` returned
|
|
28
|
+
every hit when the context was missing _or_ system. A missing identity is no
|
|
29
|
+
longer treated as a grant: object-backed hits fail closed (dropped, keeping
|
|
30
|
+
ACL-less file/http hits), and only an **explicit** system context passes
|
|
31
|
+
through. Closes the agent path where an omitted `ToolExecutionContext.actor`
|
|
32
|
+
yielded unfiltered semantic search over the whole corpus.
|
|
33
|
+
|
|
34
|
+
- **#2982 — bulk-write OWD gap.** `update({multi:true})` / `deleteMany` had no
|
|
35
|
+
single id to `canEdit`-gate, so owner scoping was skipped on private (and
|
|
36
|
+
public_read) objects. A new `SharingService.buildWriteFilter` (the edit-set
|
|
37
|
+
analogue of `buildReadFilter`) is AND-ed into the write AST for multi writes,
|
|
38
|
+
constraining them to rows the caller may edit — including the on-behalf-of
|
|
39
|
+
delegator intersection.
|
|
40
|
+
|
|
41
|
+
Tracked as the motivating evidence of ADR-0096 (execution-surface identity
|
|
42
|
+
admission); the mechanism that would prevent the class structurally is separate.
|
|
43
|
+
|
|
44
|
+
- Updated dependencies [f531a26]
|
|
45
|
+
- Updated dependencies [f531a26]
|
|
46
|
+
- Updated dependencies [f531a26]
|
|
47
|
+
- Updated dependencies [f531a26]
|
|
48
|
+
- Updated dependencies [f531a26]
|
|
49
|
+
- Updated dependencies [f531a26]
|
|
50
|
+
- Updated dependencies [3fe9df1]
|
|
51
|
+
- Updated dependencies [f531a26]
|
|
52
|
+
- Updated dependencies [f531a26]
|
|
53
|
+
- Updated dependencies [f531a26]
|
|
54
|
+
- Updated dependencies [f531a26]
|
|
55
|
+
- Updated dependencies [f531a26]
|
|
56
|
+
- Updated dependencies [f531a26]
|
|
57
|
+
- Updated dependencies [f531a26]
|
|
58
|
+
- Updated dependencies [f531a26]
|
|
59
|
+
- Updated dependencies [f531a26]
|
|
60
|
+
- Updated dependencies [f531a26]
|
|
61
|
+
- Updated dependencies [f531a26]
|
|
62
|
+
- Updated dependencies [f531a26]
|
|
63
|
+
- Updated dependencies [f531a26]
|
|
64
|
+
- Updated dependencies [f531a26]
|
|
65
|
+
- Updated dependencies [f531a26]
|
|
66
|
+
- Updated dependencies [4109153]
|
|
67
|
+
- Updated dependencies [f531a26]
|
|
68
|
+
- Updated dependencies [f531a26]
|
|
69
|
+
- Updated dependencies [f531a26]
|
|
70
|
+
- Updated dependencies [f531a26]
|
|
71
|
+
- Updated dependencies [f531a26]
|
|
72
|
+
- Updated dependencies [f531a26]
|
|
73
|
+
- Updated dependencies [f531a26]
|
|
74
|
+
- Updated dependencies [627f225]
|
|
75
|
+
- Updated dependencies [f531a26]
|
|
76
|
+
- Updated dependencies [f531a26]
|
|
77
|
+
- Updated dependencies [f531a26]
|
|
78
|
+
- @objectstack/spec@15.1.0
|
|
79
|
+
- @objectstack/platform-objects@15.1.0
|
|
80
|
+
- @objectstack/core@15.1.0
|
|
81
|
+
|
|
82
|
+
## 15.0.0
|
|
83
|
+
|
|
84
|
+
### Patch Changes
|
|
85
|
+
|
|
86
|
+
- Updated dependencies [02a014b]
|
|
87
|
+
- Updated dependencies [28b7c28]
|
|
88
|
+
- Updated dependencies [13749ec]
|
|
89
|
+
- Updated dependencies [e62c233]
|
|
90
|
+
- Updated dependencies [ed61c9b]
|
|
91
|
+
- Updated dependencies [31d04d4]
|
|
92
|
+
- @objectstack/platform-objects@15.0.0
|
|
93
|
+
- @objectstack/spec@15.0.0
|
|
94
|
+
- @objectstack/core@15.0.0
|
|
95
|
+
|
|
3
96
|
## 14.8.0
|
|
4
97
|
|
|
5
98
|
### Patch Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { SysReportSchedule, SysSavedReport } from '@objectstack/platform-objects/audit';
|
|
2
|
-
import { IReportService,
|
|
2
|
+
import { IReportService, SharingExecutionContext, SaveReportInput, SavedReport, ReportRunResult, ScheduleReportInput, ReportSchedule, ReportFormat } from '@objectstack/spec/contracts';
|
|
3
3
|
export { IReportService, ReportFormat, ReportQuery, ReportRunResult, ReportSchedule, SaveReportInput, SavedReport, ScheduleReportInput } from '@objectstack/spec/contracts';
|
|
4
4
|
import { Plugin, PluginContext } from '@objectstack/core';
|
|
5
5
|
|
|
@@ -40,6 +40,17 @@ interface ReportClock {
|
|
|
40
40
|
now(): Date;
|
|
41
41
|
}
|
|
42
42
|
declare function renderReport(rows: any[], format: ReportFormat, fields?: string[]): string;
|
|
43
|
+
/**
|
|
44
|
+
* Resolves a saved report's owner (`owner_id`) into a real, RLS-bearing
|
|
45
|
+
* `ExecutionContext` so a **scheduled** report executes under the owner's
|
|
46
|
+
* authority — the same rows the owner would see interactively — instead of
|
|
47
|
+
* bypassing RLS with a system context. Returns `null` when the owner cannot
|
|
48
|
+
* be resolved (unknown/disabled user), in which case the scheduler fails the
|
|
49
|
+
* run closed rather than running elevated (#2849 / #2980). Supplying this
|
|
50
|
+
* resolver is the reports-surface consumer of ADR-0073's user-less identity
|
|
51
|
+
* resolution.
|
|
52
|
+
*/
|
|
53
|
+
type OwnerContextResolver = (ownerId: string) => Promise<SharingExecutionContext | null>;
|
|
43
54
|
interface ReportServiceOptions {
|
|
44
55
|
engine: ReportEngine;
|
|
45
56
|
email?: ReportEmail;
|
|
@@ -51,6 +62,12 @@ interface ReportServiceOptions {
|
|
|
51
62
|
};
|
|
52
63
|
/** Cap rows per report to protect both DB and email size. */
|
|
53
64
|
maxRows?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Resolves a report owner into an RLS-bearing context for scheduled runs
|
|
67
|
+
* (see {@link OwnerContextResolver}). When omitted, scheduled reports fail
|
|
68
|
+
* closed instead of running with RLS bypassed (#2980).
|
|
69
|
+
*/
|
|
70
|
+
resolveOwnerContext?: OwnerContextResolver;
|
|
54
71
|
}
|
|
55
72
|
declare class ReportService implements IReportService {
|
|
56
73
|
private readonly engine;
|
|
@@ -58,14 +75,27 @@ declare class ReportService implements IReportService {
|
|
|
58
75
|
private readonly clock;
|
|
59
76
|
private readonly logger;
|
|
60
77
|
private readonly maxRows;
|
|
78
|
+
private readonly resolveOwnerContext?;
|
|
61
79
|
constructor(opts: ReportServiceOptions);
|
|
80
|
+
/**
|
|
81
|
+
* Authorization for a saved-report row. `sys_saved_report` is a
|
|
82
|
+
* protection-locked system object, so its rows are *read* with
|
|
83
|
+
* `SYSTEM_CTX`; the caller's right to see/mutate a specific report is
|
|
84
|
+
* enforced HERE, by owner match, not by the metadata read's own RLS —
|
|
85
|
+
* otherwise any authenticated caller could read/delete/overwrite any
|
|
86
|
+
* report by id (#2980). An explicit elevated context (`isSystem`) — the
|
|
87
|
+
* scheduler / server tooling — sees everything.
|
|
88
|
+
*/
|
|
89
|
+
private canAccessReport;
|
|
90
|
+
/** Raw metadata read of a saved report by id (no authz — callers gate). */
|
|
91
|
+
private loadReportRow;
|
|
62
92
|
saveReport(input: SaveReportInput, context: SharingExecutionContext): Promise<SavedReport>;
|
|
63
93
|
listReports(filter: {
|
|
64
94
|
object?: string;
|
|
65
95
|
ownerId?: string;
|
|
66
|
-
} | undefined,
|
|
67
|
-
getReport(reportId: string,
|
|
68
|
-
deleteReport(reportId: string,
|
|
96
|
+
} | undefined, context: SharingExecutionContext): Promise<SavedReport[]>;
|
|
97
|
+
getReport(reportId: string, context: SharingExecutionContext): Promise<SavedReport | null>;
|
|
98
|
+
deleteReport(reportId: string, context: SharingExecutionContext): Promise<void>;
|
|
69
99
|
run(reportId: string, context: SharingExecutionContext): Promise<ReportRunResult>;
|
|
70
100
|
runAdHoc(input: SaveReportInput, context: SharingExecutionContext): Promise<ReportRunResult>;
|
|
71
101
|
private executeReport;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { SysReportSchedule, SysSavedReport } from '@objectstack/platform-objects/audit';
|
|
2
|
-
import { IReportService,
|
|
2
|
+
import { IReportService, SharingExecutionContext, SaveReportInput, SavedReport, ReportRunResult, ScheduleReportInput, ReportSchedule, ReportFormat } from '@objectstack/spec/contracts';
|
|
3
3
|
export { IReportService, ReportFormat, ReportQuery, ReportRunResult, ReportSchedule, SaveReportInput, SavedReport, ScheduleReportInput } from '@objectstack/spec/contracts';
|
|
4
4
|
import { Plugin, PluginContext } from '@objectstack/core';
|
|
5
5
|
|
|
@@ -40,6 +40,17 @@ interface ReportClock {
|
|
|
40
40
|
now(): Date;
|
|
41
41
|
}
|
|
42
42
|
declare function renderReport(rows: any[], format: ReportFormat, fields?: string[]): string;
|
|
43
|
+
/**
|
|
44
|
+
* Resolves a saved report's owner (`owner_id`) into a real, RLS-bearing
|
|
45
|
+
* `ExecutionContext` so a **scheduled** report executes under the owner's
|
|
46
|
+
* authority — the same rows the owner would see interactively — instead of
|
|
47
|
+
* bypassing RLS with a system context. Returns `null` when the owner cannot
|
|
48
|
+
* be resolved (unknown/disabled user), in which case the scheduler fails the
|
|
49
|
+
* run closed rather than running elevated (#2849 / #2980). Supplying this
|
|
50
|
+
* resolver is the reports-surface consumer of ADR-0073's user-less identity
|
|
51
|
+
* resolution.
|
|
52
|
+
*/
|
|
53
|
+
type OwnerContextResolver = (ownerId: string) => Promise<SharingExecutionContext | null>;
|
|
43
54
|
interface ReportServiceOptions {
|
|
44
55
|
engine: ReportEngine;
|
|
45
56
|
email?: ReportEmail;
|
|
@@ -51,6 +62,12 @@ interface ReportServiceOptions {
|
|
|
51
62
|
};
|
|
52
63
|
/** Cap rows per report to protect both DB and email size. */
|
|
53
64
|
maxRows?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Resolves a report owner into an RLS-bearing context for scheduled runs
|
|
67
|
+
* (see {@link OwnerContextResolver}). When omitted, scheduled reports fail
|
|
68
|
+
* closed instead of running with RLS bypassed (#2980).
|
|
69
|
+
*/
|
|
70
|
+
resolveOwnerContext?: OwnerContextResolver;
|
|
54
71
|
}
|
|
55
72
|
declare class ReportService implements IReportService {
|
|
56
73
|
private readonly engine;
|
|
@@ -58,14 +75,27 @@ declare class ReportService implements IReportService {
|
|
|
58
75
|
private readonly clock;
|
|
59
76
|
private readonly logger;
|
|
60
77
|
private readonly maxRows;
|
|
78
|
+
private readonly resolveOwnerContext?;
|
|
61
79
|
constructor(opts: ReportServiceOptions);
|
|
80
|
+
/**
|
|
81
|
+
* Authorization for a saved-report row. `sys_saved_report` is a
|
|
82
|
+
* protection-locked system object, so its rows are *read* with
|
|
83
|
+
* `SYSTEM_CTX`; the caller's right to see/mutate a specific report is
|
|
84
|
+
* enforced HERE, by owner match, not by the metadata read's own RLS —
|
|
85
|
+
* otherwise any authenticated caller could read/delete/overwrite any
|
|
86
|
+
* report by id (#2980). An explicit elevated context (`isSystem`) — the
|
|
87
|
+
* scheduler / server tooling — sees everything.
|
|
88
|
+
*/
|
|
89
|
+
private canAccessReport;
|
|
90
|
+
/** Raw metadata read of a saved report by id (no authz — callers gate). */
|
|
91
|
+
private loadReportRow;
|
|
62
92
|
saveReport(input: SaveReportInput, context: SharingExecutionContext): Promise<SavedReport>;
|
|
63
93
|
listReports(filter: {
|
|
64
94
|
object?: string;
|
|
65
95
|
ownerId?: string;
|
|
66
|
-
} | undefined,
|
|
67
|
-
getReport(reportId: string,
|
|
68
|
-
deleteReport(reportId: string,
|
|
96
|
+
} | undefined, context: SharingExecutionContext): Promise<SavedReport[]>;
|
|
97
|
+
getReport(reportId: string, context: SharingExecutionContext): Promise<SavedReport | null>;
|
|
98
|
+
deleteReport(reportId: string, context: SharingExecutionContext): Promise<void>;
|
|
69
99
|
run(reportId: string, context: SharingExecutionContext): Promise<ReportRunResult>;
|
|
70
100
|
runAdHoc(input: SaveReportInput, context: SharingExecutionContext): Promise<ReportRunResult>;
|
|
71
101
|
private executeReport;
|
package/dist/index.js
CHANGED
|
@@ -154,6 +154,32 @@ var ReportService = class {
|
|
|
154
154
|
this.clock = opts.clock ?? { now: () => /* @__PURE__ */ new Date() };
|
|
155
155
|
this.logger = opts.logger ?? {};
|
|
156
156
|
this.maxRows = Math.max(1, opts.maxRows ?? 5e3);
|
|
157
|
+
this.resolveOwnerContext = opts.resolveOwnerContext;
|
|
158
|
+
}
|
|
159
|
+
// ── Access control ─────────────────────────────────────────────
|
|
160
|
+
/**
|
|
161
|
+
* Authorization for a saved-report row. `sys_saved_report` is a
|
|
162
|
+
* protection-locked system object, so its rows are *read* with
|
|
163
|
+
* `SYSTEM_CTX`; the caller's right to see/mutate a specific report is
|
|
164
|
+
* enforced HERE, by owner match, not by the metadata read's own RLS —
|
|
165
|
+
* otherwise any authenticated caller could read/delete/overwrite any
|
|
166
|
+
* report by id (#2980). An explicit elevated context (`isSystem`) — the
|
|
167
|
+
* scheduler / server tooling — sees everything.
|
|
168
|
+
*/
|
|
169
|
+
canAccessReport(row, context) {
|
|
170
|
+
if (!row) return false;
|
|
171
|
+
if (context?.isSystem) return true;
|
|
172
|
+
const userId = context?.userId;
|
|
173
|
+
return !!userId && row.owner_id === userId;
|
|
174
|
+
}
|
|
175
|
+
/** Raw metadata read of a saved report by id (no authz — callers gate). */
|
|
176
|
+
async loadReportRow(reportId) {
|
|
177
|
+
const rows = await this.engine.find("sys_saved_report", {
|
|
178
|
+
filter: { id: reportId },
|
|
179
|
+
limit: 1,
|
|
180
|
+
context: SYSTEM_CTX
|
|
181
|
+
});
|
|
182
|
+
return Array.isArray(rows) && rows[0] ? rows[0] : null;
|
|
157
183
|
}
|
|
158
184
|
// ── Report CRUD ────────────────────────────────────────────────
|
|
159
185
|
async saveReport(input, context) {
|
|
@@ -161,24 +187,25 @@ var ReportService = class {
|
|
|
161
187
|
if (!input.object) throw new Error("VALIDATION_FAILED: object is required");
|
|
162
188
|
if (!input.query) throw new Error("VALIDATION_FAILED: query is required");
|
|
163
189
|
const now = this.clock.now().toISOString();
|
|
190
|
+
const ownerId = context.isSystem ? input.ownerId ?? context.userId ?? null : context.userId ?? null;
|
|
164
191
|
const payload = {
|
|
165
192
|
name: input.name,
|
|
166
193
|
description: input.description ?? null,
|
|
167
194
|
object_name: input.object,
|
|
168
195
|
query_json: JSON.stringify(input.query ?? {}),
|
|
169
196
|
format: input.format ?? DEFAULT_FORMAT,
|
|
170
|
-
owner_id:
|
|
197
|
+
owner_id: ownerId,
|
|
171
198
|
updated_at: now
|
|
172
199
|
};
|
|
173
200
|
if (input.id) {
|
|
174
|
-
const existing = await this.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
const existing = await this.loadReportRow(input.id);
|
|
202
|
+
if (existing) {
|
|
203
|
+
if (!this.canAccessReport(existing, context)) {
|
|
204
|
+
throw new Error(`REPORT_NOT_FOUND: ${input.id}`);
|
|
205
|
+
}
|
|
206
|
+
if (!context.isSystem) payload.owner_id = existing.owner_id ?? payload.owner_id;
|
|
180
207
|
await this.engine.update("sys_saved_report", { id: input.id, ...payload }, { context: SYSTEM_CTX });
|
|
181
|
-
return rowFromSaved({ ...existing
|
|
208
|
+
return rowFromSaved({ ...existing, ...payload, id: input.id });
|
|
182
209
|
}
|
|
183
210
|
}
|
|
184
211
|
const id = input.id ?? uid("rpt");
|
|
@@ -186,10 +213,16 @@ var ReportService = class {
|
|
|
186
213
|
await this.engine.insert("sys_saved_report", row, { context: SYSTEM_CTX });
|
|
187
214
|
return rowFromSaved(row);
|
|
188
215
|
}
|
|
189
|
-
async listReports(filter,
|
|
216
|
+
async listReports(filter, context) {
|
|
190
217
|
const f = {};
|
|
191
218
|
if (filter?.object) f.object_name = filter.object;
|
|
192
|
-
if (
|
|
219
|
+
if (context?.isSystem) {
|
|
220
|
+
if (filter?.ownerId) f.owner_id = filter.ownerId;
|
|
221
|
+
} else {
|
|
222
|
+
if (!context?.userId) return [];
|
|
223
|
+
if (filter?.ownerId && filter.ownerId !== context.userId) return [];
|
|
224
|
+
f.owner_id = context.userId;
|
|
225
|
+
}
|
|
193
226
|
const rows = await this.engine.find("sys_saved_report", {
|
|
194
227
|
filter: f,
|
|
195
228
|
limit: 500,
|
|
@@ -198,16 +231,18 @@ var ReportService = class {
|
|
|
198
231
|
});
|
|
199
232
|
return Array.isArray(rows) ? rows.map(rowFromSaved) : [];
|
|
200
233
|
}
|
|
201
|
-
async getReport(reportId,
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
context: SYSTEM_CTX
|
|
206
|
-
});
|
|
207
|
-
return Array.isArray(rows) && rows[0] ? rowFromSaved(rows[0]) : null;
|
|
234
|
+
async getReport(reportId, context) {
|
|
235
|
+
const row = await this.loadReportRow(reportId);
|
|
236
|
+
if (!this.canAccessReport(row, context)) return null;
|
|
237
|
+
return rowFromSaved(row);
|
|
208
238
|
}
|
|
209
|
-
async deleteReport(reportId,
|
|
239
|
+
async deleteReport(reportId, context) {
|
|
210
240
|
if (!reportId) throw new Error("VALIDATION_FAILED: reportId is required");
|
|
241
|
+
const row = await this.loadReportRow(reportId);
|
|
242
|
+
if (!row) return;
|
|
243
|
+
if (!this.canAccessReport(row, context)) {
|
|
244
|
+
throw new Error(`REPORT_NOT_FOUND: ${reportId}`);
|
|
245
|
+
}
|
|
211
246
|
const schedules = await this.engine.find("sys_report_schedule", {
|
|
212
247
|
filter: { report_id: reportId },
|
|
213
248
|
limit: 500,
|
|
@@ -353,8 +388,8 @@ var ReportService = class {
|
|
|
353
388
|
let fired = 0, failed = 0, skipped = 0;
|
|
354
389
|
for (const schedule of list) {
|
|
355
390
|
try {
|
|
356
|
-
const
|
|
357
|
-
if (!
|
|
391
|
+
const row = await this.loadReportRow(schedule.report_id);
|
|
392
|
+
if (!row) {
|
|
358
393
|
skipped++;
|
|
359
394
|
await this.markSchedule(schedule.id, {
|
|
360
395
|
last_status: "skipped",
|
|
@@ -362,8 +397,22 @@ var ReportService = class {
|
|
|
362
397
|
});
|
|
363
398
|
continue;
|
|
364
399
|
}
|
|
400
|
+
const report = rowFromSaved(row);
|
|
401
|
+
const ownerId = report.owner_id;
|
|
402
|
+
const runContext = ownerId && this.resolveOwnerContext ? await this.resolveOwnerContext(ownerId).catch((err) => {
|
|
403
|
+
this.logger.warn?.("ReportService.dispatchDue: owner context resolution failed", err);
|
|
404
|
+
return null;
|
|
405
|
+
}) : null;
|
|
406
|
+
if (!runContext) {
|
|
407
|
+
failed++;
|
|
408
|
+
await this.markSchedule(schedule.id, {
|
|
409
|
+
last_status: "failed",
|
|
410
|
+
last_error: ownerId ? `owner '${ownerId}' context unavailable \u2014 refusing to run scheduled report with RLS bypassed (#2849/#2980)` : "report has no owner \u2014 refusing to run scheduled report with RLS bypassed (#2849/#2980)"
|
|
411
|
+
});
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
365
414
|
const fmt = schedule.format ?? "html_table";
|
|
366
|
-
const result = await this.executeReport({ ...report, format: fmt },
|
|
415
|
+
const result = await this.executeReport({ ...report, format: fmt }, runContext, false);
|
|
367
416
|
const recipients = schedule.recipients.split(",").map((s) => s.trim()).filter(Boolean);
|
|
368
417
|
const subject = renderSubject(schedule.subject_template, {
|
|
369
418
|
name: schedule.name ?? report.name,
|
|
@@ -510,7 +559,14 @@ var ReportsServicePlugin = class {
|
|
|
510
559
|
engine,
|
|
511
560
|
email,
|
|
512
561
|
logger: ctx.logger,
|
|
513
|
-
maxRows: this.options.maxRows
|
|
562
|
+
maxRows: this.options.maxRows,
|
|
563
|
+
// Scheduled reports run under the owner's resolved RLS context, not a
|
|
564
|
+
// system bypass (#2980). No owner-context resolver is wired yet — that
|
|
565
|
+
// is the reports-surface consumer of ADR-0073's user-less identity
|
|
566
|
+
// resolution (M2) — so until it lands, scheduled runs FAIL CLOSED
|
|
567
|
+
// (skipped + marked failed) rather than exfiltrate. Interactive runs
|
|
568
|
+
// (run/runAdHoc) are unaffected: they carry the caller's context.
|
|
569
|
+
resolveOwnerContext: void 0
|
|
514
570
|
});
|
|
515
571
|
ctx.registerService("reports", this.service);
|
|
516
572
|
if (this.options.disableDispatcher) {
|