Skip to content
Snippets Groups Projects
Commit 1ee38dc2 authored by Kristiaan Van den Eynde's avatar Kristiaan Van den Eynde
Browse files

Issue #3256998 by msnassar, lbodiguel, kristiaanvandeneynde, coderdan,...

Issue #3256998 by msnassar, lbodiguel, kristiaanvandeneynde, coderdan, sitiveni, naveenvalecha, Billodeau: Access to Revisions of Group Content is Broken
parent 81a22ee5
No related branches found
No related tags found
3 merge requests!207Issue #3497467 by vasyapledov: Take gid from the route instead of Group for cache context 'route.group',!193Issue #3304728 by kristiaanvandeneynde: Add member page missing due to...,!142Issue #3304728 by kristiaanvandeneynde: Add member page missing due to...
Showing with 419 additions and 2 deletions
name: 'Group Revision Support'
description: 'Enables revision access over grouped entities to be determined by the group'
package: 'Group'
type: 'module'
core_version_requirement: ^9.1 || ^10
dependencies:
- 'group:group'
services:
# Decorating group relation handlers.
group.relation_handler_decorator.permission_provider.revision_support:
class: 'Drupal\group_revision_support\Plugin\Group\RelationHandler\RevisionSupportPermissionProvider'
decorates: 'group.relation_handler.permission_provider'
decoration_priority: 50
arguments: ['@group.relation_handler_decorator.permission_provider.revision_support.inner']
shared: false
<?php
namespace Drupal\group_revision_support\Plugin\Group\RelationHandler;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\group\Plugin\Group\Relation\GroupRelationTypeInterface;
use Drupal\group\Plugin\Group\RelationHandler\PermissionProviderInterface;
use Drupal\group\Plugin\Group\RelationHandler\PermissionProviderTrait;
/**
* Alters all permission providers to add revision support.
*/
class RevisionSupportPermissionProvider implements PermissionProviderInterface {
use PermissionProviderTrait {
init as defaultInit;
}
/**
* Whether the target entity type implements the RevisionableInterface.
*
* @var bool
*/
protected bool $implementsRevisionableInterface;
/**
* Constructs a new RevisionSupportPermissionProvider.
*
* @param \Drupal\group\Plugin\Group\RelationHandler\PermissionProviderInterface $parent
* The parent permission provider.
*/
public function __construct(PermissionProviderInterface $parent) {
$this->parent = $parent;
}
/**
* {@inheritdoc}
*/
public function init($plugin_id, GroupRelationTypeInterface $group_relation_type) {
$this->defaultInit($plugin_id, $group_relation_type);
$this->implementsRevisionableInterface = $this->entityType->entityClassImplements(RevisionableInterface::class);
}
/**
* {@inheritdoc}
*/
public function getPermission($operation, $target, $scope = 'any') {
if ($target === 'entity') {
switch ($operation) {
case 'view all revisions':
return $this->getEntityViewAllRevisionsPermission();
case 'view revision':
return $this->getEntityViewRevisionPermission();
case 'revert revision':
return $this->getEntityRevertRevisionPermission();
case 'delete revision':
return $this->getEntityDeleteRevisionPermission();
}
}
return $this->parent->getPermission($operation, $target, $scope);
}
/**
* {@inheritdoc}
*/
public function buildPermissions() {
$permissions = $this->parent->buildPermissions();
// Instead of checking whether this specific permission provider allows for
// a permission to exist, we check the entire decorator chain. This avoids a
// lot of copy-pasted code to turn off or rename a permission in a decorator
// further down the chain.
$provider_chain = $this->groupRelationTypeManager()->getPermissionProvider($this->pluginId);
$prefix = 'Revisions:';
if ($name = $provider_chain->getPermission('view all revisions', 'entity')) {
$permissions[$name] = $this->buildPermission("$prefix View full version history");
}
if ($name = $provider_chain->getPermission('view revision', 'entity')) {
$permissions[$name] = $this->buildPermission("$prefix View specific entity revisions");
}
if ($name = $provider_chain->getPermission('revert revision', 'entity')) {
$permissions[$name] = $this->buildPermission("$prefix Revert specific entity revisions");
}
if ($name = $provider_chain->getPermission('delete revision', 'entity')) {
$permissions[$name] = $this->buildPermission("$prefix Delete specific entity revisions");
}
return $permissions;
}
/**
* Gets the name of the view all revisions permission for the entity.
*
* @return string|false
* The permission name or FALSE if it does not apply.
*/
protected function getEntityViewAllRevisionsPermission() {
if ($this->definesEntityPermissions && $this->implementsRevisionableInterface) {
return "view all $this->pluginId entity revisions";
}
return FALSE;
}
/**
* Gets the name of the view all revisions permission for the entity.
*
* @return string|false
* The permission name or FALSE if it does not apply.
*/
protected function getEntityViewRevisionPermission() {
if ($this->definesEntityPermissions && $this->implementsRevisionableInterface) {
return "view $this->pluginId entity revisions";
}
return FALSE;
}
/**
* Gets the name of the view all revisions permission for the entity.
*
* @return string|false
* The permission name or FALSE if it does not apply.
*/
protected function getEntityRevertRevisionPermission() {
if ($this->definesEntityPermissions && $this->implementsRevisionableInterface) {
return "revert $this->pluginId entity revisions";
}
return FALSE;
}
/**
* Gets the name of the view all revisions permission for the entity.
*
* @return string|false
* The permission name or FALSE if it does not apply.
*/
protected function getEntityDeleteRevisionPermission() {
if ($this->definesEntityPermissions && $this->implementsRevisionableInterface) {
return "delete $this->pluginId entity revisions";
}
return FALSE;
}
}
<?php
namespace Drupal\Tests\group_revision_support\Functional;
use Drupal\group\Entity\Storage\GroupRelationshipTypeStorageInterface;
use Drupal\group\PermissionScopeInterface;
use Drupal\node\NodeInterface;
use Drupal\Tests\group\Functional\GroupBrowserTestBase;
use Drupal\user\RoleInterface;
/**
* Tests that revision operations (do not) show up on a grouped entity.
*
* @group group_revision_support
*/
class GroupRevisionSupportTest extends GroupBrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'gnode'];
/**
* Gets the global (site) permissions for the group creator.
*
* @return string[]
* The permissions.
*/
protected function getGlobalPermissions() {
return [
'access content',
'edit any page content',
'delete any page content',
'view all revisions',
'revert all revisions',
'delete all revisions',
'access administration pages',
] + parent::getGlobalPermissions();
}
/**
* The group type to run the tests with.
*
* @var \Drupal\group\Entity\GroupTypeInterface
*/
protected $groupType;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Page', 'new_revision' => TRUE]);
$this->drupalPlaceBlock('local_tasks_block');
$this->setUpAccount();
$this->groupType = $this->createGroupType();
$storage = $this->entityTypeManager->getStorage('group_content_type');
assert($storage instanceof GroupRelationshipTypeStorageInterface);
$storage->save($storage->createFromPlugin($this->groupType, 'group_node:page'));
$group_role_storage = $this->entityTypeManager->getStorage('group_role');
$group_role_storage->save($group_role_storage->create([
'id' => 'foo',
'group_type' => $this->groupType->id(),
'scope' => PermissionScopeInterface::INSIDER_ID,
'global_role' => RoleInterface::AUTHENTICATED_ID,
'permissions' => [],
]));
}
/**
* Tests the revisions tab on an entity's canonical route.
*/
public function testRevisionsTab(): void {
$group_role_storage = $this->entityTypeManager->getStorage('group_role');
$node = $this->drupalCreateNode(['type' => 'page', 'title' => $this->randomString()]);
$path = '/node/1';
$href = '/node/1/revisions';
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefExists($href, 0, 'Control; "Revisions" tab shows up.');
$group_role = $group_role_storage->load('foo');
$group_role_storage->save($group_role->grantPermission('view group_node:page entity'));
$group = $this->createGroup(['type' => $this->groupType->id()]);
$group->addRelationship($node, 'group_node:page');
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefExists($href, 0, 'Grouping the node without any special support still shows the "Revisions" tab.');
\Drupal::getContainer()->get('module_installer')->install(['group_revision_support'], TRUE);
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefNotExists($href, 'Now hat Group knows about revision operations, the grouped node no longer shows the "Revisions" tab.');
$group_role = $group_role_storage->load('foo');
$group_role_storage->save($group_role->grantPermission('view all group_node:page entity revisions'));
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefExists($href, 0, 'Assigning the right Group permissions once again shows the "Revisions" tab.');
}
/**
* Tests the viewing of individual revisions for an entity.
*
* @depends testRevisionsTab
*/
public function testViewRevision(): void {
$group_role_storage = $this->entityTypeManager->getStorage('group_role');
$node_storage = $this->entityTypeManager->getStorage('node');
$path = '/node/1/revisions/1/view';
$node = $this->drupalCreateNode(['type' => 'page', 'title' => 'First title']);
$node = $node_storage->load($node->id());
assert($node instanceof NodeInterface);
$node->setNewRevision();
$node_storage->save($node->setTitle('Second title'));
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
$group_role = $group_role_storage->load('foo');
$group_role_storage->save($group_role->grantPermission('view group_node:page entity'));
$group = $this->createGroup(['type' => $this->groupType->id()]);
$group->addRelationship($node, 'group_node:page');
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
\Drupal::getContainer()->get('module_installer')->install(['group_revision_support'], TRUE);
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(403);
$group_role = $group_role_storage->load('foo');
$group_role_storage->save($group_role->grantPermission('view group_node:page entity revisions'));
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
}
/**
* Tests the revision operations on an entity's version history route.
*
* @param string $name
* The name of the operation we expect to see.
* @param string $href
* The href of the operation we expect to see.
* @param string $crud_permission
* The permission of the same CRUD operation, required by core.
* @param string $group_permission
* The group permission that should grant access when grouped and supported.
*
* @depends testRevisionsTab
* @dataProvider revisionsOperationsProvider
*/
public function testRevisionOperations(string $name, string $href, string $crud_permission, string $group_permission): void {
$group_role_storage = $this->entityTypeManager->getStorage('group_role');
$node_storage = $this->entityTypeManager->getStorage('node');
$path = '/node/1/revisions';
$node = $this->drupalCreateNode(['type' => 'page', 'title' => 'First title']);
$node = $node_storage->load($node->id());
assert($node instanceof NodeInterface);
$node->setNewRevision();
$node_storage->save($node->setTitle('Second title'));
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefExists($href, 0, 'Control; "' . $name . '" operation shows up.');
$group_role = $group_role_storage->load('foo');
$group_role_storage->save($group_role->grantPermissions([
'view group_node:page entity',
'view all group_node:page entity revisions',
// Standard revision access checks rely on update or delete access.
$crud_permission,
]));
$group = $this->createGroup(['type' => $this->groupType->id()]);
$group->addRelationship($node, 'group_node:page');
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefExists($href, 0, 'Grouping the node without any special support still shows the "' . $name . '" operation.');
\Drupal::getContainer()->get('module_installer')->install(['group_revision_support'], TRUE);
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefNotExists($href, 'Now hat Group knows about revision operations, the grouped node no longer shows the "' . $name . '" operation.');
$group_role = $group_role_storage->load('foo');
$group_role_storage->save($group_role->grantPermission($group_permission));
$this->drupalGet($path);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->linkByHrefExists($href, 0, 'Assigning the right Group permissions once again shows the "' . $name . '" operation.');
}
/**
* Data provider for ::testRevisionOperations().
*
* @return array
* A list of test scenarios.
*/
public function revisionsOperationsProvider(): array {
$cases['revert'] = [
'name' => 'Revert',
'href' => 'node/1/revisions/1/revert',
'crud_permission' => 'update any group_node:page entity',
'group_permission' => 'revert group_node:page entity revisions',
];
$cases['delete'] = [
'name' => 'Delete',
'href' => 'node/1/revisions/1/delete',
'crud_permission' => 'delete any group_node:page entity',
'group_permission' => 'delete group_node:page entity revisions',
];
return $cases;
}
}
......@@ -14,6 +14,14 @@ use Drupal\user\RoleInterface;
*/
class EntityOperationsTest extends GroupBrowserTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpAccount();
}
/**
* Checks for entity operations under given circumstances.
*
......
......@@ -42,7 +42,12 @@ abstract class GroupBrowserTestBase extends BrowserTestBase {
// Make sure we do not use user 1.
$this->createUser();
}
/**
* Sets up the Drupal account.
*/
protected function setUpAccount(): void {
// Create a user that will serve as the group creator.
$this->groupCreator = $this->createUser($this->getGlobalPermissions());
$this->drupalLogin($this->groupCreator);
......
......@@ -9,6 +9,14 @@ namespace Drupal\Tests\group\Functional;
*/
class GroupCreatorWizardTest extends GroupBrowserTestBase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setUpAccount();
}
/**
* Tests that a group creator gets a membership using the wizard.
*/
......
......@@ -31,6 +31,7 @@ class GroupRoleFormTest extends GroupBrowserTestBase {
*/
protected function setUp(): void {
parent::setUp();
$this->setUpAccount();
$this->groupRoleStorage = $this->entityTypeManager->getStorage('group_role');
......
......@@ -37,6 +37,8 @@ class GroupTypeFormTest extends GroupBrowserTestBase {
*/
protected function setUp(): void {
parent::setUp();
$this->setUpAccount();
$this->entityFieldManager = $this->container->get('entity_field.manager');
$this->commonValues = [
'Name' => 'My first group type',
......
......@@ -34,8 +34,6 @@ class PageCacheTest extends GroupBrowserTestBase {
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogout();
$this->groupType = $this->createGroupType(['creator_membership' => FALSE]);
$this->group = $this->createGroup(['type' => $this->groupType->id()]);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment