Skip to content

[Element] Add hasViewAccess flag to RelatedElementData based on view permission - #1983

Open
xIrusux wants to merge 5 commits into
2026.2from
fix/related-element-data-has-access
Open

[Element] Add hasViewAccess flag to RelatedElementData based on view permission#1983
xIrusux wants to merge 5 commits into
2026.2from
fix/related-element-data-has-access

Conversation

@xIrusux

@xIrusux xIrusux commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a hasViewAccess boolean to the RelatedElementData schema (default true), telling the
    client whether the current user is allowed to view the referenced element.
  • ElementDataService computes the flag via the core $element->isAllowed('view', $user) check —
    one indexed workspace query per row, no per-row element normalization (see performance note below).
  • Denies by default when no user can be resolved.
  • Ships a Codeception unit test covering allowed / denied / unauthenticated / non-Pimcore-user.

Enables pimcore/studio-ui-bundle#3898 (gate the per-row Open button on hasViewAccess).

Relation to #1872

Reworked from #1872 (thanks @Jonathon-Meney-Torq!), addressing the review feedback there:

  • ElementDataService stays final readonly — no subclassing needed.
  • No GridSearch change — skipping workspace/user restrictions for system.ids filter
    queries would let any authenticated user read grid data for arbitrary elements outside their
    workspace; the endpoint cannot verify the IDs actually come from a relation the user is
    viewing. Dropped entirely.
  • No canEdit / RelationNormalizationContext — the Remove action edits the parent
    object, and the Studio UI already disables it via the parent's save/publish permissions
    (edit-form-provider), so no per-row flag and no mutable normalization-context service is
    needed.

Performance note (resolved)

The first iteration computed the flag through the GDI ElementPermissionServiceInterface, but that
transforms the full element per call — DataObjectNormalizer::normalizeStandardFields() walks every
field definition and triggers lazy loads, per relation row. Since this runs on the object-detail hot
path, 84d5f65 switched to the core isAllowed() check: one cheap indexed users_workspaces_object
query per row, constant cost regardless of class complexity, with precedent in
WorkflowElementsService. Actual security enforcement stays at the (GDI-based) element endpoints —
this flag is a rendering hint.

The unit test mocks the seam, so a later switch to an index-batched approach stays cheap.

Naming

The flag is named hasViewAccess (not hasAccess) to state precisely which permission it reports,
matching the existing vocabulary (Permissions schema, UserWorkspace::hasView()) and keeping the
name unambiguous next to a possible future edit-side flag.

🤖 Generated with Claude Code

…ission

Relation rows carried no permission information, so the Studio UI
rendered per-row actions (e.g. Open) regardless of whether the current
user is allowed to view the referenced element.

Compute hasAccess in ElementDataService through the same GDI
ElementPermissionService seam that SecurityService uses for element
permission checks, denying by default when no user can be resolved.

Reworked from #1872, addressing the review feedback there: the service
stays final, no canEdit / RelationNormalizationContext (parent
save/publish gating already exists in the UI edit form), and no
GridSearch workspace bypass.

Enables pimcore/studio-ui-bundle#3898.

Co-authored-by: Jonathon Meney <253076990+Jonathon-Meney-Torq@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings August 6, 2026 20:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds hasAccess to related-element responses based on the current user’s view permission.

Changes:

  • Extends the related-element schema with hasAccess.
  • Resolves view access through the GDI permission service.
  • Tests allowed, denied, and unauthenticated scenarios.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/Element/Schema/RelatedElementData.php Adds the access flag.
src/Element/Service/ElementDataService.php Computes view access.
tests/Unit/Element/Service/ElementDataServiceTest.php Covers access outcomes.

Comment thread src/Element/Service/ElementDataService.php Outdated
…nsform

getRelatedElementData() runs inside the relation row loops, and the GDI
permission seam normalizes the whole element per call (all class fields,
including lazy loads), which can make object-detail requests
prohibitively slow for field-heavy classes with many relation rows.

