diff --git a/eZ/Publish/Core/settings/policies.yml b/eZ/Publish/Core/settings/policies.yml
index c0862d48982..c1140988d43 100644
--- a/eZ/Publish/Core/settings/policies.yml
+++ b/eZ/Publish/Core/settings/policies.yml
@@ -30,7 +30,7 @@ parameters:
administrate: ~
role:
- assign: ~
+ assign: { MemberOf: true, Role: true }
update: ~
create: ~
delete: ~
diff --git a/eZ/Publish/Core/settings/roles.yml b/eZ/Publish/Core/settings/roles.yml
index 6cb58149303..9533763c90f 100644
--- a/eZ/Publish/Core/settings/roles.yml
+++ b/eZ/Publish/Core/settings/roles.yml
@@ -146,3 +146,15 @@ services:
class: "%ezpublish.api.role.limitation_type.blocking.class%"
arguments: ['AntiSpam']
tags: [{name: ezpublish.limitationType, alias: AntiSpam}]
+
+ Ibexa\Core\Limitation\MemberOfLimitationType:
+ arguments:
+ $persistence: '@ezpublish.api.persistence_handler'
+ tags:
+ - { name: ezpublish.limitationType, alias: MemberOf }
+
+ Ibexa\Core\Limitation\RoleLimitationType:
+ arguments:
+ $persistence: '@ezpublish.api.persistence_handler'
+ tags:
+ - { name: ezpublish.limitationType, alias: Role }
diff --git a/phpunit-integration-legacy.xml b/phpunit-integration-legacy.xml
index c42c1e661bd..ace45238b11 100644
--- a/phpunit-integration-legacy.xml
+++ b/phpunit-integration-legacy.xml
@@ -73,6 +73,9 @@
eZ/Publish/SPI/Tests/Search/Content/IndexerGatewayTest.php
+
+ tests/integration
+
diff --git a/phpunit.xml b/phpunit.xml
index 082bbb28172..fe30983fe49 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -73,6 +73,9 @@
eZ/Publish/API/Repository/Tests/Values/Content
+
+ tests/lib
+
diff --git a/src/contracts/Repository/Values/User/Limitation/MemberOfLimitation.php b/src/contracts/Repository/Values/User/Limitation/MemberOfLimitation.php
new file mode 100644
index 00000000000..960cf57ceaa
--- /dev/null
+++ b/src/contracts/Repository/Values/User/Limitation/MemberOfLimitation.php
@@ -0,0 +1,21 @@
+limitationValues)) {
+ throw new InvalidArgumentType(
+ '$limitationValue->limitationValues',
+ 'array',
+ $limitationValue->limitationValues
+ );
+ }
+
+ foreach ($limitationValue->limitationValues as $key => $id) {
+ if (!is_int($id)) {
+ throw new InvalidArgumentType("\$limitationValue->limitationValues[{$key}]", 'int|string', $id);
+ }
+ }
+ }
+
+ public function validate(APILimitationValue $limitationValue)
+ {
+ $validationErrors = [];
+
+ foreach ($limitationValue->limitationValues as $key => $id) {
+ if ($id === self::SELF_USER_GROUP) {
+ continue;
+ }
+ try {
+ $this->persistence->contentHandler()->loadContentInfo($id);
+ } catch (NotFoundException $e) {
+ $validationErrors[] = new ValidationError(
+ "limitationValues[%key%] => '%value%' does not exist in the backend",
+ null,
+ [
+ 'value' => $id,
+ 'key' => $key,
+ ]
+ );
+ }
+ }
+
+ return $validationErrors;
+ }
+
+ /**
+ * @param mixed[] $limitationValues
+ *
+ * @return \eZ\Publish\API\Repository\Values\User\Limitation
+ */
+ public function buildValue(array $limitationValues): APILimitationValue
+ {
+ return new MemberOfLimitation(['limitationValues' => $limitationValues]);
+ }
+
+ public function evaluate(APILimitationValue $value, APIUserReference $currentUser, ValueObject $object, array $targets = null)
+ {
+ if (!$value instanceof MemberOfLimitation) {
+ throw new InvalidArgumentException(
+ '$value',
+ sprintf('Must be of type: %s', MemberOfLimitation::class)
+ );
+ }
+
+ if (!$object instanceof User
+ && !$object instanceof UserGroup
+ && !$object instanceof UserRoleAssignment
+ && !$object instanceof UserGroupRoleAssignment
+ ) {
+ return self::ACCESS_ABSTAIN;
+ }
+
+ if ($object instanceof User) {
+ return $this->evaluateUser($value, $object, $currentUser);
+ }
+
+ if ($object instanceof UserGroup) {
+ return $this->evaluateUserGroup($value, $object, $currentUser);
+ }
+
+ if ($object instanceof UserRoleAssignment) {
+ return $this->evaluateUser($value, $object->getUser(), $currentUser);
+ }
+
+ if ($object instanceof UserGroupRoleAssignment) {
+ return $this->evaluateUserGroup($value, $object->getUserGroup(), $currentUser);
+ }
+
+ return self::ACCESS_DENIED;
+ }
+
+ public function getCriterion(APILimitationValue $value, APIUserReference $currentUser)
+ {
+ throw new NotImplementedException('Member of Limitation Criterion');
+ }
+
+ public function valueSchema()
+ {
+ throw new NotImplementedException(__METHOD__);
+ }
+
+ private function evaluateUser(MemberOfLimitation $value, User $object, APIUserReference $currentUser): bool
+ {
+ if (empty($value->limitationValues)) {
+ return self::ACCESS_DENIED;
+ }
+
+ $userLocations = $this->persistence->locationHandler()->loadLocationsByContent($object->getUserId());
+
+ $userGroups = [];
+ foreach ($userLocations as $userLocation) {
+ $userGroups[] = $this->persistence->locationHandler()->load($userLocation->parentId);
+ }
+ $userGroupsIdList = array_column($userGroups, 'contentId');
+ $limitationValuesUserGroupsIdList = $value->limitationValues;
+
+ if (in_array(self::SELF_USER_GROUP, $limitationValuesUserGroupsIdList)) {
+ $currentUserGroupsIdList = $this->getCurrentUserGroupsIdList($currentUser);
+
+ // Granted, if current user is in exactly those same groups
+ if (count(array_intersect($userGroupsIdList, $currentUserGroupsIdList)) === count($userGroupsIdList)) {
+ return self::ACCESS_GRANTED;
+ }
+
+ // Unset SELF value, for next check
+ $key = array_search(self::SELF_USER_GROUP, $limitationValuesUserGroupsIdList);
+ unset($limitationValuesUserGroupsIdList[$key]);
+ }
+
+ // Granted, if limitationValues matched user groups 1:1
+ if (!empty($limitationValuesUserGroupsIdList)
+ && empty(array_diff($userGroupsIdList, $limitationValuesUserGroupsIdList))
+ ) {
+ return self::ACCESS_GRANTED;
+ }
+
+ return self::ACCESS_DENIED;
+ }
+
+ private function evaluateUserGroup(MemberOfLimitation $value, UserGroup $userGroup, APIUserReference $currentUser): bool
+ {
+ $limitationValuesUserGroupsIdList = $value->limitationValues;
+ if (in_array(self::SELF_USER_GROUP, $limitationValuesUserGroupsIdList)) {
+ $limitationValuesUserGroupsIdList = $this->getCurrentUserGroupsIdList($currentUser);
+ }
+
+ return in_array($userGroup->id, $limitationValuesUserGroupsIdList);
+ }
+
+ private function getCurrentUserGroupsIdList(APIUserReference $currentUser): array
+ {
+ $currentUserLocations = $this->persistence->locationHandler()->loadLocationsByContent($currentUser->getUserId());
+ $currentUserGroups = [];
+ foreach ($currentUserLocations as $currentUserLocation) {
+ $currentUserGroups[] = $this->persistence->locationHandler()->load($currentUserLocation->parentId);
+ }
+
+ return array_column(
+ $currentUserGroups,
+ 'contentId'
+ );
+ }
+}
diff --git a/src/lib/Limitation/RoleLimitationType.php b/src/lib/Limitation/RoleLimitationType.php
new file mode 100644
index 00000000000..e33e1ea6d14
--- /dev/null
+++ b/src/lib/Limitation/RoleLimitationType.php
@@ -0,0 +1,143 @@
+limitationValues)) {
+ throw new InvalidArgumentType(
+ '$limitationValue->limitationValues',
+ 'array',
+ $limitationValue->limitationValues
+ );
+ }
+
+ foreach ($limitationValue->limitationValues as $key => $id) {
+ if (!is_int($id)) {
+ throw new InvalidArgumentType("\$limitationValue->limitationValues[{$key}]", 'int|string', $id);
+ }
+ }
+ }
+
+ public function validate(APILimitationValue $limitationValue)
+ {
+ $validationErrors = [];
+
+ foreach ($limitationValue->limitationValues as $key => $id) {
+ try {
+ $this->persistence->userHandler()->loadRole($id);
+ } catch (NotFoundException $e) {
+ $validationErrors[] = new ValidationError(
+ "limitationValues[%key%] => '%value%' does not exist in the backend",
+ null,
+ [
+ 'value' => $id,
+ 'key' => $key,
+ ]
+ );
+ }
+ }
+
+ return $validationErrors;
+ }
+
+ /**
+ * @param mixed[] $limitationValues
+ *
+ * @return \eZ\Publish\API\Repository\Values\User\Limitation
+ */
+ public function buildValue(array $limitationValues): APILimitationValue
+ {
+ return new RoleLimitation(['limitationValues' => $limitationValues]);
+ }
+
+ public function evaluate(APILimitationValue $value, APIUserReference $currentUser, ValueObject $object, array $targets = null)
+ {
+ if (!$value instanceof RoleLimitation) {
+ throw new InvalidArgumentException(
+ '$value',
+ sprintf('Must be of type: %s', RoleLimitation::class)
+ );
+ }
+
+ if (
+ !$object instanceof Role
+ && !$object instanceof UserRoleAssignment
+ && !$object instanceof UserGroupRoleAssignment
+ && ($targets === null && ($object instanceof User || $object instanceof UserGroup))
+ ) {
+ return self::ACCESS_ABSTAIN;
+ }
+
+ if ($targets !== null) {
+ foreach ($targets as $target) {
+ if ($target instanceof Role && !$this->evaluateRole($value, $target)) {
+ return self::ACCESS_DENIED;
+ }
+
+ return self::ACCESS_GRANTED;
+ }
+ }
+
+ if ($object instanceof Role) {
+ return $this->evaluateRole($value, $object);
+ }
+
+ if ($object instanceof UserRoleAssignment || $object instanceof UserGroupRoleAssignment) {
+ return $this->evaluateRole($value, $object->getRole());
+ }
+
+ return self::ACCESS_DENIED;
+ }
+
+ public function getCriterion(APILimitationValue $value, APIUserReference $currentUser)
+ {
+ throw new NotImplementedException('Role Limitation Criterion');
+ }
+
+ public function valueSchema()
+ {
+ throw new NotImplementedException(__METHOD__);
+ }
+
+ private function evaluateRole(RoleLimitation $value, Role $role): bool
+ {
+ return in_array($role->id, $value->limitationValues);
+ }
+}
diff --git a/tests/integration/Core/.gitkeep b/tests/integration/Core/.gitkeep
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/tests/integration/Core/Limitation/MemberOfLimitationTest.php b/tests/integration/Core/Limitation/MemberOfLimitationTest.php
new file mode 100644
index 00000000000..5c89166a2f9
--- /dev/null
+++ b/tests/integration/Core/Limitation/MemberOfLimitationTest.php
@@ -0,0 +1,74 @@
+limitationValues[] = self::ADMIN_GROUP_ID;
+
+ $allowInUsersLimitation = new MemberOfLimitation();
+ $allowInUsersLimitation->limitationValues[] = self::USERS_GROUP_ID;
+
+ $allowInSelfGroupLimitation = new MemberOfLimitation();
+ $allowInSelfGroupLimitation->limitationValues[] = MemberOfLimitationType::SELF_USER_GROUP;
+
+ return [
+ [[$allowInAdministratorsLimitation], false],
+ [[$allowInUsersLimitation], true],
+ [[$allowInSelfGroupLimitation], true],
+ ];
+ }
+
+ /**
+ * @dataProvider userPermissionLimitationProvider
+ */
+ public function testCanUserAssignRoleToUser(array $limitations, bool $expectedResult): void
+ {
+ $repository = $this->getRepository();
+ $roleService = $repository->getRoleService();
+ $userService = $repository->getUserService();
+
+ $adminRoleThatWillBeSet = $roleService->loadRoleByIdentifier('Administrator');
+ $this->loginAsEditorUserWithLimitations('role', 'assign', $limitations);
+
+ $this->assertCanUser(
+ $expectedResult,
+ 'role',
+ 'assign',
+ $limitations,
+ $userService->loadUser($this->permissionResolver->getCurrentUserReference()->getUserId()),
+ [$adminRoleThatWillBeSet]
+ );
+
+ $this->assertCanUser(
+ $expectedResult,
+ 'role',
+ 'assign',
+ $limitations,
+ $repository->sudo(
+ static function (Repository $repository) {
+ return $repository->getUserService()->loadUserGroup(self::USERS_GROUP_ID);
+ },
+ $repository
+ ),
+ [$adminRoleThatWillBeSet]
+ );
+ }
+}
diff --git a/tests/integration/Core/Limitation/RoleLimitationTest.php b/tests/integration/Core/Limitation/RoleLimitationTest.php
new file mode 100644
index 00000000000..729862db80e
--- /dev/null
+++ b/tests/integration/Core/Limitation/RoleLimitationTest.php
@@ -0,0 +1,69 @@
+getRepository()->getRoleService();
+ $allowEditorLimitation->limitationValues[] = $roleService->loadRoleByIdentifier('Editor')->id;
+
+ $allowAdministratorLimitation = new RoleLimitation();
+ $allowAdministratorLimitation->limitationValues[] = $roleService->loadRoleByIdentifier('Administrator')->id;
+
+ return [
+ [[$allowEditorLimitation], false],
+ [[$allowAdministratorLimitation], true],
+ ];
+ }
+
+ /**
+ * @dataProvider userPermissionLimitationProvider
+ */
+ public function testCanUserAssignRole(array $limitations, bool $expectedResult): void
+ {
+ $repository = $this->getRepository();
+ $roleService = $repository->getRoleService();
+ $userService = $repository->getUserService();
+
+ $adminRoleThatWillBeSet = $roleService->loadRoleByIdentifier('Administrator');
+ $this->loginAsEditorUserWithLimitations('role', 'assign', $limitations);
+
+ $this->assertCanUser(
+ $expectedResult,
+ 'role',
+ 'assign',
+ $limitations,
+ $userService->loadUser($this->permissionResolver->getCurrentUserReference()->getUserId()),
+ [$adminRoleThatWillBeSet]
+ );
+
+ $this->assertCanUser(
+ $expectedResult,
+ 'role',
+ 'assign',
+ $limitations,
+ $repository->sudo(
+ static function (Repository $repository) {
+ return $repository->getUserService()->loadUserGroup(self::USERS_GROUP_ID);
+ },
+ $repository
+ ),
+ [$adminRoleThatWillBeSet]
+ );
+ }
+}
diff --git a/tests/lib/Limitation/MemberOfLimitationTypeTest.php b/tests/lib/Limitation/MemberOfLimitationTypeTest.php
new file mode 100644
index 00000000000..3dd5e8e9240
--- /dev/null
+++ b/tests/lib/Limitation/MemberOfLimitationTypeTest.php
@@ -0,0 +1,517 @@
+limitationType = new MemberOfLimitationType(
+ $this->getPersistenceMock()
+ );
+ }
+
+ /**
+ * @dataProvider providerForTestAcceptValue
+ */
+ public function testAcceptValue(MemberOfLimitation $limitation): void
+ {
+ $this->expectNotToPerformAssertions();
+ $this->limitationType->acceptValue($limitation);
+ }
+
+ public function providerForTestAcceptValue(): array
+ {
+ return [
+ [
+ new MemberOfLimitation([
+ 'limitationValues' => [],
+ ]),
+ ],
+ [
+ new MemberOfLimitation([
+ 'limitationValues' => [MemberOfLimitationType::SELF_USER_GROUP, 8],
+ ]),
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider providerForTestAcceptValueException
+ */
+ public function testAcceptValueException(MemberOfLimitation $limitation): void
+ {
+ $this->expectException(InvalidArgumentType::class);
+ $this->limitationType->acceptValue($limitation);
+ }
+
+ public function providerForTestAcceptValueException(): array
+ {
+ return [
+ [
+ new MemberOfLimitation([
+ 'limitationValues' => 1,
+ ]),
+ ],
+ [
+ new MemberOfLimitation([
+ 'limitationValues' => null,
+ ]),
+ ],
+ [
+ new MemberOfLimitation([
+ 'limitationValues' => 'string',
+ ]),
+ ],
+ [
+ new MemberOfLimitation([
+ 'limitationValues' => ['string'],
+ ]),
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider providerForTestAcceptValue
+ */
+ public function testValidatePass(MemberOfLimitation $limitation): void
+ {
+ $contentHandlerMock = $this->createMock(ContentHandlerInterface::class);
+
+ $contentHandlerMock
+ ->method('loadContentInfo')
+ ->with(8);
+
+ $this->getPersistenceMock()
+ ->method('contentHandler')
+ ->willReturn($contentHandlerMock);
+
+ $validationErrors = $this->limitationType->validate($limitation);
+
+ self::assertEmpty($validationErrors);
+ }
+
+ /**
+ * @dataProvider providerForTestValidateError
+ */
+ public function testValidateError(MemberOfLimitation $limitation, int $errorCount): void
+ {
+ $contentHandlerMock = $this->createMock(ContentHandlerInterface::class);
+
+ if ($limitation->limitationValues !== null) {
+ $contentHandlerMock
+ ->method('loadContentInfo')
+ ->withConsecutive([14], [18])
+ ->willReturnOnConsecutiveCalls(
+ $this->throwException(new NotFoundException('UserGroup', 18)),
+ new ContentInfo()
+ );
+
+ $this->getPersistenceMock()
+ ->method('contentHandler')
+ ->willReturn($contentHandlerMock);
+ }
+
+ $validationErrors = $this->limitationType->validate($limitation);
+ self::assertCount($errorCount, $validationErrors);
+ }
+
+ public function providerForTestValidateError()
+ {
+ return [
+ [
+ new MemberOfLimitation([
+ 'limitationValues' => [14, 18],
+ ]),
+ 1,
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider providerForTestEvaluate
+ */
+ public function testEvaluate(
+ MemberOfLimitation $limitation,
+ ValueObject $object,
+ ?bool $expected
+ ): void {
+ $locationHandlerMock = $this->createMock(LocationHandlerInterface::class);
+
+ if ($object instanceof User || $object instanceof UserRoleAssignment) {
+ $locationHandlerMock
+ ->method('loadLocationsByContent')
+ ->with($object instanceof User ? $object->getUserId() : $object->getUser()->getUserId())
+ ->willReturn([
+ new Location(['parentId' => 13]),
+ new Location(['parentId' => 14]),
+ ]);
+
+ $locationHandlerMock
+ ->method('load')
+ ->withConsecutive(
+ [13], [14]
+ )
+ ->willReturnOnConsecutiveCalls(
+ new Location(['contentId' => 14]),
+ new Location(['contentId' => 25])
+ );
+
+ $this->getPersistenceMock()
+ ->method('locationHandler')
+ ->willReturn($locationHandlerMock);
+ }
+
+ $value = (new MemberOfLimitationType($this->getPersistenceMock()))->evaluate(
+ $limitation,
+ $this->getUserMock(),
+ $object
+ );
+
+ self::assertEquals($expected, $value);
+ }
+
+ public function providerForTestEvaluate()
+ {
+ return [
+ 'valid_group_limitation' => [
+ 'limitation' => new MemberOfLimitation([
+ 'limitationValues' => [14, 18],
+ ]),
+ 'object' => new UserGroup([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 14,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'expected' => true,
+ ],
+ 'allow_non_user_groups_limitation' => [
+ 'limitation' => new MemberOfLimitation([
+ 'limitationValues' => [],
+ ]),
+ 'object' => new UserGroup([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 14,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'expected' => false,
+ ],
+ 'pass_to_next_limitation' => [
+ 'limitation' => new MemberOfLimitation([
+ 'limitationValues' => [14, 18],
+ ]),
+ 'object' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 14,
+ ]),
+ ]),
+ 'expected' => null,
+ ],
+ 'invalid_user_must_have_permission_to_every_group_user_is_in' => [
+ 'limitation' => new MemberOfLimitation([
+ 'limitationValues' => [25, 10],
+ ]),
+ 'object' => new User([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 66,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'expected' => false,
+ ],
+ 'user_must_have_permission_to_every_group_user_is_in' => [
+ 'limitation' => new MemberOfLimitation([
+ 'limitationValues' => [14, 25],
+ ]),
+ 'object' => new User([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 66,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'expected' => true,
+ ],
+ 'user_role_assigment_valid' => [
+ 'limitation' => new MemberOfLimitation([
+ 'limitationValues' => [14, 25],
+ ]),
+ 'object' => new UserRoleAssignment([
+ 'user' => new User([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 66,
+ ]),
+ ]),
+ ]),
+ ]),
+ ]),
+ 'expected' => true,
+ ],
+ 'user_role_assigment_invalid_user' => [
+ 'limitation' => new MemberOfLimitation([
+ 'limitationValues' => [25, 10],
+ ]),
+ 'object' => new UserRoleAssignment([
+ 'user' => new User([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 66,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'role' => new Role(['id' => 7]),
+ ]),
+ 'expected' => false,
+ ],
+ 'user_group_role_assigment_valid' => [
+ 'limitation' => new MemberOfLimitation([
+ 'limitationValues' => [14, 18],
+ ]),
+ 'object' => new UserGroupRoleAssignment([
+ 'userGroup' => new UserGroup([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 14,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'role' => new Role(['id' => 7]),
+ ]),
+ 'expected' => true,
+ ],
+ 'user_group_role_assigment_invalid_user_group' => [
+ 'limitation' => new MemberOfLimitation([
+ 'limitationValues' => [18],
+ ]),
+ 'object' => new UserGroupRoleAssignment([
+ 'userGroup' => new UserGroup([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 14,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'role' => new Role(['id' => 7]),
+ ]),
+ 'expected' => false,
+ ],
+ ];
+ }
+
+ /**
+ * @param \eZ\Publish\API\Repository\Values\User\User|\eZ\Publish\API\Repository\Values\User\UserRoleAssignment $object
+ * @dataProvider providerForTestEvaluateSelfGroup
+ */
+ public function testEvaluateSelfGroup(
+ MemberOfLimitation $limitation,
+ ValueObject $object,
+ array $currentUserGroupLocations,
+ ?bool $expected
+ ): void {
+ $locationHandlerMock = $this->createMock(LocationHandlerInterface::class);
+
+ $currentUserLocation = [];
+
+ foreach ($currentUserGroupLocations as $groupLocation) {
+ $currentUserLocation[] = new Location(['parentId' => $groupLocation->contentId - 1]);
+ }
+ $locationHandlerMock
+ ->method('loadLocationsByContent')
+ ->withConsecutive(
+ [$object instanceof User ? $object->getUserId() : $object->getUser()->getUserId()],
+ [$this->getUserMock()->getUserId()]
+ )
+ ->willReturnOnConsecutiveCalls(
+ [
+ new Location(['parentId' => 13]),
+ new Location(['parentId' => 43]),
+ ],
+ $currentUserLocation
+ );
+
+ $locationHandlerMock
+ ->method('load')
+ ->withConsecutive(
+ [13], [43]
+ )
+ ->willReturnOnConsecutiveCalls(
+ new Location(['contentId' => 14]),
+ new Location(['contentId' => 44]),
+ ...$currentUserGroupLocations
+ );
+
+ $this->getPersistenceMock()
+ ->method('locationHandler')
+ ->willReturn($locationHandlerMock);
+
+ $this->getPersistenceMock()
+ ->method('locationHandler')
+ ->willReturn($locationHandlerMock);
+
+ $value = (new MemberOfLimitationType($this->getPersistenceMock()))->evaluate(
+ $limitation,
+ $this->getUserMock(),
+ $object
+ );
+
+ self::assertEquals($expected, $value);
+ }
+
+ public function providerForTestEvaluateSelfGroup(): array
+ {
+ return [
+ 'role_assign_to_user_in_same_group' => [
+ 'limitation' => new MemberOfLimitation([
+ 'limitationValues' => [MemberOfLimitationType::SELF_USER_GROUP],
+ ]),
+ 'object' => new User([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 66,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'currentUserGroupLocations' => [
+ new Location(['contentId' => 14]),
+ new Location(['contentId' => 44]),
+ new Location(['contentId' => 55]),
+ ],
+ 'expected' => true,
+ ],
+ 'role_assign_to_user_in_other_group' => [
+ 'limitation' => new MemberOfLimitation([
+ 'limitationValues' => [MemberOfLimitationType::SELF_USER_GROUP],
+ ]),
+ 'object' => new User([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 66,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'currentUserGroupLocations' => [
+ new Location(['contentId' => 11]),
+ new Location(['contentId' => 14]),
+ ],
+ 'expected' => false,
+ ],
+ 'role_assign_to_user_in_overlapped_groups' => [
+ 'limitation' => new MemberOfLimitation([
+ 'limitationValues' => [MemberOfLimitationType::SELF_USER_GROUP, 14, 44],
+ ]),
+ 'object' => new User([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 66,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'currentUserGroupLocations' => [
+ new Location(['contentId' => 1]),
+ ],
+ 'expected' => true,
+ ],
+ 'user_role_assigment_to_user_in_same_group' => [
+ 'limitation' => new MemberOfLimitation([
+ 'limitationValues' => [MemberOfLimitationType::SELF_USER_GROUP],
+ ]),
+ 'object' => new UserRoleAssignment([
+ 'user' => new User([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 66,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'role' => new Role(['id' => 4]),
+ ]),
+ 'currentUserGroupLocations' => [
+ new Location(['contentId' => 14]),
+ new Location(['contentId' => 44]),
+ new Location(['contentId' => 55]),
+ ],
+ 'expected' => true,
+ ],
+ 'user_role_assigment_to_user_in_other_group' => [
+ 'limitation' => new MemberOfLimitation([
+ 'limitationValues' => [MemberOfLimitationType::SELF_USER_GROUP],
+ ]),
+ 'object' => new UserRoleAssignment([
+ 'user' => new User([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 66,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'role' => new Role(['id' => 4]),
+ ]),
+ 'currentUserGroupLocations' => [
+ new Location(['contentId' => 11]),
+ new Location(['contentId' => 14]),
+ ],
+ 'expected' => false,
+ ],
+ ];
+ }
+}
diff --git a/tests/lib/Limitation/RoleLimitationTypeTest.php b/tests/lib/Limitation/RoleLimitationTypeTest.php
new file mode 100644
index 00000000000..1defbef1d67
--- /dev/null
+++ b/tests/lib/Limitation/RoleLimitationTypeTest.php
@@ -0,0 +1,318 @@
+limitationType = new RoleLimitationType(
+ $this->getPersistenceMock()
+ );
+ }
+
+ /**
+ * @dataProvider providerForTestAcceptValue
+ */
+ public function testAcceptValue(RoleLimitation $limitation): void
+ {
+ $this->expectNotToPerformAssertions();
+ $this->limitationType->acceptValue($limitation);
+ }
+
+ public function providerForTestAcceptValue(): array
+ {
+ return [
+ [
+ new RoleLimitation([
+ 'limitationValues' => [],
+ ]),
+ ],
+ [
+ new RoleLimitation([
+ 'limitationValues' => [4, 8],
+ ]),
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider providerForTestAcceptValueException
+ */
+ public function testAcceptValueException(RoleLimitation $limitation): void
+ {
+ $this->expectException(InvalidArgumentType::class);
+ $this->limitationType->acceptValue($limitation);
+ }
+
+ public function providerForTestAcceptValueException(): array
+ {
+ return [
+ [
+ new RoleLimitation([
+ 'limitationValues' => 1,
+ ]),
+ ],
+ [
+ new RoleLimitation([
+ 'limitationValues' => null,
+ ]),
+ ],
+ [
+ new RoleLimitation([
+ 'limitationValues' => 'string',
+ ]),
+ ],
+ [
+ new RoleLimitation([
+ 'limitationValues' => ['string'],
+ ]),
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider providerForTestAcceptValue
+ */
+ public function testValidatePass(RoleLimitation $limitation): void
+ {
+ $userHandlerMock = $this->createMock(UserHandlerInterface::class);
+ $contentHandlerMock = $this->createMock(ContentHandlerInterface::class);
+
+ if ($limitation->limitationValues !== null) {
+ $userHandlerMock
+ ->method('loadRole')
+ ->withConsecutive([4, Role::STATUS_DEFINED], [8, Role::STATUS_DEFINED]);
+
+ $this->getPersistenceMock()
+ ->method('userHandler')
+ ->willReturn($userHandlerMock);
+ }
+
+ if ($limitation->limitationValues !== null) {
+ $contentHandlerMock
+ ->method('loadContentInfo')
+ ->withConsecutive([14], [21]);
+
+ $this->getPersistenceMock()
+ ->method('contentHandler')
+ ->willReturn($contentHandlerMock);
+ }
+
+ $validationErrors = $this->limitationType->validate($limitation);
+
+ self::assertEmpty($validationErrors);
+ }
+
+ /**
+ * @dataProvider providerForTestValidateError
+ */
+ public function testValidateError(RoleLimitation $limitation, int $errorCount): void
+ {
+ $userHandlerMock = $this->createMock(UserHandlerInterface::class);
+
+ if ($limitation->limitationValues !== null) {
+ $userHandlerMock
+ ->method('loadRole')
+ ->withConsecutive([4, Role::STATUS_DEFINED], [8, Role::STATUS_DEFINED])
+ ->willReturnOnConsecutiveCalls(
+ $this->throwException(new NotFoundException('Role', 4)),
+ new Role()
+ );
+
+ $this->getPersistenceMock()
+ ->method('userHandler')
+ ->willReturn($userHandlerMock);
+ }
+
+ $validationErrors = $this->limitationType->validate($limitation);
+ self::assertCount($errorCount, $validationErrors);
+ }
+
+ public function providerForTestValidateError()
+ {
+ return [
+ [
+ new RoleLimitation([
+ 'limitationValues' => [4, 8],
+ ]),
+ 1,
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider providerForTestEvaluate
+ */
+ public function testEvaluate(
+ RoleLimitation $limitation,
+ ValueObject $object,
+ ?array $targets,
+ ?bool $expected
+ ): void {
+ $locationHandlerMock = $this->createMock(LocationHandlerInterface::class);
+
+ if ($object instanceof UserRoleAssignment) {
+ $locationHandlerMock
+ ->method('loadLocationsByContent')
+ ->with($object instanceof User ? $object->getUserId() : $object->getUser()->getUserId())
+ ->willReturn([
+ new Location(['contentId' => 14]),
+ new Location(['contentId' => 25]),
+ ]);
+
+ $this->getPersistenceMock()
+ ->method('locationHandler')
+ ->willReturn($locationHandlerMock);
+ }
+
+ $value = (new RoleLimitationType($this->getPersistenceMock()))->evaluate(
+ $limitation,
+ $this->getUserMock(),
+ $object,
+ $targets
+ );
+
+ self::assertEquals($expected, $value);
+ }
+
+ public function providerForTestEvaluate()
+ {
+ return [
+ 'valid_role_limitation' => [
+ 'limitation' => new RoleLimitation([
+ 'limitationValues' => [4, 8],
+ ]),
+ 'object' => new Role(['id' => 4]),
+ 'targets' => null,
+ 'expected' => true,
+ ],
+ 'allow_non_role_limitation' => [
+ 'limitation' => new RoleLimitation([
+ 'limitationValues' => [],
+ ]),
+ 'object' => new Role(['id' => 4]),
+ 'targets' => null,
+ 'expected' => false,
+ ],
+ 'pass_to_next_limitation' => [
+ 'limitation' => new RoleLimitation([
+ 'limitationValues' => [4, 8],
+ ]),
+ 'object' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 14,
+ ]),
+ ]),
+ 'targets' => null,
+ 'expected' => null,
+ ],
+ 'user_role_assigment_valid' => [
+ 'limitation' => new RoleLimitation([
+ 'limitationValues' => [4, 8],
+ ]),
+ 'object' => new UserRoleAssignment([
+ 'user' => new User([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 66,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'role' => new Role(['id' => 4]),
+ ]),
+ 'targets' => null,
+ 'expected' => true,
+ ],
+ 'user_role_assigment_invalid_role' => [
+ 'limitation' => new RoleLimitation([
+ 'limitationValues' => [4, 8],
+ ]),
+ 'object' => new UserRoleAssignment([
+ 'user' => new User([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 66,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'role' => new Role(['id' => 7]),
+ ]),
+ 'targets' => null,
+ 'expected' => false,
+ ],
+ 'user_group_role_assigment_valid' => [
+ 'limitation' => new RoleLimitation([
+ 'limitationValues' => [4, 8],
+ ]),
+ 'object' => new UserGroupRoleAssignment([
+ 'userGroup' => new UserGroup([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 66,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'role' => new Role(['id' => 4]),
+ ]),
+ 'targets' => null,
+ 'expected' => true,
+ ],
+ 'user_group_role_assigment_invalid_role' => [
+ 'limitation' => new RoleLimitation([
+ 'limitationValues' => [4, 8],
+ ]),
+ 'object' => new UserGroupRoleAssignment([
+ 'userGroup' => new UserGroup([
+ 'content' => new Content([
+ 'versionInfo' => new VersionInfo([
+ 'contentInfo' => new ContentInfo([
+ 'id' => 66,
+ ]),
+ ]),
+ ]),
+ ]),
+ 'role' => new Role(['id' => 7]),
+ ]),
+ 'targets' => null,
+ 'expected' => false,
+ ],
+ ];
+ }
+}