annotate 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
rev   line source
Chris@5 1 <?php
Chris@5 2
Chris@5 3 namespace Drupal\jsonapi\Access;
Chris@5 4
Chris@5 5 use Drupal\content_moderation\Access\LatestRevisionCheck;
Chris@5 6 use Drupal\Core\Access\AccessResult;
Chris@5 7 use Drupal\Core\Access\AccessResultReasonInterface;
Chris@5 8 use Drupal\Core\Entity\EntityInterface;
Chris@5 9 use Drupal\Core\Entity\EntityRepositoryInterface;
Chris@5 10 use Drupal\Core\Entity\RevisionableInterface;
Chris@5 11 use Drupal\Core\Routing\RouteMatch;
Chris@5 12 use Drupal\Core\Session\AccountInterface;
Chris@5 13 use Drupal\jsonapi\Exception\EntityAccessDeniedHttpException;
Chris@5 14 use Drupal\jsonapi\JsonApiResource\LabelOnlyResourceObject;
Chris@5 15 use Drupal\jsonapi\JsonApiResource\ResourceObject;
Chris@5 16 use Drupal\jsonapi\JsonApiSpec;
Chris@5 17 use Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface;
Chris@5 18 use Drupal\media\Access\MediaRevisionAccessCheck;
Chris@5 19 use Drupal\media\MediaInterface;
Chris@5 20 use Drupal\node\Access\NodeRevisionAccessCheck;
Chris@5 21 use Drupal\node\NodeInterface;
Chris@5 22 use Symfony\Component\Routing\RouterInterface;
Chris@5 23
Chris@5 24 /**
Chris@5 25 * Checks access to entities.
Chris@5 26 *
Chris@5 27 * JSON:API needs to check access to every single entity type. Some entity types
Chris@5 28 * have non-standard access checking logic. This class centralizes entity access
Chris@5 29 * checking logic.
Chris@5 30 *
Chris@5 31 * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
Chris@5 32 * may change at any time and could break any dependencies on it.
Chris@5 33 *
Chris@5 34 * @see https://www.drupal.org/project/jsonapi/issues/3032787
Chris@5 35 * @see jsonapi.api.php
Chris@5 36 */
Chris@5 37 class EntityAccessChecker {
Chris@5 38
Chris@5 39 /**
Chris@5 40 * The JSON:API resource type repository.
Chris@5 41 *
Chris@5 42 * @var \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface
Chris@5 43 */
Chris@5 44 protected $resourceTypeRepository;
Chris@5 45
Chris@5 46 /**
Chris@5 47 * The router.
Chris@5 48 *
Chris@5 49 * @var \Symfony\Component\Routing\RouterInterface
Chris@5 50 */
Chris@5 51 protected $router;
Chris@5 52
Chris@5 53 /**
Chris@5 54 * The current user.
Chris@5 55 *
Chris@5 56 * @var \Drupal\Core\Session\AccountInterface
Chris@5 57 */
Chris@5 58 protected $currentUser;
Chris@5 59
Chris@5 60 /**
Chris@5 61 * The entity repository.
Chris@5 62 *
Chris@5 63 * @var \Drupal\Core\Entity\EntityRepositoryInterface
Chris@5 64 */
Chris@5 65 protected $entityRepository;
Chris@5 66
Chris@5 67 /**
Chris@5 68 * The node revision access check service.
Chris@5 69 *
Chris@5 70 * This will be NULL unless the node module is installed.
Chris@5 71 *
Chris@5 72 * @var \Drupal\node\Access\NodeRevisionAccessCheck|null
Chris@5 73 */
Chris@5 74 protected $nodeRevisionAccessCheck = NULL;
Chris@5 75
Chris@5 76 /**
Chris@5 77 * The media revision access check service.
Chris@5 78 *
Chris@5 79 * This will be NULL unless the media module is installed.
Chris@5 80 *
Chris@5 81 * @var \Drupal\media\Access\MediaRevisionAccessCheck|null
Chris@5 82 */
Chris@5 83 protected $mediaRevisionAccessCheck = NULL;
Chris@5 84
Chris@5 85 /**
Chris@5 86 * The latest revision check service.
Chris@5 87 *
Chris@5 88 * This will be NULL unless the content_moderation module is installed. This
Chris@5 89 * is a temporary measure. JSON:API should not need to be aware of the
Chris@5 90 * Content Moderation module.
Chris@5 91 *
Chris@5 92 * @var \Drupal\content_moderation\Access\LatestRevisionCheck
Chris@5 93 */
Chris@5 94 protected $latestRevisionCheck = NULL;
Chris@5 95
Chris@5 96 /**
Chris@5 97 * EntityAccessChecker constructor.
Chris@5 98 *
Chris@5 99 * @param \Drupal\jsonapi\ResourceType\ResourceTypeRepositoryInterface $resource_type_repository
Chris@5 100 * The JSON:API resource type repository.
Chris@5 101 * @param \Symfony\Component\Routing\RouterInterface $router
Chris@5 102 * The router.
Chris@5 103 * @param \Drupal\Core\Session\AccountInterface $account
Chris@5 104 * The current user.
Chris@5 105 * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
Chris@5 106 * The entity repository.
Chris@5 107 */
Chris@5 108 public function __construct(ResourceTypeRepositoryInterface $resource_type_repository, RouterInterface $router, AccountInterface $account, EntityRepositoryInterface $entity_repository) {
Chris@5 109 $this->resourceTypeRepository = $resource_type_repository;
Chris@5 110 $this->router = $router;
Chris@5 111 $this->currentUser = $account;
Chris@5 112 $this->entityRepository = $entity_repository;
Chris@5 113 }
Chris@5 114
Chris@5 115 /**
Chris@5 116 * Sets the node revision access check service.
Chris@5 117 *
Chris@5 118 * This is only called when node module is installed.
Chris@5 119 *
Chris@5 120 * @param \Drupal\node\Access\NodeRevisionAccessCheck $node_revision_access_check
Chris@5 121 * The node revision access check service.
Chris@5 122 */
Chris@5 123 public function setNodeRevisionAccessCheck(NodeRevisionAccessCheck $node_revision_access_check) {
Chris@5 124 $this->nodeRevisionAccessCheck = $node_revision_access_check;
Chris@5 125 }
Chris@5 126
Chris@5 127 /**
Chris@5 128 * Sets the media revision access check service.
Chris@5 129 *
Chris@5 130 * This is only called when media module is installed.
Chris@5 131 *
Chris@5 132 * @param \Drupal\media\Access\MediaRevisionAccessCheck $media_revision_access_check
Chris@5 133 * The media revision access check service.
Chris@5 134 */
Chris@5 135 public function setMediaRevisionAccessCheck(MediaRevisionAccessCheck $media_revision_access_check) {
Chris@5 136 $this->mediaRevisionAccessCheck = $media_revision_access_check;
Chris@5 137 }
Chris@5 138
Chris@5 139 /**
Chris@5 140 * Sets the media revision access check service.
Chris@5 141 *
Chris@5 142 * This is only called when content_moderation module is installed.
Chris@5 143 *
Chris@5 144 * @param \Drupal\content_moderation\Access\LatestRevisionCheck $latest_revision_check
Chris@5 145 * The latest revision access check service provided by the
Chris@5 146 * content_moderation module.
Chris@5 147 *
Chris@5 148 * @see self::$latestRevisionCheck
Chris@5 149 */
Chris@5 150 public function setLatestRevisionCheck(LatestRevisionCheck $latest_revision_check) {
Chris@5 151 $this->latestRevisionCheck = $latest_revision_check;
Chris@5 152 }
Chris@5 153
Chris@5 154 /**
Chris@5 155 * Get the object to normalize and the access based on the provided entity.
Chris@5 156 *
Chris@5 157 * @param \Drupal\Core\Entity\EntityInterface $entity
Chris@5 158 * The entity to test access for.
Chris@5 159 * @param \Drupal\Core\Session\AccountInterface $account
Chris@5 160 * (optional) The account with which access should be checked. Defaults to
Chris@5 161 * the current user.
Chris@5 162 *
Chris@5 163 * @return \Drupal\jsonapi\JsonApiResource\ResourceObject|\Drupal\jsonapi\JsonApiResource\LabelOnlyResourceObject|\Drupal\jsonapi\Exception\EntityAccessDeniedHttpException
Chris@5 164 * The ResourceObject, a LabelOnlyResourceObject or an
Chris@5 165 * EntityAccessDeniedHttpException object if neither is accessible. All
Chris@5 166 * three possible return values carry the access result cacheability.
Chris@5 167 */
Chris@5 168 public function getAccessCheckedResourceObject(EntityInterface $entity, AccountInterface $account = NULL) {
Chris@5 169 $account = $account ?: $this->currentUser;
Chris@5 170 $resource_type = $this->resourceTypeRepository->get($entity->getEntityTypeId(), $entity->bundle());
Chris@5 171 $entity = $this->entityRepository->getTranslationFromContext($entity, NULL, ['operation' => 'entity_upcast']);
Chris@5 172 $access = $this->checkEntityAccess($entity, 'view', $account);
Chris@5 173 $entity->addCacheableDependency($access);
Chris@5 174 if (!$access->isAllowed()) {
Chris@5 175 // If this is the default revision or the entity is not revisionable, then
Chris@5 176 // check access to the entity label. Revision support is all or nothing.
Chris@5 177 if (!$entity->getEntityType()->isRevisionable() || $entity->isDefaultRevision()) {
Chris@5 178 $label_access = $entity->access('view label', NULL, TRUE);
Chris@5 179 $entity->addCacheableDependency($label_access);
Chris@5 180 if ($label_access->isAllowed()) {
Chris@5 181 return LabelOnlyResourceObject::createFromEntity($resource_type, $entity);
Chris@5 182 }
Chris@5 183 $access = $access->orIf($label_access);
Chris@5 184 }
Chris@5 185 return new EntityAccessDeniedHttpException($entity, $access, '/data', 'The current user is not allowed to GET the selected resource.');
Chris@5 186 }
Chris@5 187 return ResourceObject::createFromEntity($resource_type, $entity);
Chris@5 188 }
Chris@5 189
Chris@5 190 /**
Chris@5 191 * Checks access to the given entity.
Chris@5 192 *
Chris@5 193 * @param \Drupal\Core\Entity\EntityInterface $entity
Chris@5 194 * The entity for which access should be evaluated.
Chris@5 195 * @param string $operation
Chris@5 196 * The entity operation for which access should be evaluated.
Chris@5 197 * @param \Drupal\Core\Session\AccountInterface $account
Chris@5 198 * (optional) The account with which access should be checked. Defaults to
Chris@5 199 * the current user.
Chris@5 200 *
Chris@5 201 * @return \Drupal\Core\Access\AccessResultInterface|\Drupal\Core\Access\AccessResultReasonInterface
Chris@5 202 * The access check result.
Chris@5 203 */
Chris@5 204 public function checkEntityAccess(EntityInterface $entity, $operation, AccountInterface $account) {
Chris@5 205 $access = $entity->access($operation, $account, TRUE);
Chris@5 206 if ($entity->getEntityType()->isRevisionable()) {
Chris@5 207 $access = AccessResult::neutral()->addCacheContexts(['url.query_args:' . JsonApiSpec::VERSION_QUERY_PARAMETER])->orIf($access);
Chris@5 208 if (!$entity->isDefaultRevision()) {
Chris@5 209 assert($operation === 'view', 'JSON:API does not yet support mutable operations on revisions.');
Chris@5 210 $revision_access = $this->checkRevisionViewAccess($entity, $account);
Chris@5 211 $access = $access->andIf($revision_access);
Chris@5 212 // The revision access reason should trump the primary access reason.
Chris@5 213 if (!$access->isAllowed()) {
Chris@5 214 $reason = $access instanceof AccessResultReasonInterface ? $access->getReason() : '';
Chris@5 215 $access->setReason(trim('The user does not have access to the requested version. ' . $reason));
Chris@5 216 }
Chris@5 217 }
Chris@5 218 }
Chris@5 219 return $access;
Chris@5 220 }
Chris@5 221
Chris@5 222 /**
Chris@5 223 * Checks access to the given revision entity.
Chris@5 224 *
Chris@5 225 * This should only be called for non-default revisions.
Chris@5 226 *
Chris@5 227 * There is no standardized API for revision access checking in Drupal core
Chris@5 228 * and this method shims that missing API.
Chris@5 229 *
Chris@5 230 * @param \Drupal\Core\Entity\EntityInterface $entity
Chris@5 231 * The revised entity for which to check access.
Chris@5 232 * @param \Drupal\Core\Session\AccountInterface $account
Chris@5 233 * (optional) The account with which access should be checked. Defaults to
Chris@5 234 * the current user.
Chris@5 235 *
Chris@5 236 * @return \Drupal\Core\Access\AccessResultInterface|\Drupal\Core\Access\AccessResultReasonInterface
Chris@5 237 * The access check result.
Chris@5 238 *
Chris@5 239 * @todo: remove when a generic revision access API exists in Drupal core, and
Chris@5 240 * also remove the injected "node" and "media" services.
Chris@5 241 * @see https://www.drupal.org/project/jsonapi/issues/2992833#comment-12818386
Chris@5 242 */
Chris@5 243 protected function checkRevisionViewAccess(EntityInterface $entity, AccountInterface $account) {
Chris@5 244 assert($entity instanceof RevisionableInterface);
Chris@5 245 assert(!$entity->isDefaultRevision(), 'It is not necessary to check revision access when the entity is the default revision.');
Chris@5 246 $entity_type = $entity->getEntityType();
Chris@5 247 switch ($entity_type->id()) {
Chris@5 248 case 'node':
Chris@5 249 assert($entity instanceof NodeInterface);
Chris@5 250 $access = AccessResult::allowedIf($this->nodeRevisionAccessCheck->checkAccess($entity, $account, 'view'))->cachePerPermissions()->addCacheableDependency($entity);
Chris@5 251 break;
Chris@5 252
Chris@5 253 case 'media':
Chris@5 254 assert($entity instanceof MediaInterface);
Chris@5 255 $access = AccessResult::allowedIf($this->mediaRevisionAccessCheck->checkAccess($entity, $account, 'view'))->cachePerPermissions()->addCacheableDependency($entity);
Chris@5 256 break;
Chris@5 257
Chris@5 258 default:
Chris@5 259 $reason = 'Only node and media revisions are supported by JSON:API.';
Chris@5 260 $reason .= ' For context, see https://www.drupal.org/project/jsonapi/issues/2992833#comment-12818258.';
Chris@5 261 $reason .= ' To contribute, see https://www.drupal.org/project/drupal/issues/2350939 and https://www.drupal.org/project/drupal/issues/2809177.';
Chris@5 262 $access = AccessResult::neutral($reason);
Chris@5 263 }
Chris@5 264 // Apply content_moderation's additional access logic.
Chris@5 265 // @see \Drupal\content_moderation\Access\LatestRevisionCheck::access()
Chris@5 266 if ($entity_type->getLinkTemplate('latest-version') && $entity->isLatestRevision() && isset($this->latestRevisionCheck)) {
Chris@5 267 // The latest revision access checker only expects to be invoked by the
Chris@5 268 // routing system, which makes it necessary to fake a route match.
Chris@5 269 $routes = $this->router->getRouteCollection();
Chris@5 270 $resource_type = $this->resourceTypeRepository->get($entity->getEntityTypeId(), $entity->bundle());
Chris@5 271 $route_name = sprintf('jsonapi.%s.individual', $resource_type->getTypeName());
Chris@5 272 $route = $routes->get($route_name);
Chris@5 273 $route->setOption('_content_moderation_entity_type', 'entity');
Chris@5 274 $route_match = new RouteMatch($route_name, $route, ['entity' => $entity], ['entity' => $entity->uuid()]);
Chris@5 275 $moderation_access_result = $this->latestRevisionCheck->access($route, $route_match, $account);
Chris@5 276 $access = $access->andIf($moderation_access_result);
Chris@5 277 }
Chris@5 278 return $access;
Chris@5 279 }
Chris@5 280
Chris@5 281 }