From b970d10d9f26640e93fb395bc6b245406b125440 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 00:33:40 +0000 Subject: [PATCH] fix(security): let every audience read and post on the contract's Discussion tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The contract page's Discussion tab (DESIGN.md §05) reads sys_comment, sys_activity and sys_attachment and posts to sys_comment. The platform baseline (member_default) has been explicit-allow since its wildcard retired and names none of them, and no clm_* set did either, so every non-admin audience got 403 PERMISSION_DENIED on all three and the tab read "You don't have permission...". security/explain named the layer: "No resolved permission set grants read on 'sys_comment'". The grants go in clm_requester because every audience holds it. They do not open rows beyond §04: the platform scopes each read by the caller's own read of the record the row is about, and posting a comment requires reading the record. Measured with two requesters whose contracts the other cannot open. sys_approval_request (the Approvals tab) is deliberately not granted: its data door has no parent scoping, so a read here would serve every approval request in the organization, snapshot included. That half is a maintainer decision. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HihZ11bQSqjCgjzHbpv4M1 --- src/profiles/requester.profile.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/profiles/requester.profile.ts b/src/profiles/requester.profile.ts index e3aa7d4..36a2df3 100644 --- a/src/profiles/requester.profile.ts +++ b/src/profiles/requester.profile.ts @@ -78,6 +78,35 @@ export const RequesterSet = definePermissionSet({ clm_contract_type: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false }, clm_clause: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false }, clm_approval_rule: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false }, + // The contract page's Discussion tab (DESIGN.md §05, 讨论 — the platform + // discussion panel) reads these three platform objects and posts to + // `sys_comment`. The platform baseline every member resolves + // (`member_default`) names none of them — it has been explicit-allow since + // the wildcard retired, "everything else is the application's to declare" + // — so without these lines every audience got `403 PERMISSION_DENIED` on + // all three and the tab read "You don't have permission…" (issue #86). + // They sit in this set because every audience holds it (see the header). + // + // An object-level read here does NOT open rows beyond §04, because the + // platform scopes each of these reads by the record the row is about, with + // the caller's own read of that record: a comment by the record its + // `thread_id` names, an activity row by `object_name` + `record_id`, an + // attachment by `parent_object` + `parent_id`. Posting a comment likewise + // requires reading the record it is posted on. MEASURED on 17.7.0 with two + // requesters, each owning a contract the other cannot open: each listed + // only their own contract's comment and attachment, the other's comment + // answered `404` by id, posting on the other's contract answered `403`, + // and all 640 activity rows served to one of them were about records that + // requester can read. + // + // Deliberately absent: `sys_approval_request` (the Approvals tab). Its data + // door has no such parent scoping — a read granted here serves every + // approval request in the organization, payload snapshot included, to + // every holder — so whether and how requesters see approval history is a + // maintainer decision (#86), not a line in this set. + sys_comment: { allowCreate: true, allowRead: true, allowEdit: false, allowDelete: false }, + sys_activity: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false }, + sys_attachment: { allowCreate: false, allowRead: true, allowEdit: false, allowDelete: false }, }, fields: { ...readOnly(Contract, CONTRACT_STAMPED_FIELDS),