Skip to content

A business requester and a legal counsel get "You don't have permission" on the contract's Discussion tab and a blank Approvals tab — sys_activity / sys_comment / sys_attachment / sys_approval_request answer 403 - #88

Merged
zhuangjianguo merged 1 commit into
mainfrom
claude/issue-86-discussion-approvals-403
Oct 10, 2026

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #86. This PR delivers the Discussion half. The Approvals half (sys_approval_request) needs a maintainer decision, set out below, and stays open on the card.

What changed

One file, src/profiles/requester.profile.ts. clm_requester gains three object grants. Every audience holds that set: requesters directly, and every position through src/security/bind-position-sets.ts.

object create read edit delete
sys_comment yes yes no no
sys_activity no yes no no
sys_attachment no yes no no

sys_approval_request is deliberately not granted. See Not in this PR.

Which layer refused (measured on c31c7e2, 17.7.0)

POST /api/v1/security/explain for Business Requester 1, object: sys_comment, operation: read, answered allowed: false with this layer:

object_crud denies :: No resolved permission set grants read on 'sys_comment'.

It answered the same for sys_activity, sys_attachment and sys_approval_request, and the same for Legal Counsel 1, who holds clm_legal + clm_requester + member_default. The data door answered 403 {"code":"PERMISSION_DENIED","object":"sys_comment"}.

