Skip to content

controlled_by_parent derivation ignores the master's ownership and share grants — children are readable (and writable) regardless of parent access #5386

Description

@yinlianghui

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions