' . $help . '

'; } } /** * Implements hook_permission(). */ function account_sentinel_permission() { return array( 'access account sentinel logs' => array( 'title' => t('Access Account Sentinel logs'), ), 'administer account sentinel' => array( 'title' => t("Change Account Sentinel's configuration"), ), ); } /** * Implements hook_menu(). */ function account_sentinel_menu() { // Report page. $items['admin/reports/account-sentinel'] = array( 'title' => 'Account Sentinel log', 'description' => 'List of changes to monitored roles\' accounts perceived by Account Sentinel.', 'page callback' => 'account_sentinel_page_report', 'access arguments' => array('access account sentinel logs'), 'file' => 'account_sentinel.pages.inc', 'type' => MENU_NORMAL_ITEM, ); // Settings page. $items['admin/config/system/account-sentinel'] = array( 'title' => 'Account Sentinel settings', 'description' => 'Manage Account Sentinel settings.', 'page callback' => 'drupal_get_form', 'page arguments' => array('account_sentinel_page_settings'), 'access arguments' => array('administer account sentinel'), 'file' => 'account_sentinel.pages.inc', ); // Cron handler. $items['system/account-sentinel-cron'] = array( 'title' => 'Run Account Sentinel DB check', 'page callback' => 'account_sentinel_callback_cron', 'access callback' => TRUE, 'file' => 'account_sentinel.pages.inc', 'type' => MENU_CALLBACK, ); // Cron key reset handler. $items['system/account-sentinel-reset-cron-key'] = array( 'title' => "Reset Account Sentinel's cron key", 'page callback' => 'account_sentinel_callback_reset_cron_key', 'access callback' => TRUE, 'access arguments' => array('administer account sentinel'), 'file' => 'account_sentinel.pages.inc', 'type' => MENU_CALLBACK, ); return $items; } /** * Implements hook_theme(). */ function account_sentinel_theme($existing, $type, $theme, $path) { return array( 'account_sentinel_username' => array( 'variables' => array( 'uid' => 0, ), 'file' => 'account_sentinel.themes.inc', ), ); } /** * Implements hook_user_update(). */ function account_sentinel_user_update(&$edit, $account, $category) { $new = account_sentinel_monitored_account_data($account); $original = account_sentinel_monitored_account_data($account->original); if ($new['monitored'] || $original['monitored']) { $changes = account_sentinel_detect_changes($new, $original); if (!empty($changes)) { account_sentinel_record_events($account->uid, ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK, $changes); account_sentinel_update_snapshot($new); } } } /** * Implements hook_password_strength_change(). */ function account_sentinel_password_strength_change($account, $strength) { // Store password strength for $account for later use. account_sentinel_password_strength($account->uid, password_strength_get_score($strength['score'])); } /** * Provides static storage for password strengths. * * @param int $uid * UID of the user. * @param string $score * Password's strength. * * @return mixed * Returns the score for UID's new password or FALSE if not set. */ function account_sentinel_password_strength($uid, $score = NULL) { static $strengths = array(); // Update score. if (isset($score)) { $strengths[$uid] = $score; } // Return score. if (isset($strengths[$uid])) { return $strengths[$uid]; } // Return FALSE if score is not available. return FALSE; } /** * Implements hook_account_sentinel_changes_alter(). */ function account_sentinel_account_sentinel_changes_alter(array &$changes, array &$new, array &$original) { // Include password strength information if available. $strength = account_sentinel_password_strength($original['uid']); if ($strength) { foreach ($changes as &$change) { if ($change['type'] == ACCOUNT_SENTINEL_EVENT_TYPE_PASS && !isset($change['data']['strength'])) { $change['data']['strength'] = $strength; } } } } /** * Implements hook_user_insert(). */ function account_sentinel_user_insert(&$edit, $account, $category) { $account = account_sentinel_monitored_account_data($account); if ($account['monitored']) { $events[] = array( 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_USER_ADD, 'data' => array( 'uid' => $account['uid'], 'name' => $account['name'], 'mail' => $account['mail'], ), ); foreach ($account['roles'] as $rid) { $events[] = array( 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_ADD, 'data' => array('rid' => $rid), ); } account_sentinel_record_events($account['uid'], ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK, $events); account_sentinel_update_snapshot($account); } } /** * Implements hook_user_delete(). */ function account_sentinel_user_delete($account) { $account = account_sentinel_monitored_account_data($account); if ($account['monitored']) { $events[] = array( 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_USER_DELETE, 'data' => array( 'uid' => $account['uid'], 'name' => $account['name'], 'mail' => $account['mail'], ), ); account_sentinel_record_events($account['uid'], ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK, $events); account_sentinel_delete_snapshot($account['uid']); } } /** * Implements hook_cron(). */ function account_sentinel_cron() { if (variable_get('account_sentinel_cron_method', 'drupal') == 'drupal') { watchdog('account_sentinel', "Invoked database audit from Drupal's cron."); module_load_include('inc', 'account_sentinel', 'account_sentinel.audit'); account_sentinel_audit(); } } /** * Implements hook_mail(). */ function account_sentinel_mail($key, &$message, $params) { switch ($key) { // Compose notification e-mail. case 'notification': // Collect information parameters. $origin = $params['origin']; $events = $params['events']; // Collect users' data. $users = array( 'changed' => array('uid' => $params['uid']), 'by' => array('uid' => $params['meta_data']['by_uid']), ); foreach ($users as &$user) { $uid = $user['uid']; $user_object = user_load($uid); if ($user_object !== FALSE) { $user['name'] = $user_object->name; $user['link'] = l($user_object->name, 'user/' . $uid, array( 'absolute' => TRUE, )); } else { $user['name'] = t('Unknown'); $user['link'] = $user['name']; } } // Compose e-mail. if (!empty($events)) { $message['subject'] = t( '[AS] User #@uid (@name) was changed', array( '@uid' => $users['changed']['uid'], '@name' => $users['changed']['name'], ) ); if ($origin != ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK) { $message['body'][] = '' . t('Warning: these changes were made outside of Drupal!') . ''; } $message['body'][] = t( 'User #@uid (!user) was changed by user #@by_uid (!by_user) (@ip) at @timestamp.', array( '@uid' => $users['changed']['uid'], '!user' => $users['changed']['link'], '@by_uid' => $users['by']['uid'], '!by_user' => $users['by']['link'], '@ip' => $params['meta_data']['ip'], '@timestamp' => format_date($params['meta_data']['timestamp']), ) ); $message['body'][] = t('The following changes were made to the account:'); $event_list = ''; $message['body'][] = $event_list; $message['body'][] = '--
' . t( 'Sent by Account Sentinel on @site_name.', array( '@site_name' => variable_get('site_name'), '!site_url' => url('', array('absolute' => TRUE)), ) ); } break; } } /** * Returns the ID's of monitored roles. * * If the monitored roles have not been set yet, it will return the * administrator role. * * @return int[] * Array of monitored roles' ids. */ function account_sentinel_get_monitored_roles() { $roles = variable_get('account_sentinel_monitored_roles', NULL); if ($roles === NULL) { return array( variable_get('user_admin_role', 3), ); } return $roles; } /** * Returns the module's cron key. * * @return string * The cron key. */ function account_sentinel_get_cron_key() { $key = variable_get('account_sentinel_cron_key', NULL); if ($key === NULL) { return account_sentinel_reset_cron_key(); } return $key; } /** * Returns the relevant monitored data of an $account object. * * The output is an associative array which only stores data needed by Account * Sentinel. * * @param object $account * A user entity. * * @return array * An associative array containing the monitored data. */ function account_sentinel_monitored_account_data($account) { // Only work with role IDs. $roles = array_keys($account->roles); // Only work with monitored roles. $monitored = account_sentinel_get_monitored_roles(); $roles = array_intersect($monitored, $roles); $output = array( 'uid' => $account->uid, 'name' => $account->name, 'pass' => $account->pass, 'mail' => $account->mail, 'status' => $account->status, 'roles' => $roles, 'monitored' => !empty($roles), ); return $output; } /** * Returns an array of event type string - human-readable string associations. * * @return array * Array of translatable strings mapped by their event type constants. */ function account_sentinel_event_type_strings() { return array( ACCOUNT_SENTINEL_EVENT_TYPE_NAME => t('name changed'), ACCOUNT_SENTINEL_EVENT_TYPE_PASS => t('password changed'), ACCOUNT_SENTINEL_EVENT_TYPE_MAIL => t('mail changed'), ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_ADD => t('role added'), ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_REMOVE => t('role removed'), ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_INVALID => t('invalid snapshot'), ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_MISSING => t('missing snapshot'), ACCOUNT_SENTINEL_EVENT_TYPE_USER_ADD => t('user added'), ACCOUNT_SENTINEL_EVENT_TYPE_USER_DELETE => t('user deleted'), ACCOUNT_SENTINEL_EVENT_TYPE_USER_BLOCK => t('blocked'), ACCOUNT_SENTINEL_EVENT_TYPE_USER_UNBLOCK => t('unblocked'), ); } /** * Returns an array of event origin string - human-readable string associations. * * @return array * Array of translatable strings mapped by their event origin constants. */ function account_sentinel_event_origin_strings() { return array( ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK => t('Drupal'), ACCOUNT_SENTINEL_EVENT_ORIGIN_DB_CHECK => t('database'), ); } /** * Gets the human-readable name of a given event type. * * @param string $event_type * The event's type. * * @return string * The event type's human-readable name. */ function account_sentinel_event_type_get_string($event_type) { $event_type_strings = account_sentinel_event_type_strings(); if (isset($event_type_strings[$event_type])) { return $event_type_strings[$event_type]; } return t('unknown'); } /** * Gets the human-readable name of a given event origin. * * @param string $event_origin * The event's origin. * * @return string * The event origin's human-readable name. */ function account_sentinel_event_origin_get_string($event_origin) { $event_origin_strings = account_sentinel_event_origin_strings(); if (isset($event_origin_strings[$event_origin])) { return $event_origin_strings[$event_origin]; } return t('unknown'); } /** * Generates an event's detailed human-readable message. * * @param string $event_type * The event's type. * @param array $data * Additional data from the database used to generate informative messages. * * @return string * The generated detailed event message. */ function account_sentinel_get_event_message($event_type, array $data) { switch ($event_type) { case ACCOUNT_SENTINEL_EVENT_TYPE_NAME: return t('Changed name from @name_old to @name_new.', array( '@name_old' => $data['old'], '@name_new' => $data['new'], )); case ACCOUNT_SENTINEL_EVENT_TYPE_PASS: $msg = t('Changed password.'); // Append strength information if set. if (isset($data['strength'])) { $msg .= ' ' . t('New strength: @strength.', array( '@strength' => $data['strength'], )); } return $msg; case ACCOUNT_SENTINEL_EVENT_TYPE_MAIL: return t('Changed mail from @mail_old to @mail_new.', array( '@mail_old' => $data['old'], '@mail_new' => $data['new'], )); case ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_ADD: $role = user_role_load($data['rid']); return t('Granted role @role.', array( '@role' => $role->name, )); case ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_REMOVE: $role = user_role_load($data['rid']); return t('Revoked role @role.', array( '@role' => $role->name, )); case ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_INVALID: return t("The user's snapshot was altered."); case ACCOUNT_SENTINEL_EVENT_TYPE_SNAPSHOT_MISSING: return t("The user's snapshot is missing."); case ACCOUNT_SENTINEL_EVENT_TYPE_USER_ADD: return t('Created user #@uid @name (@mail).', array( '@uid' => $data['uid'], '@name' => $data['name'], '@mail' => $data['mail'], )); case ACCOUNT_SENTINEL_EVENT_TYPE_USER_DELETE: return t('Deleted user #@uid @name (@mail).', array( '@uid' => $data['uid'], '@name' => $data['name'], '@mail' => $data['mail'], )); case ACCOUNT_SENTINEL_EVENT_TYPE_USER_BLOCK: return t('Blocked user.'); case ACCOUNT_SENTINEL_EVENT_TYPE_USER_UNBLOCK: return t('Unblocked user.'); default: return t('Unknown event.'); } } /** * Resets the cron key. * * @return string * Returns the new cron key. */ function account_sentinel_reset_cron_key() { $new_key = drupal_random_key(); variable_set('account_sentinel_cron_key', $new_key); watchdog('account_sentinel', 'Cron key reset.'); return $new_key; } /** * Compares a user account's two states and returns the list of differences. * * @param array $new * The new state of the user. * @param array $original * The original state of the user. * * @return array * Array of changes. * * @see account_sentinel_monitored_account_data($account) */ function account_sentinel_detect_changes(array $new, array $original) { $changes = array(); // Check whether name was changed. if (isset($new['name']) && $new['name'] != $original['name']) { $changes[] = array( 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_NAME, 'data' => array( 'old' => $original['name'], 'new' => $new['name'], ), ); } // Check whether pass was changed. if (isset($new['pass']) && $new['pass'] != $original['pass']) { $changes[] = array( 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_PASS, 'data' => array(), ); } // Check whether mail was changed. if (isset($new['mail']) && $new['mail'] != $original['mail']) { $changes[] = array( 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_MAIL, 'data' => array( 'old' => $original['mail'], 'new' => $new['mail'], ), ); } // Check whether status was changed. if (isset($new['status']) && $new['status'] != $original['status']) { if ($original['status']) { $type = ACCOUNT_SENTINEL_EVENT_TYPE_USER_BLOCK; } else { $type = ACCOUNT_SENTINEL_EVENT_TYPE_USER_UNBLOCK; } $changes[] = array( 'type' => $type, 'data' => array(), ); } if (isset($new['roles'])) { // Check whether roles were changed. $roles_added = array_diff($new['roles'], $original['roles']); $roles_removed = array_diff($original['roles'], $new['roles']); foreach ($roles_added as $rid) { $changes[] = array( 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_ADD, 'data' => array('rid' => $rid), ); } foreach ($roles_removed as $rid) { $changes[] = array( 'type' => ACCOUNT_SENTINEL_EVENT_TYPE_ROLE_REMOVE, 'data' => array('rid' => $rid), ); } } drupal_alter('account_sentinel_changes', $changes, $new, $original); return $changes; } /** * Records events. * * Stores account changes in the database, sets the new snapshot, sends an email * notification and invokes hook_account_sentinel_change(). * * @param int $uid * The UID of the account. * @param string $origin * The origin of the event. * @param array $events * The array of changes. */ function account_sentinel_record_events($uid, $origin, array $events) { global $user; $by_uid = ($origin == ACCOUNT_SENTINEL_EVENT_ORIGIN_HOOK) ? $user->uid : 0; $meta_data = array( 'uid' => $uid, 'origin' => $origin, 'by_uid' => $by_uid, 'ip' => ip_address(), 'timestamp' => REQUEST_TIME, ); // Store changes. foreach ($events as $event_key => $event) { $record = $meta_data; $record['type'] = $event['type']; $record['data'] = $event['data']; // Log to database. drupal_write_record('account_sentinel_logs', $record); // Inform other modules. module_invoke_all('account_sentinel_change', $record); } // Send one e-mail notification per account. account_sentinel_send_notification($uid, $origin, $events, $meta_data); } /** * Sends e-mail notification about events. * * @param int $uid * The UID of the account. * @param string $origin * The origin of the event. * @param array $events * The array of changes. * @param array $meta_data * Additional meta data about the change events. */ function account_sentinel_send_notification($uid, $origin, array $events, array $meta_data) { if (!empty($events)) { $to = variable_get('account_sentinel_email_to', ''); if ($to != '') { drupal_mail( 'account_sentinel', 'notification', $to, language_default(), array( 'uid' => $uid, 'origin' => $origin, 'events' => $events, 'meta_data' => $meta_data, ) ); } } } /** * Rebuilds the snapshot tables. * * Invoked after changing which roles are monitored. * * @param array $roles_old * Array of previously monitored role IDs. * @param array $roles_new * Array of monitored role IDs. */ function account_sentinel_rebuild_snapshots(array $roles_old, array $roles_new) { $roles_added = array_diff($roles_new, $roles_old); $roles_removed = array_diff($roles_old, $roles_new); $auth_in_old = array_search(DRUPAL_AUTHENTICATED_RID, $roles_old) !== FALSE; $auth_in_new = array_search(DRUPAL_AUTHENTICATED_RID, $roles_new) !== FALSE; $hash_key = drupal_get_hash_salt(); // Check if we don't have to modify anything. if ($auth_in_old && $auth_in_new) { // Every user is in the database, and will remain there. return; } // Check if we have to create new user snapshots. if (!$auth_in_old && !empty($roles_added)) { // Select users' data. $select = db_select('users', 'u') ->fields('u', array('uid', 'name', 'pass', 'mail', 'status')) ->condition('u.uid', '0', '<>') ->groupBy('u.uid'); // Filter by users of added roles. if (!$auth_in_new) { $select_include = db_select('users_roles', 'ur') ->fields('ur', array('uid')) ->condition('rid', $roles_added); $select->condition('uid', $select_include, 'IN'); } // Exclude users of previous roles. if (!empty($roles_old)) { $select_exclude = db_select('users_roles', 'ur') ->fields('ur', array('uid')) ->condition('rid', $roles_old); $select->condition('uid', $select_exclude, 'NOT IN'); } // Add checksum. $select->addExpression( 'sha2(concat(u.uid, u.name, u.pass, u.mail, u.status, :hash_key), 384)', 'checksum', array(':hash_key' => $hash_key) ); // Insert. $insert = db_insert('account_sentinel_users') ->fields(array('uid', 'name', 'pass', 'mail', 'status', 'checksum')) ->from($select); $insert->execute(); } // Check if we have to remove users snapshots. if (!$auth_in_new && !empty($roles_removed)) { // Delete records. $delete = db_delete('account_sentinel_users'); // Exclude users of monitored roles. if (!empty($roles_new)) { $delete_exclude = db_select('users_roles', 'ur') ->fields('ur', array('uid')) ->condition('rid', $roles_new); $delete->condition('uid', $delete_exclude, 'NOT IN'); } // Execute query. $delete->execute(); } // Check if we have to create new users_roles snapshots. $roles_added = array_diff($roles_added, array(DRUPAL_AUTHENTICATED_RID)); if (!empty($roles_added)) { // Select added roles' records. $select = db_select('users_roles', 'ur'); $select->fields('ur', array('uid', 'rid')) ->condition('rid', $roles_added); // Add checksum. $select->addExpression( 'sha2(concat(ur.uid, ur.rid, :hash_key), 384)', 'checksum', array(':hash_key' => $hash_key) ); // Insert. $insert = db_insert('account_sentinel_users_roles') ->fields(array('uid', 'rid', 'checksum')) ->from($select); $insert->execute(); } // Check if we have to remove users_roles snapshots. $roles_removed = array_diff($roles_removed, array(DRUPAL_AUTHENTICATED_RID)); if (!empty($roles_removed)) { // Delete removed roles' records. $delete = db_delete('account_sentinel_users_roles'); $delete->condition('rid', $roles_removed); $delete->execute(); } } /** * Updates a modified user's snapshot. * * @param array $account * Account details. * * @see account_sentinel_monitored_account_data($account) */ function account_sentinel_update_snapshot(array $account) { $uid = $account['uid']; account_sentinel_delete_snapshot($uid); account_sentinel_create_snapshot($account); } /** * Deletes a user's snapshots from the database. * * @param int $uid * UID of user. */ function account_sentinel_delete_snapshot($uid) { // Reset user's snapshot. db_delete('account_sentinel_users') ->condition('uid', $uid) ->execute(); // Reset users_roles snapshots. db_delete('account_sentinel_users_roles') ->condition('uid', $uid) ->execute(); } /** * Creates a snapshot of the given account's state in the database. * * @param array $account * The account. * * @see account_sentinel_monitored_account_data($account) */ function account_sentinel_create_snapshot(array $account) { $uid = $account['uid']; $hash_key = drupal_get_hash_salt(); // Update account_sentinel_users. if ($account['monitored']) { $checksum = hash('sha384', $uid . $account['name'] . $account['pass'] . $account['mail'] . $account['status'] . $hash_key); db_insert('account_sentinel_users') ->fields(array( 'uid' => $uid, 'name' => $account['name'], 'pass' => $account['pass'], 'mail' => $account['mail'], 'status' => $account['status'], 'checksum' => $checksum, )) ->execute(); } // Fill users_roles snapshots. foreach ($account['roles'] as $rid) { $checksum = hash('sha384', $uid . $rid . $hash_key); db_insert('account_sentinel_users_roles') ->fields(array( 'uid' => $uid, 'rid' => $rid, 'checksum' => $checksum, )) ->execute(); } }