From 8498460e1e7a42298aa9e215e3e980e62f63dd6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Noco=C5=84?= Date: Tue, 25 Nov 2025 16:02:50 +0100 Subject: [PATCH 1/6] Updated performAccessCheck doc --- .../src/Controller/CustomController.php | 43 +------------- .../Controller/CustomLimitationController.php | 59 +++++++++++++++++++ docs/permissions/custom_policies.md | 4 +- docs/permissions/permission_overview.md | 17 ++---- 4 files changed, 68 insertions(+), 55 deletions(-) create mode 100644 code_samples/back_office/limitation/src/Controller/CustomLimitationController.php diff --git a/code_samples/back_office/limitation/src/Controller/CustomController.php b/code_samples/back_office/limitation/src/Controller/CustomController.php index 4e60672cf5c..e94a847e0d2 100644 --- a/code_samples/back_office/limitation/src/Controller/CustomController.php +++ b/code_samples/back_office/limitation/src/Controller/CustomController.php @@ -2,57 +2,16 @@ namespace App\Controller; -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\Contracts\User\Controller\RestrictedControllerInterface; use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -class CustomController extends Controller implements RestrictedControllerInterface +class CustomController extends Controller { use AuthenticatedRememberedCheckTrait { AuthenticatedRememberedCheckTrait::performAccessCheck as public traitPerformAccessCheck; } - public function __construct( - // ..., - private readonly PermissionResolver $permissionResolver, - private readonly PermissionCheckerInterface $permissionChecker - ) { - } - - // Controller actions... - public function customAction(Request $request): Response - { - // ... - if ($this->getCustomLimitationValue()) { - // Action only for user having the custom limitation checked - } - - return new Response('...'); - } - - private function getCustomLimitationValue(): bool - { - $hasAccess = $this->permissionResolver->hasAccess('custom_module', 'custom_function_2'); - - if (is_bool($hasAccess)) { - return $hasAccess; - } - - $customLimitationValues = $this->permissionChecker->getRestrictions( - $hasAccess, - CustomLimitationValue::class - ); - - return $customLimitationValues['value'] ?? false; - } - - #[\Override] public function performAccessCheck(): void { $this->traitPerformAccessCheck(); diff --git a/code_samples/back_office/limitation/src/Controller/CustomLimitationController.php b/code_samples/back_office/limitation/src/Controller/CustomLimitationController.php new file mode 100644 index 00000000000..0f698afa0db --- /dev/null +++ b/code_samples/back_office/limitation/src/Controller/CustomLimitationController.php @@ -0,0 +1,59 @@ +getCustomLimitationValue()) { + // Action only for user having the custom limitation checked + } + + return new Response('...'); + } + + private function getCustomLimitationValue(): bool + { + $hasAccess = $this->permissionResolver->hasAccess('custom_module', 'custom_function_2'); + + if (is_bool($hasAccess)) { + return $hasAccess; + } + + $customLimitationValues = $this->permissionChecker->getRestrictions( + $hasAccess, + CustomLimitationValue::class + ); + + return $customLimitationValues['value'] ?? false; + } + + public function performAccessCheck(): void + { + $this->traitPerformAccessCheck(); + $this->denyAccessUnlessGranted(new Attribute('custom_module', 'custom_function_2')); + } +} diff --git a/docs/permissions/custom_policies.md b/docs/permissions/custom_policies.md index ac1aff60331..b142495d5c0 100644 --- a/docs/permissions/custom_policies.md +++ b/docs/permissions/custom_policies.md @@ -257,8 +257,8 @@ For example, `translations/ibexa_content_forms_policies.en.yaml`: Check if current user has this custom limitation set to true from a custom controller: -``` php -[[= include_code('code_samples/back_office/limitation/src/Controller/CustomController.php') =]] +```php +[[= include_code('code_samples/back_office/limitation/src/Controller/CustomLimitationController.php') =]] ``` ## Restrict access to form submissions diff --git a/docs/permissions/permission_overview.md b/docs/permissions/permission_overview.md index 5f89078342d..ac88341fc7b 100644 --- a/docs/permissions/permission_overview.md +++ b/docs/permissions/permission_overview.md @@ -34,23 +34,18 @@ The more role assignments and complex policies you add for a given user, the mor ## Permissions for custom controllers -You can control access to a custom controller by implementing the `performAccessCheck()` method. +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 inheriting from [`\Ibexa\Contracts\AdminUi\Controller\Controller`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Controller-Controller.html). -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). +It uses the [`AuthenticatedRememberedCheckTrait`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-User-Controller-AuthenticatedRememberedCheckTrait.html) to reuse the existing code. -``` php {skip-validation} -use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute; - -public function performAccessCheck(): void -{ - parent::performAccessCheck(); - $this->denyAccessUnlessGranted(new Attribute('section', 'view')); -} +``` php hl_lines="17-18" +[[= include_file('code_samples/back_office/limitation/src/Controller/CustomController.php', 0, 20) =]] ``` `Attribute` accepts three arguments: -- `module` is the policy module (for example,`content`) +- `module` is the policy module (for example, `content`) - `function` is the function inside the module (for example, `read`) - `limitations` are optional limitations to check against. Here you can provide two keys: - `valueObject` is the object you want to check for, for example `ContentInfo`. From e60738fd0be3cc06a3d462befe927f60006bb8de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Noco=C5=84?= Date: Tue, 25 Nov 2025 20:11:40 +0100 Subject: [PATCH 2/6] Selfreview --- docs/permissions/permission_overview.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/permissions/permission_overview.md b/docs/permissions/permission_overview.md index ac88341fc7b..59e889da5c7 100644 --- a/docs/permissions/permission_overview.md +++ b/docs/permissions/permission_overview.md @@ -34,12 +34,12 @@ The more role assignments and complex policies you add for a given user, the mor ## Permissions for custom controllers -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 inheriting from [`\Ibexa\Contracts\AdminUi\Controller\Controller`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-AdminUi-Controller-Controller.html). +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 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) to reuse the existing code. +It uses the [`AuthenticatedRememberedCheckTrait`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-User-Controller-AuthenticatedRememberedCheckTrait.html) to for the latter check. -``` php hl_lines="17-18" +``` php hl_lines="15-19" [[= include_file('code_samples/back_office/limitation/src/Controller/CustomController.php', 0, 20) =]] ``` From d94bf21aaeb3cb2dad82b54961937e338ca05c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Noco=C5=84?= Date: Wed, 26 Nov 2025 09:21:45 +0100 Subject: [PATCH 3/6] Update docs/permissions/permission_overview.md --- docs/permissions/permission_overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/permissions/permission_overview.md b/docs/permissions/permission_overview.md index 59e889da5c7..71c5d15ad37 100644 --- a/docs/permissions/permission_overview.md +++ b/docs/permissions/permission_overview.md @@ -37,7 +37,7 @@ The more role assignments and complex policies you add for a given user, the mor 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 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) to for the latter check. +It uses the [`AuthenticatedRememberedCheckTrait`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-User-Controller-AuthenticatedRememberedCheckTrait.html) for the latter check. ``` php hl_lines="15-19" [[= include_file('code_samples/back_office/limitation/src/Controller/CustomController.php', 0, 20) =]] From 6efac8c16e61bb4268cb6751ed4e95848410ba3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Noco=C5=84?= Date: Tue, 29 Sep 2026 19:51:18 +0200 Subject: [PATCH 4/6] Review feedback --- .../src/Controller/CustomController.php | 18 ++++++++++++++---- .../Controller/CustomLimitationController.php | 14 ++++++++------ .../Form/FormSubmissionServiceDecorator.php | 8 ++++---- deptrac.baseline.yaml | 2 ++ .../request_lifecycle.md | 1 + docs/permissions/custom_policies.md | 3 +++ docs/permissions/limitation_reference.md | 2 ++ docs/permissions/permission_overview.md | 11 +++++++---- 8 files changed, 41 insertions(+), 18 deletions(-) diff --git a/code_samples/back_office/limitation/src/Controller/CustomController.php b/code_samples/back_office/limitation/src/Controller/CustomController.php index e94a847e0d2..67f4c865991 100644 --- a/code_samples/back_office/limitation/src/Controller/CustomController.php +++ b/code_samples/back_office/limitation/src/Controller/CustomController.php @@ -2,19 +2,29 @@ namespace App\Controller; -use Ibexa\Contracts\AdminUi\Controller\Controller; use Ibexa\Contracts\User\Controller\AuthenticatedRememberedCheckTrait; +use Ibexa\Contracts\User\Controller\RestrictedControllerInterface; use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; -class CustomController extends Controller +class CustomController extends AbstractController implements RestrictedControllerInterface { use AuthenticatedRememberedCheckTrait { - AuthenticatedRememberedCheckTrait::performAccessCheck as public traitPerformAccessCheck; + AuthenticatedRememberedCheckTrait::performAccessCheck as private traitPerformAccessCheck; } + #[\Override] public function performAccessCheck(): void { $this->traitPerformAccessCheck(); - $this->denyAccessUnlessGranted(new Attribute('custom_module', 'custom_function_2')); + $this->denyAccessUnlessGranted(new Attribute('section', 'view')); + } + + #[Route('/custom-controller', name: 'app.custom_controller')] + public function customAction(): Response + { + return new Response('Access granted'); } } diff --git a/code_samples/back_office/limitation/src/Controller/CustomLimitationController.php b/code_samples/back_office/limitation/src/Controller/CustomLimitationController.php index 0f698afa0db..1e4895d1179 100644 --- a/code_samples/back_office/limitation/src/Controller/CustomLimitationController.php +++ b/code_samples/back_office/limitation/src/Controller/CustomLimitationController.php @@ -6,17 +6,13 @@ 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; +use Symfony\Component\Routing\Attribute\Route; class CustomLimitationController extends Controller { - use AuthenticatedRememberedCheckTrait { - AuthenticatedRememberedCheckTrait::performAccessCheck as public traitPerformAccessCheck; - } - public function __construct( // ..., private readonly PermissionResolver $permissionResolver, @@ -25,6 +21,11 @@ public function __construct( } // Controller actions... + #[Route( + '/custom-limitation', + name: 'app.custom_limitation', + defaults: ['siteaccess_group_whitelist' => '%admin_group_name%'] + )] public function customAction(Request $request): Response { // ... @@ -51,9 +52,10 @@ private function getCustomLimitationValue(): bool return $customLimitationValues['value'] ?? false; } + #[\Override] public function performAccessCheck(): void { - $this->traitPerformAccessCheck(); + parent::performAccessCheck(); $this->denyAccessUnlessGranted(new Attribute('custom_module', 'custom_function_2')); } } diff --git a/code_samples/back_office/limitation/src/Security/Form/FormSubmissionServiceDecorator.php b/code_samples/back_office/limitation/src/Security/Form/FormSubmissionServiceDecorator.php index 2f0328b8765..b231a327a5b 100644 --- a/code_samples/back_office/limitation/src/Security/Form/FormSubmissionServiceDecorator.php +++ b/code_samples/back_office/limitation/src/Security/Form/FormSubmissionServiceDecorator.php @@ -16,10 +16,10 @@ class FormSubmissionServiceDecorator implements FormSubmissionServiceInterface { public function __construct( - readonly FormSubmissionServiceInterface $innerService, - readonly PermissionResolver $permissionResolver, - readonly ContentService $contentService, - readonly FormSubmissionGateway $gateway, + public readonly FormSubmissionServiceInterface $innerService, + public readonly PermissionResolver $permissionResolver, + public readonly ContentService $contentService, + public readonly FormSubmissionGateway $gateway, ) { } diff --git a/deptrac.baseline.yaml b/deptrac.baseline.yaml index 4a0b3e6f11d..ee925d54e95 100644 --- a/deptrac.baseline.yaml +++ b/deptrac.baseline.yaml @@ -86,6 +86,8 @@ deptrac: App\Controller\CustomFilterController: - Ibexa\Bundle\Core\Controller - Ibexa\Core\MVC\Symfony\View\ContentView + App\Controller\CustomLimitationController: + - Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute App\Controller\PaginationController: - Ibexa\Bundle\Core\Controller - Ibexa\Core\Pagination\Pagerfanta\ContentSearchAdapter diff --git a/docs/infrastructure_and_maintenance/request_lifecycle.md b/docs/infrastructure_and_maintenance/request_lifecycle.md index bd403c5ae24..b6f47bed5c3 100644 --- a/docs/infrastructure_and_maintenance/request_lifecycle.md +++ b/docs/infrastructure_and_maintenance/request_lifecycle.md @@ -111,6 +111,7 @@ If it finds a location, the request receives the attributes **`locationId`** and The `locale_listener` (priority 16) sets the request's **`_locale`** attribute. + !!! note "Permission control" Another `kernel.request` event listener is the `Ibexa\AdminUi\EventListener\RequestListener` (priority 13). diff --git a/docs/permissions/custom_policies.md b/docs/permissions/custom_policies.md index b142495d5c0..c60b13083b1 100644 --- a/docs/permissions/custom_policies.md +++ b/docs/permissions/custom_policies.md @@ -261,6 +261,9 @@ Check if current user has this custom limitation set to true from a custom contr [[= 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. +For more information, see [Request lifecycle](../infrastructure_and_maintenance/request_lifecycle.md#siteaccess_group_whitelist). + ## Restrict access to form submissions By default, access to a [Form content item](form_builder_guide.md#forms-management) is controlled by the `content/read` policy. diff --git a/docs/permissions/limitation_reference.md b/docs/permissions/limitation_reference.md index b8582b99568..7085c51fc2d 100644 --- a/docs/permissions/limitation_reference.md +++ b/docs/permissions/limitation_reference.md @@ -25,6 +25,8 @@ As this is a generic limitation, you can configure your custom limitations to us Out of the box FunctionList uses it in the following way: ``` yaml +services: + # FunctionList is an ezjscore limitation, it only applies to ezjscore policies not used by # API/platform stack, so configure to use Blocking limitation to avoid LimitationNotFoundException ibexa.api.role.limitation_type.function_list: diff --git a/docs/permissions/permission_overview.md b/docs/permissions/permission_overview.md index 71c5d15ad37..bda055490e2 100644 --- a/docs/permissions/permission_overview.md +++ b/docs/permissions/permission_overview.md @@ -36,13 +36,16 @@ The more role assignments and complex policies you add for a given user, the mor 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 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]([[= 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. -``` php hl_lines="15-19" -[[= include_file('code_samples/back_office/limitation/src/Controller/CustomController.php', 0, 20) =]] +``` php hl_lines="14-16 18-23" +[[= include_code('code_samples/back_office/limitation/src/Controller/CustomController.php') =]] ``` +Back office controllers that extend `Ibexa\Contracts\AdminUi\Controller\Controller` already use `AuthenticatedRememberedCheckTrait`. +To add a policy check, override `performAccessCheck()` and call `parent::performAccessCheck()` first, as in the [custom limitation check example](custom_policies.md#custom-limitation-check). + `Attribute` accepts three arguments: - `module` is the policy module (for example, `content`) From 8466384b421fc8cbf9c40a4053114a3b58128181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Noco=C5=84?= Date: Thu, 1 Oct 2026 15:17:36 +0200 Subject: [PATCH 5/6] Fixed typo --- docs/permissions/permission_overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/permissions/permission_overview.md b/docs/permissions/permission_overview.md index bda055490e2..0a9e197b533 100644 --- a/docs/permissions/permission_overview.md +++ b/docs/permissions/permission_overview.md @@ -37,7 +37,7 @@ The more role assignments and complex policies you add for a given user, the mor 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 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. +The controller uses [`AuthenticatedRememberedCheckTrait::performAccessCheck()`](/api/php_api/php_api_reference/classes/Ibexa-Contracts-User-Controller-AuthenticatedRememberedCheckTrait.html#method_performAccessCheck), aliased as `traitPerformAccessCheck()`, for the login check. ``` php hl_lines="14-16 18-23" [[= include_code('code_samples/back_office/limitation/src/Controller/CustomController.php') =]] From 05048db93f5b680e9e1b30866f3f85daf3fd36fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Noco=C5=84?= Date: Thu, 1 Oct 2026 16:10:09 +0200 Subject: [PATCH 6/6] Update docs/permissions/custom_policies.md Co-authored-by: Adrien Dupuis <61695653+adriendupuis@users.noreply.github.com> --- docs/permissions/custom_policies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/permissions/custom_policies.md b/docs/permissions/custom_policies.md index c60b13083b1..68e1f1bad83 100644 --- a/docs/permissions/custom_policies.md +++ b/docs/permissions/custom_policies.md @@ -262,7 +262,7 @@ Check if current user has this custom limitation set to true from a custom contr ``` The `siteaccess_group_whitelist` route default limits the route to the back office SiteAccess group. -For more information, see [Request lifecycle](../infrastructure_and_maintenance/request_lifecycle.md#siteaccess_group_whitelist). +For more information, see [Request lifecycle](request_lifecycle.md#siteaccess_group_whitelist). ## Restrict access to form submissions