Gizra / og

https://www.drupal.org/project/og

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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,
          ]),
        ]);
      }
    }
  }

}