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/filter/filter.admin.inc b/core/modules/filter/filter.admin.inc
index b4a7008..db5ae8a 100644
--- a/core/modules/filter/filter.admin.inc
+++ b/core/modules/filter/filter.admin.inc
@@ -317,7 +317,10 @@ function filter_admin_format_form_submit($form, &$form_state) {
   }
   $status = filter_format_save($format);
 
-  // Save user permissions.
+  // Save user permissions. Note that for new text formats, this will overwrite
+  // any permission assignments made by filter_format_save(), but that is
+  // intentional, since choices made by the site administrator in the user
+  // interface should override the default behavior.
   if ($permission = filter_permission_name($format)) {
     foreach ($format->roles as $rid => $enabled) {
       user_role_change_permissions($rid, array($permission => $enabled));
diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index 3b8942f..364a96c 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -250,6 +250,10 @@ function filter_format_save($format) {
 
   if ($return == SAVED_NEW) {
     module_invoke_all('filter_format_insert', $format);
+    $permission = filter_permission_name($format);
+    if (!empty($permission)) {
+      user_permissions_insert(array($permission));
+    }
   }
   else {
     module_invoke_all('filter_format_update', $format);
@@ -287,6 +291,10 @@ function filter_format_disable($format) {
 
   // Allow modules to react on text format deletion.
   module_invoke_all('filter_format_disable', $format);
+  $permission = filter_permission_name($format);
+  if (!empty($permission)) {
+    user_permissions_delete(array($permission));
+  }
 
   // Clear the filter cache whenever a text format is disabled.
   filter_formats_reset();
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 93f0ddc..bd8e9a3 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -542,6 +542,9 @@ function node_type_save($info) {
     field_attach_create_bundle('node', $type->type);
 
     module_invoke_all('node_type_insert', $type);
+    if (in_array($type->type, node_permissions_get_configured_types())) {
+      user_permissions_insert(array_keys(node_list_permissions($type->type)));
+    }
     $status = SAVED_NEW;
   }
 
@@ -629,11 +632,15 @@ function node_field_extra_fields() {
  */
 function node_type_delete($type) {
   $info = node_type_get_type($type);
+  $permissions = in_array($type, node_permissions_get_configured_types()) ? array_keys(node_list_permissions($type)) : array();
   db_delete('node_type')
     ->condition('type', $type)
     ->execute();
   field_attach_delete_bundle('node', $type);
   module_invoke_all('node_type_delete', $info);
+  if ($permissions) {
+    user_permissions_delete($permissions);
+  }
 
   // Clear the node type cache.
   node_type_cache_reset();
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/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index 3a9258b..c1eb87f 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -86,17 +86,29 @@ function taxonomy_permission() {
     ),
   );
   foreach (taxonomy_vocabulary_load_multiple(FALSE) as $vocabulary) {
-    $permissions += array(
-      'edit terms in ' . $vocabulary->vid => array(
-        'title' => t('Edit terms in %vocabulary', array('%vocabulary' => $vocabulary->name)),
-      ),
-    );
-    $permissions += array(
-       'delete terms in ' . $vocabulary->vid => array(
-         'title' => t('Delete terms from %vocabulary', array('%vocabulary' => $vocabulary->name)),
-      ),
-    );
+    $permissions += taxonomy_vocabulary_list_permissions($vocabulary);
   }
+
+  return $permissions;
+}
+
+/**
+ * Generates a standard permission list for a given taxonomy vocabulary.
+ *
+ * @param $vocabulary
+ *   The vocabulary object to return permissions for.
+ *
+ * @return array
+ *   An array of permission information, suitable for use in hook_permission().
+ */
+function taxonomy_vocabulary_list_permissions($vocabulary) {
+  $permissions['edit terms in ' . $vocabulary->vid] = array(
+    'title' => t('Edit terms in %vocabulary', array('%vocabulary' => $vocabulary->name)),
+  );
+  $permissions['delete terms in ' . $vocabulary->vid] = array(
+    'title' => t('Delete terms from %vocabulary', array('%vocabulary' => $vocabulary->name)),
+  );
+
   return $permissions;
 }
 
@@ -482,6 +494,7 @@ function taxonomy_vocabulary_save($vocabulary) {
     field_attach_create_bundle('taxonomy_term', $vocabulary->machine_name);
     module_invoke_all('taxonomy_vocabulary_insert', $vocabulary);
     module_invoke_all('entity_insert', $vocabulary, 'taxonomy_vocabulary');
+    user_permissions_insert(array_keys(taxonomy_vocabulary_list_permissions($vocabulary)));
   }
 
   unset($vocabulary->original);
@@ -525,6 +538,9 @@ function taxonomy_vocabulary_delete($vid) {
     module_invoke_all('taxonomy_vocabulary_delete', $vocabulary);
     module_invoke_all('entity_delete', $vocabulary, 'taxonomy_vocabulary');
 
+    // Remove permissions associated with this vocabulary.
+    user_permissions_delete(array_keys(taxonomy_vocabulary_list_permissions($vocabulary)));
+
     cache_clear_all();
     taxonomy_vocabulary_static_reset();
 
diff --git a/core/modules/user/user.admin.inc b/core/modules/user/user.admin.inc
index f7d4552..afffd89 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 or when new permissions otherwise appear. 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..e645c98 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -3094,6 +3094,47 @@ 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).
+ *
+ * @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).
+ *
+ * @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 +3869,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', 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);
   }
 }
 
@@ -3850,9 +3901,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);
+  }
 }
 
 /**
