diff --git a/eZ/Publish/API/Repository/Tests/BaseTest.php b/eZ/Publish/API/Repository/Tests/BaseTest.php index f98488dfbd6..12101c8f244 100644 --- a/eZ/Publish/API/Repository/Tests/BaseTest.php +++ b/eZ/Publish/API/Repository/Tests/BaseTest.php @@ -663,4 +663,57 @@ protected function createFolder(array $names, $parentLocationId) return $contentService->publishVersion($contentDraft->versionInfo); } + + /** + * Create a user with new user group and assign a existing role (optionally with RoleLimitation). + * + * @param string $login User login + * @param string $email User e-mail + * @param string $userGroupName Name of the new user group to create + * @param string $roleIdentifier Role identifier to assign to the new group + * @param RoleLimitation|null $roleLimitation + * @return \eZ\Publish\API\Repository\Values\User\User + */ + protected function createCustomUserWithLogin( + $login, + $email, + $userGroupName, + $roleIdentifier, + RoleLimitation $roleLimitation = null + ) { + $repository = $this->getRepository(); + /* BEGIN: Inline */ + // ID of the "Users" user group in an eZ Publish demo installation + $rootUsersGroupId = $this->generateId('location', 4); + $roleService = $repository->getRoleService(); + $userService = $repository->getUserService(); + // Get a group create struct + $userGroupCreate = $userService->newUserGroupCreateStruct('eng-US'); + $userGroupCreate->setField('name', $userGroupName); + // Create new group with media editor rights + $userGroup = $userService->createUserGroup( + $userGroupCreate, + $userService->loadUserGroup($rootUsersGroupId) + ); + $roleService->assignRoleToUserGroup( + $roleService->loadRoleByIdentifier($roleIdentifier), + $userGroup, + $roleLimitation + ); + // Instantiate a create struct with mandatory properties + $userCreate = $userService->newUserCreateStruct( + $login, + $email, + 'secret', + 'eng-US' + ); + $userCreate->enabled = true; + // Set some fields required by the user ContentType + $userCreate->setField('first_name', 'Example'); + $userCreate->setField('last_name', ucfirst($login)); + // Create a new user instance. + $user = $userService->createUser($userCreate, array($userGroup)); + /* END: Inline */ + return $user; + } } diff --git a/eZ/Publish/API/Repository/Tests/TrashServiceAuthorizationTest.php b/eZ/Publish/API/Repository/Tests/TrashServiceAuthorizationTest.php index b78f19b1f15..7ec653bef3f 100644 --- a/eZ/Publish/API/Repository/Tests/TrashServiceAuthorizationTest.php +++ b/eZ/Publish/API/Repository/Tests/TrashServiceAuthorizationTest.php @@ -8,6 +8,12 @@ */ namespace eZ\Publish\API\Repository\Tests; +use eZ\Publish\API\Repository\Exceptions\UnauthorizedException; +use eZ\Publish\API\Repository\Values\Content\Location; +use eZ\Publish\API\Repository\Values\User\Limitation\ObjectStateLimitation; +use eZ\Publish\Core\Repository\Repository; +use eZ\Publish\Core\Repository\TrashService; + /** * Test case for operations in the TrashService using in memory storage. * @@ -224,4 +230,43 @@ public function testDeleteTrashItemThrowsUnauthorizedException() $trashService->deleteTrashItem($trashItem); /* END: Use Case */ } + + public function testTrashRequiresPremissionsToRemoveAllSubitems() + { + $this->createRoleWithPolicies('Publisher', [ + ['module' => 'content', 'function' => 'read'], + ['module' => 'content', 'function' => 'create'], + ['module' => 'content', 'function' => 'publish'], + ['module' => 'state', 'function' => 'assign'], + ['module' => 'content', 'function' => 'remove', 'limitations' => [ + new ObjectStateLimitation(['limitationValues' => [ + $this->generateId('objectstate', 2), + ]]), + ]], + ]); + $publisherUser = $this->createCustomUserWithLogin( + 'publisher', + 'publisher@example.com', + 'Publishers', + 'Publisher' + ); + /** @var Repository $repository */ + $repository = $this->getRepository(); + $repository->getPermissionResolver()->setCurrentUserReference($publisherUser); + $trashService = $repository->getTrashService(); + $locationService = $repository->getLocationService(); + $objectStateService = $repository->getObjectStateService(); + $parentContent = $this->createFolder(['eng-US' => 'Parent Folder'], 2); + $objectStateService->setContentState( + $parentContent->contentInfo, + $objectStateService->loadObjectStateGroup(2), + $objectStateService->loadObjectState(2) + ); + $parentLocation = $locationService->loadLocations($parentContent->contentInfo)[0]; + $childContent = $this->createFolder(['eng-US' => 'Child Folder'], $parentLocation->id); + + $this->refreshSearch($repository); + $this->setExpectedException(\eZ\Publish\Core\Base\Exceptions\UnauthorizedException::class); + $trashService->trash($parentLocation); + } } diff --git a/eZ/Publish/Core/Repository/Repository.php b/eZ/Publish/Core/Repository/Repository.php index 80cac554738..954eeebd0d4 100644 --- a/eZ/Publish/Core/Repository/Repository.php +++ b/eZ/Publish/Core/Repository/Repository.php @@ -575,6 +575,7 @@ public function getTrashService() $this, $this->persistenceHandler, $this->getNameSchemaService(), + $this->getPermissionCriterionResolver(), $this->serviceSettings['trash'] ); diff --git a/eZ/Publish/Core/Repository/TrashService.php b/eZ/Publish/Core/Repository/TrashService.php index 7cd1728870a..d163386c3e6 100644 --- a/eZ/Publish/Core/Repository/TrashService.php +++ b/eZ/Publish/Core/Repository/TrashService.php @@ -14,6 +14,7 @@ use eZ\Publish\API\Repository\Values\Content\Location; use eZ\Publish\Core\Repository\Values\Content\TrashItem; use eZ\Publish\API\Repository\Values\Content\TrashItem as APITrashItem; +use eZ\Publish\API\Repository\Values\Content\ContentInfo; use eZ\Publish\API\Repository\Values\Content\Query; use eZ\Publish\SPI\Persistence\Content\Location\Trashed; use eZ\Publish\Core\Base\Exceptions\InvalidArgumentValue; @@ -21,6 +22,10 @@ use eZ\Publish\API\Repository\Values\Content\SearchResult; use eZ\Publish\API\Repository\Values\Content\Query\Criterion; use eZ\Publish\API\Repository\Values\Content\Query\SortClause; +use eZ\Publish\API\Repository\PermissionCriterionResolver; +use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalAnd as CriterionLogicalAnd; +use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalNot as CriterionLogicalNot; +use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Subtree as CriterionSubtree; use DateTime; use Exception; @@ -55,14 +60,17 @@ class TrashService implements TrashServiceInterface * @param \eZ\Publish\API\Repository\Repository $repository * @param \eZ\Publish\SPI\Persistence\Handler $handler * @param \eZ\Publish\Core\Repository\Helper\NameSchemaService $nameSchemaService + * @param \eZ\Publish\API\Repository\PermissionCriterionResolver $permissionCriterionResolver * @param array $settings */ public function __construct( RepositoryInterface $repository, Handler $handler, Helper\NameSchemaService $nameSchemaService, + PermissionCriterionResolver $permissionCriterionResolver, array $settings = array() ) { + $this->permissionCriterionResolver = $permissionCriterionResolver; $this->repository = $repository; $this->persistenceHandler = $handler; $this->nameSchemaService = $nameSchemaService; @@ -113,11 +121,11 @@ public function loadTrashItem($trashItemId) */ public function trash(Location $location) { - if (!is_numeric($location->id)) { + if (empty($location->id)) { throw new InvalidArgumentValue('id', $location->id, 'Location'); } - if (!$this->repository->canUser('content', 'remove', $location->getContentInfo(), [$location])) { + if (!$this->userHasPermissionsToRemove($location->getContentInfo(), $location)) { throw new UnauthorizedException('content', 'remove'); } @@ -365,4 +373,38 @@ protected function getDateTime($timestamp) return $dateTime; } + + /** + * @param \eZ\Publish\API\Repository\Values\Content\ContentInfo $contentInfo + * @param \eZ\Publish\API\Repository\Values\Content\Location $location + * + * @return bool + * + * @throws \eZ\Publish\API\Repository\Exceptions\BadStateException + * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException + */ + private function userHasPermissionsToRemove(ContentInfo $contentInfo, Location $location) + { + if (!$this->repository->canUser('content', 'remove', $contentInfo, [$location])) { + return false; + } + $contentRemoveCriterion = $this->permissionCriterionResolver->getPermissionsCriterion('content', 'remove'); + if (!$contentRemoveCriterion instanceof Criterion) { + return (bool)$contentRemoveCriterion; + } + $query = new Query( + array( + 'limit' => 0, + 'filter' => new CriterionLogicalAnd( + array( + new CriterionSubtree($location->pathString), + new CriterionLogicalNot($contentRemoveCriterion), + ) + ), + ) + ); + $result = $this->repository->getSearchService()->findContent($query, array(), false); + + return $result->totalCount == 0; + } }