Skip to content

Commit

Permalink
Merge pull request from GHSA-394j-x37r-2q27
Browse files Browse the repository at this point in the history
IBX-3821: Added new Role and MemberOf limitations
  • Loading branch information
glye authored Nov 10, 2022
2 parents 1fcb7c4 + 1c26778 commit da3642c
Show file tree
Hide file tree
Showing 11 changed files with 1,369 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Contracts\Core\Repository\Values\User\Limitation;

use Ibexa\Contracts\Core\Repository\Values\User\Limitation;

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

public function getIdentifier(): string
{
return self::IDENTIFIER;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Contracts\Core\Repository\Values\User\Limitation;

use Ibexa\Contracts\Core\Repository\Values\User\Limitation;

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

public function getIdentifier(): string
{
return self::IDENTIFIER;
}
}
198 changes: 198 additions & 0 deletions src/lib/Limitation/MemberOfLimitationType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Core\Limitation;

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

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

/**
* @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentException
*/
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
*/
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'
);
}
}
140 changes: 140 additions & 0 deletions src/lib/Limitation/RoleLimitationType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Core\Limitation;

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

final class RoleLimitationType extends AbstractPersistenceLimitationType implements SPILimitationTypeInterface
{
/**
* @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentException
*/
public function acceptValue(APILimitationValue $limitationValue): void
{
if (!$limitationValue instanceof UserRoleLimitation) {
throw new InvalidArgumentType(
'$limitationValue',
UserRoleLimitation::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
*/
public function buildValue(array $limitationValues): APILimitationValue
{
return new UserRoleLimitation(['limitationValues' => $limitationValues]);
}

public function evaluate(APILimitationValue $value, APIUserReference $currentUser, ValueObject $object, array $targets = null)
{
if (!$value instanceof UserRoleLimitation) {
throw new InvalidArgumentException(
'$value',
sprintf('Must be of type: %s', UserRoleLimitation::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(UserRoleLimitation $value, Role $role): bool
{
return in_array($role->id, $value->limitationValues);
}
}
2 changes: 1 addition & 1 deletion src/lib/Resources/settings/policies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ parameters:
administrate: ~

role:
assign: ~
assign: { MemberOf: true, Role: true }
update: ~
create: ~
delete: ~
Expand Down
Loading

0 comments on commit da3642c

Please sign in to comment.