From c5630fa68b9129d7a0564c87c98ffadd872ad021 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Mon, 9 Jun 2025 02:14:46 +0200 Subject: [PATCH 1/4] [Contracts] Exposed `sudo` contract on PermissionResolver service --- .../Repository/PermissionResolver.php | 28 +++++++++++++++ .../Permission/CachedPermissionService.php | 2 +- .../Permission/PermissionResolver.php | 35 +++++++------------ 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/src/contracts/Repository/PermissionResolver.php b/src/contracts/Repository/PermissionResolver.php index 481ea7595b..eb184911f0 100644 --- a/src/contracts/Repository/PermissionResolver.php +++ b/src/contracts/Repository/PermissionResolver.php @@ -93,4 +93,32 @@ public function lookupLimitations( array $targets = [], array $limitationsIdentifiers = [] ): LookupLimitationResult; + + /** + * Executes the given callback with elevated permissions, bypassing the current user's limitations. + * + * Example usage: + * ``` + * $content = $permissionResolver->sudo( + * function () use ($contentId): Content { + * return $this->contentService->loadContent($contentId); + * } + * ); + * ``` + * or: + * ``` + * $content = $permissionResolver->sudo( + * static fn (): Content => $contentService->loadContent($contentId); + * ); + * ``` + * + * @phpstan-template T + * + * @phpstan-param callable(): T $callback + * + * @phpstan-return T + * + * @throws \Throwable + */ + public function sudo(callable $callback): mixed; } diff --git a/src/lib/Repository/Permission/CachedPermissionService.php b/src/lib/Repository/Permission/CachedPermissionService.php index e82bd87c38..4f56f19c30 100644 --- a/src/lib/Repository/Permission/CachedPermissionService.php +++ b/src/lib/Repository/Permission/CachedPermissionService.php @@ -135,7 +135,7 @@ public function getPermissionsCriterion(string $module = 'content', string $func /** * @internal For internal use only, do not depend on this method. */ - public function sudo(callable $callback, RepositoryInterface $outerRepository) + public function sudo(callable $callback, ?RepositoryInterface $outerRepository = null): mixed { ++$this->sudoNestingLevel; try { diff --git a/src/lib/Repository/Permission/PermissionResolver.php b/src/lib/Repository/Permission/PermissionResolver.php index 5f2de9b3b1..af5feaf59d 100644 --- a/src/lib/Repository/Permission/PermissionResolver.php +++ b/src/lib/Repository/Permission/PermissionResolver.php @@ -380,30 +380,21 @@ private function isDeniedByRoleLimitation( /** * @internal For internal use only, do not depend on this method. - * - * Allows API execution to be performed with full access sand-boxed. - * - * The closure sandbox will do a catch all on exceptions and rethrow after - * re-setting the sudo flag. - * - * Example use: - * $location = $repository->sudo( - * function ( Repository $repo ) use ( $locationId ) - * { - * return $repo->getLocationService()->loadLocation( $locationId ) - * } - * ); - * - * @param \callable(\Ibexa\Contracts\Core\Repository\Repository): mixed $callback - * @param \Ibexa\Contracts\Core\Repository\Repository $outerRepository - * - * @throws \RuntimeException Thrown on recursive sudo() use. - * @throws \Exception Re throws exceptions thrown inside $callback - * - * @return mixed */ - public function sudo(callable $callback, RepositoryInterface $outerRepository) + public function sudo(callable $callback, ?RepositoryInterface $outerRepository = null): mixed { + if (null !== $outerRepository) { + trigger_deprecation( + 'ibexa/core', + '5.0', + sprintf( + 'Calling either Repository::sudo or %s() method with outer repository as the 2nd argument is deprecated, will be removed in 6.0. ' . + 'Use `PermissionResolver::sudo()` method and inject required Repository services through DI container instead', + __METHOD__ + ) + ); + } + ++$this->sudoNestingLevel; try { $returnValue = $callback($outerRepository); From bb83e13e06334eaa8bf8cb5ca4eb34a1bba0ce21 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Mon, 9 Jun 2025 02:15:21 +0200 Subject: [PATCH 2/4] [Tests] Aligned PermissionTest with the changes --- .../Service/Mock/PermissionTest.php | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/tests/lib/Repository/Service/Mock/PermissionTest.php b/tests/lib/Repository/Service/Mock/PermissionTest.php index 268d378e1a..ccfaee38d9 100644 --- a/tests/lib/Repository/Service/Mock/PermissionTest.php +++ b/tests/lib/Repository/Service/Mock/PermissionTest.php @@ -201,27 +201,21 @@ public function testHasAccessReturnsFalse(array $roles, array $roleAssignments) /** * Test for the sudo() & hasAccess() method. + * + * @throws \Throwable */ - public function testHasAccessReturnsFalseButSudoSoTrue() + public function testHasAccessReturnsFalseButSudoSoTrue(): void { - /** @var $userHandlerMock \PHPUnit\Framework\MockObject\MockObject */ + /** @var \Ibexa\Contracts\Core\Persistence\User\Handler&\PHPUnit\Framework\MockObject\MockObject $userHandlerMock */ $userHandlerMock = $this->getPersistenceMock()->userHandler(); - $service = $this->getPermissionResolverMock(null); - $repositoryMock = $this->getRepositoryMock(); - $repositoryMock - ->expects(self::any()) - ->method('getPermissionResolver') - ->will(self::returnValue($service)); + $permissionResolverMock = $this->getPermissionResolverMock(null); $userHandlerMock ->expects(self::never()) ->method(self::anything()); - $result = $service->sudo( - static function (Repository $repo) { - return $repo->getPermissionResolver()->hasAccess('dummy-module', 'dummy-function'); - }, - $repositoryMock + $result = $permissionResolverMock->sudo( + static fn (): bool|array => $permissionResolverMock->hasAccess('dummy-module', 'dummy-function') ); self::assertTrue($result); From 8675f6327f612cb4c8e41a1dfc409594c70912e7 Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Mon, 9 Jun 2025 02:16:16 +0200 Subject: [PATCH 3/4] [PHPStan] Removed resolved issues from the baseline --- phpstan-baseline.neon | 40 ++-------------------------------------- 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index db3eb2b85b..b42505ae1d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -19734,24 +19734,12 @@ parameters: count: 1 path: src/lib/Repository/ObjectStateService.php - - - message: '#^Call to an undefined method Ibexa\\Contracts\\Core\\Repository\\PermissionResolver\:\:sudo\(\)\.$#' - identifier: method.notFound - count: 1 - path: src/lib/Repository/Permission/CachedPermissionService.php - - message: '#^Method Ibexa\\Core\\Repository\\Permission\\CachedPermissionService\:\:getPermissionsCriterion\(\) has parameter \$targets with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue count: 1 path: src/lib/Repository/Permission/CachedPermissionService.php - - - message: '#^Method Ibexa\\Core\\Repository\\Permission\\CachedPermissionService\:\:sudo\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: src/lib/Repository/Permission/CachedPermissionService.php - - message: '#^Method Ibexa\\Core\\Repository\\Permission\\LimitationService\:\:__construct\(\) has parameter \$limitationTypes with no value type specified in iterable type Traversable\.$#' identifier: missingType.iterableValue @@ -19812,12 +19800,6 @@ parameters: count: 1 path: src/lib/Repository/Permission/PermissionResolver.php - - - message: '#^PHPDoc tag @param for parameter \$callback contains unresolvable type\.$#' - identifier: parameter.unresolvableType - count: 1 - path: src/lib/Repository/Permission/PermissionResolver.php - - message: '#^PHPDoc tag @var does not specify variable name\.$#' identifier: varTag.noVariable @@ -20052,12 +20034,6 @@ parameters: count: 1 path: src/lib/Repository/ProxyFactory/ProxyGeneratorInterface.php - - - message: '#^Call to an undefined method Ibexa\\Contracts\\Core\\Repository\\PermissionResolver\:\:sudo\(\)\.$#' - identifier: method.notFound - count: 1 - path: src/lib/Repository/Repository.php - - message: '#^Method Ibexa\\Core\\Repository\\Repository\:\:__construct\(\) has parameter \$serviceSettings with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -60861,13 +60837,7 @@ parameters: - message: '#^Call to an undefined method Ibexa\\Contracts\\Core\\Persistence\\User\\Handler\:\:expects\(\)\.$#' identifier: method.notFound - count: 11 - path: tests/lib/Repository/Service/Mock/PermissionTest.php - - - - message: '#^Call to an undefined method Ibexa\\Contracts\\Core\\Repository\\PermissionResolver&PHPUnit\\Framework\\MockObject\\MockObject\:\:sudo\(\)\.$#' - identifier: method.notFound - count: 1 + count: 10 path: tests/lib/Repository/Service/Mock/PermissionTest.php - @@ -61014,12 +60984,6 @@ parameters: count: 1 path: tests/lib/Repository/Service/Mock/PermissionTest.php - - - message: '#^Method Ibexa\\Tests\\Core\\Repository\\Service\\Mock\\PermissionTest\:\:testHasAccessReturnsFalseButSudoSoTrue\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/lib/Repository/Service/Mock/PermissionTest.php - - message: '#^Method Ibexa\\Tests\\Core\\Repository\\Service\\Mock\\PermissionTest\:\:testHasAccessReturnsInvalidArgumentValueException\(\) has no return type specified\.$#' identifier: missingType.return @@ -61149,7 +61113,7 @@ parameters: - message: '#^PHPDoc tag @var has invalid value \(\$userHandlerMock \\PHPUnit\\Framework\\MockObject\\MockObject\)\: Unexpected token "\$userHandlerMock", expected type at offset 9 on line 1$#' identifier: phpDoc.parseError - count: 6 + count: 5 path: tests/lib/Repository/Service/Mock/PermissionTest.php - From 89618fdbad4bb92b828d5a4d302b2aab48c4a0ce Mon Sep 17 00:00:00 2001 From: Andrew Longosz Date: Mon, 9 Jun 2025 02:16:58 +0200 Subject: [PATCH 4/4] [PHPStan] Created baseline for side effects of exposing `sudo` --- phpstan-baseline.sudo.neon | 25 +++++++++++++++++++++++++ phpstan.neon.dist | 1 + 2 files changed, 26 insertions(+) create mode 100644 phpstan-baseline.sudo.neon diff --git a/phpstan-baseline.sudo.neon b/phpstan-baseline.sudo.neon new file mode 100644 index 0000000000..35b4bb4fad --- /dev/null +++ b/phpstan-baseline.sudo.neon @@ -0,0 +1,25 @@ +parameters: + ignoreErrors: + - + message: '#^Method Ibexa\\Contracts\\Core\\Repository\\PermissionResolver\:\:sudo\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: src/lib/Repository/Permission/CachedPermissionService.php + + - + message: '#^Callable callable\(\)\: T invoked with 1 parameter, 0 required\.$#' + identifier: arguments.count + count: 1 + path: src/lib/Repository/Permission/PermissionResolver.php + + - + message: '#^Method Ibexa\\Contracts\\Core\\Repository\\PermissionResolver\:\:sudo\(\) invoked with 2 parameters, 1 required\.$#' + identifier: arguments.count + count: 1 + path: src/lib/Repository/Repository.php + + - + message: '#^Parameter \#1 \$callback of method Ibexa\\Contracts\\Core\\Repository\\PermissionResolver\:\:sudo\(\) expects callable\(\)\: T, callable\(Ibexa\\Contracts\\Core\\Repository\\Repository\)\: T given\.$#' + identifier: argument.type + count: 1 + path: src/lib/Repository/Repository.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index c448982be5..1097b6387a 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -3,6 +3,7 @@ includes: - vendor/phpstan/phpstan-symfony/extension.neon - phpstan-baseline.neon - phpstan-baseline.pagerfanta.neon + - phpstan-baseline.sudo.neon parameters: level: 8