diff --git a/core/includes/module.inc b/core/includes/module.inc
index d61aba9..c48ef80 100644
--- a/core/includes/module.inc
+++ b/core/includes/module.inc
@@ -426,65 +426,79 @@ function module_enable($module_list, $enable_dependencies = TRUE) {
       ':name' => $module))
       ->fetchObject();
     if ($existing->status == 0) {
-      // Load the module's code.
-      drupal_load('module', $module);
-      module_load_install($module);
-
-      // Update the database and module list to reflect the new module. This
-      // needs to be done first so that the module's hook implementations,
-      // hook_schema() in particular, can be called while it is being
-      // installed.
-      db_update('system')
-        ->fields(array('status' => 1))
-        ->condition('type', 'module')
-        ->condition('name', $module)
-        ->execute();
-      // Refresh the module list to include it.
-      system_list_reset();
-      module_list(TRUE);
-      module_implements_reset();
-      _system_update_bootstrap_status();
-      // Update the registry to include it.
-      registry_update();
-      // Refresh the schema to include it.
-      drupal_get_schema(NULL, TRUE);
-
-      // Allow modules to react prior to the installation of a module.
-      module_invoke_all('modules_preinstall', array($module));
-
-      // Now install the module if necessary.
+      $modules_enabled[] = $module;
       if (drupal_get_installed_schema_version($module, TRUE) == SCHEMA_UNINSTALLED) {
-        drupal_install_schema($module);
-
-        // Set the schema version to the number of the last update provided
-        // by the module.
-        $versions = drupal_get_schema_versions($module);
-        $version = $versions ? max($versions) : SCHEMA_INSTALLED;
-
-        // If the module has no current updates, but has some that were
-        // previously removed, set the version to the value of
-        // hook_update_last_removed().
-        if ($last_removed = module_invoke($module, 'update_last_removed')) {
-          $version = max($version, $last_removed);
-        }
-        drupal_set_installed_schema_version($module, $version);
-        // Allow the module to perform install tasks.
-        module_invoke($module, 'install');
-        // Record the fact that it was installed.
         $modules_installed[] = $module;
-        watchdog('system', '%module module installed.', array('%module' => $module), WATCHDOG_INFO);
       }
+    }
+  }
 
-      // Allow modules to react prior to the enabling of a module.
-      module_invoke_all('modules_preenable', array($module));
-
-      // Enable the module.
-      module_invoke($module, 'enable');
+  // Allow modules to react on the list of modules that will be installed
+  // and/or enabled.
+  if (!empty($modules_installed)) {
+    module_invoke_all('modules_to_be_installed', $modules_installed);
+  }
+  if (!empty($modules_enabled)) {
+    module_invoke_all('modules_to_be_enabled', $modules_enabled);
+  }
 
-      // Record the fact that it was enabled.
-      $modules_enabled[] = $module;
-      watchdog('system', '%module module enabled.', array('%module' => $module), WATCHDOG_INFO);
+  // Enable (and, if necessary, install) each module.
+  foreach ($modules_enabled as $module) {
+    // Load the module's code.
+    drupal_load('module', $module);
+    module_load_install($module);
+
+    // Update the database and module list to reflect the new module. This
+    // needs to be done first so that the module's hook implementations,
+    // hook_schema() in particular, can be called while it is being installed.
+    db_update('system')
+      ->fields(array('status' => 1))
+      ->condition('type', 'module')
+      ->condition('name', $module)
+      ->execute();
+    // Refresh the module list to include it.
+    system_list_reset();
+    module_list(TRUE);
+    module_implements_reset();
+    _system_update_bootstrap_status();
+    // Update the registry to include it.
+    registry_update();
+    // Refresh the schema to include it.
+    drupal_get_schema(NULL, TRUE);
+
+    // Allow modules to react prior to the installation of a module.
+    module_invoke_all('modules_preinstall', array($module));
+
+    // Now install the module if necessary.
+    if (in_array($module, $modules_installed)) {
+      drupal_install_schema($module);
+
+      // Set the schema version to the number of the last update provided by
+      // the module.
+      $versions = drupal_get_schema_versions($module);
+      $version = $versions ? max($versions) : SCHEMA_INSTALLED;
+
+      // If the module has no current updates, but has some that were
+      // previously removed, set the version to the value of
+      // hook_update_last_removed().
+      if ($last_removed = module_invoke($module, 'update_last_removed')) {
+        $version = max($version, $last_removed);
+      }
+      drupal_set_installed_schema_version($module, $version);
+      // Allow the module to perform install tasks.
+      module_invoke($module, 'install');
+      // Record the fact that it was installed.
+      watchdog('system', '%module module installed.', array('%module' => $module), WATCHDOG_INFO);
     }
+
+    // Allow modules to react prior to the enabling of a module.
+    module_invoke_all('modules_preenable', array($module));
+
+    // Enable the module.
+    module_invoke($module, 'enable');
+
+    // Record the fact that it was enabled.
+    watchdog('system', '%module module enabled.', array('%module' => $module), WATCHDOG_INFO);
   }
 
   // If any modules were newly installed, invoke hook_modules_installed().
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 8947ac1..fe41e90 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -2079,6 +2079,18 @@ function hook_flush_caches() {
 }
 
 /**
+ * @todo Document this.
+ */
+function hook_modules_to_be_installed($modules) {
+}
+
+/**
+ * @todo Document this.
+ */
+function hook_modules_to_be_enabled($modules) {
+}
+
+/**
  * Perform necessary actions before modules are installed.
  *
  * This function allows all modules to react prior to a module being installed.
diff --git a/core/modules/user/user.admin.inc b/core/modules/user/user.admin.inc
index f7d4552..3bb710b 100644
--- a/core/modules/user/user.admin.inc
+++ b/core/modules/user/user.admin.inc
@@ -289,7 +289,7 @@ function user_admin_settings() {
     '#title' => t('Administrator role'),
     '#default_value' => variable_get('user_admin_role', 0),
     '#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.'),
   );
 
   // User registration settings.
diff --git a/core/modules/user/user.api.php b/core/modules/user/user.api.php
index 752d3d4..ad4a742 100644
--- a/core/modules/user/user.api.php
+++ b/core/modules/user/user.api.php
@@ -462,5 +462,17 @@ function hook_user_role_delete($role) {
 }
 
 /**
+ * @todo Document this.
+ */
+function hook_user_permissions_insert($permissions) {
+}
+
+/**
+ * @todo Document this.
+ */
+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 928daad..5783cd6 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -3094,6 +3094,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 no longer available user permissions from the site.
+ *
+ * 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()) {
@@ -3828,21 +3873,31 @@ function user_register_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 = variable_get('user_admin_role', 0);
-  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');
+  $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);
   }
 }
 
@@ -3850,9 +3905,15 @@ 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);
+  }
 }
 
 /**
