Skip to content

IBX-4143: [Contracts] Exposed sudo method on PermissionResolver #585

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 2 additions & 38 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

-
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

-
Expand Down
25 changes: 25 additions & 0 deletions phpstan-baseline.sudo.neon
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions src/contracts/Repository/PermissionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion src/lib/Repository/Permission/CachedPermissionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
35 changes: 13 additions & 22 deletions src/lib/Repository/Permission/PermissionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
20 changes: 7 additions & 13 deletions tests/lib/Repository/Service/Mock/PermissionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading