Skip to content

[17.0-rc][疑似平台] member_default 的 * 通配授权(C/R/E)并集合并进每个成员 —— 应用的 explicit-allow 对象门在建/读/改三轴上被整体架空,任何 profile 都能在任何对象上建记录 #703

Closed

Description

@yinlianghui

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

  1. added
    bugSomething isn't working
    upstream:objectstackBlocked on / caused by the ObjectStack platform — tracked upstream
    on Aug 5, 2026
  2. yinlianghui commented on Aug 5, 2026

    @yinlianghui
    CollaboratorAuthor

    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

  3. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    Collaborator

    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:

    const md = securityDefaultPermissionSets.find(s => s.name === 'member_default');
    Object.keys(md.objectPermissions)   // []
    md.objectPermissions['*']           // undefined

    The object_permissions["*"] = {allowCreate, allowRead, allowEdit} wildcard this card was filed about is no longer shipped — member_default carries no object permissions at all. @objectstack/platform-objects states the change in its own comments: "#5491 removed that wildcard and the platform baseline is explicit-allow."

    What member_default still carries is row-level security, not object grants — owner_only_writes and owner_only_deletes (created_by == current_user.id, scoped to position org_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_default in ctx.permissions, so test/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

  4. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    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_default carries 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

  5. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    GA closing probe — green on all three axes. Closing per the on-card ruling.

    Version: @objectstack/* 17.0.0 GA, repo at d4ddee0, 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_record query against the running server, sys_permission_set rows as the platform materialised them:

    member_default object_permissions: 25 entries, none of them '*'
      ['*']          = undefined
      crm_* entries  = 0        (only sys_user, sys_account, sys_session, ... )
    

    member_default still carries the row-level policies the rc.6 comment described — owner_only_writes / owner_only_deletes, created_by == current_user.id, scoped to org_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:

    sales_rep       positions=[org_member, sales_rep, everyone]       sets=[sales_rep, member_default]
    sales_manager   positions=[org_member, sales_manager, everyone]   sets=[sales_manager, member_default]
    service_agent   positions=[org_member, service_agent, everyone]   sets=[service_agent, member_default]
    marketing_user  positions=[org_member, marketing_user, everyone]  sets=[marketing_user, member_default]
    admin           positions=[platform_admin, org_owner, everyone]   sets=[admin_full_access, organization_admin_no_bypass, member_default]
    

    Declared posture computed by union-merging the live permission sets each principal holds; measured posture by a real HTTP call per cell.

    axis cells declared-DENY cells ALLOWED (the #703 defect) refusal shape
    create 90 0 87 total across the three axes, all 403 object-gate
    read 90 0 ditto
    edit 126 0 ditto

    Every 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:

    cell rc.2 17.0.0 GA
    service_agent create crm_opportunity 201 403
    sales_rep create crm_contract 201 403
    marketing_user create crm_case 201 403
    service_agent read crm_campaign 200, all 7 rows 403
    marketing_user read crm_product 200, all 13 rows 403

    3. 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.

    BEFORE injection    create crm_opportunity  -- 403     read crm_campaign  -- 403
    AFTER  injection    create crm_opportunity  -- 201     read crm_campaign  -- 200 rows=10
                        create crm_quote        -- 201     read crm_quote     -- 200 rows=1
           explain: "create on 'crm_opportunity' is granted by [probe_wildcard_...]"
    AFTER  removal      create crm_opportunity  -- 403     read crm_campaign  -- 403
    

    That middle block is this card's rc.2 reading reproduced exactly, including the granted by [set] sentence the card quotes from security/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:blocked in 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

  6. added
    priority:p0Critical: blocker, must ship before MVP
    and removed on Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:p0Critical: blocker, must ship before MVPupstream:objectstackBlocked on / caused by the ObjectStack platform — tracked upstream

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions