 .../Core/Entity/EntityAccessControlHandler.php     | 23 +++------
 .../Entity/EntityAccessControlHandlerTest.php      | 57 ++++++++++++++++++++++
 .../src/EntityTestAccessControlHandler.php         |  2 +
 3 files changed, 67 insertions(+), 15 deletions(-)

diff --git a/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php b/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php
index e655880..f0cb4eb 100644
--- a/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php
+++ b/core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php
@@ -61,26 +61,19 @@ public function access(EntityInterface $entity, $operation, $langcode = Language
       return $return_as_object ? $return : $return->isAllowed();
     }
 
-    // Invoke hook_entity_access() and hook_ENTITY_TYPE_access(). Hook results
-    // take precedence over overridden implementations of
-    // EntityAccessControlHandler::checkAccess(). Entities that have checks that
-    // need to be done before the hook is invoked should do so by overriding
-    // this method.
-
-    // We grant access to the entity if both of these conditions are met:
-    // - No modules say to deny access.
-    // - At least one module says to grant access.
+    // Invoke hook_entity_access() and hook_ENTITY_TYPE_access().
     $access = array_merge(
       $this->moduleHandler()->invokeAll('entity_access', array($entity, $operation, $account, $langcode)),
       $this->moduleHandler()->invokeAll($entity->getEntityTypeId() . '_access', array($entity, $operation, $account, $langcode))
     );
-
     $return = $this->processAccessHookResults($access);
-    if (!$return->isAllowed() && !$return->isForbidden()) {
-      // No module had an opinion about the access, so let's the access
-      // handler check access.
-      $return->orIf($this->checkAccess($entity, $operation, $langcode, $account));
-    }
+
+    // Hook results are not considered alone, it's also combined with the result
+    // of the overriden implementations of EntityAccessControlHandler::checkAccess().
+    // Entities that have want to change this behavior should do so by
+    // overriding this method.
+    $return->orIf($this->checkAccess($entity, $operation, $langcode, $account));
+
     $result = $this->setCache($return, $entity->uuid(), $operation, $langcode, $account);
     return $return_as_object ? $result : $result->isAllowed();
   }
diff --git a/core/modules/system/src/Tests/Entity/EntityAccessControlHandlerTest.php b/core/modules/system/src/Tests/Entity/EntityAccessControlHandlerTest.php
index 76e87f4..61e6502 100644
--- a/core/modules/system/src/Tests/Entity/EntityAccessControlHandlerTest.php
+++ b/core/modules/system/src/Tests/Entity/EntityAccessControlHandlerTest.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Access\AccessibleInterface;
 use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\entity_test\Entity\EntityTest;
 use Drupal\language\Entity\ConfigurableLanguage;
 
 /**
@@ -134,4 +135,60 @@ public function testHooks() {
     $this->assertEqual($state->get('entity_test_entity_access'), TRUE);
     $this->assertEqual($state->get('entity_test_entity_test_access'), TRUE);
   }
+
+  /**
+   * Verifies that EntityAccessControlHandler always calls ::checkAccess().
+   *
+   * Regression test for https://www.drupal.org/node/2204363.
+   */
+  public function testCheckAccessRunsAlways() {
+    $state = $this->container->get('state');
+
+    $operation = 'scream';
+
+    $entity = EntityTest::create([
+      'name' => 'checkAccess() test',
+    ]);
+    $entity->save();
+
+    // Reset the state, so we can cleanly check whether the hooks
+    $reset = function() use ($state) {
+      // Reset the state, so we can once again check if the hooks and
+      // ::checkAccess() are called.
+      $state->set('entity_test_entity_access', FALSE);
+      $state->set('entity_test_entity_test_access', FALSE);
+      $state->set('EntityTestAccessControlHandler::checkAccess', FALSE);
+      $this->assertFalse($state->get('entity_test_entity_access'));
+      $this->assertFalse($state->get('entity_test_entity_test_access'));
+      $this->assertFalse($state->get('EntityTestAccessControlHandler::checkAccess'));
+      $this->container->get('entity.manager')->getAccessControlHandler('entity_test')->resetCache();
+    };
+
+
+    // Make sure that >0 of hook_entity_access() and hook_ENTITY_TYPE_access()
+    // return an access result that grants access. In this situation, Drupal
+    // did *not* use to invoke ::checkAccess(), but it should, to match the
+    // developer's expectations. ::checkAccess() should still be able to forbid
+    // access.
+    $reset();
+    $state->set('entity_test_entity_access.' . $operation . '.' . $entity->id(), TRUE);
+    $entity->access($operation);
+
+    $this->assertTrue($state->get('entity_test_entity_access'), 'hook_entity_access() executed.');
+    $this->assertTrue($state->get('entity_test_entity_test_access'), 'hook_ENTITY_TYPE_entity_access() executed.');
+    $this->assertTrue($state->get('EntityTestAccessControlHandler::checkAccess'), 'EntityAccessControlHandler::checkAccess() executed.');
+
+
+    // Make sure both hook_entity_access() and hook_ENTITY_TYPE_access() return
+    // access results that neither allow nor forbid access. In this situation,
+    // Drupal has *always* invoked ::checkAccess().
+    $reset();
+    $state->set('entity_test_entity_access.' . $operation . '.' . $entity->id(), FALSE);
+    $entity->access($operation);
+
+    $this->assertTrue($state->get('entity_test_entity_access'), 'hook_entity_access() executed.');
+    $this->assertTrue($state->get('entity_test_entity_test_access'), 'hook_ENTITY_TYPE_entity_access() executed.');
+    $this->assertTrue($state->get('EntityTestAccessControlHandler::checkAccess'), 'EntityAccessControlHandler::checkAccess() executed.');
+  }
+
 }
diff --git a/core/modules/system/tests/modules/entity_test/src/EntityTestAccessControlHandler.php b/core/modules/system/tests/modules/entity_test/src/EntityTestAccessControlHandler.php
index 95efd80..18affff 100644
--- a/core/modules/system/tests/modules/entity_test/src/EntityTestAccessControlHandler.php
+++ b/core/modules/system/tests/modules/entity_test/src/EntityTestAccessControlHandler.php
@@ -30,6 +30,8 @@ class EntityTestAccessControlHandler extends EntityAccessControlHandler {
    * {@inheritdoc}
    */
   protected function checkAccess(EntityInterface $entity, $operation, $langcode, AccountInterface $account) {
+    \Drupal::state()->set('EntityTestAccessControlHandler::checkAccess', TRUE);
+
     if ($operation === 'view') {
       if ($langcode != LanguageInterface::LANGCODE_DEFAULT) {
         return AccessResult::allowedIfHasPermission($account, 'view test entity translations');
