You are here

class EntityAccessChecker in Drupal 9

Same name and namespace in other branches
  1. 8 core/modules/jsonapi/src/Access/EntityAccessChecker.php \Drupal\jsonapi\Access\EntityAccessChecker

Checks access to entities.

JSON:API needs to check access to every single entity type. Some entity types have non-standard access checking logic. This class centralizes entity access checking logic.

@internal JSON:API maintains no PHP API. The API is the HTTP API. This class may change at any time and could break any dependencies on it.

Hierarchy

Expanded class hierarchy of EntityAccessChecker

See also

https://www.drupal.org/project/drupal/issues/3032787

jsonapi.api.php

2 files declare their use of EntityAccessChecker
EntityResource.php in core/modules/jsonapi/src/Controller/EntityResource.php
IncludeResolver.php in core/modules/jsonapi/src/IncludeResolver.php
1 string reference to 'EntityAccessChecker'
jsonapi.services.yml in core/modules/jsonapi/jsonapi.services.yml
core/modules/jsonapi/jsonapi.services.yml
1 service uses EntityAccessChecker
jsonapi.entity_access_checker in core/modules/jsonapi/jsonapi.services.yml
Drupal\jsonapi\Access\EntityAccessChecker

File

core/modules/jsonapi/src/Access/EntityAccessChecker.php, line 37

Namespace

Drupal\jsonapi\Access
View source
class EntityAccessChecker {

  /**
   * The JSON:API resource type repository.
   *
   * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface
   */
  protected $resourceTypeRepository;

  /**
   * The router.
   *
   * @var \Symfony\Component\Routing\RouterInterface
   */
  protected $router;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * The entity repository.
   *
   * @var \Drupal\Core\Entity\EntityRepositoryInterface
   */
  protected $entityRepository;

  /**
   * The node revision access check service.
   *
   * This will be NULL unless the node module is installed.
   *
   * @var \Drupal\node\Access\NodeRevisionAccessCheck|null
   */
  protected $nodeRevisionAccessCheck = NULL;

  /**
   * The media revision access check service.
   *
   * This will be NULL unless the media module is installed.
   *
   * @var \Drupal\media\Access\MediaRevisionAccessCheck|null
   */
  protected $mediaRevisionAccessCheck = NULL;

  /**
   * The latest revision check service.
   *
   * This will be NULL unless the content_moderation module is installed. This
   * is a temporary measure. JSON:API should not need to be aware of the
   * Content Moderation module.
   *
   * @var \Drupal\content_moderation\Access\LatestRevisionCheck
   */
  protected $latestRevisionCheck = NULL;

  /**
   * EntityAccessChecker constructor.
   *
   * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
   *   The JSON:API resource type repository.
   * @param \Symfony\Component\Routing\RouterInterface $router
   *   The router.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The current user.
   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
   *   The entity repository.
   */
  public function __construct(ResourceTypeRepositoryInterface $resource_type_repository, RouterInterface $router, AccountInterface $account, EntityRepositoryInterface $entity_repository) {
    $this->resourceTypeRepository = $resource_type_repository;
    $this->router = $router;
    $this->currentUser = $account;
    $this->entityRepository = $entity_repository;
  }

  /**
   * Sets the node revision access check service.
   *
   * This is only called when node module is installed.
   *
   * @param \Drupal\node\Access\NodeRevisionAccessCheck $node_revision_access_check
   *   The node revision access check service.
   */
  public function setNodeRevisionAccessCheck(NodeRevisionAccessCheck $node_revision_access_check) {
    $this->nodeRevisionAccessCheck = $node_revision_access_check;
  }

  /**
   * Sets the media revision access check service.
   *
   * This is only called when media module is installed.
   *
   * @param \Drupal\media\Access\MediaRevisionAccessCheck $media_revision_access_check
   *   The media revision access check service.
   */
  public function setMediaRevisionAccessCheck(MediaRevisionAccessCheck $media_revision_access_check) {
    $this->mediaRevisionAccessCheck = $media_revision_access_check;
  }

