annotate core/modules/jsonapi/src/Access/TemporaryQueryGuard.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
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 }