diff --git a/config/alfresco/authentication-services-context.xml b/config/alfresco/authentication-services-context.xml
index 5fc201a51a..bc36fcdced 100644
--- a/config/alfresco/authentication-services-context.xml
+++ b/config/alfresco/authentication-services-context.xml
@@ -694,5 +694,42 @@
${alfresco_user_store.guestusername}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${system.upgradePasswordHash.jobQueryRange}
+
+
+ ${system.upgradePasswordHash.jobThreadCount}
+
+
+ ${system.upgradePasswordHash.jobBatchSize}
+
+
diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties
index b813bbf166..d567584629 100644
--- a/config/alfresco/repository.properties
+++ b/config/alfresco/repository.properties
@@ -1107,6 +1107,12 @@ links.protocosl.white.list=http,https,ftp,mailto
cmis.disable.hidden.leading.period.files=false
+# Upgrade Password Hash Job
+system.upgradePasswordHash.jobBatchSize=100
+system.upgradePasswordHash.jobQueryRange=10000
+system.upgradePasswordHash.jobThreadCount=4
+system.upgradePasswordHash.jobCronExpression=* * * * * ? 2099
+
#Virtual Folders Config Properties
virtual.folders.enabled=false
diff --git a/config/alfresco/scheduled-jobs-context.xml b/config/alfresco/scheduled-jobs-context.xml
index 2bfaa9661a..4ce4db0240 100644
--- a/config/alfresco/scheduled-jobs-context.xml
+++ b/config/alfresco/scheduled-jobs-context.xml
@@ -280,5 +280,25 @@
${system.cronJob.startDelayMinutes}
-
+
+
+
+
+
diff --git a/source/java/org/alfresco/repo/security/authentication/UpgradePasswordHashWorker.java b/source/java/org/alfresco/repo/security/authentication/UpgradePasswordHashWorker.java
new file mode 100644
index 0000000000..a990c42bff
--- /dev/null
+++ b/source/java/org/alfresco/repo/security/authentication/UpgradePasswordHashWorker.java
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+package org.alfresco.repo.security.authentication;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.batch.BatchProcessWorkProvider;
+import org.alfresco.repo.batch.BatchProcessor;
+import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorker;
+import org.alfresco.repo.batch.BatchProcessor.BatchProcessWorkerAdaptor;
+import org.alfresco.repo.domain.node.NodeDAO;
+import org.alfresco.repo.domain.patch.PatchDAO;
+import org.alfresco.repo.domain.qname.QNameDAO;
+import org.alfresco.repo.lock.JobLockService;
+import org.alfresco.repo.lock.JobLockService.JobLockRefreshCallback;
+import org.alfresco.repo.lock.LockAcquisitionException;
+import org.alfresco.repo.policy.BehaviourFilter;
+import org.alfresco.repo.site.SiteModel;
+import org.alfresco.repo.transaction.RetryingTransactionHelper;
+import org.alfresco.service.namespace.NamespaceService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.service.transaction.TransactionService;
+import org.alfresco.util.Pair;
+import org.alfresco.util.ParameterCheck;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.quartz.Job;
+import org.quartz.JobDataMap;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
+/**
+ *
Upgrade Password Hash Worker
+ *
+ * What it is
+ * A worker for a scheduled job that checks and upgrades users passwords to the system's preferred encoding.
+ *
+ * Settings that control the behaviour
+ *
+ * - ${system.upgradePasswordHash.jobBatchSize} - the number of users to process at one time.
+ * - ${system.upgradePasswordHash.jobQueryRange} - the node ID range to query for.
+ * The process will repeat from the first to the last node, querying for up to this many nodes.
+ * Only reduce the value if the NodeDAO query takes a long time.
+ * - ${system.upgradePasswordHash.jobThreadCount} - the number of threads that will handle user checks and changes.
+ * Increase or decrease this to allow for free CPU capacity on the machine executing the job.
+ *
+ *
+ * @author Gavin Cornwell
+ */
+public class UpgradePasswordHashWorker implements ApplicationContextAware, InitializingBean
+{
+ private static final QName LOCK = QName.createQName(NamespaceService.SYSTEM_MODEL_1_0_URI, "UpgradePasswordHashWorker");
+ private static final long LOCK_TTL = 60000L;
+
+ private static Log logger = LogFactory.getLog(UpgradePasswordHashWorker.class);
+
+ private JobLockService jobLockService;
+ private TransactionService transactionService;
+
+ private MutableAuthenticationDao authenticationDao;
+ private CompositePasswordEncoder passwordEncoder;
+
+ private NodeDAO nodeDAO;
+ private PatchDAO patchDAO;
+ private QNameDAO qnameDAO;
+
+ private BehaviourFilter behaviourFilter;
+ private ApplicationContext ctx;
+
+ private int queryRange = 10000;
+ private int threadCount = 2;
+ private int batchSize = 100;
+
+ public void setJobLockService(JobLockService jobLockService)
+ {
+ this.jobLockService = jobLockService;
+ }
+
+ public void setTransactionService(TransactionService transactionService)
+ {
+ this.transactionService = transactionService;
+ }
+
+ public void setAuthenticationDao(MutableAuthenticationDao authenticationDao)
+ {
+ this.authenticationDao = authenticationDao;
+ }
+
+ public void setCompositePasswordEncoder(CompositePasswordEncoder passwordEncoder)
+ {
+ this.passwordEncoder = passwordEncoder;
+ }
+
+ public void setPatchDAO(PatchDAO patchDAO)
+ {
+ this.patchDAO = patchDAO;
+ }
+
+ public void setNodeDAO(NodeDAO nodeDAO)
+ {
+ this.nodeDAO = nodeDAO;
+ }
+
+ public void setQnameDAO(QNameDAO qnameDAO)
+ {
+ this.qnameDAO = qnameDAO;
+ }
+
+ public void setBehaviourFilter(BehaviourFilter behaviourFilter)
+ {
+ this.behaviourFilter = behaviourFilter;
+ }
+
+ /**
+ * Sets the number of users to retrieve from the repository in each query.
+ *
+ * @param queryRange The query range
+ */
+ public void setQueryRange(int queryRange)
+ {
+ this.queryRange = queryRange;
+ }
+
+ /**
+ * Sets the number of threads to use to process users.
+ *
+ * @param threadCount Number of threads
+ */
+ public void setThreadCount(int threadCount)
+ {
+ this.threadCount = threadCount;
+ }
+
+ /**
+ * Sets the number of users to process at one time.
+ *
+ * @param batchSize The batch size
+ */
+ public void setBatchSize(int batchSize)
+ {
+ this.batchSize = batchSize;
+ }
+
+ /**
+ * Set the application context for event publishing during batch processing
+ */
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
+ {
+ this.ctx = applicationContext;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception
+ {
+ ParameterCheck.mandatory("jobLockService", jobLockService);
+ ParameterCheck.mandatory("transactionService", transactionService);
+ ParameterCheck.mandatory("authenticationDao", authenticationDao);
+ ParameterCheck.mandatory("compositePasswordEncoder", passwordEncoder);
+ ParameterCheck.mandatory("nodeDAO", nodeDAO);
+ ParameterCheck.mandatory("patchDAO", patchDAO);
+ ParameterCheck.mandatory("qnameDAO", qnameDAO);
+ ParameterCheck.mandatory("behaviourFilter", behaviourFilter);
+ }
+
+ /**
+ * Performs the work, including logging details of progress.
+ */
+ public UpgradePasswordHashWorkResult execute()
+ {
+ // Build refresh callback
+ final UpgradePasswordHashWorkResult progress = new UpgradePasswordHashWorkResult();
+ JobLockRefreshCallback lockCallback = new JobLockRefreshCallback()
+ {
+ @Override
+ public void lockReleased()
+ {
+ progress.inProgress.set(false);
+ }
+
+ @Override
+ public boolean isActive()
+ {
+ return progress.inProgress.get();
+ }
+ };
+
+ String lockToken = null;
+ try
+ {
+ progress.inProgress.set(true);
+
+ // Get the lock
+ lockToken = jobLockService.getLock(LOCK, LOCK_TTL);
+
+ // Start the refresh timer
+ jobLockService.refreshLock(lockToken, LOCK, LOCK_TTL, lockCallback);
+
+ // Now we know that we'll do something
+ if (logger.isInfoEnabled())
+ {
+ logger.info("Starting upgrade password hash job.");
+ }
+
+ // Do the work
+ doWork(progress);
+
+ // Done
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Upgrade password hash job " + progress);
+ }
+ }
+ catch (LockAcquisitionException e)
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Skipping upgrade password hash job: " + e.getMessage());
+ }
+ }
+ catch (Exception e)
+ {
+ progress.inProgress.set(false);
+ logger.error("Upgrade password hash job " + progress);
+ logger.error("Stopping upgrade password hash job with exception.", e);
+ }
+ finally
+ {
+ if (lockToken != null)
+ {
+ jobLockService.releaseLock(lockToken, LOCK);
+ }
+
+ progress.inProgress.set(false);
+ }
+
+ // Done
+ return progress;
+ }
+
+ /**
+ * @param progress the thread-safe progress
+ */
+ private synchronized void doWork(UpgradePasswordHashWorkResult progress) throws Exception
+ {
+ // Build batch processor
+ BatchProcessWorkProvider workProvider = new UpgradePasswordHashWorkProvider(progress);
+ BatchProcessWorker worker = new UpgradePasswordHashBatch(progress);
+ RetryingTransactionHelper retryingTransactionHelper = transactionService.getRetryingTransactionHelper();
+ retryingTransactionHelper.setForceWritable(true);
+
+ BatchProcessor batchProcessor = new BatchProcessor(
+ "UpgradePasswordHashWorker",
+ retryingTransactionHelper,
+ workProvider,
+ threadCount,
+ batchSize,
+ ctx,
+ logger,
+ 1000);
+ batchProcessor.process(worker, true);
+ }
+
+ /**
+ * Work provider for batch job providing noderefs representing users to process
+ */
+ private class UpgradePasswordHashWorkProvider implements BatchProcessWorkProvider
+ {
+ private final long maxNodeId;
+ private final UpgradePasswordHashWorkResult progress;
+ private final Pair userTypeId;
+
+ private UpgradePasswordHashWorkProvider(UpgradePasswordHashWorkResult progress)
+ {
+ this.progress = progress;
+ this.maxNodeId = patchDAO.getMaxAdmNodeID();
+ this.userTypeId = qnameDAO.getQName(ContentModel.TYPE_USER);
+ }
+
+ @Override
+ public int getTotalEstimatedWorkSize()
+ {
+ // execute a query to get total number of user nodes in the system.
+ long totalUserCount = patchDAO.getCountNodesWithTypId(ContentModel.TYPE_USER);
+
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Max NodeID: " + this.maxNodeId);
+ logger.debug("Total number of users: " + totalUserCount);
+ }
+
+ return (int)totalUserCount;
+ }
+
+ @Override
+ public Collection getNextWork()
+ {
+ // Check that there are not too many errors
+ if (progress.errors.get() > 1000)
+ {
+ logger.warn("Upgrade password hash work terminating; too many errors.");
+ return Collections.emptyList();
+ }
+
+ // Keep shifting the query window up until we get results or we hit the original max node ID
+ List ret = Collections.emptyList();
+ while (ret.isEmpty() && progress.currentMinNodeId.get() < maxNodeId)
+ {
+ // Calculate the node ID range
+ Long minNodeId = null;
+ if (progress.currentMinNodeId.get() == 0L)
+ {
+ minNodeId = 1L;
+ progress.currentMinNodeId.set(minNodeId);
+ }
+ else
+ {
+ minNodeId = progress.currentMinNodeId.addAndGet(queryRange);
+ }
+ long maxNodeId = minNodeId + queryRange;
+
+ // Query for the next set of users
+ ret = patchDAO.getNodesByTypeQNameId(this.userTypeId.getFirst(), minNodeId, maxNodeId);
+ }
+
+ // Done
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Upgrade password hash work provider found " + ret.size() + " users.");
+ }
+
+ return ret;
+ }
+ }
+
+ /**
+ * Class that does the actual node manipulation to upgrade the password hash.
+ */
+ private class UpgradePasswordHashBatch extends BatchProcessWorkerAdaptor
+ {
+ private final UpgradePasswordHashWorkResult progress;
+
+ private UpgradePasswordHashBatch(UpgradePasswordHashWorkResult progress)
+ {
+ this.progress = progress;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void process(Long nodeId) throws Throwable
+ {
+ progress.usersProcessed.incrementAndGet();
+
+ try
+ {
+ // get properties for the user
+ Map userProps = nodeDAO.getNodeProperties(nodeId);
+
+ // get the hash indicator property
+ List hashIndicator = (List)userProps.get(ContentModel.PROP_HASH_INDICATOR);
+
+ // get the username
+ String username = (String)userProps.get(ContentModel.PROP_USER_USERNAME);
+
+ // determine whether we need to upgrade the password hash for the user
+ if (hashIndicator == null || !passwordEncoder.lastEncodingIsPreferred(hashIndicator))
+ {
+ progress.usersChanged.incrementAndGet();
+
+ // We do not want any behaviours associated with our transactions
+ behaviourFilter.disableBehaviour();
+
+ // call hashedPassword on the RepositoryAuthenticationDao object
+
+
+// ((RepositoryAuthenticationDao)authenticationDao).rehashedPassword(userProps);
+
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("Upgrading password hash for user: " + username);
+ }
+ }
+ else if (logger.isTraceEnabled())
+ {
+ logger.trace("User '" + username + "' has preferred encoding");
+ }
+ }
+ catch (Exception e)
+ {
+ // Record the failure
+ progress.errors.incrementAndGet();
+
+ // Rethrow so that the processing framework can handle things
+ throw e;
+ }
+ }
+
+ @Override
+ public String getIdentifier(Long nodeId)
+ {
+ return (String)nodeDAO.getNodeProperty(nodeId, ContentModel.PROP_USER_USERNAME);
+ }
+ }
+
+ /**
+ * Thread-safe helper class to carry the job progress information.
+ */
+ public static class UpgradePasswordHashWorkResult
+ {
+ private final AtomicBoolean inProgress = new AtomicBoolean(false);
+ private final AtomicInteger usersProcessed = new AtomicInteger(0);
+ private final AtomicInteger usersChanged = new AtomicInteger(0);
+ private final AtomicInteger errors = new AtomicInteger(0);
+ private final AtomicLong currentMinNodeId = new AtomicLong(0L);
+
+ @Override
+ public String toString()
+ {
+ String part1 = "Changed";
+ String part2 = String.format(" %4d out of a potential %4d users. ", usersChanged.get(), usersProcessed.get());
+ String part3 = String.format("[%2d Errors]", errors.get());
+ return part1 + part2 + part3;
+ }
+
+ public int getUsersProcessed()
+ {
+ return usersProcessed.get();
+ }
+
+ public int getUsersChanged()
+ {
+ return usersChanged.get();
+ }
+
+ public int getErrors()
+ {
+ return errors.get();
+ }
+ }
+
+ /**
+ * A scheduled job that checks and upgrades users passwords to the system's preferred encoding.
+ *
+ * Job data:
+ *
+ * - upgradePasswordHashWorker - The worker that performs the actual processing.
+ *
+ *
+ * @see UpgradePasswordHashWorker
+ */
+ public static class UpgradePasswordHashJob implements Job
+ {
+ public static final String JOB_DATA_WORKER = "upgradePasswordHashWorker";
+
+ public void execute(JobExecutionContext context) throws JobExecutionException
+ {
+ JobDataMap jobData = context.getJobDetail().getJobDataMap();
+
+ // extract the content Cleanup to use
+ Object upgradePasswordHashWorkerObj = jobData.get(JOB_DATA_WORKER);
+ if (upgradePasswordHashWorkerObj == null || !(upgradePasswordHashWorkerObj instanceof UpgradePasswordHashWorker))
+ {
+ throw new AlfrescoRuntimeException(
+ "UpgradePasswordHashJob data '" + JOB_DATA_WORKER + "' must reference a " + UpgradePasswordHashWorker.class.getSimpleName());
+ }
+
+ UpgradePasswordHashWorker worker = (UpgradePasswordHashWorker)upgradePasswordHashWorkerObj;
+ worker.execute();
+ }
+ }
+}
diff --git a/source/test-java/org/alfresco/repo/security/authentication/UpgradePasswordHashTest.java b/source/test-java/org/alfresco/repo/security/authentication/UpgradePasswordHashTest.java
new file mode 100644
index 0000000000..b378637cc1
--- /dev/null
+++ b/source/test-java/org/alfresco/repo/security/authentication/UpgradePasswordHashTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2005-2015 Alfresco Software Limited.
+ *
+ * This file is part of Alfresco
+ *
+ * Alfresco 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.
+ *
+ * Alfresco 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 Alfresco. If not, see .
+ */
+package org.alfresco.repo.security.authentication;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.transaction.Status;
+import javax.transaction.UserTransaction;
+
+import junit.framework.TestCase;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
+import org.alfresco.repo.transaction.AlfrescoTransactionSupport.TxnReadState;
+import org.alfresco.service.ServiceRegistry;
+import org.alfresco.test_category.OwnJVMTestsCategory;
+import org.alfresco.util.ApplicationContextHelper;
+import org.junit.experimental.categories.Category;
+import org.springframework.context.ApplicationContext;
+
+@SuppressWarnings("unchecked")
+@Category(OwnJVMTestsCategory.class)
+public class UpgradePasswordHashTest extends TestCase
+{
+ private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
+
+ private UserTransaction userTransaction;
+ private ServiceRegistry serviceRegistry;
+ private UpgradePasswordHashWorker upgradePasswordHashWorker;
+ private List testUserNames;
+
+ public UpgradePasswordHashTest()
+ {
+ super();
+ }
+
+ public UpgradePasswordHashTest(String arg0)
+ {
+ super(arg0);
+ }
+
+ public void setUp() throws Exception
+ {
+ if (AlfrescoTransactionSupport.getTransactionReadState() != TxnReadState.TXN_NONE)
+ {
+ throw new AlfrescoRuntimeException(
+ "A previous tests did not clean up transaction: " +
+ AlfrescoTransactionSupport.getTransactionId());
+ }
+
+ serviceRegistry = (ServiceRegistry)ctx.getBean("ServiceRegistry");
+
+ upgradePasswordHashWorker = (UpgradePasswordHashWorker)ctx.getBean("upgradePasswordHashWorker");
+
+ userTransaction = serviceRegistry.getTransactionService().getUserTransaction();
+ userTransaction.begin();
+
+ AuthenticationUtil.setFullyAuthenticatedUser(AuthenticationUtil.getSystemUserName());
+
+ createTestUsers();
+ }
+
+ protected void createTestUsers() throws Exception
+ {
+ // create 50 users and change their properties back to how
+ // they would have been pre-upgrade.
+
+ testUserNames = new ArrayList(50);
+ }
+
+ protected void deleteTestUsers() throws Exception
+ {
+ // delete all the test users.
+ }
+
+ @Override
+ protected void tearDown() throws Exception
+ {
+ // remove all the test users we created
+ deleteTestUsers();
+
+ // cleanup transaction if necessary so we don't effect subsequent tests
+ if ((userTransaction.getStatus() == Status.STATUS_ACTIVE) || (userTransaction.getStatus() == Status.STATUS_MARKED_ROLLBACK))
+ {
+ userTransaction.rollback();
+ }
+
+ AuthenticationUtil.clearCurrentSecurityContext();
+ super.tearDown();
+ }
+
+ public void testWorkerWithDefaultConfiguration() throws Exception
+ {
+ // execute the worker to upgrade all users
+ this.upgradePasswordHashWorker.execute();
+
+ // ensure all the test users have been upgraded to use the preferred encoding
+ }
+
+ public void xxxtestWorkerWithLegacyConfiguration() throws Exception
+ {
+ // execute the worker to upgrade all users
+ this.upgradePasswordHashWorker.execute();
+
+ // ensure all the test users have been upgraded but maintain the MD4 encoding
+ }
+}