  /**
   * Sets the media revision access check service.
   *
   * This is only called when content_moderation module is installed.
   *
   * @param \Drupal\content_moderation\Access\LatestRevisionCheck $latest_revision_check
   *   The latest revision access check service provided by the
   *   content_moderation module.
   *
   * @see self::$latestRevisionCheck
   */
  public function setLatestRevisionCheck(LatestRevisionCheck $latest_revision_check) {
    $this->latestRevisionCheck = $latest_revision_check;
  }

  /**
   * Get the object to normalize and the access based on the provided entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to test access for.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   (optional) The account with which access should be checked. Defaults to
   *   the current user.
   *
   * @return \Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\LabelOnlyResourceObject|\Drupal\jsonapi\Exception\EntityAccessDeniedHttpException
   *   The ResourceObject, a LabelOnlyResourceObject or an
   *   EntityAccessDeniedHttpException object if neither is accessible. All
   *   three possible return values carry the access result cacheability.
   */
  public function getAccessCheckedResourceObject(EntityInterface $entity, AccountInterface $account = NULL) {
    $account = $account ?: $this->currentUser;
    $resource_type = $this->resourceTypeRepository
      ->get($entity
      ->getEntityTypeId(), $entity
      ->bundle());
    $entity = $this->entityRepository
      ->getTranslationFromContext($entity, NULL, [
      'operation' => 'entity_upcast',
    ]);
    $access = $this
      ->checkEntityAccess($entity, 'view', $account);
    $entity
      ->addCacheableDependency($access);
    if (!$access
      ->isAllowed()) {

      // If this is the default revision or the entity is not revisionable, then
      // check access to the entity label. Revision support is all or nothing.
      if (!$entity
        ->getEntityType()
        ->isRevisionable() || $entity
        ->isDefaultRevision()) {
        $label_access = $entity
          ->access('view label', NULL, TRUE);
        $entity
          ->addCacheableDependency($label_access);
        if ($label_access
          ->isAllowed()) {
          return LabelOnlyResourceObject::createFromEntity($resource_type, $entity);
        }
        $access = $access
          ->orIf($label_access);
      }
      return new EntityAccessDeniedHttpException($entity, $access, '/data', 'The current user is not allowed to GET the selected resource.');
    }
    return ResourceObject::createFromEntity($resource_type, $entity);
  }

  /**
   * Checks access to the given entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity for which access should be evaluated.
   * @param string $operation
   *   The entity operation for which access should be evaluated.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   (optional) The account with which access should be checked. Defaults to
   *   the current user.
   *
   * @return \Drupal\Core\Access\AccessResultInterface|\Drupal\Core\Access\AccessResultReasonInterface
   *   The access check result.
   */
  public function checkEntityAccess(EntityInterface $entity, $operation, AccountInterface $account) {
    $access = $entity
      ->access($operation, $account, TRUE);
    if ($entity
      ->getEntityType()
      ->isRevisionable()) {
      $access = AccessResult::neutral()
        ->addCacheContexts([
        'url.query_args:' . JsonApiSpec::VERSION_QUERY_PARAMETER,
      ])
        ->orIf($access);
      if (!$entity
        ->isDefaultRevision()) {
        assert($operation === 'view', 'JSON:API does not yet support mutable operations on revisions.');
        $revision_access = $this
          ->checkRevisionViewAccess($entity, $account);
        $access = $access
          ->andIf($revision_access);

        // The revision access reason should trump the primary access reason.
        if (!$access
          ->isAllowed()) {
          $reason = $access instanceof AccessResultReasonInterface ? $access
            ->getReason() : '';
          $access
            ->setReason(trim('The user does not have access to the requested version. ' . $reason));
        }
      }
    }
    return $access;
  }

