diff --git a/core/modules/user/lib/Drupal/user/Tests/UserPermissionsTest.php b/core/modules/user/lib/Drupal/user/Tests/UserPermissionsTest.php
index 2455b10..aefd590 100644
--- a/core/modules/user/lib/Drupal/user/Tests/UserPermissionsTest.php
+++ b/core/modules/user/lib/Drupal/user/Tests/UserPermissionsTest.php
@@ -73,13 +73,15 @@ function testAdministratorRole() {
     $edit['user_admin_role'] = $this->rid;
     $this->drupalPost('admin/config/people/accounts', $edit, t('Save configuration'));
 
-    // Enable aggregator module and ensure the 'administer news feeds'
+    // Enable aggregator and book module and ensure the 'administer news feeds'
     // permission is assigned by default.
     $edit = array();
     $edit['modules[Core][aggregator][enable]'] = TRUE;
+    $edit['modules[Core][book][enable]'] = TRUE;
     // Aggregator depends on file module, enable that as well.
     $edit['modules[Core][file][enable]'] = TRUE;
     $this->drupalPost('admin/modules', $edit, t('Save configuration'));
+    $this->assertTrue(user_access('edit own book content', $this->admin_user), 'The permission for node access was assigned to the administrator role.');
     $this->assertTrue(user_access('administer news feeds', $this->admin_user), 'The permission was automatically assigned to the administrator role');
   }
 
diff --git a/core/modules/user/user.admin.inc b/core/modules/user/user.admin.inc
index dcd76d7..9b9f750 100644
--- a/core/modules/user/user.admin.inc
+++ b/core/modules/user/user.admin.inc
@@ -326,7 +326,7 @@ function user_admin_settings($form, &$form_state) {
     '#title' => t('Administrator role'),
     '#default_value' => $config->get('admin_role'),
     '#options' => $roles,
-    '#description' => t('This role will be automatically assigned new permissions whenever a module is enabled. Changing this setting will not affect existing permissions.'),
+    '#description' => t('This role will be automatically assigned new permissions whenever a module is installed. Changing this setting will not affect existing permissions.'),
   );
 
   // @todo Remove this check once language settings are generalized.
diff --git a/core/modules/user/user.api.php b/core/modules/user/user.api.php
index b5bbbf7..5dcd86d 100644
--- a/core/modules/user/user.api.php
+++ b/core/modules/user/user.api.php
@@ -472,5 +472,27 @@ function hook_user_role_delete($role) {
 }
 
 /**
+ * Inform other modules that a user role has been deleted.
+ *
+ * Allows you act when a user role has been deleted. If your module stores
+ * references to roles, it's recommended that you implement this hook and delete
+ * existing instances of the deleted role in your module database tables.
+ *
+ * @param $role
+ *   The name of the role you're trying to find.
+ */
+function hook_user_permissions_insert($permissions) {
+}
+
+/**
+ * Allows you act when a permission is enabled for a user.
+ *
+ * @param $permissions
+ *   An array of permissions.
+ */
+function hook_user_permissions_delete($permissions) {
+}
+
+/**
  * @} End of "addtogroup hooks".
  */
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 0f1c93c..da236ba 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -2233,6 +2233,51 @@ function user_role_revoke_permissions($rid, array $permissions = array()) {
 }
 
 /**
+ * Adds new user permissions to the site.
+ *
+ * Modules should call this function when the list of permissions that the
+ * module provides has changed (for example, a dynamic permission has
+ * appeared).
+ *
+ * @todo: Change documentation since modules don't always have to call it.
+ *
+ * @param $permissions
+ *   A list of permission names to add.
+ */
+function user_permissions_insert(array $permissions = array()) {
+  // Allow modules to react on new permissions.
+  module_invoke_all('user_permissions_insert', $permissions);
+
+  // Grant any new permissions to the administration role.
+  $rid = variable_get('user_admin_role', 0);
+  if ($rid && !empty($permissions)) {
+    user_role_grant_permissions($rid, $permissions);
+  }
+}
+
+/**
+ * Removes user permissions that are no longer available.
+ *
+ * Modules should call this function when the list of permissions that the
+ * module provides has changed (for example, a dynamic permission has
+ * disappeared).
+ *
+ * @todo: Change documentation, or make all core modules actually call it.
+ *
+ * @param $permissions
+ *   A list of permission names to delete.
+ */
+function user_permissions_delete(array $permissions = array()) {
+  // Allow modules to act on the deleted permissions before they are removed.
+  module_invoke_all('user_permissions_delete', $permissions);
+
+  // Delete all records that currently reference the permissions.
+  db_delete('role_permission')
+    ->condition('permission', $permissions, 'IN')
+    ->execute();
+}
+
+/**
  * Implements hook_user_operations().
  */
 function user_user_operations($form = array(), $form_state = array()) {
@@ -2823,21 +2868,31 @@ function user_form_field_ui_field_edit_form_submit($form, &$form_state) {
 }
 
 /**
+ * Implements hook_modules_to_be_installed().
+ */
+function user_modules_to_be_installed($modules) {
+  // Record all currently defined permissions, for later use in
+  // user_modules_installed().
+  $all_permissions = &drupal_static(__FUNCTION__);
+  $all_permissions = array_keys(module_invoke_all('permission'));
+}
+
+/**
  * Implements hook_modules_installed().
  */
 function user_modules_installed($modules) {
-  // Assign all available permissions to the administrator role.
-  $rid = config('user.settings')->get('admin_role');
-  if ($rid) {
-    $permissions = array();
-    foreach ($modules as $module) {
-      if ($module_permissions = module_invoke($module, 'permission')) {
-        $permissions = array_merge($permissions, array_keys($module_permissions));
-      }
-    }
-    if (!empty($permissions)) {
-      user_role_grant_permissions($rid, $permissions);
-    }
+  // Find all permissions which were newly-added as a result of this module
+  // being installed. This could include permissions defined by the module
+  // itself as well as permissions defined by other modules in response to it
+  // (for example, new node permissions will appear if the module defines a
+  // content type).
+  $previous_permissions = &drupal_static('user_modules_to_be_installed', array());
+  $current_permissions = array_keys(module_invoke_all('permission'));
+  $new_permissions = array_diff($current_permissions, $previous_permissions);
+
+  // Add the new permissions to the site.
+  if (!empty($new_permissions)) {
+    user_permissions_insert($new_permissions);
   }
 }
 
@@ -2845,9 +2900,16 @@ function user_modules_installed($modules) {
  * Implements hook_modules_uninstalled().
  */
 function user_modules_uninstalled($modules) {
-   db_delete('role_permission')
-     ->condition('module', $modules, 'IN')
-     ->execute();
+  // Remove any permissions defined by this module from the site.
+  // @todo: This will skip any permissions that are not in the database (i.e.,
+  //   that are not assigned to any roles). This doesn't matter for our
+  //   purposes, but in theory it's a problem since user_permissions_delete()
+  //   invokes a hook which should get the full list of "deleted" permissions.
+  $permissions = db_query('SELECT DISTINCT(permission) FROM {role_permission} WHERE module IN (:modules)', array(':modules' => $modules))->fetchCol();
+  if (!empty($permissions)) {
+    user_permissions_delete($permissions);
+  }
+
   // Remove any potentially orphan module data stored for users.
   drupal_container()->get('user.data')->delete($modules);
 }
