diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index d96052d..b18085d 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -673,56 +673,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().
  */
 function node_ranking() {
diff --git a/core/modules/node/node.permissions.yml b/core/modules/node/node.permissions.yml
new file mode 100644
index 0000000..0c8a907
--- /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 0000000..5f35bcb
--- /dev/null
+++ b/core/modules/node/src/NodePermissions.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\NodePermissions.
+ */
+
+namespace Drupal\node;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\node\Entity\NodeType;
+
+/**
+ * Defines a class containing permission callbacks.
+ */
+class NodePermissions extends ControllerBase {
+
+  /**
+   * 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 += node_list_permissions($type);
+    }
+
+    return $perms;
+  }
+
+}
diff --git a/core/modules/user/src/PermissionHandler.php b/core/modules/user/src/PermissionHandler.php
index c396bcf..9bf024e 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;
@@ -45,6 +46,13 @@ class PermissionHandler implements PermissionHandlerInterface {
   protected $yamlDiscovery;
 
   /**
+   * The controller resolver.
+   *
+   * @var \Drupal\Core\Controller\ControllerResolverInterface
+   */
+  protected $controllerResolver;
+
+  /**
    * Constructs a new PermissionHandler.
    *
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
@@ -52,11 +60,12 @@ class PermissionHandler implements PermissionHandlerInterface {
    * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
    *   The string translation.
    */
-  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 +103,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 +145,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/PermissionHandlerTest.php b/core/modules/user/tests/src/PermissionHandlerTest.php
index 8856dd8..b0d5381 100644
--- a/core/modules/user/tests/src/PermissionHandlerTest.php
+++ b/core/modules/user/tests/src/PermissionHandlerTest.php
@@ -45,10 +45,18 @@ 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);
@@ -179,7 +187,87 @@ 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);
+
+    $actual_permissions = $this->permissionHandler->getPermissions();
+    $this->assertPermissions($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);
@@ -225,3 +313,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 7ccd7be..7ffcc65 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
