Skip to content

18.3 lint: clm_admin's modifyAllRecords on clm_contract makes its 17 field edit locks inert — the locks are declared but bind no admin - #204

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-196-admin-field-locks
Nov 3, 2026
Merged

zhuangjianguo merged 1 commit into
mainfrom
claude/issue-196-admin-field-locks

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #196

Ruling: 6617227919 (letter A, the locks win). Claim: Ruling-ref 6617227919, branch claude/issue-196-admin-field-locks, session session_017nzWFRUo2U7evMkPdVniAn.

What changed

One file, src/profiles/admin.profile.ts. Only the clm_contract entry and the header comment changed.

  • clm_contract is now { allowCreate, allowRead, allowEdit, allowDelete: true, viewAllRecords: true, writeScope: 'org', allowTransfer: true }. modifyAllRecords is gone. readOnly(Contract, CONTRACT_STAMPED_FIELDS) stays.
  • The six child entries (clm_contract_version, clm_review, clm_deviation, clm_signature, clm_obligation, clm_payment_plan) still use full. Their fields are not touched.
  • Header comment. "The super-user bits do NOT bypass field-level security" stopped being true with ADR-0066's 2026-11-02 note: a super-user set votes on every field of its object. The new text says:
    • a lock binds only when the set has no modifyAllRecords;
    • viewAllRecords votes readable and never votes an edit;
    • whole-row write comes from writeScope: 'org';
    • owner reassign comes from allowTransfer;
    • the business-RLS companion reasoning still holds;
    • a hand write of a locked field is refused with 403 "Field write denied".

writeScope semantics, read in packages/spec/src/security/permission.zod.ts at @objectstack/spec@18.3.0:

  • writeScope is the "Write (edit/delete) access DEPTH".
  • Beside viewAllRecords: true it is "READ AND HONOURED. The write short-circuit does not name viewAllRecords, so this pair stays accepted". Only the pairing with modifyAllRecords is refused.
  • No gate refused the combination, so the ruling's fallback (readScope: 'org' + writeScope: 'org') was not needed.

/api/v1/auth/me/permissions for clm.admin after the change:

  • objects.clm_contract: allowEdit/allowDelete/allowTransfer/viewAllRecords: true, modifyAllRecords: false, writeScope: 'org'
  • fields.clm_contract.approved_at: { readable: true, editable: false }
  • owner_id and jurisdiction: { readable: true, editable: true }

Readings (18.3.0, pnpm demo:zh on a throwaway DB, port 3169, signed in as clm.admin@example.com)

Every reading is on a contract another user owns. Script: sign in through /api/v1/auth/sign-in/email, then REST calls under /api/v1/data.

