Measured on 17.0.0-rc.2 while verifying a sharing decision in objectstack-ai/hotcrm#549; the app-side report with the full fixture is objectstack-ai/hotcrm#694. Filed unassigned by the hotcrm PM loop.
The gap
plugin-security's computeControlledByParentFilter derives the child filter as parentFK IN (master ids), where the master id set comes from computeRlsFilter(master, 'find', ctx) — Layer 0 (tenant) and Layer 1 (rowLevelSecurity policies) only, executed under a system context.
Owner scope and sys_record_share grants are applied by a different plugin — plugin-sharing's buildReadFilter — which returns null for any object whose effective sharing model is not private, and controlled_by_parent maps to public. So the two halves of record-level access never meet on a derived object.
The function documents the v1 scope itself:
single level — the master's OWN controlled_by_parent is not traversed transitively; master accessibility is the master's RLS filter (sharing-service grants on the master are not folded in).
The consequence of "not folded in" is stronger than it reads: an app that authors no rowLevelSecurity policy on the master gets an unrestricted master set, so the derived filter restricts nothing at all. Authoring RLS on the master is not a workaround either — it is ANDed with the sharing filter, so it also cuts off the very rows that were shared in (measured, dead end).
Measured
Real boot of ObjectQL + plugin-security + plugin-sharing over the app's own config, two accounts owned by another user, one rep holding a territory position that shares exactly one account:
| object |
OWD |
rep sees |
crm_account |
private |
acct_US only — the shared one. Correct. |
crm_contact |
controlled_by_parent (account) |
both accounts' contacts, including the account they cannot read |
crm_quote_line_item |
controlled_by_parent (quote) |
both quotes' lines, though they can read neither quote |
Write side has the same hole: assertControlledByParentWrite checks the master against computeRlsFilter(master, 'update') — also empty — so a caller holding allowEdit can write children of parents they cannot see. Measured directly: with a quote converted to controlled_by_parent, the rep edited a quote on an account they cannot read.
Why this matters beyond one app
controlled_by_parent reads, to an author, as "this child follows its parent's access" — that is what the name says and what apps model with it. Delivered behaviour is "readable by anyone holding object-level read". Line-item objects are the common shape here, and they carry per-line pricing and discounts.
This is a declared-≠-enforced surface of the worst kind: the app declares a narrowing and the engine applies none, with no warning at author time, publish time or runtime. An app author — human or AI — has no signal that the boundary they declared does not exist. hotcrm's own metadata comments, profile comments and security docs all assert the narrow reading and are all wrong; they were written against the name, not against a measurement.
Suggested direction
Resolve master accessibility through the same path a direct read of the master would take — owner scope and sys_record_share grants folded in, not RLS policies alone — for both the read filter and the write assertion. If that is not intended for v1, then the mismatch should at least be loud: reject or warn at publish time when an object declares controlled_by_parent against a master whose access depends on sharing rather than RLS, so the silent case stops being the default one.
Acceptance
- A child of a
controlled_by_parent object is invisible to a caller who cannot read its master, when the master's access comes from ownership or a share grant;
- the same holds for writes;
- a test fixture covers the ownership/share path specifically (RLS-only coverage is what hides this today).
Measured on
17.0.0-rc.2while verifying a sharing decision in objectstack-ai/hotcrm#549; the app-side report with the full fixture is objectstack-ai/hotcrm#694. Filed unassigned by the hotcrm PM loop.The gap
plugin-security'scomputeControlledByParentFilterderives the child filter asparentFK IN (master ids), where the master id set comes fromcomputeRlsFilter(master, 'find', ctx)— Layer 0 (tenant) and Layer 1 (rowLevelSecuritypolicies) only, executed under a system context.Owner scope and
sys_record_sharegrants are applied by a different plugin —plugin-sharing'sbuildReadFilter— which returnsnullfor any object whose effective sharing model is notprivate, andcontrolled_by_parentmaps topublic. So the two halves of record-level access never meet on a derived object.The function documents the v1 scope itself:
The consequence of "not folded in" is stronger than it reads: an app that authors no
rowLevelSecuritypolicy on the master gets an unrestricted master set, so the derived filter restricts nothing at all. Authoring RLS on the master is not a workaround either — it is ANDed with the sharing filter, so it also cuts off the very rows that were shared in (measured, dead end).Measured
Real boot of
ObjectQL+plugin-security+plugin-sharingover the app's own config, two accounts owned by another user, one rep holding a territory position that shares exactly one account:crm_accountcrm_contactcontrolled_by_parent(account)crm_quote_line_itemcontrolled_by_parent(quote)Write side has the same hole:
assertControlledByParentWritechecks the master againstcomputeRlsFilter(master, 'update')— also empty — so a caller holdingallowEditcan write children of parents they cannot see. Measured directly: with a quote converted tocontrolled_by_parent, the rep edited a quote on an account they cannot read.Why this matters beyond one app
controlled_by_parentreads, to an author, as "this child follows its parent's access" — that is what the name says and what apps model with it. Delivered behaviour is "readable by anyone holding object-level read". Line-item objects are the common shape here, and they carry per-line pricing and discounts.This is a declared-≠-enforced surface of the worst kind: the app declares a narrowing and the engine applies none, with no warning at author time, publish time or runtime. An app author — human or AI — has no signal that the boundary they declared does not exist. hotcrm's own metadata comments, profile comments and security docs all assert the narrow reading and are all wrong; they were written against the name, not against a measurement.
Suggested direction
Resolve master accessibility through the same path a direct read of the master would take — owner scope and
sys_record_sharegrants folded in, not RLS policies alone — for both the read filter and the write assertion. If that is not intended for v1, then the mismatch should at least be loud: reject or warn at publish time when an object declarescontrolled_by_parentagainst a master whose access depends on sharing rather than RLS, so the silent case stops being the default one.Acceptance
controlled_by_parentobject is invisible to a caller who cannot read its master, when the master's access comes from ownership or a share grant;