diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index da392615a65a0e597edc7a6adaa4035e5ecb97b2..99f3aee9f568f030d35aae82b0dbf5817ec45cc0 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -671,56 +671,6 @@ function template_preprocess_node(&$variables) {
   }
 }
 
-/**
- * Implements hook_permission().
- */
-function node_permission() {
-  $perms = array(
-    'bypass node access' => array(
-      'title' => t('Bypass content access control'),
-      'description' => t('View, edit and delete all content regardless of permission restrictions.'),
-      'restrict access' => TRUE,
-    ),
-    'administer content types' => array(
-      'title' => t('Administer content types'),
-      'description' => t('Promote, change ownership, edit revisions, and perform other tasks across all content types.'),
-      'restrict access' => TRUE,
-    ),
-    'administer nodes' => array(
-      'title' => t('Administer content'),
-      'restrict access' => TRUE,
-    ),
-    'access content overview' => array(
-      'title' => t('Access the Content overview page'),
-      'description' => t('Get an overview of <a href="!url">all content</a>.', array('!url' => \Drupal::url('system.admin_content'))),
-    ),
-    'access content' => array(
-      'title' => t('View published content'),
-    ),
-    'view own unpublished content' => array(
-      'title' => t('View own unpublished content'),
-    ),
-    'view all revisions' => array(
-      'title' => t('View all revisions'),
-    ),
-    'revert all revisions' => array(
-      'title' => t('Revert all revisions'),
-      'description' => t('Role requires permission <em>view revisions</em> and <em>edit rights</em> for nodes in question, or <em>administer nodes</em>.'),
-    ),
-    'delete all revisions' => array(
-      'title' => t('Delete all revisions'),
-      'description' => t('Role requires permission to <em>view revisions</em> and <em>delete rights</em> for nodes in question, or <em>administer nodes</em>.'),
-    ),
-  );
-
-  // Generate node permissions for all node types.
-  foreach (NodeType::loadMultiple() as $type) {
-    $perms += node_list_permissions($type);
-  }
-
-  return $perms;
-}
-
 /**
  * Implements hook_ranking().
  */
@@ -1193,48 +1143,6 @@ function node_node_access(NodeInterface $node, $op, $account) {
   return NODE_ACCESS_IGNORE;
 }
 
-/**
- * Helper function to generate standard node permission list for a given type.
- *
- * @param $name
- *   The machine name of the node type.
- *
- * @return array
- *   An array of permission names and descriptions.
- */
-function node_list_permissions($type) {
-  // Build standard list of node permissions for this type.
-  $perms = array(
-    "create $type->type content" => array(
-      'title' => t('%type_name: Create new content', array('%type_name' => $type->name)),
-    ),
-    "edit own $type->type content" => array(
-      'title' => t('%type_name: Edit own content', array('%type_name' => $type->name)),
-    ),
-    "edit any $type->type content" => array(
-      'title' => t('%type_name: Edit any content', array('%type_name' => $type->name)),
-    ),
-    "delete own $type->type content" => array(
-      'title' => t('%type_name: Delete own content', array('%type_name' => $type->name)),
-    ),
-    "delete any $type->type content" => array(
-      'title' => t('%type_name: Delete any content', array('%type_name' => $type->name)),
-    ),
-    "view $type->type revisions" => array(
-      'title' => t('%type_name: View revisions', array('%type_name' => $type->name)),
-    ),
-    "revert $type->type revisions" => array(
-      'title' => t('%type_name: Revert revisions', array('%type_name' => $type->name)),
-      'description' => t('Role requires permission <em>view revisions</em> and <em>edit rights</em> for nodes in question, or <em>administer nodes</em>.'),
-    ),
-    "delete $type->type revisions" => array(
-      'title' => t('%type_name: Delete revisions', array('%type_name' => $type->name)),
-      'description' => t('Role requires permission to <em>view revisions</em> and <em>delete rights</em> for nodes in question, or <em>administer nodes</em>.'),
-    ),
-  );
-  return $perms;
-}
-
 /**
  * Fetches an array of permission IDs granted to the given user ID.
  *
diff --git a/core/modules/node/node.permissions.yml b/core/modules/node/node.permissions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..31a49927f6f0d2f7b6a805814cdb01fd8a7b8324
--- /dev/null
+++ b/core/modules/node/node.permissions.yml
@@ -0,0 +1,27 @@
+bypass node access:
+  title: 'Bypass content access control'
+  description: 'View edit and delete all content regardless of permission restrictions.'
+  'restrict access': TRUE
+administer content types:
+  title: 'Administer content types'
+  description: 'Promote change ownership edit revisions and perform other tasks across all content types.'
+  'restrict access': TRUE
+administer nodes:
+  title: 'Administer content'
+  'restrict access': TRUE
+access content:
+  title: 'View published content'
+view own unpublished content:
+  title: 'View own unpublished content'
+view all revisions:
+  title: 'View all revisions'
+revert all revisions:
+  title: 'Revert all revisions'
+  description: 'Role requires permission <em>view revisions</em> and <em>edit rights</em> for nodes in question or <em>administer nodes</em>.'
+delete all revisions:
+  title: 'Delete all revisions'
+  description: 'Role requires permission to <em>view revisions</em> and <em>delete rights</em> for nodes in question or <em>administer nodes</em>.'
+
+permission_callbacks:
+  - \Drupal\node\NodePermissions::nodeTypePermissions
+  - \Drupal\node\NodePermissions::contentPermissions
diff --git a/core/modules/node/src/NodePermissions.php b/core/modules/node/src/NodePermissions.php
new file mode 100644
index 0000000000000000000000000000000000000000..5f25c64938c9195b5c5f947eca2db65159929d7e
--- /dev/null
+++ b/core/modules/node/src/NodePermissions.php
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\NodePermissions.
+ */
+
+namespace Drupal\node;
+
+use Drupal\Core\Routing\UrlGeneratorTrait;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\node\Entity\NodeType;
+
+/**
+ * Defines a class containing permission callbacks.
+ */
+class NodePermissions {
+
+  use StringTranslationTrait;
+  use UrlGeneratorTrait;
+
+  /**
+   * Returns an array of content permissions.
+   *
+   * @return array
+   */
+  public function contentPermissions() {
+    return array(
+      'access content overview' => array(
+        'title' => $this->t('Access the Content overview page'),
+        'description' => $this->t('Get an overview of <a href="!url">all content</a>.', array('!url' => $this->url('system.admin_content'))),
+      ),
+    );
+  }
+
+  /**
+   * Returns an array of node type permissions.
+   *
+   * @return array
+   */
+  public function nodeTypePermissions() {
+    $perms = array();
+    // Generate node permissions for all node types.
+    foreach (NodeType::loadMultiple() as $type) {
+      $perms += $this->buildPermissions($type);
+    }
+
+    return $perms;
+  }
+
+  /**
+   * Builds a standard list of node permissions for a given type.
+   *
+   * @param \Drupal\node\Entity\NodeType $type
+   *   The machine name of the node type.
+   *
+   * @return array
+   *   An array of permission names and descriptions.
+   */
+  protected function buildPermissions(NodeType $type) {
+    $type_id = $type->id();
+    $type_params = array('%type_name' => $type->label());
+
+    return array(
+      "create $type_id content" => array(
+        'title' => $this->t('%type_name: Create new content', $type_params),
+      ),
+      "edit own $type_id content" => array(
+        'title' => $this->t('%type_name: Edit own content', $type_params),
+      ),
+      "edit any $type_id content" => array(
+        'title' => $this->t('%type_name: Edit any content', $type_params),
+      ),
+      "delete own $type_id content" => array(
+        'title' => $this->t('%type_name: Delete own content', $type_params),
+      ),
+      "delete any $type_id content" => array(
+        'title' => $this->t('%type_name: Delete any content', $type_params),
+      ),
+      "view $type_id revisions" => array(
+        'title' => $this->t('%type_name: View revisions', $type_params),
+      ),
+      "revert $type_id revisions" => array(
+        'title' => $this->t('%type_name: Revert revisions', $type_params),
+        'description' => t('Role requires permission <em>view revisions</em> and <em>edit rights</em> for nodes in question, or <em>administer nodes</em>.'),
+      ),
+      "delete $type_id revisions" => array(
+        'title' => $this->t('%type_name: Delete revisions', $type_params),
+        'description' => $this->t('Role requires permission to <em>view revisions</em> and <em>delete rights</em> for nodes in question, or <em>administer nodes</em>.'),
+      ),
+    );
+  }
+
+}
diff --git a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php
index adf573e1865163dc011e1afc0c7eb4ec7d12ea42..c8a8987211583a4806cc0475039991416d26f35b 100644
--- a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php
+++ b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php
@@ -77,8 +77,8 @@ function testEnableModulesLoad() {
    * Tests expected installation behavior of enableModules().
    */
   function testEnableModulesInstall() {
-    $module = 'node';
-    $table = 'node_access';
+    $module = 'module_test';
+    $table = 'module_test';
 
     // Verify that the module does not exist yet.
     $this->assertFalse(\Drupal::moduleHandler()->moduleExists($module), "$module module not found.");
diff --git a/core/modules/user/src/PermissionHandler.php b/core/modules/user/src/PermissionHandler.php
index 2336593c33f09c2da356b9a43c6f0c2bb13ed72c..1cbe222fdd1b333c12582746ccab7d4fc0bbc79f 100644
--- a/core/modules/user/src/PermissionHandler.php
+++ b/core/modules/user/src/PermissionHandler.php
@@ -8,6 +8,7 @@
 namespace Drupal\user;
 
 use Drupal\Component\Discovery\YamlDiscovery;
+use Drupal\Core\Controller\ControllerResolverInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\StringTranslation\TranslationInterface;
@@ -44,6 +45,13 @@ class PermissionHandler implements PermissionHandlerInterface {
    */
   protected $yamlDiscovery;
 
+  /**
+   * The controller resolver.
+   *
+   * @var \Drupal\Core\Controller\ControllerResolverInterface
+   */
+  protected $controllerResolver;
+
   /**
    * Constructs a new PermissionHandler.
    *
@@ -51,12 +59,15 @@ class PermissionHandler implements PermissionHandlerInterface {
    *   The module handler.
    * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
    *   The string translation.
+   * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
+   *   The controller resolver.
    */
-  public function __construct(ModuleHandlerInterface $module_handler, TranslationInterface $string_translation) {
+  public function __construct(ModuleHandlerInterface $module_handler, TranslationInterface $string_translation, ControllerResolverInterface $controller_resolver) {
     // @todo It would be nice if you could pull all module directories from the
     //   container.
     $this->moduleHandler = $module_handler;
     $this->stringTranslation = $string_translation;
+    $this->controllerResolver = $controller_resolver;
   }
 
   /**
@@ -94,7 +105,38 @@ public function getPermissions() {
    */
   protected function buildPermissionsYaml() {
     $all_permissions = array();
+    $all_callback_permissions = array();
+
     foreach ($this->getYamlDiscovery()->findAll() as $provider => $permissions) {
+      // The top-level 'permissions_callback' is a list of methods in controller
+      // syntax, see \Drupal\Core\Controller\ControllerResolver. These methods
+      // should return an array of permissions in the same structure.
+      if (isset($permissions['permission_callbacks'])) {
+        foreach ($permissions['permission_callbacks'] as $permission_callback) {
+          $callback = $this->controllerResolver->getControllerFromDefinition($permission_callback);
+          if ($callback_permissions = call_user_func($callback)) {
+            // Add any callback permissions to the array of permissions. Any
+            // defaults can then get processed below.
+            foreach ($callback_permissions as $name => $callback_permission) {
+              if (!is_array($callback_permission)) {
+                $callback_permission = array(
+                  'title' => $callback_permission,
+                );
+              }
+
+              $callback_permission += array(
+                'description' => NULL,
+              );
+              $callback_permission['provider'] = $provider;
+
+              $all_callback_permissions[$name] = $callback_permission;
+            }
+          }
+        }
+
+        unset($permissions['permission_callbacks']);
+      }
+
       foreach ($permissions as &$permission) {
         if (!is_array($permission)) {
           $permission = array(
@@ -105,9 +147,11 @@ protected function buildPermissionsYaml() {
         $permission['description'] = isset($permission['description']) ? $this->t($permission['description']) : NULL;
         $permission['provider'] = $provider;
       }
+
       $all_permissions += $permissions;
     }
-    return $all_permissions;
+
+    return $all_permissions + $all_callback_permissions;
   }
 
   /**
diff --git a/core/modules/user/tests/src/Unit/PermissionHandlerTest.php b/core/modules/user/tests/src/Unit/PermissionHandlerTest.php
index 9312544a3d6f6571b9c84d4004cbc3da7e80b21c..a25bbce71333d8f0d9db0f7feb40ea5115a2cfd3 100644
--- a/core/modules/user/tests/src/Unit/PermissionHandlerTest.php
+++ b/core/modules/user/tests/src/Unit/PermissionHandlerTest.php
@@ -44,11 +44,19 @@ class PermissionHandlerTest extends UnitTestCase {
    */
   protected $stringTranslation;
 
+  /**
+   * The mocked controller resolver.
+   *
+   * @var \Drupal\Core\Controller\ControllerResolverInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $controllerResolver;
+
   /**
    * {@inheritdoc}
    */
   protected function setUp() {
     $this->stringTranslation = $this->getStringTranslationStub();
+    $this->controllerResolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface');
   }
 
   /**
@@ -112,7 +120,7 @@ public function testBuildPermissionsModules() {
       ->method('getModuleList')
       ->willReturn(array_flip($modules));
 
-    $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation);
+    $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
 
     // Setup system_rebuild_module_data().
     $this->permissionHandler->setSystemRebuildModuleData($extensions);
@@ -180,7 +188,10 @@ public function testBuildPermissionsYaml() {
       ->method('getModuleList')
       ->willReturn(array_flip($modules));
 
-    $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation);
+    $this->controllerResolver->expects($this->never())
+      ->method('getControllerFromDefinition');
+
+    $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
 
     // Setup system_rebuild_module_data().
     $this->permissionHandler->setSystemRebuildModuleData($extensions);
@@ -229,7 +240,7 @@ public function testBuildPermissionsSortPerModule() {
       ->method('getModuleList')
       ->willReturn(array_flip($modules));
 
-    $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation);
+    $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
 
     // Setup system_rebuild_module_data().
     $this->permissionHandler->setSystemRebuildModuleData($extensions);
@@ -239,6 +250,143 @@ public function testBuildPermissionsSortPerModule() {
     $this->assertEquals(['access_module_a1', 'access_module_a2'], array_keys($actual_permissions));
   }
 
+  /**
+   * Tests dynamic callback permissions provided by YML files.
+   *
+   * @covers ::__construct
+   * @covers ::getPermissions
+   * @covers ::buildPermissions
+   * @covers ::buildPermissionsYaml
+   */
+  public function testBuildPermissionsYamlCallback() {
+    vfsStreamWrapper::register();
+    $root = new vfsStreamDirectory('modules');
+    vfsStreamWrapper::setRoot($root);
+
+    $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+    $this->moduleHandler->expects($this->once())
+      ->method('getModuleDirectories')
+      ->willReturn(array(
+        'module_a' => vfsStream::url('modules/module_a'),
+        'module_b' => vfsStream::url('modules/module_b'),
+        'module_c' => vfsStream::url('modules/module_c'),
+      ));
+
+    $url = vfsStream::url('modules');
+    mkdir($url . '/module_a');
+    file_put_contents($url . '/module_a/module_a.permissions.yml',
+"permission_callbacks:
+  - 'Drupal\\user\\Tests\\TestPermissionCallbacks::singleDescription'
+");
+    mkdir($url . '/module_b');
+    file_put_contents($url . '/module_b/module_b.permissions.yml',
+"permission_callbacks:
+  - 'Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescription'
+");
+    mkdir($url . '/module_c');
+    file_put_contents($url . '/module_c/module_c.permissions.yml',
+"permission_callbacks:
+  - 'Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescriptionRestrictAccess'
+");
+
+    $modules = array('module_a', 'module_b', 'module_c');
+    $extensions = array(
+      'module_a' => $this->mockModuleExtension('module_a', 'Module a'),
+      'module_b' => $this->mockModuleExtension('module_b', 'Module b'),
+      'module_c' => $this->mockModuleExtension('module_c', 'Module c'),
+    );
+
+    $this->moduleHandler->expects($this->any())
+      ->method('getImplementations')
+      ->with('permission')
+      ->willReturn(array());
+
+    $this->moduleHandler->expects($this->any())
+      ->method('getModuleList')
+      ->willReturn(array_flip($modules));
+
+    $this->controllerResolver->expects($this->at(0))
+      ->method('getControllerFromDefinition')
+      ->with('Drupal\\user\\Tests\\TestPermissionCallbacks::singleDescription')
+      ->willReturn(array(new TestPermissionCallbacks(), 'singleDescription'));
+    $this->controllerResolver->expects($this->at(1))
+      ->method('getControllerFromDefinition')
+      ->with('Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescription')
+      ->willReturn(array(new TestPermissionCallbacks(), 'titleDescription'));
+    $this->controllerResolver->expects($this->at(2))
+      ->method('getControllerFromDefinition')
+      ->with('Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescriptionRestrictAccess')
+      ->willReturn(array(new TestPermissionCallbacks(), 'titleDescriptionRestrictAccess'));
+
+    $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
+
+    // Setup system_rebuild_module_data().
+    $this->permissionHandler->setSystemRebuildModuleData($extensions);
+
+    $actual_permissions = $this->permissionHandler->getPermissions();
+    $this->assertPermissions($actual_permissions);
+  }
+
+  /**
+   * Tests a YAML file containing both static permissions and a callback.
+   */
+  public function testPermissionsYamlStaticAndCallback() {
+    vfsStreamWrapper::register();
+    $root = new vfsStreamDirectory('modules');
+    vfsStreamWrapper::setRoot($root);
+
+    $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+    $this->moduleHandler->expects($this->once())
+      ->method('getModuleDirectories')
+      ->willReturn(array(
+        'module_a' => vfsStream::url('modules/module_a'),
+      ));
+
+    $url = vfsStream::url('modules');
+    mkdir($url . '/module_a');
+    file_put_contents($url . '/module_a/module_a.permissions.yml',
+"'access module a':
+  title: 'Access A'
+  description: 'bla bla'
+permission_callbacks:
+  - 'Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescription'
+");
+
+    $modules = array('module_a');
+    $extensions = array(
+      'module_a' => $this->mockModuleExtension('module_a', 'Module a'),
+    );
+
+    $this->moduleHandler->expects($this->any())
+      ->method('getImplementations')
+      ->with('permission')
+      ->willReturn(array());
+
+    $this->moduleHandler->expects($this->any())
+      ->method('getModuleList')
+      ->willReturn(array_flip($modules));
+
+    $this->controllerResolver->expects($this->once())
+      ->method('getControllerFromDefinition')
+      ->with('Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescription')
+      ->willReturn(array(new TestPermissionCallbacks(), 'titleDescription'));
+
+    $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
+
+    // Setup system_rebuild_module_data().
+    $this->permissionHandler->setSystemRebuildModuleData($extensions);
+
+    $actual_permissions = $this->permissionHandler->getPermissions();
+
+    $this->assertCount(2, $actual_permissions);
+    $this->assertEquals($actual_permissions['access module a']['title'], 'Access A');
+    $this->assertEquals($actual_permissions['access module a']['provider'], 'module_a');
+    $this->assertEquals($actual_permissions['access module a']['description'], 'bla bla');
+    $this->assertEquals($actual_permissions['access module b']['title'], 'Access B');
+    $this->assertEquals($actual_permissions['access module b']['provider'], 'module_a');
+    $this->assertEquals($actual_permissions['access module b']['description'], 'bla bla');
+  }
+
   /**
    * Checks that the permissions are like expected.
    *
@@ -276,3 +424,32 @@ public function setSystemRebuildModuleData(array $extensions) {
   }
 
 }
+
+class TestPermissionCallbacks {
+
+  public function singleDescription() {
+    return array(
+      'access_module_a' => 'single_description'
+    );
+  }
+
+  public function titleDescription() {
+    return array(
+      'access module b' => array(
+        'title' => 'Access B',
+        'description' => 'bla bla',
+      ),
+    );
+  }
+
+  public function titleDescriptionRestrictAccess() {
+    return array(
+      'access_module_c' => array(
+        'title' => 'Access C',
+        'description' => 'bla bla',
+        'restrict access' => TRUE,
+      ),
+    );
+  }
+
+}
diff --git a/core/modules/user/user.services.yml b/core/modules/user/user.services.yml
index 7ccd7beaedcd8a01c3a3eaebfe24862e876e913e..7ffcc65f4fc187abccef1ddd7a6251dd5a51e99e 100644
--- a/core/modules/user/user.services.yml
+++ b/core/modules/user/user.services.yml
@@ -61,7 +61,7 @@ services:
       - { name: backend_overridable }
   user.permissions:
     class: Drupal\user\PermissionHandler
-    arguments: ['@module_handler', '@string_translation']
+    arguments: ['@module_handler', '@string_translation', '@controller_resolver']
 
 parameters:
   user.tempstore.expire: 604800