granulare Berechtigungen zum Erstellen und Bearbeiten von Benutzern in Drupal 7

Ab und zu kann es vorkommen, dass man bestimmten Benutzern bzw. Benutzergruppen Berechtigungen für bestimmte Bereiche von Drupal geben will, die so von Drupal nicht vorgesehen sind.

Beispiele dafür sind die Berechtigungen, neue Benutzer anzulegen bzw. bestehende Benutzerkonten zu bearbeiten. Dies ist bei Drupal nur mit der Berechtigung „administer users“ („Benutzer verwalten“) möglich, womit jedoch auch Funktionen freigeschaltet werden, die man den betroffenen Benutzern unter keinen Umständen einräumen möchte.

Glücklicherweise kann man das Problem mit einem eigenen kleinen Modul, ein paar Hooks und ein wenig Copy&Paste-Magie umgehen.

Die Lösung

In unserem Modul (nennen wir es „dingens“) müssen wir uns ein paar zusätzliche Berechtigungen definieren, einen neuen Menüpunkt definieren und die Zugriffsbeschränkungen für bestehende Menüpunkte verändern:

<?php 
/**
 * Implements hook_menu().
 */
function dingens_menu() {
  $items = array();
  // Add menu item for „new account“-page.
  $items['admin/people/new'] = array(
    'title' => 'Add user',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('dingens_user_register_form'),
    'access arguments' => array('create new users'),
  );
  return $items;
}

 

/**
 * Implements hook_menu_alter().
 */
function dingens_menu_alter(&$items) {
  // Replace access callback for user-edit pages.
  $items['user/%user/edit']['access callback'] = 'dingens_user_edit_access';
  if (($categories = _user_categories()) && (count($categories) > 1)) {
    foreach ($categories as $key => $category) {
      // 'account' is already handled by the MENU_DEFAULT_LOCAL_TASK.
      if ($category['name'] != 'account') {
        $items['user/%user_category/edit/' . $category['name']]['access callback'] = isset($category['access callback']) ? $category['access callback'] :'dingens_user_edit_access';
      }
    }
  }
}

/**
 * Implements hook_permission().
 */
function dingens_permission() {
  return array(
    'create new users' => array(
      'title' => t('Create new users'),
      'description' => t('Create new user accounts (without being able to edit the created accounts).'),
    ),
    'edit user accounts' => array(
      'title' => t('Edit user accounts'),
      'description' => t('Edit existing user accounts (without being able to create new accounts).'),
    ),
  );
}
?>

Leider ist die Berechtigung „administer users“ und der Benutzer mit der uid 1 ziemlich fest in den Funktionen, die für die Erstellung der Formulare zum Erstellen und Bearbeiten von Benutzerkonten zuständig sind, verdrahtet, so dass wir diese Funktionen kopieren und leicht anpassen müssen.

<?php
/**
 * Access callback for user account editing.
 */
function dingens_user_edit_access($account) {
  return $account->uid != 1 && (($GLOBALS['user']->uid == $account->uid) ||user_access('administer users') || user_access('edit user accounts')) && $account->uid > 0;
}

 

/**
 * Form builder; the user registration form (without default admin checks).
 */
function dingens_user_register_form($form, &$form_state) {
  $form['#user'] = drupal_anonymous_user();
  $form['#user_category'] = 'register';

  $form['#attached']['library'][] = array('system', 'jquery.cookie');
  $form['#attributes']['class'][] = 'user-info-from-cookie';

  // Start with the default user account fields.
  dingens_user_account_form($form, $form_state);

  // Attach field widgets, and hide the ones where the 'user_register_form'
  // setting is not on.
  field_attach_form('user', $form['#user'], $form, $form_state);
  foreach (field_info_instances('user', 'user') as $field_name => $instance) {
    if (empty($instance['settings']['user_register_form'])) {
      $form[$field_name]['#access'] = FALSE;
    }
  }

  $form['actions'] = array('#type' => 'actions');
  $form['actions']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Create new account'),
  );

  $form['#validate'][] = 'user_register_validate';
  // Add the final user registration form submit handler.
  $form['#submit'][] = 'user_register_submit';

  return $form;
}

/**
 * Helper function to add default user account fields to user registration form.
 */
function dingens_user_account_form(&$form, &$form_state) {
  global $user;

  $account = $form['#user'];

  $form['#validate'][] = 'user_account_form_validate';

  // Account information.
  $form['account'] = array(
    '#type'   => 'container',
    '#weight' => -10,
  );
  // Only show name field on registration form or user can change own username.
  $form['account']['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Username'),
    '#maxlength' => USERNAME_MAX_LENGTH,
    '#description' => t('Spaces are allowed; punctuation is not allowed except for periods, hyphens, apostrophes, and underscores.'),
    '#required' => TRUE,
    '#attributes' => array('class' => array('username')),
    '#default_value' => '',
    '#access' =>  TRUE,
    '#weight' => -10,
  );

  $form['account']['mail'] = array(
    '#type' => 'textfield',
    '#title' => t('E-mail address'),
    '#maxlength' => EMAIL_MAX_LENGTH,
    '#description' => t('A valid e-mail address. All e-mails from the system will be sent to this address. The e-mail address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by e-mail.'),
    '#required' => TRUE,
    '#default_value' => '',
  );

  $form['account']['pass'] = array(
    '#type' => 'password_confirm',
    '#size' => 25,
    '#description' => t('Provide a password for the new account in both fields.'),
    '#required' => TRUE,
  );

  // To skip the current password field, the user must have logged in via a
  // one-time link and have the token in the URL.
  $pass_reset = isset($_SESSION['pass_reset_' . $account->uid]) && isset($_GET['pass-reset-token']) && ($_GET['pass-reset-token'] ==$_SESSION['pass_reset_' . $account->uid]);
  $protected_values = array();
  $current_pass_description = '';
  // The user may only change their own password without their current
  // password if they logged in via a one-time login link.
  if (!$pass_reset) {
    $protected_values['mail'] = $form['account']['mail']['#title'];
    $protected_values['pass'] = t('Password');
    $request_new = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.'))));
    $current_pass_description = t('Enter your current password to change the %mail or %pass. !request_new.', array('%mail' => $protected_values['mail'], '%pass' =>$protected_values['pass'], '!request_new' => $request_new));
  }
  // The user must enter their current password to change to a new one.
  if ($user->uid == $account->uid) {
    $form['account']['current_pass_required_values'] = array(
      '#type' => 'value',
      '#value' => $protected_values,
    );
    $form['account']['current_pass'] = array(
      '#type' => 'password',
      '#title' => t('Current password'),
      '#size' => 25,
      '#access' => !empty($protected_values),
      '#description' => $current_pass_description,
      '#weight' => -5,
      '#attributes' => array('autocomplete' => 'off'),
    );
    $form['#validate'][] = 'user_validate_current_pass';
  }

  $form['account']['status'] = array(
    '#type' => 'radios',
    '#title' => t('Status'),
    '#default_value' => 1,
    '#options' => array(t('Blocked'), t('Active')),
    '#access' => TRUE,
  );

  $roles = array_map('check_plain', user_roles(TRUE));
  // The disabled checkbox subelement for the 'authenticated user' role
  // must be generated separately and added to the checkboxes element,
  // because of a limitation in Form API not supporting a single disabled
  // checkbox within a set of checkboxes.
  // @todo This should be solved more elegantly. See issue #119038.
  $checkbox_authenticated = array(
    '#type' => 'checkbox',
    '#title' => $roles[DRUPAL_AUTHENTICATED_RID],
    '#default_value' => TRUE,
    '#disabled' => TRUE,
  );
  unset($roles[DRUPAL_AUTHENTICATED_RID]);
  $form['account']['roles'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Roles'),
    '#default_value' => array(),
    '#options' => $roles,
    '#access' => TRUE,
    DRUPAL_AUTHENTICATED_RID => $checkbox_authenticated,
  );

  $form['account']['notify'] = array(
    '#type' => 'checkbox',
    '#title' => t('Notify user of new account'),
    '#access' => TRUE,
  );

  // Signature.
  $form['signature_settings'] = array(
    '#type' => 'fieldset',
    '#title' => t('Signature settings'),
    '#weight' => 1,
    '#access' => variable_get('user_signatures', 0),
  );

  $form['signature_settings']['signature'] = array(
    '#type' => 'text_format',
    '#title' => t('Signature'),
    '#default_value' => isset($account->signature) ? $account->signature : '',
    '#description' => t('Your signature will be publicly displayed at the end of your comments.'),
    '#format' => isset($account->signature_format) ? $account->signature_format :NULL,
  );

  // Picture/avatar.
  $form['picture'] = array(
    '#type' => 'fieldset',
    '#title' => t('Picture'),
    '#weight' => 1,
    '#access' => variable_get('user_pictures', 0),
  );
  $form['picture']['picture'] = array(
    '#type' => 'value',
    '#value' => isset($account->picture) ? $account->picture : NULL,
  );
  $form['picture']['picture_current'] = array(
    '#markup' => theme('user_picture', array('account' => $account)),
  );
  $form['picture']['picture_delete'] = array(
    '#type' => 'checkbox',
    '#title' => t('Delete picture'),
    '#access' => !empty($account->picture->fid),
    '#description' => t('Check this box to delete your current picture.'),
  );
  $form['picture']['picture_upload'] = array(
    '#type' => 'file',
    '#title' => t('Upload picture'),
    '#size' => 48,
    '#description' => t('Your virtual face or picture. Pictures larger than @dimensions pixels will be scaled down.', array('@dimensions' =>variable_get('user_picture_dimensions', '85x85'))) . ' ' .filter_xss_admin(variable_get('user_picture_guidelines', '')),
  );
  $form['#validate'][] = 'user_validate_picture';
}
?>

Damit kann man nun bestimmten Benutzern die Berechtigungen zum Erstellen von neuen bzw. zum Bearbeiten bestehender Benutzerkonten geben, ohne die Berechtigung „administer users“ vergeben zu müssen.
Nachteil dieser Methode ist natürlich, dass man Teile des Drupal-Kerns kopiert und damit eventuelle Änderungen an diesen Funktionen (z.B. durch Sicherheitsupdates) auch selbst nachpflegen muss.

Zusätzlich könnte man nun noch per hook_form_alter() bestimmte Elemente der Formulare ausblenden (<?php $element['#access'] = FALSE; ?>!), so dass die Benutzer mit den gerade erstellten Berechtigungen beispielsweise keine Rollenzuordnungen machen können.