Commit 809aafac authored by kylebrowning's avatar kylebrowning

updates user resource to log accounts created from services

parent 95d4b067
......@@ -324,6 +324,7 @@ function _user_resource_create($account) {
if ($uri = services_resource_uri(array('user', $user['uid']))) {
$user['uri'] = $uri;
}
_user_resource_update_services_user($user['uid'], time());
return $user;
}
}
......@@ -388,6 +389,8 @@ function _user_resource_update($uid, $account) {
$form_state['values']['pass']['pass2'] = $account['pass'];
}
debug($account);
debug($form_state['values']);
// If user is changing name, make sure they have permission.
if (isset($account['name']) && $account['name'] != $account_loaded->name && !(user_access('change own username') || user_access('administer users'))) {
return services_error(t('You are not allowed to change your username.'), 406);
......@@ -407,10 +410,40 @@ function _user_resource_update($uid, $account) {
$account = (object) $account;
services_remove_user_data($account);
$account = (array) $account;
_user_resource_update_services_user($uid, time());
return $account;
}
}
function _user_resource_update_services_user($uid, $time) {
//Determine if a row exists, if not that means we are creating the user,
//If so that means we are updating it.
$result = db_select('services_user', 'su')
->fields('su')
->condition('uid', $uid,'=')
->execute()
->fetchAssoc();
//check the result
if (!$result) {
$id = db_insert('services_user')
->fields(array(
'uid' => $uid,
'created' => $time,
'changed' => $time,
))
->execute();
} else {
db_update('services_user')
->fields(array(
'uid' => $uid,
'changed' => $time,
)
)
->execute();
}
}
/**
* Delete a user.
*
......
<?php
function services_security_admin_form($form, &$form_state) {
// Check to see if anything has been stored.
if ($form_state['rebuild']) {
$form_state['input'] = array();
}
if (empty($form_state['storage'])) {
// No step has been set so start with the first.
$form_state['storage'] = array(
'step' => 'services_security_form_decision',
);
}
// Return the current form
$function = $form_state['storage']['step'];
$form = $function($form, $form_state);
return $form;
}
function services_security_form_decision($form, &$form_state) {
$values = '';
if(!empty($form_state['storage'])) {
$values = $form_state['storage'];
}
$notice = '<div style="color:red;"><strong>A Services security update mitigation step has already been run on this site.</strong></div>';
$services_security_update = variable_get('services_security_update_1', FALSE);
//If services security has not run before, lets set the notice to nothing.
if(!$services_security_update) {
$notice = '';
}
$form['markup'] = array(
'#markup' => $notice . 'Due to a bug in services, user accounts registered through services\' user_resource have been created with the password "1" since August 2013.
<p>Services provides the following options to mitigate this vulnerability on your site:
<ol>
<li>Invalidate the password of all user accounts that have been registered after this bug was introduced. This will force all users who registered after August 30th, 2013 to reset their password, regardless of how those accounts were created. <strong>This is the safest option</strong>.</li>
<li>Invalidate the password of all user accounts which currently have their password set to "1". This will require users who attempted to register to reset their password.
This option will take a long time to run especially if you have a lot of users on your site.
<strong style="color:red;">This option may not be effective from a security perspective because an attacker may have already changed passwords to something other than "1".</strong></li>
<li>Do nothing.</li>
</ol>
</p>
<p>There are many reasons why the third option (do nothing) would be suitable to you:
<ol>
<li>Services User Resource was never enabled</li>
<li>Anonymous users did not have permission to register</li>
<li>A custom/contrib resource was enabled that users used in order to register</li>
<li>You have an SSO provider and users do not register through Services</li>
<li>Users were never registered through Services because the API was not public</li>
<li>You were using a version of Services older than 7.x-3.6 and never used Services 7.x-3.6 on your site.</li>
</ol>
</p>
<p><strong>Things you should do as general best practices:</strong>
<ol>
<li>Check all accounts that have administrator access and verify they are accounts you know. If not, its recommended to disable those accounts</li>
<li>If you choose option 1 or 2 you should let your users know that they will need to request a password reset via the regular form at user/password.</li>
</ol></p>',
);
$form['fieldset'] = array(
'#type' => 'fieldset',
'#title' => t('I understand. Let\'s do something about it!'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['fieldset']['security_options'] = array(
'#type' => 'radios',
'#title' => t('Please select from the following options'),
'#options' => array(
t('Invalidate password of all user accounts created after August 30th, 2013 (safest)'),
t('Invalidate password of all user accounts with a password of "1".'),
t('Do nothing'),
),
'#default_value' => isset($values['security_options']) ? $values['security_options'] : 2,
'#required' => TRUE,
);
$form['fieldset']['submit'] = array(
'#type' => 'submit',
'#value' => t('Submit'),
);
if(isset($form_state['decided_security_option'])) {
unset($form['fieldset']);
}
return $form;
}
function services_security_admin_form_submit($form, &$form_state) {
$values = $form_state['values'];
if (isset($values['back']) && $values['op'] == $values['back']) {
// Moving back in form.
$step = $form_state['storage']['step'];
// Call current step submit handler if it exists to unset step form data.
if (function_exists($step . '_submit')) {
$function = $step . '_submit';
$function($form, $form_state);
}
// Remove the last saved step so we use it next.
$last_step = array_pop($form_state['storage']['steps']);
$form_state['storage']['step'] = $last_step;
}
else {
// Record step.
$step = $form_state['storage']['step'];
$form_state['storage']['steps'][] = $step;
// Call step submit handler if it exists.
if (function_exists($step . '_submit')) {
$function = $step . '_submit';
$function($form, $form_state);
}
}
return;
}
function services_security_form_confirm($form, &$form_state) {
$values = array();
if (!empty($form_state['storage'])) {
$values = $form_state['storage'];
}
switch($values['security_options']) {
case 0:
$markup = 'All user account created since August 30th, 2013 will have their password invalidated, this cannot be undone.';
break;
case 1:
$markup = 'All user account which still have their password set to "1" will have their password invalidated, this cannot be undone.';
break;
case 2:
$markup = 'Do nothing.';
break;
}
$form['markup'] = array(
'#markup' => $markup . '<br>',
);
$form['back'] = array(
'#type' => 'submit',
'#value' => t('Back'),
'#limit_validation_errors' => array(),
'#submit' => array('services_security_form_confirm_submit'),
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Confirm'),
);
return $form;
}
function services_security_form_confirm_submit($form, &$form_state) {
$values = $form_state['values'];
$form_state['rebuild'] = TRUE;
//If they hit back, lets send them back.
if (isset($values['back']) && $values['op'] == $values['back']) {
$form_state['storage']['step'] = 'services_security_form_decision';
} else {
//Setup the batch processing.
$security_options = services_security_get_update_options();
$op = $security_options[$form_state['storage']['security_options']];
services_security_setup_batch($op, FALSE);
}
}
//
function services_security_form_decision_submit($form, &$form_state) {
$values = $form_state['values'];
$form_state['rebuild'] = TRUE;
$form_state['storage']['security_options'] = check_plain($form_state['values']['security_options']);
$form_state['storage']['step'] = 'services_security_form_confirm';
}
function theme_services_resource_table($variables) {
$table = $variables['table'];
......@@ -123,4 +291,4 @@ function theme_services_resource_table($variables) {
else {
return theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'resource-form-table')));
}
}
\ No newline at end of file
}
<?php
function services_drush_command() {
$items = array();
$items['services-security-update-1'] = array(
'description' => dt('Run the updates for services 7.x-3.9 security update.'),
'arguments' => array(
'op' => 'op',
),
'examples' => array(
'drush services-security-update-1 reset-users-since-date' => 'Reset password of users that have registered since August 30th, 2013.',
'drush services-security-update-1 reset-users-with-password_one' => 'Reset password of users which are set to "1".',
'drush services-security-update-1 do-nothing' => "Don't reset any user password.",
),
);
return $items;
}
function services_drush_help($section) {
switch ($section) {
case 'drush:services_security_update_1':
return dt("services_security_update_1 op");
}
}
function drush_services_security_update_1($op) {
// Convert $op to match functions.
$allowed_ops = array(
'reset-users-since-date' => 'services_security_update_reset_users_since_date',
'reset-users-with-password_one' => 'services_security_update_reset_users_with_password_one',
'do-nothing' => 'services_security_update_do_nothing',
);
$op = $allowed_ops[$op];
if (!$op) {
drush_print(dt('This operation does not exist or is not allowed, see drush help services-security-update-1.'));
}
else {
drush_print(dt('Running operation: ') . $op);
$drush = TRUE;
services_security_setup_batch($op, $drush);
}
}
......@@ -84,7 +84,29 @@ function services_schema() {
),
),
);
$schema['services_user'] = array(
'description' => 'Stores users created/updated by services.',
'fields' => array(
'uid' => array(
'type' => 'int',
'description' => 'User id that has been created by Drupal',
'unsigned' => TRUE,
'not null' => TRUE,
),
'created' => array(
'description' => 'The Unix timestamp when the node was most recently created by services.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'changed' => array(
'description' => 'The Unix timestamp when the user was most recently updated by services.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
);
return $schema;
}
......@@ -123,11 +145,31 @@ function services_requirements($phase) {
'title' => 'Services Authentication Mechanism',
);
}
$services_security_update = variable_get('services_security_update_1', FALSE);
if(!$services_security_update) {
$url = url('admin/config/services/services-security');
$requirements['services'] = array(
'description' => $t('Services has issued a security update with the most recent module release. Administrative steps are required to secure your Drupal installation <a href="!url">here</a>.', array('!url' => $url)),
'severity' => REQUIREMENT_ERROR,
'value' => 'Steps needed',
'title' => 'Services Authentication Mechanism',
);
}
}
return $requirements;
}
/**
* Implements hook_install().
*/
function services_install() {
$ret = array();
//Set our security update to true since it wont need to be displayed
//on the status report page
variable_set('services_security_update_1', TRUE);
}
/**
* Implements hook_uninstall().
*/
......@@ -235,27 +277,32 @@ function services_update_7400() {
services_endpoint_save($endpoint);
}
}
/**
* Update 7401 Changes passwords of all users that were created/registered since (30 July, 2013)
* Read more here drupal.org node 2284449.
* Skip the user password change by setting the variable services_skip_security_update_7401 to TRUE.
* NOTE: This Overwrites password hashes using an invalid prefix which can never match a password hash during login.
* All affected users will have to request new one time login links to reset their passwords.
* Update 7402 adds services_user table so that services can see users created/update by itself.
*/
function services_update_7401() {
// http://drupal.org/node/2284449.
// Update users created only after the bug has been introduced (30 July, 2013).
$skip_update = variable_get('services_skip_security_update_7401', FALSE);
if ($skip_update === FALSE) {
$affected_accounts = db_update('users')
->fields(array(
'pass' => "ZZZservices_security",
))
->condition('created', 1374278400, '>=')
->execute();
return t('@accounts user passwords have been reset.', array('@accounts' => $affected_accounts));
} else {
return t('Skipped security update 7401 due to variable services_skip_security_update_7401 being set to TRUE');
}
function services_update_7402() {
$schema['services_user'] = array(
'description' => 'Stores users created/updated by services.',
'fields' => array(
'uid' => array(
'type' => 'int',
'description' => 'User id that has been created by Drupal',
'unsigned' => TRUE,
'not null' => TRUE,
),
'created' => array(
'description' => 'The Unix timestamp when the node was most recently created by services.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'changed' => array(
'description' => 'The Unix timestamp when the user was most recently updated by services.',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
),
);
db_create_table('services_user', $schema['services_user']);
}
......@@ -10,6 +10,169 @@
*/
define('SERVICES_REQUIRED_CTOOLS_API', '1.7');
/*
* Function to return list of batch options
*/
function services_security_get_update_options() {
return array(
'services_security_update_reset_users_since_date',
'services_security_update_reset_users_with_password_one',
'services_security_update_do_nothing',
);
}
/*
* Function to setup batch processing.
*/
function services_security_setup_batch($op, $drush = FALSE) {
switch ($op) {
case 'services_security_update_reset_users_since_date':
services_security_update_reset_users_since_date();
services_security_user_update_finish();
break;
case 'services_security_update_reset_users_with_password_one':
batch_set(services_security_update_reset_users_with_password_one());
if ($drush) {
$batch =& batch_get();
//Because we are doing this on the back-end, we set progressive to false.
$batch['progressive'] = FALSE;
//Start processing the batch operations.
drush_backend_batch_process();
}
break;
case 'services_security_update_do_nothing':
services_security_user_update_finish();
break;
}
}
function services_security_update_reset_users_since_date() {
// Update all users created since August 30th, 2013
$result = db_update('users')
->fields(array(
'pass' => "ZZZservices_security",
))
->condition('created', 1377892483, '>=')
->execute();
drupal_set_message($result . ' users were updated');
}
function services_security_user_update_finished($success, $results, $operations) {
if ($success) {
drupal_set_message(t('@count users passwords were reset.', array('@count' => count($results))));
services_security_user_update_finish();
}
else {
$error_operation = reset($operations);
drupal_set_message(
t('An error occurred while processing @operation with arguments : @args',
array(
'@operation' => $error_operation[0],
'@args' => print_r($error_operation[0], TRUE),
)
)
);
}
}
/**
* Executes final tasks at the end of the security update follow up.
*/
function services_security_user_update_finish() {
drupal_set_message('Services security update follow up is complete.');
variable_set('services_security_update_1', TRUE);
if (!drupal_is_cli()) {
drupal_goto('admin/reports/status');
}
}
function services_security_update_reset_users_with_password_one() {
$users = array();
// Query the database to get all user ID
$query = db_select('users', 'u')
->fields('u', array('uid'));
$users = $query->execute()->fetchAll();
$num_of_users = count($users);
$progress = 0; // where to start
$limit = variable_get('services_security_reset_limit_per_batch', 10); // how many to process for each run
$max = $num_of_users; // how many records to process until stop
//Set up our batch operations
while ($progress < $max) {
$operations[] = array('services_security_update_reset_users_with_password_one_op', array($progress, $limit, $max));
$progress = $progress + $limit;
}
// build the batch instructions
$batch = array(
'operations' => $operations,
'finished' => 'services_security_user_update_finished',
'file' => drupal_get_path('module', 'services') . '/services.admin.inc',
'progress_message' => t('Processed batch #@current out of @total.'),
);
return $batch;
}
function services_security_update_reset_users_with_password_one_op($progress, $limit, $max, &$context) {
//Set default starting values
if (empty($context['sandbox'])) {
$context['sandbox'] = array();
$context['sandbox']['progress'] = 0;
$context['sandbox']['current_user'] = 0;
$context['sandbox']['max'] = $limit;
}
//required for user_check_password
require_once('./includes/password.inc');
//Set the password we are looking for.
$password = "1";
//Fetch all users in our current range.
$result = db_select('users', 'u')
->fields('u', array('uid', 'pass',))
->orderBy('u.uid', 'ASC')
->range($progress, $limit)
->execute()
->fetchAll();
// Loop through our ranged results and check their password.
foreach ($result as $row) {
$uid = $row->uid;
$pass = $row->pass;
//Setup account object, much faster than user_load
$account = new stdClass();
$account->uid = $uid;
$account->pass = $pass;
// Check the current user's password against the password.
if (user_check_password($password, $account)) {
// This means we have a matched password.
// Process the user
$updated_user = db_update('users')
->fields(array(
'pass' => "ZZZservices_security",
))
->condition('uid', $uid)
->execute();
$context['results'][] = 'Updating user uid: '. $uid;
}
// Update our progress information.
$context['sandbox']['progress']++;
$context['sandbox']['current_user'] = $uid;
// update progress for message
$shown_progress = $progress + $limit;
// update message during each run so you know where you are in the process
$context['message'] = 'Checking user uid: '. $uid;
}
// Inform the batch engine that we are not finished,
// and provide an estimation of the completion level we reached.
if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
$context['finished'] = (($context['sandbox']['max'] - $context['sandbox']['progress']) <= $limit) || ($context['sandbox']['progress'] >= $context['sandbox']['max']);
}
}
/**
* Implements hook_help().
*/
......@@ -105,7 +268,15 @@ function services_menu() {
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
$items['admin/config/services/services-security'] = array(
'type' => MENU_NORMAL_ITEM,
'title' => 'Services Security update',
'description' => 'Services module security updates',
'page callback' => 'drupal_get_form',
'page arguments' => array('services_security_admin_form'),
'access arguments' => array('administer site configuration'),
'file' => 'services.admin.inc',
);
return $items;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment