Repository navigation
A requester cannot launch a contract — clm_requester has no read on sys_business_unit, and our_entity is required - #126
Merged
zhuangjianguo merged 1 commit intoOct 25, 2026
Conversation
clm_contract.our_entity is a required lookup to sys_business_unit since #118, and no CLM set granted read on that platform object, so a member who is not an administrator was refused the launch dialog's "Our signing entity" picker (403) and saw the entity's raw id on the record page. clm_requester, which every member holds through the everyone anchor, now grants object-level read on sys_business_unit — read only, no create, edit or delete. A read grant is not a high-privilege bit, so the anchor binding stays valid. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn
Contributor
Author
席位核对 — ACCEPT · head
|
| check | reading |
|---|---|
| shape | draft → main, Fixes #124; assignee zhuangjianguo; mergeable clean |
| CI on the head | both Validate check-runs success on 19e8dac = PR head.sha |
| scope | 1 file (+11): a read-only sys_business_unit object grant in clm_requester, beside its other platform-object reads. Within the claim's surface. Nothing governed |
| the card | measured as real personas (requester, legal counsel, finance controller), not the dev admin: GET sys_business_unit 403 → 200 (the 3 companies); picker refused → 3 options; requester and counsel launched with an entity picked and the record page names it; write probes stay 403 |
| anchor | clm_requester is still bound as the additive baseline (explain, before and after); no high-privilege, anchor or refusal line in the boot log; pnpm lint raises no security-anchor-high-privilege |
| not in this PR | a finance controller still cannot launch: clm_finance's field locks on the legal fields and status (§13 Q1) also apply to the draft they create, so create_contract is refused (Field write denied). Unmasked by this fix, not caused by it; it is a design question, so the seat files it as its own card for the decision box |
Landing: under the maintainer's merge authorization (project thread: 「你可以自己审核的就自己合并」).
Generated by Claude Code
zhuangjianguo
marked this pull request as ready for review
October 25, 2026 14:45
This was referenced Oct 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #124
What changed
One line in
src/profiles/requester.profile.ts:clm_requestergrants object-level read on the platform'ssys_business_unit(allowCreate/allowEdit/allowDelete: false), with a comment saying why it sits there.clm_contract.our_entityhas been a required lookup tosys_business_unitsince our_entity is a lookup to the platform's sys_business_unit (kind company), seeded as data #118, and no CLM set granted read on it.member_defaultgrants none either. So every member who is not an administrator was refused the launch dialog's "Our signing entity" picker, and the record page showed the entity's raw id.clm_requesterbecause every member holds that set through theeveryoneanchor (PR Every member holds clm_requester by default; the app declares its eleven capability tokens #122). The legal, finance and records sets inherit it that way. Measured below: explain attributes the legal and finance personas' read toclm_requester. So the other four sets are left unchanged.sys_comment,sys_activity,sys_attachmentandsys_approval_request. The new line passesvalidate,lintand the boot without a diagnostic.describeHighPrivilegeBits(@objectstack/spec18.1.0,security/high-privilege.ts) flags only undeclaredsystemPermissions,viewAllRecords/modifyAllRecords,allowDelete/allowTransferandallowExport. A read grant has none of these. Measured below: nosecurity-anchor-high-privilegefinding in lint or validate, no refusal in the boot log, and explain still reportsclm_requesteras the additive baseline.public: rows are shared org-wide, as explain says. The read therefore reaches the organization's own units.README needs no change: no paragraph describes the launch dialog's entity picker or a grant for it.
Measurements: 18.1.0,
pnpm demo --port 3424, one throwaway local databaseBefore =
origin/main1264e02, after = this branch19e8dac, same database for both. I created the personas the README way, as the dev admin:POST /api/v1/auth/admin/create-user×3 → 200:Business Requester 1(no position),Legal Counsel 1, andFinance Controller 1.POST /api/v1/data/sys_user_position→ 201 forclm_legal_counseland forclm_finance_controller.POST /api/v1/auth/change-password×3 → 200.pnpm demoagain, which handed the 43 / 34 contracts to the requester and the counsel.Every reading below was taken signed in as the persona, never as the dev admin. Browser: Playwright with Chromium at
/opt/pw-browsers/chromium.GET /api/v1/data/sys_business_unit403 PERMISSION_DENIED(objectsys_business_unit)200, total 3: ARC-US Arcadia Holdings Inc. · ARC-EU Arcadia Europe GmbH · ARC-APAC Arcadia Asia Pacific Pte. Ltd., allkind: company,active: trueclm_legal_counsel)403200, the same 3clm_finance_controller)403200, the same 3GET /api/v1/data/clm_contract?populate=our_entityanswered the raw id before ("our_entity":"G8yvrH008o2ERCyS") for all three personas. After, it answers the expanded unit ({"code":"ARC-US","name":"Arcadia Holdings Inc.",…}).Read only, measured as Business Requester 1 after the change:
POST /api/v1/data/sys_business_unit→403 PERMISSION_DENIED.PATCH /api/v1/data/sys_business_unit/{ARC-US}→403.security/explainonsys_business_unit:readallowed ("read on 'sys_business_unit' is granted by [clm_requester]").create,updateanddeleteare denied ("No resolved permission set grants …").Launch dialog: "Our signing entity" picker (sidebar Launch a Contract)
GET /api/v1/data/sys_business_unit?populate=parent_business_unit_id,manager_user_id&top=50&filter=["and",["kind","=","company"],["active","=",true]]→ 403. The popover read "You do not have permission to perform this action. Contact your administrator if you need access." with a Retry button. The browser console logged one new403.Launch with an entity picked, then the record page (after)
Business Requester 1
POST /api/v1/actions/clm_contract/launch_contract200.NDA-2026-0021(draft, owner Business Requester 1,our_entity→ ARC-EU), and the dialog moved on to the version upload screen.sys_business_unitrequest that failed.Legal Counsel 1
The same path with Arcadia Asia Pacific Pte. Ltd. created
NDA-2026-0022(draft,our_entity→ ARC-APAC). Its record page reads "OUR SIGNING ENTITY | Arcadia Asia Pacific Pte. Ltd.".Finance Controller 1
create_contractnode was refused: resume →400, "Node 'create_contract' failed: create_record(clm_contract) failed: [Security] Field write denied: not permitted to edit [confidentiality_term_months, governing_law, status, summary] on 'clm_contract'".clm_finance's legal-field FLS, not this grant. See Acceptance notes. Per the persona rule: finance launch NOT MEASURED, refused by a permission check, no alternate route taken.Record page of a seeded contract (
AMD-2026-0001), read by all three personasG8yvrH008o2ERCyS(the raw id),GET /api/v1/data/sys_business_unit/G8yvrH008o2ERCyS→403, and the console loggedHTTP request failed … 403 [PERMISSION_DENIED]. The server loggedAccess denied: operation 'find' on object 'sys_business_unit' is not permitted for positions [org_member, everyone]andFailed to expand relationship field; retaining foreign key IDs {"field":"our_entity","reference":"sys_business_unit"}.sys_business_unit403 request, and the console has nothing new: the same pre-login401, the same resource404, and the same three pre-existingrecord.statuspredicate warnings as before.security/explain and the boot
clm_requester, member_default, andclm_requesteris "via additive baseline (ADR-0090 D5)", identical to before. Legal Counsel 1:clm_legal, clm_requester, member_default. Finance Controller 1:clm_finance, clm_requester, member_default. Both lists are the same as before.updated 150on the hand-over run,skipped 823on the re-run) and the oneInline seed exceeded … budgettiming line. In all three boot logs,high-privilege,security-anchor,refusedanddegradedeach match 0 lines.sys_business_unitin exactly twoAccess deniedlines: theinsertand theupdateof my two write probes above. It has nofinddenial and noFailed to expandline.Gates: head
19e8dac, exit codes captured to files before any pipeAcceptance notes
Finance Controller 1, the intake'screate_contractnode is refused withField write denied … [confidentiality_term_months, governing_law, status, summary].clm_financedeclares the legal fields andstatusread-only (finance.profile.ts,readOnly(Contract, [...CONTRACT_LEGAL_FIELDS, 'status', …])).clm_requesterdeclares nothing on those fields, and only the sets that declare a field vote (see_grants.ts). So the finance lock wins on the create too, although the flow writes a brand-new draft.mainthis was hidden, because the picker's 403 stopped every non-admin member before that node. It is outside this card's file surface and is reported to the seat for a card.clm_requesteralone, which is the requester's shape. The grant reaches the records set through the same baseline.manager_user_id(asys_userlookup). It answered 200 for all three personas, so this card needed nosys_usergrant.🤖 Generated with Claude Code
https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn