Skip to content

PermissionDeniedError (403) message is shown verbatim to end users too — English-only, leaks the object API name and the internal positions vocabulary (sibling of #7307, different package) #7414

Description

@baozhoutao

Summary

Sibling of #7307, different producer. #7307 covers the DELETE_RESTRICTED (409) copy thrown by
cascadeDeleteRelations in packages/objectql. The same problem exists for
PermissionDeniedError (403) thrown by packages/plugin-security: its message is surfaced
verbatim to end users by Console's toast, and it is English-only, embeds the machine object name,
and exposes the internal positions vocabulary.

Filing separately rather than commenting on #7307 because the fix sites are different packages
— rewriting the objectql message does not touch this one. Happy for it to be closed as a duplicate
if you'd rather track both under #7307.

Where

packages/plugin-security (observed in @objectstack/plugin-security@17.0.0-rc.5,
dist/index.js, the object-permission gate):

const allowed = this.permissionEvaluator.checkObjectPermission(
  opCtx.operation, opCtx.object, permissionSets, { isPrivate: secMeta.isPrivate },
);
if (!allowed) {
  throw new PermissionDeniedError(
    `[Security] Access denied: operation '${opCtx.operation}' on object '${opCtx.object}' is not permitted for positions [${positions.join(", ")}]`,
    { operation: opCtx.operation, object: opCtx.object, positions, permissionSets: explicitPermissionSets },
  );
}

As with #7307, the transport is finePERMISSION_DENIED maps to a real 403 and the message is
not swallowed into a generic 500. This is purely about the user-facing copy.

Actual behaviour

A downstream business app (names generalised). A user deletes a parent record they are explicitly
authorised to delete; the engine cascades into a child object on which that user's role holds only
read. The cascade's re-authorisation of the child fails, and this red toast appears in an otherwise
fully localized UI:

[Security] Access denied: operation 'delete' on object 'app_child_object'
is not permitted for positions [org_member, everyone]

Three things make it unusable as end-user copy:

  1. English-only, in a UI whose every other string is localized;
  2. app_child_object is a machine name the operator has never seen — the object they acted on
    was the parent, and this names a child they may not even know exists;
  3. positions [org_member, everyone] is internal authorization vocabulary. It also reads as a
    contradiction to the operator, who does have delete rights on the record they clicked.

The operator's takeaway is "the system is broken", not "you lack a permission" — and certainly not
anything actionable.

Why it lands on users more often than it looks

This message is not confined to obviously-unauthorized actions. Because
cascadeDeleteRelations re-enters ObjectQL.delete() for every cascade child, each child is
re-authorised independently. So a parent-level permission that the app deliberately granted can
still produce this 403, naming a child object the user never addressed. Any app where parent and
child object permissions differ — the normal case for derived/snapshot/audit child tables — will
surface it on an ordinary delete.

Expected

Same shape as #7307:

  • an error code (PERMISSION_DENIED) plus structured fields (operation, object, positions)
    are already attached — those are the right channel for the machine detail, and they are already
    there;
  • the message should be a short, localizable, business-readable sentence that does not name
    internal objects or positions;
  • if a single generic string is preferred, "You do not have permission to perform this action."
    plus the existing structured payload is already a strict improvement over the current string.

Related

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions