You are here

administerusersbyrole.module in Administer Users by Role 7.2

Provides fine-grained permissions for creating, editing, and deleting users.

This module allows site builders to set up fine-grained permissions for allowing users to edit and cancel other users: more specific than Drupal Core's all-or-nothing 'administer users' permission. It also provides and enforces a 'create users' permission.

File

administerusersbyrole.module
View source
<?php

/**
 * @file
 * Provides fine-grained permissions for creating, editing, and deleting users.
 *
 * This module allows site builders to set up fine-grained permissions for
 * allowing users to edit and cancel other users:  more specific than
 * Drupal Core's all-or-nothing 'administer users' permission.  It also
 * provides and enforces a 'create users' permission.
 */

/**
 * Implements hook_permission().
 */
function administerusersbyrole_permission() {
  $roles = user_roles(TRUE);

  // Exclude the admin role.  Once you can edit an admin, you can set their password, log in and do anything,
  // which defeats the point of using this module.
  $admin_rid = variable_get('user_admin_role', 0);
  $perms = array();
  $perms['create users'] = array(
    'title' => 'Create new users',
  );
  $perms['access users overview'] = array(
    'title' => 'Access the users overview page',
  );
  $ops = array(
    'edit' => t('edit'),
    'cancel' => t('cancel'),
  );
  foreach ($roles as $rid => $role) {
    if ($rid == $admin_rid) {
      continue;
    }
    foreach ($ops as $op => $operation) {
      $perm_string = _administerusersbyrole_build_perm_string($rid, $op);
      if ($rid === DRUPAL_AUTHENTICATED_RID) {
        $perm_title = drupal_ucfirst(t("@operation users with no custom roles", array(
          '@operation' => $operation,
        )));
      }
      else {
        $perm_title = drupal_ucfirst(t("@operation users with role %role", array(
          '@operation' => $operation,
          '%role' => $role,
        )));
      }
      $perms[$perm_string] = array(
        'title' => $perm_title,
      );
    }
  }
  return $perms;
}

/**
 * Implements hook_menu_alter().
 */
function administerusersbyrole_menu_alter(&$items) {

  // Dependency was added on chain_menu_access and, immediately after an update, the dependency may be missing.
  // Make sure we don't render the whole website unusable in this case.
  if (!module_exists('chain_menu_access')) {
    return;
  }
  chain_menu_access_chain($items, 'user/%user', '_administerusersbyrole_check_access', array(
    1,
    'edit',
  ), TRUE);
  chain_menu_access_chain($items, 'user/%user/edit', '_administerusersbyrole_check_access', array(
    1,
    'edit',
  ), TRUE);
  chain_menu_access_chain($items, 'user/%user/cancel', '_administerusersbyrole_check_access', array(
    1,
    'cancel',
  ), TRUE);
  $items['user/%user/cancel']['page callback'] = 'administerusersbyrole_cancel_confirm_wrapper';
  $items['user/%user/cancel']['page arguments'] = array(
    1,
  );
  chain_menu_access_chain($items, 'admin/people', 'user_access', array(
    'access users overview',
  ), TRUE);

  // The code in the user module to create a user relies on 'administer users' permission being set, so pass an argument to elevate permissions.
  chain_menu_access_chain($items, 'admin/people/create', '_administerusersbyrole_can_create_users', array(
    'elevate',
  ), TRUE);

  // Compatibility with other contrib modules.
  // If 'password_policy_password_tab' module (a sub-module of 'password_policy') is enabled, then check and update access.
  if (module_exists('password_policy_password_tab')) {
    chain_menu_access_chain($items, 'user/%user/password', '_administerusersbyrole_check_access', array(
      1,
      'edit',
    ), TRUE);
  }
}

/**
 * Implements hook_module_implements_alter().
 *
 * We need to be after the call from the entity module so our hook takes precedence.
 */
function administerusersbyrole_module_implements_alter(&$implementations, $hook) {
  if ($hook == 'entity_info_alter') {

    // Move our hook implementation to the bottom.
    $group = $implementations['administerusersbyrole'];
    unset($implementations['administerusersbyrole']);
    $implementations['administerusersbyrole'] = $group;
  }
}

/**
 * Implements hook_views_default_views_alter().
 */
function administerusersbyrole_views_default_views_alter(&$views) {
  if (isset($views['admin_views_user'])) {

    // Add a tag to the admin_views module users view.
    $handler =& $views['admin_views_user']->display['default']->handler;
    $handler->display->display_options['query']['options']['query_tags'] = array(
      'administerusersbyrole_edit_access',
    );
  }
}

/**
 * Implements hook_query_alter().
 */
function administerusersbyrole_query_alter(QueryAlterableInterface $query) {

  // The tag administerusersbyrole_edit_access is used to indicate that we should filter out users where there isn't edit access.
  if ($query
    ->hasTag('administerusersbyrole_edit_access') && !user_access('administer users')) {

    // Exclude the root user.
    $query
      ->condition('users.uid', 1, '<>');
    $roles = user_roles(TRUE);
    foreach ($roles as $rid => $role) {
      if (!user_access(_administerusersbyrole_build_perm_string($rid, 'edit'))) {
        $exclude[$rid] = $rid;
      }
    }
    if (isset($exclude[DRUPAL_AUTHENTICATED_RID])) {

      // No permission unless there is a role.
      $query
        ->join('users_roles', 'users_roles_2', 'users_roles_2.uid=users.uid');
      unset($exclude[DRUPAL_AUTHENTICATED_RID]);
    }

    // Do an "anti-join" on the excluded roles - add a left join and then check the results set is null.
    // NB We don't have to check that $exclude might be empty, because it always contains the admin role.
    $urAlias = $query
      ->leftjoin('users_roles', 'ur', 'ur.uid=users.uid AND ur.rid IN (:exclude)', array(
      ':exclude' => $exclude,
    ));
    $query
      ->isNull("{$urAlias}.uid");
  }
}

/**
 * Determine access to create user accounts.
 */
function _administerusersbyrole_can_create_users($extra = '') {
  if (user_access('create users')) {
    if ($extra === 'elevate') {
      _administerusersbyrole_temp_administer_users();
    }
    return TRUE;
  }
  return FALSE;
}

/**
 * Wrapper function for the cancel confirm form that first elevates to 'administer users' permission
 * if required.
 */
function administerusersbyrole_cancel_confirm_wrapper($account) {

  // If we are granting permissions, elevate to 'administer users'.
  // Don't do this for a user cancelling their own account.
  if (_administerusersbyrole_check_access($account, 'cancel')) {
    _administerusersbyrole_temp_administer_users();
  }
  return drupal_get_form('user_cancel_confirm_form', $account);
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function administerusersbyrole_form_user_admin_account_alter(&$form, &$form_state, $form_id) {
  if (!user_access('administer users')) {

    // Remove rows if user doesn't have permission to edit them.
    // This deliberately removes users with cancel access but not edit access.  Although it seems attractive to keep them,
    // please DO NOT change this behaviour.  If we did, then users would be able to run bulk edits on other users
    // when having cancel permission but no edit permission.
    foreach ($form['accounts']['#options'] as $uid => $fields) {
      $account = user_load($uid);

      // This form exposes operations such as block account that shouldn't be available on a user's own account,
      // so just check our own permissions.
      if (!_administerusersbyrole_check_access($account, 'edit')) {
        unset($form['accounts']['#options'][$uid]);
      }
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Check for cancel permissions.  The access control for the people admin page restricts the roles to those
 * which we have _edit_ access rather than cancel access.
 */
function administerusersbyrole_form_user_multiple_cancel_confirm_alter(&$form, &$form_state) {
  $anyallowed = FALSE;
  foreach ($form_state['input']['accounts'] as $uid) {
    $account = user_load($uid);

    // This form bypasses checks and restrictions present when cancelling the user's own account,
    // so just check our own permissions.
    if (_administerusersbyrole_check_access($account, 'cancel')) {
      $anyallowed = TRUE;
    }
    else {
      drupal_set_message(t('You do not have permission to cancel %user.', array(
        '%user' => $account->name,
      )), 'error');
      unset($form_state['input']['accounts'][$uid]);
      unset($form['accounts'][$uid]);
    }
  }
  if (!$anyallowed) {
    drupal_goto(drupal_substr($form['#action'], 1));
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Add extra visibility depending on our permissions.
 */
function administerusersbyrole_form_user_profile_form_alter(&$form, &$form_state) {
  $account = $form['#user'];

  // We just check against this module's own permissions.
  // Don't check against the permissions in Drupal core, as those checks have already done and are subtle.
  // (For example, users can't necesarily change their own username and can't block their own account.)
  if (_administerusersbyrole_check_access($account, 'edit')) {
    $form['account']['name']['#access'] = TRUE;
    $form['account']['status']['#access'] = TRUE;
  }
  if (_administerusersbyrole_check_access($account, 'cancel')) {
    $form['actions']['cancel']['#access'] = TRUE;
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Add a wrapper to elevate user permissions during AJAX submissions
 */
function administerusersbyrole_form_user_register_form_alter(&$form, &$form_state) {
  $form_state['wrapper_callback'] = '_administerusersbyrole_user_profile_form_wrapper';
}

/**
 * Handler to elevate user permissions when form is reloaded, as by an AJAX submission
 */
function _administerusersbyrole_user_profile_form_wrapper($form, &$form_state) {
  _administerusersbyrole_can_create_users('elevate');
  return $form;
}

/**
 * Implements hook_entity_info_alter().
 */
function administerusersbyrole_entity_info_alter(&$entity_info) {
  $entity_info['user']['access callback'] = 'administerusersbyrole_metadata_user_access';
}

/**
 * Implements hook_entity_property_info_alter().
 */
function administerusersbyrole_entity_property_info_alter(&$info) {
  $properties =& $info['user']['properties'];
  $properties['name']['access callback'] = 'administerusersbyrole_metadata_user_properties_access';
  $properties['mail']['access callback'] = 'administerusersbyrole_metadata_user_properties_access';
  $properties['status']['access callback'] = 'administerusersbyrole_metadata_user_properties_access';
  $properties['theme']['access callback'] = 'administerusersbyrole_metadata_user_properties_access';
}

/**
 * Access callback for the user entity.
 *
 * NB the names of variables here are deliberately different from the ones used in the entity module.
 * The reason is to be consistent with the rest of this module.  In the entity module:
 * - Parameter 2 is named '$entity'.  Entity is a user account, which is referred to as $account
 *   throughout in this module, so we stick with that.
 * - Parameter 3 is named '$account', meaning the account to check as, which is referred to as $check_as
 *   in this module.
 */
function administerusersbyrole_metadata_user_access($op, $account = NULL, $check_as = NULL) {

  // Call the base function.
  if (entity_metadata_user_access($op, $account, $check_as)) {
    return TRUE;
  }
  $convert = array(
    'delete' => 'cancel',
    'update' => 'edit',
  );
  if (isset($convert[$op])) {

    // Call our own function.
    $check_as = isset($check_as) ? $check_as : $GLOBALS['user'];
    return _administerusersbyrole_check_access($account, $convert[$op], $check_as);
  }
  return FALSE;
}

/**
 * Access callback for user entity properties.
 */
function administerusersbyrole_metadata_user_properties_access($op, $property, $account = NULL, $check_as = NULL) {

  // Call the base function.
  if (entity_metadata_user_properties_access($op, $property, $account, $check_as)) {
    return TRUE;
  }
  $check_as = isset($check_as) ? $check_as : $GLOBALS['user'];
  return _administerusersbyrole_check_access($account, 'edit', $check_as);
}

/**
 * Check access to perform an operation on an account.
 *
 * This function checks the permissions of this module only.  The calling code needs
 * to check any Drupal core permissions that should also allow access.
 */
function _administerusersbyrole_check_access($account, $op, $check_as = NULL) {

  // Never allow uid 0 (anonymous) or 1 (master admin).
  if ($account->uid <= 1) {
    return FALSE;
  }

  // We may have been passed a mock account object. If so, load the user to ensure
  // that we have roles to check against.
  if (!isset($account->roles)) {
    $account = user_load($account->uid);
  }
  foreach ($account->roles as $rid => $role) {

    // If there is only DRUPAL_AUTHENTICATED_RID, then we must test for it, otherwise skip it.
    if ($rid === DRUPAL_AUTHENTICATED_RID && count($account->roles) > 1) {
      continue;
    }
    if (!user_access(_administerusersbyrole_build_perm_string($rid, $op), $check_as)) {
      return FALSE;
    }
  }
  return TRUE;
}

/**
 * Generates a permission string for a given a role.
 */
function _administerusersbyrole_build_perm_string($role_id, $op = 'edit') {
  $perm = "{$op} users with role {$role_id}";
  return $perm;
}

/**
 * Temporarily override 'administer users' for the duration or processing this page.
 */
function _administerusersbyrole_temp_administer_users() {
  global $user;
  $static =& drupal_static('user_access');
  $static[$user->uid]['administer users'] = TRUE;
}

/**
 * Implements hook_views_api().
 */
function administerusersbyrole_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'administerusersbyrole') . '/views',
  );
}

Functions

Namesort descending Description
administerusersbyrole_cancel_confirm_wrapper Wrapper function for the cancel confirm form that first elevates to 'administer users' permission if required.
administerusersbyrole_entity_info_alter Implements hook_entity_info_alter().
administerusersbyrole_entity_property_info_alter Implements hook_entity_property_info_alter().
administerusersbyrole_form_user_admin_account_alter Implements hook_form_FORM_ID_alter().
administerusersbyrole_form_user_multiple_cancel_confirm_alter Implements hook_form_FORM_ID_alter().
administerusersbyrole_form_user_profile_form_alter Implements hook_form_FORM_ID_alter().
administerusersbyrole_form_user_register_form_alter Implements hook_form_FORM_ID_alter().
administerusersbyrole_menu_alter Implements hook_menu_alter().
administerusersbyrole_metadata_user_access Access callback for the user entity.
administerusersbyrole_metadata_user_properties_access Access callback for user entity properties.
administerusersbyrole_module_implements_alter Implements hook_module_implements_alter().
administerusersbyrole_permission Implements hook_permission().
administerusersbyrole_query_alter Implements hook_query_alter().
administerusersbyrole_views_api Implements hook_views_api().
administerusersbyrole_views_default_views_alter Implements hook_views_default_views_alter().
_administerusersbyrole_build_perm_string Generates a permission string for a given a role.
_administerusersbyrole_can_create_users Determine access to create user accounts.
_administerusersbyrole_check_access Check access to perform an operation on an account.
_administerusersbyrole_temp_administer_users Temporarily override 'administer users' for the duration or processing this page.
_administerusersbyrole_user_profile_form_wrapper Handler to elevate user permissions when form is reloaded, as by an AJAX submission