diff --git a/core/lib/Drupal/Core/Entity/EntityAccessController.php b/core/lib/Drupal/Core/Entity/EntityAccessController.php
index a64426a..d7c62e3 100644
--- a/core/lib/Drupal/Core/Entity/EntityAccessController.php
+++ b/core/lib/Drupal/Core/Entity/EntityAccessController.php
@@ -10,39 +10,190 @@
 use Drupal\user\Plugin\Core\Entity\User;
 
 /**
- * Defines a base class for entity access controllers.
- *
- * Defaults to FALSE (access denied) for 'view', 'create', 'update' and 'delete'
- * access checks.
+ * Defines a default implementation for entity access controllers.
  */
 class EntityAccessController implements EntityAccessControllerInterface {
 
   /**
+   * Stores calculcated access check results.
+   *
+   * @var array
+   */
+  protected $accessCache = array();
+
+  /**
    * Implements \Drupal\Core\Entity\EntityAccessControllerInterface::viewAccess().
    */
   public function viewAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
-    return FALSE;
+    if (($access = $this->getCache($entity, 'view', $langcode, $account)) !== NULL) {
+      return $access;
+    }
+
+    $access = (bool) $this->access($entity, 'view', $langcode, $account);
+    return $this->setCache($access, $entity, 'view', $langcode, $account);
   }
 
   /**
    * Implements \Drupal\Core\Entity\EntityAccessControllerInterface::createAccess().
    */
   public function createAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
-    return FALSE;
+    if (($access = $this->getCache($entity, 'create', $langcode, $account)) !== NULL) {
+      return $access;
+    }
+
+    $access = (bool) $this->access($entity, 'create', $langcode, $account);
+    return $this->setCache($access, $entity, 'create', $langcode, $account);
   }
 
   /**
    * Implements \Drupal\Core\Entity\EntityAccessControllerInterface::updateAccess().
    */
   public function updateAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
-    return FALSE;
+    if (($access = $this->getCache($entity, 'update', $langcode, $account)) !== NULL) {
+      return $access;
+    }
+
+    $access = (bool) $this->access($entity, 'update', $langcode, $account);
+    return $this->setCache($access, $entity, 'update', $langcode, $account);
   }
 
   /**
    * Implements \Drupal\Core\Entity\EntityAccessControllerInterface::deleteAccess().
    */
   public function deleteAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
-    return FALSE;
+    if (($access = $this->getCache($entity, 'delete', $langcode, $account)) !== NULL) {
+      return $access;
+    }
+
+    $access = (bool) $this->access($entity, 'delete', $langcode, $account);
+    return $this->setCache($access, $entity, 'delete', $langcode, $account);
+  }
+
+  /**
+   * Performs default, shared access checks.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity for which to check 'create' access.
+   * @param string $operation
+   *   The entity operation. Usually one of 'view', 'edit', 'create' or
+   *   'delete'.
+   * @param string $langcode
+   *   (optional) The language code for which to check access. Defaults to
+   *   LANGUAGE_DEFAULT.
+   * @param \Drupal\user\Plugin\Core\Entity\User $account
+   *   (optional) The user for which to check access, or NULL to check access
+   *   for the current user. Defaults to NULL.
+   *
+   * @return bool|null
+   *   TRUE if access was granted, FALSE if access was denied and NULL if access
+   *   could not be reliably determined.
+   */
+  protected function access(EntityInterface $entity, $operation, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
+    $account = $account ?: user_load($GLOBALS['user']->uid);
+
+    // 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.
+    $access = module_invoke_all($entity->entityType() . '_access', $entity, $operation, $account, $langcode);
+    if (in_array(FALSE, $access, TRUE)) {
+      return FALSE;
+    }
+    elseif (in_array(TRUE, $access, TRUE)) {
+      return TRUE;
+    }
+  }
+
+  /**
+   * Retrieves the cache id for the given entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity for which to check 'create' access.
+   * @param string $operation
+   *   The entity operation. Usually one of 'view', 'edit', 'create' or
+   *   'delete'.
+   * @param string $langcode
+   *   (optional) The language code for which to check access. Defaults to
+   *   LANGUAGE_DEFAULT.
+   * @param \Drupal\user\Plugin\Core\Entity\User $account
+   *   (optional) The user for which to check access, or NULL to check access
+   *   for the current user. Defaults to NULL.
+   *
+   * @return string
+   *   The cache id as used by EntityAccessController::setCache() and
+   *   EntityAccessController::getCache().
+   */
+  protected function cacheId(EntityInterface $entity, $operation, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
+    // In some cases (e.g. 'create' access checks) the entity has not been saved
+    // yet and therefore does not have an id in which case we use the bundle as
+    // identifier for the cache entry.
+    return $entity->isNew() ? $entity->bundle() : $entity->id();
+  }
+
+  /**
+   * Tries to retrieve a previously cached access value from the static cache.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity for which to check 'create' access.
+   * @param string $operation
+   *   The entity operation. Usually one of 'view', 'edit', 'create' or
+   *   'delete'.
+   * @param string $langcode
+   *   (optional) The language code for which to check access. Defaults to
+   *   LANGUAGE_DEFAULT.
+   * @param \Drupal\user\Plugin\Core\Entity\User $account
+   *   (optional) The user for which to check access, or NULL to check access
+   *   for the current user. Defaults to NULL.
+   *
+   * @return bool|null
+   *   TRUE if access was granted, FALSE if access was denied and NULL if there
+   *   is no record for the given user, operation, langcode and entity in the
+   *   cache.
+   */
+  protected function getCache(EntityInterface $entity, $operation, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
+    // @todo Remove this once we can rely on $account.
+    $account = $account ?: user_load($GLOBALS['user']->uid);
+    $cid = $this->cacheId($entity, $operation, $langcode, $account);
+    $uid = $account->id();
+
+    // Return from cache if a value has been set for it previously.
+    if (isset($this->accessCache[$uid][$cid][$langcode][$operation])) {
+      return $this->accessCache[$uid][$cid][$langcode][$operation];
+    }
+  }
+
+  /**
+   * Statically caches whether the given user has access.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity for which to check 'create' access.
+   * @param string $operation
+   *   The entity operation. Usually one of 'view', 'edit', 'create' or
+   *   'delete'.
+   * @param string $langcode
+   *   (optional) The language code for which to check access. Defaults to
+   *   LANGUAGE_DEFAULT.
+   * @param \Drupal\user\Plugin\Core\Entity\User $account
+   *   (optional) The user for which to check access, or NULL to check access
+   *   for the current user. Defaults to NULL.
+   *
+   * @return bool
+   *   TRUE if access was granted, FALSE otherwise.
+   */
+  protected function setCache($access, EntityInterface $entity, $operation, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
+    // @todo Remove this once we can rely on $account.
+    $account = $account ?: user_load($GLOBALS['user']->uid);
+    $cid = $this->cacheId($entity, $operation, $langcode, $account);
+    $uid = $account->id();
+
+    // Save the given value in the static cache and directly return it.
+    return $this->accessCache[$uid][$cid][$langcode][$operation] = (bool) $access;
+  }
+
+  /**
+   * Implements \Drupal\Core\Entity\EntityAccessControllerInterface::clearCache().
+   */
+  public function resetCache() {
+    $this->accessCache = array();
   }
 
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityAccessControllerInterface.php b/core/lib/Drupal/Core/Entity/EntityAccessControllerInterface.php
index a40477c..66730f6 100644
--- a/core/lib/Drupal/Core/Entity/EntityAccessControllerInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityAccessControllerInterface.php
@@ -82,4 +82,9 @@ public function updateAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAU
    *   TRUE if access was granted, FALSE otherwise.
    */
   public function deleteAccess(EntityInterface $entity, $langcode = LANGUAGE_DEFAULT, User $account = NULL);
+
+  /**
+   * Clears all cached access checks.
+   */
+  public function resetCache();
 }
