Skip to content

plugin-security: start() can return before this.logger = ctx.logger, leaving the = {} sink permanent #10706

Description

@os-warren

Observation filed while landing PR #10691 (the #10556 optional-error-sink paydown). Not a claim; unassigned. ⛔ Deliberately not fixed there, and it is independent of the open design call on #10556 about what that default sink should be — it survives whichever way that is ruled.

⚠️This card corrects an earlier, overstated version of the same observation that I reported on #10556. The correction is in the last section, and the corrected severity is materially lower. Recording it here so the wrong version does not outlive the right one.

The mechanic

packages/plugins/plugin-security/src/security-plugin.ts:

  • :722private logger: { info?; warn?; error? } = {} — the field is an empty object from construction.
  • :836this.logger = ctx.logger — the only assignment, inside start().
  • :824 and :829 — two return; statements in start() that sit above :836:
try{ql=ctx.getService<IObjectQLEngine>('objectql');metadata=ctx.getService<IMetadataService>('metadata');}catch(e){ctx.logger.warn('ObjectQL or metadata service not available, security middleware not registered');return;// :824 — :836 never runs}if(!ql||typeofql.registerMiddleware!=='function'){ctx.logger.warn('ObjectQL engine does not support middleware, security middleware not registered');return;// :829 — :836 never runs}

On either path this.logger stays {}for the lifetime of the plugin instance. The field is never reassigned anywhere else (grep -n 'this.logger = ' → one hit).

Note both bail-outs report through ctx.logger, which is a real sink — so the bail-out itself is loud. What is left holding {} is the plugin's own field.

Reachability — the part that needs care

The file has 11this.logger.* report sites. They split into three groups, and only the third is a live concern:

sitesenclosing scopereachable with logger === {}?
:1048, :1069, :2245, :2258, :2428, :2470closures created inside start(), registered at :1175 and on the engine middleware — all after :836No. On an early-return boot they are never created, because registration is also after :836.
:4594, :4935, :4946, :5495private helpers driven by the engine middlewareNo, same reason — the middleware is not registered on those paths.
:3576checkAuthoredRowWrite, a public instance method (:3501, no private)Possibly. It does not depend on registration. A host holding the SecurityPlugin instance can call it directly, and on an early-return boot — or between init() and start() — its this.logger.warn?.() goes nowhere.

⚠️ The third row is not demonstrated, only shown to be structurally possible: no in-repo caller invokes checkAuthoredRowWrite on the instance rather than through the registered security service. Whoever picks this up should establish or refute that first — it is the difference between a latent hazard and a live defect.

Why it is worth a card even at the lower severity

The value of = {} is that it makes "no sink" unrepresentable as a state you can notice: the field always looks assignable and every report site is written this.logger.warn?.(…), so no code path can tell a configured sink from the empty default. The two early returns are what turn a boot-ordering detail into a permanent one. Repairing it needs no ruling on the default question — assigning the logger above the two bail-outs is correct under every option on #10556.

The correction

I first reported this as: "the security service registered back in init() keeps serving, so its six fail-closed reports — including hasWriteBypass failed … denying and the ADR-0123 tenant-wall refusal — go nowhere at all."

That was wrong on two counts, and both were found by mapping line numbers to enclosing methods instead of eyeballing the file:

  1. registerService('security', …) is at :1175, inside start() (which spans :812–:3085), not in init() (:739–:811). On the early-return paths the service is therefore never registered at all — so those reports are unreachable, not silent. "Goes nowhere" implied a report that is emitted and dropped; nothing is emitted.
  2. The count was six; the file has eleventhis.logger.* sites. Six was what a partial grep window showed.

What survives the correction is the mechanic in the first section and the single public-method path above.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions