CVE-2022-48365: Improper Privilege Management (CWE-269)
value: An issue was discovered in eZ Platform Ibexa Kernel before 1.3.26. The Company admin role gives excessive privileges.
CWE description: The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.
CVSS v3 base score: 7.2
Published: 2023-03-12T05:15Z
Repository: https://github.com/ezsystems/ezpublish-kernel
Language: PHP
Files changed: eZ/Publish/Core/settings/policies.yml, eZ/Publish/Core/settings/roles.yml, phpunit-integration-legacy.xml, phpunit.xml, src/contracts/Repository/Values/User/Limitation/MemberOfLimitation.php, src/contracts/Repository/Values/User/Limitation/RoleLimitation.php, src/lib/Limitation/MemberOfLimitationType.php, src/lib/Limitation/RoleLimitationType.php, tests/integration/Core/Limitation/MemberOfLimitationTest.php, tests/integration/Core/Limitation/RoleLimitationTest.php, tests/lib/Limitation/MemberOfLimitationTypeTest.php, tests/lib/Limitation/RoleLimitationTypeTest.php
Commit date: 2022-11-10 12:22:29 +0100
Commit message: Merge pull request from GHSA-99r3-xmmq-7q7g


Diff: diff --git a/eZ/Publish/Core/settings/policies.yml b/eZ/Publish/Core/settings/policies.yml
index c0862d4898..c1140988d4 100644
--- a/eZ/Publish/Core/settings/policies.yml
+++ b/eZ/Publish/Core/settings/policies.yml
@@ -28,11 +28,11 @@ parameters:
         state:
             assign: { Class: true, Section: true, Owner: true, Group: true, Node: true, Subtree: true, State: true, NewState: true }
             administrate: ~
 
         role:
-            assign: ~
+            assign: { MemberOf: true, Role: true }
             update: ~
             create: ~
             delete: ~
             read: ~
 
diff --git a/eZ/Publish/Core/settings/roles.yml b/eZ/Publish/Core/settings/roles.yml
index 6cb5814930..9533763c90 100644
--- a/eZ/Publish/Core/settings/roles.yml
+++ b/eZ/Publish/Core/settings/roles.yml
@@ -144,5 +144,17 @@ services:
 
     ezpublish.api.role.limitation_type.ezcomments.anti_spam:
         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 c42c1e661b..ace45238b1 100644
--- a/phpunit-integration-legacy.xml
+++ b/phpunit-integration-legacy.xml
@@ -71,10 +71,13 @@
             <directory>eZ/Publish/Core/FieldType/Tests/Integration</directory>
         </testsuite>
         <testsuite name="gateway-integration">
             <file>eZ/Publish/SPI/Tests/Search/Content/IndexerGatewayTest.php</file>
         </testsuite>
+        <testsuite name="Ibexa\Tests\Integration\">
+            <directory>tests/integration</directory>
+        </testsuite>
     </testsuites>
     <filter>
         <whitelist>
             <directory suffix=".php">eZ/Publish/API/Repository</directory>
             <exclude>
diff --git a/phpunit.xml b/phpunit.xml
index 082bbb2817..fe30983fe4 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -71,10 +71,13 @@
         <directory>eZ/Publish/Core/Helper</directory>
     </testsuite>
     <testsuite name="eZ\Publish\API\Repository\Values\Content">
       <directory>eZ/Publish/API/Repository/Tests/Values/Content</directory>
     </testsuite>
