Mercurial > hg > cmmr2012-drupal-site
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; + } + +}