Repository navigation
fix(platform-objects,plugin-audit,plugin-approvals)!: mail bodies are read only with manage_platform_settings, and the audit mirrors never copy them in clear - #24688
Conversation
…atform_settings and mask them in the ledger mirrors Claude-Session: https://claude.ai/code/session_01CAtkMwk5tY1jPaUCdKosnk Co-authored-by: Claude <noreply@anthropic.com>
…out, so a reader withheld one field keeps its global search Claude-Session: https://claude.ai/code/session_01CAtkMwk5tY1jPaUCdKosnk Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CAtkMwk5tY1jPaUCdKosnk Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 23 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 7 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 439ecf3e1c4fd93b401eefa07c4c206716957146 && git checkout 439ecf3e1c4fd93b401eefa07c4c206716957146
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 92ce3af404a9b6ca16881a89f2e841d29da52175 e7f2dd03933e75164c156918ab9b4a8a60e7b4f6 && git checkout -B drift-repro 92ce3af404a9b6ca16881a89f2e841d29da52175 && git merge --no-ff e7f2dd03933e75164c156918ab9b4a8a60e7b4f6
node scripts/docs-audit/affected-docs.mjs --json 92ce3af404a9b6ca16881a89f2e841d29da52175
|
Contract reviewServed-tier: Reviewed: PR #24688 (draft, base ① Derived judgments1. Every door is closed for a principal without The mechanism is one declaration, field
Door the matrix missed: none found that is open. Doors named above but not probed by the new test: export, aggregate/group-by, analytics; each is closed by an existing gate that the diff does not touch. 2. The field-level choice versus the claim's wish (organization administrators keep their own non-identity bodies). The dev measured that The dev's proposal B (relax the
3. The mirrors' masking (plugin-audit). Pre-fix stored mirror rows: still hold bodies in clear at rest. They are served only to a reader served the body (the platform administrator reads the 18.0 copy: dogfood :235-240), so there is no escalation, but it is a credential copied at rest and the hazard in item 2 above. This is a residual. The dev assigns its carrier to #24581, yet #24681 (the #24581 PR) states it does not rewrite 4. The 5. Tests. The matrix file counts 22 cases: data API 6, global search 3, mirrors 8, identity log and writers 5. Platform-admin controls at :180-189, :204-208, :235-240, :278-282 make every "absent" a gate, not an empty fixture. The organization-admin non-body control is :145-159 (subject and status served for the identity row AND the organization's own invitation). No body, link or token value appears in a title, message or log: probes compare a span read as the system by inclusion and report a boolean (:71-77). The "9 red on unchanged 6. The 18.x line rule R1-R4 on the merged tree. Dev-reported on 7. Interaction with PR #24681 (head 8. Semver, Clause-② and the changeset. 9. CI check-runs on ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Blocking items: none. Residuals R1-R3 above carry their fixes; R2 wants a card filed by the seat before or at landing. |
Conflicts and restatements, the ceremony being open on this branch: - runtime boot guard, its unit, kernel and dogfood pins: #24682's 18.0 "fresh installs only" refusal gives way to the marker-based refusal that names the ceremony's plan and apply. - cli.mdx: the open-ceremony text kept; #24678 and step 4's "until then the ceremony refuses" sentences restated; #24722's audit-mail-bodies row and section kept beside it. - B2: #24661's seven 18.0 upgrade-guide entries prescribe the ceremony again (the ups entry keeps the full apply), pinned in organization-ownership-upgrade-entries.test.ts. - metadata-lifecycle, metadata-service, capabilities, permission-sets: the 18.0 deferral restated. - platform-core.v18-boot-refusals revision 5. - objectql: WALLED_ORGANIZATION_LESS_BRIDGE, isWalledOrganizationLessBridgeObject and the engine reader are deleted (sys_secret and sys_flow_credential are column-less here); the refusal names systemFields: { tenant: false }. The tripwire is deleted; the flow-credential walled pin is kept, renamed walled-flow-credential-write.test.ts. - #24688's principal x door matrix restated over a ceremonied fixture. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V
…before the ceremony The ceremony's sibling-moves step asks for the sibling logs this release added (sys_identity_email, sys_platform_setting_audit) rather than creating them, so the operator's sequence is a refused boot of this release first, then the plan and its apply. The shared helper now takes that boot and throws unless the ownership guard refused it; #24688's principal x door matrix uses it (25 cases, green). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V
Closes #24625
Clause-②: yes (narrowing)
Dispatched by the epic seat (
epic:#15194) under claim 6278944518, on the maintainer's order. Measured record: 6278938192.The defect, re-measured on
mainbfc1539feThe test this PR adds was run on unchanged
main, over #24552's 18.0 fixture undersingle. The fixture holds one 18.0 password-reset row and one organization-invitation row insys_email. Result: 9 red, 11 green, matching the seat's matrix. The 9 red were:manage_platform_settingswas servedbody_textandbody_htmlof the 18.0 reset row on/datalist, get and query.sys_activityandsys_audit_logmirrors of the reset row served the body to that administrator.sys_identity_emailrow held its body in clear at rest. So did the mirror rows for a newsys_emailrow.Anonymous (401), member (403, no search hit), and the identity log for both non-platform principals (403) were already closed.
The fix, and why this one
The selector question first. No column tells an 18.0 identity row in
sys_emailapart from tenant mail:related_object, andsys_emailhas no template column.service-messaging's email channel) also carries norelated_object.So a row gate of "related to
sys_useror to nothing" would hide every notification from the organization administrator. Per the dispatch, with no reliable selector the fix is field-level.Read gate (platform-objects).
body_textandbody_htmlonsys_emailandsys_identity_emaildeclarerequiredPermissions: ['manage_platform_settings'](ADR-0066 D3). This one declaration closes every door that serves rows through the engine:/datalist, get and query (the key is dropped,?select=included). A body predicate is refused with 403.sys_activity/sys_audit_logread redaction, which serves a mirror the fields the data plane serves. The security(plugin-audit): a filter over the stored activity text answers by row presence: the generic list door evaluates the predicate at rest, before #21081's read-time redaction, so a reader can probe a field value it is not served #21154 value-column guard also refuses that administrator's body probes on the mirrors, pinned or not.The administrator keeps every row: envelope, subject, delivery state and linkage. They lose only the bodies.
Mirror writers (plugin-audit). The ledger view masks both logs' body columns on every row, the way it masks a credential field. A non-empty body is stored as the read mask and an unset one stays
null. This covers the create, update and delete snapshots and the activity row. The two names live once, inplatform-objects/src/audit/mail-log-body.ts.A shipped defect this change would have triggered (plugin-approvals).
sys_approval_requestfree-text search auto-includedpayload_json. The snapshot guard refuses any query naming that column to a reader withheld any field of any object. Once mail bodies are withheld from the organization administrator, that administrator's whole global search sweep returned 403 (measured on this branch before the fix). Their approvals list search would also have failed. The object now declaressearchableFields= the ADR-0061 auto-default minuspayload_json. A test holds the declared list to that derivation. A structured filter on the snapshot is unchanged for the readers the guard admits.What is not changed.
Tests: principal × door
packages/qa/dogfood/test/identity-mail-read-gate.dogfood.test.tsruns over the 18.0 fixture: 22 cases. Every probe is a link-token span read as the system and compared by inclusion, so no value appears in a title, message or log./datalist / get / querysys_activity/sys_audit_logsys_identity_emailThe writers are pinned too: the mirror rows of a HEAD-written
sys_identity_emailrow and of a HEAD-writtensys_emailrow hold no body at rest. Unit tests:platform-objects/src/audit/mail-log-body.test.ts: the declaration, with no object-level gate onsys_email.plugin-audit/src/audit-internal-field-omission.test.ts: the mask on every snapshot side, null preserved, and a non-mail-log control.plugin-approvals/src/sys-approval-request.search-fields.test.ts.Reverse verification. Each leg used
scripts/ablation-replace.mjs, then a rebuild of the package, thenablation-dist-preflight(marker present, then absent after restore). Each leg was restored to the HEAD blob withgit diff HEADempty.requiredPermissionsremoved fromsys_email→ 8 dogfood red: the organization-admin list, get, query, search and both mirror reads and both mirror probes.payload_jsonput back into the approvals search set → 1 unit red and the organization-admin global search red.Proof (head
e7f2dd039)pnpm dev -- --fresh -p 38625boots the showcase. The seeded admin signs in, and/data/sys_email,/data/sys_identity_email,/data/showcase_taskand/searchanswer 200. A password-reset request answers 200. The log has no error lines. The server was stopped and the port is free.upgrade-18x-harness.dogfood.test.tsis green. It ran together with the new matrix,search-skip-unreadable,audit-log-admin-search,activity-text-predicateandsignup-organization-census: 6 files, 62 passed.packages/cliserve-walled-posture-refusal.test.ts(integration project): 5 passed.node scripts/pm/dispatch-gates.mjs --commandsderived 74 commands, and all 74 exited 0. The--ranreconciliation reads 74 run, 0 NOT-MEASURED.check-plugin-teardown-shape --self-testneeded its pinned fixture commit fetched.check:dual-build-cjs-loadsneeded a full build. Both were re-run green after the fix.check-governed-merges --branch HEAD: NOT governed, 531 changed lines.Acceptance notes
plugin-approvals/src/sys-approval-request.object.ts(fix 3). It is the shipped defect this change would otherwise trigger for every organization administrator. Without it, the narrowing ships a global-search outage.sys_activity/sys_audit_logis now refused to them, as the security(plugin-audit): a filter over the stored activity text answers by row presence: the generic list door evaluates the predicate at rest, before #21081's read-time redaction, so a reader can probe a field value it is not served #21154 guard does for any reader withheld a field. Those copies hold 18.0 identity bodies at rest, so this is the oracle closing, not a regression.@objectstack/plugin-email@17.3.0has the same exposure. The backport torelease/17.xis owed when that branch is cut (seat record 6278938192).🤖 Generated with Claude Code
https://claude.ai/code/session_01CAtkMwk5tY1jPaUCdKosnk
Generated by Claude Code