You are here

userone.module in User One 7

Same filename and directory in other branches
  1. 8 userone.module
  2. 6 userone.module

User One module.

Provide limited access to user one account even from administrators

File

userone.module
View source
<?php

/**
 * @file
 * User One module.
 *
 * Provide limited access to user one account even from administrators
 */

/**
 * Implements hook_help().
 */
function userone_help($path, $arg) {
  switch ($path) {
    case 'admin/help#userone':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The User One module provides limited access to user one account and augment Drupal core to discourage brute-force login attacks. It helps protect user one account from users with permissions to administer users.') . '</p>';
      $output .= '<h3>' . t('Features') . '</h3>';
      $output .= '<ul>';
      $output .= '<li>' . t("User one account is protected from viewing and editing. Users -- even with 'Administer users' permission -- will be denied access.") . '</li>';
      $output .= '<li>' . t('User one account is hidden from user listing page, /admin/people.') . '</li>';
      $output .= '<li>' . t("User one account is hidden from user lists such as in blocks, Who's New and Who's Online. User One provides its own version of Who's Online block for correct count of logged in users besides hiding user one.") . '</li>';
      $output .= '<li>' . t("User One exposes Drupal's built-in values to change otherwise inaccessible such as number of allowed login attempts and time window to remember such login attempts.") . '</li>';
      $output .= '<li>' . t('While Drupal temporarily deny login after multiple failed logins, User One goes one step further to allow permanently block such IPs automatically and notify the site admin.') . '</li>';
      $output .= '</ul>';
      return $output;
  }
}

/**
 * Implements hook_menu_alter().
 * To prevent users from editing user 1 account.
 */
function userone_menu_alter(&$items) {
  $items['user/%user/edit']['access callback'] = 'userone_edit_access';
}

/**
 * Augment core's user_edit_access().
 * Block edit access to user 1 account.
 */
function userone_edit_access($account) {
  if ($account->uid == 1 and $GLOBALS['user']->uid != 1) {
    return FALSE;
  }
  return user_edit_access($account);
}

/**
 * Implements hook_user_view().
 * Block viewing user 1 profile and redirect to one's own.
 */
function userone_user_view($account, $view_mode, $langcode) {
  if ($account->uid == 1 and $GLOBALS['user']->uid != 1) {
    drupal_goto('user');
  }
}

/**
 * Implements hook_form_alter().
 * Hide user 1 in the user listing at admin/people
 */
function userone_form_user_admin_account_alter(&$form, $form_state, $form_id) {
  unset($form['accounts']['#options'][1]);
}

/**
 * Implements hook_menu().
 */
function userone_menu() {
  global $user;
  $items = array();
  $items['admin/config/people/userone'] = array(
    'title' => 'User One',
    'description' => 'Configure User One settings to better protect site admin account.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'userone_admin_settings',
    ),
    'access callback' => 'userone_admin_access',
    'type' => MENU_NORMAL_ITEM,
  );
  return $items;
}
function userone_admin_access() {
  return $GLOBALS['user']->uid == 1 ? TRUE : FALSE;
}
function userone_admin_settings() {

  // Since select values are string, resave them as integer.
  foreach (array(
    'ip_limit' => 50,
    'ip_window' => 3600,
    'user_limit' => 5,
    'user_window' => 21600,
  ) as $param => $default) {
    if (is_string($val = variable_get("user_failed_login_{$param}", $default))) {
      variable_set("user_failed_login_{$param}", (int) $val);
    }
  }
  $form['userone_edit_access_info'] = array(
    '#type' => 'item',
    '#title' => t('Access to user one edit blocked'),
    '#markup' => t('No account except user one account can edit user one account.'),
  );
  $form['userone_view_access_info'] = array(
    '#type' => 'item',
    '#title' => t('Access to user one profile blocked'),
    '#markup' => t('No account except user one account can view user one account.'),
  );
  $form['failed_login'] = array(
    '#type' => 'fieldset',
    '#title' => 'Allowed failed login attempts',
    '#description' => "This setting exposes Drupal's built-in configuration values otherwise inaccessible. It applies to all users, not just user one.",
  );
  $form['failed_login']['user_failed_login_ip_limit'] = array(
    '#type' => 'select',
    '#title' => t('Allowed failed login attempts for an IP address (default 50)'),
    '#options' => array(
      1 => 1,
      2 => 2,
      3 => 3,
      4 => 4,
      5 => 5,
      10 => 10,
      25 => 25,
      50 => 50,
      75 => 75,
      100 => 100,
      250 => 250,
    ),
    '#default_value' => variable_get('user_failed_login_ip_limit', 50),
    '#description' => t("Do not allow any login from the current user's IP if the limit has been reached. Default is 50 failed attempts allowed in one hour. This is independent of the per-user limit to catch attempts from one IP to log in to many different user accounts.  We have a reasonably high limit since there may be only one apparent IP for all users at an institution."),
  );
  $form['failed_login']['user_failed_login_ip_window'] = array(
    '#type' => 'select',
    '#title' => t('Failed login window for an IP address (default 1 hour)'),
    '#options' => array(
      300 => '5 minutes',
      600 => '10 minutes',
      900 => '15 minutes',
      1800 => '30 minutes',
      2700 => '45 minutes',
      3600 => '1 hour',
      7200 => '2 hours',
      10800 => '3 hours',
      14400 => '4 hours',
      18000 => '5 hours',
      21600 => '6 hours',
      28800 => '8 hours',
      36000 => '10 hours',
      43200 => '12 hours',
      86400 => '24 hours',
    ),
    '#default_value' => variable_get('user_failed_login_ip_window', 3600),
    '#description' => t('Time period during which failed logins are accounted for.'),
  );
  $form['failed_login']['userone_block_ip_on_failed_login_ip'] = array(
    '#type' => 'checkbox',
    '#title' => t('Permanently block IP when failed logins breaks threshold') . ' (' . l(t('See blocked IPs'), 'admin/config/people/ip-blocking') . ')',
    '#default_value' => variable_get('userone_block_ip_on_failed_login_ip', FALSE),
    '#description' => 'User one account will be notified when an IP is blocked.',
  );
  $form['failed_login']['divider'] = array(
    '#type' => 'item',
    '#markup' => '<hr style="width: 50%" />',
  );
  $form['failed_login']['user_failed_login_user_limit'] = array(
    '#type' => 'select',
    '#title' => t('Allowed failed login attempts for an account (default 5)'),
    '#options' => array(
      1 => 1,
      2 => 2,
      3 => 3,
      4 => 4,
      5 => 5,
      10 => 10,
      25 => 25,
      50 => 50,
      75 => 75,
      100 => 100,
      250 => 250,
    ),
    '#default_value' => variable_get('user_failed_login_user_limit', 5),
    '#description' => t('User will be allowed to attempt logins this many times within the period (see below).'),
  );
  $form['failed_login']['user_failed_login_user_window'] = array(
    '#type' => 'select',
    '#title' => t('Failed login window for an account (default 6 hours)'),
    '#options' => array(
      300 => '5 minutes',
      600 => '10 minutes',
      900 => '15 minutes',
      1800 => '30 minutes',
      2700 => '45 minutes',
      3600 => '1 hour',
      7200 => '2 hours',
      10800 => '3 hours',
      14400 => '4 hours',
      18000 => '5 hours',
      21600 => '6 hours',
      28800 => '8 hours',
      36000 => '10 hours',
      43200 => '12 hours',
      86400 => '24 hours',
    ),
    '#default_value' => variable_get('user_failed_login_user_window', 21600),
    '#description' => t('Number of failed logins for an account will be accounted for this period'),
  );
  $form['failed_login']['userone_block_ip_on_failed_login_user1'] = array(
    '#type' => 'checkbox',
    '#title' => t('Permanently block IP when failed logins <strong>for user one</strong> breaks threshold') . ' (' . l(t('See blocked IPs'), 'admin/config/people/ip-blocking') . ')',
    '#default_value' => variable_get('userone_block_ip_on_failed_login_user1', FALSE),
    '#description' => 'User one account will be notified when an IP is blocked.',
  );
  return system_settings_form($form);
}

/**
 * Implements hook_block_info().
 */
function userone_block_info() {
  $blocks['online']['info'] = t("Who's online (userone)");
  $blocks['online']['cache'] = DRUPAL_NO_CACHE;
  $blocks['online']['properties']['administrative'] = TRUE;
  return $blocks;
}

/**
 * Implements hook_block_view().
 * Code is identical from user_block_view() except it excludes user 1 in the count.
 */
function userone_block_view($delta = '') {
  if ($delta == 'online') {
    if (user_access('access content')) {

      // Count users active within the defined period.
      $interval = REQUEST_TIME - variable_get('user_block_seconds_online', 900);

      // Perform database queries to gather online user lists. We use s.timestamp
      // rather than u.access because it is much faster.
      $authenticated_count = db_query("SELECT COUNT(DISTINCT s.uid) FROM {sessions} s WHERE s.timestamp >= :timestamp AND s.uid > 1", array(
        ':timestamp' => $interval,
      ))
        ->fetchField();
      $output = '<p>' . format_plural($authenticated_count, 'There is currently 1 user online.', 'There are currently @count users online.') . '</p>';

      // Display a list of currently online users.
      $max_users = variable_get('user_block_max_list_count', 10);
      if ($authenticated_count && $max_users) {
        $items = db_query_range('SELECT u.uid, u.name, MAX(s.timestamp) AS max_timestamp FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.timestamp >= :interval AND s.uid > 1 GROUP BY u.uid, u.name ORDER BY max_timestamp DESC', 0, $max_users, array(
          ':interval' => $interval,
        ))
          ->fetchAll();
        $output .= theme('user_list', array(
          'users' => $items,
        ));
      }
      $block['subject'] = t('Who\'s online');
      $block['content'] = $output;
    }
    return $block;
  }
}

/**
 * Implements hook_preprocess_HOOK().
 * Removes user 1 from the user list; affects blocks, Who's New and Who's Online.
 */
function userone_preprocess_user_list(&$vars) {
  foreach ($vars['users'] as $key => $usr) {
    if ($usr->uid == 1) {
      unset($vars['users'][$key]);
      return;
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 * Append to the list of core validators to block IPs.
 * Duplicate code to avoid using hook_form_alter() for better performance.
 */
function userone_form_user_login_alter(&$form, &$form_state) {
  $form['#validate'][] = 'userone_block_ips';
}
function userone_form_user_login_block_alter(&$form, &$form_state) {
  $form['#validate'][] = 'userone_block_ips';
}
function userone_block_ips($form, &$form_state) {
  if (!empty($form_state['uid'])) {
    return;
  }
  if (variable_get('userone_block_ip_on_failed_login_ip', FALSE)) {

    // Find IPs that broke threshold and block them permanently.
    $result = db_query("SELECT identifier AS ip FROM {flood} WHERE event = :event AND timestamp > :timestamp GROUP BY identifier HAVING COUNT(*) > :threshold", array(
      ':event' => 'failed_login_attempt_ip',
      ':timestamp' => REQUEST_TIME - variable_get('user_failed_login_ip_window', 3600),
      ':threshold' => variable_get('user_failed_login_ip_limit', 50),
    ));
    foreach ($result as $row) {
      if (!db_query("SELECT * FROM {blocked_ips} WHERE ip = :ip", array(
        ':ip' => $row->ip,
      ))
        ->fetchField()) {
        db_insert('blocked_ips')
          ->fields(array(
          'ip' => $row->ip,
        ))
          ->execute();
        $blocked_ip[] = $row->ip;
      }
    }
  }
  if (variable_get('userone_block_ip_on_failed_login_user1', FALSE)) {

    // Find IPs that broke threshold with user 1 and block them permanently.
    $result = db_query("SELECT identifier AS uid_ip FROM {flood} WHERE event = :event AND timestamp > :timestamp AND identifier LIKE '1-%' GROUP BY identifier HAVING COUNT(*) > :threshold", array(
      ':event' => 'failed_login_attempt_user',
      ':timestamp' => REQUEST_TIME - variable_get('user_failed_login_user_window', 21600),
      ':threshold' => variable_get('user_failed_login_user_limit', 5),
    ));
    foreach ($result as $row) {
      list($uid, $ip) = explode('-', $row->uid_ip);
      if (!db_query("SELECT * FROM {blocked_ips} WHERE ip = :ip", array(
        ':ip' => $ip,
      ))
        ->fetchField()) {
        db_insert('blocked_ips')
          ->fields(array(
          'ip' => $ip,
        ))
          ->execute();
        $blocked_ip[] = $ip . ' (failed logins for user id ' . $uid . ')';
      }
    }
  }

  // Notify user one.
  if (!empty($blocked_ip)) {
    $user1 = user_load(1);
    $params['subject'] = variable_get('site_name') . ': Blocked IP due to multiple failed logins';
    $params['body'][] = 'Hi User One,';
    $params['body'][] = 'There were suspected login activities and associated IP has been blocked.';
    $params['body'][] = 'Blocked IP: ' . implode(', ', $blocked_ip);
    $params['body'][] = 'You can review the list of blocked IPs at ' . url('admin/config/people/ip-blocking', array(
      'absolute' => TRUE,
    ));
    $params['body'][] = 'Thank you.';
    $params['body'][] = 'Sent by User One module.';
    drupal_mail('userone', 'blocked-ip', $user1->mail, language_default(), $params);

    //drupal_mail('userone', 'blocked-ip', $usr->mail, language_default(), $params, $from);
  }
}

/*
 * Implements hook_mail().
 */
function userone_mail($key, &$message, $params) {
  $message['subject'] = $params['subject'];
  $message['body'] = $params['body'];
}

Functions

Namesort descending Description
userone_admin_access
userone_admin_settings
userone_block_info Implements hook_block_info().
userone_block_ips
userone_block_view Implements hook_block_view(). Code is identical from user_block_view() except it excludes user 1 in the count.
userone_edit_access Augment core's user_edit_access(). Block edit access to user 1 account.
userone_form_user_admin_account_alter Implements hook_form_alter(). Hide user 1 in the user listing at admin/people
userone_form_user_login_alter Implements hook_form_FORM_ID_alter(). Append to the list of core validators to block IPs. Duplicate code to avoid using hook_form_alter() for better performance.
userone_form_user_login_block_alter
userone_help Implements hook_help().
userone_mail
userone_menu Implements hook_menu().
userone_menu_alter Implements hook_menu_alter(). To prevent users from editing user 1 account.
userone_preprocess_user_list Implements hook_preprocess_HOOK(). Removes user 1 from the user list; affects blocks, Who's New and Who's Online.
userone_user_view Implements hook_user_view(). Block viewing user 1 profile and redirect to one's own.