Skip to content

Permission sets: no way to grant "edit any record" without also granting delete (modifyAllRecords implies allowDelete) #25470

Closed as duplicate

Description

@marchtian

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)

  1. When an entry sets modifyAllRecords: true and allowDelete: false explicitly, honour allowDelete: false (bypass ownership for edit only). (not chosen — changes the security semantics; ruling 6377890121)
  2. A separate bit, e.g. editAllRecords (ownership bypass for update only), alongside viewAllRecords / modifyAllRecords. (not chosen; ruling 6377890121)
  3. 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

  1. marchtian commented on Oct 23, 2026

    @marchtian
    CollaboratorAuthor

    Second case in hotsrm: srm_supplier (OWD public_read). Category managers must edit suppliers they don't own (legacy suppliers imported by an admin, hotsrm#15), so their permission set got modifyAllRecords: true with allowDelete: false, and they can now delete any supplier, which cascades to its contacts and certificates through master_detail. Same need as above: edit all, delete none.


    Generated by Claude Code

  2. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 23, 2026
  3. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    Contributor

    State · director · pm:awaiting-maintainer → pm:queue (priority:p2 stays) · ruling 6377731715 letter A · 2026-10-24T01:00Z

    Director seat, summon #49 (session session_0162wFwDdLFGhaSvDdSnZ9Sx, account os-zhuang; written as objectstack-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 carried pm:awaiting-maintainer with no Maintainer-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 — modifyAllRecords is the write bypass for edit, delete and transfer (packages/plugins/plugin-security/src/permission-evaluator.ts:76–83, :289; the spec fold packages/spec/src/security/permission.zod.ts:214 is the same). The schema refuses, at publish and in lint, an object-permission entry that sets modifyAllRecords: true beside an explicit allowDelete: 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": a criteria sharing rule with accessLevel: 'edit' to the position on a public_read object (packages/spec/src/security/sharing.zod.ts:35, :76; ADR-0111 D3 removed edit-share delete; the object's effective model read is an enforcing posture under D7). Whether a rule's condition can select every row is unverified — the dev reads it first and the docs state what they find. Not taken: B (honour allowDelete: false under Modify All — rewrites the security fold), a new editAllRecords key. Workload S.


    Generated by Claude Code

  4. os-elon-musk commented on Oct 24, 2026

    @os-elon-musk
    Collaborator

    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 2 editAllRecords 不采)。工作量 S。A 是做事,不是「不做」,卡不关。总裁决:#25496 评论 6377842908。

    维护者动作已完成:摘 pm:awaiting-maintainer,回 pm:queue;裁不派。


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    Contributor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions