Skip to content

The runtime dispatcher serialises a PermissionDeniedError's details to the client, so positions / permissionSets reach the browser on the /data transport #7450

Description

@os-zhuang

Found while implementing #7414 (PR #7449). Filed separately because fixing it means changing what a 403 body carries, which #7414 explicitly ruled out of scope (that card is user-facing copy only, structured payload untouched).

What was measured

plugin-security's object gate attaches a structured payload to every PermissionDeniedError:

throw new PermissionDeniedError(
  message,
  { operation: opCtx.operation, object: opCtx.object, positions, permissionSets: explicitPermissionSets },
);

The two transports treat that payload differently, and only one of them was checked when #7414 was written.

@objectstack/restmapDataError, packages/rest/src/rest-server.ts:726-737:

return {
    status: 403,
    body: {
        error: error?.message ?? 'Permission denied',
        code: 'PERMISSION_DENIED',
        ...(object ? { object } : {}),
    },
};

error.details is never read. Nothing structured reaches the client, and the object that does ride is the object the route named, not error.details.object.

@objectstack/runtimehttp-dispatcher.ts:1945-1951:

} catch (e) {
    if (isPermissionDeniedError(e)) {
        return {
            handled: true,
            response: this.error(e.message, 403, { code: 'PERMISSION_DENIED', ...(e.details ?? {}) }),
        };
    }
    throw e;
}

this.error hands that object to apiErrorResponse / buildApiError (packages/runtime/src/error-envelope.ts), which promotes the string code into error.code and puts everything else on the wire as error.details. /data is served by this dispatcher (the domain registry, packages/runtime/src/domains/data.ts), and that domain handler does not catch — so a permission denial on an ordinary CRUD request answers with error.details.positions, error.details.permissionSets, error.details.operation and error.details.object.

So on that transport a client is told the caller's position names, the names of the permission sets attached to their context, and — on a cascade delete, where cascadeDeleteRelations re-authorises each child independently — the API name of a child object the caller never addressed.

Why this matters beyond tidiness

#7414's "Expected" section asserted that the structured fields "are the right channel for the machine detail, and they are already there", and PR #7423 (the sibling DELETE_RESTRICTED card) justified shipping a developerMessage over the wire on the premise that the envelope already carried the same API names. Both readings assume one answer to "where do these fields land". There are two, and they disagree. PR #7449 declined to ship a developer sentence on either transport for exactly this reason, but it deliberately left the existing details disclosure standing because narrowing it is an envelope change, not a copy change.

What a fix would have to decide

  1. Is positions / permissionSets intended client-facing detail at all, or server-side diagnostics that leaked through a generic spread? (operation and object are far less sensitive — the caller supplied both, except on a cascade child.)
  2. If they are not: narrow the spread at the dispatcher (allowlist rather than ...e.details), which is a wire-visible change for any consumer already reading them.
  3. Either way the two transports should agree on what a PERMISSION_DENIED body carries. Today one carries nothing structured and the other carries everything.

Related: #7270 (runtime re-declares PermissionDeniedError with nothing enforcing the identity) touches the same class from the type side.

Reproduction pointers

  • packages/runtime/src/http-dispatcher.ts — the catch at the end of dispatch()
  • packages/runtime/src/error-envelope.tssplitSemanticCode / buildApiError
  • packages/runtime/src/domains/data.ts — no local catch, so the gate's throw reaches the dispatcher catch
  • packages/rest/src/rest-server.tsmapDataError's PERMISSION_DENIED branch, for the contrast
  • packages/rest/src/rest.test.ts — the pin PR fix(plugin-security,spec): PERMISSION_DENIED stops showing developer copy to end users (#7414) #7449 added for the REST half

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions