 core/lib/Drupal/Core/Access/AccessResult.php       |  9 ++-
 .../DefaultExceptionHtmlSubscriber.php             |  5 +-
 .../Core/Render/MainContent/HtmlRenderer.php       |  9 +++
 core/lib/Drupal/Core/Routing/AccessAwareRouter.php | 10 +++-
 .../Core/Routing/AccessAwareRouterInterface.php    |  5 ++
 .../contact/src/Access/ContactPageAccess.php       |  2 +-
 .../contact/src/Controller/ContactController.php   | 69 +---------------------
 core/modules/contact/src/MessageForm.php           | 38 +++++++++++-
 .../contact/src/Tests/ContactPersonalTest.php      |  6 +-
 .../contact/src/Tests/ContactSitewideTest.php      |  3 +-
 .../modules/node/src/Tests/Views/FrontPageTest.php |  7 ++-
 core/modules/simpletest/src/WebTestBase.php        | 13 +++-
 .../Tests/Cache/PageCacheTagsIntegrationTest.php   |  2 +
 core/modules/views/src/Tests/GlossaryTest.php      |  1 +
 .../Drupal/Tests/Core/Access/AccessResultTest.php  | 48 +++++++++++----
 .../Tests/Core/Routing/AccessAwareRouterTest.php   | 23 ++++++--
 16 files changed, 154 insertions(+), 96 deletions(-)

diff --git a/core/lib/Drupal/Core/Access/AccessResult.php b/core/lib/Drupal/Core/Access/AccessResult.php
index db78d89..1ef2a40 100644
--- a/core/lib/Drupal/Core/Access/AccessResult.php
+++ b/core/lib/Drupal/Core/Access/AccessResult.php
@@ -339,7 +339,14 @@ public function setCacheMaxAge($max_age) {
    * @return $this
    */
   public function cachePerRole() {
-    $this->addCacheContexts(array('user.roles'));
+    $this->addCacheContexts(['user.roles'])
+      // @todo this is always needed when the 'user.roles' cache context is set:
+      //   when caching per role, and something has changed in that role (e.g.
+      //   a permission is granted or revoked), then the results for that role
+      //   will change! To invalidate accordingly, we need this cache tag.
+      //   This will be handled automatically once
+      //   https://www.drupal.org/node/2428703 lands.
+      ->addCacheTags(Cache::buildTags('config:user.role', \Drupal::currentUser()->getRoles(), '.'));
     return $this;
   }
 
diff --git a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
index a98bf57..5861e85 100644
--- a/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/DefaultExceptionHtmlSubscriber.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Core\EventSubscriber;
 
+use Drupal\Core\Routing\AccessAwareRouterInterface;
 use Drupal\Core\Url;
 use Drupal\Core\Utility\Error;
 use Psr\Log\LoggerInterface;
@@ -111,8 +112,10 @@ protected function makeSubrequest(GetResponseForExceptionEvent $event, $url, $st
       }
 
       try {
-        // Persist the 'exception' attribute to the subrequest.
+        // Persist the 'exception' and access result attributes to the
+        // subrequest.
         $sub_request->attributes->set('exception', $request->attributes->get('exception'));
+        $sub_request->attributes->set(AccessAwareRouterInterface::ACCESS_RESULT, $request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT));
 
         // Carry over the session to the subrequest.
         if ($session = $request->getSession()) {
diff --git a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
index 249b47b..ecd712a 100644
--- a/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -11,6 +11,7 @@
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheContexts;
+use Drupal\Core\Cache\CacheableInterface;
 use Drupal\Core\Controller\TitleResolverInterface;
 use Drupal\Core\Display\PageVariantInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
@@ -18,6 +19,7 @@
 use Drupal\Core\Render\Renderer;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Render\RenderEvents;
+use Drupal\Core\Routing\AccessAwareRouterInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Symfony\Component\DependencyInjection\ContainerAwareTrait;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -155,6 +157,13 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
       }
     }
 
+    // Merge the request's access result cacheability metadata, if it has any.
+    $access_result = $request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT);
+    if ($access_result instanceof CacheableInterface) {
+      $cache_contexts = Cache::mergeContexts($cache_contexts, $access_result->getCacheContexts());
+      $cache_tags = Cache::mergeTags($cache_tags, $access_result->getCacheTags());
+    }
+
     // Set the generator in the HTTP header.
     list($version) = explode('.', \Drupal::VERSION, 2);
 
diff --git a/core/lib/Drupal/Core/Routing/AccessAwareRouter.php b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
index 1cf937c..20b0ecf 100644
--- a/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
+++ b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
@@ -101,7 +101,15 @@ public function matchRequest(Request $request) {
    *   The request to access check.
    */
   protected function checkAccess(Request $request) {
-    if (!$this->accessManager->checkRequest($request, $this->account)) {
+    // The cacheability (if any) of this request's access check result must be
+    // applied to the response.
+    $access_result = $this->accessManager->checkRequest($request, $this->account, TRUE);
+    // Allow a master request to set the access result for a subrequest: if an
+    // access result attribute is already set, don't overwrite it.
+    if (!$request->attributes->has(AccessAwareRouterInterface::ACCESS_RESULT)) {
+      $request->attributes->set(AccessAwareRouterInterface::ACCESS_RESULT, $access_result);
+    }
+    if (!$access_result->isAllowed()) {
       throw new AccessDeniedHttpException();
     }
   }
diff --git a/core/lib/Drupal/Core/Routing/AccessAwareRouterInterface.php b/core/lib/Drupal/Core/Routing/AccessAwareRouterInterface.php
index 34fa75a..b6cfe4a 100644
--- a/core/lib/Drupal/Core/Routing/AccessAwareRouterInterface.php
+++ b/core/lib/Drupal/Core/Routing/AccessAwareRouterInterface.php
@@ -16,6 +16,11 @@
 interface AccessAwareRouterInterface extends RouterInterface, RequestMatcherInterface {
 
   /**
+   * Attribute name of the access result for the request..
+   */
+  const ACCESS_RESULT = '_access_result';
+
+  /**
    * {@inheritdoc}
    *
    * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
diff --git a/core/modules/contact/src/Access/ContactPageAccess.php b/core/modules/contact/src/Access/ContactPageAccess.php
index 97d6b06..cb5630c 100644
--- a/core/modules/contact/src/Access/ContactPageAccess.php
+++ b/core/modules/contact/src/Access/ContactPageAccess.php
@@ -62,7 +62,7 @@ public function access(UserInterface $user, AccountInterface $account) {
 
     // Anonymous users cannot have contact forms.
     if ($contact_account->isAnonymous()) {
-      return AccessResult::forbidden();
+      return AccessResult::forbidden()->cachePerRole();
     }
 
     // Users may not contact themselves.
diff --git a/core/modules/contact/src/Controller/ContactController.php b/core/modules/contact/src/Controller/ContactController.php
index 648a064..bfc8564 100644
--- a/core/modules/contact/src/Controller/ContactController.php
+++ b/core/modules/contact/src/Controller/ContactController.php
@@ -9,13 +9,9 @@
 
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Controller\ControllerBase;
-use Drupal\Core\Datetime\DateFormatter;
-use Drupal\Core\Flood\FloodInterface;
 use Drupal\contact\ContactFormInterface;
 use Drupal\user\UserInterface;
 use Drupal\Component\Utility\String;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
 /**
@@ -24,43 +20,6 @@
 class ContactController extends ControllerBase {
 
   /**
-   * The flood service.
-   *
-   * @var \Drupal\Core\Flood\FloodInterface
-   */
-  protected $flood;
-
-  /**
-   * The date formatter service.
-   *
-   * @var \Drupal\Core\Datetime\DateFormatter
-   */
-  protected $dateFormatter;
-
-  /**
-   * Constructs a ContactController object.
-   *
-   * @param \Drupal\Core\Flood\FloodInterface $flood
-   *   The flood service.
-   * @param \Drupal\Core\Datetime\DateFormatter $date_formatter
-   *   The date service.
-   */
-  public function __construct(FloodInterface $flood, DateFormatter $date_formatter) {
-    $this->flood = $flood;
-    $this->dateFormatter = $date_formatter;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('flood'),
-      $container->get('date.formatter')
-    );
-  }
-
-  /**
    * Presents the site-wide contact form.
    *
    * @param \Drupal\contact\ContactFormInterface $contact_form
@@ -74,10 +33,6 @@ public static function create(ContainerInterface $container) {
    *   contact form.
    */
   public function contactSitePage(ContactFormInterface $contact_form = NULL) {
-    // Check if flood control has been activated for sending emails.
-    if (!$this->currentUser()->hasPermission('administer contact forms')) {
-      $this->contactFloodControl();
-    }
     $config = $this->config('contact.settings');
 
     // Use the default form if no form has been passed.
@@ -106,6 +61,7 @@ public function contactSitePage(ContactFormInterface $contact_form = NULL) {
 
     $form = $this->entityFormBuilder()->getForm($message);
     $form['#title'] = String::checkPlain($contact_form->label());
+    $form['#cache']['contexts'][] = 'user.roles';
     $form['#cache']['tags'] = Cache::mergeTags(isset($form['#cache']['tags']) ? $form['#cache']['tags'] : [],  $config->getCacheTags());
     return $form;
   }
@@ -129,11 +85,6 @@ public function contactPersonalPage(UserInterface $user) {
       throw new NotFoundHttpException();
     }
 
-    // Check if flood control has been activated for sending emails.
-    if (!$this->currentUser()->hasPermission('administer contact forms') && !$this->currentUser()->hasPermission('administer users')) {
-      $this->contactFloodControl();
-    }
-
     $message = $this->entityManager()->getStorage('contact_message')->create(array(
       'contact_form' => 'personal',
       'recipient' => $user->id(),
@@ -141,24 +92,8 @@ public function contactPersonalPage(UserInterface $user) {
 
     $form = $this->entityFormBuilder()->getForm($message);
     $form['#title'] = $this->t('Contact @username', array('@username' => $user->getUsername()));
+    $form['#cache']['contexts'][] = 'user.roles';
     return $form;
   }
 
-  /**
-   * Throws an exception if the current user triggers flood control.
-   *
-   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
-   */
-  protected function contactFloodControl() {
-    $limit = $this->config('contact.settings')->get('flood.limit');
-    $interval = $this->config('contact.settings')->get('flood.interval');
-    if (!$this->flood->isAllowed('contact', $limit, $interval)) {
-      drupal_set_message($this->t('You cannot send more than %limit messages in @interval. Try again later.', array(
-        '%limit' => $limit,
-        '@interval' => $this->dateFormatter->formatInterval($interval),
-      )), 'error');
-      throw new AccessDeniedHttpException();
-    }
-  }
-
 }
diff --git a/core/modules/contact/src/MessageForm.php b/core/modules/contact/src/MessageForm.php
index 908ed17..b721db5 100644
--- a/core/modules/contact/src/MessageForm.php
+++ b/core/modules/contact/src/MessageForm.php
@@ -8,6 +8,7 @@
 namespace Drupal\contact;
 
 use Drupal\Component\Utility\String;
+use Drupal\Core\Datetime\DateFormatter;
 use Drupal\Core\Entity\ContentEntityForm;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Flood\FloodInterface;
@@ -50,6 +51,13 @@ class MessageForm extends ContentEntityForm {
   protected $mailHandler;
 
   /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatter
+   */
+  protected $dateFormatter;
+
+  /**
    * Constructs a MessageForm object.
    *
    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
@@ -60,12 +68,15 @@ class MessageForm extends ContentEntityForm {
    *   The language manager service.
    * @param \Drupal\contact\MailHandlerInterface $mail_handler
    *   The contact mail handler service.
+   * @param \Drupal\Core\Datetime\DateFormatter $date_formatter
+   *   The date service.
    */
-  public function __construct(EntityManagerInterface $entity_manager, FloodInterface $flood, LanguageManagerInterface $language_manager, MailHandlerInterface $mail_handler) {
+  public function __construct(EntityManagerInterface $entity_manager, FloodInterface $flood, LanguageManagerInterface $language_manager, MailHandlerInterface $mail_handler, DateFormatter $date_formatter) {
     parent::__construct($entity_manager);
     $this->flood = $flood;
     $this->languageManager = $language_manager;
     $this->mailHandler = $mail_handler;
+    $this->dateFormatter = $date_formatter;
   }
 
   /**
@@ -76,7 +87,8 @@ public static function create(ContainerInterface $container) {
       $container->get('entity.manager'),
       $container->get('flood'),
       $container->get('language_manager'),
-      $container->get('contact.mail_handler')
+      $container->get('contact.mail_handler'),
+      $container->get('date.formatter')
     );
   }
 
@@ -175,6 +187,28 @@ public function preview(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
+  public function validate(array $form, FormStateInterface $form_state) {
+    parent::validate($form, $form_state);
+
+    $message = $this->entity;
+
+    // Check if flood control has been activated for sending emails.
+    if (!$this->currentUser()->hasPermission('administer contact forms') && (!$message->isPersonal() || !$this->currentUser()->hasPermission('administer users'))) {
+      $limit = $this->config('contact.settings')->get('flood.limit');
+      $interval = $this->config('contact.settings')->get('flood.interval');
+
+      if (!$this->flood->isAllowed('contact', $limit, $interval)) {
+        $form_state->setErrorByName('', $this->t('You cannot send more than %limit messages in @interval. Try again later.', array(
+          '%limit' => $limit,
+          '@interval' => $this->dateFormatter->formatInterval($interval),
+        )));
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function save(array $form, FormStateInterface $form_state) {
     $message = $this->entity;
     $user = $this->currentUser();
diff --git a/core/modules/contact/src/Tests/ContactPersonalTest.php b/core/modules/contact/src/Tests/ContactPersonalTest.php
index 03965ad..42b9b6a 100644
--- a/core/modules/contact/src/Tests/ContactPersonalTest.php
+++ b/core/modules/contact/src/Tests/ContactPersonalTest.php
@@ -152,11 +152,15 @@ function testPersonalContactAccess() {
     // Test that anonymous users can access admin user's contact form.
     $this->drupalGet('user/' . $this->adminUser->id() . '/contact');
     $this->assertResponse(200);
+    $this->assertCacheContext('user.roles');
+    $this->assertCacheTag('config:user.role.anonymous');
 
     // Revoke the personal contact permission for the anonymous user.
     user_role_revoke_permissions(RoleInterface::ANONYMOUS_ID, array('access user contact forms'));
     $this->drupalGet('user/' . $this->contactUser->id() . '/contact');
     $this->assertResponse(403);
+    $this->assertCacheContext('user.roles');
+    $this->assertCacheTag('config:user.role.anonymous');
     $this->drupalGet('user/' . $this->adminUser->id() . '/contact');
     $this->assertResponse(403);
 
@@ -235,7 +239,7 @@ function testPersonalContactFlood() {
     }
 
     // Submit contact form one over limit.
-    $this->drupalGet('user/' . $this->contactUser->id(). '/contact');
+    $this->submitPersonalContact($this->contactUser);
     $this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array('%number' => $flood_limit, '@interval' => \Drupal::service('date.formatter')->formatInterval($this->config('contact.settings')->get('flood.interval')))), 'Normal user denied access to flooded contact form.');
 
     // Test that the admin user can still access the contact form even though
diff --git a/core/modules/contact/src/Tests/ContactSitewideTest.php b/core/modules/contact/src/Tests/ContactSitewideTest.php
index 793388a..df2816e 100644
--- a/core/modules/contact/src/Tests/ContactSitewideTest.php
+++ b/core/modules/contact/src/Tests/ContactSitewideTest.php
@@ -230,8 +230,7 @@ function testSiteWideContact() {
       $this->assertText(t('Your message has been sent.'));
     }
     // Submit contact form one over limit.
-    $this->drupalGet('contact');
-    $this->assertResponse(403);
+    $this->submitContact($this->randomMachineName(16), $recipients[0], $this->randomMachineName(16), $id, $this->randomMachineName(64));
     $this->assertRaw(t('You cannot send more than %number messages in @interval. Try again later.', array('%number' => $this->config('contact.settings')->get('flood.limit'), '@interval' => \Drupal::service('date.formatter')->formatInterval(600))));
 
     // Test listing controller.
diff --git a/core/modules/node/src/Tests/Views/FrontPageTest.php b/core/modules/node/src/Tests/Views/FrontPageTest.php
index ed6fd3d..9e14395 100644
--- a/core/modules/node/src/Tests/Views/FrontPageTest.php
+++ b/core/modules/node/src/Tests/Views/FrontPageTest.php
@@ -241,7 +241,7 @@ protected function assertFrontPageViewCacheTags($do_assert_views_caches) {
     $view = Views::getView('frontpage');
     $view->setDisplay('page_1');
 
-    $cache_contexts = ['user.node_grants:view', 'languages'];
+    $cache_contexts = ['user.node_grants:view', 'languages', 'user.roles'];
 
     // Test before there are any nodes.
     $empty_node_listing_cache_tags = [
@@ -257,7 +257,7 @@ protected function assertFrontPageViewCacheTags($do_assert_views_caches) {
     $this->assertPageCacheContextsAndTags(
       Url::fromRoute('view.frontpage.page_1'),
       $cache_contexts,
-      Cache::mergeTags($empty_node_listing_cache_tags, ['rendered'])
+      Cache::mergeTags($empty_node_listing_cache_tags, ['config:user.role.anonymous', 'rendered'])
     );
 
     // Create some nodes on the frontpage view. Add more than 10 nodes in order
@@ -317,7 +317,7 @@ protected function assertFrontPageViewCacheTags($do_assert_views_caches) {
     $this->assertPageCacheContextsAndTags(
       Url::fromRoute('view.frontpage.page_1'),
       $cache_contexts,
-      Cache::mergeTags($first_page_output_cache_tags, ['rendered'])
+      Cache::mergeTags($first_page_output_cache_tags, ['config:user.role.anonymous', 'rendered'])
     );
 
     // Second page.
@@ -329,6 +329,7 @@ protected function assertFrontPageViewCacheTags($do_assert_views_caches) {
       'node:4',
       'node:5',
       // The rest.
+      'config:user.role.anonymous',
       'config:filter.format.plain_text',
       'config:views.view.frontpage',
       'node_list',
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index 63cf76a..f939af3 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -2740,6 +2740,17 @@ protected function buildUrl($path, array $options = array()) {
   }
 
   /**
+   * Asserts whether an expected cache context was present in the last response.
+   *
+   * @param string $expected_cache_context
+   *   The expected cache context.
+   */
+  protected function assertCacheContext($expected_cache_context) {
+    $cache_contexts = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Contexts'));
+    $this->assertTrue(in_array($expected_cache_context, $cache_contexts), "'" . $expected_cache_context . "' is present in the X-Drupal-Cache-Contexts header.");
+  }
+
+  /**
    * Asserts whether an expected cache tag was present in the last response.
    *
    * @param string $expected_cache_tag
@@ -2747,7 +2758,7 @@ protected function buildUrl($path, array $options = array()) {
    */
   protected function assertCacheTag($expected_cache_tag) {
     $cache_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'));
-    $this->assertTrue(in_array($expected_cache_tag, $cache_tags));
+    $this->assertTrue(in_array($expected_cache_tag, $cache_tags), "'" . $expected_cache_tag . "' is present in the X-Drupal-Cache-Tags header.");
   }
 
 }
diff --git a/core/modules/system/src/Tests/Cache/PageCacheTagsIntegrationTest.php b/core/modules/system/src/Tests/Cache/PageCacheTagsIntegrationTest.php
index 8078913..aa84eed 100644
--- a/core/modules/system/src/Tests/Cache/PageCacheTagsIntegrationTest.php
+++ b/core/modules/system/src/Tests/Cache/PageCacheTagsIntegrationTest.php
@@ -101,6 +101,7 @@ function testPageCacheTags() {
       'config:system.menu.footer',
       'config:system.menu.main',
       'config:system.site',
+      'config:user.role.anonymous',
     ));
 
     // Full node page 2.
@@ -130,6 +131,7 @@ function testPageCacheTags() {
       'comment_list',
       'node_list',
       'config:views.view.comments_recent',
+      'config:user.role.anonymous',
     ));
   }
 
diff --git a/core/modules/views/src/Tests/GlossaryTest.php b/core/modules/views/src/Tests/GlossaryTest.php
index 1df4701..b78225d 100644
--- a/core/modules/views/src/Tests/GlossaryTest.php
+++ b/core/modules/views/src/Tests/GlossaryTest.php
@@ -88,6 +88,7 @@ public function testGlossaryView() {
     // Verify cache tags.
     $this->enablePageCaching();
     $this->assertPageCacheContextsAndTags(Url::fromRoute('view.glossary.page_1'), ['languages', 'url', 'user'], [
+      'config:user.role.anonymous',
       'config:views.view.glossary',
       'node:' . $nodes_by_char['a'][0]->id(), 'node:' . $nodes_by_char['a'][1]->id(), 'node:' . $nodes_by_char['a'][2]->id(),
       'node_list',
diff --git a/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
index 42d2d2a..1c1a44f 100644
--- a/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
+++ b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
@@ -13,6 +13,7 @@
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableInterface;
 use Drupal\Tests\UnitTestCase;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
 
 /**
  * @coversDefaultClass \Drupal\Core\Access\AccessResult
@@ -30,6 +31,23 @@ protected function assertDefaultCacheability(AccessResult $access) {
   }
 
   /**
+   * Mocks the current user service, so that ::cachePerRole() can work.
+   *
+   * @see ::cachePerRole
+   *
+   * @todo remove this when https://www.drupal.org/node/2428703 lands.
+   */
+  protected function mockCurrentUserService() {
+    $container = new ContainerBuilder();
+    $current_user = $this->getMock('\Drupal\Core\Session\AccountProxyInterface');
+    $current_user->expects($this->any())
+      ->method('getRoles')
+      ->willReturn(['authenticated', 'llama_herder']);
+    $container->set('current_user', $current_user);
+    \Drupal::setContainer($container);
+  }
+
+  /**
    * Tests the construction of an AccessResult object.
    *
    * @covers ::__construct
@@ -325,7 +343,9 @@ public function testCacheMaxAge() {
    * @covers ::allowedIfHasPermission
    */
   public function testCacheContexts() {
-    $verify = function (AccessResult $access, array $contexts) {
+    $this->mockCurrentUserService();
+
+    $verify = function (AccessResult $access, array $contexts, array $tags = []) {
       $this->assertFalse($access->isAllowed());
       $this->assertFalse($access->isForbidden());
       $this->assertTrue($access->isNeutral());
@@ -333,7 +353,7 @@ public function testCacheContexts() {
       $this->assertSame('default', $access->getCacheBin());
       $this->assertSame(Cache::PERMANENT, $access->getCacheMaxAge());
       $this->assertSame($contexts, $access->getCacheContexts());
-      $this->assertSame([], $access->getCacheTags());
+      $this->assertSame($tags, $access->getCacheTags());
     };
 
     $access = AccessResult::neutral()->addCacheContexts(['foo']);
@@ -357,10 +377,11 @@ public function testCacheContexts() {
 
     // ::cachePerRole() convenience method.
     $contexts = array('user.roles');
-    $a = AccessResult::neutral()->addCacheContexts($contexts);
-    $verify($a, $contexts);
+    $tags = ['config:user.role.authenticated', 'config:user.role.llama_herder'];
+    $a = AccessResult::neutral()->addCacheContexts($contexts)->addCacheTags($tags);
+    $verify($a, $contexts, $tags);
     $b = AccessResult::neutral()->cachePerRole();
-    $verify($b, $contexts);
+    $verify($b, $contexts, $tags);
     $this->assertEquals($a, $b);
 
     // ::cachePerUser() convenience method.
@@ -373,12 +394,12 @@ public function testCacheContexts() {
 
     // Both.
     $contexts = array('user', 'user.roles');
-    $a = AccessResult::neutral()->addCacheContexts($contexts);
-    $verify($a, $contexts);
+    $a = AccessResult::neutral()->addCacheContexts($contexts)->addCacheTags($tags);
+    $verify($a, $contexts, $tags);
     $b = AccessResult::neutral()->cachePerRole()->cachePerUser();
-    $verify($b, $contexts);
+    $verify($b, $contexts, $tags);
     $c = AccessResult::neutral()->cachePerUser()->cachePerRole();
-    $verify($c, $contexts);
+    $verify($c, $contexts, $tags);
     $this->assertEquals($a, $b);
     $this->assertEquals($a, $c);
 
@@ -393,7 +414,7 @@ public function testCacheContexts() {
     // Verify the object when using the ::allowedIfHasPermission() convenience
     // static method.
     $b = AccessResult::allowedIfHasPermission($account, 'may herd llamas');
-    $verify($b, $contexts);
+    $verify($b, $contexts, $tags);
   }
 
   /**
@@ -467,13 +488,15 @@ public function testCacheTags() {
    * @covers ::inheritCacheability
    */
   public function testInheritCacheability() {
+    $this->mockCurrentUserService();
+
     // andIf(); 1st has defaults, 2nd has custom tags, contexts and max-age.
     $access = AccessResult::allowed();
     $other = AccessResult::allowed()->setCacheMaxAge(1500)->cachePerRole()->addCacheTags(['node:20011988']);
     $this->assertTrue($access->inheritCacheability($other) instanceof AccessResult);
     $this->assertTrue($access->isCacheable());
     $this->assertSame(['user.roles'], $access->getCacheContexts());
-    $this->assertSame(['node:20011988'], $access->getCacheTags());
+    $this->assertSame(['config:user.role.authenticated', 'config:user.role.llama_herder', 'node:20011988'], $access->getCacheTags());
     $this->assertSame('default', $access->getCacheBin());
     $this->assertSame(1500, $access->getCacheMaxAge());
 
@@ -824,6 +847,8 @@ public function testAndOrCacheabilityPropagation(AccessResultInterface $first, $
    * @dataProvider providerTestAllowedIfHasPermissions
    */
   public function testAllowIfHasPermissions($permissions, $conjunction, $expected_access) {
+    $this->mockCurrentUserService();
+
     $account = $this->getMock('\Drupal\Core\Session\AccountInterface');
     $account->expects($this->any())
       ->method('hasPermission')
@@ -842,6 +867,7 @@ public function testAllowIfHasPermissions($permissions, $conjunction, $expected_
    * @return array
    */
   public function providerTestAllowedIfHasPermissions() {
+    $this->mockCurrentUserService();
     return [
       [[], 'AND', AccessResult::allowedIf(FALSE)->cachePerRole()],
       [[], 'OR', AccessResult::allowedIf(FALSE)->cachePerRole()],
diff --git a/core/tests/Drupal/Tests/Core/Routing/AccessAwareRouterTest.php b/core/tests/Drupal/Tests/Core/Routing/AccessAwareRouterTest.php
index 3e8676f..da58cd9 100644
--- a/core/tests/Drupal/Tests/Core/Routing/AccessAwareRouterTest.php
+++ b/core/tests/Drupal/Tests/Core/Routing/AccessAwareRouterTest.php
@@ -7,7 +7,9 @@
 
 namespace Drupal\Tests\Core\Routing;
 
+use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Routing\AccessAwareRouter;
+use Drupal\Core\Routing\AccessAwareRouterInterface;
 use Drupal\Tests\UnitTestCase;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
@@ -73,13 +75,18 @@ protected function setupRouter() {
   public function testMatchRequestAllowed() {
     $this->setupRouter();
     $request = new Request();
+    $access_result = AccessResult::allowed();
     $this->accessManager->expects($this->once())
       ->method('checkRequest')
       ->with($request)
-      ->will($this->returnValue(TRUE));
+      ->willReturn($access_result);
     $parameters = $this->router->matchRequest($request);
-    $this->assertSame($request->attributes->all(), array(RouteObjectInterface::ROUTE_OBJECT => $this->route));
-    $this->assertSame($parameters, array(RouteObjectInterface::ROUTE_OBJECT => $this->route));
+    $expected = [
+      RouteObjectInterface::ROUTE_OBJECT => $this->route,
+      AccessAwareRouterInterface::ACCESS_RESULT => $access_result,
+    ];
+    $this->assertSame($expected, $request->attributes->all());
+    $this->assertSame($expected, $parameters);
   }
 
   /**
@@ -90,11 +97,17 @@ public function testMatchRequestAllowed() {
   public function testMatchRequestDenied() {
     $this->setupRouter();
     $request = new Request();
+    $access_result = AccessResult::forbidden();
     $this->accessManager->expects($this->once())
       ->method('checkRequest')
       ->with($request)
-      ->will($this->returnValue(FALSE));
-    $this->router->matchRequest($request);
+      ->willReturn($access_result);
+    $parameters = $this->router->matchRequest($request);
+    $expected = [
+      AccessAwareRouterInterface::ACCESS_RESULT => $access_result,
+    ];
+    $this->assertSame($expected, $request->attributes->all());
+    $this->assertSame($expected, $parameters);
   }
 
   /**
