Access to Revisions of Group content is broken
MPParsley opened this issue · comments
Drupal core 9.3 now provides a generic access API for node and media revisions.
See https://www.drupal.org/project/drupal/issues/3043321
This breaks the access to node revisions routes.
See also https://www.drupal.org/project/group/issues/3256998.
@pfrenssen, we should probably add GroupPermission (view all permissions) & GroupContentOperationPermission (view any $bundle_id content revisions)?
Yes this is a good suggestion and something we should do. I was actually working on this recently but I was not able to finish this, and I have since moved to a new project. The new project is not using OG unfortunately so I will not have time to work on this any more. I will attach my work in progress.
<?php
declare(strict_types = 1);
namespace Drupal\group_content\EventSubscriber;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\og\Event\GroupContentEntityOperationAccessEventInterface;
use Drupal\og\Event\PermissionEventInterface as OgPermissionEventInterface;
use Drupal\og\GroupContentOperationPermission;
use Drupal\og\GroupPermission;
use Drupal\og\OgAccessInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Event subscribers for group content.
*/
class EventSubscriber implements EventSubscriberInterface {
use StringTranslationTrait;
/**
* The service providing information about bundles.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected EntityTypeBundleInfoInterface $entityTypeBundleInfo;
/**
* The currently logged in user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected AccountInterface $currentUser;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* The OG access handler.
*
* @var \Drupal\og\OgAccessInterface
*/
protected OgAccessInterface $ogAccess;
/**
* Constructs an EventSubscriber object.
*
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The service providing information about bundles.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current logged in user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\og\OgAccessInterface $og_access
* The OG access handler.
*/
public function __construct(
EntityTypeBundleInfoInterface $entity_type_bundle_info,
AccountInterface $current_user,
EntityTypeManagerInterface $entity_type_manager,
OgAccessInterface $og_access
) {
$this->entityTypeBundleInfo = $entity_type_bundle_info;
$this->currentUser = $current_user;
$this->entityTypeManager = $entity_type_manager;
$this->ogAccess = $og_access;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
GroupContentEntityOperationAccessEventInterface::EVENT_NAME => ['checkEntityRevisionAccess', 10],
OgPermissionEventInterface::EVENT_NAME => [['provideOgRevisionPermissions']],
];
}
/**
* Determines access to entity revisions for group content.
*
* @param \Drupal\og\Event\GroupContentEntityOperationAccessEventInterface $event
* The event fired when a group content entity operation is performed.
*/
public function checkEntityRevisionAccess(GroupContentEntityOperationAccessEventInterface $event): void {
$content = $event->getGroupContent();
$event->addCacheableDependency($content);
// Do a quick exit if the event is not for the entity operations we handle.
$operation = $event->getOperation();
if (!in_array($operation, ['view all revisions', 'revert revision'])) {
return;
}
$bundle = $content->bundle();
$bundle_permission = "view $bundle revisions";
// There should be at least two revisions. If the vid of the given node
// and the vid of the default revision differ, then we already have two
// different revisions so there is no need for a separate database check.
// @todo This used to be the default behaviour in Drupal but this has since
// been changed. We should change this so it aligns with core behavior.
// @see https://www.drupal.org/node/3001224
$node_storage = $this->entityTypeManager->getStorage('node');
if ($content->isDefaultRevision() && ($node_storage->countDefaultLanguageRevisions($content) == 1)) {
$event->denyAccess();
return;
}
// The global "administer nodes" permissions gives full access to revisions.
// @see parent::checkAccess()
$account = $event->getUser();
if ($account->hasPermission('administer nodes')) {
$event->grantAccess();
$event->stopPropagation();
return;
}
// Check if the user has group level permission to view all revisions, or
// view revisions for the bundle of the group content.
$group = $event->getGroup();
$result = $this->ogAccess->userAccess($group, 'view all revisions', $account)
->orIf($this->ogAccess->userAccess($group, $bundle_permission, $account));
// If the user owns the entity, check if they can 'view own revisions'.
if (!$result->isAllowed() && (int) $content->getOwnerId() === (int) $account->id()) {
$result = $result->orIf($this->ogAccess->userAccess($group, 'view own revisions', $account));
}
// If neither of the access checks are allowed, we have no opinion.
if (!$result->isAllowed()) {
return;
}
// Check if the access to the default revision and finally, if the
// node passed in is not the default revision then access to that, too.
$node_access = $this->entityTypeManager->getAccessControlHandler('node');
$result = $result->andIf($node_access->access($node_storage->load($content->id()), 'view', $account, TRUE));
if (!$content->isDefaultRevision()) {
$result = $result->andIf($node_access->access($content, 'view', $account, TRUE));
}
if ($result->isAllowed()) {
$event->grantAccess();
}
}
/**
* Declare OG permissions for handling revisions.
*
* @param \Drupal\og\Event\PermissionEventInterface $event
* The OG permission event.
*/
public function provideOgRevisionPermissions(OgPermissionEventInterface $event) {
$group_content_bundle_ids = $event->getGroupContentBundleIds();
if (!empty($group_content_bundle_ids['node'])) {
// Add a global permission that allows to access all the revisions.
$event->setPermissions([
new GroupPermission([
'name' => 'view all revisions',
'title' => $this->t('View all revisions'),
'restrict access' => TRUE,
]),
new GroupPermission([
'name' => 'view own revisions',
'title' => $this->t('View own revisions'),
'restrict access' => TRUE,
]),
new GroupPermission([
'name' => 'revert all revisions',
'title' => $this->t('Revert all revisions'),
'restrict access' => TRUE,
]),
new GroupPermission([
'name' => 'delete all revisions',
'title' => $this->t('Delete all revisions'),
'restrict access' => TRUE,
]),
]);
$bundle_info = $this->entityTypeBundleInfo->getBundleInfo('node');
foreach ($group_content_bundle_ids['node'] as $bundle_id) {
$bundle_label = $bundle_info[$bundle_id]['label'];
$event->setPermissions([
new GroupContentOperationPermission([
'name' => "view $bundle_id revisions",
'title' => $this->t('%bundle: View revisions', ['%bundle' => $bundle_label]),
'operation' => 'view revision',
'entity type' => 'node',
'bundle' => $bundle_id,
]),
new GroupContentOperationPermission([
'name' => "revert $bundle_id revisions",
'title' => $this->t('%bundle: Revert revisions', ['%bundle' => $bundle_label]),
'operation' => 'revert revision',
'entity type' => 'node',
'bundle' => $bundle_id,
]),
new GroupContentOperationPermission([
'name' => "delete $bundle_id revisions",
'title' => $this->t('%bundle: Delete revisions', ['%bundle' => $bundle_label]),
'operation' => 'delete revision',
'entity type' => 'node',
'bundle' => $bundle_id,
]),
]);
}
}
}
}