Repository navigation
plugin-reports' executeReport rebuilds a 5-field subset of the execution envelope, dropping accessible_org_ids — a group-posture report silently under-reports #7204
Description
Activity
Triage:
pm:queue+domain:services.- Classification: concrete defect — the projection at
packages/plugins/plugin-reports/src/report-service.ts:466(re-verified onorigin/main@f40c5b4) dropsaccessible_org_ids, which the engine reads by name atpackages/objectql/src/engine.ts:2248to widengroup-posture tenant scope. Direction is under-reporting (fail toward isolation) — a correctness defect on a shipped read path; restore-invariant shape ⇒ queue. - Routing: fix lands in
packages/plugins/plugin-reports⇒domain:services. Family precedent service-storage's attachment kit carries the same 5-fieldcallerContextprojection #7141 fixed for comments #7145 (the service-storage twin of plugin-audit'scallerContextrebuilds a 5-field subset of the execution envelope, droppingonBehalfOfbefore the sharing gates that are documented to fail closed on it #7141 — same projection shape, same lane class). - Dedup: card's own search confirmed; plugin-audit's
callerContextrebuilds a 5-field subset of the execution envelope, droppingonBehalfOfbefore the sharing gates that are documented to fail closed on it #7141 (plugin-audit twin) is closed and does not cover this file; Re-annotate plugin-approvals + plugin-reports onto the fullExecutionContextand delete the now-redundant casts (services half of #7070) #7135 deliberately excluded this body change from its scope, and the boundary is recorded on both cards. - Not
target:v17: same grading as plugin-audit'scallerContextrebuilds a 5-field subset of the execution envelope, droppingonBehalfOfbefore the sharing gates that are documented to fail closed on it #7141/service-storage's attachment kit carries the same 5-fieldcallerContextprojection #7141 fixed for comments #7145 — blast radius unestablished (no measuredgroup-posture deployment with saved reports). Re-grade trigger: a measured group-posture + saved-reports usage flips this to class ①. - Dispatch note: the card's "why the naive fix may not be right" section (in-place context mutation hazard, the
isSystemfallback doing real work,assertExportAllowedrunning pre-projection) is the reverse-verification checklist — carry it into the dev brief verbatim.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Classification: concrete defect — the projection at
Claim: PM loop round 2 (
domain:servicesseat #6021)
Session:session_015fkdTyGmMD5s8ZtEifvuGy
Branch:claude/issue-7204-report-envelope-forwarding
Worktree:objectstack-issue-7204
Domain:domain:services
File surface:packages/plugins/plugin-reports/src/report-service.ts+ its tests (a new/extended test file in the same package). (Stop on breach; explain in the report.)
Container & model: M,mode:subagent,model: opus
Serial constraints cleared: PR #7206 (annotation widening, this file) MERGED — base includes it; PR #7222 (app-todo, disjoint) in the merge queue; no in-flight claim touches plugin-reports. Premise re-verified by triage @f40c5b4(projection atreport-service.ts:466, engine read atengine.ts:2248).
Generated by Claude Code
ACCEPT. PR #7283, reviewed by the
domain:servicesseat (#6021, sessionsession_015fkdTyGmMD5s8ZtEifvuGy). Marking ready and queueing.Verified against GitHub rather than the report: 6 files —
report-service.ts, the new group-posture integration suite (real kernel + real SQL driver, row-set assertions), +6 structural cases in the existing suite, changeset, andpackage.json/lockfile for the two test-only devDependencies (declared in the PR; Validate Package Dependencies green). 26/26 check runs completed, allsuccess; ESLint (04:54:48Z) and TypeScript Type Check (05:01:01Z) read by job conclusion.What earns the accept:
- Repro first, on row sets:
INTERACTIVE ['a1','a2','b1']vsREPORT ['a1','a2']measured pre-fix — the card's exact claim, then equal after. Assertions land on returned rows, not "the key is on the context object", which is what this defect looked like from one layer up. - The PM's mechanism hypothesis was measured, not assumed: the in-place-mutation hazard DOES apply here, in a different direction than plugin-audit's
callerContextrebuilds a 5-field subset of the execution envelope, droppingonBehalfOfbefore the sharing gates that are documented to fail closed on it #7141 (stale inbound__readScopefrom an earlier object in the same request + write-back through the forwarded reference), so "forward whole minus__keys, as a fresh object" was chosen over the naivecontext: context— and BOTH halves are pinned. - Two-direction reverse verification with every pin red in exactly one direction: fix removed → the 5 row-set/timezone pins go red while the 3 posture guards (empty set / isolated / no provider) stay green — the guards are the proof the change didn't trade under-reporting for exposure; naive forward-whole → the 3 preservation pins (
__strip, fresh object, projection defaults) go red instead. No pin is decoration. timezonewas brought in scope by measurement (its read-path consumer isapplyFormulaPlan, pinned on the row's value with a deterministic UTC+14 disagreement), andassertExportAllowed's un-projected input is pinned by identity.
Out-of-scope finding verified filed: #7284 (
finding) —withoutOperationPrivateKeysnow hand-copied in three packages (#7141/#7145/#7204); the extraction's home (spec vs core vs plugin-security export) is a real decision, correctly recorded rather than guessed.
Generated by Claude Code
- Repro first, on row sets:
Out-of-scope observation from implementing #7135, filed rather than fixed — that card was bounded to annotation widening plus redundant-cast deletion, and the change below is a RUNTIME change with an enforcement consequence. This is the plugin-reports twin of #7141 (the plugin-audit one), same shape, different consequence.
Fact
packages/plugins/plugin-reports/src/report-service.ts—executeReport()does not forward the caller's execution envelope to the engine read that produces the report. It rebuilds a five-field projection of it:That comment states the intent exactly — the report must see the rows the caller would see interactively — and the projection is what stops it. #7135 widened this method's parameter ANNOTATION to the full
ExecutionContext(socontexthere now genuinely holds the whole envelope); the body was deliberately left alone, because changing it is not inert.The field that matters:
accessible_org_idspackages/objectql/src/engine.ts:2181reads it straight off the execution context to widen the driver's native tenant scope:accessible_org_idsis not in the projection, soopts.tenantIdsis never set for a report. Per the ADR-0105 D2 comment immediately above that block, an absent set makes drivers "fall back to equality: fail toward isolation, never toward exposure".So the direction here is under-reporting, not a leak — and that is worth stating plainly, because it makes this a correctness bug rather than a security one. Under the
grouptenancy posture a user's read reach is their whole membership set; the same query returns that union in an interactive list view and collapses to active-org equality inside a saved or scheduled report. The report is silently short rows, with no error and nothing in the output saying so.posture(ADR-0095 D2),org_user_ids,systemPermissions,onBehalfOfandtimezoneare dropped by the same projection.timezoneis read two lines up in the same engine method (hasTz), which governs date-dependent driver generation.What I could not establish
Whether any deployment currently runs the
groupposture with saved reports in use — I did not tracetenancyPostureProviderback to a real configuration, so the blast radius is unmeasured. What is definite is that the projection drops a field the engine reads by name, in a method whose own comment promises the caller's identity is what executes. Grading is triage's call — filed unlabeled.Why the naive fix may not be right
context: contextis the obvious change, and #7141 records why that direction needs its own evidence in the sibling package: plugin-security's middleware MUTATES the operation context in place (sc.__readScope = ...), so a forwarded envelope can carry access DEPTH resolved for a different object. For reports the object of the operation ISreport.object_name, so that specific hazard may not apply here — but theisSystemfallback in the projection (context.isSystem ?? false) is doing real work and must survive whatever replaces it, andassertExportAllowedruns against the un-projected context just above. Worth its own card and its own reverse verification rather than a rider.Dedup
Searched open issues for
executeReport/ report-service context projection,accessible_org_ids+ projection, and plugin-reports + RLS: no hits. #7141 is the same shape in plugin-audit and is closed; it does not cover this file. Not inside #7135's completion scope — that card is annotation widening plus redundant-cast deletion with implementation bodies untouched, exactly as #6523's contract half separated from its consumer half. Standalone rather than a sub-issue.Related: #7135 (where the annotation was widened and this was deliberately not folded in), #7141 (plugin-audit twin), #7070, #6523, #6206 (ruling).
Generated by Claude Code