Skip to content

Updated performAccessCheck doc - #2959

Merged
mnocon merged 8 commits into
5.0from
fix-performAccessCheck
Oct 1, 2026
Merged

mnocon merged 8 commits into
5.0from
fix-performAccessCheck

Conversation

@mnocon

@mnocon mnocon commented Nov 25, 2025 •

Copy link
Copy Markdown
Contributor

Things done:

  1. CustomController is renamed to CustomLimitationController
  2. I've decided that instead of splicing a new code sample for the doc with many include_file calls, it's cheaper to create a new short code sample based on the existing one. Reasoning: PHPStan and Rector make the maintenence of PHP files easier than fixing fragmented include_file calls manually.

@mnocon
mnocon force-pushed the fix-performAccessCheck branch from 8451cec to f2b1166 Compare November 25, 2025 15:08
@github-actions

github-actions Bot commented Nov 25, 2025 •

Copy link
Copy Markdown

@mnocon mnocon self-assigned this Nov 25, 2025
Comment thread docs/permissions/permission_overview.md Outdated
@mnocon
mnocon marked this pull request as ready for review November 26, 2025 08:21
@mnocon
mnocon requested a review from adriendupuis November 26, 2025 08:22
Comment thread docs/permissions/permission_overview.md Outdated
Comment on lines +39 to +40
In the following example the user doesn't have access to the controller unless they have the `section/view` policy and are [logged in using the "rememeber me cookie"]([[= symfony_doc =]]/security.html#checking-to-see-if-a-user-is-logged-in).
It uses the [`AuthenticatedRememberedCheckTrait`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-User-Controller-AuthenticatedRememberedCheckTrait.html) for the latter check.

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.

In the description paragraph,
the policy must match example's new Attribute('custom_module', 'custom_function_2'),
the functions should be introduced in the same order than they're used in the example,
and there was a typo in "rememeber".

