 core/modules/node/node.services.yml                |   5 +
 .../src/Cache/NodeAccessViewGrantsCacheContext.php |  72 +++++++++++
 core/modules/node/src/NodeGrantDatabaseStorage.php |  24 +++-
 .../Tests/NodeAccessViewGrantsCacheContextTest.php | 143 +++++++++++++++++++++
 4 files changed, 237 insertions(+), 7 deletions(-)

diff --git a/core/modules/node/node.services.yml b/core/modules/node/node.services.yml
index 68f0af1..eb48e7e 100644
--- a/core/modules/node/node.services.yml
+++ b/core/modules/node/node.services.yml
@@ -39,3 +39,8 @@ services:
     arguments: ['@current_route_match']
     tags:
       - { name: page_cache_response_policy }
+  cache_context.node_view_grants:
+    class: Drupal\node\Cache\NodeAccessViewGrantsCacheContext
+    arguments: ['@current_user']
+    tags:
+      - { name: cache.context }
diff --git a/core/modules/node/src/Cache/NodeAccessViewGrantsCacheContext.php b/core/modules/node/src/Cache/NodeAccessViewGrantsCacheContext.php
new file mode 100644
index 0000000..d1944ef
--- /dev/null
+++ b/core/modules/node/src/Cache/NodeAccessViewGrantsCacheContext.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Cache\NodeAccessViewGrantsCacheContext.
+ */
+
+namespace Drupal\node\Cache;
+
+use Drupal\Core\Cache\CacheContextInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the node access view grants cache context service.
+ *
+ * This allows for node access grants-sensitive caching when viewing nodes.
+ *
+ * node_query_node_access_alter().
+ */
+class NodeAccessViewGrantsCacheContext implements CacheContextInterface {
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $user;
+
+  /**
+   * Constructs a new NodeAccessViewGrantsCacheContext service.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $user
+   *   The current user.
+   */
+  public function __construct(AccountInterface $user) {
+    $this->user = $user;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getLabel() {
+    return t("Content access view grants");
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getContext() {
+    // If the current user either:
+    // - can bypass node access
+    // - has a global view grant (such as a view grant for node ID 0) — note
+    //   that this is automatically the case if no node access modules exist (no
+    //   hook_node_grants() implementations)
+    // then we don't need to determine the exact node view grants for the
+    // current user.
+    if ($this->user->hasPermission('bypass node access')) {
+      return 'bypass';
+    }
+    if (node_access_view_all_nodes($this->user)) {
+      return 'all';
+    }
+
+    $grants = node_access_grants('view', $this->user);
+    $grants_context_parts = [];
+    foreach ($grants as $realm => $gids) {
+      $grants_context_parts[] = $realm . ':' . implode(',', $gids);
+    }
+    return implode(';', $grants_context_parts);
+  }
+
+}
diff --git a/core/modules/node/src/NodeGrantDatabaseStorage.php b/core/modules/node/src/NodeGrantDatabaseStorage.php
index 9cddb2e..a664862 100644
--- a/core/modules/node/src/NodeGrantDatabaseStorage.php
+++ b/core/modules/node/src/NodeGrantDatabaseStorage.php
@@ -88,16 +88,26 @@ public function access(NodeInterface $node, $operation, $langcode, AccountInterf
       $query->condition($grants);
     }
 
-    // Node grants currently don't have any cacheability metadata. Hopefully, we
-    // can add that in the future, which would allow this access check result to
-    // be cacheable. For now, this must remain marked as uncacheable, even when
-    // it is theoretically cacheable, because we don't have the necessary meta-
-    // data to know it for a fact.
+    // Only the 'view' node grant can currently be cached; the others currently
+    // don't have any cacheability metadata. Hopefully, we can add that in the
+    // future, which would allow this access check result to be cacheable in all
+    // cases. For now, this must remain marked as uncacheable, even when it is
+    // theoretically cacheable, because we don't have the necessary metadata to
+    // know it for a fact.
+    $set_cacheability = function (AccessResult $access_result, $op) {
+      if ($op === 'view') {
+        return $access_result->addCacheContexts('cache_context.node_view_grants');
+      }
+      else {
+        return $access_result->setCacheable(FALSE);
+      }
+    };
+
     if ($query->execute()->fetchField()) {
-      return AccessResult::allowed()->setCacheable(FALSE);
+      return $set_cacheability(AccessResult::allowed());
     }
     else {
-      return AccessResult::forbidden()->setCacheable(FALSE);
+      return $set_cacheability(AccessResult::forbidden());
     }
   }
 
diff --git a/core/modules/node/src/Tests/NodeAccessViewGrantsCacheContextTest.php b/core/modules/node/src/Tests/NodeAccessViewGrantsCacheContextTest.php
new file mode 100644
index 0000000..94adac7
--- /dev/null
+++ b/core/modules/node/src/Tests/NodeAccessViewGrantsCacheContextTest.php
@@ -0,0 +1,143 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Tests\NodeAccessViewGrantsCacheContextTest.
+ */
+
+namespace Drupal\node\Tests;
+
+/**
+ * Tests the node access view grants cache context service.
+ *
+ * @group node
+ * @group Cache
+ */
+class NodeAccessViewGrantsCacheContextTest extends NodeTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('node_access_test');
+
+  /**
+   * User with permission to view content.
+   */
+  protected $accessUser;
+
+  /**
+   * User without permission to view content.
+   */
+  protected $noAccessUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    node_access_rebuild();
+
+    // Create some content.
+    $this->drupalCreateNode();
+    $this->drupalCreateNode();
+    $this->drupalCreateNode();
+    $this->drupalCreateNode();
+
+    // Create user with simple node access permission. The 'node test view'
+    // permission is implemented and granted by the node_access_test module.
+    $this->accessUser = $this->drupalCreateUser(array('access content overview', 'access content', 'node test view'));
+    $this->noAccessUser = $this->drupalCreateUser(array('access content overview', 'access content'));
+    $this->noAccessUser2 = $this->drupalCreateUser(array('access content overview', 'access content'));
+
+    $this->userMapping = [
+      1 => $this->root_user,
+      2 => $this->accessUser,
+      3 => $this->noAccessUser,
+    ];
+  }
+
+  /**
+   * Asserts that for each given user, the expected cache context is returned.
+   *
+   * @param array $expected
+   *   Expected values, keyed by user ID, expected cache contexts as values.
+   */
+  protected function assertCacheContext(array $expected) {
+    foreach ($expected as $uid => $context) {
+      if ($uid > 0) {
+        $this->drupalLogin($this->userMapping[$uid]);
+      }
+      $this->pass('Asserting cache context for user ' . $uid . '.');
+      $this->assertIdentical($context, $this->container->get('cache_context.node_view_grants')->getContext());
+    }
+    $this->drupalLogout();
+  }
+
+  /**
+   * Tests NodeAccessViewGrantsCacheContext::getContext().
+   */
+  public function testCacheContext() {
+    $this->assertCacheContext([
+      0 => 'all:0;node_access_test_author:0;node_access_all:0',
+      1 => 'bypass',
+      2 => 'all:0;node_access_test_author:2;node_access_test:8888,8889',
+      3 => 'all:0;node_access_test_author:3',
+    ]);
+
+    // Grant view to all nodes (because nid = 0) for users in the
+    // 'node_access_all' realm.
+    $record = array(
+      'nid' => 0,
+      'gid' => 0,
+      'realm' => 'node_access_all',
+      'grant_view' => 1,
+      'grant_update' => 0,
+      'grant_delete' => 0,
+    );
+    db_insert('node_access')->fields($record)->execute();
+
+    // Put user accessUser (uid 0) in the realm.
+    \Drupal::state()->set('node_access_test.no_access_uid', 0);
+    drupal_static_reset('node_access_view_all_nodes');
+    $this->assertCacheContext([
+      0 => 'all',
+      1 => 'bypass',
+      2 => 'all:0;node_access_test_author:2;node_access_test:8888,8889',
+      3 => 'all:0;node_access_test_author:3',
+    ]);
+
+    // Put user accessUser (uid 2) in the realm.
+    \Drupal::state()->set('node_access_test.no_access_uid', $this->accessUser->id());
+    drupal_static_reset('node_access_view_all_nodes');
+    $this->assertCacheContext([
+      0 => 'all:0;node_access_test_author:0',
+      1 => 'bypass',
+      2 => 'all',
+      3 => 'all:0;node_access_test_author:3',
+    ]);
+
+    // Put user noAccessUser (uid 3) in the realm.
+    \Drupal::state()->set('node_access_test.no_access_uid', $this->noAccessUser->id());
+    drupal_static_reset('node_access_view_all_nodes');
+    $this->assertCacheContext([
+      0 => 'all:0;node_access_test_author:0',
+      1 => 'bypass',
+      2 => 'all:0;node_access_test_author:2;node_access_test:8888,8889',
+      3 => 'all',
+    ]);
+
+    // Uninstall the node_access_test module
+    $this->container->get('module_installer')->uninstall(['node_access_test']);
+    drupal_static_reset('node_access_view_all_nodes');
+    $this->assertCacheContext([
+      0 => 'all',
+      1 => 'bypass',
+      2 => 'all',
+      3 => 'all',
+    ]);
+  }
+
+}
