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