Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EZP-30997: Fixed permission checks when copying translations during publishing #2858

Merged
merged 11 commits into from
Dec 5, 2019
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
*/
declare(strict_types=1);

namespace eZ\Publish\API\Repository\Tests\Limitation;
namespace eZ\Publish\API\Repository\Tests\Values\User\Limitation;

use eZ\Publish\API\Repository\ContentService;
use eZ\Publish\API\Repository\Exceptions\UnauthorizedException;
use eZ\Publish\API\Repository\Tests\BaseTest;
use eZ\Publish\API\Repository\Values\Content\Content;
use eZ\Publish\API\Repository\Values\User\Limitation\LanguageLimitation;
use eZ\Publish\API\Repository\Values\User\User;

Expand All @@ -24,26 +26,38 @@
*/
class LanguageLimitationTest extends BaseTest
{
/** @var string */
private const ENG_US = 'eng-US';

/** @var string */
private const ENG_GB = 'eng-GB';

/** @var string */
private const GER_DE = 'ger-DE';

/**
* Create editor who is allowed to modify only specific translations of a Content item.
*
* @param array $allowedTranslationsList list of translations (language codes) which editor can modify.
* @param string $login
*
* @return \eZ\Publish\API\Repository\Values\User\User
*
* @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
* @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
*/
private function createEditorUserWithLanguageLimitation(array $allowedTranslationsList): User
{
private function createEditorUserWithLanguageLimitation(
array $allowedTranslationsList,
string $login = 'editor'
): User {
$limitations = [
// limitation for specific translations
new LanguageLimitation(['limitationValues' => $allowedTranslationsList]),
];

return $this->createUserWithPolicies(
'editor',
$login,
[
['module' => 'content', 'function' => 'read'],
['module' => 'content', 'function' => 'versionread'],
Expand Down Expand Up @@ -235,4 +249,157 @@ public function testPublishVersionIsNotAllowedIfModifiedOtherTranslations(array
$this->expectException(UnauthorizedException::class);
$contentService->publishVersion($folderDraft->getVersionInfo());
}

/**
* @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
* @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
*/
public function testPublishVersionTranslation(): void
mikadamczyk marked this conversation as resolved.
Show resolved Hide resolved
{
$repository = $this->getRepository();
$contentService = $repository->getContentService();
$permissionResolver = $repository->getPermissionResolver();

$draft = $this->createMultilingualFolderDraft($contentService);

$contentUpdateStruct = $contentService->newContentUpdateStruct();

$contentUpdateStruct->setField('name', 'Draft 1 DE', self::GER_DE);

$contentService->updateContent($draft->versionInfo, $contentUpdateStruct);

$admin = $permissionResolver->getCurrentUserReference();
$permissionResolver->setCurrentUserReference($this->createEditorUserWithLanguageLimitation([self::GER_DE]));

$contentService->publishVersion($draft->versionInfo, [self::GER_DE]);

$permissionResolver->setCurrentUserReference($admin);
$content = $contentService->loadContent($draft->contentInfo->id);
$this->assertEquals(
[
self::ENG_US => 'Published US',
self::GER_DE => 'Draft 1 DE',
],
$content->fields['name']
);
}

/**
* @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
* @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
*/
public function testPublishVersionTranslationIsNotAllowed(): void
mikadamczyk marked this conversation as resolved.
Show resolved Hide resolved
{
$repository = $this->getRepository();
$contentService = $repository->getContentService();
$permissionResolver = $repository->getPermissionResolver();

$draft = $this->createMultilingualFolderDraft($contentService);

$contentUpdateStruct = $contentService->newContentUpdateStruct();

$contentUpdateStruct->setField('name', 'Draft 1 EN', self::ENG_US);

$contentService->updateContent($draft->versionInfo, $contentUpdateStruct);

$permissionResolver->setCurrentUserReference($this->createEditorUserWithLanguageLimitation([self::GER_DE]));

$this->expectException(UnauthorizedException::class);
$contentService->publishVersion($draft->versionInfo, [self::ENG_US]);
}

/**
* @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
* @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
*/
public function testPublishVersionTranslationIsNotAllowedWithTwoEditors(): void
mikadamczyk marked this conversation as resolved.
Show resolved Hide resolved
mikadamczyk marked this conversation as resolved.
Show resolved Hide resolved
{
$repository = $this->getRepository();
$contentService = $repository->getContentService();
$permissionResolver = $repository->getPermissionResolver();

$editorDE = $this->createEditorUserWithLanguageLimitation([self::GER_DE], 'editor-de');
$editorUS = $this->createEditorUserWithLanguageLimitation([self::ENG_US], 'editor-us');

// German editor publishes content in German language
$permissionResolver->setCurrentUserReference($editorDE);

$folder = $this->createFolder([self::GER_DE => 'German Folder'], 2);

// American editor creates and saves English draft
$permissionResolver->setCurrentUserReference($editorUS);

$folder = $contentService->loadContent($folder->id);
$folderDraft = $contentService->createContentDraft($folder->contentInfo);
$folderUpdateStruct = $contentService->newContentUpdateStruct();
$folderUpdateStruct->setField('name', 'English Folder', self::ENG_US);
$folderDraft = $contentService->updateContent(
$folderDraft->versionInfo,
$folderUpdateStruct
);

// German editor tries to publish English translation
$permissionResolver->setCurrentUserReference($editorDE);
$folderDraftVersionInfo = $contentService->loadVersionInfo(
$folderDraft->contentInfo,
$folderDraft->versionInfo->versionNo
);
self::assertTrue($folderDraftVersionInfo->isDraft());
$this->expectException(UnauthorizedException::class);
$this->expectExceptionMessage("User does not have access to 'publish' 'content'");
$contentService->publishVersion($folderDraftVersionInfo, [self::ENG_US]);
}

/**
* @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
* @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
*/
public function testPublishVersionTranslationWhenUserHasNoAccessToAllLanguages(): void
mikadamczyk marked this conversation as resolved.
Show resolved Hide resolved
{
$repository = $this->getRepository();
$contentService = $repository->getContentService();
$permissionResolver = $repository->getPermissionResolver();

$draft = $this->createMultilingualFolderDraft($contentService);

$contentUpdateStruct = $contentService->newContentUpdateStruct();

$contentUpdateStruct->setField('name', 'Draft 1 DE', self::GER_DE);
$contentUpdateStruct->setField('name', 'Draft 1 GB', self::ENG_GB);

$contentService->updateContent($draft->versionInfo, $contentUpdateStruct);

$permissionResolver->setCurrentUserReference(
$this->createEditorUserWithLanguageLimitation([self::GER_DE])
);
$this->expectException(UnauthorizedException::class);
$this->expectExceptionMessage("User does not have access to 'publish' 'content'");
$contentService->publishVersion($draft->versionInfo, [self::GER_DE, self::ENG_GB]);
}

/**
* @param \eZ\Publish\API\Repository\ContentService $contentService
*
* @return \eZ\Publish\API\Repository\Values\Content\Content
*
* @throws \eZ\Publish\API\Repository\Exceptions\ForbiddenException
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
* @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
mikadamczyk marked this conversation as resolved.
Show resolved Hide resolved
*/
private function createMultilingualFolderDraft(ContentService $contentService): Content
{
$publishedContent = $this->createFolder(
[
self::ENG_US => 'Published US',
self::GER_DE => 'Published DE',
],
$this->generateId('location', 2)
);

return $contentService->createContentDraft($publishedContent->contentInfo);
}
}
6 changes: 6 additions & 0 deletions eZ/Publish/Core/Limitation/LanguageLimitationType.php
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,12 @@ private function evaluateVersionTarget(
}
}

// intent to publish Version in specified languages
if (!empty($version->forPublishLanguageCodesList) || null !== $version->forPublishLanguageCodesList) {
$diff = array_diff($version->forPublishLanguageCodesList, $value->limitationValues);
$accessVote = empty($diff) ? self::ACCESS_GRANTED : self::ACCESS_DENIED;
}

return $accessVote;
}

Expand Down
78 changes: 55 additions & 23 deletions eZ/Publish/Core/Repository/ContentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use eZ\Publish\API\Repository\Values\Content\RelationList\Item\RelationListItem;
use eZ\Publish\API\Repository\Values\Content\RelationList\Item\UnauthorizedRelationListItem;
use eZ\Publish\API\Repository\Values\User\UserReference;
use eZ\Publish\Core\Repository\Values\Content\Content;
use eZ\Publish\Core\Repository\Values\Content\Location;
use eZ\Publish\API\Repository\Values\Content\Language;
use eZ\Publish\SPI\Persistence\Handler;
Expand Down Expand Up @@ -1267,20 +1268,12 @@ public function loadContentDraftList(?User $user = null, int $offset = 0, int $l
*/
public function updateContent(APIVersionInfo $versionInfo, APIContentUpdateStruct $contentUpdateStruct)
{
$contentUpdateStruct = clone $contentUpdateStruct;

/** @var $content \eZ\Publish\Core\Repository\Values\Content\Content */
$content = $this->loadContent(
$versionInfo->getContentInfo()->id,
null,
$versionInfo->versionNo
);
if (!$content->versionInfo->isDraft()) {
throw new BadStateException(
'$versionInfo',
'Version is not a draft and can not be updated'
);
}

if (!$this->repository->getPermissionResolver()->canUser(
'content',
Expand All @@ -1298,6 +1291,39 @@ public function updateContent(APIVersionInfo $versionInfo, APIContentUpdateStruc
throw new UnauthorizedException('content', 'edit', ['contentId' => $content->id]);
}

return $this->internalUpdateContent($versionInfo, $contentUpdateStruct);
}

/**
* Updates the fields of a draft without checking the permissions.
*
* @throws \eZ\Publish\API\Repository\Exceptions\ContentFieldValidationException if a field in the $contentCreateStruct is not valid,
* or if a required field is missing / set to an empty value.
* @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException If field definition does not exist in the ContentType,
* or value is set for non-translatable field in language
* other than main.
*
* @throws \eZ\Publish\API\Repository\Exceptions\BadStateException if the version is not a draft
* @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if a property on the struct is invalid.
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
*/
protected function internalUpdateContent(APIVersionInfo $versionInfo, APIContentUpdateStruct $contentUpdateStruct): Content
{
$contentUpdateStruct = clone $contentUpdateStruct;

/** @var $content \eZ\Publish\Core\Repository\Values\Content\Content */
$content = $this->internalLoadContent(
$versionInfo->getContentInfo()->id,
null,
$versionInfo->versionNo
);
if (!$content->versionInfo->isDraft()) {
throw new BadStateException(
'$versionInfo',
'Version is not a draft and can not be updated'
);
}

$mainLanguageCode = $content->contentInfo->mainLanguageCode;
if ($contentUpdateStruct->initialLanguageCode === null) {
$contentUpdateStruct->initialLanguageCode = $mainLanguageCode;
Expand Down Expand Up @@ -1425,7 +1451,7 @@ public function updateContent(APIVersionInfo $versionInfo, APIContentUpdateStruc
)->id,
]
);
$existingRelations = $this->loadRelations($versionInfo);
$existingRelations = $this->internalLoadRelations($versionInfo);

$this->repository->beginTransaction();
try {
Expand Down Expand Up @@ -1576,23 +1602,18 @@ public function publishVersion(APIVersionInfo $versionInfo, array $translations
$versionInfo->versionNo
);

$fromContent = null;
if ($content->contentInfo->currentVersionNo !== $versionInfo->versionNo) {
$fromContent = $this->internalLoadContent(
$content->contentInfo->id,
null,
$content->contentInfo->currentVersionNo
);
// should not occur now, might occur in case of un-publish
if (!$fromContent->contentInfo->isPublished()) {
$fromContent = null;
}
$targets = [];
if (!empty($translations)) {
$targets[] = (new Target\Builder\VersionBuilder())
->publishTranslations($translations)
->build();
}

if (!$this->repository->getPermissionResolver()->canUser(
'content',
'publish',
$content
$content,
$targets
)) {
throw new UnauthorizedException(
'content', 'publish', ['contentId' => $content->id]
Expand Down Expand Up @@ -1621,7 +1642,6 @@ public function publishVersion(APIVersionInfo $versionInfo, array $translations
* @throws \eZ\Publish\API\Repository\Exceptions\ContentValidationException
* @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
* @throws \eZ\Publish\API\Repository\Exceptions\UnauthorizedException
*/
protected function copyTranslationsFromPublishedVersion(APIVersionInfo $versionInfo, array $translations = []): void
{
Expand Down Expand Up @@ -1670,7 +1690,7 @@ protected function copyTranslationsFromPublishedVersion(APIVersionInfo $versionI
}
}

$this->updateContent($versionInfo, $updateStruct);
$this->internalUpdateContent($versionInfo, $updateStruct);
pawbuj marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -1929,6 +1949,18 @@ public function loadRelations(APIVersionInfo $versionInfo)
throw new UnauthorizedException('content', $function);
}

return $this->internalLoadRelations($versionInfo);
}

/**
* Loads all outgoing relations for the given version without checking the permissions.
*
* @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
*
* @return \eZ\Publish\API\Repository\Values\Content\Relation[]
*/
protected function internalLoadRelations(APIVersionInfo $versionInfo): array
{
$contentInfo = $versionInfo->getContentInfo();
$spiRelations = $this->persistenceHandler->contentHandler()->loadRelations(
$contentInfo->id,
Expand Down
Loading