+    <testsuite name="Ibexa\Tests\Core\">
+        <directory>tests/lib</directory>
+    </testsuite>
   </testsuites>
   <filter>
     <whitelist>
       <directory suffix=".php">eZ</directory>
       <exclude>
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 0000000000..960cf57cea
--- /dev/null
+++ b/src/contracts/Repository/Values/User/Limitation/MemberOfLimitation.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @copyright Copyright (C) eZ Systems 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\Core\Repository\Values\User\Limitation;
+
+use eZ\Publish\API\Repository\Values\User\Limitation;
+
+final class MemberOfLimitation extends Limitation
+{
+    public const IDENTIFIER = 'MemberOf';
+
+    public function getIdentifier(): string
+    {
+        return self::IDENTIFIER;
+    }
+}
diff --git a/src/contracts/Repository/Values/User/Limitation/RoleLimitation.php b/src/contracts/Repository/Values/User/Limitation/RoleLimitation.php
new file mode 100644
index 0000000000..9cb1b4898b
--- /dev/null
+++ b/src/contracts/Repository/Values/User/Limitation/RoleLimitation.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @copyright Copyright (C) eZ Systems 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\Core\Repository\Values\User\Limitation;
+
+use eZ\Publish\API\Repository\Values\User\Limitation;
+
+final class RoleLimitation extends Limitation
+{
+    public const IDENTIFIER = 'Role';
+
+    public function getIdentifier(): string
+    {
+        return self::IDENTIFIER;
+    }
+}
diff --git a/src/lib/Limitation/MemberOfLimitationType.php b/src/lib/Limitation/MemberOfLimitationType.php
new file mode 100644
index 0000000000..38be25fa12
--- /dev/null
+++ b/src/lib/Limitation/MemberOfLimitationType.php
@@ -0,0 +1,201 @@
+<?php
+
+/**
+ * @copyright Copyright (C) eZ Systems 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\Core\Limitation;
+
+use eZ\Publish\API\Repository\Exceptions\NotImplementedException;
+use eZ\Publish\API\Repository\Values\User\User;
+use eZ\Publish\API\Repository\Values\User\UserGroup;
+use eZ\Publish\API\Repository\Values\User\UserGroupRoleAssignment;
+use eZ\Publish\API\Repository\Values\User\UserReference as APIUserReference;
+use eZ\Publish\API\Repository\Exceptions\NotFoundException;
+use eZ\Publish\API\Repository\Values\User\UserRoleAssignment;
+use eZ\Publish\API\Repository\Values\ValueObject;
+use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
+use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
+use eZ\Publish\Core\FieldType\ValidationError;
+use eZ\Publish\Core\Limitation\AbstractPersistenceLimitationType;
+use eZ\Publish\SPI\Limitation\Type as SPILimitationTypeInterface;
+use eZ\Publish\API\Repository\Values\User\Limitation as APILimitationValue;
+use Ibexa\Contracts\Core\Repository\Values\User\Limitation\MemberOfLimitation;
+
+final class MemberOfLimitationType extends AbstractPersistenceLimitationType implements SPILimitationTypeInterface
+{
+    public const SELF_USER_GROUP = -1;
+
+    /**
+     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentType
+     */
+    public function acceptValue(APILimitationValue $limitationValue): void
+    {
+        if (!$limitationValue instanceof MemberOfLimitation) {
+            throw new InvalidArgumentType(
+                '$limitationValue',
+                MemberOfLimitation::class,
+                $limitationValue
+            );
+        }
+
+        if (!is_array($limitationValue->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 0000000000..e33e1ea6d1
--- /dev/null
+++ b/src/lib/Limitation/RoleLimitationType.php
@@ -0,0 +1,143 @@
+<?php
+
+/**
+ * @copyright Copyright (C) eZ Systems 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\Core\Limitation;
+
+use eZ\Publish\API\Repository\Exceptions\NotImplementedException;
+use eZ\Publish\API\Repository\Values\User\Role;
+use eZ\Publish\API\Repository\Values\User\User;
+use eZ\Publish\API\Repository\Values\User\UserGroup;
+use eZ\Publish\API\Repository\Values\User\UserGroupRoleAssignment;
+use eZ\Publish\API\Repository\Values\User\UserReference as APIUserReference;
+use eZ\Publish\API\Repository\Exceptions\NotFoundException;
+use eZ\Publish\API\Repository\Values\User\UserRoleAssignment;
+use eZ\Publish\API\Repository\Values\ValueObject;
+use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
+use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
+use eZ\Publish\Core\FieldType\ValidationError;
+use eZ\Publish\Core\Limitation\AbstractPersistenceLimitationType;
+use eZ\Publish\SPI\Limitation\Type as SPILimitationTypeInterface;
+use eZ\Publish\API\Repository\Values\User\Limitation as APILimitationValue;
+use Ibexa\Contracts\Core\Repository\Values\User\Limitation\RoleLimitation;
+
+final class RoleLimitationType extends AbstractPersistenceLimitationType implements SPILimitationTypeInterface
+{
+    /**
+     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentType
+     */
+    public function acceptValue(APILimitationValue $limitationValue): void
+    {
+        if (!$limitationValue instanceof RoleLimitation) {
+            throw new InvalidArgumentType(
+                '$limitationValue',
+                RoleLimitation::class,
+                $limitationValue
+            );
+        }
+
+        if (!is_array($limitationValue->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 e69de29bb2..0000000000
diff --git a/tests/integration/Core/Limitation/MemberOfLimitationTest.php b/tests/integration/Core/Limitation/MemberOfLimitationTest.php
new file mode 100644
index 0000000000..5c89166a2f
--- /dev/null
+++ b/tests/integration/Core/Limitation/MemberOfLimitationTest.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * @copyright Copyright (C) eZ Systems 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\Tests\Integration\Core\Limitation;
+
+use eZ\Publish\API\Repository\Repository;
+use eZ\Publish\API\Repository\Tests\Limitation\PermissionResolver\BaseLimitationIntegrationTest;
+use Ibexa\Contracts\Core\Repository\Values\User\Limitation\MemberOfLimitation;
+use Ibexa\Core\Limitation\MemberOfLimitationType;
+
+class MemberOfLimitationTest extends BaseLimitationIntegrationTest
+{
+    private const ADMIN_GROUP_ID = 14;
+    private const USERS_GROUP_ID = 4;
+
+    public function userPermissionLimitationProvider(): array
+    {
+        $allowInAdministratorsLimitation = new MemberOfLimitation();
+        $allowInAdministratorsLimitation->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 0000000000..729862db80
--- /dev/null
+++ b/tests/integration/Core/Limitation/RoleLimitationTest.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @copyright Copyright (C) eZ Systems 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\Tests\Integration\Core\Limitation;
+
+use eZ\Publish\API\Repository\Repository;
+use eZ\Publish\API\Repository\Tests\Limitation\PermissionResolver\BaseLimitationIntegrationTest;
+use Ibexa\Contracts\Core\Repository\Values\User\Limitation\RoleLimitation;
+
+class RoleLimitationTest extends BaseLimitationIntegrationTest
+{
+    private const USERS_GROUP_ID = 4;
+
+    public function userPermissionLimitationProvider(): array
+    {
+        $allowEditorLimitation = new RoleLimitation();
+        $roleService = $this->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 0000000000..3dd5e8e924
--- /dev/null
+++ b/tests/lib/Limitation/MemberOfLimitationTypeTest.php
@@ -0,0 +1,517 @@
+<?php
+
+/**
+ * @copyright Copyright (C) eZ Systems 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\Tests\Core\Limitation;
+
+use eZ\Publish\API\Repository\Values\Content\ContentInfo;
+use eZ\Publish\API\Repository\Values\ValueObject;
+use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
+use eZ\Publish\Core\Base\Exceptions\NotFoundException;
+use eZ\Publish\Core\Limitation\Tests\Base;
+use eZ\Publish\Core\Persistence\Legacy\Content\Handler as ContentHandlerInterface;
+use eZ\Publish\Core\Repository\Values\User\UserGroupRoleAssignment;
+use eZ\Publish\SPI\Persistence\Content\Location;
+use eZ\Publish\SPI\Persistence\Content\Location\Handler as LocationHandlerInterface;
+use eZ\Publish\Core\Repository\Values\Content\Content;
+use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
+use eZ\Publish\Core\Repository\Values\User\Role;
+use eZ\Publish\Core\Repository\Values\User\User;
+use eZ\Publish\Core\Repository\Values\User\UserGroup;
+use eZ\Publish\Core\Repository\Values\User\UserRoleAssignment;
+use Ibexa\Contracts\Core\Repository\Values\User\Limitation\MemberOfLimitation;
+use Ibexa\Core\Limitation\MemberOfLimitationType;
+
+class MemberOfLimitationTypeTest extends Base
+{
+    /** @var \Ibexa\Core\Limitation\MemberOfLimitationType */
+    private $limitationType;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->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 0000000000..1defbef1d6
--- /dev/null
+++ b/tests/lib/Limitation/RoleLimitationTypeTest.php
@@ -0,0 +1,318 @@
+<?php
+
+/**
+ * @copyright Copyright (C) eZ Systems 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\Tests\Core\Limitation;
+
+use eZ\Publish\API\Repository\Values\Content\ContentInfo;
+use eZ\Publish\API\Repository\Values\ValueObject;
+use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
+use eZ\Publish\Core\Base\Exceptions\NotFoundException;
+use eZ\Publish\Core\Limitation\Tests\Base;
+use eZ\Publish\Core\Persistence\Legacy\User\Handler as UserHandlerInterface;
+use eZ\Publish\Core\Persistence\Legacy\Content\Handler as ContentHandlerInterface;
+use eZ\Publish\Core\Repository\Values\User\UserGroup;
+use eZ\Publish\Core\Repository\Values\User\UserGroupRoleAssignment;
+use eZ\Publish\SPI\Persistence\Content\Location;
+use eZ\Publish\SPI\Persistence\Content\Location\Handler as LocationHandlerInterface;
+use eZ\Publish\Core\Repository\Values\Content\Content;
+use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
+use eZ\Publish\Core\Repository\Values\User\Role;
+use eZ\Publish\Core\Repository\Values\User\User;
+use eZ\Publish\Core\Repository\Values\User\UserRoleAssignment;
+use Ibexa\Contracts\Core\Repository\Values\User\Limitation\RoleLimitation;
+use Ibexa\Core\Limitation\RoleLimitationType;
+
+class RoleLimitationTypeTest extends Base
+{
+    /** @var \Ibexa\Core\Limitation\RoleLimitationType */
+    private $limitationType;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->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,
+            ],
+        ];
+    }
+}

Vulnerable code:             assign: ~
Fixed code:             assign: { MemberOf: true, Role: true }

    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 }
        <testsuite name="Ibexa\Tests\Integration\">
            <directory>tests/integration</directory>
        </testsuite>
    <testsuite name="Ibexa\Tests\Core\">
        <directory>tests/lib</directory>
    </testsuite>
<?php

/**
 * @copyright Copyright (C) eZ Systems 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\Core\Repository\Values\User\Limitation;

use eZ\Publish\API\Repository\Values\User\Limitation;

final class MemberOfLimitation extends Limitation
{
    public const IDENTIFIER = 'MemberOf';

    public function getIdentifier(): string
    {
        return self::IDENTIFIER;
    }
}
<?php

/**
 * @copyright Copyright (C) eZ Systems 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\Core\Repository\Values\User\Limitation;

use eZ\Publish\API\Repository\Values\User\Limitation;

final class RoleLimitation extends Limitation
{
    public const IDENTIFIER = 'Role';

    public function getIdentifier(): string
    {
        return self::IDENTIFIER;
    }
}
<?php

/**
 * @copyright Copyright (C) eZ Systems 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\Core\Limitation;

use eZ\Publish\API\Repository\Exceptions\NotImplementedException;
use eZ\Publish\API\Repository\Values\User\User;
use eZ\Publish\API\Repository\Values\User\UserGroup;
use eZ\Publish\API\Repository\Values\User\UserGroupRoleAssignment;
use eZ\Publish\API\Repository\Values\User\UserReference as APIUserReference;
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
use eZ\Publish\API\Repository\Values\User\UserRoleAssignment;
use eZ\Publish\API\Repository\Values\ValueObject;
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
use eZ\Publish\Core\FieldType\ValidationError;
use eZ\Publish\Core\Limitation\AbstractPersistenceLimitationType;
use eZ\Publish\SPI\Limitation\Type as SPILimitationTypeInterface;
use eZ\Publish\API\Repository\Values\User\Limitation as APILimitationValue;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation\MemberOfLimitation;

final class MemberOfLimitationType extends AbstractPersistenceLimitationType implements SPILimitationTypeInterface
{
    public const SELF_USER_GROUP = -1;

    /**
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentType
     */
    public function acceptValue(APILimitationValue $limitationValue): void
    {
        if (!$limitationValue instanceof MemberOfLimitation) {
            throw new InvalidArgumentType(
                '$limitationValue',
                MemberOfLimitation::class,
                $limitationValue
            );
        }

        if (!is_array($limitationValue->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'
        );
    }
}
<?php

/**
 * @copyright Copyright (C) eZ Systems 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\Core\Limitation;

use eZ\Publish\API\Repository\Exceptions\NotImplementedException;
use eZ\Publish\API\Repository\Values\User\Role;
use eZ\Publish\API\Repository\Values\User\User;
use eZ\Publish\API\Repository\Values\User\UserGroup;
use eZ\Publish\API\Repository\Values\User\UserGroupRoleAssignment;
use eZ\Publish\API\Repository\Values\User\UserReference as APIUserReference;
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
use eZ\Publish\API\Repository\Values\User\UserRoleAssignment;
use eZ\Publish\API\Repository\Values\ValueObject;
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
use eZ\Publish\Core\FieldType\ValidationError;
use eZ\Publish\Core\Limitation\AbstractPersistenceLimitationType;
use eZ\Publish\SPI\Limitation\Type as SPILimitationTypeInterface;
use eZ\Publish\API\Repository\Values\User\Limitation as APILimitationValue;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation\RoleLimitation;

final class RoleLimitationType extends AbstractPersistenceLimitationType implements SPILimitationTypeInterface
{
    /**
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentType
     */
    public function acceptValue(APILimitationValue $limitationValue): void
    {
        if (!$limitationValue instanceof RoleLimitation) {
            throw new InvalidArgumentType(
                '$limitationValue',
                RoleLimitation::class,
                $limitationValue
            );
        }

        if (!is_array($limitationValue->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);
    }
}
<?php

/**
 * @copyright Copyright (C) eZ Systems 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\Tests\Integration\Core\Limitation;

use eZ\Publish\API\Repository\Repository;
use eZ\Publish\API\Repository\Tests\Limitation\PermissionResolver\BaseLimitationIntegrationTest;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation\MemberOfLimitation;
use Ibexa\Core\Limitation\MemberOfLimitationType;

class MemberOfLimitationTest extends BaseLimitationIntegrationTest
{
    private const ADMIN_GROUP_ID = 14;
    private const USERS_GROUP_ID = 4;

    public function userPermissionLimitationProvider(): array
    {
        $allowInAdministratorsLimitation = new MemberOfLimitation();
        $allowInAdministratorsLimitation->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]
        );
    }
}
<?php

/**
 * @copyright Copyright (C) eZ Systems 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\Tests\Integration\Core\Limitation;

use eZ\Publish\API\Repository\Repository;
use eZ\Publish\API\Repository\Tests\Limitation\PermissionResolver\BaseLimitationIntegrationTest;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation\RoleLimitation;

class RoleLimitationTest extends BaseLimitationIntegrationTest
{
    private const USERS_GROUP_ID = 4;

    public function userPermissionLimitationProvider(): array
    {
        $allowEditorLimitation = new RoleLimitation();
        $roleService = $this->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]
        );
    }
}
<?php

/**
 * @copyright Copyright (C) eZ Systems 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\Tests\Core\Limitation;

use eZ\Publish\API\Repository\Values\Content\ContentInfo;
use eZ\Publish\API\Repository\Values\ValueObject;
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
use eZ\Publish\Core\Limitation\Tests\Base;
use eZ\Publish\Core\Persistence\Legacy\Content\Handler as ContentHandlerInterface;
use eZ\Publish\Core\Repository\Values\User\UserGroupRoleAssignment;
use eZ\Publish\SPI\Persistence\Content\Location;
use eZ\Publish\SPI\Persistence\Content\Location\Handler as LocationHandlerInterface;
use eZ\Publish\Core\Repository\Values\Content\Content;
use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
use eZ\Publish\Core\Repository\Values\User\Role;
use eZ\Publish\Core\Repository\Values\User\User;
use eZ\Publish\Core\Repository\Values\User\UserGroup;
use eZ\Publish\Core\Repository\Values\User\UserRoleAssignment;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation\MemberOfLimitation;
use Ibexa\Core\Limitation\MemberOfLimitationType;

class MemberOfLimitationTypeTest extends Base
{
    /** @var \Ibexa\Core\Limitation\MemberOfLimitationType */
    private $limitationType;

    protected function setUp(): void
    {
        parent::setUp();

        $this->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,
            ],
        ];
    }
}
<?php

/**
 * @copyright Copyright (C) eZ Systems 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\Tests\Core\Limitation;

use eZ\Publish\API\Repository\Values\Content\ContentInfo;
use eZ\Publish\API\Repository\Values\ValueObject;
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentType;
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
use eZ\Publish\Core\Limitation\Tests\Base;
use eZ\Publish\Core\Persistence\Legacy\User\Handler as UserHandlerInterface;
use eZ\Publish\Core\Persistence\Legacy\Content\Handler as ContentHandlerInterface;
use eZ\Publish\Core\Repository\Values\User\UserGroup;
use eZ\Publish\Core\Repository\Values\User\UserGroupRoleAssignment;
use eZ\Publish\SPI\Persistence\Content\Location;
use eZ\Publish\SPI\Persistence\Content\Location\Handler as LocationHandlerInterface;
use eZ\Publish\Core\Repository\Values\Content\Content;
use eZ\Publish\Core\Repository\Values\Content\VersionInfo;
use eZ\Publish\Core\Repository\Values\User\Role;
use eZ\Publish\Core\Repository\Values\User\User;
use eZ\Publish\Core\Repository\Values\User\UserRoleAssignment;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation\RoleLimitation;
use Ibexa\Core\Limitation\RoleLimitationType;

class RoleLimitationTypeTest extends Base
{
    /** @var \Ibexa\Core\Limitation\RoleLimitationType */
    private $limitationType;

    protected function setUp(): void
    {
        parent::setUp();

        $this->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,
            ],
        ];
    }
}