Suggested change
In the following example the user doesn't have access to the controller unless they have the `section/view` policy and are [logged in using the "rememeber me cookie"]([[= symfony_doc =]]/security.html#checking-to-see-if-a-user-is-logged-in).
It uses the [`AuthenticatedRememberedCheckTrait`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-User-Controller-AuthenticatedRememberedCheckTrait.html) for the latter check.
In the following example the user doesn't have access to the controller unless they are [logged in using the "remember me cookie"]([[= symfony_doc =]]/security.html#checking-to-see-if-a-user-is-logged-in) and have the `custom_module/custom_function_2` policy.
It uses the [`AuthenticatedRememberedCheckTrait::performAccessCheck()`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-User-Controller-AuthenticatedRememberedCheckTrait.html#method_performAccessCheck) for the cookie check.

I still don't get why we need to use the trait directly when we extends Admin UI Controller which already uses it so parent::performAccessCheck would work. https://doc.ibexa.co/en/5.0/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Controller-Controller.html.

Where you implements the RestrictedControllerInterface you don't inherit performAccessCheck, got to implement it, and to use the trait's one, here you need the method alias to have a method of the same name and still use it.

Comment on lines +5 to +18
use App\Security\Limitation\CustomLimitationValue;
use Ibexa\Contracts\AdminUi\Controller\Controller;
use Ibexa\Contracts\AdminUi\Permission\PermissionCheckerInterface;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\User\Controller\AuthenticatedRememberedCheckTrait;
use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class CustomLimitationController extends Controller
{
use AuthenticatedRememberedCheckTrait {
AuthenticatedRememberedCheckTrait::performAccessCheck as public traitPerformAccessCheck;
}

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.

Maybe it's a front office controller and we don't want to extend that. Could it simply implements the RestrictedControllerInterface?

Suggested change
use App\Security\Limitation\CustomLimitationValue;
use Ibexa\Contracts\AdminUi\Controller\Controller;
use Ibexa\Contracts\AdminUi\Permission\PermissionCheckerInterface;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\User\Controller\AuthenticatedRememberedCheckTrait;
use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class CustomLimitationController extends Controller
{
use AuthenticatedRememberedCheckTrait {
AuthenticatedRememberedCheckTrait::performAccessCheck as public traitPerformAccessCheck;
}
use App\Security\Limitation\CustomLimitationValue;
use Ibexa\Contracts\AdminUi\Permission\PermissionCheckerInterface;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\User\Controller\AuthenticatedRememberedCheckTrait;
use Ibexa\Contracts\User\Controller\RestrictedControllerInterface
use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class CustomLimitationController implements RestrictedControllerInterface
{
use AuthenticatedRememberedCheckTrait {
AuthenticatedRememberedCheckTrait::performAccessCheck as public traitPerformAccessCheck;
}

Comment thread docs/permissions/permission_overview.md Outdated
You can control access to a custom controller by implementing the [`RestrictedControllerInterface`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-User-Controller-RestrictedControllerInterface.html) interface directly or, for back office controllers, by extending the [`\Ibexa\Contracts\AdminUi\Controller\Controller`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Controller-Controller.html) class.

In the following example the user doesn't have access to the controller unless they have the `section/view` policy:
In the following example the user doesn't have access to the controller unless they have the `section/view` policy and are [logged in using the "rememeber me cookie"]([[= symfony_doc =]]/security.html#checking-to-see-if-a-user-is-logged-in).

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.

Suggested change
In the following example the user doesn't have access to the controller unless they have the `section/view` policy and are [logged in using the "rememeber me cookie"]([[= symfony_doc =]]/security.html#checking-to-see-if-a-user-is-logged-in).
In the following example, the user doesn't have access to the controller unless they have the `section/view` policy and are [logged in using the "remember me" cookie]([[= symfony_doc =]]/security.html#checking-to-see-if-a-user-is-logged-in).

@mnocon
mnocon force-pushed the fix-performAccessCheck branch from a5a560a to 6efac8c Compare September 29, 2026 17:51
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Summary

Status Count
🔍 Total 750791
🔗 Unique 15091
✅ Successful 6285
⏳ Timeouts 0
🔀 Redirected 27
👻 Excluded 744470
❓ Unknown 0
🚫 Errors 36
⛔ Unsupported 0

Errors per input

Errors in site/cdp/cdp_activation/cdp_data_export/index.html

Errors in site/content_management/field_types/field_type_reference/urlfield/index.html

Errors in site/getting_started/requirements/index.html

Errors in site/ibexa_cloud/ibexa_cloud_guide/index.html

Errors in site/ibexa_products/ibexa_commerce/index.html

Errors in site/ibexa_products/ibexa_experience/index.html

Errors in site/ibexa_products/ibexa_headless/index.html

Errors in site/infrastructure_and_maintenance/cache/http_cache/reverse_proxy/index.html

Errors in site/infrastructure_and_maintenance/security/reporting_issues/index.html

Errors in site/product_catalog/quable/install_quable/index.html

Errors in site/product_catalog/quable/quable_api/index.html

Errors in site/recommendations/raptor_integration/connector_installation_configuration/index.html

Errors in site/recommendations/raptor_integration/raptor_connector/index.html

Errors in site/recommendations/raptor_integration/raptor_connector_guide/index.html

Errors in site/release_notes/cohesivo_v6.0_deprecations/index.html

Errors in site/release_notes/ez_platform_v1.10.0/index.html

Errors in site/release_notes/ez_platform_v1.11.0/index.html

Errors in site/release_notes/ez_platform_v1.12.0/index.html

Errors in site/release_notes/ez_platform_v1.13.0_lts/index.html

Errors in site/release_notes/ez_platform_v1.7.0_lts/index.html

Errors in site/release_notes/ez_platform_v1.8.0/index.html

Errors in site/release_notes/ez_platform_v1.9.0/index.html

Errors in site/release_notes/ibexa_dxp_v4.0/index.html

Errors in site/release_notes/ibexa_dxp_v4.4/index.html

Errors in site/release_notes/ibexa_dxp_v4.6/index.html

Errors in site/release_notes/ibexa_dxp_v5.0/index.html

Errors in site/resources/new_in_doc/index.html

Redirects per input

Redirects in site/cdp/cdp_activation/cdp_data_export/index.html

Redirects in site/content_management/field_types/field_type_reference/urlfield/index.html

Redirects in site/getting_started/requirements/index.html

Redirects in site/ibexa_cloud/ibexa_cloud_guide/index.html

Redirects in site/ibexa_products/ibexa_commerce/index.html

Redirects in site/ibexa_products/ibexa_experience/index.html

Redirects in site/ibexa_products/ibexa_headless/index.html

Redirects in site/infrastructure_and_maintenance/cache/http_cache/reverse_proxy/index.html

Redirects in site/infrastructure_and_maintenance/security/reporting_issues/index.html

Redirects in site/product_catalog/quable/quable_api/index.html

Redirects in site/release_notes/cohesivo_v6.0_deprecations/index.html

Redirects in site/release_notes/ez_platform_v1.13.0_lts/index.html

Redirects in site/release_notes/ez_platform_v1.7.0_lts/index.html

Redirects in site/release_notes/ibexa_dxp_v4.0/index.html

Redirects in site/release_notes/ibexa_dxp_v4.4/index.html

Redirects in site/release_notes/ibexa_dxp_v5.0/index.html

Redirects in site/resources/new_in_doc/index.html

Full Github Actions output

@mnocon

mnocon commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

@adriendupuis I've reworked this example so that:

  • one Controller is a frontend controller implementing RestrictedControllerInterface, and NOT extending our Controller
  • second Controller is a back office controller, extending the AdminUI
    Tested the behavior on a local instance:
URL User Expected
/custom-controller anonymous redirect to login
/custom-controller logged in, no section/view 403
/custom-controller logged in, with section/view 200, "Access granted"
/admin/custom-limitation anonymous redirect to login
/admin/custom-limitation logged in, no custom_module/custom_function_2 403
/admin/custom-limitation logged in, with the policy 200
/custom-limitation (the site siteaccess) anyone 404, "not allowed in the current SiteAccess"

Comment thread docs/permissions/permission_overview.md Outdated

In the following example the user doesn't have access to the controller unless they have the `section/view` policy:
In the following example, the user doesn't have access to the controller unless they are [logged in]([[= symfony_doc =]]/security.html#checking-to-see-if-a-user-is-logged-in) and have the `section/view` policy.
The controller uses [`AuthenticatedRememberedCheckTrait::performAccessCheck()`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-User-Controller-AuthenticatedRememberedCheckTrait.html#method_performAccessCheck), aliases as `traitPerformAccessCheck()`, for the login check.

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.

"aliased as" maybe?

@adriendupuis adriendupuis 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.

I ❤️ it

$this->denyAccessUnlessGranted(new Attribute('section', 'view'));
}
``` php hl_lines="14-16 18-23"
[[= include_code('code_samples/back_office/limitation/src/Controller/CustomController.php') =]]

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.

OK, I understand why a renamed file is in fact still there 😅

[[= include_code('code_samples/back_office/limitation/src/Controller/CustomLimitationController.php') =]]
```

The `siteaccess_group_whitelist` route default limits the route to the back office SiteAccess group.

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.

Really good to explain that part.

Comment thread docs/permissions/custom_policies.md Outdated
Co-authored-by: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

code_samples/ change report

Before (on target branch)After (in current PR)

code_samples/back_office/limitation/src/Controller/CustomController.php


code_samples/back_office/limitation/src/Controller/CustomController.php

docs/permissions/custom_policies.md@260:``` php
docs/permissions/custom_policies.md@261:[[= include_code('code_samples/back_office/limitation/src/Controller/CustomController.php') =]]
docs/permissions/permission_overview.md@42:``` php hl_lines="14-16 18-23"
docs/permissions/permission_overview.md@43:[[= include_code('code_samples/back_office/limitation/src/Controller/CustomController.php') =]]
docs/permissions/permission_overview.md@44:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Controller;
004⫶
005⫶use Ibexa\Contracts\User\Controller\AuthenticatedRememberedCheckTrait;
006⫶use Ibexa\Contracts\User\Controller\RestrictedControllerInterface;
007⫶use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute;
008⫶use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
009⫶use Symfony\Component\HttpFoundation\Response;
010⫶use Symfony\Component\Routing\Attribute\Route;
011⫶
012⫶class CustomController extends AbstractController implements RestrictedControllerInterface
013⫶{
014❇️ use AuthenticatedRememberedCheckTrait {
015❇️ AuthenticatedRememberedCheckTrait::performAccessCheck as private traitPerformAccessCheck;
016❇️ }
017⫶
018❇️ #[\Override]
019❇️ public function performAccessCheck(): void
020❇️ {
021❇️ $this->traitPerformAccessCheck();
022❇️ $this->denyAccessUnlessGranted(new Attribute('section', 'view'));
023❇️ }
024⫶
025⫶ #[Route('/custom-controller', name: 'app.custom_controller')]
026⫶ public function customAction(): Response
027⫶ {
028⫶ return new Response('<html><body>Access granted</body></html>');
029⫶ }
030⫶}


code_samples/back_office/limitation/src/Controller/CustomLimitationController.php

docs/permissions/custom_policies.md@260:```php
docs/permissions/custom_policies.md@261:[[= include_code('code_samples/back_office/limitation/src/Controller/CustomLimitationController.php') =]]
docs/permissions/custom_policies.md@262:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Controller;
004⫶
005⫶use App\Security\Limitation\CustomLimitationValue;
006⫶use Ibexa\Contracts\AdminUi\Controller\Controller;
007⫶use Ibexa\Contracts\AdminUi\Permission\PermissionCheckerInterface;
008⫶use Ibexa\Contracts\Core\Repository\PermissionResolver;
docs/permissions/custom_policies.md@262:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\Controller;
004⫶
005⫶use App\Security\Limitation\CustomLimitationValue;
006⫶use Ibexa\Contracts\AdminUi\Controller\Controller;
007⫶use Ibexa\Contracts\AdminUi\Permission\PermissionCheckerInterface;
008⫶use Ibexa\Contracts\Core\Repository\PermissionResolver;
009⫶use Ibexa\Contracts\User\Controller\AuthenticatedRememberedCheckTrait;
010⫶use Ibexa\Contracts\User\Controller\RestrictedControllerInterface;
011⫶use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute;
012⫶use Symfony\Component\HttpFoundation\Request;
013⫶use Symfony\Component\HttpFoundation\Response;
014⫶
015⫶class CustomController extends Controller implements RestrictedControllerInterface
016⫶{
017⫶ use AuthenticatedRememberedCheckTrait {
018⫶ AuthenticatedRememberedCheckTrait::performAccessCheck as public traitPerformAccessCheck;
019⫶ }
020⫶
021⫶ public function __construct(
022⫶ // ...,
023⫶ private readonly PermissionResolver $permissionResolver,
024⫶ private readonly PermissionCheckerInterface $permissionChecker
025⫶ ) {
026⫶ }
027⫶
028⫶ // Controller actions...
009⫶use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute;
010⫶use Symfony\Component\HttpFoundation\Request;
011⫶use Symfony\Component\HttpFoundation\Response;
012⫶use Symfony\Component\Routing\Attribute\Route;
013⫶
014⫶class CustomLimitationController extends Controller
015⫶{
016⫶ public function __construct(
017⫶ // ...,
018⫶ private readonly PermissionResolver $permissionResolver,
019⫶ private readonly PermissionCheckerInterface $permissionChecker
020⫶ ) {
021⫶ }
022⫶
023⫶ // Controller actions...
024⫶ #[Route(
025⫶ '/custom-limitation',
026⫶ name: 'app.custom_limitation',
027⫶ defaults: ['siteaccess_group_whitelist' => '%admin_group_name%']
028⫶ )]
029⫶    public function customAction(Request $request): Response
030⫶ {
031⫶ // ...
032⫶ if ($this->getCustomLimitationValue()) {
033⫶ // Action only for user having the custom limitation checked
034⫶ }
035⫶
036⫶ return new Response('<html><body>...</body></html>');
037⫶ }
038⫶
039⫶ private function getCustomLimitationValue(): bool
040⫶ {
041⫶ $hasAccess = $this->permissionResolver->hasAccess('custom_module', 'custom_function_2');
042⫶
043⫶ if (is_bool($hasAccess)) {
044⫶ return $hasAccess;
045⫶ }
046⫶
047⫶ $customLimitationValues = $this->permissionChecker->getRestrictions(
048⫶ $hasAccess,
049⫶ CustomLimitationValue::class
050⫶ );
051⫶
052⫶ return $customLimitationValues['value'] ?? false;
053⫶ }
054⫶
055⫶ #[\Override]
056⫶ public function performAccessCheck(): void
057⫶ {
029⫶    public function customAction(Request $request): Response
030⫶ {
031⫶ // ...
032⫶ if ($this->getCustomLimitationValue()) {
033⫶ // Action only for user having the custom limitation checked
034⫶ }
035⫶
036⫶ return new Response('<html><body>...</body></html>');
037⫶ }
038⫶
039⫶ private function getCustomLimitationValue(): bool
040⫶ {
041⫶ $hasAccess = $this->permissionResolver->hasAccess('custom_module', 'custom_function_2');
042⫶
043⫶ if (is_bool($hasAccess)) {
044⫶ return $hasAccess;
045⫶ }
046⫶
047⫶ $customLimitationValues = $this->permissionChecker->getRestrictions(
048⫶ $hasAccess,
049⫶ CustomLimitationValue::class
050⫶ );
051⫶
052⫶ return $customLimitationValues['value'] ?? false;
053⫶ }
054⫶
055⫶ #[\Override]
056⫶ public function performAccessCheck(): void
057⫶ {
058⫶        $this->traitPerformAccessCheck();
058⫶        parent::performAccessCheck();
059⫶        $this->denyAccessUnlessGranted(new Attribute('custom_module', 'custom_function_2'));
060⫶ }
061⫶}

059⫶        $this->denyAccessUnlessGranted(new Attribute('custom_module', 'custom_function_2'));
060⫶ }
061⫶}


code_samples/back_office/limitation/src/Controller/CustomLimitationController.php

Download colorized diff

@mnocon
mnocon merged commit d6023b4 into 5.0 Oct 1, 2026
10 of 11 checks passed
@mnocon
mnocon deleted the fix-performAccessCheck branch October 1, 2026 14:12
mnocon added a commit that referenced this pull request Oct 1, 2026
* Updated performAccessCheck doc

* Selfreview

* Update docs/permissions/permission_overview.md

* Review feedback

* Fixed typo

* Update docs/permissions/custom_policies.md

Co-authored-by: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com>

---------

Co-authored-by: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants