From 6c0d0544357585ef5b370e5f5c6786919b73efda Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 17 Oct 2024 10:18:40 +0200 Subject: [PATCH 01/26] Added UsersWithPermissionInfoToContentItemController --- ...hPermissionInfoToContentItemController.php | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/bundle/Controller/Permission/UsersWithPermissionInfoToContentItemController.php diff --git a/src/bundle/Controller/Permission/UsersWithPermissionInfoToContentItemController.php b/src/bundle/Controller/Permission/UsersWithPermissionInfoToContentItemController.php new file mode 100644 index 0000000000..992f795a87 --- /dev/null +++ b/src/bundle/Controller/Permission/UsersWithPermissionInfoToContentItemController.php @@ -0,0 +1,121 @@ +configResolver = $configResolver; + $this->userQueryType = $userQueryType; + $this->searchService = $searchService; + $this->userService = $userService; + $this->userWithPermissionsMapper = $userWithPermissionsMapper; + $this->limit = $limit; + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + public function listAction( + Request $request, + ContentInfo $contentInfo, + string $module, + string $function, + ?Location $location = null + ): JsonResponse { + $searchQuery = $this->getQuery($request->query); + $users = $this->searchService->findContentInfo($searchQuery, [], false); + $targets = null !== $location ? [$location] : []; + + $response = $this->userWithPermissionsMapper->mapSearchResults( + $users, + $contentInfo, + $module, + $function, + $targets + ); + + return new JsonResponse($response); + } + + private function getQuery(ParameterBag $query): Query + { + $limit = $query->getInt('limit', $this->limit); + $offset = $query->getInt('offset'); + $phrase = $query->get('phrase'); + + return $this->userQueryType->getQuery( + [ + 'limit' => $limit, + 'offset' => $offset, + 'phrase' => $phrase, + 'section_identifiers' => ['users'], + 'exclude_users_ids' => [$this->getAnonymousUserId()], + 'exclude_paths' => [$this->getUserRegistrationGroupId()], + ] + ); + } + + private function getAnonymousUserId(): int + { + return $this->configResolver->getParameter('anonymous_user_id'); + } + + private function getUserRegistrationGroupId(): string + { + $groupId = $this->configResolver->getParameter('user_registration.group_id'); + + $userGroup = $this->repository->sudo( + fn (): UserGroup => $this->userService->loadUserGroup($groupId) + ); + + $location = $userGroup->getContentInfo()->getMainLocation(); + if (null === $location) { + throw new LogicException('User registration group must have a main location.'); + } + + return $location->getPathString(); + } +} From d244b37112635b65e545ac2dbac31f995a484575 Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 17 Oct 2024 10:18:53 +0200 Subject: [PATCH 02/26] Added route --- src/bundle/Resources/config/routing.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/bundle/Resources/config/routing.yaml b/src/bundle/Resources/config/routing.yaml index cc9fd2b3ca..370a0c9036 100644 --- a/src/bundle/Resources/config/routing.yaml +++ b/src/bundle/Resources/config/routing.yaml @@ -971,3 +971,14 @@ ibexa.focus_mode.change: path: /user/focus-mode controller: 'Ibexa\Bundle\AdminUi\Controller\User\FocusModeController::changeAction' methods: [GET, POST] + +# +# Users +# +ibexa.permission.users_with_permission_info: + path: /permission/users-with-permission-info/{contentInfoId}/{module}/{function} + controller: 'Ibexa\Bundle\AdminUi\Controller\Permission\UsersWithPermissionInfoToContentItemController::listAction' + methods: [GET] + requirements: + module: \w+ + function: \w+ From 34eb334fc2d339cd85f308b6c6014f91bf25ad87 Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 17 Oct 2024 10:20:36 +0200 Subject: [PATCH 03/26] Added UserQueryType --- src/lib/QueryType/UserQueryType.php | 102 ++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/lib/QueryType/UserQueryType.php diff --git a/src/lib/QueryType/UserQueryType.php b/src/lib/QueryType/UserQueryType.php new file mode 100644 index 0000000000..deb0fe4635 --- /dev/null +++ b/src/lib/QueryType/UserQueryType.php @@ -0,0 +1,102 @@ +setDefined('phrase'); + $resolver->setDefined('exclude_users_ids'); + $resolver->setDefined('exclude_paths'); + $resolver->setDefined('section_identifiers'); + + $resolver->setAllowedTypes('phrase', ['string', 'null']); + $resolver->setAllowedTypes('exclude_users_ids', ['array']); + $resolver->setAllowedTypes('exclude_paths', ['array']); + $resolver->setAllowedTypes('section_identifiers', ['array']); + + $resolver->setDefaults( + [ + 'phrase' => null, + 'exclude_users_ids' => [], + 'exclude_paths' => [], + 'section_identifiers' => [], + ], + ); + } + + /** + * @param array $parameters + */ + protected function doGetQuery(array $parameters): Query + { + $parameters['filter']['siteaccess_aware'] = false; + + return parent::doGetQuery($parameters); + } + + /** + * @param array $parameters + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidCriterionArgumentException + */ + protected function getQueryFilter(array $parameters): Criterion + { + $userContentTypeIdentifiers = $this->getUserContentTypeIdentifiers(); + $criteria = [new Criterion\ContentTypeIdentifier($userContentTypeIdentifiers)]; + + if (!empty($parameters['exclude_users_ids'])) { + $excludedUsersIds = new Criterion\ContentId($parameters['exclude_users_ids']); + $criteria[] = new Criterion\LogicalNot($excludedUsersIds); + } + + if (!empty($parameters['exclude_paths'])) { + $excludedParentLocationIds = new Criterion\Subtree($parameters['exclude_paths']); + $criteria[] = new Criterion\LogicalNot($excludedParentLocationIds); + } + + if (!empty($parameters['section_identifiers'])) { + $criteria[] = new Criterion\SectionIdentifier($parameters['section_identifiers']); + } + + if (!empty($parameters['phrase'])) { + $phrase = '*' . $parameters['phrase'] . '*'; + $criteria[] = new Criterion\LogicalOr( + [ + new Criterion\Field('first_name', Criterion\Operator::LIKE, $phrase), + new Criterion\Field('last_name', Criterion\Operator::LIKE, $phrase), + new Criterion\UserEmail($phrase, Criterion\Operator::LIKE), + ] + ); + } + + return new Criterion\LogicalAnd($criteria); + } + + public static function getName(): string + { + return 'IbexaAdminUi:User'; + } + + /** + * @return array + */ + private function getUserContentTypeIdentifiers(): array + { + return $this->configResolver->getParameter('user_content_type_identifier'); + } +} From 76158951cb010c8268ce62accf86d86b0c514277 Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 17 Oct 2024 10:20:53 +0200 Subject: [PATCH 04/26] Added UsersWithPermissionInfoToContentItemMapper --- ...sWithPermissionInfoToContentItemMapper.php | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 src/lib/User/Mapper/UsersWithPermissionInfoToContentItemMapper.php diff --git a/src/lib/User/Mapper/UsersWithPermissionInfoToContentItemMapper.php b/src/lib/User/Mapper/UsersWithPermissionInfoToContentItemMapper.php new file mode 100644 index 0000000000..018b93b092 --- /dev/null +++ b/src/lib/User/Mapper/UsersWithPermissionInfoToContentItemMapper.php @@ -0,0 +1,122 @@ +userService = $userService; + $this->permissionResolver = $permissionResolver; + } + + /** + * @param array<\Ibexa\Contracts\Core\Repository\Values\ValueObject> $targets + * + * @phpstan-return TUserData + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + public function mapSearchResults( + SearchResult $searchResult, + ContentInfo $contentInfo, + string $module, + string $function, + array $targets = [] + ): array { + $currentUserReference = $this->permissionResolver->getCurrentUserReference(); + + $results = $this->groupByPermissions($searchResult, $contentInfo, $module, $function, $targets); + + $this->permissionResolver->setCurrentUserReference($currentUserReference); + + return $results; + } + + /** + * @param array<\Ibexa\Contracts\Core\Repository\Values\ValueObject> $targets + * + * @phpstan-return TUserData + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + private function groupByPermissions( + SearchResult $searchResult, + ContentInfo $contentInfo, + string $module, + string $function, + array $targets = [] + ): array { + $results = [ + 'access' => [], + 'no_access' => [], + ]; + + foreach ($searchResult as $result) { + /** @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo $userContentInfo */ + $userContentInfo = $result->valueObject; + + $user = $this->userService->loadUser($userContentInfo->getId()); + $userReference = new UserReference($user->getUserId()); + $userData = $this->getUserData($user); + + $this->permissionResolver->setCurrentUserReference($userReference); + + if ($this->permissionResolver->canUser($module, $function, $contentInfo, $targets)) { + $results['access'][] = $userData; + } else { + $results['no_access'][] = $userData; + } + } + + return $results; + } + + /** + * @return array{ + * name: string, + * email: string, + * } + */ + private function getUserData(User $user): array + { + return [ + 'name' => $user->getName() ?? $user->getLogin(), + 'email' => $user->email, + ]; + } +} From adfbf1426132cf03cd505b7f6793b3cf6fc5d5ac Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 17 Oct 2024 10:21:09 +0200 Subject: [PATCH 05/26] [DI] Added services definitions --- src/bundle/Resources/config/services.yaml | 5 +++++ src/bundle/Resources/config/services/controllers.yaml | 8 ++++++++ src/bundle/Resources/config/services/query_types.yaml | 2 ++ 3 files changed, 15 insertions(+) diff --git a/src/bundle/Resources/config/services.yaml b/src/bundle/Resources/config/services.yaml index f4d9534827..55910c5bd6 100644 --- a/src/bundle/Resources/config/services.yaml +++ b/src/bundle/Resources/config/services.yaml @@ -40,6 +40,9 @@ imports: - { resource: services/role_form_mappers.yaml } - { resource: services/security.yaml } +parameters: + ibexa.admin_ui.load_users_with_permission_info.limit: 10 + services: _defaults: autowire: true @@ -162,3 +165,5 @@ services: $siteAccessGroups: '%ibexa.site_access.groups%' tags: - {name: kernel.event_subscriber} + + Ibexa\AdminUi\User\Mapper\UsersWithPermissionInfoToContentItemMapper: ~ diff --git a/src/bundle/Resources/config/services/controllers.yaml b/src/bundle/Resources/config/services/controllers.yaml index c80448447a..9055884ee0 100644 --- a/src/bundle/Resources/config/services/controllers.yaml +++ b/src/bundle/Resources/config/services/controllers.yaml @@ -252,3 +252,11 @@ services: $imageMappings: '%ibexa.dam_widget.image.mappings%' tags: - controller.service_arguments + + Ibexa\Bundle\AdminUi\Controller\Permission\UsersWithPermissionInfoToContentItemController: + parent: Ibexa\Rest\Server\Controller + arguments: + $userQueryType: '@Ibexa\AdminUi\QueryType\UserQueryType' + $limit: '%ibexa.admin_ui.load_users_with_permission_info.limit%' + tags: + - controller.service_arguments diff --git a/src/bundle/Resources/config/services/query_types.yaml b/src/bundle/Resources/config/services/query_types.yaml index c099fcdac9..4efe77566a 100644 --- a/src/bundle/Resources/config/services/query_types.yaml +++ b/src/bundle/Resources/config/services/query_types.yaml @@ -15,3 +15,5 @@ services: Ibexa\AdminUi\QueryType\MediaLocationSubtreeQueryType: ~ Ibexa\AdminUi\QueryType\TrashSearchQueryType: ~ + + Ibexa\AdminUi\QueryType\UserQueryType: ~ From 55c2b8bd9b9f9c38d82125ed26f32d146bde77dc Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 17 Oct 2024 10:21:35 +0200 Subject: [PATCH 06/26] [Tests] Added integration test --- ...ersWithPermissionInfoToContentItemTest.php | 203 ++++++++++++++++++ tests/integration/Resources/routing.yaml | 3 + 2 files changed, 206 insertions(+) create mode 100644 tests/integration/REST/GetUsersWithPermissionInfoToContentItemTest.php diff --git a/tests/integration/REST/GetUsersWithPermissionInfoToContentItemTest.php b/tests/integration/REST/GetUsersWithPermissionInfoToContentItemTest.php new file mode 100644 index 0000000000..d5af5c7b3c --- /dev/null +++ b/tests/integration/REST/GetUsersWithPermissionInfoToContentItemTest.php @@ -0,0 +1,203 @@ + 'application/json', + 'X-Siteaccess' => 'admin', + ]; + private const EDITOR_USER_GROUP_REMOTE_ID = '3c160cca19fb135f83bd02d911f04db2'; + private const USER_REGISTRATION_REMOTE_ID = '5f7f0bdb3381d6a461d8c29ff53d908f'; + private const MEDIA_CONTENT_ITEM_ID = 41; + private const MEDIA_LOCATION_ID = 43; + private const MODULE_CONTENT = 'content'; + private const FUNCTION_EDIT = 'edit'; + private const FUNCTION_READ = 'read'; + + private UserService $userService; + + private RoleService $roleService; + + protected function setUp(): void + { + parent::setUp(); + + $ibexaTestCore = $this->getIbexaTestCore(); + $this->userService = $ibexaTestCore->getUserService(); + $this->roleService = $ibexaTestCore->getRoleService(); + + $this->createUsers(); + } + + /** + * @dataProvider provideDataForTestGetUsersWithPermissionsEndpoint + * + * @param array $queryParameters + */ + public function testGetUsersWithPermissionsEndpoint( + int $contentId, + string $module, + string $function, + array $queryParameters, + string $expectedResponse + ): void { + $uri = $this->getUri($contentId, $module, $function, $queryParameters); + $this->client->request('GET', $uri, [], [], self::HEADERS); + + $response = $this->client->getResponse(); + + self::assertSame(200, $response->getStatusCode()); + self::assertSame($expectedResponse, $response->getContent()); + } + + /** + * @return iterable, + * string, + * }> + */ + public function provideDataForTestGetUsersWithPermissionsEndpoint(): iterable + { + yield 'Check content-read for content item 41' => [ + self::MEDIA_CONTENT_ITEM_ID, + self::MODULE_CONTENT, + self::FUNCTION_READ, + [], + '{"access":[{"name":"Administrator User","email":"admin@link.invalid"},{"name":"John Doe","email":"john@link.invalid"},{"name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', + ]; + + yield 'Check content-read for content item 41 and location 51' => [ + self::MEDIA_CONTENT_ITEM_ID, + self::MODULE_CONTENT, + self::FUNCTION_READ, + ['locationId' => 51], + '{"access":[{"name":"Administrator User","email":"admin@link.invalid"},{"name":"John Doe","email":"john@link.invalid"},{"name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', + ]; + + yield 'Check content-read for content item 41 and phrase=adm' => [ + self::MEDIA_CONTENT_ITEM_ID, + self::MODULE_CONTENT, + self::FUNCTION_READ, + ['phrase' => 'Adm*'], + '{"access":[{"name":"Administrator User","email":"admin@link.invalid"}],"no_access":[]}', + ]; + + yield 'Check content-read for phrase=undef*' => [ + self::MEDIA_CONTENT_ITEM_ID, + self::MODULE_CONTENT, + self::FUNCTION_READ, + ['phrase' => 'undef*'], + '{"access":[],"no_access":[]}', + ]; + + yield 'Check content-edit for content item 2 and phrase=jo' => [ + self::MEDIA_CONTENT_ITEM_ID, + self::MODULE_CONTENT, + self::FUNCTION_EDIT, + [ + 'phrase' => 'jo*', + ], + '{"access":[{"name":"John Doe","email":"john@link.invalid"},{"name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', + ]; + + yield 'Check content-edit for content item 41 and phrase=bar*' => [ + self::MEDIA_CONTENT_ITEM_ID, + self::MODULE_CONTENT, + self::FUNCTION_EDIT, + [ + 'phrase' => 'bar*', + ], + '{"access":[{"name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', + ]; + + yield 'Check content-edit for content item 41 and location 43 and phrase=joshua@link.invalid' => [ + self::MEDIA_CONTENT_ITEM_ID, + self::MODULE_CONTENT, + self::FUNCTION_EDIT, + [ + 'phrase' => 'joshua*', + 'locationId' => self::MEDIA_LOCATION_ID, + ], + '{"access":[{"name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', + ]; + } + + private function createUsers(): void + { + $this->getIbexaTestCore()->setAdministratorUser(); + + $role = $this->roleService->loadRoleByIdentifier('Editor'); + $editorGroup = $this->userService->loadUserGroupByRemoteId(self::EDITOR_USER_GROUP_REMOTE_ID); + + $user1CreateStruct = $this->createUserCreateStruct('john', 'John', 'Doe', 'john@link.invalid'); + + $user1 = $this->userService->createUser($user1CreateStruct, [$editorGroup]); + $this->roleService->assignRoleToUser($role, $user1); + + $user2CreateStruct = $this->createUserCreateStruct('josh', 'Josh', 'Bar', 'joshua@link.invalid'); + $user2 = $this->userService->createUser($user2CreateStruct, [$editorGroup]); + $this->roleService->assignRoleToUser($role, $user2); + + // Guest user should not be visible on the list + $guestCreateStruct = $this->createUserCreateStruct('guest', 'Guest', 'Guest', 'guest@link.invalid'); + $groupGuest = $this->userService->loadUserGroupByRemoteId(self::USER_REGISTRATION_REMOTE_ID); + $guest = $this->userService->createUser($guestCreateStruct, [$groupGuest]); + + $roleMember = $this->roleService->loadRoleByIdentifier('Member'); + $this->roleService->assignRoleToUser($roleMember, $guest); + } + + private function createUserCreateStruct( + string $login, + string $firstName, + string $lastName, + string $email + ): UserCreateStruct { + $userCreateStruct = $this->userService->newUserCreateStruct( + $login, + $email, + $login, + 'eng-GB' + ); + + $userCreateStruct->setField('first_name', $firstName); + $userCreateStruct->setField('last_name', $lastName); + + return $userCreateStruct; + } + + /** + * @param array $queryParameters + */ + private static function getUri( + int $contentId, + string $module, + string $function, + array $queryParameters = [] + ): string { + return sprintf( + self::ENDPOINT_URL, + $contentId, + $module, + $function, + http_build_query($queryParameters), + ); + } +} diff --git a/tests/integration/Resources/routing.yaml b/tests/integration/Resources/routing.yaml index 83a0143bc3..7c425026d6 100644 --- a/tests/integration/Resources/routing.yaml +++ b/tests/integration/Resources/routing.yaml @@ -1,3 +1,6 @@ +ibexa.admin_ui: + resource: '@IbexaAdminUiBundle/Resources/config/routing.yaml' + ibexa.admin_ui.rest: resource: '@IbexaAdminUiBundle/Resources/config/routing_rest.yaml' prefix: '%ibexa.rest.path_prefix%' From cabdfe7140690198cc5da91d7d4a5912c0d80a33 Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Fri, 18 Oct 2024 09:39:59 +0200 Subject: [PATCH 07/26] Renamed method getUserRegistrationGroupId to getUserRegistrationGroupPath --- .../UsersWithPermissionInfoToContentItemController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bundle/Controller/Permission/UsersWithPermissionInfoToContentItemController.php b/src/bundle/Controller/Permission/UsersWithPermissionInfoToContentItemController.php index 992f795a87..2121a32d9d 100644 --- a/src/bundle/Controller/Permission/UsersWithPermissionInfoToContentItemController.php +++ b/src/bundle/Controller/Permission/UsersWithPermissionInfoToContentItemController.php @@ -93,7 +93,7 @@ private function getQuery(ParameterBag $query): Query 'phrase' => $phrase, 'section_identifiers' => ['users'], 'exclude_users_ids' => [$this->getAnonymousUserId()], - 'exclude_paths' => [$this->getUserRegistrationGroupId()], + 'exclude_paths' => [$this->getUserRegistrationGroupPath()], ] ); } @@ -103,7 +103,7 @@ private function getAnonymousUserId(): int return $this->configResolver->getParameter('anonymous_user_id'); } - private function getUserRegistrationGroupId(): string + private function getUserRegistrationGroupPath(): string { $groupId = $this->configResolver->getParameter('user_registration.group_id'); From 6d016f15fe416bb91ea69688b91c99d2edb8bb5e Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Fri, 18 Oct 2024 09:47:21 +0200 Subject: [PATCH 08/26] Fixed phpstan-type TUserData --- .../UsersWithPermissionInfoToContentItemMapper.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/User/Mapper/UsersWithPermissionInfoToContentItemMapper.php b/src/lib/User/Mapper/UsersWithPermissionInfoToContentItemMapper.php index 018b93b092..7acf44ab94 100644 --- a/src/lib/User/Mapper/UsersWithPermissionInfoToContentItemMapper.php +++ b/src/lib/User/Mapper/UsersWithPermissionInfoToContentItemMapper.php @@ -16,14 +16,14 @@ /** * @phpstan-type TUserData array{ - * access: array{ + * access: array, + * no_access: array, * } */ final class UsersWithPermissionInfoToContentItemMapper From ac8e35bb29f6a358d533fb4d05d110236b6778e4 Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 31 Oct 2024 09:46:47 +0100 Subject: [PATCH 09/26] Reworked UserQueryType --- src/lib/QueryType/UserQueryType.php | 76 ++++++++++++++++------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/src/lib/QueryType/UserQueryType.php b/src/lib/QueryType/UserQueryType.php index deb0fe4635..e3d2fa3d89 100644 --- a/src/lib/QueryType/UserQueryType.php +++ b/src/lib/QueryType/UserQueryType.php @@ -11,31 +11,28 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Core\QueryType\BuiltIn\AbstractQueryType; +use RuntimeException; use Symfony\Component\OptionsResolver\OptionsResolver; final class UserQueryType extends AbstractQueryType { + private const USER_ADMIN_ID = 14; + protected function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); $resolver->setDefined('phrase'); - $resolver->setDefined('exclude_users_ids'); - $resolver->setDefined('exclude_paths'); - $resolver->setDefined('section_identifiers'); - $resolver->setAllowedTypes('phrase', ['string', 'null']); - $resolver->setAllowedTypes('exclude_users_ids', ['array']); - $resolver->setAllowedTypes('exclude_paths', ['array']); - $resolver->setAllowedTypes('section_identifiers', ['array']); + + $resolver->setDefined('extra_criteria'); + $resolver->setAllowedTypes('extra_criteria', [Criterion::class, 'null']); $resolver->setDefaults( [ 'phrase' => null, - 'exclude_users_ids' => [], - 'exclude_paths' => [], - 'section_identifiers' => [], - ], + 'extra_criteria' => null, + ] ); } @@ -56,30 +53,22 @@ protected function doGetQuery(array $parameters): Query */ protected function getQueryFilter(array $parameters): Criterion { - $userContentTypeIdentifiers = $this->getUserContentTypeIdentifiers(); - $criteria = [new Criterion\ContentTypeIdentifier($userContentTypeIdentifiers)]; - - if (!empty($parameters['exclude_users_ids'])) { - $excludedUsersIds = new Criterion\ContentId($parameters['exclude_users_ids']); - $criteria[] = new Criterion\LogicalNot($excludedUsersIds); - } + $criteria = [ + new Criterion\IsUserEnabled(), + $this->excludeSystemUsers(), + ]; - if (!empty($parameters['exclude_paths'])) { - $excludedParentLocationIds = new Criterion\Subtree($parameters['exclude_paths']); - $criteria[] = new Criterion\LogicalNot($excludedParentLocationIds); - } - - if (!empty($parameters['section_identifiers'])) { - $criteria[] = new Criterion\SectionIdentifier($parameters['section_identifiers']); + if (!empty($parameters['extra_criteria'])) { + $criteria[] = $parameters['extra_criteria']; } if (!empty($parameters['phrase'])) { - $phrase = '*' . $parameters['phrase'] . '*'; + $phrase = $this->cleanSearchPhrase($parameters['phrase']); $criteria[] = new Criterion\LogicalOr( [ - new Criterion\Field('first_name', Criterion\Operator::LIKE, $phrase), - new Criterion\Field('last_name', Criterion\Operator::LIKE, $phrase), - new Criterion\UserEmail($phrase, Criterion\Operator::LIKE), + new Criterion\ContentName('*' . $phrase . '*'), + // Used with EQ operator and without wildcards due to hashing email in solr and elasticsearch + new Criterion\UserEmail($phrase, Criterion\Operator::EQ), ] ); } @@ -92,11 +81,30 @@ public static function getName(): string return 'IbexaAdminUi:User'; } - /** - * @return array - */ - private function getUserContentTypeIdentifiers(): array + private function cleanSearchPhrase(string $phrase): string + { + $sanitizedPhrase = preg_replace('/[^a-zA-Z0-9@._-]/', '', $phrase); + if (null === $sanitizedPhrase) { + throw new RuntimeException('Could not to sanitize search phrase.'); + } + + return $sanitizedPhrase; + } + + private function excludeSystemUsers(): Criterion + { + return new Criterion\LogicalNot( + new Criterion\ContentId( + [ + self::USER_ADMIN_ID, + $this->getAnonymousUserId(), + ] + ), + ); + } + + private function getAnonymousUserId(): int { - return $this->configResolver->getParameter('user_content_type_identifier'); + return $this->configResolver->getParameter('anonymous_user_id'); } } From 878441381196650d2ce1ca83b471540ac9633fcd Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 31 Oct 2024 09:47:22 +0100 Subject: [PATCH 10/26] Added PermissionCheckContextResolver --- .../PermissionCheckContextResolver.php | 43 +++++++++++++++++++ ...ermissionCheckContextResolverInterface.php | 16 +++++++ 2 files changed, 59 insertions(+) create mode 100644 src/lib/Permission/PermissionCheckContextResolver.php create mode 100644 src/lib/Permission/PermissionCheckContextResolverInterface.php diff --git a/src/lib/Permission/PermissionCheckContextResolver.php b/src/lib/Permission/PermissionCheckContextResolver.php new file mode 100644 index 0000000000..9e15c24302 --- /dev/null +++ b/src/lib/Permission/PermissionCheckContextResolver.php @@ -0,0 +1,43 @@ + */ + private iterable $permissionContextProviders; + + /** + * @param iterable<\Ibexa\Contracts\AdminUi\Permission\PermissionCheckContextProviderInterface> $permissionContextProviders + */ + public function __construct(iterable $permissionContextProviders) + { + $this->permissionContextProviders = $permissionContextProviders; + } + + /** + * @throws \Ibexa\Contracts\Core\Exception\InvalidArgumentException + */ + public function resolve(string $module, string $function, Request $request): PermissionCheckContext + { + foreach ($this->permissionContextProviders as $provider) { + if ($provider->supports($module, $function)) { + return $provider->getPermissionCheckContext($module, $function, $request); + } + } + + throw new InvalidArgumentException( + '$request', + 'Unsupported permission context.' + ); + } +} diff --git a/src/lib/Permission/PermissionCheckContextResolverInterface.php b/src/lib/Permission/PermissionCheckContextResolverInterface.php new file mode 100644 index 0000000000..6cbd11fc9c --- /dev/null +++ b/src/lib/Permission/PermissionCheckContextResolverInterface.php @@ -0,0 +1,16 @@ + Date: Thu, 31 Oct 2024 09:47:58 +0100 Subject: [PATCH 11/26] Added PermissionCheckContextProviderInterface --- ...PermissionCheckContextProviderInterface.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/contracts/Permission/PermissionCheckContextProviderInterface.php diff --git a/src/contracts/Permission/PermissionCheckContextProviderInterface.php b/src/contracts/Permission/PermissionCheckContextProviderInterface.php new file mode 100644 index 0000000000..443901a9ab --- /dev/null +++ b/src/contracts/Permission/PermissionCheckContextProviderInterface.php @@ -0,0 +1,18 @@ + Date: Thu, 31 Oct 2024 09:48:45 +0100 Subject: [PATCH 12/26] Added ContentItemContextProvider --- .../ContentItemContextProvider.php | 111 ++++++++++++++++++ src/lib/Values/PermissionCheckContext.php | 53 +++++++++ 2 files changed, 164 insertions(+) create mode 100644 src/lib/Permission/ContextProvider/ContentItemContextProvider.php create mode 100644 src/lib/Values/PermissionCheckContext.php diff --git a/src/lib/Permission/ContextProvider/ContentItemContextProvider.php b/src/lib/Permission/ContextProvider/ContentItemContextProvider.php new file mode 100644 index 0000000000..4e1e018cf5 --- /dev/null +++ b/src/lib/Permission/ContextProvider/ContentItemContextProvider.php @@ -0,0 +1,111 @@ + */ + private array $userContentTypeIdentifiers; + + /** + * @param array $userContentTypeIdentifiers + */ + public function __construct( + ContentService $contentService, + LocationService $locationService, + array $userContentTypeIdentifiers + ) { + $this->contentService = $contentService; + $this->locationService = $locationService; + $this->userContentTypeIdentifiers = $userContentTypeIdentifiers; + } + + public function supports(string $module, string $function): bool + { + return self::POLICY_MODULE_CONTENT === $module; + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidCriterionArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException + */ + public function getPermissionCheckContext( + string $module, + string $function, + Request $request + ): PermissionCheckContext { + $query = $request->query; + + $contentInfo = $this->getContentInfo($query); + $targets = $this->getTargets($query); + $criteria = $this->createCriteria(); + + return new PermissionCheckContext($contentInfo, $targets, $criteria); + } + + private function getContentInfo(ParameterBag $query): ContentInfo + { + $contentId = $query->getInt('contentId'); + + return $this->contentService->loadContentInfo($contentId); + } + + /** + * @return array<\Ibexa\Contracts\Core\Repository\Values\ValueObject> + * + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException + * @throws \Ibexa\Contracts\Core\Exception\InvalidArgumentException + */ + private function getTargets(ParameterBag $query): array + { + if (!$query->has('locationId')) { + return []; + } + + $locationId = $query->getInt('locationId'); + if ($locationId <= 0) { + throw new InvalidArgumentException( + 'locationId', + 'Expected value should be greater than 0.' + ); + } + + $location = $this->locationService->loadLocation($locationId); + + return [$location]; + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidCriterionArgumentException + */ + private function createCriteria(): Criterion + { + $criteria = [new Criterion\ContentTypeIdentifier($this->userContentTypeIdentifiers)]; + + return new Criterion\LogicalAnd($criteria); + } +} diff --git a/src/lib/Values/PermissionCheckContext.php b/src/lib/Values/PermissionCheckContext.php new file mode 100644 index 0000000000..5fadbe7b48 --- /dev/null +++ b/src/lib/Values/PermissionCheckContext.php @@ -0,0 +1,53 @@ + */ + private array $targets; + + private ?Criterion $criteria; + + /** + * @param array<\Ibexa\Contracts\Core\Repository\Values\ValueObject> $targets + */ + public function __construct( + ValueObject $subject, + array $targets, + ?Criterion $criteria = null + ) { + $this->subject = $subject; + $this->targets = $targets; + $this->criteria = $criteria; + } + + public function getSubject(): ValueObject + { + return $this->subject; + } + + /** + * @return array<\Ibexa\Contracts\Core\Repository\Values\ValueObject> + */ + public function getTargets(): array + { + return $this->targets; + } + + public function getCriteria(): ?Criterion + { + return $this->criteria; + } +} From 53f57fd23b6d0da44ca04fe45f14342121870f5e Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 31 Oct 2024 09:49:04 +0100 Subject: [PATCH 13/26] Reworked UsersWithPermissionInfoController --- .../UsersWithPermissionInfoController.php | 87 +++++++++++++ ...hPermissionInfoToContentItemController.php | 121 ------------------ 2 files changed, 87 insertions(+), 121 deletions(-) create mode 100644 src/bundle/Controller/Permission/UsersWithPermissionInfoController.php delete mode 100644 src/bundle/Controller/Permission/UsersWithPermissionInfoToContentItemController.php diff --git a/src/bundle/Controller/Permission/UsersWithPermissionInfoController.php b/src/bundle/Controller/Permission/UsersWithPermissionInfoController.php new file mode 100644 index 0000000000..118340815f --- /dev/null +++ b/src/bundle/Controller/Permission/UsersWithPermissionInfoController.php @@ -0,0 +1,87 @@ +userQueryType = $userQueryType; + $this->permissionCheckContextResolver = $permissionCheckContextResolver; + $this->searchService = $searchService; + $this->userWithPermissionsMapper = $userWithPermissionsMapper; + $this->limit = $limit; + } + + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + public function listAction( + Request $request, + string $module, + string $function + ): JsonResponse { + $context = $this->permissionCheckContextResolver->resolve($module, $function, $request); + $searchQuery = $this->getQuery( + $request->query, + $context->getCriteria() + ); + $users = $this->searchService->findContentInfo($searchQuery, [], false); + + $response = $this->userWithPermissionsMapper->mapSearchResults( + $users, + $context, + $module, + $function + ); + + return new JsonResponse($response); + } + + private function getQuery( + ParameterBag $query, + ?Query\Criterion $criteria + ): Query { + $parameters = [ + 'limit' => $query->getInt('limit', $this->limit), + 'offset' => $query->getInt('offset'), + 'phrase' => $query->get('phrase'), + 'extra_criteria' => $criteria, + ]; + + return $this->userQueryType->getQuery($parameters); + } +} diff --git a/src/bundle/Controller/Permission/UsersWithPermissionInfoToContentItemController.php b/src/bundle/Controller/Permission/UsersWithPermissionInfoToContentItemController.php deleted file mode 100644 index 2121a32d9d..0000000000 --- a/src/bundle/Controller/Permission/UsersWithPermissionInfoToContentItemController.php +++ /dev/null @@ -1,121 +0,0 @@ -configResolver = $configResolver; - $this->userQueryType = $userQueryType; - $this->searchService = $searchService; - $this->userService = $userService; - $this->userWithPermissionsMapper = $userWithPermissionsMapper; - $this->limit = $limit; - } - - /** - * @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException - * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException - * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException - */ - public function listAction( - Request $request, - ContentInfo $contentInfo, - string $module, - string $function, - ?Location $location = null - ): JsonResponse { - $searchQuery = $this->getQuery($request->query); - $users = $this->searchService->findContentInfo($searchQuery, [], false); - $targets = null !== $location ? [$location] : []; - - $response = $this->userWithPermissionsMapper->mapSearchResults( - $users, - $contentInfo, - $module, - $function, - $targets - ); - - return new JsonResponse($response); - } - - private function getQuery(ParameterBag $query): Query - { - $limit = $query->getInt('limit', $this->limit); - $offset = $query->getInt('offset'); - $phrase = $query->get('phrase'); - - return $this->userQueryType->getQuery( - [ - 'limit' => $limit, - 'offset' => $offset, - 'phrase' => $phrase, - 'section_identifiers' => ['users'], - 'exclude_users_ids' => [$this->getAnonymousUserId()], - 'exclude_paths' => [$this->getUserRegistrationGroupPath()], - ] - ); - } - - private function getAnonymousUserId(): int - { - return $this->configResolver->getParameter('anonymous_user_id'); - } - - private function getUserRegistrationGroupPath(): string - { - $groupId = $this->configResolver->getParameter('user_registration.group_id'); - - $userGroup = $this->repository->sudo( - fn (): UserGroup => $this->userService->loadUserGroup($groupId) - ); - - $location = $userGroup->getContentInfo()->getMainLocation(); - if (null === $location) { - throw new LogicException('User registration group must have a main location.'); - } - - return $location->getPathString(); - } -} From d41001bc965078c1d9d622f8843894927dc33dac Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 31 Oct 2024 09:49:35 +0100 Subject: [PATCH 14/26] Reworked UsersWithPermissionInfoMapper --- .../Mapper/UsersWithPermissionInfoMapper.php} | 56 +++++++++---------- 1 file changed, 25 insertions(+), 31 deletions(-) rename src/lib/{User/Mapper/UsersWithPermissionInfoToContentItemMapper.php => Permission/Mapper/UsersWithPermissionInfoMapper.php} (73%) diff --git a/src/lib/User/Mapper/UsersWithPermissionInfoToContentItemMapper.php b/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php similarity index 73% rename from src/lib/User/Mapper/UsersWithPermissionInfoToContentItemMapper.php rename to src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php index 7acf44ab94..77449b2017 100644 --- a/src/lib/User/Mapper/UsersWithPermissionInfoToContentItemMapper.php +++ b/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php @@ -5,32 +5,31 @@ * @license For full copyright and license information view LICENSE file distributed with this source code. */ -namespace Ibexa\AdminUi\User\Mapper; +namespace Ibexa\AdminUi\Permission\Mapper; +use Ibexa\AdminUi\Values\PermissionCheckContext; use Ibexa\Contracts\Core\Repository\PermissionResolver; use Ibexa\Contracts\Core\Repository\UserService; -use Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo; use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult; use Ibexa\Contracts\Core\Repository\Values\User\User; use Ibexa\Core\Repository\Values\User\UserReference; /** * @phpstan-type TUserData array{ - * access: array, - * no_access: array, - * } + * id: int, + * name: string, + * email: string, + * } + * @phpstan-type TPermissionInfoData array{ + * access: array, + * no_access: array, + * } */ -final class UsersWithPermissionInfoToContentItemMapper +final class UsersWithPermissionInfoMapper { private PermissionResolver $permissionResolver; - private UserService $userService; + private UserService $userService; public function __construct( UserService $userService, @@ -41,9 +40,7 @@ public function __construct( } /** - * @param array<\Ibexa\Contracts\Core\Repository\Values\ValueObject> $targets - * - * @phpstan-return TUserData + * @phpstan-return TPermissionInfoData * * @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException @@ -51,14 +48,13 @@ public function __construct( */ public function mapSearchResults( SearchResult $searchResult, - ContentInfo $contentInfo, + PermissionCheckContext $permissionContext, string $module, - string $function, - array $targets = [] + string $function ): array { $currentUserReference = $this->permissionResolver->getCurrentUserReference(); - $results = $this->groupByPermissions($searchResult, $contentInfo, $module, $function, $targets); + $results = $this->groupByPermissions($searchResult, $permissionContext, $module, $function); $this->permissionResolver->setCurrentUserReference($currentUserReference); @@ -66,9 +62,7 @@ public function mapSearchResults( } /** - * @param array<\Ibexa\Contracts\Core\Repository\Values\ValueObject> $targets - * - * @phpstan-return TUserData + * @phpstan-return TPermissionInfoData * * @throws \Ibexa\Contracts\Core\Repository\Exceptions\BadStateException * @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException @@ -76,10 +70,9 @@ public function mapSearchResults( */ private function groupByPermissions( SearchResult $searchResult, - ContentInfo $contentInfo, + PermissionCheckContext $context, string $module, - string $function, - array $targets = [] + string $function ): array { $results = [ 'access' => [], @@ -96,7 +89,10 @@ private function groupByPermissions( $this->permissionResolver->setCurrentUserReference($userReference); - if ($this->permissionResolver->canUser($module, $function, $contentInfo, $targets)) { + $object = $context->getSubject(); + $targets = $context->getTargets(); + + if ($this->permissionResolver->canUser($module, $function, $object, $targets)) { $results['access'][] = $userData; } else { $results['no_access'][] = $userData; @@ -107,14 +103,12 @@ private function groupByPermissions( } /** - * @return array{ - * name: string, - * email: string, - * } + * @phpstan-return TUserData */ private function getUserData(User $user): array { return [ + 'id' => $user->getUserId(), 'name' => $user->getName() ?? $user->getLogin(), 'email' => $user->email, ]; From a948b739eeaff346f4bc9e5f4f654c9f550f307f Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 31 Oct 2024 09:49:59 +0100 Subject: [PATCH 15/26] [Tests] Reworked GetUsersWithPermissionInfoTest --- ...php => GetUsersWithPermissionInfoTest.php} | 63 +++++++++++++------ tests/integration/Resources/ibexa.yaml | 3 + 2 files changed, 48 insertions(+), 18 deletions(-) rename tests/integration/REST/{GetUsersWithPermissionInfoToContentItemTest.php => GetUsersWithPermissionInfoTest.php} (72%) diff --git a/tests/integration/REST/GetUsersWithPermissionInfoToContentItemTest.php b/tests/integration/REST/GetUsersWithPermissionInfoTest.php similarity index 72% rename from tests/integration/REST/GetUsersWithPermissionInfoToContentItemTest.php rename to tests/integration/REST/GetUsersWithPermissionInfoTest.php index d5af5c7b3c..546394c7f6 100644 --- a/tests/integration/REST/GetUsersWithPermissionInfoToContentItemTest.php +++ b/tests/integration/REST/GetUsersWithPermissionInfoTest.php @@ -12,10 +12,11 @@ use Ibexa\Contracts\Core\Repository\UserService; use Ibexa\Contracts\Core\Repository\Values\User\UserCreateStruct; use Ibexa\Contracts\Test\Rest\WebTestCase; +use LogicException; -final class GetUsersWithPermissionInfoToContentItemTest extends WebTestCase +final class GetUsersWithPermissionInfoTest extends WebTestCase { - private const ENDPOINT_URL = 'permission/users-with-permission-info/%d/%s/%s?%s'; + private const ENDPOINT_URL = 'permission/users-with-permission-info/%s/%s?%s'; private const HEADERS = [ 'HTTP_ACCEPT' => 'application/json', 'X-Siteaccess' => 'admin', @@ -55,13 +56,20 @@ public function testGetUsersWithPermissionsEndpoint( array $queryParameters, string $expectedResponse ): void { - $uri = $this->getUri($contentId, $module, $function, $queryParameters); + $uri = $this->getUri($module, $function, $queryParameters); $this->client->request('GET', $uri, [], [], self::HEADERS); $response = $this->client->getResponse(); self::assertSame(200, $response->getStatusCode()); - self::assertSame($expectedResponse, $response->getContent()); + + $content = $response->getContent(); + if (false === $content) { + throw new LogicException('Missing response content'); + } + + $fixedResponse = $this->doReplaceResponse($content); + self::assertSame($expectedResponse, $fixedResponse); } /** @@ -79,31 +87,40 @@ public function provideDataForTestGetUsersWithPermissionsEndpoint(): iterable self::MEDIA_CONTENT_ITEM_ID, self::MODULE_CONTENT, self::FUNCTION_READ, - [], - '{"access":[{"name":"Administrator User","email":"admin@link.invalid"},{"name":"John Doe","email":"john@link.invalid"},{"name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', + ['contentId' => 41], + '{"access":[{"id":"__FIXED_ID__","name":"John Doe","email":"john@link.invalid"},{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[{"id":"__FIXED_ID__","name":"Guest Guest","email":"guest@link.invalid"}]}', ]; yield 'Check content-read for content item 41 and location 51' => [ self::MEDIA_CONTENT_ITEM_ID, self::MODULE_CONTENT, self::FUNCTION_READ, - ['locationId' => 51], - '{"access":[{"name":"Administrator User","email":"admin@link.invalid"},{"name":"John Doe","email":"john@link.invalid"},{"name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', + [ + 'contentId' => 41, + 'locationId' => 51, + ], + '{"access":[{"id":"__FIXED_ID__","name":"John Doe","email":"john@link.invalid"},{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[{"id":"__FIXED_ID__","name":"Guest Guest","email":"guest@link.invalid"}]}', ]; yield 'Check content-read for content item 41 and phrase=adm' => [ self::MEDIA_CONTENT_ITEM_ID, self::MODULE_CONTENT, self::FUNCTION_READ, - ['phrase' => 'Adm*'], - '{"access":[{"name":"Administrator User","email":"admin@link.invalid"}],"no_access":[]}', + [ + 'contentId' => 41, + 'phrase' => 'Adm*', + ], + '{"access":[],"no_access":[]}', ]; yield 'Check content-read for phrase=undef*' => [ self::MEDIA_CONTENT_ITEM_ID, self::MODULE_CONTENT, self::FUNCTION_READ, - ['phrase' => 'undef*'], + [ + 'contentId' => 41, + 'phrase' => 'undef*', + ], '{"access":[],"no_access":[]}', ]; @@ -112,9 +129,10 @@ public function provideDataForTestGetUsersWithPermissionsEndpoint(): iterable self::MODULE_CONTENT, self::FUNCTION_EDIT, [ + 'contentId' => 41, 'phrase' => 'jo*', ], - '{"access":[{"name":"John Doe","email":"john@link.invalid"},{"name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', + '{"access":[{"id":"__FIXED_ID__","name":"John Doe","email":"john@link.invalid"},{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', ]; yield 'Check content-edit for content item 41 and phrase=bar*' => [ @@ -122,9 +140,10 @@ public function provideDataForTestGetUsersWithPermissionsEndpoint(): iterable self::MODULE_CONTENT, self::FUNCTION_EDIT, [ + 'contentId' => 41, 'phrase' => 'bar*', ], - '{"access":[{"name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', + '{"access":[{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', ]; yield 'Check content-edit for content item 41 and location 43 and phrase=joshua@link.invalid' => [ @@ -132,10 +151,11 @@ public function provideDataForTestGetUsersWithPermissionsEndpoint(): iterable self::MODULE_CONTENT, self::FUNCTION_EDIT, [ - 'phrase' => 'joshua*', + 'phrase' => 'joshua@link.invalid', + 'contentId' => 41, 'locationId' => self::MEDIA_LOCATION_ID, ], - '{"access":[{"name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', + '{"access":[{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', ]; } @@ -147,7 +167,6 @@ private function createUsers(): void $editorGroup = $this->userService->loadUserGroupByRemoteId(self::EDITOR_USER_GROUP_REMOTE_ID); $user1CreateStruct = $this->createUserCreateStruct('john', 'John', 'Doe', 'john@link.invalid'); - $user1 = $this->userService->createUser($user1CreateStruct, [$editorGroup]); $this->roleService->assignRoleToUser($role, $user1); @@ -187,17 +206,25 @@ private function createUserCreateStruct( * @param array $queryParameters */ private static function getUri( - int $contentId, string $module, string $function, array $queryParameters = [] ): string { return sprintf( self::ENDPOINT_URL, - $contentId, $module, $function, http_build_query($queryParameters), ); } + + private function doReplaceResponse(string $jsonResponse): string + { + $fixedResponse = preg_replace('~"id":\d+~', '"id":"__FIXED_ID__"', $jsonResponse); + if (null === $fixedResponse) { + throw new LogicException('Failed to replace JSON response.'); + } + + return $fixedResponse; + } } diff --git a/tests/integration/Resources/ibexa.yaml b/tests/integration/Resources/ibexa.yaml index 08f5404385..35da846656 100644 --- a/tests/integration/Resources/ibexa.yaml +++ b/tests/integration/Resources/ibexa.yaml @@ -1,3 +1,6 @@ +parameters: + ibexa.admin_ui.permission_check_context.content.user_content_type_identifiers: [user] + ibexa: system: default: From 47a2af3ec047f54c933ec8be1050cd42d80d57fb Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 31 Oct 2024 09:50:26 +0100 Subject: [PATCH 16/26] [DI] Added services definitions --- src/bundle/Resources/config/routing.yaml | 4 ++-- src/bundle/Resources/config/services.yaml | 3 ++- .../Resources/config/services/controllers.yaml | 2 +- .../Resources/config/services/permissions.yaml | 17 +++++++++++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/bundle/Resources/config/routing.yaml b/src/bundle/Resources/config/routing.yaml index 370a0c9036..415b1c2372 100644 --- a/src/bundle/Resources/config/routing.yaml +++ b/src/bundle/Resources/config/routing.yaml @@ -976,8 +976,8 @@ ibexa.focus_mode.change: # Users # ibexa.permission.users_with_permission_info: - path: /permission/users-with-permission-info/{contentInfoId}/{module}/{function} - controller: 'Ibexa\Bundle\AdminUi\Controller\Permission\UsersWithPermissionInfoToContentItemController::listAction' + path: /permission/users-with-permission-info/{module}/{function} + controller: 'Ibexa\Bundle\AdminUi\Controller\Permission\UsersWithPermissionInfoController::listAction' methods: [GET] requirements: module: \w+ diff --git a/src/bundle/Resources/config/services.yaml b/src/bundle/Resources/config/services.yaml index 55910c5bd6..f90cccc335 100644 --- a/src/bundle/Resources/config/services.yaml +++ b/src/bundle/Resources/config/services.yaml @@ -42,6 +42,7 @@ imports: parameters: ibexa.admin_ui.load_users_with_permission_info.limit: 10 + ibexa.admin_ui.permission_check_context.content.user_content_type_identifiers: [editor] services: _defaults: @@ -166,4 +167,4 @@ services: tags: - {name: kernel.event_subscriber} - Ibexa\AdminUi\User\Mapper\UsersWithPermissionInfoToContentItemMapper: ~ + Ibexa\AdminUi\Permission\Mapper\UsersWithPermissionInfoMapper: ~ diff --git a/src/bundle/Resources/config/services/controllers.yaml b/src/bundle/Resources/config/services/controllers.yaml index 9055884ee0..e6384a6886 100644 --- a/src/bundle/Resources/config/services/controllers.yaml +++ b/src/bundle/Resources/config/services/controllers.yaml @@ -253,7 +253,7 @@ services: tags: - controller.service_arguments - Ibexa\Bundle\AdminUi\Controller\Permission\UsersWithPermissionInfoToContentItemController: + Ibexa\Bundle\AdminUi\Controller\Permission\UsersWithPermissionInfoController: parent: Ibexa\Rest\Server\Controller arguments: $userQueryType: '@Ibexa\AdminUi\QueryType\UserQueryType' diff --git a/src/bundle/Resources/config/services/permissions.yaml b/src/bundle/Resources/config/services/permissions.yaml index c152f57d37..50dbd4c7c1 100644 --- a/src/bundle/Resources/config/services/permissions.yaml +++ b/src/bundle/Resources/config/services/permissions.yaml @@ -15,3 +15,20 @@ services: Ibexa\AdminUi\Permission\LimitationResolverInterface: alias: Ibexa\AdminUi\Permission\LimitationResolver + + Ibexa\AdminUi\Permission\PermissionCheckContextResolver: + arguments: + $permissionContextProviders: !tagged_iterator ibexa.admin_ui.permission_check_context.provider + + Ibexa\AdminUi\Permission\PermissionCheckContextResolverInterface: + alias: Ibexa\AdminUi\Permission\PermissionCheckContextResolver + + Ibexa\Contracts\AdminUi\Permission\PermissionCheckContextProviderInterface: ~ + + Ibexa\AdminUi\Permission\ContextProvider\ContentItemContextProvider: + arguments: + $userContentTypeIdentifiers: '%ibexa.admin_ui.permission_check_context.content.user_content_type_identifiers%' + tags: + - + name: ibexa.admin_ui.permission_check_context.provider + priority: -100 From 0abf8b82875a6fc245696235452fdd69e2fefb13 Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 31 Oct 2024 09:53:28 +0100 Subject: [PATCH 17/26] Added strict types declaration --- .../Permission/PermissionCheckContextProviderInterface.php | 1 + src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php | 1 + src/lib/Permission/PermissionCheckContextResolver.php | 1 + src/lib/Permission/PermissionCheckContextResolverInterface.php | 1 + 4 files changed, 4 insertions(+) diff --git a/src/contracts/Permission/PermissionCheckContextProviderInterface.php b/src/contracts/Permission/PermissionCheckContextProviderInterface.php index 443901a9ab..04c1f8bc1e 100644 --- a/src/contracts/Permission/PermissionCheckContextProviderInterface.php +++ b/src/contracts/Permission/PermissionCheckContextProviderInterface.php @@ -4,6 +4,7 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace Ibexa\Contracts\AdminUi\Permission; diff --git a/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php b/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php index 77449b2017..e707c671a9 100644 --- a/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php +++ b/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php @@ -4,6 +4,7 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace Ibexa\AdminUi\Permission\Mapper; diff --git a/src/lib/Permission/PermissionCheckContextResolver.php b/src/lib/Permission/PermissionCheckContextResolver.php index 9e15c24302..93f903e5e3 100644 --- a/src/lib/Permission/PermissionCheckContextResolver.php +++ b/src/lib/Permission/PermissionCheckContextResolver.php @@ -4,6 +4,7 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace Ibexa\AdminUi\Permission; diff --git a/src/lib/Permission/PermissionCheckContextResolverInterface.php b/src/lib/Permission/PermissionCheckContextResolverInterface.php index 6cbd11fc9c..5cdedc9242 100644 --- a/src/lib/Permission/PermissionCheckContextResolverInterface.php +++ b/src/lib/Permission/PermissionCheckContextResolverInterface.php @@ -4,6 +4,7 @@ * @copyright Copyright (C) Ibexa AS. All rights reserved. * @license For full copyright and license information view LICENSE file distributed with this source code. */ +declare(strict_types=1); namespace Ibexa\AdminUi\Permission; From 59d0982c46dfed951f98d45ad15ff2d08d1dfa13 Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 31 Oct 2024 10:11:25 +0100 Subject: [PATCH 18/26] Moved PermissionCheckContext to contracts --- .../Permission/PermissionCheckContextProviderInterface.php | 2 +- src/{lib => contracts}/Values/PermissionCheckContext.php | 2 +- .../Permission/ContextProvider/ContentItemContextProvider.php | 2 +- src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php | 2 +- src/lib/Permission/PermissionCheckContextResolver.php | 2 +- src/lib/Permission/PermissionCheckContextResolverInterface.php | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename src/{lib => contracts}/Values/PermissionCheckContext.php (96%) diff --git a/src/contracts/Permission/PermissionCheckContextProviderInterface.php b/src/contracts/Permission/PermissionCheckContextProviderInterface.php index 04c1f8bc1e..642b01997e 100644 --- a/src/contracts/Permission/PermissionCheckContextProviderInterface.php +++ b/src/contracts/Permission/PermissionCheckContextProviderInterface.php @@ -8,7 +8,7 @@ namespace Ibexa\Contracts\AdminUi\Permission; -use Ibexa\AdminUi\Values\PermissionCheckContext; +use Ibexa\Contracts\AdminUi\Values\PermissionCheckContext; use Symfony\Component\HttpFoundation\Request; interface PermissionCheckContextProviderInterface diff --git a/src/lib/Values/PermissionCheckContext.php b/src/contracts/Values/PermissionCheckContext.php similarity index 96% rename from src/lib/Values/PermissionCheckContext.php rename to src/contracts/Values/PermissionCheckContext.php index 5fadbe7b48..3e88ba3c4d 100644 --- a/src/lib/Values/PermissionCheckContext.php +++ b/src/contracts/Values/PermissionCheckContext.php @@ -6,7 +6,7 @@ */ declare(strict_types=1); -namespace Ibexa\AdminUi\Values; +namespace Ibexa\Contracts\AdminUi\Values; use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion; use Ibexa\Contracts\Core\Repository\Values\ValueObject; diff --git a/src/lib/Permission/ContextProvider/ContentItemContextProvider.php b/src/lib/Permission/ContextProvider/ContentItemContextProvider.php index 4e1e018cf5..ba3f20853b 100644 --- a/src/lib/Permission/ContextProvider/ContentItemContextProvider.php +++ b/src/lib/Permission/ContextProvider/ContentItemContextProvider.php @@ -8,8 +8,8 @@ namespace Ibexa\AdminUi\Permission\ContextProvider; -use Ibexa\AdminUi\Values\PermissionCheckContext; use Ibexa\Contracts\AdminUi\Permission\PermissionCheckContextProviderInterface; +use Ibexa\Contracts\AdminUi\Values\PermissionCheckContext; use Ibexa\Contracts\Core\Exception\InvalidArgumentException; use Ibexa\Contracts\Core\Repository\ContentService; use Ibexa\Contracts\Core\Repository\LocationService; diff --git a/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php b/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php index e707c671a9..a4163e800c 100644 --- a/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php +++ b/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php @@ -8,7 +8,7 @@ namespace Ibexa\AdminUi\Permission\Mapper; -use Ibexa\AdminUi\Values\PermissionCheckContext; +use Ibexa\Contracts\AdminUi\Values\PermissionCheckContext; use Ibexa\Contracts\Core\Repository\PermissionResolver; use Ibexa\Contracts\Core\Repository\UserService; use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult; diff --git a/src/lib/Permission/PermissionCheckContextResolver.php b/src/lib/Permission/PermissionCheckContextResolver.php index 93f903e5e3..8d924b7835 100644 --- a/src/lib/Permission/PermissionCheckContextResolver.php +++ b/src/lib/Permission/PermissionCheckContextResolver.php @@ -8,7 +8,7 @@ namespace Ibexa\AdminUi\Permission; -use Ibexa\AdminUi\Values\PermissionCheckContext; +use Ibexa\Contracts\AdminUi\Values\PermissionCheckContext; use Ibexa\Contracts\Core\Exception\InvalidArgumentException; use Symfony\Component\HttpFoundation\Request; diff --git a/src/lib/Permission/PermissionCheckContextResolverInterface.php b/src/lib/Permission/PermissionCheckContextResolverInterface.php index 5cdedc9242..5df9ed9c1d 100644 --- a/src/lib/Permission/PermissionCheckContextResolverInterface.php +++ b/src/lib/Permission/PermissionCheckContextResolverInterface.php @@ -8,7 +8,7 @@ namespace Ibexa\AdminUi\Permission; -use Ibexa\AdminUi\Values\PermissionCheckContext; +use Ibexa\Contracts\AdminUi\Values\PermissionCheckContext; use Symfony\Component\HttpFoundation\Request; interface PermissionCheckContextResolverInterface From 6b9bb599dcdfcd260a8d2a204b1a75b0b9f31ebe Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Thu, 31 Oct 2024 13:30:28 +0100 Subject: [PATCH 19/26] Reoreder variables in UsersWithPermissionInforMapper::groupByPermissions --- src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php b/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php index a4163e800c..c80fb27387 100644 --- a/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php +++ b/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php @@ -83,15 +83,14 @@ private function groupByPermissions( foreach ($searchResult as $result) { /** @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo $userContentInfo */ $userContentInfo = $result->valueObject; - $user = $this->userService->loadUser($userContentInfo->getId()); - $userReference = new UserReference($user->getUserId()); - $userData = $this->getUserData($user); + $userReference = new UserReference($user->getUserId()); $this->permissionResolver->setCurrentUserReference($userReference); $object = $context->getSubject(); $targets = $context->getTargets(); + $userData = $this->getUserData($user); if ($this->permissionResolver->canUser($module, $function, $object, $targets)) { $results['access'][] = $userData; From 997ccf90c37182000c961823bd6756c262d23aa8 Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Wed, 27 Nov 2024 08:28:59 +0100 Subject: [PATCH 20/26] Update src/lib/QueryType/UserQueryType.php Co-authored-by: Konrad Oboza --- src/lib/QueryType/UserQueryType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/QueryType/UserQueryType.php b/src/lib/QueryType/UserQueryType.php index e3d2fa3d89..67ad518862 100644 --- a/src/lib/QueryType/UserQueryType.php +++ b/src/lib/QueryType/UserQueryType.php @@ -85,7 +85,7 @@ private function cleanSearchPhrase(string $phrase): string { $sanitizedPhrase = preg_replace('/[^a-zA-Z0-9@._-]/', '', $phrase); if (null === $sanitizedPhrase) { - throw new RuntimeException('Could not to sanitize search phrase.'); + throw new RuntimeException('Could not sanitize search phrase.'); } return $sanitizedPhrase; From 6b198829a7e6ed6db3716c54845968b6ddcb92b7 Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Wed, 27 Nov 2024 11:30:02 +0100 Subject: [PATCH 21/26] Dropped UserQueryType::excludeSystemUsers method --- src/lib/QueryType/UserQueryType.php | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/lib/QueryType/UserQueryType.php b/src/lib/QueryType/UserQueryType.php index 67ad518862..ea7186bac7 100644 --- a/src/lib/QueryType/UserQueryType.php +++ b/src/lib/QueryType/UserQueryType.php @@ -16,8 +16,6 @@ final class UserQueryType extends AbstractQueryType { - private const USER_ADMIN_ID = 14; - protected function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); @@ -55,7 +53,6 @@ protected function getQueryFilter(array $parameters): Criterion { $criteria = [ new Criterion\IsUserEnabled(), - $this->excludeSystemUsers(), ]; if (!empty($parameters['extra_criteria'])) { @@ -90,21 +87,4 @@ private function cleanSearchPhrase(string $phrase): string return $sanitizedPhrase; } - - private function excludeSystemUsers(): Criterion - { - return new Criterion\LogicalNot( - new Criterion\ContentId( - [ - self::USER_ADMIN_ID, - $this->getAnonymousUserId(), - ] - ), - ); - } - - private function getAnonymousUserId(): int - { - return $this->configResolver->getParameter('anonymous_user_id'); - } } From 7e0576e5d67496cc416aebc00f485f3adb3e93ad Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Wed, 27 Nov 2024 12:36:30 +0100 Subject: [PATCH 22/26] [Tests] Fixed test --- .../REST/GetUsersWithPermissionInfoTest.php | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/tests/integration/REST/GetUsersWithPermissionInfoTest.php b/tests/integration/REST/GetUsersWithPermissionInfoTest.php index 546394c7f6..cf535a0409 100644 --- a/tests/integration/REST/GetUsersWithPermissionInfoTest.php +++ b/tests/integration/REST/GetUsersWithPermissionInfoTest.php @@ -69,6 +69,7 @@ public function testGetUsersWithPermissionsEndpoint( } $fixedResponse = $this->doReplaceResponse($content); + self::assertSame($expectedResponse, $fixedResponse); } @@ -88,7 +89,7 @@ public function provideDataForTestGetUsersWithPermissionsEndpoint(): iterable self::MODULE_CONTENT, self::FUNCTION_READ, ['contentId' => 41], - '{"access":[{"id":"__FIXED_ID__","name":"John Doe","email":"john@link.invalid"},{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[{"id":"__FIXED_ID__","name":"Guest Guest","email":"guest@link.invalid"}]}', + '{"access":[{"id":"__FIXED_ID__","name":"Administrator User","email":"admin@link.invalid"},{"id":"__FIXED_ID__","name":"John Doe","email":"john@link.invalid"},{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[{"id":"__FIXED_ID__","name":"Anonymous User","email":"anonymous@link.invalid"},{"id":"__FIXED_ID__","name":"Guest Guest","email":"guest@link.invalid"}]}', ]; yield 'Check content-read for content item 41 and location 51' => [ @@ -99,18 +100,7 @@ public function provideDataForTestGetUsersWithPermissionsEndpoint(): iterable 'contentId' => 41, 'locationId' => 51, ], - '{"access":[{"id":"__FIXED_ID__","name":"John Doe","email":"john@link.invalid"},{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[{"id":"__FIXED_ID__","name":"Guest Guest","email":"guest@link.invalid"}]}', - ]; - - yield 'Check content-read for content item 41 and phrase=adm' => [ - self::MEDIA_CONTENT_ITEM_ID, - self::MODULE_CONTENT, - self::FUNCTION_READ, - [ - 'contentId' => 41, - 'phrase' => 'Adm*', - ], - '{"access":[],"no_access":[]}', + '{"access":[{"id":"__FIXED_ID__","name":"Administrator User","email":"admin@link.invalid"},{"id":"__FIXED_ID__","name":"John Doe","email":"john@link.invalid"},{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[{"id":"__FIXED_ID__","name":"Anonymous User","email":"anonymous@link.invalid"},{"id":"__FIXED_ID__","name":"Guest Guest","email":"guest@link.invalid"}]}', ]; yield 'Check content-read for phrase=undef*' => [ From bb77f851df1e60e8b01bde56592743cf6c878853 Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Mon, 9 Dec 2024 10:10:41 +0100 Subject: [PATCH 23/26] [Routing] Set option expose --- src/bundle/Resources/config/routing.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bundle/Resources/config/routing.yaml b/src/bundle/Resources/config/routing.yaml index 415b1c2372..18263e88b9 100644 --- a/src/bundle/Resources/config/routing.yaml +++ b/src/bundle/Resources/config/routing.yaml @@ -979,6 +979,8 @@ ibexa.permission.users_with_permission_info: path: /permission/users-with-permission-info/{module}/{function} controller: 'Ibexa\Bundle\AdminUi\Controller\Permission\UsersWithPermissionInfoController::listAction' methods: [GET] + options: + expose: true requirements: module: \w+ function: \w+ From 6eec554547dfb66764b69cb0c6b84f1676479596 Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Mon, 9 Dec 2024 10:11:42 +0100 Subject: [PATCH 24/26] Moved user search phrase pattern to const --- src/lib/QueryType/UserQueryType.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/QueryType/UserQueryType.php b/src/lib/QueryType/UserQueryType.php index ea7186bac7..240cb90793 100644 --- a/src/lib/QueryType/UserQueryType.php +++ b/src/lib/QueryType/UserQueryType.php @@ -16,6 +16,8 @@ final class UserQueryType extends AbstractQueryType { + private const USER_SEARCH_PHRASE_PATTERN = '/[^a-zA-Z0-9@._-]/'; + protected function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); @@ -80,7 +82,7 @@ public static function getName(): string private function cleanSearchPhrase(string $phrase): string { - $sanitizedPhrase = preg_replace('/[^a-zA-Z0-9@._-]/', '', $phrase); + $sanitizedPhrase = preg_replace(self::USER_SEARCH_PHRASE_PATTERN, '', $phrase); if (null === $sanitizedPhrase) { throw new RuntimeException('Could not sanitize search phrase.'); } From 5675b8b17ccab20ab030cf90935878368900fd3c Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Mon, 9 Dec 2024 10:13:46 +0100 Subject: [PATCH 25/26] Fixed UsersWithPermissionInfoMapper --- .../Mapper/UsersWithPermissionInfoMapper.php | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php b/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php index c80fb27387..0a117ce4a8 100644 --- a/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php +++ b/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php @@ -10,6 +10,7 @@ use Ibexa\Contracts\AdminUi\Values\PermissionCheckContext; use Ibexa\Contracts\Core\Repository\PermissionResolver; +use Ibexa\Contracts\Core\Repository\Repository; use Ibexa\Contracts\Core\Repository\UserService; use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult; use Ibexa\Contracts\Core\Repository\Values\User\User; @@ -30,14 +31,18 @@ final class UsersWithPermissionInfoMapper { private PermissionResolver $permissionResolver; + private Repository $repository; + private UserService $userService; public function __construct( - UserService $userService, - PermissionResolver $permissionResolver + PermissionResolver $permissionResolver, + Repository $repository, + UserService $userService ) { - $this->userService = $userService; $this->permissionResolver = $permissionResolver; + $this->repository = $repository; + $this->userService = $userService; } /** @@ -55,11 +60,11 @@ public function mapSearchResults( ): array { $currentUserReference = $this->permissionResolver->getCurrentUserReference(); - $results = $this->groupByPermissions($searchResult, $permissionContext, $module, $function); - - $this->permissionResolver->setCurrentUserReference($currentUserReference); - - return $results; + try { + return $this->groupByPermissions($searchResult, $permissionContext, $module, $function); + } finally { + $this->permissionResolver->setCurrentUserReference($currentUserReference); + } } /** @@ -83,8 +88,7 @@ private function groupByPermissions( foreach ($searchResult as $result) { /** @var \Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo $userContentInfo */ $userContentInfo = $result->valueObject; - $user = $this->userService->loadUser($userContentInfo->getId()); - + $user = $this->loadUser($userContentInfo->getId()); $userReference = new UserReference($user->getUserId()); $this->permissionResolver->setCurrentUserReference($userReference); @@ -102,6 +106,16 @@ private function groupByPermissions( return $results; } + /** + * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException + */ + private function loadUser(int $userId): User + { + return $this->repository->sudo( + fn (): User => $this->userService->loadUser($userId) + ); + } + /** * @phpstan-return TUserData */ From c4fc802ca5250ef2c3af156f2287c0c27bd5c548 Mon Sep 17 00:00:00 2001 From: Tomasz Kryszan Date: Mon, 9 Dec 2024 12:03:30 +0100 Subject: [PATCH 26/26] Renamed no_access key to noAccess --- .../Mapper/UsersWithPermissionInfoMapper.php | 6 +++--- .../REST/GetUsersWithPermissionInfoTest.php | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php b/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php index 0a117ce4a8..0ed5fef0eb 100644 --- a/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php +++ b/src/lib/Permission/Mapper/UsersWithPermissionInfoMapper.php @@ -24,7 +24,7 @@ * } * @phpstan-type TPermissionInfoData array{ * access: array, - * no_access: array, + * noAccess: array, * } */ final class UsersWithPermissionInfoMapper @@ -82,7 +82,7 @@ private function groupByPermissions( ): array { $results = [ 'access' => [], - 'no_access' => [], + 'noAccess' => [], ]; foreach ($searchResult as $result) { @@ -99,7 +99,7 @@ private function groupByPermissions( if ($this->permissionResolver->canUser($module, $function, $object, $targets)) { $results['access'][] = $userData; } else { - $results['no_access'][] = $userData; + $results['noAccess'][] = $userData; } } diff --git a/tests/integration/REST/GetUsersWithPermissionInfoTest.php b/tests/integration/REST/GetUsersWithPermissionInfoTest.php index cf535a0409..38a469f668 100644 --- a/tests/integration/REST/GetUsersWithPermissionInfoTest.php +++ b/tests/integration/REST/GetUsersWithPermissionInfoTest.php @@ -89,7 +89,7 @@ public function provideDataForTestGetUsersWithPermissionsEndpoint(): iterable self::MODULE_CONTENT, self::FUNCTION_READ, ['contentId' => 41], - '{"access":[{"id":"__FIXED_ID__","name":"Administrator User","email":"admin@link.invalid"},{"id":"__FIXED_ID__","name":"John Doe","email":"john@link.invalid"},{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[{"id":"__FIXED_ID__","name":"Anonymous User","email":"anonymous@link.invalid"},{"id":"__FIXED_ID__","name":"Guest Guest","email":"guest@link.invalid"}]}', + '{"access":[{"id":"__FIXED_ID__","name":"Administrator User","email":"admin@link.invalid"},{"id":"__FIXED_ID__","name":"John Doe","email":"john@link.invalid"},{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"noAccess":[{"id":"__FIXED_ID__","name":"Anonymous User","email":"anonymous@link.invalid"},{"id":"__FIXED_ID__","name":"Guest Guest","email":"guest@link.invalid"}]}', ]; yield 'Check content-read for content item 41 and location 51' => [ @@ -100,7 +100,7 @@ public function provideDataForTestGetUsersWithPermissionsEndpoint(): iterable 'contentId' => 41, 'locationId' => 51, ], - '{"access":[{"id":"__FIXED_ID__","name":"Administrator User","email":"admin@link.invalid"},{"id":"__FIXED_ID__","name":"John Doe","email":"john@link.invalid"},{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[{"id":"__FIXED_ID__","name":"Anonymous User","email":"anonymous@link.invalid"},{"id":"__FIXED_ID__","name":"Guest Guest","email":"guest@link.invalid"}]}', + '{"access":[{"id":"__FIXED_ID__","name":"Administrator User","email":"admin@link.invalid"},{"id":"__FIXED_ID__","name":"John Doe","email":"john@link.invalid"},{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"noAccess":[{"id":"__FIXED_ID__","name":"Anonymous User","email":"anonymous@link.invalid"},{"id":"__FIXED_ID__","name":"Guest Guest","email":"guest@link.invalid"}]}', ]; yield 'Check content-read for phrase=undef*' => [ @@ -111,7 +111,7 @@ public function provideDataForTestGetUsersWithPermissionsEndpoint(): iterable 'contentId' => 41, 'phrase' => 'undef*', ], - '{"access":[],"no_access":[]}', + '{"access":[],"noAccess":[]}', ]; yield 'Check content-edit for content item 2 and phrase=jo' => [ @@ -122,7 +122,7 @@ public function provideDataForTestGetUsersWithPermissionsEndpoint(): iterable 'contentId' => 41, 'phrase' => 'jo*', ], - '{"access":[{"id":"__FIXED_ID__","name":"John Doe","email":"john@link.invalid"},{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', + '{"access":[{"id":"__FIXED_ID__","name":"John Doe","email":"john@link.invalid"},{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"noAccess":[]}', ]; yield 'Check content-edit for content item 41 and phrase=bar*' => [ @@ -133,7 +133,7 @@ public function provideDataForTestGetUsersWithPermissionsEndpoint(): iterable 'contentId' => 41, 'phrase' => 'bar*', ], - '{"access":[{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', + '{"access":[{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"noAccess":[]}', ]; yield 'Check content-edit for content item 41 and location 43 and phrase=joshua@link.invalid' => [ @@ -145,7 +145,7 @@ public function provideDataForTestGetUsersWithPermissionsEndpoint(): iterable 'contentId' => 41, 'locationId' => self::MEDIA_LOCATION_ID, ], - '{"access":[{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"no_access":[]}', + '{"access":[{"id":"__FIXED_ID__","name":"Josh Bar","email":"joshua@link.invalid"}],"noAccess":[]}', ]; }