The platform baseline member_default has granted only what it names explicitly since its wildcard was retired. Its 17.7.0 source says "Everything else is the application's to declare" (plugin-security, the objectstack-ai/objectstack#5491 block). So the refusal came from this app's permission sets, and the grant is ours to make.

Why these grants stay inside DESIGN.md §04

The platform scopes each of these reads by the record the row is about, using the caller's own read of that record:

  • comments: by the record their thread_id names (plugin-audit comment read visibility);
  • activity: by object_name + record_id (plugin-audit parent-record read gate, objectstack-ai/objectstack#21069);
  • attachments: by parent_object + parent_id (service-storage attachment read visibility).

Posting a comment also requires reading the record it is posted on.

I measured this with the grant in place. Business Requester 1 owns AMD-2026-0001. Business Requester 2 owns AMD-2026-0002, which BR1 cannot open (GET /api/v1/data/clm_contract/ID gives 404):

probe as BR1 control
GET sys_comment (unfiltered) total=1 (her own thread) admin total=2
GET sys_comment filtered to BR2's thread total=0
GET sys_comment/ID of BR2's comment 404
POST sys_comment on BR2's contract 403 on her own contract 201
GET sys_attachment (unfiltered) total=1 (her own) admin 2, LC1 2
GET sys_activity, all pages 640 rows, 359 distinct parents in 14 objects. Re-read as BR1, she can read all of them (0 unreadable) admin 1886

Browser

Setup: Chromium 1194 on port 3486. Followed the README operator setup with admin/create-user: Business Requester 1–3 hold clm_requester (a sys_user_permission_set row), Legal Counsel 1–2 hold clm_legal_counsel, plus a General Manager. Then a second pnpm demo hands the contracts over: BR1 has 43.

Before (c31c7e2): BR1 and LC1, contract AMD-2026-0001, Discussion tab.

  • The panel read "You don't have permission to view activity on this record. You don't have permission to view comments on this record. … You don't have access to these attachments."
  • Network: GET 403 on sys_comment?filter=["thread_id","=","clm_contract:ID"], sys_activity?…object_name…record_id…, sys_attachment?…parent_object…parent_id… and sys_approval_request?top=1….
  • The console logged each one as HTTP request failed … 403 [PERMISSION_DENIED].

After (b970d10): BR1.

  • Discussion tab reads go GET 200 on comments (total=1), activity and attachments (total=1, c1-notes.txt listed).
  • Typed in Leave a comment… and pressed Ctrl+Enter: POST 201 /api/v1/data/sys_comment. The comment shows as Business Requester 1 · just now.

After: LC1, same contract.

  • She sees both of BR1's comments (total=2) and the attachment.
  • Posted: POST 201, and the comment is visible.

Negative: BR1 opens BR2's contract AMD-2026-0002.

  • The page shows "Record not found".
  • The Discussion reads answer sys_comment total=0 and sys_attachment total=0, although BR2's comment and c2-secret.txt exist on that contract (admin reads both).

Approvals tab, after: still GET 403 sys_approval_request, and the tab is blank for BR1 and LC1. This is expected, because that grant is the open decision.

Console after: none of the Discussion 403s remain. What is left is the sys_approval_request 403 above, plus warnings that already appeared in the baseline run: the view:calendar / view:timeline fallback notice, a pre-sign-in 401 on get-session, the organization/list fetch race, and header-action predicates evaluated before the record loads.

Boot: no degraded capability. 818 ok / 2 errors on my reboots. Both errors are my own fixture: two contracts I sent to in_approval for the approvals measurement, which the demo upsert can no longer move back to in_review. A clean database boots with 820 rows.

Not in this PR: the Approvals tab (needs a maintainer decision)

The Approvals tab is record:related_list on sys_approval_request, so it reads the generic data door. Unlike the three objects above, that door has no parent-record scoping. Its visibility rule lives only on /api/v1/approvals/*: participants, plus the opt-in record-reader tier from objectstack-ai/objectstack#8652.

To show what a read grant would expose, I created two approval requests through legal's send_for_approval: one on BR1's ICA-2026-0004 and one on BR2's SUP-2026-0001. Then I measured with a temporary, uncommitted read grant:

  • BR1, BR2 and BR3 each listed both requests. BR3 owns neither contract.
  • BR1 read BR2's request by id. The payload snapshot of a contract she cannot open came back with it: contract_number SUP-2026-0001, title, amount 447000, party, risk_level, liability_cap. Field-level security narrows a snapshot; row visibility does not.
  • On the approvals door, the owner BR1 gets total=0 for her own contract's request: she is not a participant, and the record-reader tier is off. LC1 likewise sees only the request she submitted.
  • The tier cannot be switched on from app metadata. recordReaderVisibleObjects is a constructor option of ApprovalsServicePlugin, and the CLI's serve constructs that plugin with no arguments. It is still unwired on objectstack main 4638625.

Granting the read would give every employee every approval request in the organization, which is wider than §04. Per the lane ruling, that scope is the maintainer's call. The options and the recommendation are in the os-dev-report on the card.

Acceptance notes

  • The comment grant is not contract-only. sys_comment create applies to any record the user can read: the platform's comment rule is "read the parent". So requesters can also comment on other record pages they can read (for example the public_read configuration objects), because the synthesized default record page also mounts the discussion panel. The rows remain readable only by readers of that record.
  • Upload is visible but refused. The attachments panel on the Discussion tab now shows an Upload button. This PR grants no sys_attachment create: §05 describes the tab as comments and @, and files have their own objects (versions, signatures). Measured for BR1 and LC1: upload/complete 200, then POST /api/v1/data/sys_attachment 403, with the toast "You don't have permission to do that." The refusal is honest. Whether Upload should work is raised as a question on the card.
  • The grants follow clm_requester. If the open Q2 decision (card [Decision] DESIGN.md §04 makes clm_requester every employee's default, but a set that grants clm_requester.access cannot bind to everyone #11) changes which audiences hold clm_requester, these three grants must move with it. This PR does not change any binding.
  • Section number. The card and the dispatch cite DESIGN.md §07 for the tab list; it is §05 (合同详情页). No DESIGN.md text was touched.

Gates (HEAD b970d10, each exit code captured before any pipe)

pnpm validate        EXIT=0   ✓ Validation passed (1008ms)
pnpm lint            EXIT=0   6 suggestion(s), all pre-existing approval-approvers-may-resolve-empty; 0 errors, 0 warnings
pnpm typecheck       EXIT=0   tsc --noEmit
pnpm lint:i18n-gate  EXIT=0   ✓ i18n gate · COVERAGE : 0 missing keys across 2 locale(s)

Part of #86


Generated by Claude Code

…cussion tab

The contract page's Discussion tab (DESIGN.md §05) reads sys_comment,
sys_activity and sys_attachment and posts to sys_comment. The platform
baseline (member_default) has been explicit-allow since its wildcard
retired and names none of them, and no clm_* set did either, so every
non-admin audience got 403 PERMISSION_DENIED on all three and the tab
read "You don't have permission...". security/explain named the layer:
"No resolved permission set grants read on 'sys_comment'".

The grants go in clm_requester because every audience holds it. They do
not open rows beyond §04: the platform scopes each read by the caller's
own read of the record the row is about, and posting a comment requires
reading the record. Measured with two requesters whose contracts the
other cannot open.

sys_approval_request (the Approvals tab) is deliberately not granted:
its data door has no parent scoping, so a read here would serve every
approval request in the organization, snapshot included. That half is
a maintainer decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HihZ11bQSqjCgjzHbpv4M1
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.

2 participants