Skip to content

[finding] The bulk AST write path passes opCtx.operation raw, so purge / transfer / restore derive no row scope while update / delete now do (#7665 asymmetry) #7809

Description

@os-help

Found by the PM while reviewing PR #7792 (#7665). Not a regression — the behaviour described here is the same before and after that PR — but #7665 creates an asymmetry between the two write paths that deserves a deliberate answer rather than an accident.

What #7665 changed, and where it stops

#7665 makes an empty write-class policy collection derive its row scope from the caller's select narrowing, inside computeLayeredRlsFilter. The derivation is written for operation === 'update' || operation === 'delete'.

On the by-id write pre-image gate (step 2.7) that is complete coverage, because the gate normalises first (security-plugin.ts:1487-1489):

const rlsOperation =
  opCtx.operation === 'purge' ? 'delete'
  : opCtx.operation === 'transfer' || opCtx.operation === 'restore' ? 'update'
  : opCtx.operation;

so all six write verbs reach the derivation as update or delete.

On the bulk AST injection path (step 3) there is no such normalisation — opCtx.operation is passed raw:

  • security-plugin.ts:2113computeRlsFilter(permissionSets, opCtx.object, opCtx.operation, opCtx.context)
  • security-plugin.ts:2133 — the ADR-0090 D10 delegator half, same shape

A bulk write arriving as purge, transfer or restore therefore collects no policies for that literal operation name, derives nothing, and has no row scope AND-ed into its AST — while a bulk update or delete on the same select-only object now does.

Why it is worth answering rather than assuming

Two questions, and they have different answers:

  1. Can those verbs reach the AST path at all? If opCtx.ast is only ever populated for find / update / delete, the asymmetry is theoretical and the right fix is a comment saying so. That is a reading job on the engine's dispatch, not a guess.
  2. If they can, then a select-only object has bulk purge / transfer / restore unscoped by row visibility while by-id and bulk update / delete are scoped — the same class of defect [security] A by-id write is not gated by record visibility — a contributor mutates records they cannot read, when only select-scope RLS is authored #7665 fixed, on the verbs whose blast radius is largest (purge destroys; transfer reassigns ownership).

Note the two paths already normalise differently for a reason that may be legitimate — the 2.7 gate is asking "may this caller write THIS row", the AST path is scoping a set — so the fix may be "normalise there too" or may be "these verbs never take the AST path, and here is the assertion that says so".

Related, deliberately not merged into this

Provenance

PM review of PR #7792, verified against origin/main at the time of writing (:1487-1489 normalisation present; :2113 / :2133 raw). Filed unassigned and unqueued — recording, not claiming. Sizing note for whoever takes it: the @objectstack/verify harness gap (#7762) does not apply here, but the coverage would need a bulk-write fixture on a select-only object, which the #7665 suite (select-only-write-visibility.test.ts) already has the personas for.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions