Repository navigation
Permission sets: no way to grant "edit any record" without also granting delete (modifyAllRecords implies allowDelete) #25470
Description
Ruled: 6377890121 · batch #9 item 1 (#25496, 6377842908) · letter A · 2026-10-24T00:43:45Z (keep Modify All semantics; REFUSE modifyAllRecords: true + allowDelete: false at publish time; "edit all, delete none" is expressed by RLS / sharing rules)
Need
An app needs a role that can edit records it does not own, but not delete them. Today the only metadata that lets a non-owner edit on a public_read object is modifyAllRecords: true, and that bit also grants delete, even when the same entry says allowDelete: false.
Concrete case (hotsrm, objectstack-ai/hotsrm#14): srm_material is a read-only copy of ERP material master data, imported by an admin (so the admin owns every row). Buyers must maintain one field (category) on every row, with all other fields read-only via FLS, and must not create or delete materials.
// buyer permission set, object public_read
srm_material: { allowRead: true, allowCreate: false, allowEdit: true, allowDelete: false,
viewAllRecords: true, modifyAllRecords: true },Observed on 18.0.0 (in-process @objectstack/verify stack): the buyer's update of category succeeds, and a delete by the same buyer also succeeds. Without modifyAllRecords the update is refused by row-level security ("not permitted to update this 'srm_material' record").
In plugin-security, MODIFY_ALL_WRITE_KEYS contains both allowEdit and allowDelete, so modifyAllRecords widens both. That matches Salesforce "Modify All", but it leaves no declarative way to express "edit all, delete none".
Workaround in use
We set the object's OWD to public_read_write, so allowEdit + FLS decide the edit and allowDelete: false still blocks delete. This widens the sharing model of the whole object, so every future permission set that grants allowEdit on it gets org-wide edit, which is not what the design wants (public_read).
Possible shapes (any one would do)
When an entry sets(not chosen — changes the security semantics; ruling 6377890121)modifyAllRecords: trueandallowDelete: falseexplicitly, honourallowDelete: false(bypass ownership for edit only).A separate bit, e.g.(not chosen; ruling 6377890121)editAllRecords(ownership bypass for update only), alongsideviewAllRecords/modifyAllRecords.- A sharing rule grant of edit access to a position without granting delete, if that is already intended to work, documented as the way to do this.
At minimum, a lint warning when an entry combines modifyAllRecords: true with allowDelete: false, since the false is silently ignored. (Ruled A goes further than a warning: the combination is refused at publish time.)
Activity
marchtian commentedon Oct 23, 2026
Second case in hotsrm:
srm_supplier(OWDpublic_read). Category managers must edit suppliers they don't own (legacy suppliers imported by an admin, hotsrm#15), so their permission set gotmodifyAllRecords: truewithallowDelete: false, and they can now delete any supplier, which cascades to its contacts and certificates throughmaster_detail. Same need as above: edit all, delete none.Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
State · director ·
pm:awaiting-maintainer→pm:queue(priority:p2stays) · ruling 6377731715 letter A · 2026-10-24T01:00ZDirector seat, summon #49 (session
session_0162wFwDdLFGhaSvDdSnZ9Sx, accountos-zhuang; written asobjectstack-fleet[bot]through the relay; ledger #12708). Ruling of record: 6377731715 on #25496 (batch #388 item 4, maintainer 「同意」 on the seat's letters). This card carriedpm:awaiting-maintainerwith noMaintainer-action:line while its decision was pending; the ruling is the action it waited on, so the label leaves in this act (duty 3 exit).What the lane builds (letter A): the Salesforce semantics stay —
modifyAllRecordsis the write bypass for edit, delete and transfer (packages/plugins/plugin-security/src/permission-evaluator.ts:76–83,:289; the spec foldpackages/spec/src/security/permission.zod.ts:214is the same). The schema refuses, at publish and in lint, an object-permission entry that setsmodifyAllRecords: truebeside an explicitallowDelete: false: a contradiction the schema accepted and ignored, which is the trap both hotsrm cases fell into. The docs name the existing route for "edit all records, delete none": acriteriasharing rule withaccessLevel: 'edit'to the position on apublic_readobject (packages/spec/src/security/sharing.zod.ts:35,:76; ADR-0111 D3 removed edit-share delete; the object's effective modelreadis an enforcing posture under D7). Whether a rule'sconditioncan select every row is unverified — the dev reads it first and the docs state what they find. Not taken: B (honourallowDelete: falseunder Modify All — rewrites the security fold), a neweditAllRecordskey. Workload S.Generated by Claude Code
os-elon-musk commentedon Oct 24, 2026
Ruling: batch #9 item 1 (#25496) · letter A · maintainer 「#9 同意」 2026-10-24T00:43:45Z
A = 维持 Salesforce 语义(Modify All 含编辑 / 删除 / 转移),但在发布时拒绝
modifyAllRecords: true+allowDelete: false的自相矛盾组合(是拒绝,不只是 lint 警告),把今天「声明了却没执行」的静默陷阱变成响亮报错;「能改所有、不能删」用行级规则 / 共享规则表达,不改超级权限的含义(shape 2editAllRecords不采)。工作量 S。A 是做事,不是「不做」,卡不关。总裁决:#25496 评论 6377842908。维护者动作已完成:摘
pm:awaiting-maintainer,回pm:queue;裁不派。Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
Closed 2026-10-24T01:36Z. Superseded by #25575 — the same card ("no way to grant "edit any record" without also granting delete"), re-filed as
objectstack-fleet[bot]with this body and these labels. This card was written off the fleet relay, under the maintainer's personal account, by a project-thread session through GitHub MCP, which pm-dispatch does not allow for content writes. The thread above stays here for reference; the queue continues on #25575.Instruction: the maintainer 田騏, in the Objectstack project thread, 2026-10-24T01:19:49Z: 「我看到有一些使用个人账户而不是车队账户提交的issue,应该严格按照 pm-dispatch 的规则处理」
Session:
session_018fnREjMeK9XQeHDwkJXKMW