Merged 5.1-MC1 (5.1.0) to HEAD (5.1)

119056 adavis: Merged 5.1.N (5.1.1) to 5.1-MC1 (5.1.0)
      117338 adavis: Merged 5.0.2-CLOUD42 (Cloud ) to 5.1.N (5.1.1)
         117246 adavis: Merged 5.0.2-CLOUD (Cloud ) to 5.0.2-CLOUD42 (Cloud )
            114515 adavis: Merged BCRYPT to 5.0.2-CLOUD
               113961 gcornwell: MNT-14892: Added UpgradePasswordHashWorker job and skeleton test class


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@119895 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
Jean-Pierre Huynh
2015-12-10 09:59:44 +00:00
parent f7631e37b8
commit 05375c63fc
5 changed files with 688 additions and 1 deletions

View File

@@ -695,4 +695,41 @@
</property> </property>
</bean> </bean>
<!-- UpgradePasswordHashWorker -->
<bean id="upgradePasswordHashWorker" class="org.alfresco.repo.security.authentication.UpgradePasswordHashWorker">
<property name="jobLockService">
<ref bean="jobLockService" />
</property>
<property name="transactionService">
<ref bean="transactionService" />
</property>
<property name="authenticationDao">
<ref bean="authenticationDao" />
</property>
<property name="compositePasswordEncoder">
<ref bean="compositePasswordEncoder" />
</property>
<property name="behaviourFilter">
<ref bean="policyBehaviourFilter" />
</property>
<property name="patchDAO">
<ref bean="patchDAO"/>
</property>
<property name="nodeDAO">
<ref bean="nodeDAO"/>
</property>
<property name="qnameDAO">
<ref bean="qnameDAO"/>
</property>
<property name="queryRange">
<value>${system.upgradePasswordHash.jobQueryRange}</value>
</property>
<property name="threadCount">
<value>${system.upgradePasswordHash.jobThreadCount}</value>
</property>
<property name="batchSize">
<value>${system.upgradePasswordHash.jobBatchSize}</value>
</property>
</bean>
</beans> </beans>

View File

@@ -1107,6 +1107,12 @@ links.protocosl.white.list=http,https,ftp,mailto
cmis.disable.hidden.leading.period.files=false 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 Config Properties
virtual.folders.enabled=false virtual.folders.enabled=false

View File

@@ -281,4 +281,24 @@
<value>${system.cronJob.startDelayMinutes}</value> <value>${system.cronJob.startDelayMinutes}</value>
</property> </property>
</bean> </bean>
<!-- Definition for the upgrade password hash job -->
<!--
<bean id="upgradePasswordHashJobDetail" class="org.springframework.scheduling.quartz.JobDetailBean">
<property name="jobClass"
value="org.alfresco.repo.security.authenticationUpgradePasswordHashWorker$UpgradePasswordHashJob" />
<property name="jobDataAsMap">
<map>
<entry key="upgradePasswordHashWorker" value-ref="upgradePasswordHashWorker" />
</map>
</property>
</bean>
<bean id="upgradePasswordHashJobTrigger" class="org.alfresco.util.CronTriggerBean">
<property name="jobDetail" ref="upgradePasswordHashJobDetail" />
<property name="scheduler" ref="schedulerFactory" />
<property name="cronExpression" value="${system.upgradePasswordHash.jobCronExpression}" />
<property name="startDelayMinutes" value="${system.cronJob.startDelayMinutes}" />
</bean>
-->
</beans> </beans>

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
/**
* <h1>Upgrade Password Hash Worker</h1>
*
* <h2>What it is</h2>
* A worker for a scheduled job that checks and upgrades users passwords to the system's preferred encoding.
*
* <h2>Settings that control the behaviour</h2>
* <ul>
* <li><b>${system.upgradePasswordHash.jobBatchSize}</b> - the number of users to process at one time.</li>
* <li><b>${system.upgradePasswordHash.jobQueryRange}</b> - 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.</li>
* <li><b>${system.upgradePasswordHash.jobThreadCount}</b> - 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.</li>
* </ul>
*
* @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<Long> workProvider = new UpgradePasswordHashWorkProvider(progress);
BatchProcessWorker<Long> worker = new UpgradePasswordHashBatch(progress);
RetryingTransactionHelper retryingTransactionHelper = transactionService.getRetryingTransactionHelper();
retryingTransactionHelper.setForceWritable(true);
BatchProcessor<Long> batchProcessor = new BatchProcessor<Long>(
"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<Long>
{
private final long maxNodeId;
private final UpgradePasswordHashWorkResult progress;
private final Pair<Long, QName> 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<Long> 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<Long> 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<Long>
{
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<QName, Serializable> userProps = nodeDAO.getNodeProperties(nodeId);
// get the hash indicator property
List<String> hashIndicator = (List<String>)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.
* <p>
* Job data:
* <ul>
* <li><b>upgradePasswordHashWorker</b> - The worker that performs the actual processing.</li>
* </ul>
*
* @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();
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> 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<String>(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
}
}