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