Repository navigation
clm_admin edits contracts past submitted: the administrator carries its own permissive update policies - #117
Merged
Conversation
…ation Every position also holds clm_requester, whose contract_requester_edit_window (status in draft/submitted) and obligation_requester_own were the only update policies on clm_contract and clm_obligation for an administrator. Modify All Data skips ownership and sharing, but the platform drops business row-level security for it only on an object whose posture is access.default 'private' (ADR-0066 D2); the CLM objects declare an OWD, not that posture, so the requester's window capped the administrator: PATCH on a contract past submitted answered 403, and every child write under one was refused with "master not editable (row-level security)". Measured on 18.1.0. clm_admin now carries the same permissive companions clm_legal already does (contract_admin_edit_any_status, obligation_admin_update_any), and the header comment that claimed the super-user bits bypass RLS is corrected. 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, Part of #11, no closing keyword; assignee zhuangjianguo |
| CI on the head | both Validate check-runs success on cc06d5d = PR head.sha; mergeable clean |
| scope | 1 file, src/profiles/admin.profile.ts (+38/−4); nothing governed |
| the ruling | 6395821523 step 2 — classified as this app's own missing rule, the branch the ruling says to fix directly: 18.1 skips business RLS for a Modify All Data holder only on an access.default: 'private' object (ADR-0066 D2), RLS policies OR-merge across held sets, so clm_requester's two windows were the admin's only update policies. Two permissive companions on clm_admin (same pattern as legal.profile.ts); the false header claim corrected. Not a platform workaround: the platform behaves as designed |
| measured (dev, 18.1.0) | before: admin PATCH 403 on 6 post-submitted statuses, 403 on reviews under them, 403 on others' obligations; after: contracts 200 ×8 (cancelled / expired 422 by the app's archive lock, same for legal), reviews 200 ×7, obligations 200 ×5; requester window control still 403 / 200; boot warning set as main |
| stays on #11 | step 1 (§04 gate × holder table, governed) and step 3: isDefault: true on clm_requester is refused by 18.1 lint/validate/compile (security-anchor-high-privilege) unless objectstack.config.ts declares the app's eleven capability tokens — outside the card and touching AGENTS.md's capability rule, so it goes to the maintainer as a decision |
Landing: under the maintainer's merge authorization (project thread: 「你可以自己审核的就自己合并」).
Generated by Claude Code
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.
Part of #11 — ruling steps 2–3: step 2 fixed in
clm_admin(this app's own missing rule, measured on 18.1.0); step 3 measured and not adopted —isDefault: trueonclm_requesteris refused unless the stack declares its capability tokens, which lies outside this card's file surface.#11 remains open: step 1 (the §04 gate × holder table) is governed text for the design-document PR, and step 3 waits on the decision below.
Step 2 — the
clm_adminupdate capRe-measured on 18.1.0 (origin/main
f5ad133,pnpm demo, personaPersona Adminholding positionclm_admin): the defect is still there.clm_adminf5ad133PATCH clm_contractindraft/submittedPATCH clm_contractinin_review·in_approval·approved·signing·active·terminatedPERMISSION_DENIED×6PATCH clm_contractincancelled/expired(both archived)INVALID_STATE"archived … only its summary can still be edited" — the app's own archive lock, which applies to everyonePATCH clm_reviewunder a contract pastsubmitted(7 statuses)PATCH clm_obligationowned by someone else (5 statuses)Classification: (a) this app's own missing rule, not a platform defect. Evidence, from the installed
@objectstack/plugin-security@18.1.0andsecurity/explain:computeLayeredRlsFilterskips business RLS for a Modify All Data holder only whenposturePermits && superuserBypass, andposturePermitsismeta.isPrivate || tenancyDisabled || isBetterAuthManaged, whereisPrivateisobj.access.default === 'private'— the ADR-0066 D2 exposure posture ("exempts it from wildcard RLS via the posture-gated superuser bypass"), not the OWD.clm_contractdeclaressharingModel: 'private'and noaccessblock, so the bypass skips ownership and sharing but not RLS.security/explainforPersona Admin,updateon anactivecontract, on main:vama_bypass→ admitted via[clm_admin];rls→excluded,rowFilter: {"status":{"$in":["draft","submitted"]}}— the requester's window alone. On this branch the same call'srlslayer readsadmittedwith the admin's full-vocabulary policy OR-ed in.clm_legalcarriescontract_legal_edit_any_statusfor exactly this reason ("the OR-merge trap",legal.profile.ts).admin.profile.tsinstead asserted that "the super-user bits also bypass business row-level security", which is true only on anaccess.default: 'private'object.Fix:
clm_admincarriescontract_admin_edit_any_status(clm_contractupdate, every status) andobligation_admin_update_any(clm_obligationupdate, every status), each the whole vocabulary of its field read from the object — the same shape as the legal companions — and the header comment says why. No object, view or flow is touched; the §04 table is not changed.Controls (this branch):
Persona Requesterowning onein_reviewand onesubmittedcontract —PATCH403 and 200 respectively: the requester window still holds.Persona Counsel(clm_legal): 15 of 17 edits 200, the other two the same archived 422s.No upstream card: the seam is this app's metadata. (Setting
access: { default: 'private' }onclm_contractwould be the other route to the bypass; it also opts the object out of'*'wildcard grants, and it is an object edit, so it is not taken here.)Step 3 —
isDefault: trueonclm_requesterMeasured on 18.1.0 in three legs; none of it is committed.
Leg A —
isDefault: truealone: refused.pnpm lintexit 1,pnpm validateexit 1, andobjectstack dev --compilerefuses to start, verbatim:The platform fix (objectstack#17189 / #18535) excuses a
systemPermissionstoken only when the stack declares it undercapabilities:(describeHighPrivilegeBits(def, { declaredCapabilities }); the lint rule readsstack.capabilities). This app declares none.Leg B — plus a
capabilities:block inobjectstack.config.tsdeclaring the eleven tokens the five sets grant (scope: 'org'): accepted.pnpm lint0,pnpm validate0, boot clean (no refusal line), andsecurity/explainfor a brand-new member with no position and no grant (Persona Blank): permission sets[member_default]on main →[clm_requester, member_default],clm_requestervia "additive baseline (ADR-0090 D5)";createandreadonclm_contractgo fromallowed: falsetoallowed: true.Leg C — Leg B plus the seven
RequesterSet.nameentries removed frompositions.ts:pnpm validate/lint/typecheck0, and every persona's effective sets unchanged (explain principal, before vs after, same database):clm_adminclm_admin, clm_requester, member_defaultclm_legal_counsel·clm_legal_headclm_legal, clm_requester, member_defaultclm_finance_controllerclm_finance, clm_requester, member_defaultclm_records_managerclm_records, clm_requester, member_defaultclm_executive·clm_general_managerclm_requester, member_defaultsys_user_permission_setrow)clm_requester, member_defaultmember_default→clm_requester, member_default(the intended change)Contract row counts per persona unchanged (executive 23, GM 4, requester 2, finance 82, records 82); the admin's step-2 edits still answer 200 — the ruling's warning holds: after the
everyonebinding the administrator still holdsclm_requester, so step 2's fix is needed either way.Why it is not in this PR: Leg B needs a
capabilities:declaration inobjectstack.config.ts, which is outside this card's file surface (src/profiles/**·src/sharing/positions.ts), and the retraction also makes README text false (below). It goes back to the seat as a decision.Gates — head
cc06d5dBoot: the only boot-time warning on this branch is
Plugin superseded: 'com.objectstack.metadata', present on main as well; no new warning.Acceptance notes
src/security/bind-position-sets.ts(retired in PR Upgrade the@objectstack/*dependencies from 17.7.0 to 18.1.0, walking the official upgrade checklists of 18.0 and 18.1 #110), and sayssys_user_permission_set.permission_set_idis id-typed — on 18.1 the field ispermission_set, a name (POST /api/v1/data/sys_user_permission_set {"user_id": …, "permission_set": "clm_requester"}→ 201). If step 3 lands, its "requesters hold no position … granted as asys_user_permission_setrow" and "the dev admin … holds noclm_*permission set" paragraphs change too.security/explainattributes a set that a position definition distributes asvia: "direct grant"(e.g.clm_legalandclm_requesterforPersona Counsel, who has nosys_user_permission_setrow); only a set whose name equals the position's (clm_admin) readsposition:clm_admin. Platform-side, reported for the seat.🤖 Generated with Claude Code
https://claude.ai/code/session_017nzWFRUo2U7evMkPdVniAn