diff core/modules/jsonapi/src/Access/EntityAccessChecker.php @ 5:12f9dff5fda9 tip

Update to Drupal core 8.7.1
author Chris Cannam
date Thu, 09 May 2019 15:34:47 +0100
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/modules/jsonapi/src/Access/EntityAccessChecker.php	Thu May 09 15:34:47 2019 +0100
@@ -0,0 +1,281 @@
+<?php
+
+namespace Drupal\jsonapi\Access;
+
+use Drupal\content_moderation\Access\LatestRevisionCheck;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Access\AccessResultReasonInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityRepositoryInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\Core\Routing\RouteMatch;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\jsonapi\Exception\EntityAccessDeniedHttpException;
+use Drupal\jsonapi\JsonApiResource\LabelOnlyResourceObject;
+use Drupal\jsonapi\JsonApiResource\ResourceObject;
+use Drupal\jsonapi\JsonApiSpec;
+use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
+use Drupal\media\Access\MediaRevisionAccessCheck;
+use Drupal\media\MediaInterface;
+use Drupal\node\Access\NodeRevisionAccessCheck;
+use Drupal\node\NodeInterface;
+use Symfony\Component\Routing\RouterInterface;
+
+/**
+ * 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.
+ *
+ * @see https://www.drupal.org/project/jsonapi/issues/3032787
+ * @see jsonapi.api.php
+ */
+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/jsonapi/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/jsonapi/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;
+  }
+
+}