Skip to content

A permission set accepts a hierarchy readScope beside viewAllRecords: true, never reads it, and emits no diagnostic — the declaration materialises and a capability census counts it as coverage #16870

Description

@huangyiirene

Filed by the repo:hotcrm execution seat (session session_01PpRjGNnwyo2J1rrmekxB1W, R57) as a platform-gap card, per the hotcrm charter — 「建设 hotcrm 时发现的平台能力缺口,立普通平台卡到 objectstack」. Filed unassigned and unlabelled: this seat ⛔ does not produce domain:* or grading for this board.

Surfaced while implementing hotcrm#1378, where a maintainer ruling asked for a hierarchy readScope to be declared so an app's requires: ['hierarchy-security'] would finally be exercised on the read path. The declaration turned out to be inert, and nothing anywhere says so.

The defect

An object permission may carry both viewAllRecords: true and a row-level readScope. On @objectstack/* 17.3.0 the read-scope resolver returns org on viewAllRecords || modifyAllRecords before it ever consults readScope. So in that combination:

  • the readScope value is never read by any runtime path;
  • the declaration is nonetheless accepted with zero diagnostics — no validate error, no lint finding, no boot warning;
  • it is materialised into sys_permission_set.object_permissions, so it is visible to anything that inspects the deployed shape.

⇒ An author writes a row-scope narrowing, the platform stores it, and it changes nothing. ⛔ Nothing in the toolchain says the key was discarded.

Measured, on a real app

Measured in-process against the installed 17.3.0 packages by hotcrm#1378's dev, driving a real Sales Manager over ObjectQL with a buildContextForUser context (isAdmin false, perms [member_default, sales_manager]), against three seeded opportunities — the manager's own, a direct report's, and an unrelated rep's:

arm grant shape rows read
1 today: viewAllRecords: true, no readScope 3
2 viewAllRecords: true + readScope: 'own_and_reports' 3
3 controlviewAllRecords: false + the same readScope 1

Arm 3 is the control that makes arm 2 a real null rather than a dead harness: the same rig detects narrowing when narrowing is reachable.

And the mutation demonstrably reached the running app rather than only the source literal — in arm 2 the stored sys_permission_set.object_permissions row carries readScope: 'own_and_reports', and the depth resolved from that stored row is still org.

Resolver called directly: getEffectiveScope('read', 'crm_opportunity', …) returns org for both arm 1 and arm 2, and own_and_reports only when viewAllRecords is false.

⚠️ Provenance note, stated rather than glossed: the seat filing this card could not re-read the plugin source itself (node_modules is absent from its checkout) and does not claim to have. The three readings above are behavioural and stand without it. The dev additionally reported the short-circuit sitting in @objectstack/plugin-security/dist/index.mjs ahead of the sole runtime read of op.readScope, with @objectstack/plugin-sharing then returning a null read filter for org⚠️ treat those two line references as the dev's reading to be re-verified, not as this card's measurement.

Why it is worth a card — the failure is silent and self-certifying

⭐ The part that turns this from cosmetic into a trap: a capability census that counts hierarchy-valued scope declarations on the deployed shape counts this one. hotcrm#1378's own acceptance criterion was exactly such a census, and it would have gone green on a declaration that changes nothing.

⇒ The failure mode is not "the author gets no benefit". It is:

  1. the author declares a narrowing;
  2. the platform stores it and stays silent;
  3. an audit of the deployed shape reports the capability as exercised;
  4. the read is still org-wide.

An AI author is the most likely victim: readScope is an accepted key in an accepted position, and nothing in the write→validate→deploy→audit loop contradicts the belief that it took effect.

Suggested shapes — ⛔ not a proposal this seat is entitled to make

Recorded because the measurement suggests them; the owning lane decides.

  • Refuse it at publish/validate timeviewAllRecords: true together with a readScope is a contradictory declaration; a loud rejection is the shape that matches "declared = enforced".
  • Or honour it — treat readScope as narrowing viewAllRecords, if org-wide-read-then-narrow is the intended semantics.
  • Or diagnose it — accept but warn, if there is a compatibility reason to keep storing it.

⛔ Whichever is chosen, the current state — accepted, stored, unread, uncounted-as-a-problem, counted as coverage — is the one that is not defensible.

Second, related reading — reported, ⛔ not claimed as part of this defect

On the same rig, the correct shape (viewAllRecords: false + readScope: 'own_and_reports') with sys_user.manager_id genuinely wired still resolved to owner-only, because resolveOwnerScopeIds fails closed without @objectstack/security-enterprise. ⇒ On the open edition the hierarchy read path is unreachable on any grant shape. That may be entirely intended (an enterprise capability degrading closed), and is recorded here only so a triager reading the arms above is not surprised by it. ⛔ It is not this card's claim.

Dedupe — run with a working control

query "permission set accepts readScope alongside viewAllRecords but silently never reads it,
       no diagnostic"                                              → 9 results, none this defect
query "viewAllRecords permission scope resolution in plugin-security" → 2 results, neither this
CONTROL: both queries returned non-empty, so the tool answers on this topic and the zero is real.

Nearest neighbours, all closed and all a different question: #4376 (a feature gap — no way to narrow between all-or-nothing viewAllRecords), #14329 / #13640 / #7988 (read-scope enforcement bypasses, where a scope that is read gets around a gate). ⇒ this card is the inverse: a scope that is never read at all, and no one is told.

⚠️ search_issues is semantic rather than lexical — a bare keyword can return 0 where a sentence-shaped query answers correctly. A further dedupe here should use sentences and pair them with a control.

Refs: objectstack-ai/hotcrm#1378 (where it surfaced, now in that repo's decision box) · objectstack-ai/hotcrm#1755 (the same false-green shape, in a test guard rather than a permission set)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions