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