diff --git a/core/lib/Drupal/Core/Database/database.api.php b/core/lib/Drupal/Core/Database/database.api.php
index 82b6408669ae269846978bed5b5c35960f99ea1a..a7ccdc32a08a9b1a2574dd3bc6ebcd6c42897f30 100644
--- a/core/lib/Drupal/Core/Database/database.api.php
+++ b/core/lib/Drupal/Core/Database/database.api.php
@@ -5,7 +5,7 @@
  * Hooks related to the Database system and the Schema API.
  */
 
-use Drupal\Core\Database\Query\Condition;
+use Drupal\Core\Database\Query\SelectInterface;
 
 /**
  * @defgroup database Database abstraction layer
@@ -425,6 +425,12 @@ function hook_query_alter(Drupal\Core\Database\Query\AlterableInterface $query)
 /**
  * Perform alterations to a structured query for a given tag.
  *
+ * Some common tags include:
+ * - 'entity_reference': For queries that return entities that may be referenced
+ *   by an entity reference field.
+ * - ENTITY_TYPE . '_access': For queries of entities that will be displayed in
+ *   a listing (e.g., from Views) and therefore require access control.
+ *
  * @param $query
  *   An Query object describing the composite parts of a SQL query.
  *
@@ -436,34 +442,40 @@ function hook_query_alter(Drupal\Core\Database\Query\AlterableInterface $query)
  * @ingroup database
  */
 function hook_query_TAG_alter(Drupal\Core\Database\Query\AlterableInterface $query) {
-  // Skip the extra expensive alterations if site has no node access control modules.
-  if (!node_access_view_all_nodes()) {
-    // Prevent duplicates records.
-    $query->distinct();
-    // The recognized operations are 'view', 'update', 'delete'.
-    if (!$op = $query->getMetaData('op')) {
-      $op = 'view';
-    }
-    // Skip the extra joins and conditions for node admins.
-    if (!\Drupal::currentUser()->hasPermission('bypass node access')) {
-      // The node_access table has the access grants for any given node.
-      $access_alias = $query->join('node_access', 'na', '%alias.nid = n.nid');
-      $or = new Condition('OR');
-      // If any grant exists for the specified user, then user has access to the node for the specified operation.
-      foreach (node_access_grants($op, $query->getMetaData('account')) as $realm => $gids) {
-        foreach ($gids as $gid) {
-          $or->condition((new Condition('AND'))
-            ->condition($access_alias . '.gid', $gid)
-            ->condition($access_alias . '.realm', $realm)
-          );
-        }
-      }
+  // This is an example of a possible hook_query_media_access_alter()
+  // implementation. In other words, alter queries of media entities that
+  // require access control (have the 'media_access' query tag).
 
-      if (count($or->conditions())) {
-        $query->condition($or);
-      }
+  // Determine which media entities we want to remove from the query. In this
+  // example, we hard-code some media IDs.
+  $media_entities_to_hide = [1, 3];
+
+  // In this example, we're only interested in applying our media access
+  // restrictions to SELECT queries. hook_media_access() can be used to apply
+  // access control to 'update' and 'delete' operations.
+  if (!($query instanceof SelectInterface)) {
+    return;
+  }
 
-      $query->condition($access_alias . 'grant_' . $op, 1, '>=');
+  // The tables in the query. This can include media entity tables and other
+  // tables. Tables might be joined more than once, with aliases.
+  $query_tables = $query->getTables();
+
+  // The tables belonging to media entity storage.
+  $table_mapping = \Drupal::entityTypeManager()->getStorage('media')->getTableMapping();
+  $media_tables = $table_mapping->getTableNames();
+
+  // For each table in the query, if it's a media entity storage table, add a
+  // condition to filter out records belonging to a media entity that we wish
+  // to hide.
+  foreach ($query_tables as $alias => $info) {
+    // Skip over subqueries.
+    if ($info['table'] instanceof SelectInterface) {
+      continue;
+    }
+    $real_table_name = $info['table'];
+    if (in_array($real_table_name, $media_tables)) {
+      $query->condition("$alias.mid", $media_entities_to_hide, 'NOT IN');
     }
   }
 }
diff --git a/core/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php
index f557458d380193e1ca3cab9a13dbed565d9f88a2..d2ffcc4caeb0ad9691a31dc0ba583b550390d2dd 100644
--- a/core/lib/Drupal/Core/Entity/entity.api.php
+++ b/core/lib/Drupal/Core/Entity/entity.api.php
@@ -634,6 +634,10 @@
 /**
  * Control entity operation access.
  *
+ * Note that this hook is not called for listings (e.g., from entity queries
+ * and Views). For nodes, see @link node_access Node access rights @endlink for
+ * a full explanation. For other entity types, see hook_query_TAG_alter().
+ *
  * @param \Drupal\Core\Entity\EntityInterface $entity
  *   The entity to check access to.
  * @param string $operation
@@ -654,6 +658,7 @@
  * @see \Drupal\Core\Entity\EntityAccessControlHandler
  * @see hook_entity_create_access()
  * @see hook_ENTITY_TYPE_access()
+ * @see hook_query_TAG_alter()
  *
  * @ingroup entity_api
  */
@@ -665,6 +670,10 @@ function hook_entity_access(\Drupal\Core\Entity\EntityInterface $entity, $operat
 /**
  * Control entity operation access for a specific entity type.
  *
+ * Note that this hook is not called for listings (e.g., from entity queries
+ * and Views). For nodes, see @link node_access Node access rights @endlink for
+ * a full explanation. For other entity types, see hook_query_TAG_alter().
+ *
  * @param \Drupal\Core\Entity\EntityInterface $entity
  *   The entity to check access to.
  * @param string $operation
@@ -678,6 +687,7 @@ function hook_entity_access(\Drupal\Core\Entity\EntityInterface $entity, $operat
  * @see \Drupal\Core\Entity\EntityAccessControlHandler
  * @see hook_ENTITY_TYPE_create_access()
  * @see hook_entity_access()
+ * @see hook_query_TAG_alter()
  *
  * @ingroup entity_api
  */
diff --git a/core/modules/views/src/EntityViewsData.php b/core/modules/views/src/EntityViewsData.php
index cf886f4caf895b971ead4b71e7e52ced341c38b4..f8d8ed6301707009d33f3bde12a53a208459e5ec 100644
--- a/core/modules/views/src/EntityViewsData.php
+++ b/core/modules/views/src/EntityViewsData.php
@@ -178,6 +178,7 @@ public function getViewsData() {
       'field' => $base_field,
       'title' => $this->entityType->getLabel(),
       'cache_contexts' => $this->entityType->getListCacheContexts(),
+      'access query tag' => $this->entityType->id() . '_access',
     ];
     $data[$base_table]['table']['entity revision'] = FALSE;
 
diff --git a/core/modules/views/tests/modules/views_test_query_access/views_test_query_access.info.yml b/core/modules/views/tests/modules/views_test_query_access/views_test_query_access.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..eb54049ccad1e228ba5e4557e067a1aa918ddb6d
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_query_access/views_test_query_access.info.yml
@@ -0,0 +1,8 @@
+name: 'Views test query access'
+type: module
+description: 'Module to test entity query access in Views.'
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+  - drupal:views
diff --git a/core/modules/views/tests/modules/views_test_query_access/views_test_query_access.module b/core/modules/views/tests/modules/views_test_query_access/views_test_query_access.module
new file mode 100644
index 0000000000000000000000000000000000000000..b120c83ea9297cfa29ba13612040ddf43a229084
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_query_access/views_test_query_access.module
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * @file
+ * Module to test entity query access in Views.
+ */
+
+use Drupal\Core\Database\Query\AlterableInterface;
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
+use Drupal\Core\Entity\Sql\DefaultTableMapping;
+
+/**
+ * Implements hook_query_TAG_alter() for the 'media_access' query tag.
+ */
+function views_test_query_access_query_media_access_alter(AlterableInterface $query) {
+  _views_test_query_access_restrict_by_uuid($query);
+}
+
+/**
+ * Implements hook_query_TAG_alter() for the 'block_content_access' query tag.
+ */
+function views_test_query_access_query_block_content_access_alter(AlterableInterface $query) {
+  _views_test_query_access_restrict_by_uuid($query);
+}
+
+/**
+ * Excludes entities with the 'hidden-ENTITY_TYPE_ID' UUID from a query.
+ *
+ * @param \Drupal\Core\Database\Query\AlterableInterface $query
+ *   The Views select query to alter.
+ */
+function _views_test_query_access_restrict_by_uuid(AlterableInterface $query) {
+  if (!($query instanceof SelectInterface)) {
+    return;
+  }
+
+  /** @var \Drupal\views\ViewExecutable $view */
+  $view = $query->getMetaData('view');
+  $entity_type = $view->getBaseEntityType();
+
+  $storage = \Drupal::entityTypeManager()->getStorage($entity_type->id());
+  if (!($storage instanceof SqlEntityStorageInterface)) {
+    return;
+  }
+
+  $table_mapping = $storage->getTableMapping();
+  if (!($table_mapping instanceof DefaultTableMapping)) {
+    return;
+  }
+
+  $base_table = $table_mapping->getBaseTable();
+  $data_table = $table_mapping->getDataTable();
+
+  // We are excluding entities by UUID, which means we need to be certain the
+  // base table is joined in the query.
+  $tables = $query->getTables();
+  if (isset($tables[$data_table]) && !isset($tables[$base_table])) {
+    $data_table_alias = $tables[$data_table]['alias'];
+    $id_key = $entity_type->getKey('id');
+    $base_table = $query->innerJoin($base_table, NULL, "$data_table_alias.$id_key = $base_table.$id_key");
+  }
+
+  // Figure out the column name of the UUID field and add a condition on that.
+  $base_field_definitions = \Drupal::service('entity_field.manager')
+    ->getBaseFieldDefinitions($entity_type->id());
+  $uuid_key = $entity_type->getKey('uuid');
+  $uuid_column_name = $table_mapping->getFieldColumnName($base_field_definitions[$uuid_key], NULL);
+  $query->condition("$base_table.$uuid_column_name", 'hidden-' . $entity_type->id(), '<>');
+}
diff --git a/core/modules/views/tests/src/Functional/Entity/EntityQueryAccessTest.php b/core/modules/views/tests/src/Functional/Entity/EntityQueryAccessTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..73a7f655109d5c277a677efa376aa2cdf842dfb4
--- /dev/null
+++ b/core/modules/views/tests/src/Functional/Entity/EntityQueryAccessTest.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Drupal\Tests\views\Functional\Entity;
+
+use Drupal\block_content\Entity\BlockContent;
+use Drupal\block_content\Entity\BlockContentType;
+use Drupal\media\Entity\Media;
+use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
+use Drupal\Tests\views\Functional\ViewTestBase;
+
+/**
+ * Tests that Views respects 'ENTITY_TYPE_access' query tags.
+ *
+ * @group views
+ */
+class EntityQueryAccessTest extends ViewTestBase {
+
+  use MediaTypeCreationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'media_test_source',
+    'views_test_query_access',
+  ];
+
+  /**
+   * Tests that the 'media_access' query tag is respected by Views.
+   */
+  public function testMediaEntityQueryAccess() {
+    $this->container->get('module_installer')->install(['media']);
+
+    $media_type = $this->createMediaType('test');
+    $source_field = $media_type->getSource()
+      ->getSourceFieldDefinition($media_type)
+      ->getName();
+
+    $hidden_media = Media::create([
+      'bundle' => $media_type->id(),
+      // This UUID should prevent this media item from being visible in the
+      // view.
+      // @see views_test_access_query_media_access_alter()
+      'uuid' => 'hidden-media',
+      'name' => $this->randomString(),
+      $source_field => $this->randomString(),
+    ]);
+    $hidden_media->save();
+
+    $accessible_media = Media::create([
+      'bundle' => $media_type->id(),
+      'name' => $this->randomString(),
+      $source_field => $this->randomString(),
+    ]);
+    $accessible_media->save();
+
+    $account = $this->drupalCreateUser([
+      'access media overview',
+      'administer media',
+    ]);
+    $this->drupalLogin($account);
+
+    $this->drupalGet('/admin/content/media');
+    $assert_session = $this->assertSession();
+    $assert_session->statusCodeEquals(200);
+    $assert_session->linkExists($accessible_media->label());
+    $assert_session->linkNotExists($hidden_media->label());
+  }
+
+  /**
+   * Tests that the 'block_content_access' query tag is respected by Views.
+   */
+  public function testBlockContentEntityQueryAccess() {
+    $this->container->get('module_installer')->install(['block_content']);
+
+    BlockContentType::create([
+      'id' => 'test',
+      'label' => 'Test',
+    ])->save();
+
+    $hidden_block = BlockContent::create([
+      'type' => 'test',
+      // This UUID should prevent this block from being visible in the view.
+      // @see views_test_access_query_block_content_access_alter()
+      'uuid' => 'hidden-block_content',
+      'info' => $this->randomString(),
+    ]);
+    $hidden_block->save();
+
+    $accessible_block = BlockContent::create([
+      'type' => 'test',
+      'info' => $this->randomString(),
+    ]);
+    $accessible_block->save();
+
+    $account = $this->drupalCreateUser([
+      'administer blocks',
+    ]);
+    $this->drupalLogin($account);
+
+    $this->drupalGet('/admin/structure/block/block-content');
+    $assert_session = $this->assertSession();
+    $assert_session->statusCodeEquals(200);
+    $assert_session->pageTextContains($accessible_block->label());
+    $assert_session->pageTextNotContains($hidden_block->label());
+  }
+
+}