Commit b8a61f20 authored by catch's avatar catch

Issue #2598038 by alexpott, sethcardoza, quietone, Jaesin: Invalid passwords...

Issue #2598038 by alexpott, sethcardoza, quietone, Jaesin: Invalid passwords after D7 to D8 migration
parent 42d2ce92
......@@ -28,6 +28,8 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
->setSetting('case_sensitive', TRUE);
$properties['existing'] = DataDefinition::create('string')
->setLabel(new TranslatableMarkup('Existing password'));
$properties['pre_hashed'] = DataDefinition::create('boolean')
->setLabel(new TranslatableMarkup('Determines if a password needs hashing'));
return $properties;
}
......@@ -40,8 +42,11 @@ public function preSave() {
$entity = $this->getEntity();
// Update the user password if it has changed.
if ($entity->isNew() || (strlen(trim($this->value)) > 0 && $this->value != $entity->original->{$this->getFieldDefinition()->getName()}->value)) {
if ($this->pre_hashed) {
// Reset the pre_hashed value since it has now been used.
$this->pre_hashed = FALSE;
}
elseif ($entity->isNew() || (strlen(trim($this->value)) > 0 && $this->value != $entity->original->{$this->getFieldDefinition()->getName()}->value)) {
// Allow alternate password hashing schemes.
$this->value = \Drupal::service('password')->hash(trim($this->value));
// Abort if the hashing failed and returned FALSE.
......
......@@ -40789,7 +40789,7 @@
->values(array(
'uid' => '2',
'name' => 'Odo',
'pass' => '$S$DZ4P7zZOh92vgrgZDBbv8Pu6lQB337OJ1wsOy21602G4A5F7.M9K',
'pass' => '$S$DGFZUE.FhrXbe4y52eC7p0ZVRGD/gOPtVctDlmC89qkujnBokAlJ',
'mail' => 'odo@local.host',
'theme' => '',
'signature' => '',
......@@ -3,6 +3,7 @@
namespace Drupal\migrate_drupal_ui\Tests\d6;
use Drupal\migrate_drupal_ui\Tests\MigrateUpgradeTestBase;
use Drupal\user\Entity\User;
/**
* Tests Drupal 6 upgrade using the migrate UI.
......@@ -70,4 +71,16 @@ protected function getEntityCounts() {
];
}
/**
* Executes all steps of migrations upgrade.
*/
protected function testMigrateUpgrade() {
parent::testMigrateUpgrade();
// Ensure migrated users can log in.
$user = User::load(2);
$user->pass_raw = 'john.doe_pass';
$this->drupalLogin($user);
}
}
......@@ -3,6 +3,7 @@
namespace Drupal\migrate_drupal_ui\Tests\d7;
use Drupal\migrate_drupal_ui\Tests\MigrateUpgradeTestBase;
use Drupal\user\Entity\User;
/**
* Tests Drupal 7 upgrade using the migrate UI.
......@@ -70,4 +71,16 @@ protected function getEntityCounts() {
];
}
/**
* Executes all steps of migrations upgrade.
*/
protected function testMigrateUpgrade() {
parent::testMigrateUpgrade();
// Ensure migrated users can log in.
$user = User::load(2);
$user->pass_raw = 'a password';
$this->drupalLogin($user);
}
}
<?php
namespace Drupal\user;
use Drupal\Core\Password\PasswordInterface;
/**
* Replaces the original 'password' service in order to prefix the MD5 re-hashed
* passwords with the 'U' flag. The new salted hash is recreated on first login
* similarly to the D6->D7 upgrade path.
*/
class MigratePassword implements PasswordInterface {
/**
* The original password service.
*
* @var \Drupal\Core\Password\PasswordInterface
*/
protected $originalPassword;
/**
* Indicates if MD5 password prefixing is enabled.
*/
protected $enabled = FALSE;
/**
* Builds the replacement password service class.
*
* @param \Drupal\Core\Password\PasswordInterface $original_password
* The password object.
*/
public function __construct(PasswordInterface $original_password) {
$this->originalPassword = $original_password;
}
/**
* {@inheritdoc}
*/
public function check($password, $hash) {
return $this->originalPassword->check($password, $hash);
}
/**
* {@inheritdoc}
*/
public function needsRehash($hash) {
return $this->originalPassword->needsRehash($hash);
}
/**
* {@inheritdoc}
*/
public function hash($password) {
$hash = $this->originalPassword->hash($password);
// Allow prefixing only if the service was asked to prefix. Check also if
// the $password pattern is conforming to a MD5 result.
if ($this->enabled && preg_match('/^[0-9a-f]{32}$/', $password)) {
$hash = 'U' . $hash;
}
return $hash;
}
/**
* Enables the MD5 password prefixing.
*/
public function enableMd5Prefixing() {
$this->enabled = TRUE;
}
/**
* Disables the MD5 password prefixing.
*/
public function disableMd5Prefixing() {
$this->enabled = FALSE;
}
/**
* Implements the PhpassHashedPassword::getCountLog2() method.
*
* @todo: Revisit this whole alternate password service:
* https://www.drupal.org/node/2540594.
*/
public function getCountLog2($setting) {
return $this->originalPassword->getCountLog2($setting);
}
}
......@@ -3,14 +3,13 @@
namespace Drupal\user\Plugin\migrate\destination;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Field\Plugin\Field\FieldType\EmailItem;
use Drupal\Core\Password\PasswordInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateException;
use Drupal\user\MigratePassword;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -55,9 +54,7 @@ class EntityUser extends EntityContentBase {
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, EntityManagerInterface $entity_manager, FieldTypePluginManagerInterface $field_type_manager, PasswordInterface $password) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $entity_manager, $field_type_manager);
if (isset($configuration['md5_passwords'])) {
$this->password = $password;
}
$this->password = $password;
}
/**
......@@ -83,24 +80,29 @@ public static function create(ContainerInterface $container, array $configuratio
* @throws \Drupal\migrate\MigrateException
*/
public function import(Row $row, array $old_destination_id_values = array()) {
if ($this->password) {
if ($this->password instanceof MigratePassword) {
$this->password->enableMd5Prefixing();
}
else {
throw new MigrateException('Password service has been altered by another module, aborting.');
}
}
// Do not overwrite the root account password.
if ($row->getDestinationProperty('uid') == 1) {
$row->removeDestinationProperty('pass');
}
$ids = parent::import($row, $old_destination_id_values);
if ($this->password) {
$this->password->disableMd5Prefixing();
}
return parent::import($row, $old_destination_id_values);
}
return $ids;
/**
* {@inheritdoc}
*/
protected function save(ContentEntityInterface $entity, array $old_destination_id_values = array()) {
// Do not overwrite the root account password.
if ($entity->id() != 1) {
// Set the pre_hashed password so that the PasswordItem field does not hash
// already hashed passwords. If the md5_passwords configuration option is
// set we need to rehash the password and prefix with a U.
// @see \Drupal\Core\Field\Plugin\Field\FieldType\PasswordItem::preSave()
$entity->pass->pre_hashed = TRUE;
if (isset($this->configuration['md5_passwords'])) {
$entity->pass->value = 'U' . $this->password->hash($entity->pass->value);
}
}
return parent::save($entity, $old_destination_id_values);
}
/**
......
<?php
namespace Drupal\user;
use Drupal\Core\DependencyInjection\ServiceModifierInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
/**
* Swaps the original 'password' service in order to handle password hashing for
* user migrations that have passwords hashed to MD5.
*
* @see \Drupal\migrate\MigratePassword
* @see \Drupal\Core\Password\PhpassHashedPassword
*/
class UserServiceProvider implements ServiceModifierInterface {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
$container->setDefinition('password_original', $container->getDefinition('password'));
$container->setDefinition('password', $container->getDefinition('password_migrate'));
}
}
......@@ -44,6 +44,8 @@ protected function setUp() {
* The username.
* @param string $mail
* The user's email address.
* @param string $password
* The password for this user.
* @param int $access
* The last access time.
* @param int $login
......@@ -59,7 +61,7 @@ protected function setUp() {
* @param bool $has_picture
* Whether the user is expected to have a picture attached.
*/
protected function assertEntity($id, $label, $mail, $access, $login, $blocked, $langcode, $init, array $roles = [RoleInterface::AUTHENTICATED_ID], $has_picture = FALSE) {
protected function assertEntity($id, $label, $mail, $password, $access, $login, $blocked, $langcode, $init, array $roles = [RoleInterface::AUTHENTICATED_ID], $has_picture = FALSE) {
/** @var \Drupal\user\UserInterface $user */
$user = User::load($id);
$this->assertTrue($user instanceof UserInterface);
......@@ -77,13 +79,29 @@ protected function assertEntity($id, $label, $mail, $access, $login, $blocked, $
$this->assertIdentical($init, $user->getInitialEmail());
$this->assertIdentical($roles, $user->getRoles());
$this->assertIdentical($has_picture, !$user->user_picture->isEmpty());
$this->assertIdentical($password, $user->getPassword());
}
/**
* Tests the Drupal 7 user to Drupal 8 migration.
*/
public function testUser() {
$this->assertEntity(2, 'Odo', 'odo@local.host', '0', '0', FALSE, '', 'odo@local.host');
$password = '$S$DGFZUE.FhrXbe4y52eC7p0ZVRGD/gOPtVctDlmC89qkujnBokAlJ';
$this->assertEntity(2, 'Odo', 'odo@local.host', $password, '0', '0', FALSE, '', 'odo@local.host');
// Ensure that the user can authenticate.
$this->assertEquals(2, \Drupal::service('user.auth')->authenticate('Odo', 'a password'));
// After authenticating the password will be rehashed because the password
// stretching iteration count has changed from 15 in Drupal 7 to 16 in
// Drupal 8.
$user = User::load(2);
$rehash = $user->getPassword();
$this->assertNotEquals($password, $rehash);
// Authenticate again and there should be no re-hash.
$this->assertEquals(2, \Drupal::service('user.auth')->authenticate('Odo', 'a password'));
$user = User::load(2);
$this->assertEquals($rehash, $user->getPassword());
}
}
......@@ -66,9 +66,6 @@ services:
arguments: ['@current_user', '@entity.manager']
tags:
- { name: 'context_provider' }
password_migrate:
class: Drupal\user\MigratePassword
arguments: ['@password_original']
parameters:
user.tempstore.expire: 604800
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