You are here

class EntityAccessChecker in Drupal 10

Same name and namespace in other branches
  1. 8 core/modules/jsonapi/src/Access/EntityAccessChecker.php \Drupal\jsonapi\Access\EntityAccessChecker
  2. 9 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 33

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 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 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.
   */
  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();
    $access = $entity
      ->access('view all revisions', $account, TRUE);

    // 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