Skip to content

Commit

Permalink
IBX-4032: Added limitation to prevent changing content ownership (#162)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nattfarinn authored Oct 28, 2022
1 parent 7a117a7 commit 2db1cbf
Show file tree
Hide file tree
Showing 7 changed files with 412 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?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 ChangeOwnerLimitation extends Limitation
{
public const IDENTIFIER = 'ChangeOwner';

public const LIMITATION_VALUE_SELF = -1;

/**
* @param int[] $limitationValues
*/
public function __construct(array $limitationValues)
{
parent::__construct([
'limitationValues' => $limitationValues,
]);
}

public function getIdentifier(): string
{
return self::IDENTIFIER;
}
}
120 changes: 120 additions & 0 deletions src/lib/Limitation/ChangeOwnerLimitationType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?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\Exception\InvalidArgumentType;
use Ibexa\Contracts\Core\Limitation\Type as SPILimitationTypeInterface;
use Ibexa\Contracts\Core\Repository\Exceptions\NotImplementedException;
use Ibexa\Contracts\Core\Repository\Values\Content\ContentCreateStruct;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation;
use Ibexa\Contracts\Core\Repository\Values\User\Limitation\ChangeOwnerLimitation;
use Ibexa\Contracts\Core\Repository\Values\User\UserReference as APIUserReference;
use Ibexa\Contracts\Core\Repository\Values\ValueObject as APIValueObject;
use Ibexa\Core\FieldType\ValidationError;

final class ChangeOwnerLimitationType extends AbstractPersistenceLimitationType implements SPILimitationTypeInterface
{
public function acceptValue(Limitation $limitationValue): void
{
if (!is_array($limitationValue->limitationValues)) {
throw new InvalidArgumentType(
'$limitationValue->limitationValues',
'array',
$limitationValue->limitationValues
);
}

foreach ($limitationValue->limitationValues as $key => $value) {
if (is_string($value) && is_numeric($value)) {
$limitationValue->limitationValues[$key] = (int)$value;
}
}

$limitationValue->limitationValues = array_filter($limitationValue->limitationValues);
$limitationValue->limitationValues = array_unique($limitationValue->limitationValues);
}

public function validate(Limitation $limitationValue): array
{
$validationErrors = [];

foreach ($limitationValue->limitationValues as $key => $value) {
if (is_int($value)) {
continue;
}

$validationErrors[] = new ValidationError(
"limitationValues[%key%] => '%value%' must be an integer",
null,
[
'value' => $value,
'key' => $key,
]
);
}

return $validationErrors;
}

public function buildValue(array $limitationValues): ChangeOwnerLimitation
{
return new ChangeOwnerLimitation($limitationValues);
}

public function evaluate(
Limitation $value,
APIUserReference $currentUser,
APIValueObject $object,
array $targets = null
): ?bool {
if (!$object instanceof ContentCreateStruct) {
return self::ACCESS_ABSTAIN;
}

$limitationValues = array_filter($value->limitationValues);

if (empty($limitationValues)) {
return self::ACCESS_GRANTED;
}

$userId = $currentUser->getUserId();
$limitationValues = array_map(
static fn (int $value): int => $value === ChangeOwnerLimitation::LIMITATION_VALUE_SELF ? $userId : $value,
$limitationValues
);

if (!is_numeric($object->ownerId)) {
return self::ACCESS_ABSTAIN;
}

if (in_array((int)$object->ownerId, $limitationValues, true)) {
return self::ACCESS_GRANTED;
}

return self::ACCESS_DENIED;
}

public function getCriterion(Limitation $value, APIUserReference $currentUser): Criterion\UserMetadata
{
return new Criterion\UserMetadata(
Criterion\UserMetadata::OWNER,
Criterion\Operator::IN,
$value->limitationValues
);
}

/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotImplementedException
*/
public function valueSchema(): void
{
throw new NotImplementedException(__METHOD__);
}
}
4 changes: 2 additions & 2 deletions src/lib/Resources/settings/policies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ parameters:
read: { Class: true, Section: true, Owner: true, Group: true, Node: true, Subtree: true, State: true }
diff: { Class: true, Section: true, Owner: true, Node: true, Subtree: true }
view_embed: { Class: true, Section: true, Owner: true, Node: true, Subtree: true }
create: { Class: true, Section: true, ParentOwner: true, ParentGroup: true, ParentClass: true, ParentDepth: true, Node: true, Subtree: true, Language: true }
edit: { Class: true, Section: true, Owner: true, Group: true, Node: true, Subtree: true, Language: true, State: true }
create: { Class: true, Section: true, ParentOwner: true, ParentGroup: true, ParentClass: true, ParentDepth: true, Node: true, Subtree: true, Language: true, ChangeOwner: true }
edit: { Class: true, Section: true, Owner: true, Group: true, Node: true, Subtree: true, Language: true, State: true, ChangeOwner: true }
publish: { Class: true, Section: true, Owner: true, Group: true, Node: true, Subtree: true, Language: true, State: true }
manage_locations: { Class: true , Section: true , Owner: true, Subtree: true, State: true }
hide: { Class: true, Section: true, Owner: true, Group: true, Node: true, Subtree: true, Language: true }
Expand Down
6 changes: 6 additions & 0 deletions src/lib/Resources/settings/roles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ services:
tags:
- {name: ibexa.permissions.limitation_type, alias: Status}

Ibexa\Core\Limitation\ChangeOwnerLimitationType:
arguments:
$persistence: '@ibexa.api.persistence_handler'
tags:
- { name: ibexa.permissions.limitation_type, alias: ChangeOwner }

## Non implemented Limitations
# Configured to use "blocking" limitation (as they are not implemented) to avoid LimitationNotFoundException

Expand Down
13 changes: 7 additions & 6 deletions tests/integration/Core/Repository/BaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -312,14 +312,15 @@ private function assertPropertiesEqual($propertyName, $expectedValue, $actualVal
/**
* Create a user in editor user group.
*/
protected function createUserVersion1(string $login = 'user', ?string $email = null, ContentType $contentType = null): User
{
protected function createUserVersion1(
string $login = 'user',
?string $email = null,
ContentType $contentType = null,
int $userGroupId = 13
): User {
$repository = $this->getRepository();

/* BEGIN: Inline */
// ID of the "Editors" user group in an Ibexa demo installation
$editorsGroupId = 13;

$userService = $repository->getUserService();

// Instantiate a create struct with mandatory properties
Expand All @@ -341,7 +342,7 @@ protected function createUserVersion1(string $login = 'user', ?string $email = n
}

// Load parent group for the user
$group = $userService->loadUserGroup($editorsGroupId);
$group = $userService->loadUserGroup($userGroupId);

// Create a new user instance.
$user = $userService->createUser($userCreate, [$group]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
*/
namespace Ibexa\Tests\Integration\Core\Repository\Values\User\Limitation;

use Ibexa\Contracts\Core\Repository\Values\Content\Content;
use Ibexa\Contracts\Core\Repository\Values\Content\ContentCreateStruct;
use Ibexa\Contracts\Core\Repository\Values\Content\Location;
use Ibexa\Contracts\Core\Repository\Values\Content\LocationCreateStruct;
use Ibexa\Contracts\Core\Repository\Values\User\PolicyCreateStruct;
use Ibexa\Contracts\Core\Repository\Values\User\Role;
use Ibexa\Tests\Integration\Core\Repository\BaseTest;
Expand All @@ -21,10 +24,8 @@ abstract class BaseLimitationTest extends BaseTest
{
/**
* Creates a published wiki page.
*
* @return \Ibexa\Contracts\Core\Repository\Values\Content\Content
*/
protected function createWikiPage()
protected function createWikiPage(): Content
{
$repository = $this->getRepository();

Expand All @@ -40,22 +41,35 @@ protected function createWikiPage()

/**
* Creates a fresh clean content draft.
*
* @return \Ibexa\Contracts\Core\Repository\Values\Content\Content
*/
protected function createWikiPageDraft()
protected function createWikiPageDraft(): Content
{
$repository = $this->getRepository();
$contentService = $repository->getContentService();

// $parentLocationId is the id of the /Home/Contact-Us node
$parentLocationId = $this->generateId('location', 60);
$sectionId = $this->generateId('section', 1);
/* BEGIN: Inline */
$contentTypeService = $repository->getContentTypeService();

$locationCreate = $this->createWikiPageLocationCreateStruct($parentLocationId);
$wikiPageCreate = $this->createWikiPageContentCreateStruct();

// Create a draft
$draft = $contentService->createContent(
$wikiPageCreate,
[$locationCreate]
);

return $draft;
}

/**
* Creates a basic LocationCreateStruct.
*/
protected function createWikiPageLocationCreateStruct(int $parentLocationId): LocationCreateStruct
{
$repository = $this->getRepository();
$locationService = $repository->getLocationService();
$contentService = $repository->getContentService();

// Configure new location
// $parentLocationId is the id of the /Home/Contact-Us node
$locationCreate = $locationService->newLocationCreateStruct($parentLocationId);

$locationCreate->priority = 23;
Expand All @@ -64,26 +78,45 @@ protected function createWikiPageDraft()
$locationCreate->sortField = Location::SORT_FIELD_NODE_ID;
$locationCreate->sortOrder = Location::SORT_ORDER_DESC;

return $locationCreate;
}

/**
* Creates a basic ContentCreateStruct.
*/
protected function createWikiPageContentCreateStruct(
?int $ownerId = null,
?string $remoteId = 'abcdef0123456789abcdef0123456789'
): ContentCreateStruct {
$repository = $this->getRepository();
$contentTypeService = $repository->getContentTypeService();
$contentService = $repository->getContentService();

$sectionId = $this->generateId('section', 1);

// Load content type
$wikiPageType = $contentTypeService->loadContentTypeByIdentifier('wiki_page');

// Configure new content object
$wikiPageCreate = $contentService->newContentCreateStruct($wikiPageType, 'eng-US');

$wikiPageCreate->setField('title', 'An awesome wiki page');
$wikiPageCreate->remoteId = 'abcdef0123456789abcdef0123456789';

if (null === $remoteId) {
$remoteId = md5(time());
}

$wikiPageCreate->remoteId = $remoteId;

// $sectionId is the ID of section 1
$wikiPageCreate->sectionId = $sectionId;
$wikiPageCreate->alwaysAvailable = true;

// Create a draft
$draft = $contentService->createContent(
$wikiPageCreate,
[$locationCreate]
);
/* END: Inline */
// Optional: Configure owner
if ($ownerId !== null) {
$wikiPageCreate->ownerId = $ownerId;
}

return $draft;
return $wikiPageCreate;
}

protected function addPolicyToRole(string $roleIdentifier, PolicyCreateStruct $policyCreateStruct): Role
Expand Down
Loading

0 comments on commit 2db1cbf

Please sign in to comment.