  /**
   * Checks access to the given revision entity.
   *
   * This should only be called for non-default revisions.
   *
   * There is no standardized API for revision access checking in Drupal core
   * and this method shims that missing API.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The revised entity for which to check access.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   (optional) The account with which access should be checked. Defaults to
   *   the current user.
   *
   * @return \Drupal\Core\Access\AccessResultInterface|\Drupal\Core\Access\AccessResultReasonInterface
   *   The access check result.
   *
   * @todo: remove when a generic revision access API exists in Drupal core, and
   * also remove the injected "node" and "media" services.
   * @see https://www.drupal.org/project/drupal/issues/2992833#comment-12818386
   */
  protected function checkRevisionViewAccess(EntityInterface $entity, AccountInterface $account) {
    assert($entity instanceof RevisionableInterface);
    assert(!$entity
      ->isDefaultRevision(), 'It is not necessary to check revision access when the entity is the default revision.');
    $entity_type = $entity
      ->getEntityType();
    switch ($entity_type
      ->id()) {
      case 'node':
        assert($entity instanceof NodeInterface);
        $access = AccessResult::allowedIf($this->nodeRevisionAccessCheck
          ->checkAccess($entity, $account, 'view'))
          ->cachePerPermissions()
          ->addCacheableDependency($entity);
        break;
      case 'media':
        assert($entity instanceof MediaInterface);
        $access = AccessResult::allowedIf($this->mediaRevisionAccessCheck
          ->checkAccess($entity, $account, 'view'))
          ->cachePerPermissions()
          ->addCacheableDependency($entity);
        break;
      default:
        $reason = 'Only node and media revisions are supported by JSON:API.';
        $reason .= ' For context, see https://www.drupal.org/project/drupal/issues/2992833#comment-12818258.';
        $reason .= ' To contribute, see https://www.drupal.org/project/drupal/issues/2350939 and https://www.drupal.org/project/drupal/issues/2809177.';
        $access = AccessResult::neutral($reason);
    }

    // Apply content_moderation's additional access logic.
    // @see \Drupal\content_moderation\Access\LatestRevisionCheck::access()
    if ($entity_type
      ->getLinkTemplate('latest-version') && $entity
      ->isLatestRevision() && isset($this->latestRevisionCheck)) {

      // The latest revision access checker only expects to be invoked by the
      // routing system, which makes it necessary to fake a route match.
      $routes = $this->router
        ->getRouteCollection();
      $resource_type = $this->resourceTypeRepository
        ->get($entity
        ->getEntityTypeId(), $entity
        ->bundle());
      $route_name = sprintf('jsonapi.%s.individual', $resource_type
        ->getTypeName());
      $route = $routes
        ->get($route_name);
      $route
        ->setOption('_content_moderation_entity_type', 'entity');
      $route_match = new RouteMatch($route_name, $route, [
        'entity' => $entity,
      ], [
        'entity' => $entity
          ->uuid(),
      ]);
      $moderation_access_result = $this->latestRevisionCheck
        ->access($route, $route_match, $account);
      $access = $access
        ->andIf($moderation_access_result);
    }
    return $access;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
EntityAccessChecker::$currentUser protected property The current user.
EntityAccessChecker::$entityRepository protected property The entity repository.
EntityAccessChecker::$latestRevisionCheck protected property The latest revision check service.
EntityAccessChecker::$mediaRevisionAccessCheck protected property The media revision access check service.
EntityAccessChecker::$nodeRevisionAccessCheck protected property The node revision access check service.
EntityAccessChecker::$resourceTypeRepository protected property The JSON:API resource type repository.
EntityAccessChecker::$router protected property The router.
EntityAccessChecker::checkEntityAccess public function Checks access to the given entity.
EntityAccessChecker::checkRevisionViewAccess protected function Checks access to the given revision entity.
EntityAccessChecker::getAccessCheckedResourceObject public function Get the object to normalize and the access based on the provided entity.
EntityAccessChecker::setLatestRevisionCheck public function Sets the media revision access check service.
EntityAccessChecker::setMediaRevisionAccessCheck public function Sets the media revision access check service.
EntityAccessChecker::setNodeRevisionAccessCheck public function Sets the node revision access check service.
EntityAccessChecker::__construct public function EntityAccessChecker constructor.