Commit eb00599a authored by Navneet Singh's avatar Navneet Singh Committed by Navneet Singh
Browse files

Merge pull request #3109 from goalgorilla/feature/3309659-entity-access-checker-for-any-entity-type

Issue #3309659 by chmez: Extend entity access checker to cover all entity types
parent 0f94fb82
Loading
Loading
Loading
Loading
+13 −3
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@
 * @todo Add support for multiple entity types.
 */

use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityFormInterface;
use Drupal\Core\Entity\EntityInterface;
@@ -25,10 +26,19 @@ use Drupal\social_post\Entity\PostInterface;
const ENTITY_ACCESS_BY_FIELD_ALLOWED_REALM = 1;

/**
 * Implements hook_node_access().
 * Implements hook_ENTITY_TYPE_access().
 */
function entity_access_by_field_node_access(NodeInterface $node, $op, AccountInterface $account) {
  return EntityAccessHelper::getEntityAccessResult($node, $op, $account);
function entity_access_by_field_node_access(
  EntityInterface $entity,
  string $operation,
  AccountInterface $account
): AccessResultInterface {
  return EntityAccessHelper::getEntityAccessResult(
    $entity,
    $operation,
    $account,
    'administer nodes',
  );
}

/**
+167 −84
Original line number Diff line number Diff line
@@ -2,12 +2,15 @@

namespace Drupal\entity_access_by_field;

use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\node\NodeInterface;
use Drupal\group\Entity\GroupContent;
use Drupal\group\Entity\Group;
use Drupal\social_event\EventEnrollmentInterface;
use Drupal\user\EntityOwnerInterface;

/**
 * Helper class for checking entity access.
@@ -42,116 +45,196 @@ class EntityAccessHelper {
  }

  /**
   * NodeAccessCheck for given operation, node and user account.
   * EntityAccessCheck for given operation, entity and user account.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to check access to.
   * @param string $operation
   *   The operation that is to be performed on $entity. Usually one of:
   *   - "view".
   *   - "update".
   *   - "delete".
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The account trying to access the entity.
   * @param string|null $permission
   *   (optional) The admin permission. Defaults to NULL.
   */
  public static function nodeAccessCheck(NodeInterface $node, $op, AccountInterface $account) {
    if ($op === 'view') {
  public static function entityAccessCheck(
    EntityInterface $entity,
    string $operation,
    AccountInterface $account,
    string $permission = NULL
  ): int {
    if ($operation !== 'view' || !$entity instanceof EntityOwnerInterface) {
      return self::NEUTRAL;
    }

    // Check published status.
      if (isset($node->status) && (int) $node->status->value === NodeInterface::NOT_PUBLISHED) {
        $unpublished_own = $account->hasPermission('view own unpublished content');
    if (isset($entity->status) && !$entity->status->value) {
      if ($entity->getOwnerId() === $account->id()) {
        if (!$account->hasPermission('view own unpublished content')) {
          return self::FORBIDDEN;
        }
      }
      else {
        if ($permission === NULL) {
          $definition = \Drupal::entityTypeManager()->getDefinition(
            $entity->getEntityTypeId(),
          );

          if ($definition !== NULL) {
            $permission = $definition->getAdminPermission();
          }
        }

        if (
          ($node->getOwnerId() !== $account->id() && !$account->hasPermission('administer nodes')) ||
          ($node->getOwnerId() === $account->id() && !$unpublished_own)
          !empty($permission) &&
          is_string($permission) &&
          !$account->hasPermission($permission)
        ) {
          return EntityAccessHelper::FORBIDDEN;
          return self::FORBIDDEN;
        }
      }
    }

      $field_definitions = $node->getFieldDefinitions();
    if (!$entity instanceof FieldableEntityInterface) {
      return self::NEUTRAL;
    }

      /** @var \Drupal\Core\Field\FieldConfigInterface $field_definition */
      foreach ($field_definitions as $field_name => $field_definition) {
        if ($field_definition->getType() === 'entity_access_field') {
          $field_values = $node->get($field_name)->getValue();
    $field_definitions = $entity->getFieldDefinitions();
    $access = TRUE;

          if (!empty($field_values)) {
            foreach ($field_values as $field_value) {
              if (isset($field_value['value'])) {
    foreach ($field_definitions as $field_name => $field_definition) {
      if (
        $field_definition->getType() !== 'entity_access_field' ||
        ($field = $entity->get($field_name))->isEmpty()
      ) {
        continue;
      }

                if (in_array($field_value['value'], EntityAccessHelper::getIgnoredValues())) {
                  return EntityAccessHelper::NEUTRAL;
      foreach (array_column($field->getValue(), 'value') as $field_value) {
        if (in_array($field_value, EntityAccessHelper::getIgnoredValues())) {
          return self::NEUTRAL;
        }

                $permission_label = "node.{$node->bundle()}.{$field_definition->getName()}:{$field_value['value']}";
        $permission = sprintf(
          'view %s.%s.%s:%s content',
          $entity->getEntityTypeId(),
          $entity->bundle(),
          $field_definition->getName(),
          $field_value,
        );

                // When content is posted in a group and the account does not
                // have permission we return Access::ignore.
                if ($field_value['value'] === 'group') {
        // When content is posted in a group and the account does not have
        // permission we return Access::ignore.
        if ($field_value === 'group') {
          // Don't look no further.
          if ($account->hasPermission('manage all groups')) {
                    return EntityAccessHelper::NEUTRAL;
                  }
                  elseif (!$account->hasPermission('view ' . $permission_label . ' content')) {
                    // If user doesn't have permission we just check user
                    // membership in groups where the node attached as
                    // group content.
                    $group_contents = GroupContent::loadByEntity($node);
                    // Check recursively - if user is a member at least in one
                    // group we should allow to check access by gnode module.
                    /* @see gnode_node_access() */
                    foreach ($group_contents as $group_content) {
                      $group = $group_content->getGroup();
                      if ($group instanceof Group && $group->getMember($account)) {
                        return EntityAccessHelper::NEUTRAL;
                      }
            return self::NEUTRAL;
          }
          elseif (
            !$account->hasPermission($permission) &&
            $entity instanceof ContentEntityInterface
          ) {
            // If user doesn't have permission we just check user membership in
            // groups where the node attached as group content.
            $group_contents = GroupContent::loadByEntity($entity);

            // Check recursively - if user is a member at least in one group we
            // should allow to check access by gnode module.
            // @see gnode_node_access()
            foreach ($group_contents as $group_content) {
              if ($group_content->getGroup()->getMember($account)) {
                return self::NEUTRAL;
              }
            }
                if ($account->hasPermission('view ' . $permission_label . ' content')) {
                  return EntityAccessHelper::ALLOW;
          }
                if (($account->id() !== 0) && ($account->id() === $node->getOwnerId())) {
                  return EntityAccessHelper::ALLOW;
        }

        if (
          $account->hasPermission($permission) ||
          $account->isAuthenticated() &&
          $account->id() === $entity->getOwnerId()
        ) {
          return self::ALLOW;
        }
      }
          }

      $access = FALSE;
    }
      }
      if (isset($access) && $access === FALSE) {
        return EntityAccessHelper::FORBIDDEN;
      }
    }
    return EntityAccessHelper::NEUTRAL;

    return 1 - (int) $access;
  }

  /**
   * Gets the Entity access for the given node.
   * Gets the Entity access for the given entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to check access to.
   * @param string $operation
   *   The operation that is to be performed on $entity. Usually one of:
   *   - "view".
   *   - "update".
   *   - "delete".
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The account trying to access the entity.
   * @param string|null $permission
   *   (optional) The admin permission. Defaults to NULL.
   */
  public static function getEntityAccessResult(NodeInterface $node, $op, AccountInterface $account) {
    $access = EntityAccessHelper::nodeAccessCheck($node, $op, $account);
  public static function getEntityAccessResult(
    EntityInterface $entity,
    string $operation,
    AccountInterface $account,
    string $permission = NULL
  ): AccessResultInterface {
    $access = self::entityAccessCheck(
      $entity,
      $operation,
      $account,
      $permission,
    );

    $moduleHandler = \Drupal::service('module_handler');
    // If the social_event_invite module is enabled and a person got invited
    // then allow access to view the node.
    // @todo Come up with a better solution for this code.
    if ($moduleHandler->moduleExists('social_event_invite') && $node->id()) {
      if ($op == 'view') {
        $conditions = [
          'field_account' => $account->id(),
          'field_event' => $node->id(),
        ];
    if (
      \Drupal::moduleHandler()->moduleExists('social_event_invite') &&
      $entity->getEntityTypeId() === 'node' &&
      !$entity->isNew() &&
      $operation === 'view'
    ) {
      $ids = \Drupal::entityQuery('event_enrollment')
        ->accessCheck()
        ->condition('field_account', $account->id())
        ->condition('field_event', $entity->id())
        ->range(0, 1)
        ->execute();

      if (!empty($ids)) {
        $enrollment = \Drupal::entityTypeManager()
          ->getStorage('event_enrollment')
          ->load(reset($ids));

        // Load the current Event enrollments so we can check duplicates.
        $storage = \Drupal::entityTypeManager()->getStorage('event_enrollment');
        $enrollments = $storage->loadByProperties($conditions);
        if ($enrollment !== NULL) {
          $status = (int) $enrollment->field_request_or_invite_status->value;

        if ($enrollment = array_pop($enrollments)) {
          if ((int) $enrollment->field_request_or_invite_status->value !== EventEnrollmentInterface::REQUEST_OR_INVITE_DECLINED
            && (int) $enrollment->field_request_or_invite_status->value !== EventEnrollmentInterface::INVITE_INVALID_OR_EXPIRED) {
            $access = EntityAccessHelper::ALLOW;
          if (
            $status !== EventEnrollmentInterface::REQUEST_OR_INVITE_DECLINED &&
            $status !== EventEnrollmentInterface::INVITE_INVALID_OR_EXPIRED
          ) {
            $access = self::ALLOW;
          }
        }
      }
    }

    switch ($access) {
      case EntityAccessHelper::ALLOW:
        return AccessResult::allowed()->cachePerPermissions()->addCacheableDependency($node);
      case self::ALLOW:
        return AccessResult::allowed()
          ->cachePerPermissions()
          ->addCacheableDependency($entity);

      case EntityAccessHelper::FORBIDDEN:
      case self::FORBIDDEN:
        return AccessResult::forbidden();
    }

+52 −12
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ use Drupal\entity_access_by_field\EntityAccessHelper;
class EntityAccessTest extends UnitTestCase {

  use ProphecyTrait;

  /**
   * The field type random machinename.
   *
@@ -50,9 +51,9 @@ class EntityAccessTest extends UnitTestCase {
  protected $nodeOwnerId;

  /**
   * Tests the EntityAccessHelper::nodeAccessCheck for Neutral Access.
   * Tests the EntityAccessHelper::entityAccessCheck for Neutral Access.
   */
  public function testNeutralAccess() {
  public function testNeutralAccess(): void {

    $node = $this->prophesize(NodeInterface::class);

@@ -68,17 +69,27 @@ class EntityAccessTest extends UnitTestCase {
    $op = 'view';

    $account = $this->prophesize(AccountInterface::class)->reveal();
    $access_result = EntityAccessHelper::nodeAccessCheck($node, $op, $account);

    /** @var \Drupal\node\NodeInterface $node */
    /** @var \Drupal\Core\Session\AccountInterface $account */
    $access_result = EntityAccessHelper::entityAccessCheck(
      $node,
      $op,
      $account,
      'administer nodes',
    );

    $this->assertEquals(EntityAccessHelper::NEUTRAL, $access_result);
    $this->assertNotEquals(EntityAccessHelper::ALLOW, $access_result);
    $this->assertNotEquals(EntityAccessHelper::FORBIDDEN, $access_result);
  }

  /**
   * Tests the EntityAccessHelper::nodeAccessCheck for Forbidden Access.
   * Tests the EntityAccessHelper::entityAccessCheck for Forbidden Access.
   */
  public function testForbiddenAccess() {
  public function testForbiddenAccess(): void {
    $node = $this->prophesize(NodeInterface::class);
    $node->getEntityTypeId()->willReturn('node');
    $node->bundle()->willReturn('article');

    $this->fieldValue = 'public';
@@ -107,10 +118,19 @@ class EntityAccessTest extends UnitTestCase {
    $account = $this->prophesize(AccountInterface::class);
    $account->hasPermission('view ' . $this->fieldId . ':' . $this->fieldValue . ' content')
      ->willReturn(FALSE);
    $account->isAuthenticated()->willReturn(TRUE);
    $account->id()->willReturn($this->accountId);
    $account = $account->reveal();

    $access_result = EntityAccessHelper::nodeAccessCheck($node, $op, $account);
    /** @var \Drupal\node\NodeInterface $node */
    /** @var \Drupal\Core\Session\AccountInterface $account */
    $access_result = EntityAccessHelper::entityAccessCheck(
      $node,
      $op,
      $account,
      'administer nodes',
    );

    $this->assertEquals(EntityAccessHelper::FORBIDDEN, $access_result);
    $this->assertNotEquals(EntityAccessHelper::NEUTRAL, $access_result);
    $this->assertNotEquals(EntityAccessHelper::ALLOW, $access_result);
@@ -118,10 +138,11 @@ class EntityAccessTest extends UnitTestCase {
  }

  /**
   * Tests the EntityAccessHelper::nodeAccessCheck for Allowed Access.
   * Tests the EntityAccessHelper::entityAccessCheck for Allowed Access.
   */
  public function testAllowedAccess() {
  public function testAllowedAccess(): void {
    $node = $this->prophesize(NodeInterface::class);
    $node->getEntityTypeId()->willReturn('node');
    $node->bundle()->willReturn('article');

    $this->fieldId = 'node.article.field_content_visibility';
@@ -154,20 +175,30 @@ class EntityAccessTest extends UnitTestCase {
    $account = $this->prophesize(AccountInterface::class);
    $account->hasPermission('view ' . $this->fieldId . ':' . $this->fieldValue . ' content')
      ->willReturn(TRUE);
    $account->isAuthenticated()->willReturn(TRUE);
    $account->id()->willReturn($this->accountId);
    $account = $account->reveal();

    $access_result = EntityAccessHelper::nodeAccessCheck($node, $op, $account);
    /** @var \Drupal\node\NodeInterface $node */
    /** @var \Drupal\Core\Session\AccountInterface $account */
    $access_result = EntityAccessHelper::entityAccessCheck(
      $node,
      $op,
      $account,
      'administer nodes',
    );

    $this->assertEquals(EntityAccessHelper::ALLOW, $access_result);
    $this->assertNotEquals(EntityAccessHelper::NEUTRAL, $access_result);
    $this->assertNotEquals(EntityAccessHelper::FORBIDDEN, $access_result);
  }

  /**
   * Tests the EntityAccessHelper::nodeAccessCheck for Author Access Allowed.
   * Tests the EntityAccessHelper::entityAccessCheck for Author Access Allowed.
   */
  public function testAuthorAccessAllowed() {
  public function testAuthorAccessAllowed(): void {
    $node = $this->prophesize(NodeInterface::class);
    $node->getEntityTypeId()->willReturn('node');
    $node->bundle()->willReturn('article');

    $this->fieldValue = 'nonexistant';
@@ -198,10 +229,19 @@ class EntityAccessTest extends UnitTestCase {
    $account = $this->prophesize(AccountInterface::class);
    $account->hasPermission('view ' . $this->fieldId . ':' . $this->fieldValue . ' content')
      ->willReturn(FALSE);
    $account->isAuthenticated()->willReturn(TRUE);
    $account->id()->willReturn($this->accountId);
    $account = $account->reveal();

    $access_result = EntityAccessHelper::nodeAccessCheck($node, $op, $account);
    /** @var \Drupal\node\NodeInterface $node */
    /** @var \Drupal\Core\Session\AccountInterface $account */
    $access_result = EntityAccessHelper::entityAccessCheck(
      $node,
      $op,
      $account,
      'administer nodes',
    );

    $this->assertEquals(EntityAccessHelper::ALLOW, $access_result);
    $this->assertNotEquals(EntityAccessHelper::NEUTRAL, $access_result);
    $this->assertNotEquals(EntityAccessHelper::FORBIDDEN, $access_result);
+2 −92
Original line number Diff line number Diff line
@@ -1730,16 +1730,6 @@ parameters:
			count: 1
			path: modules/custom/entity_access_by_field/entity_access_by_field.module
		-
			message: "#^Function entity_access_by_field_node_access\\(\\) has no return type specified\\.$#"
			count: 1
			path: modules/custom/entity_access_by_field/entity_access_by_field.module
		-
			message: "#^Function entity_access_by_field_node_access\\(\\) has parameter \\$op with no type specified\\.$#"
			count: 1
			path: modules/custom/entity_access_by_field/entity_access_by_field.module
		-
			message: "#^Function entity_access_by_field_node_access_explain\\(\\) has no return type specified\\.$#"
			count: 1
@@ -1825,26 +1815,6 @@ parameters:
			count: 1
			path: modules/custom/entity_access_by_field/src/EntityAccessByFieldPermissions.php
		-
			message: "#^Method Drupal\\\\entity_access_by_field\\\\EntityAccessHelper\\:\\:getEntityAccessResult\\(\\) has no return type specified\\.$#"
			count: 1
			path: modules/custom/entity_access_by_field/src/EntityAccessHelper.php
		-
			message: "#^Method Drupal\\\\entity_access_by_field\\\\EntityAccessHelper\\:\\:getEntityAccessResult\\(\\) has parameter \\$op with no type specified\\.$#"
			count: 1
			path: modules/custom/entity_access_by_field/src/EntityAccessHelper.php
		-
			message: "#^Method Drupal\\\\entity_access_by_field\\\\EntityAccessHelper\\:\\:nodeAccessCheck\\(\\) has no return type specified\\.$#"
			count: 1
			path: modules/custom/entity_access_by_field/src/EntityAccessHelper.php
		-
			message: "#^Method Drupal\\\\entity_access_by_field\\\\EntityAccessHelper\\:\\:nodeAccessCheck\\(\\) has parameter \\$op with no type specified\\.$#"
			count: 1
			path: modules/custom/entity_access_by_field/src/EntityAccessHelper.php
		-
			message: "#^Method Drupal\\\\entity_access_by_field\\\\Form\\\\EntityVisibilityForm\\:\\:submitForm\\(\\) has no return type specified\\.$#"
			count: 1
@@ -1866,68 +1836,8 @@ parameters:
			path: modules/custom/entity_access_by_field/src/Plugin/search_api/processor/EntityAccessByField.php
		-
			message: "#^Call to an undefined method Prophecy\\\\Prophecy\\\\ObjectProphecy\\:\\:bundle\\(\\)\\.$#"
			count: 3
			path: modules/custom/entity_access_by_field/tests/src/Unit/EntityAccessTest.php
		-
			message: "#^Call to an undefined method Prophecy\\\\Prophecy\\\\ObjectProphecy\\:\\:get\\(\\)\\.$#"
			count: 3
			path: modules/custom/entity_access_by_field/tests/src/Unit/EntityAccessTest.php
		-
			message: "#^Call to an undefined method Prophecy\\\\Prophecy\\\\ObjectProphecy\\:\\:getCacheContexts\\(\\)\\.$#"
			count: 2
			path: modules/custom/entity_access_by_field/tests/src/Unit/EntityAccessTest.php
		-
			message: "#^Call to an undefined method Prophecy\\\\Prophecy\\\\ObjectProphecy\\:\\:getFieldDefinitions\\(\\)\\.$#"
			count: 4
			path: modules/custom/entity_access_by_field/tests/src/Unit/EntityAccessTest.php
		-
			message: "#^Call to an undefined method Prophecy\\\\Prophecy\\\\ObjectProphecy\\:\\:getOwnerId\\(\\)\\.$#"
			count: 3
			path: modules/custom/entity_access_by_field/tests/src/Unit/EntityAccessTest.php
		-
			message: "#^Call to an undefined method Prophecy\\\\Prophecy\\\\ObjectProphecy\\:\\:hasPermission\\(\\)\\.$#"
			count: 3
			path: modules/custom/entity_access_by_field/tests/src/Unit/EntityAccessTest.php
		-
			message: "#^Call to an undefined method Prophecy\\\\Prophecy\\\\ObjectProphecy\\:\\:id\\(\\)\\.$#"
			count: 3
			path: modules/custom/entity_access_by_field/tests/src/Unit/EntityAccessTest.php
		-
			message: "#^Method Drupal\\\\entity_access_by_field\\\\Tests\\\\EntityAccessTest\\:\\:testAllowedAccess\\(\\) has no return type specified\\.$#"
			count: 1
			path: modules/custom/entity_access_by_field/tests/src/Unit/EntityAccessTest.php
		-
			message: "#^Method Drupal\\\\entity_access_by_field\\\\Tests\\\\EntityAccessTest\\:\\:testAuthorAccessAllowed\\(\\) has no return type specified\\.$#"
			count: 1
			path: modules/custom/entity_access_by_field/tests/src/Unit/EntityAccessTest.php
		-
			message: "#^Method Drupal\\\\entity_access_by_field\\\\Tests\\\\EntityAccessTest\\:\\:testForbiddenAccess\\(\\) has no return type specified\\.$#"
			count: 1
			path: modules/custom/entity_access_by_field/tests/src/Unit/EntityAccessTest.php
		-
			message: "#^Method Drupal\\\\entity_access_by_field\\\\Tests\\\\EntityAccessTest\\:\\:testNeutralAccess\\(\\) has no return type specified\\.$#"
			count: 1
			path: modules/custom/entity_access_by_field/tests/src/Unit/EntityAccessTest.php
		-
			message: "#^Parameter \\#1 \\$node of static method Drupal\\\\entity_access_by_field\\\\EntityAccessHelper\\:\\:nodeAccessCheck\\(\\) expects Drupal\\\\node\\\\NodeInterface, object given\\.$#"
			count: 4
			path: modules/custom/entity_access_by_field/tests/src/Unit/EntityAccessTest.php
		-
			message: "#^Parameter \\#3 \\$account of static method Drupal\\\\entity_access_by_field\\\\EntityAccessHelper\\:\\:nodeAccessCheck\\(\\) expects Drupal\\\\Core\\\\Session\\\\AccountInterface, object given\\.$#"
			count: 4
			message: '#^Call to an undefined method Prophecy\\Prophecy\\ObjectProphecy::(bundle|get|getCacheContexts|getEntityTypeId|getFieldDefinitions|getOwnerId|hasPermission|id|isAuthenticated)\(\).$#'
			count: 27
			path: modules/custom/entity_access_by_field/tests/src/Unit/EntityAccessTest.php
		-