From def7e0df02945992077dd5c3367d390e79140c38 Mon Sep 17 00:00:00 2001 From: hakermi Date: Wed, 26 Jul 2023 17:03:04 +0100 Subject: [PATCH] feat: Implement an upgrade plugin to update passwords hashing algorithm - 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. --- data-upgrade-users/pom.xml | 7 + .../migration/UserPasswordHashMigration.java | 142 ++++++++++++++++++ .../resources/conf/portal/configuration.xml | 34 +++++ .../UserPasswordHashMigrationTest.java | 122 +++++++++++++++ 4 files changed, 305 insertions(+) create mode 100644 data-upgrade-users/src/main/java/org/exoplatform/migration/UserPasswordHashMigration.java create mode 100644 data-upgrade-users/src/test/java/org/exoplatform/migration/UserPasswordHashMigrationTest.java diff --git a/data-upgrade-users/pom.xml b/data-upgrade-users/pom.xml index 324e8b454..f98cb6882 100644 --- a/data-upgrade-users/pom.xml +++ b/data-upgrade-users/pom.xml @@ -35,5 +35,12 @@ social-component-service provided + + + org.exoplatform.gatein.portal + exo.portal.component.identity + test-jar + test + diff --git a/data-upgrade-users/src/main/java/org/exoplatform/migration/UserPasswordHashMigration.java b/data-upgrade-users/src/main/java/org/exoplatform/migration/UserPasswordHashMigration.java new file mode 100644 index 000000000..aac1045a2 --- /dev/null +++ b/data-upgrade-users/src/main/java/org/exoplatform/migration/UserPasswordHashMigration.java @@ -0,0 +1,142 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * Copyright (C) 2023 Meeds Association + * contact@meeds.io + * 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 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; + } +} diff --git a/data-upgrade-users/src/main/resources/conf/portal/configuration.xml b/data-upgrade-users/src/main/resources/conf/portal/configuration.xml index 1f94e0e9b..8be0406f5 100644 --- a/data-upgrade-users/src/main/resources/conf/portal/configuration.xml +++ b/data-upgrade-users/src/main/resources/conf/portal/configuration.xml @@ -93,6 +93,40 @@ + + UserPasswordHashMigration + addUpgradePlugin + org.exoplatform.migration.UserPasswordHashMigration + Update users passwords hash algorithm + + + product.group.id + The groupId of the product + org.exoplatform.platform + + + plugin.upgrade.target.version + The plugin target version (will not be executed if previous version is equal or higher than 6.3.1) + + 6.5.0 + + + plugin.execution.order + The plugin execution order + 100 + + + plugin.upgrade.execute.once + The plugin must be executed only once + true + + + plugin.upgrade.async.execution + The plugin will be executed in an asynchronous mode + true + + + diff --git a/data-upgrade-users/src/test/java/org/exoplatform/migration/UserPasswordHashMigrationTest.java b/data-upgrade-users/src/test/java/org/exoplatform/migration/UserPasswordHashMigrationTest.java new file mode 100644 index 000000000..54d9af998 --- /dev/null +++ b/data-upgrade-users/src/test/java/org/exoplatform/migration/UserPasswordHashMigrationTest.java @@ -0,0 +1,122 @@ +/* + * This file is part of the Meeds project (https://meeds.io/). + * Copyright (C) 2023 Meeds Association + * contact@meeds.io + * 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 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); + } +}