Repository navigation
[17.0-rc][疑似平台] member_default 的 * 通配授权(C/R/E)并集合并进每个成员 —— 应用的 explicit-allow 对象门在建/读/改三轴上被整体架空,任何 profile 都能在任何对象上建记录 #703
Description
Found during the 17.0 GA acceptance sweep on @objectstack/* 17.0.0-rc.2 (current main), 5-profile permission matrix phase (188 probes, each user with their own Bearer token).
Symptom
The platform's member_default permission set carries object_permissions["*"] = {allowCreate:true, allowRead:true, allowEdit:true, allowDelete:false} and is union-merged (most-permissive) into every org member. The app's explicit-allow object gate is therefore erased on the create, read and edit axes for all 17 objects. Only delete is still profile-driven.
security/explain states it outright, e.g. for service_agent — whose profile carries an explicit all-false deny on crm_opportunity:
create on 'crm_opportunity' is granted by [member_default]
Measured
- 21 create-denial probes, ALL returned 201. Every profile creates on every object once validation passes: service_agent creates opportunities (explicitly denied), sales_rep creates contracts/forecasts/knowledge articles, marketing_user creates cases/tasks/events/products/quotes.
- member_default also grants own-row EDIT on objects the profile forbids editing: service_agent edited its own crm_account (200) where the profile declares no edit anywhere.
- The read axis was already partially documented (the "200 with 0 rows" pattern for ungranted private objects, noted in fix(security): grant the six objects that had no permissions at all (#488) #547's PR). What was NOT documented: on public_read objects the wildcard yields 200 with ALL rows for non-holders (agent read all 7 campaigns, marketing read all 13 products) — real unauthorized reads, not empty sets.
Why this is p0
The entire #547 remediation ("grant the six objects that had no permissions at all") and every profile's create/edit posture is fiction at runtime: the app declares explicit-allow, the platform enforces default-allow-except-delete. Any org member can create records on any object and edit anything they created. HotCRM's authorization test suite (authorization-coverage.test.ts) validates declarations, so CI is green while the runtime posture is default-open.
Reproduction
# as a service_agent-bound user (explicit all-false deny on crm_opportunity)
POST /api/v1/data/crm_opportunity {"name":"X","stage":"prospecting","amount":1,"crm_account":<id>,"close_date":"2026-09-01"}
→ 201
GET /api/v1/security/explain?object=crm_opportunity&operation=create
→ "... granted by [member_default]"Attribution
Platform (permission-set union semantics / the shipped member_default content). The CRM metadata is correct and is being overridden. If the wildcard is intentional platform policy, the app needs a supported way to opt out — explicit deny entries do not win against the union today.
Full probe log: acceptance run w4/results.jsonl (M1/M9 in the mismatch register). Refs #547 #548
Activity
docs(acceptance): finalize — permission matrix does not hold; all 8 s…
docs(acceptance): mark the handoff page complete — all 8 sections exe…
yinlianghui commentedon Aug 5, 2026
Blocked-by: objectstack-ai/objectstack#5491
分诊(PM 循环第 1 轮,会话
session_01VHrPAGEgFDoHjphqYG4BMa):判定为平台缺陷(permission-set 并集语义 / member_default 通配内容),⛔ 不在 hotcrm 侧修。已按跨仓协议在平台仓立镜像单 objectstack#5491(查重:#2753/#3005/#1985 均已关闭且不覆盖本缺陷),带完整探针证据。本单打pm:blocked,平台修复落地后由解锁扫回队复测。CRM 侧元数据声明无误、无需改动;authorization-coverage.test.ts验的是声明面,平台修复后需补一次运行时矩阵复测(验收线的回归波次覆盖)。Generated by Claude Code
huangyiirene commentedon Aug 11, 2026
Re-measured on
@objectstack/*17.0.0-rc.6 during the rc.5 → rc.6 upgrade (#1059, PR #1066). The root cause named here is gone.Read at runtime, not from source:
The
object_permissions["*"] = {allowCreate, allowRead, allowEdit}wildcard this card was filed about is no longer shipped —member_defaultcarries no object permissions at all.@objectstack/platform-objectsstates the change in its own comments: "#5491 removed that wildcard and the platform baseline is explicit-allow."What
member_defaultstill carries is row-level security, not object grants —owner_only_writesandowner_only_deletes(created_by == current_user.id, scoped to positionorg_member), which is the co-gate #705/#548 discuss rather than the create/read/edit hole here.Scope note: this is a static read of the shipped permission set, not a re-run of the 188-probe matrix. It settles whether the wildcard exists; a full re-run of the create-denial probes under real Bearer tokens would be needed to confirm the end-to-end posture is now explicit-allow as declared.
One side effect already observed and handled in #1066: rc.6 enumerates
member_defaultinctx.permissions, sotest/parent-derived-reach.test.ts's negative control now sees['member_default', 'sales_rep']. Reach is unchanged — every sharing assertion in that file still passes.Generated by Claude Code
hotlong commentedon Aug 14, 2026
Maintainer ruling (2026-08-14, verbatim: 「同意」, item 6 of the six-item list: 「#703 探针复跑后关」): this card closes after one full GA permission probe — verify the explicit-allow object gates hold on all three axes and
member_defaultcarries no wildcard union entry. The rc.6 static read (2026-08-11, on this card) already found the root cause gone, and the platform mirror objectstack#5491 is closed via merged PR objectstack#6684; the probe is the closing confirmation, executed under the GA close-out sweep #1150 (group 3). Green ⇒ close with the reading; red ⇒ reopen the question on #1150 and nominate a fresh objectstack mirror.Generated by Claude Code
hotlong commentedon Aug 14, 2026
GA closing probe — green on all three axes. Closing per the on-card ruling.
Version:
@objectstack/*17.0.0 GA, repo atd4ddee0,objectstack dev -p 4001, real Bearer tokens (five principals, each signed in with its own password).This is the runtime probe the ruling asked for, not a repeat of the rc.6 static read. The rc.6 comment flagged its own limit — "this is a static read … a full re-run of the create-denial probes under real Bearer tokens would be needed to confirm the end-to-end posture is now explicit-allow as declared." That re-run is below.
1. The wildcard, read from the live database (not from source)
sys_recordquery against the running server,sys_permission_setrows as the platform materialised them:member_defaultstill carries the row-level policies the rc.6 comment described —owner_only_writes/owner_only_deletes,created_by == current_user.id, scoped toorg_member— which are #705's subject, not this card's.2. The runtime matrix — 5 principals x 18 objects, three axes
Principals resolved by the platform's own
security/explain:Declared posture computed by union-merging the live permission sets each principal holds; measured posture by a real HTTP call per cell.
403object-gateEvery one of the 87 declared-deny refusals carried the object-gate message — "You do not have permission to perform this action" — which is a different string from the record-level refusal ("You do not have access to this record") that appears where the object gate allows and a row/record layer narrows. The two layers are distinguishable and the deny cells all landed on the object gate.
The card's own headline cells, then and now:
crm_opportunitycrm_contractcrm_casecrm_campaigncrm_product3. Why this is not a false green
Positive controls. A blanket 403 would look identical to a green if only denials were counted. Read axis: 72/72 declared-allow cells returned 200. Create axis: every object had at least one principal create successfully with the same payload template before any denial on that object was counted (three objects —
crm_forecast,crm_campaign_member,crm_event_attendee— failed that bar on the first pass because my payloads failed validation for every principal including admin, so they were re-run with corrected payloads and are included above).Falsifiability — the probe was made to fail, on purpose. Asking "what would this print if the defect were still present?" is not enough when the answer is a guess, so the rc.2 defect shape was re-created at runtime: a permission set carrying
object_permissions['*'] = {allowCreate, allowRead, allowEdit}was bound to the service_agent principal, and the same cells re-run.That middle block is this card's rc.2 reading reproduced exactly, including the
granted by [set]sentence the card quotes fromsecurity/explain. The probe can print the defect; on GA as shipped it does not. The injected set and its binding were removed and the removal re-verified (0 leftovers).Verdict
Non-reproducer on 17.0.0 GA. Root cause gone (objectstack#5491, merged PR objectstack#6684) and the end-to-end posture is explicit-allow as declared, on create, read and edit. Closing per the maintainer ruling of 2026-08-14 on this card (「#703 探针复跑后关」), and dropping
pm:blockedin the same write.Probes:
10-703-matrix.mjs,11-703-falsify.mjs,12-703-gapfill.mjs— throwaway scaffolding, deleted. Executed under #1152 (GA close-out sweep #1150, group 3).Generated by Claude Code
Generated by Claude Code