Chris@18
|
1 <?php
|
Chris@18
|
2
|
Chris@18
|
3 namespace Drupal\jsonapi\Access;
|
Chris@18
|
4
|
Chris@18
|
5 use Drupal\Core\Access\AccessResult;
|
Chris@18
|
6 use Drupal\Core\Cache\CacheableMetadata;
|
Chris@18
|
7 use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
|
Chris@18
|
8 use Drupal\Core\Entity\EntityFieldManagerInterface;
|
Chris@18
|
9 use Drupal\Core\Entity\EntityTypeInterface;
|
Chris@18
|
10 use Drupal\Core\Entity\Query\QueryInterface;
|
Chris@18
|
11 use Drupal\Core\Extension\ModuleHandlerInterface;
|
Chris@18
|
12 use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
Chris@18
|
13 use Drupal\Core\Session\AccountInterface;
|
Chris@18
|
14 use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
|
Chris@18
|
15 use Drupal\jsonapi\Query\EntityCondition;
|
Chris@18
|
16 use Drupal\jsonapi\Query\EntityConditionGroup;
|
Chris@18
|
17 use Drupal\jsonapi\Query\Filter;
|
Chris@18
|
18 use Drupal\workspaces\WorkspaceInterface;
|
Chris@18
|
19
|
Chris@18
|
20 /**
|
Chris@18
|
21 * Adds sufficient access control to collection queries.
|
Chris@18
|
22 *
|
Chris@18
|
23 * This class will be removed when new Drupal core APIs have been put in place
|
Chris@18
|
24 * to make it obsolete.
|
Chris@18
|
25 *
|
Chris@18
|
26 * @internal JSON:API maintains no PHP API. The API is the HTTP API. This class
|
Chris@18
|
27 * may change at any time and could break any dependencies on it.
|
Chris@18
|
28 *
|
Chris@18
|
29 * @todo These additional security measures should eventually reside in the
|
Chris@18
|
30 * Entity API subsystem but were introduced here to address a security
|
Chris@18
|
31 * vulnerability. The following two issues should obsolesce this class:
|
Chris@18
|
32 *
|
Chris@18
|
33 * @see https://www.drupal.org/project/drupal/issues/2809177
|
Chris@18
|
34 * @see https://www.drupal.org/project/drupal/issues/777578
|
Chris@18
|
35 *
|
Chris@18
|
36 * @see https://www.drupal.org/project/jsonapi/issues/3032787
|
Chris@18
|
37 * @see jsonapi.api.php
|
Chris@18
|
38 */
|
Chris@18
|
39 class TemporaryQueryGuard {
|
Chris@18
|
40
|
Chris@18
|
41 /**
|
Chris@18
|
42 * The entity field manager.
|
Chris@18
|
43 *
|
Chris@18
|
44 * @var \Drupal\Core\Entity\EntityFieldManagerInterface
|
Chris@18
|
45 */
|
Chris@18
|
46 protected static $fieldManager;
|
Chris@18
|
47
|
Chris@18
|
48 /**
|
Chris@18
|
49 * The module handler.
|
Chris@18
|
50 *
|
Chris@18
|
51 * @var \Drupal\Core\Extension\ModuleHandlerInterface
|
Chris@18
|
52 */
|
Chris@18
|
53 protected static $moduleHandler;
|
Chris@18
|
54
|
Chris@18
|
55 /**
|
Chris@18
|
56 * Sets the entity field manager.
|
Chris@18
|
57 *
|
Chris@18
|
58 * This must be called before calling ::applyAccessControls().
|
Chris@18
|
59 *
|
Chris@18
|
60 * @param \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager
|
Chris@18
|
61 * The entity field manager.
|
Chris@18
|
62 */
|
Chris@18
|
63 public static function setFieldManager(EntityFieldManagerInterface $field_manager) {
|
Chris@18
|
64 static::$fieldManager = $field_manager;
|
Chris@18
|
65 }
|
Chris@18
|
66
|
Chris@18
|
67 /**
|
Chris@18
|
68 * Sets the module handler.
|
Chris@18
|
69 *
|
Chris@18
|
70 * This must be called before calling ::applyAccessControls().
|
Chris@18
|
71 *
|
Chris@18
|
72 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
Chris@18
|
73 * The module handler.
|
Chris@18
|
74 */
|
Chris@18
|
75 public static function setModuleHandler(ModuleHandlerInterface $module_handler) {
|
Chris@18
|
76 static::$moduleHandler = $module_handler;
|
Chris@18
|
77 }
|
Chris@18
|
78
|
Chris@18
|
79 /**
|
Chris@18
|
80 * Applies access controls to an entity query.
|
Chris@18
|
81 *
|
Chris@18
|
82 * @param \Drupal\jsonapi\Query\Filter $filter
|
Chris@18
|
83 * The filters applicable to the query.
|
Chris@18
|
84 * @param \Drupal\Core\Entity\Query\QueryInterface $query
|
Chris@18
|
85 * The query to which access controls should be applied.
|
Chris@18
|
86 * @param \Drupal\Core\Cache\CacheableMetadata $cacheability
|
Chris@18
|
87 * Collects cacheability for the query.
|
Chris@18
|
88 */
|
Chris@18
|
89 public static function applyAccessControls(Filter $filter, QueryInterface $query, CacheableMetadata $cacheability) {
|
Chris@18
|
90 assert(static::$fieldManager !== NULL);
|
Chris@18
|
91 assert(static::$moduleHandler !== NULL);
|
Chris@18
|
92 $filtered_fields = static::collectFilteredFields($filter->root());
|
Chris@18
|
93 $field_specifiers = array_map(function ($field) {
|
Chris@18
|
94 return explode('.', $field);
|
Chris@18
|
95 }, $filtered_fields);
|
Chris@18
|
96 static::secureQuery($query, $query->getEntityTypeId(), static::buildTree($field_specifiers), $cacheability);
|
Chris@18
|
97 }
|
Chris@18
|
98
|
Chris@18
|
99 /**
|
Chris@18
|
100 * Applies tags, metadata and conditions to secure an entity query.
|
Chris@18
|
101 *
|
Chris@18
|
102 * @param \Drupal\Core\Entity\Query\QueryInterface $query
|
Chris@18
|
103 * The query to be secured.
|
Chris@18
|
104 * @param string $entity_type_id
|
Chris@18
|
105 * An entity type ID.
|
Chris@18
|
106 * @param array $tree
|
Chris@18
|
107 * A tree of field specifiers in an entity query condition. The tree is a
|
Chris@18
|
108 * multi-dimensional array where the keys are field specifiers and the
|
Chris@18
|
109 * values are multi-dimensional array of the same form, containing only
|
Chris@18
|
110 * subsequent specifiers. @see ::buildTree().
|
Chris@18
|
111 * @param \Drupal\Core\Cache\CacheableMetadata $cacheability
|
Chris@18
|
112 * Collects cacheability for the query.
|
Chris@18
|
113 * @param string|null $field_prefix
|
Chris@18
|
114 * Internal use only. Contains a string representation of the previously
|
Chris@18
|
115 * visited field specifiers.
|
Chris@18
|
116 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition
|
Chris@18
|
117 * Internal use only. The current field storage definition, if known.
|
Chris@18
|
118 *
|
Chris@18
|
119 * @see \Drupal\Core\Database\Query\AlterableInterface::addTag()
|
Chris@18
|
120 * @see \Drupal\Core\Database\Query\AlterableInterface::addMetaData()
|
Chris@18
|
121 * @see \Drupal\Core\Database\Query\ConditionInterface
|
Chris@18
|
122 */
|
Chris@18
|
123 protected static function secureQuery(QueryInterface $query, $entity_type_id, array $tree, CacheableMetadata $cacheability, $field_prefix = NULL, FieldStorageDefinitionInterface $field_storage_definition = NULL) {
|
Chris@18
|
124 $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
|
Chris@18
|
125 // Config entity types are not fieldable, therefore they do not have field
|
Chris@18
|
126 // access restrictions, nor entity references to other entity types.
|
Chris@18
|
127 if ($entity_type instanceof ConfigEntityTypeInterface) {
|
Chris@18
|
128 return;
|
Chris@18
|
129 }
|
Chris@18
|
130 foreach ($tree as $specifier => $children) {
|
Chris@18
|
131 // The field path reconstructs the entity condition fields.
|
Chris@18
|
132 // E.g. `uid.0` would become `uid.0.name` if $specifier === 'name'.
|
Chris@18
|
133 $child_prefix = (is_null($field_prefix)) ? $specifier : "$field_prefix.$specifier";
|
Chris@18
|
134 if (is_null($field_storage_definition)) {
|
Chris@18
|
135 // When the field storage definition is NULL, this specifier is the
|
Chris@18
|
136 // first specifier in an entity query field path or the previous
|
Chris@18
|
137 // specifier was a data reference that has been traversed. In both
|
Chris@18
|
138 // cases, the specifier must be a field name.
|
Chris@18
|
139 $field_storage_definitions = static::$fieldManager->getFieldStorageDefinitions($entity_type_id);
|
Chris@18
|
140 static::secureQuery($query, $entity_type_id, $children, $cacheability, $child_prefix, $field_storage_definitions[$specifier]);
|
Chris@18
|
141 // When $field_prefix is NULL, this must be the first specifier in the
|
Chris@18
|
142 // entity query field path and a condition for the query's base entity
|
Chris@18
|
143 // type must be applied.
|
Chris@18
|
144 if (is_null($field_prefix)) {
|
Chris@18
|
145 static::applyAccessConditions($query, $entity_type_id, NULL, $cacheability);
|
Chris@18
|
146 }
|
Chris@18
|
147 }
|
Chris@18
|
148 else {
|
Chris@18
|
149 // When the specifier is an entity reference, it can contain an entity
|
Chris@18
|
150 // type specifier, like so: `entity:node`. This extracts the `entity`
|
Chris@18
|
151 // portion. JSON:API will have already validated that the property
|
Chris@18
|
152 // exists.
|
Chris@18
|
153 $split_specifier = explode(':', $specifier, 2);
|
Chris@18
|
154 list($property_name, $target_entity_type_id) = array_merge($split_specifier, count($split_specifier) === 2 ? [] : [NULL]);
|
Chris@18
|
155 // The specifier is either a field property or a delta. If it is a data
|
Chris@18
|
156 // reference or a delta, then it needs to be traversed to the next
|
Chris@18
|
157 // specifier. However, if the specific is a simple field property, i.e.
|
Chris@18
|
158 // it is neither a data reference nor a delta, then there is no need to
|
Chris@18
|
159 // evaluate the remaining specifiers.
|
Chris@18
|
160 $property_definition = $field_storage_definition->getPropertyDefinition($property_name);
|
Chris@18
|
161 if ($property_definition instanceof DataReferenceDefinitionInterface) {
|
Chris@18
|
162 // Because the filter is following an entity reference, ensure
|
Chris@18
|
163 // access is respected on those targeted entities.
|
Chris@18
|
164 // Examples:
|
Chris@18
|
165 // - node_query_node_access_alter()
|
Chris@18
|
166 $target_entity_type_id = $target_entity_type_id ?: $field_storage_definition->getSetting('target_type');
|
Chris@18
|
167 $query->addTag("{$target_entity_type_id}_access");
|
Chris@18
|
168 static::applyAccessConditions($query, $target_entity_type_id, $child_prefix, $cacheability);
|
Chris@18
|
169 // Keep descending the tree.
|
Chris@18
|
170 static::secureQuery($query, $target_entity_type_id, $children, $cacheability, $child_prefix);
|
Chris@18
|
171 }
|
Chris@18
|
172 elseif (is_null($property_definition)) {
|
Chris@18
|
173 assert(is_numeric($property_name), 'The specifier is not a property name, it must be a delta.');
|
Chris@18
|
174 // Keep descending the tree.
|
Chris@18
|
175 static::secureQuery($query, $entity_type_id, $children, $cacheability, $child_prefix, $field_storage_definition);
|
Chris@18
|
176 }
|
Chris@18
|
177 }
|
Chris@18
|
178 }
|
Chris@18
|
179 }
|
Chris@18
|
180
|
Chris@18
|
181 /**
|
Chris@18
|
182 * Applies access conditions to ensure 'view' access is respected.
|
Chris@18
|
183 *
|
Chris@18
|
184 * Since the given entity type might not be the base entity type of the query,
|
Chris@18
|
185 * the field prefix should be applied to ensure that the conditions are
|
Chris@18
|
186 * applied to the right subset of entities in the query.
|
Chris@18
|
187 *
|
Chris@18
|
188 * @param \Drupal\Core\Entity\Query\QueryInterface $query
|
Chris@18
|
189 * The query to which access conditions should be applied.
|
Chris@18
|
190 * @param string $entity_type_id
|
Chris@18
|
191 * The entity type for which to access conditions should be applied.
|
Chris@18
|
192 * @param string|null $field_prefix
|
Chris@18
|
193 * A prefix to add before any query condition fields. NULL if no prefix
|
Chris@18
|
194 * should be added.
|
Chris@18
|
195 * @param \Drupal\Core\Cache\CacheableMetadata $cacheability
|
Chris@18
|
196 * Collects cacheability for the query.
|
Chris@18
|
197 */
|
Chris@18
|
198 protected static function applyAccessConditions(QueryInterface $query, $entity_type_id, $field_prefix, CacheableMetadata $cacheability) {
|
Chris@18
|
199 $access_condition = static::getAccessCondition($entity_type_id, $cacheability);
|
Chris@18
|
200 if ($access_condition) {
|
Chris@18
|
201 $prefixed_condition = !is_null($field_prefix)
|
Chris@18
|
202 ? static::addConditionFieldPrefix($access_condition, $field_prefix)
|
Chris@18
|
203 : $access_condition;
|
Chris@18
|
204 $filter = new Filter($prefixed_condition);
|
Chris@18
|
205 $query->condition($filter->queryCondition($query));
|
Chris@18
|
206 }
|
Chris@18
|
207 }
|
Chris@18
|
208
|
Chris@18
|
209 /**
|
Chris@18
|
210 * Prefixes all fields in an EntityConditionGroup.
|
Chris@18
|
211 */
|
Chris@18
|
212 protected static function addConditionFieldPrefix(EntityConditionGroup $group, $field_prefix) {
|
Chris@18
|
213 $prefixed = [];
|
Chris@18
|
214 foreach ($group->members() as $member) {
|
Chris@18
|
215 if ($member instanceof EntityConditionGroup) {
|
Chris@18
|
216 $prefixed[] = static::addConditionFieldPrefix($member, $field_prefix);
|
Chris@18
|
217 }
|
Chris@18
|
218 else {
|
Chris@18
|
219 $field = !empty($field_prefix) ? "{$field_prefix}." . $member->field() : $member->field();
|
Chris@18
|
220 $prefixed[] = new EntityCondition($field, $member->value(), $member->operator());
|
Chris@18
|
221 }
|
Chris@18
|
222 }
|
Chris@18
|
223 return new EntityConditionGroup($group->conjunction(), $prefixed);
|
Chris@18
|
224 }
|
Chris@18
|
225
|
Chris@18
|
226 /**
|
Chris@18
|
227 * Gets an EntityConditionGroup that filters out inaccessible entities.
|
Chris@18
|
228 *
|
Chris@18
|
229 * @param string $entity_type_id
|
Chris@18
|
230 * The entity type ID for which to get an EntityConditionGroup.
|
Chris@18
|
231 * @param \Drupal\Core\Cache\CacheableMetadata $cacheability
|
Chris@18
|
232 * Collects cacheability for the query.
|
Chris@18
|
233 *
|
Chris@18
|
234 * @return \Drupal\jsonapi\Query\EntityConditionGroup|null
|
Chris@18
|
235 * An EntityConditionGroup or NULL if no conditions need to be applied to
|
Chris@18
|
236 * secure an entity query.
|
Chris@18
|
237 */
|
Chris@18
|
238 protected static function getAccessCondition($entity_type_id, CacheableMetadata $cacheability) {
|
Chris@18
|
239 $current_user = \Drupal::currentUser();
|
Chris@18
|
240 $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
|
Chris@18
|
241
|
Chris@18
|
242 // Get the condition that handles generic restrictions, such as published
|
Chris@18
|
243 // and owner.
|
Chris@18
|
244 $generic_condition = static::getAccessConditionForKnownSubsets($entity_type, $current_user, $cacheability);
|
Chris@18
|
245
|
Chris@18
|
246 // Some entity types require additional conditions. We don't know what
|
Chris@18
|
247 // contrib entity types require, so they are responsible for implementing
|
Chris@18
|
248 // hook_query_ENTITY_TYPE_access_alter(). Some core entity types have
|
Chris@18
|
249 // logic in their access control handler that isn't mirrored in
|
Chris@18
|
250 // hook_query_ENTITY_TYPE_access_alter(), so we duplicate that here until
|
Chris@18
|
251 // that's resolved.
|
Chris@18
|
252 $specific_condition = NULL;
|
Chris@18
|
253 switch ($entity_type_id) {
|
Chris@18
|
254 case 'block_content':
|
Chris@18
|
255 // Allow access only to reusable blocks.
|
Chris@18
|
256 // @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess()
|
Chris@18
|
257 if (isset(static::$fieldManager->getBaseFieldDefinitions($entity_type_id)['reusable'])) {
|
Chris@18
|
258 $specific_condition = new EntityCondition('reusable', 1);
|
Chris@18
|
259 $cacheability->addCacheTags($entity_type->getListCacheTags());
|
Chris@18
|
260 }
|
Chris@18
|
261 break;
|
Chris@18
|
262
|
Chris@18
|
263 case 'comment':
|
Chris@18
|
264 // @see \Drupal\comment\CommentAccessControlHandler::checkAccess()
|
Chris@18
|
265 $specific_condition = static::getCommentAccessCondition($entity_type, $current_user, $cacheability);
|
Chris@18
|
266 break;
|
Chris@18
|
267
|
Chris@18
|
268 case 'entity_test':
|
Chris@18
|
269 // This case is only necessary for testing comment access controls.
|
Chris@18
|
270 // @see \Drupal\jsonapi\Tests\Functional\CommentTest::testCollectionFilterAccess()
|
Chris@18
|
271 $blacklist = \Drupal::state()->get('jsonapi__entity_test_filter_access_blacklist', []);
|
Chris@18
|
272 $cacheability->addCacheTags(['state:jsonapi__entity_test_filter_access_blacklist']);
|
Chris@18
|
273 $specific_conditions = [];
|
Chris@18
|
274 foreach ($blacklist as $id) {
|
Chris@18
|
275 $specific_conditions[] = new EntityCondition('id', $id, '<>');
|
Chris@18
|
276 }
|
Chris@18
|
277 if ($specific_conditions) {
|
Chris@18
|
278 $specific_condition = new EntityConditionGroup('AND', $specific_conditions);
|
Chris@18
|
279 }
|
Chris@18
|
280 break;
|
Chris@18
|
281
|
Chris@18
|
282 case 'file':
|
Chris@18
|
283 // Allow access only to public files and files uploaded by the current
|
Chris@18
|
284 // user.
|
Chris@18
|
285 // @see \Drupal\file\FileAccessControlHandler::checkAccess()
|
Chris@18
|
286 $specific_condition = new EntityConditionGroup('OR', [
|
Chris@18
|
287 new EntityCondition('uri', 'public://', 'STARTS_WITH'),
|
Chris@18
|
288 new EntityCondition('uid', $current_user->id()),
|
Chris@18
|
289 ]);
|
Chris@18
|
290 $cacheability->addCacheTags($entity_type->getListCacheTags());
|
Chris@18
|
291 break;
|
Chris@18
|
292
|
Chris@18
|
293 case 'shortcut':
|
Chris@18
|
294 // Unless the user can administer shortcuts, allow access only to the
|
Chris@18
|
295 // user's currently displayed shortcut set.
|
Chris@18
|
296 // @see \Drupal\shortcut\ShortcutAccessControlHandler::checkAccess()
|
Chris@18
|
297 if (!$current_user->hasPermission('administer shortcuts')) {
|
Chris@18
|
298 $specific_condition = new EntityCondition('shortcut_set', shortcut_current_displayed_set()->id());
|
Chris@18
|
299 $cacheability->addCacheContexts(['user']);
|
Chris@18
|
300 $cacheability->addCacheTags($entity_type->getListCacheTags());
|
Chris@18
|
301 }
|
Chris@18
|
302 break;
|
Chris@18
|
303
|
Chris@18
|
304 case 'user':
|
Chris@18
|
305 // Disallow querying values of the anonymous user.
|
Chris@18
|
306 // @see \Drupal\user\UserAccessControlHandler::checkAccess()
|
Chris@18
|
307 $specific_condition = new EntityCondition('uid', '0', '!=');
|
Chris@18
|
308 break;
|
Chris@18
|
309
|
Chris@18
|
310 case 'workspace':
|
Chris@18
|
311 // The default workspace is always viewable, no matter what, so if
|
Chris@18
|
312 // the generic condition prevents that, add an OR.
|
Chris@18
|
313 // @see \Drupal\workspaces\WorkspaceAccessControlHandler::checkAccess()
|
Chris@18
|
314 if ($generic_condition) {
|
Chris@18
|
315 $specific_condition = new EntityConditionGroup('OR', [
|
Chris@18
|
316 $generic_condition,
|
Chris@18
|
317 new EntityCondition('id', WorkspaceInterface::DEFAULT_WORKSPACE),
|
Chris@18
|
318 ]);
|
Chris@18
|
319 // The generic condition is now part of the specific condition.
|
Chris@18
|
320 $generic_condition = NULL;
|
Chris@18
|
321 }
|
Chris@18
|
322 break;
|
Chris@18
|
323 }
|
Chris@18
|
324
|
Chris@18
|
325 // Return a combined condition.
|
Chris@18
|
326 if ($generic_condition && $specific_condition) {
|
Chris@18
|
327 return new EntityConditionGroup('AND', [$generic_condition, $specific_condition]);
|
Chris@18
|
328 }
|
Chris@18
|
329 elseif ($generic_condition) {
|
Chris@18
|
330 return $generic_condition instanceof EntityConditionGroup ? $generic_condition : new EntityConditionGroup('AND', [$generic_condition]);
|
Chris@18
|
331 }
|
Chris@18
|
332 elseif ($specific_condition) {
|
Chris@18
|
333 return $specific_condition instanceof EntityConditionGroup ? $specific_condition : new EntityConditionGroup('AND', [$specific_condition]);
|
Chris@18
|
334 }
|
Chris@18
|
335
|
Chris@18
|
336 return NULL;
|
Chris@18
|
337 }
|
Chris@18
|
338
|
Chris@18
|
339 /**
|
Chris@18
|
340 * Gets an access condition for the allowed JSONAPI_FILTER_AMONG_* subsets.
|
Chris@18
|
341 *
|
Chris@18
|
342 * If access is allowed for the JSONAPI_FILTER_AMONG_ALL subset, then no
|
Chris@18
|
343 * conditions are returned. Otherwise, if access is allowed for
|
Chris@18
|
344 * JSONAPI_FILTER_AMONG_PUBLISHED, JSONAPI_FILTER_AMONG_ENABLED, or
|
Chris@18
|
345 * JSONAPI_FILTER_AMONG_OWN, then a condition group is returned for the union
|
Chris@18
|
346 * of allowed subsets. If no subsets are allowed, then static::alwaysFalse()
|
Chris@18
|
347 * is returned.
|
Chris@18
|
348 *
|
Chris@18
|
349 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
Chris@18
|
350 * The entity type for which to check filter access.
|
Chris@18
|
351 * @param \Drupal\Core\Session\AccountInterface $account
|
Chris@18
|
352 * The account for which to check access.
|
Chris@18
|
353 * @param \Drupal\Core\Cache\CacheableMetadata $cacheability
|
Chris@18
|
354 * Collects cacheability for the query.
|
Chris@18
|
355 *
|
Chris@18
|
356 * @return \Drupal\jsonapi\Query\EntityConditionGroup|null
|
Chris@18
|
357 * An EntityConditionGroup or NULL if no conditions need to be applied to
|
Chris@18
|
358 * secure an entity query.
|
Chris@18
|
359 */
|
Chris@18
|
360 protected static function getAccessConditionForKnownSubsets(EntityTypeInterface $entity_type, AccountInterface $account, CacheableMetadata $cacheability) {
|
Chris@18
|
361 // Get the combined access results for each JSONAPI_FILTER_AMONG_* subset.
|
Chris@18
|
362 $access_results = static::getAccessResultsFromEntityFilterHook($entity_type, $account);
|
Chris@18
|
363
|
Chris@18
|
364 // No conditions are needed if access is allowed for all entities.
|
Chris@18
|
365 $cacheability->addCacheableDependency($access_results[JSONAPI_FILTER_AMONG_ALL]);
|
Chris@18
|
366 if ($access_results[JSONAPI_FILTER_AMONG_ALL]->isAllowed()) {
|
Chris@18
|
367 return NULL;
|
Chris@18
|
368 }
|
Chris@18
|
369
|
Chris@18
|
370 // If filtering is not allowed across all entities, but is allowed for
|
Chris@18
|
371 // certain subsets, then add conditions that reflect those subsets. These
|
Chris@18
|
372 // will be grouped in an OR to reflect that access may be granted to
|
Chris@18
|
373 // more than one subset. If no conditions are added below, then
|
Chris@18
|
374 // static::alwaysFalse() is returned.
|
Chris@18
|
375 $conditions = [];
|
Chris@18
|
376
|
Chris@18
|
377 // The "published" subset.
|
Chris@18
|
378 $published_field_name = $entity_type->getKey('published');
|
Chris@18
|
379 if ($published_field_name) {
|
Chris@18
|
380 $access_result = $access_results[JSONAPI_FILTER_AMONG_PUBLISHED];
|
Chris@18
|
381 $cacheability->addCacheableDependency($access_result);
|
Chris@18
|
382 if ($access_result->isAllowed()) {
|
Chris@18
|
383 $conditions[] = new EntityCondition($published_field_name, 1);
|
Chris@18
|
384 $cacheability->addCacheTags($entity_type->getListCacheTags());
|
Chris@18
|
385 }
|
Chris@18
|
386 }
|
Chris@18
|
387
|
Chris@18
|
388 // The "enabled" subset.
|
Chris@18
|
389 // @todo Remove ternary when the 'status' key is added to the User entity type.
|
Chris@18
|
390 $status_field_name = $entity_type->id() === 'user' ? 'status' : $entity_type->getKey('status');
|
Chris@18
|
391 if ($status_field_name) {
|
Chris@18
|
392 $access_result = $access_results[JSONAPI_FILTER_AMONG_ENABLED];
|
Chris@18
|
393 $cacheability->addCacheableDependency($access_result);
|
Chris@18
|
394 if ($access_result->isAllowed()) {
|
Chris@18
|
395 $conditions[] = new EntityCondition($status_field_name, 1);
|
Chris@18
|
396 $cacheability->addCacheTags($entity_type->getListCacheTags());
|
Chris@18
|
397 }
|
Chris@18
|
398 }
|
Chris@18
|
399
|
Chris@18
|
400 // The "owner" subset.
|
Chris@18
|
401 // @todo Remove ternary when the 'uid' key is added to the User entity type.
|
Chris@18
|
402 $owner_field_name = $entity_type->id() === 'user' ? 'uid' : $entity_type->getKey('owner');
|
Chris@18
|
403 if ($owner_field_name) {
|
Chris@18
|
404 $access_result = $access_results[JSONAPI_FILTER_AMONG_OWN];
|
Chris@18
|
405 $cacheability->addCacheableDependency($access_result);
|
Chris@18
|
406 if ($access_result->isAllowed()) {
|
Chris@18
|
407 $cacheability->addCacheContexts(['user']);
|
Chris@18
|
408 if ($account->isAuthenticated()) {
|
Chris@18
|
409 $conditions[] = new EntityCondition($owner_field_name, $account->id());
|
Chris@18
|
410 $cacheability->addCacheTags($entity_type->getListCacheTags());
|
Chris@18
|
411 }
|
Chris@18
|
412 }
|
Chris@18
|
413 }
|
Chris@18
|
414
|
Chris@18
|
415 // If no conditions were added above, then access wasn't granted to any
|
Chris@18
|
416 // subset, so return alwaysFalse().
|
Chris@18
|
417 if (empty($conditions)) {
|
Chris@18
|
418 return static::alwaysFalse($entity_type);
|
Chris@18
|
419 }
|
Chris@18
|
420
|
Chris@18
|
421 // If more than one condition was added above, then access was granted to
|
Chris@18
|
422 // more than one subset, so combine them with an OR.
|
Chris@18
|
423 if (count($conditions) > 1) {
|
Chris@18
|
424 return new EntityConditionGroup('OR', $conditions);
|
Chris@18
|
425 }
|
Chris@18
|
426
|
Chris@18
|
427 // Otherwise return the single condition.
|
Chris@18
|
428 return $conditions[0];
|
Chris@18
|
429 }
|
Chris@18
|
430
|
Chris@18
|
431 /**
|
Chris@18
|
432 * Gets the combined access result for each JSONAPI_FILTER_AMONG_* subset.
|
Chris@18
|
433 *
|
Chris@18
|
434 * This invokes hook_jsonapi_entity_filter_access() and
|
Chris@18
|
435 * hook_jsonapi_ENTITY_TYPE_filter_access() and combines the results from all
|
Chris@18
|
436 * of the modules into a single set of results.
|
Chris@18
|
437 *
|
Chris@18
|
438 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
Chris@18
|
439 * The entity type for which to check filter access.
|
Chris@18
|
440 * @param \Drupal\Core\Session\AccountInterface $account
|
Chris@18
|
441 * The account for which to check access.
|
Chris@18
|
442 *
|
Chris@18
|
443 * @return \Drupal\Core\Access\AccessResultInterface[]
|
Chris@18
|
444 * The array of access results, keyed by subset. See
|
Chris@18
|
445 * hook_jsonapi_entity_filter_access() for details.
|
Chris@18
|
446 */
|
Chris@18
|
447 protected static function getAccessResultsFromEntityFilterHook(EntityTypeInterface $entity_type, AccountInterface $account) {
|
Chris@18
|
448 /* @var \Drupal\Core\Access\AccessResultInterface[] $combined_access_results */
|
Chris@18
|
449 $combined_access_results = [
|
Chris@18
|
450 JSONAPI_FILTER_AMONG_ALL => AccessResult::neutral(),
|
Chris@18
|
451 JSONAPI_FILTER_AMONG_PUBLISHED => AccessResult::neutral(),
|
Chris@18
|
452 JSONAPI_FILTER_AMONG_ENABLED => AccessResult::neutral(),
|
Chris@18
|
453 JSONAPI_FILTER_AMONG_OWN => AccessResult::neutral(),
|
Chris@18
|
454 ];
|
Chris@18
|
455
|
Chris@18
|
456 // Invoke hook_jsonapi_entity_filter_access() and
|
Chris@18
|
457 // hook_jsonapi_ENTITY_TYPE_filter_access() for each module and merge its
|
Chris@18
|
458 // results with the combined results.
|
Chris@18
|
459 foreach (['jsonapi_entity_filter_access', 'jsonapi_' . $entity_type->id() . '_filter_access'] as $hook) {
|
Chris@18
|
460 foreach (static::$moduleHandler->getImplementations($hook) as $module) {
|
Chris@18
|
461 $module_access_results = static::$moduleHandler->invoke($module, $hook, [$entity_type, $account]);
|
Chris@18
|
462 if ($module_access_results) {
|
Chris@18
|
463 foreach ($module_access_results as $subset => $access_result) {
|
Chris@18
|
464 $combined_access_results[$subset] = $combined_access_results[$subset]->orIf($access_result);
|
Chris@18
|
465 }
|
Chris@18
|
466 }
|
Chris@18
|
467 }
|
Chris@18
|
468 }
|
Chris@18
|
469
|
Chris@18
|
470 return $combined_access_results;
|
Chris@18
|
471 }
|
Chris@18
|
472
|
Chris@18
|
473 /**
|
Chris@18
|
474 * Gets an access condition for a comment entity.
|
Chris@18
|
475 *
|
Chris@18
|
476 * Unlike all other core entity types, Comment entities' access control
|
Chris@18
|
477 * depends on access to a referenced entity. More challenging yet, that entity
|
Chris@18
|
478 * reference field may target different entity types depending on the comment
|
Chris@18
|
479 * bundle. This makes the query access conditions sufficiently complex to
|
Chris@18
|
480 * merit a dedicated method.
|
Chris@18
|
481 *
|
Chris@18
|
482 * @param \Drupal\Core\Entity\EntityTypeInterface $comment_entity_type
|
Chris@18
|
483 * The comment entity type object.
|
Chris@18
|
484 * @param \Drupal\Core\Session\AccountInterface $current_user
|
Chris@18
|
485 * The current user.
|
Chris@18
|
486 * @param \Drupal\Core\Cache\CacheableMetadata $cacheability
|
Chris@18
|
487 * Collects cacheability for the query.
|
Chris@18
|
488 * @param int $depth
|
Chris@18
|
489 * Internal use only. The recursion depth. It is possible to have comments
|
Chris@18
|
490 * on comments, but since comment access is dependent on access to the
|
Chris@18
|
491 * entity on which they live, this method can recurse endlessly.
|
Chris@18
|
492 *
|
Chris@18
|
493 * @return \Drupal\jsonapi\Query\EntityConditionGroup|null
|
Chris@18
|
494 * An EntityConditionGroup or NULL if no conditions need to be applied to
|
Chris@18
|
495 * secure an entity query.
|
Chris@18
|
496 */
|
Chris@18
|
497 protected static function getCommentAccessCondition(EntityTypeInterface $comment_entity_type, AccountInterface $current_user, CacheableMetadata $cacheability, $depth = 1) {
|
Chris@18
|
498 // If a comment is assigned to another entity or author the cache needs to
|
Chris@18
|
499 // be invalidated.
|
Chris@18
|
500 $cacheability->addCacheTags($comment_entity_type->getListCacheTags());
|
Chris@18
|
501 // Constructs a big EntityConditionGroup which will filter comments based on
|
Chris@18
|
502 // the current user's access to the entities on which each comment lives.
|
Chris@18
|
503 // This is especially complex because comments of different bundles can
|
Chris@18
|
504 // live on entities of different entity types.
|
Chris@18
|
505 $comment_entity_type_id = $comment_entity_type->id();
|
Chris@18
|
506 $field_map = static::$fieldManager->getFieldMapByFieldType('entity_reference');
|
Chris@18
|
507 assert(isset($field_map[$comment_entity_type_id]['entity_id']['bundles']), 'Every comment has an `entity_id` field.');
|
Chris@18
|
508 $bundle_ids_by_target_entity_type_id = [];
|
Chris@18
|
509 foreach ($field_map[$comment_entity_type_id]['entity_id']['bundles'] as $bundle_id) {
|
Chris@18
|
510 $field_definitions = static::$fieldManager->getFieldDefinitions($comment_entity_type_id, $bundle_id);
|
Chris@18
|
511 $commented_entity_field_definition = $field_definitions['entity_id'];
|
Chris@18
|
512 // Each commented entity field definition has a setting which indicates
|
Chris@18
|
513 // the entity type of the commented entity reference field. This differs
|
Chris@18
|
514 // per bundle.
|
Chris@18
|
515 $target_entity_type_id = $commented_entity_field_definition->getSetting('target_type');
|
Chris@18
|
516 $bundle_ids_by_target_entity_type_id[$target_entity_type_id][] = $bundle_id;
|
Chris@18
|
517 }
|
Chris@18
|
518 $bundle_specific_access_conditions = [];
|
Chris@18
|
519 foreach ($bundle_ids_by_target_entity_type_id as $target_entity_type_id => $bundle_ids) {
|
Chris@18
|
520 // Construct a field specifier prefix which targets the commented entity.
|
Chris@18
|
521 $condition_field_prefix = "entity_id.entity:$target_entity_type_id";
|
Chris@18
|
522 // Ensure that for each possible commented entity type (which varies per
|
Chris@18
|
523 // bundle), a condition is created that restricts access based on access
|
Chris@18
|
524 // to the commented entity.
|
Chris@18
|
525 $bundle_condition = new EntityCondition($comment_entity_type->getKey('bundle'), $bundle_ids, 'IN');
|
Chris@18
|
526 // Comments on comments can create an infinite recursion! If the target
|
Chris@18
|
527 // entity type ID is comment, we need special behavior.
|
Chris@18
|
528 if ($target_entity_type_id === $comment_entity_type_id) {
|
Chris@18
|
529 $nested_comment_condition = $depth <= 3
|
Chris@18
|
530 ? static::getCommentAccessCondition($comment_entity_type, $current_user, $cacheability, $depth + 1)
|
Chris@18
|
531 : static::alwaysFalse($comment_entity_type);
|
Chris@18
|
532 $prefixed_comment_condition = static::addConditionFieldPrefix($nested_comment_condition, $condition_field_prefix);
|
Chris@18
|
533 $bundle_specific_access_conditions[$target_entity_type_id] = new EntityConditionGroup('AND', [$bundle_condition, $prefixed_comment_condition]);
|
Chris@18
|
534 }
|
Chris@18
|
535 else {
|
Chris@18
|
536 $target_condition = static::getAccessCondition($target_entity_type_id, $cacheability);
|
Chris@18
|
537 $bundle_specific_access_conditions[$target_entity_type_id] = !is_null($target_condition)
|
Chris@18
|
538 ? new EntityConditionGroup('AND', [
|
Chris@18
|
539 $bundle_condition,
|
Chris@18
|
540 static::addConditionFieldPrefix($target_condition, $condition_field_prefix),
|
Chris@18
|
541 ])
|
Chris@18
|
542 : $bundle_condition;
|
Chris@18
|
543 }
|
Chris@18
|
544 }
|
Chris@18
|
545
|
Chris@18
|
546 // This condition ensures that the user is only permitted to see the
|
Chris@18
|
547 // comments for which the user is also able to view the entity on which each
|
Chris@18
|
548 // comment lives.
|
Chris@18
|
549 $commented_entity_condition = new EntityConditionGroup('OR', array_values($bundle_specific_access_conditions));
|
Chris@18
|
550 return $commented_entity_condition;
|
Chris@18
|
551 }
|
Chris@18
|
552
|
Chris@18
|
553 /**
|
Chris@18
|
554 * Gets an always FALSE entity condition group for the given entity type.
|
Chris@18
|
555 *
|
Chris@18
|
556 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
Chris@18
|
557 * The entity type for which to construct an impossible condition.
|
Chris@18
|
558 *
|
Chris@18
|
559 * @return \Drupal\jsonapi\Query\EntityConditionGroup
|
Chris@18
|
560 * An EntityConditionGroup which cannot evaluate to TRUE.
|
Chris@18
|
561 */
|
Chris@18
|
562 protected static function alwaysFalse(EntityTypeInterface $entity_type) {
|
Chris@18
|
563 return new EntityConditionGroup('AND', [
|
Chris@18
|
564 new EntityCondition($entity_type->getKey('id'), 1, '<'),
|
Chris@18
|
565 new EntityCondition($entity_type->getKey('id'), 1, '>'),
|
Chris@18
|
566 ]);
|
Chris@18
|
567 }
|
Chris@18
|
568
|
Chris@18
|
569 /**
|
Chris@18
|
570 * Recursively collects all entity query condition fields.
|
Chris@18
|
571 *
|
Chris@18
|
572 * Entity conditions can be nested within AND and OR groups. This recursively
|
Chris@18
|
573 * finds all unique fields in an entity query condition.
|
Chris@18
|
574 *
|
Chris@18
|
575 * @param \Drupal\jsonapi\Query\EntityConditionGroup $group
|
Chris@18
|
576 * The root entity condition group.
|
Chris@18
|
577 * @param array $fields
|
Chris@18
|
578 * Internal use only.
|
Chris@18
|
579 *
|
Chris@18
|
580 * @return array
|
Chris@18
|
581 * An array of entity query condition field names.
|
Chris@18
|
582 */
|
Chris@18
|
583 protected static function collectFilteredFields(EntityConditionGroup $group, array $fields = []) {
|
Chris@18
|
584 foreach ($group->members() as $member) {
|
Chris@18
|
585 if ($member instanceof EntityConditionGroup) {
|
Chris@18
|
586 $fields = static::collectFilteredFields($member, $fields);
|
Chris@18
|
587 }
|
Chris@18
|
588 else {
|
Chris@18
|
589 $fields[] = $member->field();
|
Chris@18
|
590 }
|
Chris@18
|
591 }
|
Chris@18
|
592 return array_unique($fields);
|
Chris@18
|
593 }
|
Chris@18
|
594
|
Chris@18
|
595 /**
|
Chris@18
|
596 * Copied from \Drupal\jsonapi\IncludeResolver.
|
Chris@18
|
597 *
|
Chris@18
|
598 * @see \Drupal\jsonapi\IncludeResolver::buildTree()
|
Chris@18
|
599 */
|
Chris@18
|
600 protected static function buildTree(array $paths) {
|
Chris@18
|
601 $merged = [];
|
Chris@18
|
602 foreach ($paths as $parts) {
|
Chris@18
|
603 // This complex expression is needed to handle the string, "0", which
|
Chris@18
|
604 // would be evaluated as FALSE.
|
Chris@18
|
605 if (!is_null(($field_name = array_shift($parts)))) {
|
Chris@18
|
606 $previous = isset($merged[$field_name]) ? $merged[$field_name] : [];
|
Chris@18
|
607 $merged[$field_name] = array_merge($previous, [$parts]);
|
Chris@18
|
608 }
|
Chris@18
|
609 }
|
Chris@18
|
610 return !empty($merged) ? array_map([static::class, __FUNCTION__], $merged) : $merged;
|
Chris@18
|
611 }
|
Chris@18
|
612
|
Chris@18
|
613 }
|