diff --git a/core/modules/node/lib/Drupal/node/NodeAccessController.php b/core/modules/node/lib/Drupal/node/NodeAccessController.php
new file mode 100644
index 0000000..d27cc52
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/NodeAccessController.php
@@ -0,0 +1,133 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\NodeAccessController.
+ */
+
+namespace Drupal\node;
+
+use Drupal\user\Plugin\Core\Entity\User;
+use Drupal\Core\Entity\EntityAccessController;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityNG;
+
+/**
+ * Form controller for the node edit forms.
+ */
+class NodeAccessController extends EntityAccessController {
+
+  /**
+   * Overrides \Drupal\Core\Entity\EntityAccessController::viewAccess().
+   */
+  public function viewAccess(EntityInterface $node, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
+    if (($cached = $this->getCache($node, 'view', $langcode, $account)) !== NULL ) {
+      return $cached;
+    }
+
+    if (($access = $this->access($node, 'view', $langcode, $account)) !== NULL) {
+      return $this->setCache((bool) $access, $node, 'view', $langcode, $account);
+    };
+
+    // If no modules implement hook_node_grants(), the default behavior is to
+    // allow all users to view published nodes, so reflect that here.
+    $status = (bool) $node instanceof EntityNG ? $node->status : $node->get('status', $langcode)->value;
+    return $this->setCache($status, $node, 'view', $langcode, $account);
+  }
+
+  /**
+   * Overrides \Drupal\Core\Entity\EntityAccessController::access().
+   */
+  protected function access(EntityInterface $node, $operation, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
+    if (user_access('bypass node access', $account)) {
+      return TRUE;
+    }
+
+    if (!user_access('access content', $account)) {
+      return FALSE;
+    }
+
+    if (($access = parent::access($node, $operation, $langcode, $account)) !== NULL) {
+      return (bool) $access;
+    };
+
+    // Fetch information from the node object if possible.
+    $node_status = isset($node->status) ? $node->status : NULL;
+    $node_uid = isset($node->uid) ? $node->uid : NULL;
+    // If it is a proper EntityNG object, use the proper methods.
+    if ($node instanceof EntityNG) {
+      $node_status = $node->get('status', $langcode)->value;
+      $node_uid = $node->get('uid', $langcode)->value;
+    }
+
+    // Check if authors can view their own unpublished nodes.
+    if ($operation == 'view' && !$node_status && user_access('view own unpublished content', $account)) {
+      $account = $account ?: user_load($GLOBALS['user']->uid);
+      if ($account->id() != 0 && $account->id() == $node_uid) {
+        return TRUE;
+      }
+    }
+
+    // If no module specified either allow or deny, we fall back to the
+    // node_access table.
+    if (($grants = $this->accessGrants($node, $operation, $langcode, $account)) !== NULL) {
+      return (bool) $grants;
+    }
+  }
+
+  /**
+   * Determines access to nodes based on node grants.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity for which to check 'create' access.
+   * @param string $operation
+   *   The entity operation. Usually one of 'view', 'edit', 'create' or
+   *   'delete'.
+   * @param string $langcode
+   *   (optional) The language code for which to check access. Defaults to
+   *   LANGUAGE_DEFAULT.
+   * @param \Drupal\user\Plugin\Core\Entity\User $account
+   *   (optional) The user for which to check access, or NULL to check access
+   *   for the current user. Defaults to NULL.
+   *
+   * @return bool|null
+   *   TRUE if access was granted, FALSE if access was denied or NULL if no
+   *   module implements hook_node_grants(), the node does not (yet) have an id
+   *   or none of the implementing modules explicitly granted or denied access.
+   */
+  protected function accessGrants(EntityInterface $node, $operation, $langcode = LANGUAGE_DEFAULT, User $account = NULL) {
+    // If no module implements the hook or the node does not have an id there is
+    // no point in querying the database for access grants.
+    if (!module_implements('node_grants') || !$node->id()) {
+      return;
+    }
+
+    // Check the database for potential access grants.
+    $query = db_select('node_access');
+    $query->addExpression('1');
+    $query->condition('grant_' . $operation, 1, '>=');
+    $nids = db_or()->condition('nid', $node->id());
+    $status = $node instanceof EntityNG ? $node->status : $node->get('status', $langcode)->value;
+    if ($status) {
+      $nids->condition('nid', 0);
+    }
+    $query->condition($nids);
+    $query->range(0, 1);
+
+    $grants = db_or();
+    foreach (node_access_grants($operation, $account) as $realm => $gids) {
+      foreach ($gids as $gid) {
+        $grants->condition(db_and()
+          ->condition('gid', $gid)
+          ->condition('realm', $realm));
+      }
+    }
+
+    if (count($grants) > 0) {
+      $query->condition($grants);
+    }
+
+    return $query->execute()->fetchField();
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
index 32c61ca..b1c60f5 100644
--- a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
+++ b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php
@@ -22,6 +22,7 @@
  *   module = "node",
  *   controller_class = "Drupal\node\NodeStorageController",
  *   render_controller_class = "Drupal\node\NodeRenderController",
+ *   access_controller_class = "Drupal\node\NodeAccessController",
  *   form_controller_class = {
  *     "default" = "Drupal\node\NodeFormController"
  *   },
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php
index ae2aee4..b418ef4 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php
@@ -79,7 +79,7 @@ function testNodeAccess() {
     $this->assertNodeAccess($expected_node_access, $node, $web_user, 'hr');
 
     // Reset the node access cache and turn on our test node_access() code.
-    drupal_static_reset('node_access');
+    entity_access_controller('node')->resetCache();
     state()->set('node_access_test_secret_catalan', 1);
 
     // Tests that Hungarian is still accessible.
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 0b7262c..1c0a10e 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -19,6 +19,7 @@
 use Drupal\Core\Template\Attribute;
 use Drupal\entity\Plugin\Core\Entity\EntityDisplay;
 use Drupal\file\Plugin\Core\Entity\File;
+use Drupal\user\Plugin\Core\Entity\User;
 
 /**
  * Denotes that the node is not published.
@@ -56,7 +57,7 @@
  * Modules should return this value from hook_node_access() to allow access to a
  * node.
  */
-const NODE_ACCESS_ALLOW = 'allow';
+const NODE_ACCESS_ALLOW = TRUE;
 
 /**
  * Denotes that access is denied for a node.
@@ -64,7 +65,7 @@
  * Modules should return this value from hook_node_access() to deny access to a
  * node.
  */
-const NODE_ACCESS_DENY = 'deny';
+const NODE_ACCESS_DENY = FALSE;
 
 /**
  * Denotes that access is unaffected for a node.
@@ -2513,126 +2514,31 @@ function node_form_system_themes_admin_form_submit($form, &$form_state) {
  *   http://drupal.org/node/1658846
  */
 function node_access($op, $node, $account = NULL, $langcode = NULL) {
-  $rights = &drupal_static(__FUNCTION__, array());
-
-  if (!$node || !in_array($op, array('view', 'update', 'delete', 'create'), TRUE)) {
-    // If there was no node to check against, or the $op was not one of the
-    // supported ones, we return access denied.
-    return FALSE;
-  }
-  // If no user object is supplied, the access check is for the current user.
-  if (empty($account)) {
-    $account = $GLOBALS['user'];
+  if (!$node instanceof EntityInterface) {
+    $type = is_object($node) ? $node->type : $node;
+    $node = entity_create('node', array('type' => $type));
   }
 
-  // $node may be a node object, a node type object, or a node type string.
-  // Use the node ID as the primary cache ID, or the node type name otherwise.
-  $cid = is_object($node) ? (isset($node->nid) ? $node->nid : $node->type) : $node;
-
-  // If no language code was provided, default to the node's langcode or
-  // to an empty langcode if a node type was requested. The latter is purely
-  // for caching purposes.
+  // If no language code was provided, default to the node's langcode.
   if (empty($langcode)) {
-    $langcode = (is_object($node) && isset($node->nid)) ? $node->langcode : '';
-  }
-
-  // Fetch information from the node object if possible.
-  $node_status = NULL;
-  $node_uid = NULL;
-  if (is_object($node)) {
-    $node_status = (isset($node->status)) ? $node->status : $node_status;
-    $node_uid = (isset($node->uid)) ? $node->uid : $node_uid;
-    // If it is a proper EntityNG object, use the proper methods.
-    if ($node instanceof \Drupal\Core\Entity\EntityNG){
-      $node_status = $node->get('status', $langcode)->value;
-      $node_uid = $node->get('uid', $langcode)->value ;
-    }
-  }
-
-  // If we've already checked access for this node, user and op, return from
-  // cache.
-  if (isset($rights[$account->uid][$cid][$langcode][$op])) {
-    return $rights[$account->uid][$cid][$langcode][$op];
-  }
-
-  if (user_access('bypass node access', $account)) {
-    $rights[$account->uid][$cid][$langcode][$op] = TRUE;
-    return TRUE;
-  }
-  if (!user_access('access content', $account)) {
-    $rights[$account->uid][$cid][$langcode][$op] = FALSE;
-    return FALSE;
-  }
-
-  // We grant access to the node if both of the following conditions are met:
-  // - No modules say to deny access.
-  // - At least one module says to grant access.
-  // If no module specified either allow or deny, we fall back to the
-  // node_access table.
-  $access = module_invoke_all('node_access', $node, $op, $account, $langcode);
-  if (in_array(NODE_ACCESS_DENY, $access, TRUE)) {
-    $rights[$account->uid][$cid][$langcode][$op] = FALSE;
-    return FALSE;
-  }
-  elseif (in_array(NODE_ACCESS_ALLOW, $access, TRUE)) {
-    $rights[$account->uid][$cid][$langcode][$op] = TRUE;
-    return TRUE;
-  }
-
-  // Check if authors can view their own unpublished nodes.
-  if ($op == 'view' && empty($node_status) && user_access('view own unpublished content', $account) && $account->uid === $node_uid && $account->uid != 0) {
-    $rights[$account->uid][$cid][$langcode][$op] = TRUE;
-    return TRUE;
+    $langcode = $node->langcode;
   }
 
-  // If the module did not override the access rights, use those set in the
-  // node_access table.
-  if ($op != 'create' && !empty($node->nid)) {
-    if (module_implements('node_grants')) {
-      $query = db_select('node_access');
-      $query->addExpression('1');
-      $query->condition('grant_' . $op, 1, '>=');
-      $nids = db_or()->condition('nid', $node->nid);
-      if ($node->status) {
-        $nids->condition('nid', 0);
-      }
-      $query->condition($nids);
-      $query->range(0, 1);
-
-      $grants = db_or();
-      foreach (node_access_grants($op, $account) as $realm => $gids) {
-        foreach ($gids as $gid) {
-          $grants->condition(db_and()
-            ->condition('gid', $gid)
-            ->condition('realm', $realm)
-          );
-        }
-      }
-      if (count($grants) > 0) {
-        $query->condition($grants);
-      }
-      $result =  (bool) $query
-        ->execute()
-        ->fetchField();
-      $rights[$account->uid][$cid][$langcode][$op] = $result;
-      return $result;
-    }
-    elseif (is_object($node) && $op == 'view' && !empty($node_status)) {
-      // If no modules implement hook_node_grants(), the default behavior is to
-      // allow all users to view published nodes, so reflect that here.
-      $rights[$account->uid][$cid][$langcode][$op] = TRUE;
-      return TRUE;
-    }
+  // Make sure that if an account is passed, that it is a fully loaded user
+  // object.
+  if ($account && !($account instanceof User)) {
+    $account = user_load($account->uid);
   }
 
-  return FALSE;
+  $method = $op . 'Access';
+  return entity_access_controller('node')->$method($node, $langcode, $account);
 }
 
 /**
  * Implements hook_node_access().
  */
 function node_node_access($node, $op, $account) {
-  $type = is_string($node) ? $node : $node->type;
+  $type = $node->bundle();
 
   $configured_types = node_permissions_get_configured_types();
   if (isset($configured_types[$type])) {
