Skip to content

Commit

Permalink
feat: Implement an upgrade plugin to update passwords hashing algorit…
Browse files Browse the repository at this point in the history
…hm - EXO-65151 - Meeds-io/MIPs#69

Prior to this change, As a part of the upgrade mechanism of old hashed passwords we need to create an UP to re-hash old passwords by the new hash algo.
This PR creates an UP which has to update the old passwords hash by the new algo by re-hashing and adding the needed salt attribute.
  • Loading branch information
hakermi committed Jul 27, 2023
1 parent 83f400a commit def7e0d
Show file tree
Hide file tree
Showing 4 changed files with 305 additions and 0 deletions.
7 changes: 7 additions & 0 deletions data-upgrade-users/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,12 @@
<artifactId>social-component-service</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.exoplatform.gatein.portal</groupId>
<artifactId>exo.portal.component.identity</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* This file is part of the Meeds project (https://meeds.io/).
* Copyright (C) 2023 Meeds Association
* [email protected]
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.exoplatform.migration;

import org.apache.commons.codec.binary.Hex;
import org.exoplatform.commons.api.settings.SettingService;
import org.exoplatform.commons.api.settings.SettingValue;
import org.exoplatform.commons.api.settings.data.Context;
import org.exoplatform.commons.api.settings.data.Scope;
import org.exoplatform.commons.persistence.impl.EntityManagerService;
import org.exoplatform.commons.upgrade.UpgradePluginExecutionContext;
import org.exoplatform.commons.upgrade.UpgradeProductPlugin;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.container.component.RequestLifeCycle;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.organization.idm.PicketLinkIDMService;
import org.exoplatform.web.security.security.SecureRandomService;
import org.picketlink.idm.api.User;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.security.SecureRandom;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class UserPasswordHashMigration extends UpgradeProductPlugin {

private static final Log LOG =
ExoLogger.getExoLogger(UserPasswordHashMigration.class);

private final EntityManagerService entityManagerService;

private final PicketLinkIDMService picketLinkIDMService;

private final SecureRandomService secureRandomService;

private final SettingService settingService;

private static final String PASSWORD_SALT_USER_ATTRIBUTE = "passwordSalt128";

private static final String USER_PASSWORD_HASH_MIGRATION_SETTING_KEY = "userPasswordHashMigrationEnded";

private static final String USER_PASSWORD_HASH_MIGRATION = "userPasswordHashMigration";

public UserPasswordHashMigration(EntityManagerService entityManagerService,
PicketLinkIDMService picketLinkIDMService,
SecureRandomService secureRandomService,
SettingService settingService,
InitParams initParams) {
super(initParams);
this.entityManagerService = entityManagerService;
this.picketLinkIDMService = picketLinkIDMService;
this.secureRandomService = secureRandomService;
this.settingService = settingService;
}

@Override
public void processUpgrade(String s, String s1) {
LOG.info("Start upgrade of users passwords hashing algorithm");
long startupTime = System.currentTimeMillis();

PortalContainer container = PortalContainer.getInstance();
RequestLifeCycle.begin(container);
AtomicInteger updatedPasswords = new AtomicInteger();
EntityManager entityManager = this.entityManagerService.getEntityManager();
try {
String sqlString = "SELECT jbid_io.NAME, jbid_io_creden.TEXT FROM " +
" ((jbid_io_attr INNER JOIN jbid_io_creden ON jbid_io_attr.IDENTITY_OBJECT_ID = jbid_io_creden.IDENTITY_OBJECT_ID)" +
" INNER JOIN jbid_io ON jbid_io_attr.IDENTITY_OBJECT_ID = jbid_io.ID)" +
" INNER JOIN( SELECT jbid_io_attr.IDENTITY_OBJECT_ID," +
" min(CASE WHEN jbid_io_attr.NAME = 'passwordSalt' THEN jbid_io_attr.NAME ELSE NULL END) AS salt," +
" min(CASE WHEN jbid_io_attr.NAME = 'passwordSalt128' THEN jbid_io_attr.NAME ELSE NULL END) AS salt128" +
" FROM jbid_io_attr GROUP BY jbid_io_attr.IDENTITY_OBJECT_ID" +
" HAVING salt IS NOT NULL AND salt128 IS NULL) jia ON jbid_io_attr.IDENTITY_OBJECT_ID = jia.IDENTITY_OBJECT_ID;";

Query nativeQuery = entityManager.createNativeQuery(sqlString);
List<Object[]> result = nativeQuery.getResultList();
result.forEach(item -> {
String userName = (String) item[0];
String passwordHash = (String) item[1];
try {
String saltString = Hex.encodeHexString(generateRandomSalt());
User user = picketLinkIDMService.getIdentitySession().getPersistenceManager().findUser(userName);
picketLinkIDMService.getExtendedAttributeManager().addAttribute(userName, PASSWORD_SALT_USER_ATTRIBUTE, saltString);
picketLinkIDMService.getExtendedAttributeManager().updatePassword(user, passwordHash);
updatedPasswords.getAndIncrement();
LOG.info("{}/{} passwords have been updated", updatedPasswords.get(), result.size());
} catch (Exception e) {
LOG.error("Error while creating attribute salt and updating password hash for user : {}", userName, e);
}
});
} catch (Exception e) {
LOG.error("Error while getting old users passwords hash", e);
} finally {
RequestLifeCycle.end();
}
LOG.info("End upgrade of users passwords hashing algorithm. {} passwords has been updated. It took {} ms",
updatedPasswords.get(),
(System.currentTimeMillis() - startupTime));
}

@Override
public void afterUpgrade() {
settingService.set(Context.GLOBAL.id(USER_PASSWORD_HASH_MIGRATION),
Scope.APPLICATION.id(USER_PASSWORD_HASH_MIGRATION),
USER_PASSWORD_HASH_MIGRATION_SETTING_KEY,
SettingValue.create(true));
}

@Override
public boolean shouldProceedToUpgrade(String newVersion,
String previousGroupVersion,
UpgradePluginExecutionContext previousUpgradePluginExecution) {
SettingValue<?> settingValue = settingService.get(Context.GLOBAL.id(USER_PASSWORD_HASH_MIGRATION),
Scope.APPLICATION.id(USER_PASSWORD_HASH_MIGRATION),
USER_PASSWORD_HASH_MIGRATION_SETTING_KEY);
return settingValue == null || settingValue.getValue().equals("false");
}

private byte[] generateRandomSalt() {
SecureRandom secureRandom = secureRandomService.getSecureRandom();
byte[] salt = new byte[16];
secureRandom.nextBytes(salt);
return salt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,40 @@
</value-param>
</init-params>
</component-plugin>
<component-plugin>
<name>UserPasswordHashMigration</name>
<set-method>addUpgradePlugin</set-method>
<type>org.exoplatform.migration.UserPasswordHashMigration</type>
<description>Update users passwords hash algorithm</description>
<init-params>
<value-param>
<name>product.group.id</name>
<description>The groupId of the product</description>
<value>org.exoplatform.platform</value>
</value-param>
<value-param>
<name>plugin.upgrade.target.version</name>
<description>The plugin target version (will not be executed if previous version is equal or higher than 6.3.1)
</description>
<value>6.5.0</value>
</value-param>
<value-param>
<name>plugin.execution.order</name>
<description>The plugin execution order</description>
<value>100</value>
</value-param>
<value-param>
<name>plugin.upgrade.execute.once</name>
<description>The plugin must be executed only once</description>
<value>true</value>
</value-param>
<value-param>
<name>plugin.upgrade.async.execution</name>
<description>The plugin will be executed in an asynchronous mode</description>
<value>true</value>
</value-param>
</init-params>
</component-plugin>
</external-component-plugins>

</configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* This file is part of the Meeds project (https://meeds.io/).
* Copyright (C) 2023 Meeds Association
* [email protected]
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.exoplatform.migration;

import org.exoplatform.commons.api.settings.SettingService;
import org.exoplatform.commons.api.settings.SettingValue;
import org.exoplatform.commons.api.settings.data.Context;
import org.exoplatform.commons.api.settings.data.Scope;
import org.exoplatform.commons.persistence.impl.EntityManagerService;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.services.organization.OrganizationService;
import org.exoplatform.services.organization.UserHandler;
import org.exoplatform.services.organization.idm.PicketLinkIDMService;
import org.exoplatform.web.security.security.SecureRandomService;
import org.gatein.portal.idm.impl.store.attribute.ExtendedAttributeManager;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.picketlink.idm.api.IdentitySession;
import org.picketlink.idm.api.PersistenceManager;

import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.Query;

import java.util.ArrayList;
import java.util.List;

import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class UserPasswordHashMigrationTest {

@Mock
private EntityManagerService entityManagerService;

@Mock
private PicketLinkIDMService picketLinkIDMService;

private SettingService settingService;

private SecureRandomService secureRandomService;

private PortalContainer container;

private UserPasswordHashMigration userPasswordHashMigration;

private static final String USER_PASSWORD_HASH_MIGRATION_SETTING_KEY = "userPasswordHashMigrationEnded";

private static final String USER_PASSWORD_HASH_MIGRATION = "userPasswordHashMigration";

@Before
public void setUp() {
container = PortalContainer.getInstance();
settingService = container.getComponentInstanceOfType(SettingService.class);
secureRandomService = container.getComponentInstanceOfType(SecureRandomService.class);
userPasswordHashMigration = new UserPasswordHashMigration(entityManagerService,
picketLinkIDMService,
secureRandomService,
settingService,
new InitParams());
}

@Test
public void processUpgrade() throws Exception {
List<Object[]> result = new ArrayList<>();
result.add(new String[] { "user", "passwordHash" });
EntityManager entityManager = mock(EntityManager.class);
EntityTransaction entityTransaction = mock(EntityTransaction.class);
when(entityTransaction.isActive()).thenReturn(true);
when(entityManager.getTransaction()).thenReturn(entityTransaction);
Query query = mock(Query.class);
when(entityManager.createNativeQuery(anyString())).thenReturn(query);
when(query.getResultList()).thenReturn(result);
when(entityManagerService.getEntityManager()).thenReturn(entityManager);

IdentitySession identitySession = mock(IdentitySession.class);
PersistenceManager persistenceManager = mock(PersistenceManager.class);
ExtendedAttributeManager extendedAttributeManager = mock(ExtendedAttributeManager.class);
when(identitySession.getPersistenceManager()).thenReturn(persistenceManager);
when(picketLinkIDMService.getIdentitySession()).thenReturn(identitySession);
when(picketLinkIDMService.getExtendedAttributeManager()).thenReturn(extendedAttributeManager);
boolean proceedToUpgrade = userPasswordHashMigration.shouldProceedToUpgrade(null, null, null);
assertTrue(proceedToUpgrade);
SettingValue<?> settingValue = settingService.get(Context.GLOBAL.id(USER_PASSWORD_HASH_MIGRATION),
Scope.APPLICATION.id(USER_PASSWORD_HASH_MIGRATION),
USER_PASSWORD_HASH_MIGRATION_SETTING_KEY);
assertNull(settingValue);
userPasswordHashMigration.processUpgrade(null, null);
userPasswordHashMigration.afterUpgrade();
verify(extendedAttributeManager, times(1)).updatePassword(any(), anyString());
verify(extendedAttributeManager, times(1)).addAttribute(anyString(), anyString(), anyString());

settingValue = settingService.get(Context.GLOBAL.id(USER_PASSWORD_HASH_MIGRATION),
Scope.APPLICATION.id(USER_PASSWORD_HASH_MIGRATION),
USER_PASSWORD_HASH_MIGRATION_SETTING_KEY);
assertNotNull(settingValue);
assertEquals(true, settingValue.getValue());

proceedToUpgrade = userPasswordHashMigration.shouldProceedToUpgrade(null, null, null);
assertFalse(proceedToUpgrade);
}
}

0 comments on commit def7e0d

Please sign in to comment.