You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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):
constallowed=this.permissionEvaluator.checkObjectPermission(opCtx.operation,opCtx.object,permissionSets,{isPrivate: secMeta.isPrivate},);if(!allowed){thrownewPermissionDeniedError(`[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 fine — PERMISSION_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:
English-only, in a UI whose every other string is localized;
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;
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.
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.
Two things the dispatch will require beyond the card's own ask, recorded here so the reasoning is public rather than buried in an envelope:
Whether this fix actually closes the disclosure depends on a fact nobody has measured yet. The card says the structured fields (operation, object, positions) "are the right channel for the machine detail, and they are already there". That is right if the channel is server-side. If the ADR-0112 envelope serialises those fields to the HTTP client, then rewriting only the message leaves app_child_object and the positions vocabulary reaching the same browser through the payload, and the card's stated goal is not met. The dev is asked to measure where those fields land and to say so plainly — the answer changes what "fixed" means here, and it may turn part of this into a second card rather than something to decide inside a copy fix.
⛔ Enforcement is not in scope: the 403 stays a 403, PERMISSION_DENIED stays, the structured payload stays, and the gate's decision logic is untouched. This is user-facing copy only.
If an earlier claim with a different session ID exists in this thread, that one wins and I stand down.
Activity
claude commentedon Aug 10, 2026
Triage:
pm:queue+domain:identity.domain:engine-core) but a different producer in a different package. The copy is live onorigin/main@b948a41atpackages/plugins/plugin-security/src/security-plugin.ts:1160(the[Security] Access denied: operation ... positions ...template). Structured fields plus thePERMISSION_DENIEDcode already exist on the error, so this is user-facing-copy repair with the same shape as DELETE_RESTRICTED (409) message is shown verbatim to end users: English-only, leaks API names, and contains developer-facing advice #7307's.packages/plugins/plugin-security⇒domain:identity.@objectstack/runtimere-declaresPermissionDeniedError"structurally identical" to plugin-security's, with nothing enforcing the identity #7270 (runtime re-declaresPermissionDeniedError) is a different defect (class identity, not copy). Clean.target:v17: display-class copy, same grading as DELETE_RESTRICTED (409) message is shown verbatim to end users: English-only, leaks API names, and contains developer-facing advice #7307 (plugin-audit: renderTrackedChangeSummary never receives the translator, and displayFieldValue prints raw lookup ids — sys_activity.summary shipsRating Owner: ∅ → oBK25…onto every feed surface (re-routed from objectui#4055) #7230-family) — transport and enforcement are correct today.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 10, 2026
Claim — PM loop,
domain:identityseat (execution).session_01BM1tNf5U3nEbHKR4fo5qVQclaude/issue-7414-permission-denied-user-copyobjectstack-issue-7414(dedicated, offorigin/main)domain:identitypackages/plugins/plugin-security/src/**(+ its tests, a changeset, and whatever locale bundle the fix needs)security-plugin.tsmoved twice today — PR docs(plugin-security): restate the upsertEnvPermissionSet JSDoc for ADR-0094 D5-R (#7082) #7346 ([finding]permission-set-projection.ts:332still states the ADR-0094 direction that D5-R retired — PR #6962 replaced the file-header copy and left the function-level one #7082, merged 08:12:09Z, comments only) and PR fix(plugin-security): checkAuthoredRowWrite answers the declaration, not the caller's read scope (#7281) #7400 (checkAuthoredRowWrite abstains on everyprivate-OWD cross-owner row, so #5493's by-id widener deferral is inert on the posture it was filed for — and its unit test cannot see it (fake engine bypasses middleware) #7281, merged 10:21:28Z, thecheckAuthoredRowWriteprobe scope). Both are landed, not in flight; this lane has zero open PRs. DELETE_RESTRICTED (409) message is shown verbatim to end users: English-only, leaks API names, and contains developer-facing advice #7307 is the sibling card but lands inpackages/objectqlunderdomain:engine-core— disjoint package, no lock. Nothing to wait on.Two things the dispatch will require beyond the card's own ask, recorded here so the reasoning is public rather than buried in an envelope:
Whether this fix actually closes the disclosure depends on a fact nobody has measured yet. The card says the structured fields (
operation,object,positions) "are the right channel for the machine detail, and they are already there". That is right if the channel is server-side. If the ADR-0112 envelope serialises those fields to the HTTP client, then rewriting only themessageleavesapp_child_objectand thepositionsvocabulary reaching the same browser through the payload, and the card's stated goal is not met. The dev is asked to measure where those fields land and to say so plainly — the answer changes what "fixed" means here, and it may turn part of this into a second card rather than something to decide inside a copy fix.Mirror DELETE_RESTRICTED (409) message is shown verbatim to end users: English-only, leaks API names, and contains developer-facing advice #7307's shape rather than inventing a second one. Same defect class, adjacent producer; two different remedies for one class is how a codebase ends up with two de-facto contracts (this lane just spent four cards keeping
audit-writers.tsto a single shape). The dev will check DELETE_RESTRICTED (409) message is shown verbatim to end users: English-only, leaks API names, and contains developer-facing advice #7307's state first and follow whatever it established, or say explicitly why this producer cannot.⛔ Enforcement is not in scope: the 403 stays a 403,
PERMISSION_DENIEDstays, the structured payload stays, and the gate's decision logic is untouched. This is user-facing copy only.If an earlier claim with a different session ID exists in this thread, that one wins and I stand down.
Generated by Claude Code
test(plugin-security,spec): pin the new engine double to the producer…
test(runtime): re-spell the share-links 403 pin for the localized mes…
fix(plugin-security,spec): PERMISSION_DENIED stops showing developer …
fix(plugin-security,spec): the end-user-facing `[Security] Access den…
test(spec): the last system/ file group's test titles state each cite…