Switch to the core AbstractElement::isAllowed() check: one cheap
workspace lookup per row, no transform. Also split the 123-char
Property annotation line flagged by Sonar.

Co-authored-by: Jonathon Meney <253076990+Jonathon-Meney-Torq@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@xIrusux

xIrusux commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Concrete example: a relation field with 20 Car rows, where Car itself has several lazy relations and localized fields → 20 × (full field walk + those lazy loads). That can easily be 100+ queries for one field that previously cost zero.

The real cost, per data-object relation row

ElementPermissionService::isAllowed('view', …)DataObjectToSearchResultItemTransformer::transform() → and here's the problem, in DataObjectNormalizer:

  • normalizeStandardFields() runs unconditionally — the SKIP_LAZY_LOADED_FIELDS flag only guards a handful of system fields (workflow, dependencies, tags), not this.
  • It iterates every field definition of the related object's class and calls $dataObject->get($key) on each (DataObjectNormalizer.php:171-176). Getter access is exactly what triggers Pimcore's lazy loading — so every lazy relation / localized field on each related object can fire its own DB queries. If the class allows inheritance, there's a second walk with inherited values (line 165-168).

Options for the team discussion (ranked as I'd argue them)

  1. Batch via the search index in RelationDataService: one GDI search by the relation IDs with the user; each returned SearchResultItem already carries permissions computed from the same workspace logic — zero model normalization, one index query per relation field. This is the philosophy Martin is defending ("grids stay fast because they read the index"). Caveat: index staleness for just-created/moved elements.
  2. Classic $element->isAllowed('view', $user) per row (what TorqIT's PR did; precedent exists at WorkflowElementsService.php:75): one cheap workspace SQL per row, no transform. Trade-off: two permission engines in play — core vs GDI — with possible edge-case divergence from what the open-element endpoint (GDI-based) enforces.
  3. Keep the GDI seam, add a request-scoped memo keyed by element path in ElementDataService: only helps duplicate rows; the per-distinct-row transform cost remains. Least invasive, least effective.
  4. Ship as-is with the cost documented: fine for typical relation sizes, but a fat class × large relation field is a genuine regression risk.

Update: 84d5f65 switches the implementation to option 2 (core isAllowed(), no per-row transform), addressing the Copilot hot-path finding. Happy to rework to option 1 if the team prefers the index-batched approach.

@xIrusux
xIrusux requested a review from martineiber August 10, 2026 08:26
@martineiber martineiber added this to the 2026.2.7 milestone Aug 20, 2026
@martineiber
martineiber requested a balanced review from Copilot August 20, 2026 09:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/Element/Service/ElementDataService.php:64

  • This contradicts the stated contract that hasAccess uses the same GDI permission seam as the get/open endpoints. Those endpoints call SecurityService::hasElementPermission() (ElementService.php:85,107), which delegates to ElementPermissionServiceInterface, while this invokes Pimcore's core isAllowed() path (including different workflow/event behavior). The two results can therefore diverge, so the UI may disable an element that the endpoint allows, or expose an Open action that still returns 403. Use the endpoint permission path here (mapping ForbiddenException to false) and update the test to mock that seam, or explicitly revise the PR's contract if classic semantics are intentional.
        // Intentionally the core check, not the GDI permission seam: this runs per relation row,
        // and the GDI check normalizes the whole element per call (see PR #1983).
        return $element->isAllowed(ElementPermissions::VIEW_PERMISSION, $user);

martineiber and others added 2 commits August 20, 2026 12:00
The flag specifically reports the view permission; the precise name keeps it
unambiguous next to a possible future edit-side flag and matches the existing
permission vocabulary (Permissions schema, UserWorkspace::hasView()).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@martineiber martineiber changed the title [Element] Add hasAccess flag to RelatedElementData based on view permission [Element] Add hasViewAccess flag to RelatedElementData based on view permission Aug 20, 2026
@martineiber martineiber self-assigned this Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants