diff --git a/src/contracts/Persistence/User/Handler.php b/src/contracts/Persistence/User/Handler.php index 446796f03d..e479ef5abe 100644 --- a/src/contracts/Persistence/User/Handler.php +++ b/src/contracts/Persistence/User/Handler.php @@ -208,6 +208,18 @@ public function loadRoleAssignment($roleAssignmentId); */ public function loadRoleAssignmentsByRoleId($roleId); + /** + * Loads Role's assignments based on provided $offset and $limit arguments. + * + * @return \Ibexa\Contracts\Core\Persistence\User\RoleAssignment[] + */ + public function loadRoleAssignmentsByRoleIdWithOffsetAndLimit(int $roleId, int $offset, ?int $limit): array; + + /** + * Counts Role's assignments taking into consideration related and existing user and user group objects. + */ + public function countRoleAssignments(int $roleId): int; + /** * Loads roles assignments to a user/group. * diff --git a/src/contracts/Repository/Decorator/RoleServiceDecorator.php b/src/contracts/Repository/Decorator/RoleServiceDecorator.php index 74fdbcefae..4b13dcd2bd 100644 --- a/src/contracts/Repository/Decorator/RoleServiceDecorator.php +++ b/src/contracts/Repository/Decorator/RoleServiceDecorator.php @@ -149,6 +149,16 @@ public function getRoleAssignments(Role $role): iterable return $this->innerService->getRoleAssignments($role); } + public function loadRoleAssignments(Role $role, int $offset = 0, ?int $limit = null): iterable + { + return $this->innerService->loadRoleAssignments($role, $offset, $limit); + } + + public function countRoleAssignments(Role $role): int + { + return $this->innerService->countRoleAssignments($role); + } + public function getRoleAssignmentsForUser( User $user, bool $inherited = false diff --git a/src/contracts/Repository/RoleService.php b/src/contracts/Repository/RoleService.php index 543eb29146..3b3da53410 100644 --- a/src/contracts/Repository/RoleService.php +++ b/src/contracts/Repository/RoleService.php @@ -277,6 +277,30 @@ public function loadRoleAssignment(int $roleAssignmentId): RoleAssignment; */ public function getRoleAssignments(Role $role): iterable; + /** + * Returns the assigned users and user groups to this role with $offset and $limit arguments. + * + * @return \Ibexa\Contracts\Core\Repository\Values\User\RoleAssignment[] + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read a role + */ + public function loadRoleAssignments( + Role $role, + int $offset = 0, + ?int $limit = null + ): iterable; + + /** + * Returns the number of users and user groups assigned to this role. + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read a role + */ + public function countRoleAssignments(Role $role): int; + /** * Returns UserRoleAssignments assigned to the given User, excluding the ones the current user is not allowed to read. * diff --git a/src/lib/Persistence/Cache/UserHandler.php b/src/lib/Persistence/Cache/UserHandler.php index d7eab69c88..0a5999dffc 100644 --- a/src/lib/Persistence/Cache/UserHandler.php +++ b/src/lib/Persistence/Cache/UserHandler.php @@ -35,6 +35,7 @@ class UserHandler extends AbstractInMemoryPersistenceHandler implements UserHand private const BY_IDENTIFIER_SUFFIX = 'by_identifier_suffix'; private const LOCATION_PATH_IDENTIFIER = 'location_path'; private const ROLE_ASSIGNMENT_WITH_BY_ROLE_SUFFIX_IDENTIFIER = 'role_assignment_with_by_role_suffix'; + private const ROLE_ASSIGNMENT_WITH_BY_ROLE_SUFFIX_OFFSET_LIMIT_IDENTIFIER = 'role_assignment_with_by_role_offset_limit_suffix'; private const ROLE_ASSIGNMENT_WITH_BY_GROUP_INHERITED_SUFFIX_IDENTIFIER = 'role_assignment_with_by_group_inherited_suffix'; private const ROLE_ASSIGNMENT_WITH_BY_GROUP_SUFFIX_IDENTIFIER = 'role_assignment_with_by_group_suffix'; private const USER_WITH_ACCOUNT_KEY_SUFFIX_IDENTIFIER = 'user_with_account_key_suffix'; @@ -495,6 +496,43 @@ function () use ($roleId) { ); } + public function loadRoleAssignmentsByRoleIdWithOffsetAndLimit(int $roleId, int $offset, ?int $limit): array + { + return $this->getListCacheValue( + $this->cacheIdentifierGenerator->generateKey( + self::ROLE_ASSIGNMENT_WITH_BY_ROLE_SUFFIX_OFFSET_LIMIT_IDENTIFIER, + [$roleId, $offset, $limit], + true + ), + function () use ($roleId, $offset, $limit): array { + return $this->persistenceHandler + ->userHandler() + ->loadRoleAssignmentsByRoleIdWithOffsetAndLimit($roleId, $offset, $limit); + }, + $this->getRoleAssignmentTags, + $this->getRoleAssignmentKeys, + function () use ($roleId): array { + return [ + $this->cacheIdentifierGenerator->generateTag( + self::ROLE_ASSIGNMENT_ROLE_LIST_IDENTIFIER, + [$roleId] + ), + $this->cacheIdentifierGenerator->generateTag(self::ROLE_IDENTIFIER, [$roleId]), + ]; + }, + [$roleId, $offset, $limit] + ); + } + + public function countRoleAssignments(int $roleId): int + { + $this->logger->logCall(__METHOD__, ['roleId' => $roleId]); + + return $this->persistenceHandler + ->userHandler() + ->countRoleAssignments($roleId); + } + /** * {@inheritdoc} */ @@ -698,19 +736,32 @@ public function unassignRole($contentId, $roleId) return $return; } - /** - * {@inheritdoc} - */ - public function removeRoleAssignment($roleAssignmentId) + public function removeRoleAssignment($roleAssignmentId): void { - $this->logger->logCall(__METHOD__, ['assignment' => $roleAssignmentId]); - $return = $this->persistenceHandler->userHandler()->removeRoleAssignment($roleAssignmentId); + $roleAssignment = $this->persistenceHandler->userHandler()->loadRoleAssignment($roleAssignmentId); + + $this->logger->logCall( + __METHOD__, + [ + 'assignment' => $roleAssignmentId, + 'contentId' => $roleAssignment->contentId, + 'roleId' => $roleAssignment->roleId, + ] + ); + + $this->persistenceHandler->userHandler()->removeRoleAssignment($roleAssignmentId); $this->cache->invalidateTags([ $this->cacheIdentifierGenerator->generateTag(self::ROLE_ASSIGNMENT_IDENTIFIER, [$roleAssignmentId]), + $this->cacheIdentifierGenerator->generateTag( + self::ROLE_ASSIGNMENT_GROUP_LIST_IDENTIFIER, + [$roleAssignment->contentId] + ), + $this->cacheIdentifierGenerator->generateTag( + self::ROLE_ASSIGNMENT_ROLE_LIST_IDENTIFIER, + [$roleAssignment->roleId] + ), ]); - - return $return; } } diff --git a/src/lib/Persistence/Legacy/User/Handler.php b/src/lib/Persistence/Legacy/User/Handler.php index 00ca8a64ea..c476a49e35 100644 --- a/src/lib/Persistence/Legacy/User/Handler.php +++ b/src/lib/Persistence/Legacy/User/Handler.php @@ -650,6 +650,25 @@ public function loadRoleAssignmentsByRoleId($roleId) return $this->mapper->mapRoleAssignments($data); } + /** + * @return \Ibexa\Contracts\Core\Persistence\User\RoleAssignment[] + */ + public function loadRoleAssignmentsByRoleIdWithOffsetAndLimit(int $roleId, int $offset, ?int $limit): array + { + $data = $this->roleGateway->loadRoleAssignmentsByRoleIdWithOffsetAndLimit($roleId, $offset, $limit); + + if (empty($data)) { + return []; + } + + return $this->mapper->mapRoleAssignments($data); + } + + public function countRoleAssignments(int $roleId): int + { + return $this->roleGateway->countRoleAssignments($roleId); + } + /** * Loads roles assignments to a user/group. * diff --git a/src/lib/Persistence/Legacy/User/Mapper.php b/src/lib/Persistence/Legacy/User/Mapper.php index e35cdc3d78..df7abea750 100644 --- a/src/lib/Persistence/Legacy/User/Mapper.php +++ b/src/lib/Persistence/Legacy/User/Mapper.php @@ -108,8 +108,6 @@ public function mapPolicies(array $data) /** * Map role data to a role. * - * @param array $data - * * @return \Ibexa\Contracts\Core\Persistence\User\Role */ public function mapRole(array $data) @@ -134,8 +132,6 @@ public function mapRole(array $data) /** * Map data for a set of roles. * - * @param array $data - * * @return \Ibexa\Contracts\Core\Persistence\User\Role[] */ public function mapRoles(array $data) diff --git a/src/lib/Persistence/Legacy/User/Role/Gateway.php b/src/lib/Persistence/Legacy/User/Role/Gateway.php index b0780af6d4..f3d33d6722 100644 --- a/src/lib/Persistence/Legacy/User/Role/Gateway.php +++ b/src/lib/Persistence/Legacy/User/Role/Gateway.php @@ -24,7 +24,6 @@ abstract class Gateway public const POLICY_LIMITATION_TABLE = 'ezpolicy_limitation'; public const POLICY_LIMITATION_VALUE_TABLE = 'ezpolicy_limitation_value'; public const USER_ROLE_TABLE = 'ezuser_role'; - public const ROLE_SEQ = 'ezrole_id_seq'; public const POLICY_SEQ = 'ezpolicy_id_seq'; public const POLICY_LIMITATION_SEQ = 'ezpolicy_limitation_id_seq'; @@ -101,6 +100,20 @@ abstract public function loadRoleAssignmentsByGroupId( */ abstract public function loadRoleAssignmentsByRoleId(int $roleId): array; + /** + * Load a Role assignments for given Role ID with provided $offset and $limit arguments. + */ + abstract public function loadRoleAssignmentsByRoleIdWithOffsetAndLimit( + int $roleId, + int $offset, + ?int $limit + ): array; + + /** + * Count Role's assignments taking into consideration related and existing user and user group objects. + */ + abstract public function countRoleAssignments(int $roleId): int; + /** * Return User Policies data associated with User. * diff --git a/src/lib/Persistence/Legacy/User/Role/Gateway/DoctrineDatabase.php b/src/lib/Persistence/Legacy/User/Role/Gateway/DoctrineDatabase.php index 76518e5e91..652aa02ab5 100644 --- a/src/lib/Persistence/Legacy/User/Role/Gateway/DoctrineDatabase.php +++ b/src/lib/Persistence/Legacy/User/Role/Gateway/DoctrineDatabase.php @@ -15,6 +15,7 @@ use Ibexa\Contracts\Core\Persistence\User\Policy; use Ibexa\Contracts\Core\Persistence\User\Role; use Ibexa\Contracts\Core\Persistence\User\RoleUpdateStruct; +use Ibexa\Core\Persistence\Legacy\Content\Gateway as ContentGateway; use Ibexa\Core\Persistence\Legacy\User\Role\Gateway; /** @@ -342,6 +343,72 @@ public function loadRoleAssignmentsByRoleId(int $roleId): array return $statement->fetchAll(FetchMode::ASSOCIATIVE); } + /** + * @throws \Doctrine\DBAL\Driver\Exception + * @throws \Doctrine\DBAL\Exception + */ + public function loadRoleAssignmentsByRoleIdWithOffsetAndLimit(int $roleId, int $offset, ?int $limit): array + { + $query = $this + ->buildLoadRoleAssignmentsQuery( + [ + 'user_role.id', + 'user_role.contentobject_id', + 'user_role.limit_identifier', + 'user_role.limit_value', + 'user_role.role_id', + ], + $roleId + ) + ->setFirstResult($offset); + + if ($limit !== null) { + $query->setMaxResults($limit); + } + + return $query + ->execute() + ->fetchAllAssociative(); + } + + /** + * @throws \Doctrine\DBAL\Driver\Exception + * @throws \Doctrine\DBAL\Exception + */ + public function countRoleAssignments(int $roleId): int + { + $query = $this->buildLoadRoleAssignmentsQuery( + [$this->connection->getDatabasePlatform()->getCountExpression('user_role.id')], + $roleId + ); + + return (int)$query->execute()->fetchOne(); + } + + /** + * @param array $columns + */ + private function buildLoadRoleAssignmentsQuery(array $columns, int $roleId): QueryBuilder + { + $query = $this->connection->createQueryBuilder(); + $query + ->select(...$columns) + ->from(self::USER_ROLE_TABLE, 'user_role') + ->innerJoin( + 'user_role', + ContentGateway::CONTENT_ITEM_TABLE, + 'content_object', + 'user_role.contentobject_id = content_object.id' + )->where( + $query->expr()->eq( + 'role_id', + $query->createPositionalParameter($roleId, ParameterType::INTEGER) + ) + ); + + return $query; + } + public function loadPoliciesByUserId(int $userId): array { $groupIds = $this->fetchUserGroups($userId); diff --git a/src/lib/Persistence/Legacy/User/Role/Gateway/ExceptionConversion.php b/src/lib/Persistence/Legacy/User/Role/Gateway/ExceptionConversion.php index bdbe261600..c7429bcc98 100644 --- a/src/lib/Persistence/Legacy/User/Role/Gateway/ExceptionConversion.php +++ b/src/lib/Persistence/Legacy/User/Role/Gateway/ExceptionConversion.php @@ -132,6 +132,24 @@ public function loadRoleAssignmentsByRoleId(int $roleId): array } } + public function loadRoleAssignmentsByRoleIdWithOffsetAndLimit(int $roleId, int $offset, ?int $limit): array + { + try { + return $this->innerGateway->loadRoleAssignmentsByRoleIdWithOffsetAndLimit($roleId, $offset, $limit); + } catch (DBALException | PDOException $e) { + throw DatabaseException::wrap($e); + } + } + + public function countRoleAssignments(int $roleId): int + { + try { + return $this->innerGateway->countRoleAssignments($roleId); + } catch (DBALException | PDOException $e) { + throw DatabaseException::wrap($e); + } + } + public function loadPoliciesByUserId(int $userId): array { try { diff --git a/src/lib/Repository/RoleService.php b/src/lib/Repository/RoleService.php index 86e7ca5e01..b57f1fda30 100644 --- a/src/lib/Repository/RoleService.php +++ b/src/lib/Repository/RoleService.php @@ -865,10 +865,6 @@ public function loadRoleAssignment(int $roleAssignmentId): RoleAssignment } /** - * Returns the assigned user and user groups to this role. - * - * @param \Ibexa\Contracts\Core\Repository\Values\User\Role $role - * * @return \Ibexa\Contracts\Core\Repository\Values\User\RoleAssignment[] * * @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException @@ -881,25 +877,53 @@ public function getRoleAssignments(APIRole $role): iterable throw new UnauthorizedException('role', 'read'); } + $persistenceRoleAssignments = $this->userHandler->loadRoleAssignmentsByRoleId($role->id); + + return $this->buildRoleAssignmentsFromPersistence($role, $persistenceRoleAssignments); + } + + public function loadRoleAssignments(APIRole $role, int $offset = 0, ?int $limit = null): iterable + { + if (!$this->permissionResolver->canUser('role', 'read', $role)) { + throw new UnauthorizedException('role', 'read'); + } + + $persistenceRoleAssignments = $this->userHandler->loadRoleAssignmentsByRoleIdWithOffsetAndLimit( + $role->id, + $offset, + $limit + ); + + return $this->buildRoleAssignmentsFromPersistence($role, $persistenceRoleAssignments); + } + + /** + * @param array $persistenceRoleAssignments + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException + */ + private function buildRoleAssignmentsFromPersistence( + APIRole $role, + array $persistenceRoleAssignments + ): array { $userService = $this->repository->getUserService(); - $spiRoleAssignments = $this->userHandler->loadRoleAssignmentsByRoleId($role->id); - $roleAssignments = []; - foreach ($spiRoleAssignments as $spiRoleAssignment) { + $roleAssignments = []; + foreach ($persistenceRoleAssignments as $persistenceRoleAssignment) { // First check if the Role is assigned to a User // If no User is found, see if it belongs to a UserGroup try { - $user = $userService->loadUser($spiRoleAssignment->contentId); + $user = $userService->loadUser($persistenceRoleAssignment->contentId); $roleAssignments[] = $this->roleDomainMapper->buildDomainUserRoleAssignmentObject( - $spiRoleAssignment, + $persistenceRoleAssignment, $user, $role ); } catch (APINotFoundException $e) { try { - $userGroup = $userService->loadUserGroup($spiRoleAssignment->contentId); + $userGroup = $userService->loadUserGroup($persistenceRoleAssignment->contentId); $roleAssignments[] = $this->roleDomainMapper->buildDomainUserGroupRoleAssignmentObject( - $spiRoleAssignment, + $persistenceRoleAssignment, $userGroup, $role ); @@ -912,6 +936,22 @@ public function getRoleAssignments(APIRole $role): iterable return $roleAssignments; } + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException if the authenticated user is not allowed to read a role + */ + public function countRoleAssignments(APIRole $role): int + { + if (!$this->permissionResolver->canUser('role', 'read', $role)) { + throw new UnauthorizedException('role', 'read'); + } + + // Skipping building domain user role assignment object as done in `buildRoleAssignmentsFromPersistence` + // due to inner joining user content which is sufficient in this case + return $this->userHandler->countRoleAssignments($role->id); + } + /** * @see \Ibexa\Contracts\Core\Repository\RoleService::getRoleAssignmentsForUser() * diff --git a/src/lib/Repository/UserService.php b/src/lib/Repository/UserService.php index 695bff6f6a..1a06e9f38e 100644 --- a/src/lib/Repository/UserService.php +++ b/src/lib/Repository/UserService.php @@ -289,8 +289,13 @@ public function deleteUserGroup(APIUserGroup $userGroup): iterable $this->repository->beginTransaction(); try { + foreach ($this->userHandler->loadRoleAssignmentsByGroupId($userGroup->id) as $roleAssignment) { + $this->userHandler->removeRoleAssignment($roleAssignment->id); + } //@todo: what happens to sub user groups and users below sub user groups - $affectedLocationIds = $this->repository->getContentService()->deleteContent($loadedUserGroup->getVersionInfo()->getContentInfo()); + $affectedLocationIds = $this->repository->getContentService()->deleteContent( + $loadedUserGroup->getVersionInfo()->getContentInfo() + ); $this->repository->commit(); } catch (Exception $e) { $this->repository->rollback(); @@ -625,7 +630,13 @@ public function deleteUser(APIUser $user): iterable $this->repository->beginTransaction(); try { - $affectedLocationIds = $this->repository->getContentService()->deleteContent($loadedUser->getVersionInfo()->getContentInfo()); + foreach ($this->userHandler->loadRoleAssignmentsByGroupId($user->id) as $roleAssignment) { + $this->userHandler->removeRoleAssignment($roleAssignment->id); + } + + $affectedLocationIds = $this->repository->getContentService()->deleteContent( + $loadedUser->getVersionInfo()->getContentInfo() + ); // User\Handler::delete call is currently used to clear cache only $this->userHandler->delete($loadedUser->id); diff --git a/src/lib/Resources/settings/storage_engines/cache.yml b/src/lib/Resources/settings/storage_engines/cache.yml index 2966ccbc06..1fb13b0c3d 100644 --- a/src/lib/Resources/settings/storage_engines/cache.yml +++ b/src/lib/Resources/settings/storage_engines/cache.yml @@ -54,6 +54,7 @@ parameters: role_assignment_role_list: 'rarl-%s' role_with_by_id_suffix: 'r-%s-bi' role_assignment_with_by_role_suffix: 'ra-%s-bro' + role_assignment_with_by_role_offset_limit_suffix: 'ra-%%s-bro-%%s-%%s' role_assignment_with_by_group_inherited_suffix: 'ra-%s-bgi' role_assignment_with_by_group_suffix: 'ra-%s-bg' section: 'se-%s' @@ -150,6 +151,7 @@ parameters: role_assignment_role_list: 'rarl-%s' role_with_by_id_suffix: 'r-%s-bi' role_assignment_with_by_role_suffix: 'ra-%s-bro' + role_assignment_with_by_role_offset_limit_suffix: 'ra-%%s-bro-%%s-%%s' role_assignment_with_by_group_inherited_suffix: 'ra-%s-bgi' role_assignment_with_by_group_suffix: 'ra-%s-bg' section: 'se-%s' diff --git a/tests/integration/Core/Repository/RoleServiceTest.php b/tests/integration/Core/Repository/RoleServiceTest.php index 0e96b04317..09d1aebd5c 100644 --- a/tests/integration/Core/Repository/RoleServiceTest.php +++ b/tests/integration/Core/Repository/RoleServiceTest.php @@ -11,6 +11,7 @@ use Ibexa\Contracts\Core\Repository\Exceptions\LimitationValidationException; use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException; use Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException; +use Ibexa\Contracts\Core\Repository\RoleService; use Ibexa\Contracts\Core\Repository\Values\User\Limitation; use Ibexa\Contracts\Core\Repository\Values\User\Limitation\ContentTypeLimitation; use Ibexa\Contracts\Core\Repository\Values\User\Limitation\LanguageLimitation; @@ -20,10 +21,12 @@ use Ibexa\Contracts\Core\Repository\Values\User\PolicyCreateStruct; use Ibexa\Contracts\Core\Repository\Values\User\PolicyUpdateStruct; use Ibexa\Contracts\Core\Repository\Values\User\Role; +use Ibexa\Contracts\Core\Repository\Values\User\RoleAssignment; use Ibexa\Contracts\Core\Repository\Values\User\RoleCopyStruct; use Ibexa\Contracts\Core\Repository\Values\User\RoleCreateStruct; use Ibexa\Contracts\Core\Repository\Values\User\RoleDraft; use Ibexa\Contracts\Core\Repository\Values\User\RoleUpdateStruct; +use Ibexa\Contracts\Core\Repository\Values\User\User; use Ibexa\Contracts\Core\Repository\Values\User\UserGroupRoleAssignment; use Ibexa\Contracts\Core\Repository\Values\User\UserRoleAssignment; @@ -1790,7 +1793,7 @@ public function testLoadRoleAssignment() $roleService = $repository->getRoleService(); $user = $repository->getUserService()->loadUser(14); - // Check inital empty assigments (also warms up potential cache to validate it is correct below) + // Check initial empty assignments (also warms up potential cache to validate it is correct below) $this->assertCount(0, $roleService->getRoleAssignmentsForUser($user)); // Assignment to user group @@ -1877,6 +1880,100 @@ public function testGetRoleAssignmentsContainExpectedLimitation(array $roleAssig ); } + /** + * @covers \Ibexa\Contracts\Core\Repository\RoleService::loadRoleAssignments() + */ + public function testLoadRoleAssignments(): void + { + $repository = $this->getRepository(); + $roleService = $repository->getRoleService(); + + $role = $this->createRoleWithPolicies('testLoadRoleAssignments', []); + $user = $this->createUser('test', 'Test', 'Test'); + $user2 = $this->createUser('test2', 'Test2', 'Test2'); + + $roleService->assignRoleToUser($role, $user); + $roleService->assignRoleToUser($role, $user2); + + $loadedRole = $roleService->loadRole($role->id); + + $roleAssignments = $roleService->loadRoleAssignments($loadedRole, 0, 1); + + self::assertCount(1, $roleAssignments); + self::assertInstanceOf(UserRoleAssignment::class, $roleAssignments[0]); + } + + /** + * @covers \Ibexa\Contracts\Core\Repository\RoleService::countRoleAssignments() + */ + public function testLoadRoleAssignmentsWithDeletedUser(): void + { + $repository = $this->getRepository(); + $roleService = $repository->getRoleService(); + $userService = $repository->getUserService(); + + $role = $roleService->loadRoleByIdentifier('Editor'); + $roleAssignments = $roleService->loadRoleAssignments($role); + $expectedCount = count($roleAssignments); + + // Adding user should add '1' to the assignments count + $newUser = $this->createUser('login', 'Test', 'Test'); + $roleService->assignRoleToUser($role, $newUser); + ++$expectedCount; + + $roleAssignments = $roleService->loadRoleAssignments($role); + + self::assertCount($expectedCount, $roleAssignments); + + // Removing user should subtract '1' from the assignments count + $userService->deleteUser($newUser); + --$expectedCount; + + $roleAssignments = $roleService->loadRoleAssignments($role); + + self::assertCount($expectedCount, $roleAssignments); + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\Exception + */ + public function testCountRoleAssignmentsAfterRemovingRoleAssignment(): void + { + $repository = $this->getRepository(); + $roleService = $repository->getRoleService(); + + $role = $roleService->loadRoleByIdentifier('Editor'); + $newUser = $this->createUser('login', 'Test', 'Test'); + $roleService->assignRoleToUser($role, $newUser); + $roleAssignmentsCount = $roleService->countRoleAssignments($role); + + $userRoleAssignment = $this->loadRoleAssignmentForUser($roleService, $role, $newUser); + $roleService->removeRoleAssignment($userRoleAssignment); + + self::assertEquals($roleAssignmentsCount - 1, $roleService->countRoleAssignments($role)); + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\Exception + */ + public function testCountRoleAssignmentsAfterDeletingUser(): void + { + $repository = $this->getRepository(); + $roleService = $repository->getRoleService(); + $userService = $repository->getUserService(); + + $role = $roleService->loadRoleByIdentifier('Editor'); + $newUser = $this->createUser('login', 'Test', 'Test'); + $roleService->assignRoleToUser($role, $newUser); + + $roleAssignmentsCount = $roleService->countRoleAssignments($role); + + $userService->deleteUser($newUser); + $afterUserDeleteCount = $roleService->countRoleAssignments($role); + + self::assertEquals($roleAssignmentsCount - 1, $afterUserDeleteCount); + } + /** * Test for the assignRoleToUser() method. * @@ -2964,6 +3061,21 @@ private function createUserGroupVersion1() return $userGroup; } + + private function loadRoleAssignmentForUser(RoleService $roleService, Role $role, User $newUser): UserRoleAssignment + { + [$userRoleAssignment] = array_values( + array_filter( + (array)$roleService->getRoleAssignments($role), + static function (RoleAssignment $roleAssignment) use ($newUser): bool { + return $roleAssignment instanceof UserRoleAssignment + && $roleAssignment->getUser()->login === $newUser->login; + } + ) + ); + + return $userRoleAssignment; + } } class_alias(RoleServiceTest::class, 'eZ\Publish\API\Repository\Tests\RoleServiceTest'); diff --git a/tests/lib/Persistence/Cache/UserHandlerTest.php b/tests/lib/Persistence/Cache/UserHandlerTest.php index 6560833455..108f91494c 100644 --- a/tests/lib/Persistence/Cache/UserHandlerTest.php +++ b/tests/lib/Persistence/Cache/UserHandlerTest.php @@ -14,6 +14,7 @@ use Ibexa\Contracts\Core\Persistence\User\RoleAssignment; use Ibexa\Contracts\Core\Persistence\User\RoleCreateStruct; use Ibexa\Contracts\Core\Persistence\User\RoleUpdateStruct; +use Ibexa\Core\Persistence\Cache\UserHandler; use Ibexa\Core\Persistence\Legacy\Content\Location\Handler as SPILocationHandler; /** @@ -108,6 +109,7 @@ public function providerForUnCachedMethods(): array null, false, ], + ['countRoleAssignments', [9], null, [], null, [], 1], ['createRole', [new RoleCreateStruct()]], ['createRoleDraft', [new RoleCreateStruct()]], ['loadRole', [9, 1]], @@ -159,7 +161,6 @@ public function providerForUnCachedMethods(): array null, ['ragl-14', 'rarl-9'], ], - ['removeRoleAssignment', [11], [['role_assignment', [11], false]], null, ['ra-11']], ]; } @@ -227,7 +228,17 @@ public function providerForCachedLoadMethodsHit(): array $role, ], ['loadRoleAssignment', [11], 'ibx-ra-11', null, null, [['role_assignment', [], true]], ['ibx-ra'], $roleAssignment], - ['loadRoleAssignmentsByRoleId', [9], 'ibx-ra-9-bro', null, null, [['role_assignment_with_by_role_suffix', [9], true]], ['ibx-ra-9-bro'], [$roleAssignment]], + ['loadRoleAssignmentsByRoleId', [$role->id], 'ibx-ra-9-bro', null, null, [['role_assignment_with_by_role_suffix', [9], true]], ['ibx-ra-9-bro'], [$roleAssignment]], + [ + 'loadRoleAssignmentsByRoleIdWithOffsetAndLimit', + [9, 0, 10], + 'ibx-ra-9-bro-0-10', + null, + null, + [['role_assignment_with_by_role_offset_limit_suffix', [9, 0, 10], true]], + ['ibx-ra-9-bro-0-10'], + [$roleAssignment], + ], [ 'loadRoleAssignmentsByGroupId', [14], @@ -393,6 +404,24 @@ public function providerForCachedLoadMethodsMiss(): array ['ibx-ra-9-bro'], [$roleAssignment], ], + [ + 'loadRoleAssignmentsByRoleIdWithOffsetAndLimit', + [9, 0, 10], + 'ibx-ra-9-bro-0-10', + [ + ['role_assignment_role_list', [9], false], + ['role', [9], false], + ['role_assignment', [11], false], + ['role_assignment_group_list', [14], false], + ['role_assignment_role_list', [9], false], + ], + ['rarl-9', 'r-9', 'ra-11', 'ragl-14', 'rarl-9'], + [ + ['role_assignment_with_by_role_offset_limit_suffix', [9, 0, 10], true], + ], + ['ibx-ra-9-bro-0-10'], + [$roleAssignment], + ], [ 'loadRoleAssignmentsByGroupId', [14], @@ -564,6 +593,48 @@ public function testAssignRole() $handler = $this->persistenceCacheHandler->userHandler(); $handler->assignRole($contentId, $roleId); } + + public function testRemoveRoleAssignment(): void + { + $handler = $this->persistenceCacheHandler->userHandler(); + $methodName = 'removeRoleAssignment'; + + $innerHandler = $this->createMock(SPIUserHandler::class); + $this->persistenceHandlerMock->method('userHandler')->willReturn($innerHandler); + $roleAssignmentId = 1; + $contentId = 2; + $roleId = 3; + $innerHandler + ->method('loadRoleAssignment') + ->willReturn( + new RoleAssignment(['id' => $roleAssignmentId, 'contentId' => $contentId, 'roleId' => $roleId]) + ); + + $this->loggerMock->method('logCall')->with( + UserHandler::class . "::$methodName", + [ + 'assignment' => $roleAssignmentId, + 'contentId' => $contentId, + 'roleId' => $roleId, + ] + ); + $innerHandler->method($methodName)->with($roleAssignmentId); + + $tags = [ + "ra-$roleAssignmentId", + "ragl-$contentId", + "rarl-$roleId", + ]; + $this->cacheIdentifierGeneratorMock + ->expects(self::exactly(count($tags))) + ->method('generateTag') + ->withConsecutive(['role_assignment'], ['role_assignment_group_list'], ['role_assignment_role_list']) + ->willReturnOnConsecutiveCalls(...$tags); + + $this->cacheMock->method('invalidateTags')->with($tags); + + $handler->removeRoleAssignment($roleAssignmentId); + } } class_alias(UserHandlerTest::class, 'eZ\Publish\Core\Persistence\Cache\Tests\UserHandlerTest'); diff --git a/tests/lib/Persistence/Legacy/User/Role/Gateway/DoctrineDatabaseTest.php b/tests/lib/Persistence/Legacy/User/Role/Gateway/DoctrineDatabaseTest.php index fa8539693d..e58c9f10fc 100644 --- a/tests/lib/Persistence/Legacy/User/Role/Gateway/DoctrineDatabaseTest.php +++ b/tests/lib/Persistence/Legacy/User/Role/Gateway/DoctrineDatabaseTest.php @@ -164,6 +164,34 @@ public function testLoadRoleAssignmentsByRoleId(): void ); } + /** + * @covers \Ibexa\Core\Persistence\Legacy\User\Role\Gateway\DoctrineDatabase::loadRoleAssignmentsByRoleIdWithOffsetAndLimit + */ + public function testLoadRoleAssignmentsByRoleIdWithOffsetAndLimit(): void + { + $gateway = $this->getDatabaseGateway(); + + self::assertEquals( + [ + [ + 'contentobject_id' => '11', + 'id' => '28', + 'limit_identifier' => '', + 'limit_value' => '', + 'role_id' => '1', + ], + [ + 'contentobject_id' => '42', + 'id' => '31', + 'limit_identifier' => '', + 'limit_value' => '', + 'role_id' => '1', + ], + ], + $gateway->loadRoleAssignmentsByRoleIdWithOffsetAndLimit(1, 0, 2) + ); + } + /** * Returns a ready to test DoctrineDatabase gateway. * diff --git a/tests/lib/Persistence/Legacy/User/UserHandlerTest.php b/tests/lib/Persistence/Legacy/User/UserHandlerTest.php index 573508556a..08f27b0748 100644 --- a/tests/lib/Persistence/Legacy/User/UserHandlerTest.php +++ b/tests/lib/Persistence/Legacy/User/UserHandlerTest.php @@ -1067,12 +1067,12 @@ public function testLoadComplexRoleAssignments() ); } - public function testLoadRoleAssignmentsByRoleId() + public function testLoadRoleAssignmentsByRoleId(): void { $this->insertSharedDatabaseFixture(); $handler = $this->getUserHandler(); - $this->assertEquals( + self::assertEquals( [ new Persistence\User\RoleAssignment( [ @@ -1100,6 +1100,32 @@ public function testLoadRoleAssignmentsByRoleId() ); } + public function testLoadRoleAssignmentsByRoleIdWithOffsetAndLimit(): void + { + $this->insertSharedDatabaseFixture(); + $handler = $this->getUserHandler(); + + self::assertEquals( + [ + new Persistence\User\RoleAssignment( + [ + 'id' => 28, + 'roleId' => 1, + 'contentId' => 11, + ] + ), + new Persistence\User\RoleAssignment( + [ + 'id' => 31, + 'roleId' => 1, + 'contentId' => 42, + ] + ), + ], + $handler->loadRoleAssignmentsByRoleIdWithOffsetAndLimit(1, 0, 2) + ); + } + public function testLoadRoleDraftByRoleId() { $this->insertSharedDatabaseFixture(); diff --git a/tests/lib/Persistence/Legacy/User/_fixtures/roles.php b/tests/lib/Persistence/Legacy/User/_fixtures/roles.php index a874812eb2..d1c9fe209a 100644 --- a/tests/lib/Persistence/Legacy/User/_fixtures/roles.php +++ b/tests/lib/Persistence/Legacy/User/_fixtures/roles.php @@ -128,4 +128,12 @@ 'role_id' => '4', ], ], + 'ezcontentobject' => [ + [ + 'id' => '11', + ], + [ + 'id' => '42', + ], + ], ]; diff --git a/tests/lib/Repository/Service/Mock/UserTest.php b/tests/lib/Repository/Service/Mock/UserTest.php index 6143b47297..fc38331218 100644 --- a/tests/lib/Repository/Service/Mock/UserTest.php +++ b/tests/lib/Repository/Service/Mock/UserTest.php @@ -6,10 +6,16 @@ */ namespace Ibexa\Tests\Core\Repository\Service\Mock; +use Exception; +use Ibexa\Contracts\Core\Persistence\User\Handler as PersistenceUserHandler; +use Ibexa\Contracts\Core\Persistence\User\RoleAssignment; use Ibexa\Contracts\Core\Repository\ContentService as APIContentService; use Ibexa\Contracts\Core\Repository\PasswordHashService; +use Ibexa\Contracts\Core\Repository\Repository; +use Ibexa\Contracts\Core\Repository\UserService as APIUserService; use Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo as APIContentInfo; use Ibexa\Contracts\Core\Repository\Values\Content\VersionInfo as APIVersionInfo; +use Ibexa\Contracts\Core\Repository\Values\User\User; use Ibexa\Contracts\Core\Repository\Values\User\User as APIUser; use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Ibexa\Core\Repository\User\PasswordValidatorInterface; @@ -17,121 +23,58 @@ use Ibexa\Tests\Core\Repository\Service\Mock\Base as BaseServiceMockTest; /** - * Mock test case for User Service. + * @covers \Ibexa\Core\Repository\UserService */ class UserTest extends BaseServiceMockTest { + private const MOCKED_USER_ID = 42; + /** - * Test for the deleteUser() method. - * - * @covers \Ibexa\Contracts\Core\Repository\UserService::deleteUser + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException */ - public function testDeleteUser() + public function testDeleteUser(): void { $repository = $this->getRepositoryMock(); $userService = $this->getPartlyMockedUserService(['loadUser']); $contentService = $this->createMock(APIContentService::class); + /* @var \PHPUnit\Framework\MockObject\MockObject $userHandler */ $userHandler = $this->getPersistenceMock()->userHandler(); $user = $this->createMock(APIUser::class); - $loadedUser = $this->createMock(APIUser::class); - $versionInfo = $this->createMock(APIVersionInfo::class); $contentInfo = $this->createMock(APIContentInfo::class); + $this->mockDeleteUserFlow($repository, $userService, $contentService, $user, $contentInfo, $userHandler); - $user->expects($this->once()) - ->method('__get') - ->with('id') - ->will($this->returnValue(42)); - - $versionInfo->expects($this->once()) - ->method('getContentInfo') - ->will($this->returnValue($contentInfo)); - - $loadedUser->expects($this->once()) - ->method('getVersionInfo') - ->will($this->returnValue($versionInfo)); - - $loadedUser->expects($this->once()) - ->method('__get') - ->with('id') - ->will($this->returnValue(42)); - - $userService->expects($this->once()) - ->method('loadUser') - ->with(42) - ->will($this->returnValue($loadedUser)); - - $repository->expects($this->once())->method('beginTransaction'); + $contentService->expects(self::once())->method('deleteContent')->with($contentInfo); + $userHandler->expects(self::once())->method('delete')->with(self::MOCKED_USER_ID); + $repository->expects(self::once())->method('commit'); - $contentService->expects($this->once()) - ->method('deleteContent') - ->with($contentInfo); - - $repository->expects($this->once()) - ->method('getContentService') - ->will($this->returnValue($contentService)); - - /* @var \PHPUnit\Framework\MockObject\MockObject $userHandler */ - $userHandler->expects($this->once()) - ->method('delete') - ->with(42); - - $repository->expects($this->once())->method('commit'); - - /* @var \Ibexa\Contracts\Core\Repository\Values\User\User $user */ $userService->deleteUser($user); } /** - * Test for the deleteUser() method. - * - * @covers \Ibexa\Contracts\Core\Repository\UserService::deleteUser + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException */ - public function testDeleteUserWithRollback() + public function testDeleteUserWithRollback(): void { - $this->expectException(\Exception::class); - $repository = $this->getRepositoryMock(); $userService = $this->getPartlyMockedUserService(['loadUser']); $contentService = $this->createMock(APIContentService::class); + /* @var \Ibexa\Contracts\Core\Persistence\User\Handler&\PHPUnit\Framework\MockObject\MockObject $userHandler */ + $userHandler = $this->getPersistenceMock()->userHandler(); $user = $this->createMock(APIUser::class); - $loadedUser = $this->createMock(APIUser::class); - $versionInfo = $this->createMock(APIVersionInfo::class); $contentInfo = $this->createMock(APIContentInfo::class); + $this->mockDeleteUserFlow($repository, $userService, $contentService, $user, $contentInfo, $userHandler); - $user->expects($this->once()) - ->method('__get') - ->with('id') - ->will($this->returnValue(42)); - - $versionInfo->expects($this->once()) - ->method('getContentInfo') - ->will($this->returnValue($contentInfo)); - - $loadedUser->expects($this->once()) - ->method('getVersionInfo') - ->will($this->returnValue($versionInfo)); - - $userService->expects($this->once()) - ->method('loadUser') - ->with(42) - ->will($this->returnValue($loadedUser)); - - $repository->expects($this->once())->method('beginTransaction'); - - $contentService->expects($this->once()) + $exception = new Exception(); + $contentService->expects(self::once()) ->method('deleteContent') ->with($contentInfo) - ->will($this->throwException(new \Exception())); - - $repository->expects($this->once()) - ->method('getContentService') - ->will($this->returnValue($contentService)); + ->willThrowException($exception); - $repository->expects($this->once())->method('rollback'); + $repository->expects(self::once())->method('rollback'); - /* @var \Ibexa\Contracts\Core\Repository\Values\User\User $user */ + $this->expectExceptionObject($exception); $userService->deleteUser($user); } @@ -142,12 +85,12 @@ public function testDeleteUserWithRollback() * * @param string[] $methods * - * @return \Ibexa\Core\Repository\UserService|\PHPUnit\Framework\MockObject\MockObject + * @return \Ibexa\Contracts\Core\Repository\UserService&\PHPUnit\Framework\MockObject\MockObject */ - protected function getPartlyMockedUserService(array $methods = null) + protected function getPartlyMockedUserService(array $methods = null): APIUserService { return $this->getMockBuilder(UserService::class) - ->setMethods($methods) + ->onlyMethods($methods) ->setConstructorArgs( [ $this->getRepositoryMock(), @@ -161,6 +104,44 @@ protected function getPartlyMockedUserService(array $methods = null) ) ->getMock(); } + + /** + * @param \Ibexa\Contracts\Core\Repository\Repository&\PHPUnit\Framework\MockObject\MockObject $repository + * @param \Ibexa\Contracts\Core\Repository\UserService&\PHPUnit\Framework\MockObject\MockObject $userService + * @param \Ibexa\Contracts\Core\Repository\ContentService&\PHPUnit\Framework\MockObject\MockObject $contentService + * @param \Ibexa\Contracts\Core\Repository\Values\User\User&\PHPUnit\Framework\MockObject\MockObject $user + * @param \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo&\PHPUnit\Framework\MockObject\MockObject $contentInfo + * @param \Ibexa\Contracts\Core\Persistence\User\Handler&\PHPUnit\Framework\MockObject\MockObject $userHandler + */ + private function mockDeleteUserFlow( + Repository $repository, + APIUserService $userService, + APIContentService $contentService, + User $user, + APIContentInfo $contentInfo, + PersistenceUserHandler $userHandler + ): void { + $loadedUser = $this->createMock(APIUser::class); + $versionInfo = $this->createMock(APIVersionInfo::class); + + $user->method('__get')->with('id')->willReturn(self::MOCKED_USER_ID); + $versionInfo->method('getContentInfo')->willReturn($contentInfo); + $loadedUser->method('getVersionInfo')->willReturn($versionInfo); + $loadedUser->method('__get')->with('id')->willReturn(self::MOCKED_USER_ID); + + $userService->method('loadUser')->with(self::MOCKED_USER_ID)->willReturn($loadedUser); + + $userHandler + ->expects(self::once()) + ->method('loadRoleAssignmentsByGroupId') + ->with(self::MOCKED_USER_ID) + ->willReturn([new RoleAssignment(['id' => 1])]); + + $userHandler->method('removeRoleAssignment')->with(1); + + $repository->expects(self::once())->method('beginTransaction'); + $repository->expects(self::once())->method('getContentService')->willReturn($contentService); + } } class_alias(UserTest::class, 'eZ\Publish\Core\Repository\Tests\Service\Mock\UserTest');