CVE-2022-48365 (PHP, published 2023-03-12T05:15Z)
Description: value: An issue was discovered in eZ Platform Ibexa Kernel before 1.3.26. The Company admin role gives excessive privileges.
Severity: nan (CVSS v3 base score: 7.2)
Weakness: CWE-269 \u2014 Improper Privilege Management
The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.
Repository: https://github.com/ezsystems/ezpublish-kernel
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 message: Merge pull request from GHSA-99r3-xmmq-7q7g



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