Step before (origin/main b38df5a) after (c50654e)
GET another user's active contract 200 200
PATCH jurisdiction (ordinary field) 200 200
PATCH approved_at 200, value unchanged on read-back (2026-07-24 stays) 403 [Security] Field write denied: not permitted to edit [approved_at] on 'clm_contract' · PERMISSION_DENIED
PATCH route_finance (flip) 200, value unchanged on read-back (true stays) 403 [Security] Field write denied: not permitted to edit [route_finance] on 'clm_contract' · PERMISSION_DENIED
PATCH owner_id to a third user (reassign) 200, read back 200, read back
PATCH child clm_review.comments on that contract 200 200
requester1 creates a throwaway draft, admin DELETEs it 201 / 200, then GET 404 201 / 200, then GET 404
pnpm lint: security-superuser-field-mask-inert 17 (18 warnings in total) 0 (1 warning in total: obligation_requester_own, #64's)

Before the change, a PATCH of a stamped field answered 200 and the stored value did not move. The write was dropped silently, so on 18.3 the "overwrite" the issue describes did not land through REST. The lock now refuses the write out loud.

Admin screen flow (browser, Chromium, zh-CN, as clm.admin)

Editing another user's active contract used to rely on modifyAllRecords for the row write. The steps:

  1. Open 订单 — 黑貂保险 (ORD-2026-0015, 生效中, 业务承办人 Business Requester 3).
  2. Click 编辑 and change 管辖. Click 更新.
  3. The dialog closes and a toast reads 合同“订单 — 黑貂保险”已更新. The page shows the new value.
  4. API read-back: jurisdiction is written, updated_by is clm.admin, and owner_id is unchanged.

The only console error during the run was one unrelated 404 resource load, which also appears on origin/main. The edit form does not offer 批准时间, the 路由 flags, or 业务承办人. The form omits owner_id for every user: objectui plugin-form autoLayout.ts drops SERVER_OWNED_FIELD_NAMES, so this does not depend on permissions. In the console, reassignment is a REST write, measured above as 200. That was already true on origin/main.

Gates (worktree head c50654e; each exit code captured before any pipe)

pnpm install            → exit 0
pnpm validate           → exit 0   (1 warning: security-controlled-by-parent-owner-policy-without-opt-in, obligation_requester_own, #64)
pnpm lint               → exit 0   "1 warning(s), 6 suggestion(s)"   (security-superuser-field-mask-inert: 0)
pnpm typecheck          → exit 0   "tsc --noEmit"
pnpm lint:i18n-gate     → exit 0   "COVERAGE : 0 missing keys across 2 locale(s)"
CLM_DEMO_SEED=1 pnpm validate → exit 0

Baseline on origin/main b38df5a: pnpm lint exit 0, "18 warning(s), 6 suggestion(s)", 17 of them security-superuser-field-mask-inert.

Acceptance notes

  • DESIGN.md §04 is unchanged, as the ruling says. No governed file is touched.
  • Inside rowLevelSecurity (outside this card's surface), the comment on contract_admin_edit_any_status still says "Modify All Data does not skip them on this object". That is still true, but the administrator no longer holds Modify All on the contract. The companion is now needed because neither writeScope: 'org' nor any super-user bit skips business RLS. A one-line wording follow-up is offered in the report.
  • openAllExcept(Signature, []) and open(Review, ['internal_note']) repeat what the children's modifyAllRecords already votes since ADR-0066's 2026-11-02 note. They are harmless and left as they are.
  • DESIGN.md §04 gives clm_requester update on its own obligations, and controlled_by_parent refuses it for all 200 — 我负责的履约 is read-only for the audience it is built for #64 edits the clm_obligation entry of the same file. Whichever PR lands second rebases.

🤖 Generated with Claude Code

https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn

clm_admin held modifyAllRecords on clm_contract. Since ADR-0066's
2026-11-02 note a super-user set votes editable on every field of its
object, so the 17 stamped-field locks (routing flags, stage timestamps,
AI fields) bound no administrator; 18.3 lints it as
security-superuser-field-mask-inert.

Per the ruling on #196 (letter A, the locks win), the contract entry
now grants viewAllRecords + writeScope 'org' + allowTransfer instead:
every row readable, every row editable and deletable, owner reassign,
and no edit vote on the locked fields. The six child entries keep their
super-user bits; they lock nothing. The header comment now says a lock
binds only without modifyAll and whole-row write comes from writeScope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · head c50654e · 2026-11-03T23:08Z

check reading
ruling batch 12 item 2, letter A (6617227919): the locks win; clm_admin drops modifyAllRecords on clm_contract for viewAllRecords + writeScope: 'org' + allowTransfer
shape draft → main, Fixes #196; base b38df5a, mergeable clean
CI on the head both Validate check-runs success on c50654e = PR head.sha
scope 1 file, src/profiles/admin.profile.ts: the clm_contract entry exactly as ruled, readOnly(Contract, CONTRACT_STAMPED_FIELDS) kept, the six child entries unchanged, the header comment rewritten for ADR-0066's 2026-11-02 note. Lint 18 → 1 warning (the remaining one is #64's)
measured (18.3.0, as clm.admin on another user's contract) read 200; ordinary field 200; approved_at / route_finance 403 「Field write denied」 (before: 200 with the value silently unchanged); owner_id reassign 200; child write 200; delete 200; browser edit as the admin saves
residuals none for this card. The contract_admin_edit_any_status inline comment still names Modify All; it stays true and is left for the next card that touches the file

Landing: under the maintainer's ruling (batch 12, 「同意」 2026-11-03T22:34Z) and merge authorization (project thread: 「你可以自己审核的就自己合并」).


Generated by Claude Code

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

18.3 lint: clm_admin's modifyAllRecords on clm_contract makes its 17 field edit locks inert — the locks are declared but bind no admin

2 participants