Skip to content

finding(app-shell): buildExpressionUser(null) omits id, so a ctx.user.id gate faults and fails OPEN for every signed-out visitor #6534

Description

@os-support-ai

Found while implementing objectui#6515. Filed unassigned, recording only — not graded, no type.

Measured on the objectui#6515 branch, which is origin/main @ 28ed9ce8f plus that card's fix. The defect is older than that branch and unaffected by it: it lives in the normaliser's own anonymous branch, which objectui#6515 does not touch.

What was measured

buildExpressionUser has two branches, and only one of them carries id:

if(!u){return{name: 'Anonymous',email: '',role: 'guest',isPlatformAdmin: false,positions: []};}return{id: u.id,name: u.name,email: u.email,role: ,isPlatformAdmin: ,positions: };

So 'id' in buildExpressionUser(null) === false. A CEL predicate naming ctx.user.id /
current_user.id / os.user.id therefore hits an unbound key for a signed-out visitor,
FAULTS, and evaluateVisibility fails OPEN — the gate renders its field or action for
exactly the principal it was written to exclude, with nothing on screen to say so.

This is the same mechanism objectui#6515 fixed for RecordFormPage, but one level down: it
is in the shared normaliser, so it reaches every mount site — AppContent and the
console's InternalFormRoute included, both of which have always called the normaliser
correctly.

Measured directly, as a test on the #6515 branch
(packages/app-shell/src/providers/expressionUser.mountParity.test.tsx, the case named
RECORDS: a ctx.user.id gate STILL fails open for a signed-out visitor): with
authState.user = null and a field gated on ctx.user.id == 'u_admin', the field is
PRESENT in the schema handed to ObjectForm. The signed-IN case beside it is green — there
id is present and the gate bites.

Why it was not fixed in objectui#6515

That card's face is "make the mount site publish the normaliser's output unchanged". This
would change what the normaliser's output is, which is a different decision and has a
pin standing on it: AppContent.expressionUserShape.test.ts asserts the anonymous branch
toStrictEqual the five-key shape above.

The decision this needs

Three candidate shapes, and the choice is not mechanical:

  • id: null — the key exists and answers. ctx.user.id == '…' evaluates FALSE for a
    signed-out visitor, which is the semantically right answer. Costs: null is a value CEL
    authors then have to know about.
  • id: undefined — rejected on precedent. objectui#5424 removed roles from this exact
    object precisely because present-and-always-undefined "is the shape that teaches the
    wrong thing": the context answers rather than being plainly absent, and the answer is
    silently wrong.
  • Leave it absent — the status quo, i.e. accept fail-open for id-shaped gates on
    anonymous sessions. Defensible only if no anonymous surface evaluates one; that is a
    measurement nobody has taken.

Whichever is chosen, the two branches should end up agreeing on one key set — that
symmetry is what objectui#5424 was closing when it removed roles, and this is the last
place the two branches still disagree.

Related


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions