Skip to content

On objects that carry sharing rules, sharing middleware answers FORBIDDEN before RLS update-wideners are consulted — the identical widener works on rule-less objects #5493

Description

@yinlianghui

Part of objectstack-ai/hotcrm#706

Found during HotCRM's 17.0 GA acceptance sweep on @objectstack/* 17.0.0-rc.2. This is a regression against runtime-verified behavior: HotCRM #547 added two RLS update-wideners and its PR body records live verification (member insert as marketing_user → 201).

Measured on rc.2 (marketing_user, own token)

Widener Object rc.2 behavior
marketing_campaign_member_updates crm_campaign_member (no sharing rules of its own) ✅ works — PATCH admin-owned member → 200 (control: sales_rep → 403)
marketing_campaign_updates crm_campaign (carries sharing rules) ❌ PATCH not-own campaign → 403 "FORBIDDEN: insufficient privileges to update crm_campaign"

The error shape is the tell: the failing denial is the sharing-middleware FORBIDDEN, not the row-gate's "[Security] … (row-level security)" shape every other write denial in the sweep produced. The failing layer answers before RLS evaluation on objects that carry sharing rules — the widener is never consulted on crm_campaign, while on rule-less crm_campaign_member it is.

Reproduction

# as marketing_user
PATCH /api/v1/data/crm_campaign/ {"description":"x"}        → 403 FORBIDDEN (middleware shape)
PATCH /api/v1/data/crm_campaign_member/ {"status":"responded"}  → 200

Impact

Marketing can move members but cannot edit campaigns they didn't create — the exact posture the wideners were written to fix, failing subtly (half the mechanism works).

App declarations unchanged since they were verified working; attribution is the ordering of sharing middleware vs RLS widener evaluation on objects with sharing rules.

Evidence: HotCRM acceptance run w4/results.jsonl (M5). Related sibling: objectstack-ai/hotcrm#705 (write gate ignores share access levels — a different layer, same subsystem).

Blocked-by: #6735
(2026-08-08 identity PM note: maintainer ruled Q1=A / Q2=A1 — see comment 5226389104. Step 1 = #6735 (ISecurityService authored-row-write verdict, contract + plugin-security implementation, spec-seat transfer). This card is step 2: plugin-sharing consumption + the substance-bearing flip of the #5493 control in row-write-widener-composition.test.ts, dispatched by the identity lane after #6735 merges. Bulk half tracked as #6736. Prior line "Blocked-by: #5492" resolved via PR #6684, superseded by this one. Mechanism refinement from the round-2 dev: the discriminator is "does record sharing enforce on this object at all" (checkEdit deny vs abstain), not "carries sharing rules" — see comment 5226364929.)

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions