Skip to content

Roll-up summary recompute runs under the CALLER's context: a granted child insert 500s (ERR_SUMMARY_RECOMPUTE) after the row is already written #7719

Description

@baozhoutao

Summary

A member with a legitimate create grant on a CHILD object gets HTTP 500 from POST /api/v1/data/<child>after the row has already been written — whenever the child feeds a roll-up summary on a PARENT record the member may not write. The roll-up recompute runs under the caller's execution context, so updating the parent's summary fields raises PermissionDeniedError, which surfaces as SummaryRecomputeError / ERR_SUMMARY_RECOMPUTE.

The caller sees a 500 for a write that succeeded. A client that retries (or a user who clicks Save again) creates a duplicate row.

Repro (real runtime, examples/app-showcase)

  1. Boot the showcase, sign in as admin, sign up a plain member (holds only the everyone baseline showcase_member_default, which grants showcase_task: create + read).
  2. As that member:
POST /api/v1/data/showcase_task
{ "title": "SummaryBugProbe-1", "project": "<a project owned by someone else>",
  "assignee": "<member email>", "status": "todo", "priority": "low" }
  1. Response: 500 {"error":"Internal server error","code":"INTERNAL_ERROR"}
  2. As admin: GET /api/v1/data/showcase_task?filters=[["title","=","SummaryBugProbe-1"]]1 row. The write landed.

Server log for that request:

ERROR Insert operation failed {"object":"showcase_task", …
  SummaryRecomputeError: Roll-up summary recompute failed after retries for 2 parent record(s);
  the triggering records WERE written (summary values may be stale).
  failures: [
    { childObject: 'showcase_task', parentObject: 'showcase_project',
      parentId: '6nljsw5hhuT_DPUL', field: 'task_count',      error: [PermissionDeniedError] },
    { childObject: 'showcase_task', parentObject: 'showcase_project',
      parentId: '6nljsw5hhuT_DPUL', field: 'total_estimate',  error: [PermissionDeniedError] }
  ]
  code: 'ERR_SUMMARY_RECOMPUTE'

PATCH on the member's own child row fails the same way (500, row updated).

Cause

packages/objectql/src/engine.ts passes the caller's context straight into the recompute:

  • L7578 — const summaryFailures = await this.recomputeSummaries(object, result, null, opCtx.context);
  • L8478 / L9174 — the update paths do the same.

So the parent write inherits the caller's permissions. But a roll-up is engine-derived state, not a user write: whether showcase_project.task_count may be refreshed is not a question about the member's grant on showcase_project. The retry loop (framework#3147) cannot help — a permission denial is deterministic, so it burns every retry and then throws.

Impact

Any parent/child pair where the child is more widely writable than the parent — the ordinary shape for line items, tasks, comments, time entries. The member's own create is a legitimate, granted operation; it 500s on a permission check about a record they never asked to touch, and the failure is not atomic (row written, error returned).

Suggested direction

Recompute summaries under a system context (the recompute is engine-owned state, and the permission decision that matters — may the caller write the CHILD — has already been made), and keep SummaryRecomputeError for genuinely transient failures. If a caller-scoped recompute must stay for some reason, a permission denial should not be retried and should not fail the caller's otherwise-successful write.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions