You are here

role_delegation.module in Role Delegation 8

Allows admins to grant roles the authority to assign selected roles to users.

This module allows site administrators to grant some roles the authority to change roles assigned to users, without them needing the 'administer access control' permission.

It provides its own tab in the user profile so that roles can be changed without needing access to the user edit form.

File

role_delegation.module
View source
<?php

/**
 * @file
 * Allows admins to grant roles the authority to assign selected roles to users.
 *
 * This module allows site administrators to grant some roles the authority to
 * change roles assigned to users, without them needing the 'administer access
 * control' permission.
 *
 * It provides its own tab in the user profile so that roles can be changed
 * without needing access to the user edit form.
 */
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\role_delegation\DelegatableRoles;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
use Drupal\user\UserInterface;

/**
 * Implements hook_help().
 */
function role_delegation_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.role_delegation':
      $output = '<p>' . t('This module allows site administrators to grant some roles the authority to assign selected roles to users, without them needing the <em>administer permissions</em> permission.') . '</p>';
      $output .= '<p>' . t('It provides its own tab in the user profile so that roles can be assigned without needing access to the user edit form.') . '</p>';
      return $output;
  }
}

/**
 * Implements hook_ENTITY_TYPE_delete().
 */
function role_delegation_user_role_delete(RoleInterface $entity) {
  $permission = sprintf('assign %s role', $entity
    ->id());

  /** @var array $roles */
  $roles = \Drupal::entityQuery('user_role')
    ->condition('permissions.*', $permission)
    ->condition('id', $entity
    ->id(), '<>')
    ->execute();

  /** @var \Drupal\user\RoleInterface $role */
  foreach (Role::loadMultiple($roles) as $role) {
    $role
      ->revokePermission($permission);
    $role
      ->save();
  }
}

/**
 * Implements hook_ENTITY_TYPE_presave().
 */
function role_delegation_user_presave(UserInterface $entity) {
  if (!$entity
    ->hasField('role_change')) {
    return;
  }
  $submitted_roles = [];
  foreach ($entity->role_change as $item) {
    $submitted_roles[] = $item->target_id;
  }

  // Change roles based on the field for role delegation.
  if ($submitted_roles !== DelegatableRoles::$emptyFieldValue) {
    $current_user = \Drupal::currentUser();
    $delegatable_roles = array_keys(\Drupal::service('delegatable_roles')
      ->getAssignableRoles($current_user));

    // Of the roles that were submitted, only add ones that the user has access
    // to use.
    $add_roles = array_intersect($delegatable_roles, $submitted_roles);
    foreach ($add_roles as $id) {
      $entity
        ->addRole($id);
    }

    // Any roles that the user has access to use and did not include in
    // submission are removals.
    $remove_roles = array_diff($delegatable_roles, $submitted_roles);
    foreach ($remove_roles as $id) {
      $entity
        ->removeRole($id);
    }
  }
}

/**
 * Implements hook_ENTITY_TYPE_load().
 */
function role_delegation_user_load($entities) {

  // This is a workaround for known limitations of computed fields: since they
  // are not stored, they are also not loaded with the user, so values must be
  // manually supplied. This allows us to later determine that an empty field
  // actually means intentional role removals, as opposed to field data not
  // being sent/no access to field.
  // Things may later with https://www.drupal.org/node/2392845.
  foreach ($entities as $user_entity) {
    if ($user_entity
      ->hasField('role_change')) {
      $user_entity
        ->set('role_change', DelegatableRoles::$emptyFieldValue);
    }
  }
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 */
function role_delegation_form_user_form_alter(&$form, FormStateInterface $form_state, $form_id) {

  // Add an entity builder for the user entity to ensure that it recieves the
  // "empty" value when the field is not accessible.
  if (isset($form['role_change'])) {
    $form['role_change']['#group'] = 'account';
    $form['#entity_builders'][] = 'role_delegation_user_form_builder';
  }
}

/**
 * Implements hook_field_widget_form_alter().
 */
function role_delegation_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) {

  /** @var \Drupal\Core\Field\FieldItemListInterface $items */

  /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
  $items = $context['items'];
  $field_definition = $items
    ->getFieldDefinition();

  // Since the field is computed, the default value of the form element will be
  // empty, so we need to adjust it.
  if ($field_definition
    ->getTargetEntityTypeId() === 'user' && $field_definition
    ->getName() === 'role_change' && isset($element['#options'])) {
    $roles_current = $items
      ->getEntity()
      ->getRoles();
    $roles_options = array_keys($element['#options']);
    $element['#default_value'] = array_intersect($roles_current, $roles_options);
  }
}

/**
 * Implements hook_options_list_alter().
 */
function role_delegation_options_list_alter(array &$options, array $context) {

  /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
  $field_definition = $context['fieldDefinition'];

  // By default, ALL the entities for a given type will be used for the options
  // on an enity reference field, but we only want the user to be able to choose
  // from the roles they can assign.
  if ($field_definition
    ->getTargetEntityTypeId() === 'user' && $field_definition
    ->getName() === 'role_change') {
    $current_user = \Drupal::currentUser();
    $options = \Drupal::service('delegatable_roles')
      ->getAssignableRoles($current_user);
  }
}

/**
 * Entity builder for the user form with empty field value for "role_change".
 *
 * @see role_delegation_form_alter()
 */
function role_delegation_user_form_builder($entity_type, UserInterface $user, &$form, FormStateInterface $form_state) {

  // If the user has no access to the "role_change" field, then the form will
  // submit an empty array for the field, which will make later processing think
  // it was intentional. Set it to the empty field value to correct this.
  if (!isset($form['role_change']['#access']) || !$form['role_change']['#access']) {
    $user
      ->set('role_change', DelegatableRoles::$emptyFieldValue);
  }
}

/**
 * Implements hook_entity_base_field_info().
 */
function role_delegation_entity_base_field_info(EntityTypeInterface $entity_type) {
  $fields = [];
  if ($entity_type
    ->id() === 'user') {
    $fields['role_change'] = BaseFieldDefinition::create('entity_reference')
      ->setLabel(t('Roles'))
      ->setSetting('target_type', 'user_role')
      ->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED)
      ->setComputed(TRUE)
      ->setDisplayOptions('form', [
      'type' => 'options_buttons',
      'weight' => 1,
    ])
      ->setSetting('handler', 'role_change:user_role')
      ->setDefaultValue(DelegatableRoles::$emptyFieldValue);
  }
  return $fields;
}

/**
 * Implements hook_entity_field_access().
 */
function role_delegation_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
  if ($operation === 'edit' && $field_definition
    ->getName() === 'role_change' && $field_definition
    ->getTargetEntityTypeId() === 'user') {

    // Deny access if the user has access to the normal roles field.
    if ($account
      ->hasPermission('administer permissions')) {
      return AccessResult::forbidden()
        ->cachePerPermissions();
    }

    // Or if they don't have at least one role that allows them to delegate.
    $permissions = \Drupal::service('permission_generator.role_delegation')
      ->rolePermissions();
    $permissions = array_merge([
      'assign all roles',
    ], array_keys($permissions));
    foreach ($permissions as $permission) {
      if ($account
        ->hasPermission($permission)) {
        return AccessResult::allowed()
          ->cachePerPermissions();
      }
    }
    return AccessResult::forbidden()
      ->cachePerPermissions();
  }
  return AccessResult::neutral();
}

/**
 * Implements hook_entity_operation().
 */
function role_delegation_entity_operation(EntityInterface $entity) {
  $operations = [];
  if (!$entity instanceof UserInterface) {
    return $operations;
  }

  // Hide the entity operation when the current user has the 'administer users'
  // permission. Roles can be edited on the user edit page.
  if (!\Drupal::currentUser()
    ->hasPermission('administer users')) {
    return;
  }
  $url = Url::fromRoute('role_delegation.edit_form', [
    'user' => $entity
      ->id(),
  ]);

  // Check if the current user has access to the role_delegation edit form.
  if ($url
    ->access()) {
    $operations['role_delegation'] = [
      'title' => t('Roles'),
      'weight' => 210,
      'url' => $url,
    ];
  }
  return $operations;
}

/**
 * Implements hook_views_data_alter().
 */
function role_delegation_views_data_alter(array &$data) {
  $data['users']['user_bulk_form']['field']['id'] = 'role_delegation_user_bulk_form';
}