+
+
\ No newline at end of file
diff --git a/config/alfresco/workflow/reset-password-workflow-model.xml b/config/alfresco/workflow/reset-password-workflow-model.xml
new file mode 100644
index 0000000000..ab55d7b2ad
--- /dev/null
+++ b/config/alfresco/workflow/reset-password-workflow-model.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ bpm:startTask
+
+ resetpasswordwf:resetPasswordInitialProperties
+
+
+
+
+ bpm:workflowTask
+
+ resetpasswordwf:resetPasswordInitialProperties
+
+
+
+
+ bpm:workflowTask
+
+ resetpasswordwf:resetPasswordInitialProperties
+
+
+
+
+ bpm:workflowTask
+
+ resetpasswordwf:resetPasswordInitialProperties
+
+
+
+
+
+
+
+
+ d:text
+
+
+ d:text
+
+
+ d:text
+
+
+ d:text
+
+
+
+
+
\ No newline at end of file
diff --git a/config/alfresco/workflow/reset-password_processdefinition.bpmn20.xml b/config/alfresco/workflow/reset-password_processdefinition.bpmn20.xml
new file mode 100644
index 0000000000..6273f89651
--- /dev/null
+++ b/config/alfresco/workflow/reset-password_processdefinition.bpmn20.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${resetpasswordwf_endTimer}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config/alfresco/workflow/workflow-messages.properties b/config/alfresco/workflow/workflow-messages.properties
index 7ff1ad8b06..a9047101f0 100644
--- a/config/alfresco/workflow/workflow-messages.properties
+++ b/config/alfresco/workflow/workflow-messages.properties
@@ -146,3 +146,7 @@ workflowtask.outcome.Approve=Approved
workflowtask.outcome.Reject=Rejected
workflowtask.already.done.error=This task has already been completed and is no longer editable.
+
+# Reset password Task Definitions
+resetpasswordwf_resetpassword.resetpassword.workflow.title=Request Password Reset.
+resetpasswordwf_resetpassword.resetpassword.workflow.description=Used to request a password reset for a user's own login.
\ No newline at end of file
diff --git a/source/java/org/alfresco/repo/security/authentication/ResetPasswordService.java b/source/java/org/alfresco/repo/security/authentication/ResetPasswordService.java
new file mode 100644
index 0000000000..9f90a4ef31
--- /dev/null
+++ b/source/java/org/alfresco/repo/security/authentication/ResetPasswordService.java
@@ -0,0 +1,92 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+
+package org.alfresco.repo.security.authentication;
+
+import org.activiti.engine.delegate.DelegateExecution;
+import org.alfresco.repo.client.config.ClientAppConfig.ClientApp;
+import org.alfresco.repo.client.config.ClientAppNotFoundException;
+import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetPasswordDetails;
+import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetPasswordEmailDetails;
+
+/**
+ * Reset password service.
+ *
+ * @author Jamal Kaabi-Mofrad
+ * @since 5.2.1
+ */
+public interface ResetPasswordService
+{
+ /**
+ * Request password reset (starts the workflow).
+ *
+ * @param userId the user id
+ * @param clientName the client app name (used to lookup the client that is registered to send emails so that
+ * client's specific configuration could be used.)
+ */
+ void requestReset(String userId, String clientName);
+
+ /**
+ * Validates the request reset password workflow and updates the workflow.
+ *
+ * @param resetDetails the {@code ResetPasswordDetails} object
+ */
+ void resetPassword(ResetPasswordDetails resetDetails);
+
+ /**
+ * Sends reset password email.
+ *
+ * @param execution the {@code DelegateExecution} object (is provided when a user requests password reset)
+ * @param fallbackEmailTemplatePath the class path of the fallback email template (request reset password email)
+ * @param emailSubject the email subject key
+ */
+ void sendResetPasswordEmail(DelegateExecution execution, String fallbackEmailTemplatePath, String emailSubject);
+
+ /**
+ * Updates the user's new password.
+ *
+ * @param execution the {@code DelegateExecution} object
+ * @param fallbackEmailTemplatePath the class path of the fallback email template (confirmation email)
+ * @param emailSubject the email subject key
+ */
+ void performResetPassword(DelegateExecution execution, String fallbackEmailTemplatePath, String emailSubject);
+
+ /**
+ * Sends an email.
+ *
+ * @param emailDetails the {@code ResetPasswordEmailDetails} object
+ */
+ void sendEmail(ResetPasswordEmailDetails emailDetails);
+
+ /**
+ * Gets the registered client.
+ *
+ * @param clientName the client name
+ * @return {@code ClientApp} object
+ * @throws ClientAppNotFoundException if no {@code ClientApp} is found with the given name
+ */
+ ClientApp getClientAppConfig(String clientName);
+}
diff --git a/source/java/org/alfresco/repo/security/authentication/ResetPasswordServiceImpl.java b/source/java/org/alfresco/repo/security/authentication/ResetPasswordServiceImpl.java
new file mode 100644
index 0000000000..38189efb83
--- /dev/null
+++ b/source/java/org/alfresco/repo/security/authentication/ResetPasswordServiceImpl.java
@@ -0,0 +1,776 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2017 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+
+package org.alfresco.repo.security.authentication;
+
+import org.activiti.engine.HistoryService;
+import org.activiti.engine.TaskService;
+import org.activiti.engine.delegate.DelegateExecution;
+import org.activiti.engine.task.Task;
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.model.ContentModel;
+import org.alfresco.repo.action.executer.MailActionExecuter;
+import org.alfresco.repo.admin.SysAdminParams;
+import org.alfresco.repo.client.config.ClientAppConfig;
+import org.alfresco.repo.client.config.ClientAppConfig.ClientApp;
+import org.alfresco.repo.client.config.ClientAppNotFoundException;
+import org.alfresco.repo.workflow.BPMEngineRegistry;
+import org.alfresco.repo.workflow.WorkflowModel;
+import org.alfresco.repo.workflow.WorkflowModelResetPassword;
+import org.alfresco.repo.workflow.activiti.ActivitiConstants;
+import org.alfresco.service.cmr.action.Action;
+import org.alfresco.service.cmr.action.ActionService;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.security.MutableAuthenticationService;
+import org.alfresco.service.cmr.security.PersonService;
+import org.alfresco.service.cmr.workflow.WorkflowDefinition;
+import org.alfresco.service.cmr.workflow.WorkflowException;
+import org.alfresco.service.cmr.workflow.WorkflowInstance;
+import org.alfresco.service.cmr.workflow.WorkflowPath;
+import org.alfresco.service.cmr.workflow.WorkflowService;
+import org.alfresco.service.cmr.workflow.WorkflowTask;
+import org.alfresco.service.cmr.workflow.WorkflowTaskQuery;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.util.EmailHelper;
+import org.alfresco.util.GUID;
+import org.alfresco.util.ParameterCheck;
+import org.alfresco.util.PropertyCheck;
+import org.alfresco.util.UrlUtil;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.extensions.surf.util.I18NUtil;
+import org.springframework.extensions.webscripts.WebScriptException;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Reset password implementation based on workflow.
+ *
+ * @author Jamal Kaabi-Mofrad
+ * @since 5.2.1
+ */
+public class ResetPasswordServiceImpl implements ResetPasswordService
+{
+ private static final Log LOGGER = LogFactory.getLog(ResetPasswordServiceImpl.class);
+
+ private static final String TIMER_END = "PT1H";
+ private static final String WORKFLOW_DESCRIPTION_KEY = "resetpasswordwf_resetpassword.resetpassword.workflow.description";
+ private static final String FTL_TEMPLATE_ASSETS_URL = "template_assets_url";
+ private static final String FTL_RESET_PASSWORD_URL = "reset_password_url";
+ private static final String FTL_USER_NAME = "userName";
+
+ private WorkflowService workflowService;
+ private HistoryService activitiHistoryService;
+ private ActionService actionService;
+ private PersonService personService;
+ private NodeService nodeService;
+ private SysAdminParams sysAdminParams;
+ private MutableAuthenticationService authenticationService;
+ private TaskService activitiTaskService;
+ private EmailHelper emailHelper;
+ private ClientAppConfig clientAppConfig;
+ private String timerEnd = TIMER_END;
+ private String defaultEmailSender;
+
+ public void setWorkflowService(WorkflowService workflowService)
+ {
+ this.workflowService = workflowService;
+ }
+
+ public void setActivitiHistoryService(HistoryService activitiHistoryService)
+ {
+ this.activitiHistoryService = activitiHistoryService;
+ }
+
+ public void setActionService(ActionService actionService)
+ {
+ this.actionService = actionService;
+ }
+
+ public void setPersonService(PersonService personService)
+ {
+ this.personService = personService;
+ }
+
+ public void setNodeService(NodeService nodeService)
+ {
+ this.nodeService = nodeService;
+ }
+
+ public void setSysAdminParams(SysAdminParams sysAdminParams)
+ {
+ this.sysAdminParams = sysAdminParams;
+ }
+
+ public void setAuthenticationService(MutableAuthenticationService authenticationService)
+ {
+ this.authenticationService = authenticationService;
+ }
+
+ public void setActivitiTaskService(TaskService activitiTaskService)
+ {
+ this.activitiTaskService = activitiTaskService;
+ }
+
+ public void setEmailHelper(EmailHelper emailHelper)
+ {
+ this.emailHelper = emailHelper;
+ }
+
+ public void setClientAppConfig(ClientAppConfig clientAppConfig)
+ {
+ this.clientAppConfig = clientAppConfig;
+ }
+
+ public void setTimerEnd(String timerEnd)
+ {
+ if (StringUtils.isNotEmpty(timerEnd))
+ {
+ this.timerEnd = timerEnd;
+ }
+ }
+
+ public void setDefaultEmailSender(String defaultEmailSender)
+ {
+ this.defaultEmailSender = defaultEmailSender;
+ }
+
+ public void init()
+ {
+ PropertyCheck.mandatory(this, "workflowService", workflowService);
+ PropertyCheck.mandatory(this, "activitiHistoryService", activitiHistoryService);
+ PropertyCheck.mandatory(this, "actionService", actionService);
+ PropertyCheck.mandatory(this, "personService", personService);
+ PropertyCheck.mandatory(this, "nodeService", nodeService);
+ PropertyCheck.mandatory(this, "sysAdminParams", sysAdminParams);
+ PropertyCheck.mandatory(this, "authenticationService", authenticationService);
+ PropertyCheck.mandatory(this, "activitiTaskService", activitiTaskService);
+ PropertyCheck.mandatory(this, "emailHelper", emailHelper);
+ PropertyCheck.mandatory(this, "clientAppConfig", clientAppConfig);
+ PropertyCheck.mandatory(this, "defaultEmailSender", defaultEmailSender);
+ }
+
+ @Override
+ public void requestReset(String userId, String clientName)
+ {
+ ParameterCheck.mandatoryString("userId", userId);
+ ParameterCheck.mandatoryString("clientName", clientName);
+
+ String userEmail = validateUserAndGetEmail(userId);
+
+ // Get the (latest) workflow definition for reset-password.
+ WorkflowDefinition wfDefinition = workflowService.getDefinitionByName(WorkflowModelResetPassword.WORKFLOW_DEFINITION_NAME);
+
+ // create workflow properties
+ Map props = new HashMap<>(7);
+ props.put(WorkflowModel.PROP_WORKFLOW_DESCRIPTION, I18NUtil.getMessage(WORKFLOW_DESCRIPTION_KEY));
+ props.put(WorkflowModelResetPassword.WF_PROP_USERNAME, userId);
+ props.put(WorkflowModelResetPassword.WF_PROP_USER_EMAIL, userEmail);
+ props.put(WorkflowModelResetPassword.WF_PROP_CLIENT_NAME, clientName);
+ props.put(WorkflowModel.ASSOC_PACKAGE, workflowService.createPackage(null));
+
+ String guid = GUID.generate();
+ props.put(WorkflowModelResetPassword.WF_PROP_KEY, guid);
+ props.put(WorkflowModelResetPassword.WF_PROP_TIMER_END, timerEnd);
+
+ // start the workflow
+ WorkflowPath path = workflowService.startWorkflow(wfDefinition.getId(), props);
+ if (path.isActive())
+ {
+ WorkflowTask startTask = workflowService.getStartTask(path.getInstance().getId());
+ workflowService.endTask(startTask.getId(), null);
+ }
+ }
+
+ protected String validateUserAndGetEmail(String userId)
+ {
+ if (!personService.personExists(userId))
+ {
+ throw new ResetPasswordWorkflowInvalidUserException("User does not exist: " + userId);
+ }
+ else if (!personService.isEnabled(userId))
+ {
+ throw new ResetPasswordWorkflowInvalidUserException("User is disabled: " + userId);
+ }
+
+ NodeRef personNode = personService.getPerson(userId, false);
+ return (String) nodeService.getProperty(personNode, ContentModel.PROP_EMAIL);
+ }
+
+ @Override
+ public void resetPassword(ResetPasswordDetails resetDetails)
+ {
+ ParameterCheck.mandatory("resetDetails", resetDetails);
+
+ validateIdAndKey(resetDetails.getWorkflowId(), resetDetails.getWorkflowKey(), resetDetails.getUserId());
+
+ // So now we know that the workflow instance exists, is active and has the correct key. We can proceed.
+ WorkflowTaskQuery processTaskQuery = new WorkflowTaskQuery();
+ processTaskQuery.setProcessId(resetDetails.getWorkflowId());
+ List tasks = workflowService.queryTasks(processTaskQuery, false);
+
+ if (tasks.isEmpty())
+ {
+ throw new InvalidResetPasswordWorkflowException(
+ "Invalid workflow identifier: " + resetDetails.getWorkflowId() + ", " + resetDetails.getWorkflowKey());
+ }
+ WorkflowTask task = tasks.get(0);
+
+ // Set the provided password into the task. We will remove this after we have updated the user's authentication details.
+ Map props = Collections.singletonMap(WorkflowModelResetPassword.WF_PROP_PASSWORD, resetDetails.getPassword());
+
+ // Note the taskId as taken from the WorkflowService will include a "activiti$" prefix.
+ final String taskId = task.getId();
+ workflowService.updateTask(taskId, props, null, null);
+ workflowService.endTask(taskId, null);
+
+ // Remove the previous task from Activiti's history - so that the password will not be in the database.
+ // See http://www.activiti.org/userguide/index.html#history for a description of how Activiti stores historical records of
+ // processes, tasks and properties.
+ // The activitiHistoryService does not expect the activiti$ prefix.
+ final String activitiTaskId = taskId.replace("activiti$", "");
+ activitiHistoryService.deleteHistoricTaskInstance(activitiTaskId);
+
+ if (LOGGER.isDebugEnabled())
+ {
+ LOGGER.debug("Deleting historical task for security reasons " + activitiTaskId);
+ }
+ }
+
+ /**
+ * This method ensures that the id refers to an in-progress workflow and that the key matches
+ * that stored in the workflow.
+ *
+ * @throws WebScriptException a 404 if any of the above is not true.
+ */
+ private void validateIdAndKey(String id, String key, String userId)
+ {
+ WorkflowInstance workflowInstance = null;
+ try
+ {
+ workflowInstance = workflowService.getWorkflowById(id);
+ }
+ catch (WorkflowException ignored)
+ {
+ // Intentionally empty.
+ }
+
+ if (workflowInstance == null)
+ {
+ if (LOGGER.isDebugEnabled())
+ {
+ LOGGER.debug("The reset password workflow instance with the id [" + id + "] is not found.");
+ }
+ throw new ResetPasswordWorkflowNotFoundException("Reset Password cannot be linked to an ongoing secure process.");
+ }
+
+ String recoveredKey;
+ String username;
+ if (workflowInstance.isActive())
+ {
+ // If the workflow is active we will be able to read the path properties.
+ Map pathProps = workflowService.getPathProperties(id);
+
+ username = (String) pathProps.get(WorkflowModelResetPassword.WF_PROP_USERNAME);
+ recoveredKey = (String) pathProps.get(WorkflowModelResetPassword.WF_PROP_KEY);
+ }
+ else
+ {
+ if (LOGGER.isDebugEnabled())
+ {
+ LOGGER.debug("The reset password workflow instance with the id [" + id + "] is not active.");
+ }
+ throw new InvalidResetPasswordWorkflowException("Reset Password cannot be linked to an ongoing secure process.");
+ }
+ if (username == null || recoveredKey == null || !recoveredKey.equals(key))
+ {
+ if (LOGGER.isDebugEnabled())
+ {
+ if (username == null)
+ {
+ LOGGER.debug("The recovered user name is null for the reset password workflow instance with the id [" + id + "]");
+ }
+ else if (recoveredKey == null)
+ {
+ LOGGER.debug("The recovered key is null for the reset password workflow instance with the id [" + id + "]");
+ }
+ else
+ {
+ LOGGER.debug("The recovered key [" + recoveredKey + "] does not match the given workflow key [" + key
+ + "] for the reset password workflow instance with the id [" + id + "]");
+ }
+ }
+ throw new InvalidResetPasswordWorkflowException("Reset Password cannot be linked to an ongoing secure process.");
+ }
+ else if (!username.equals(userId))
+ {
+ if (LOGGER.isDebugEnabled())
+ {
+ LOGGER.debug("The given user id [" + userId + "] does not match the person's user id [" + username
+ + "] who requested the password reset.");
+ }
+ throw new InvalidResetPasswordWorkflowException("Reset Password cannot be linked to an ongoing secure process.");
+ }
+ }
+
+ @Override
+ public ClientApp getClientAppConfig(String clientName)
+ {
+ ParameterCheck.mandatoryString("clientName", clientName);
+
+ ClientApp clientApp = clientAppConfig.getClient(clientName);
+ if (clientApp == null)
+ {
+ throw new ClientAppNotFoundException("Client was not found [" + clientName + "]");
+ }
+ return clientApp;
+ }
+
+
+ @Override
+ public void sendResetPasswordEmail(DelegateExecution execution, String fallbackEmailTemplatePath, String emailSubject)
+ {
+ Map variables = execution.getVariables();
+ final String userName = (String) variables.get(WorkflowModelResetPassword.WF_PROP_USERNAME_ACTIVITI);
+ final String toEmail = (String) variables.get(WorkflowModelResetPassword.WF_PROP_USER_EMAIL_ACTIVITI);
+ final String clientName = (String) variables.get(WorkflowModelResetPassword.WF_PROP_CLIENT_NAME_ACTIVITI);
+ final String key = (String) variables.get(WorkflowModelResetPassword.WF_PROP_KEY_ACTIVITI);
+ final String id = execution.getProcessInstanceId();
+
+ final ClientApp clientApp = getClientAppConfig(clientName);
+ Map emailTemplateModel = Collections.singletonMap(FTL_RESET_PASSWORD_URL,
+ createResetPasswordUrl(clientApp, id, key));
+
+ final String templatePath = emailHelper.getEmailTemplate(clientName,
+ getResetPasswordEmailTemplate(clientApp),
+ fallbackEmailTemplatePath);
+
+ ResetPasswordEmailDetails emailRequest = new ResetPasswordEmailDetails()
+ .setUserName(userName)
+ .setUserEmail(toEmail)
+ .setTemplatePath(templatePath)
+ .setTemplateAssetsUrl(clientApp.getTemplateAssetsUrl())
+ .setEmailSubject(emailSubject)
+ .setTemplateModel(emailTemplateModel);
+
+ sendEmail(emailRequest);
+ }
+
+ @Override
+ public void performResetPassword(DelegateExecution execution, String fallbackEmailTemplatePath, String emailSubject)
+ {
+ // This method chooses to take a rather indirect route to access the password value.
+ // This is for security reasons. We do not want to store the password in the Activiti DB.
+
+ // We can get the username from the execution (process scope).
+ Map variables = execution.getVariables();
+ final String userName = (String) variables.get(WorkflowModelResetPassword.WF_PROP_USERNAME_ACTIVITI);
+ final String userEmail = (String) variables.get(WorkflowModelResetPassword.WF_PROP_USER_EMAIL_ACTIVITI);
+ final String clientName = (String) variables.get(WorkflowModelResetPassword.WF_PROP_CLIENT_NAME_ACTIVITI);
+
+ // But we cannot get the password from the execution as we have intentionally not stored the password there.
+ // Instead we recover the password from the specific task in which it was set.
+ List activitiTasks = activitiTaskService.createTaskQuery().taskDefinitionKey(WorkflowModelResetPassword.TASK_RESET_PASSWORD)
+ .processInstanceId(execution.getProcessInstanceId()).list();
+ if (activitiTasks.size() != 1)
+ {
+ throw new ResetPasswordWorkflowException("Unexpected count of task instances: " + activitiTasks.size());
+ }
+ Task activitiTask = activitiTasks.get(0);
+ String activitiTaskId = activitiTask.getId();
+ final String password = (String) activitiTaskService.getVariable(activitiTaskId, WorkflowModelResetPassword.WF_PROP_PASSWORD_ACTIVITI);
+
+ if (LOGGER.isDebugEnabled())
+ {
+ LOGGER.debug("Retrieved new password from task " + activitiTaskId);
+ }
+
+ ParameterCheck.mandatoryString(WorkflowModelResetPassword.WF_PROP_USERNAME_ACTIVITI, userName);
+ ParameterCheck.mandatoryString(WorkflowModelResetPassword.WF_PROP_PASSWORD_ACTIVITI, password);
+
+ if (LOGGER.isDebugEnabled())
+ {
+ LOGGER.debug("Changing password for " + userName);
+ // Don't LOG the password. :)
+ }
+
+ this.authenticationService.setAuthentication(userName, password.toCharArray());
+
+ // Now notify the user
+ final ClientApp clientApp = getClientAppConfig(clientName);
+ Map emailTemplateModel = Collections.singletonMap(FTL_USER_NAME, userName);
+
+ final String templatePath = emailHelper.getEmailTemplate(clientName,
+ getConfirmResetPasswordEmailTemplate(clientApp),
+ fallbackEmailTemplatePath);
+
+ ResetPasswordEmailDetails emailRequest = new ResetPasswordEmailDetails()
+ .setUserName(userName)
+ .setUserEmail(userEmail)
+ .setTemplatePath(templatePath)
+ .setTemplateAssetsUrl(clientApp.getTemplateAssetsUrl())
+ .setEmailSubject(emailSubject)
+ .setTemplateModel(emailTemplateModel);
+
+ sendEmail(emailRequest);
+ }
+
+ @Override
+ public void sendEmail(ResetPasswordEmailDetails emailRequest)
+ {
+ // Prepare the email
+ Map templateModel = new HashMap<>();
+ // Replace '${shareUrl}' placeholder if it does exist.
+ final String templateAssetsUrl = getUrl(emailRequest.getTemplateAssetsUrl(), ClientAppConfig.PROP_TEMPLATE_ASSETS_URL);
+ templateModel.put(FTL_TEMPLATE_ASSETS_URL, templateAssetsUrl);
+ if (emailRequest.getTemplateModel() != null)
+ {
+ templateModel.putAll(emailRequest.getTemplateModel());
+ }
+
+ Map actionParams = new HashMap<>(8);
+ String fromEmail = emailRequest.getFromEmail();
+ if(StringUtils.isEmpty(fromEmail))
+ {
+ fromEmail = this.defaultEmailSender;
+ }
+ actionParams.put(MailActionExecuter.PARAM_FROM, fromEmail);
+ actionParams.put(MailActionExecuter.PARAM_TO, emailRequest.getUserEmail());
+ actionParams.put(MailActionExecuter.PARAM_SUBJECT, emailRequest.getEmailSubject());
+ // Pick the template
+ actionParams.put(MailActionExecuter.PARAM_TEMPLATE, emailRequest.getTemplatePath());
+ actionParams.put(MailActionExecuter.PARAM_TEMPLATE_MODEL, (Serializable) templateModel);
+
+ final Locale locale = emailHelper.getUserLocaleOrDefault(emailRequest.getUserName());
+ actionParams.put(MailActionExecuter.PARAM_LOCALE, locale);
+
+ actionParams.put(MailActionExecuter.PARAM_IGNORE_SEND_FAILURE, true);
+ actionParams.put(MailActionExecuter.PARAM_IGNORE_SEND_FAILURE, emailRequest.ignoreSendFailure);
+ // Now send the email
+ Action mailAction = actionService.createAction(MailActionExecuter.NAME, actionParams);
+ actionService.executeAction(mailAction, null, false, true);
+ }
+
+ private String getUrl(String url, String propName)
+ {
+ if (url == null)
+ {
+ LOGGER.warn("The url for the property [" + propName + "] is not configured.");
+ return "";
+ }
+
+ if (url.endsWith("/"))
+ {
+ url = url.substring(0, url.length() - 1);
+ }
+ return UrlUtil.replaceShareUrlPlaceholder(url, sysAdminParams);
+ }
+
+ protected String getResetPasswordEmailTemplate(ClientApp clientApp)
+ {
+ return clientApp.getProperty("requestResetPasswordTemplatePath");
+ }
+
+ protected String getConfirmResetPasswordEmailTemplate(ClientApp clientApp)
+ {
+ return clientApp.getProperty("confirmResetPasswordTemplatePath");
+ }
+
+ /**
+ * This method creates a URL for the 'reset password' link which appears in the email
+ */
+ protected String createResetPasswordUrl(ClientApp clientApp, final String id, final String key)
+ {
+ StringBuilder sb = new StringBuilder(100);
+
+ String pageUrl = clientApp.getProperty("resetPasswordPageUrl");
+ if (StringUtils.isEmpty(pageUrl))
+ {
+ sb.append(UrlUtil.getShareUrl(sysAdminParams));
+
+ LOGGER.warn("'resetPasswordPageUrl' property is not set for the client [" + clientApp.getName()
+ + "]. The default base url of Share will be used [" + sb.toString() + "]");
+ }
+ else
+ {
+ // We pass an empty string as we know that the pageUrl is not null
+ sb.append(getUrl(pageUrl, ""));
+ }
+
+ sb.append("?key=").append(key)
+ .append("&id=").append(BPMEngineRegistry.createGlobalId(ActivitiConstants.ENGINE_ID, id));
+
+ return sb.toString();
+ }
+
+ /**
+ * @author Jamal Kaabi-Mofrad
+ */
+ public static class ResetPasswordDetails
+ {
+ private String userId;
+ private String password;
+ private String workflowId;
+ private String workflowKey;
+
+ public String getUserId()
+ {
+ return userId;
+ }
+
+ public ResetPasswordDetails setUserId(String userId)
+ {
+ this.userId = userId;
+ return this;
+ }
+
+ public String getPassword()
+ {
+ return password;
+ }
+
+ public ResetPasswordDetails setPassword(String password)
+ {
+ this.password = password;
+ return this;
+ }
+
+ public String getWorkflowId()
+ {
+ return workflowId;
+ }
+
+ public ResetPasswordDetails setWorkflowId(String workflowId)
+ {
+ this.workflowId = workflowId;
+ return this;
+ }
+
+ public String getWorkflowKey()
+ {
+ return workflowKey;
+ }
+
+ public ResetPasswordDetails setWorkflowKey(String workflowKey)
+ {
+ this.workflowKey = workflowKey;
+ return this;
+ }
+
+ @Override
+ public String toString()
+ {
+ final StringBuilder sb = new StringBuilder(100);
+ sb.append("ResetPasswordDetails [userId=").append(userId)
+ .append(", workflowId=").append(workflowId)
+ .append(", workflowKey=").append(workflowKey)
+ .append(']');
+ return sb.toString();
+ }
+ }
+
+ /**
+ * @author Jamal Kaabi-Mofrad
+ */
+ public static class ResetPasswordEmailDetails
+ {
+ private String userName;
+ private String userEmail;
+ private String fromEmail;
+ private String templatePath;
+ private String templateAssetsUrl;
+ private Map templateModel;
+ private String emailSubject;
+ private boolean ignoreSendFailure = true;
+
+ public String getUserName()
+ {
+ return userName;
+ }
+
+ public ResetPasswordEmailDetails setUserName(String userName)
+ {
+ this.userName = userName;
+ return this;
+ }
+
+ public String getUserEmail()
+ {
+ return userEmail;
+ }
+
+ public ResetPasswordEmailDetails setUserEmail(String userEmail)
+ {
+ this.userEmail = userEmail;
+ return this;
+ }
+
+ public String getFromEmail()
+ {
+ return fromEmail;
+ }
+
+ public ResetPasswordEmailDetails setFromEmail(String fromEmail)
+ {
+ this.fromEmail = fromEmail;
+ return this;
+ }
+
+ public String getTemplatePath()
+ {
+ return templatePath;
+ }
+
+ public ResetPasswordEmailDetails setTemplatePath(String templatePath)
+ {
+ this.templatePath = templatePath;
+ return this;
+ }
+
+ public String getTemplateAssetsUrl()
+ {
+ return templateAssetsUrl;
+ }
+
+ public ResetPasswordEmailDetails setTemplateAssetsUrl(String templateAssetsUrl)
+ {
+ this.templateAssetsUrl = templateAssetsUrl;
+ return this;
+ }
+
+ public Map getTemplateModel()
+ {
+ return templateModel;
+ }
+
+ public ResetPasswordEmailDetails setTemplateModel(Map templateModel)
+ {
+ this.templateModel = templateModel;
+ return this;
+ }
+
+ public String getEmailSubject()
+ {
+ return emailSubject;
+ }
+
+ public ResetPasswordEmailDetails setEmailSubject(String emailSubject)
+ {
+ this.emailSubject = emailSubject;
+ return this;
+ }
+
+ public boolean isIgnoreSendFailure()
+ {
+ return ignoreSendFailure;
+ }
+
+ public ResetPasswordEmailDetails setIgnoreSendFailure(boolean ignoreSendFailure)
+ {
+ this.ignoreSendFailure = ignoreSendFailure;
+ return this;
+ }
+
+ @Override
+ public String toString()
+ {
+ final StringBuilder sb = new StringBuilder(250);
+ sb.append("ResetPasswordEmailDetails [userName=").append(userName)
+ .append(", userEmail=").append(userEmail)
+ .append(", fromEmail=").append(fromEmail)
+ .append(", templatePath=").append(templatePath)
+ .append(", templateAssetsUrl=").append(templateAssetsUrl)
+ .append(", templateModel=").append(templateModel)
+ .append(", emailSubject=").append(emailSubject)
+ .append(", ignoreSendFailure=").append(ignoreSendFailure)
+ .append(']');
+ return sb.toString();
+ }
+ }
+
+ /**
+ * @author Jamal Kaabi-Mofrad
+ * @since 5.2.1
+ */
+ public static class ResetPasswordWorkflowException extends AlfrescoRuntimeException
+ {
+ private static final long serialVersionUID = -694208478609278943L;
+
+ public ResetPasswordWorkflowException(String msgId)
+ {
+ super(msgId);
+ }
+ }
+
+ /**
+ * @author Jamal Kaabi-Mofrad
+ * @since 5.2.1
+ */
+ public static class ResetPasswordWorkflowNotFoundException extends ResetPasswordWorkflowException
+ {
+ private static final long serialVersionUID = -7492264073778098895L;
+
+ public ResetPasswordWorkflowNotFoundException(String msgId)
+ {
+ super(msgId);
+ }
+ }
+
+ /**
+ * @author Jamal Kaabi-Mofrad
+ * @since 5.2.1
+ */
+ public static class InvalidResetPasswordWorkflowException extends ResetPasswordWorkflowException
+ {
+ private static final long serialVersionUID = -4685359036247580984L;
+
+ public InvalidResetPasswordWorkflowException(String msgId)
+ {
+ super(msgId);
+ }
+ }
+
+ /**
+ * @author Jamal Kaabi-Mofrad
+ * @since 5.2.1
+ */
+ public static class ResetPasswordWorkflowInvalidUserException extends ResetPasswordWorkflowException
+ {
+ private static final long serialVersionUID = -6524046975575636256L;
+
+ public ResetPasswordWorkflowInvalidUserException(String msgId)
+ {
+ super(msgId);
+ }
+ }
+}
diff --git a/source/java/org/alfresco/repo/security/authentication/activiti/AbstractResetPasswordDelegate.java b/source/java/org/alfresco/repo/security/authentication/activiti/AbstractResetPasswordDelegate.java
new file mode 100644
index 0000000000..8028d07526
--- /dev/null
+++ b/source/java/org/alfresco/repo/security/authentication/activiti/AbstractResetPasswordDelegate.java
@@ -0,0 +1,44 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2017 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+
+package org.alfresco.repo.security.authentication.activiti;
+
+import org.alfresco.repo.security.authentication.ResetPasswordService;
+import org.alfresco.repo.workflow.activiti.BaseJavaDelegate;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ * @since 5.2.1
+ */
+public abstract class AbstractResetPasswordDelegate extends BaseJavaDelegate
+{
+ protected ResetPasswordService resetPasswordService;
+
+ public void setResetPasswordService(ResetPasswordService resetPasswordService)
+ {
+ this.resetPasswordService = resetPasswordService;
+ }
+}
diff --git a/source/java/org/alfresco/repo/security/authentication/activiti/PerformResetPasswordDelegate.java b/source/java/org/alfresco/repo/security/authentication/activiti/PerformResetPasswordDelegate.java
new file mode 100644
index 0000000000..722aab3f62
--- /dev/null
+++ b/source/java/org/alfresco/repo/security/authentication/activiti/PerformResetPasswordDelegate.java
@@ -0,0 +1,48 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+
+package org.alfresco.repo.security.authentication.activiti;
+
+import org.activiti.engine.delegate.DelegateExecution;
+import org.activiti.engine.delegate.JavaDelegate;
+
+/**
+ * This {@link JavaDelegate activiti delegate} is executed when a user resets his/her password.
+ *
+ * @author Jamal Kaabi-Mofrad
+ * @since 5.2.1
+ */
+public class PerformResetPasswordDelegate extends AbstractResetPasswordDelegate
+{
+ private static final String EMAIL_SUBJECT_KEY = "reset-password-confirmation.email.subject";
+ private static final String EMAIL_TEMPLATE_PATH = "alfresco/templates/reset-password-email-templates/reset-password-confirmation-email-template.ftl";
+
+ @Override
+ public void execute(DelegateExecution execution) throws Exception
+ {
+ resetPasswordService.performResetPassword(execution, EMAIL_TEMPLATE_PATH, EMAIL_SUBJECT_KEY);
+ }
+}
diff --git a/source/java/org/alfresco/repo/security/authentication/activiti/SendResetPasswordEmailDelegate.java b/source/java/org/alfresco/repo/security/authentication/activiti/SendResetPasswordEmailDelegate.java
new file mode 100644
index 0000000000..5948f10e50
--- /dev/null
+++ b/source/java/org/alfresco/repo/security/authentication/activiti/SendResetPasswordEmailDelegate.java
@@ -0,0 +1,48 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+
+package org.alfresco.repo.security.authentication.activiti;
+
+import org.activiti.engine.delegate.DelegateExecution;
+import org.activiti.engine.delegate.JavaDelegate;
+
+/**
+ * This {@link JavaDelegate activiti delegate} is executed when a user request password reset.
+ *
+ * @author Jamal Kaabi-Mofrad
+ * @since 5.2.1
+ */
+public class SendResetPasswordEmailDelegate extends AbstractResetPasswordDelegate
+{
+ private static final String EMAIL_SUBJECT_KEY = "reset-password-request.email.subject";
+ private static final String EMAIL_TEMPLATE_PATH = "alfresco/templates/reset-password-email-templates/reset-password-email-template.ftl";
+
+ @Override
+ public void execute(DelegateExecution execution) throws Exception
+ {
+ resetPasswordService.sendResetPasswordEmail(execution, EMAIL_TEMPLATE_PATH, EMAIL_SUBJECT_KEY);
+ }
+}
diff --git a/source/java/org/alfresco/repo/workflow/WorkflowModelResetPassword.java b/source/java/org/alfresco/repo/workflow/WorkflowModelResetPassword.java
new file mode 100644
index 0000000000..c2795b06f0
--- /dev/null
+++ b/source/java/org/alfresco/repo/workflow/WorkflowModelResetPassword.java
@@ -0,0 +1,62 @@
+/*
+ * #%L
+ * Alfresco Remote API
+ * %%
+ * Copyright (C) 2005 - 2016 Alfresco Software Limited
+ * %%
+ * This file is part of the Alfresco software.
+ * If the software was purchased under a paid Alfresco license, the terms of
+ * the paid license agreement will prevail. Otherwise, the software is
+ * provided under the following open source license terms:
+ *
+ * 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 .
+ * #L%
+ */
+
+package org.alfresco.repo.workflow;
+
+import org.alfresco.service.namespace.QName;
+
+/**
+ * @author Jamal Kaabi-Mofrad
+ * @since 5.2.1
+ */
+public interface WorkflowModelResetPassword
+{
+ // namespace
+ String NAMESPACE_URI = "http://www.alfresco.org/model/workflow/resetpassword/1.0";
+
+ // process name
+ String WORKFLOW_DEFINITION_NAME = "activiti$resetPassword";
+
+ // task names
+ String TASK_RESET_PASSWORD = "resetPasswordTask";
+
+ // timers
+ QName WF_PROP_TIMER_END = QName.createQName(NAMESPACE_URI, "endTimer");
+
+ // workflow properties
+ QName WF_PROP_USERNAME = QName.createQName(NAMESPACE_URI, "userName");
+ QName WF_PROP_USER_EMAIL = QName.createQName(NAMESPACE_URI, "userEmail");
+ QName WF_PROP_KEY = QName.createQName(NAMESPACE_URI, "key");
+ QName WF_PROP_PASSWORD = QName.createQName(NAMESPACE_URI, "password");
+ QName WF_PROP_CLIENT_NAME = QName.createQName(NAMESPACE_URI, "clientName");
+
+ // workflow execution context variable names
+ String WF_PROP_USERNAME_ACTIVITI = "resetpasswordwf_userName";
+ String WF_PROP_USER_EMAIL_ACTIVITI = "resetpasswordwf_userEmail";
+ String WF_PROP_KEY_ACTIVITI = "resetpasswordwf_key";
+ String WF_PROP_PASSWORD_ACTIVITI = "resetpasswordwf_password";
+ String WF_PROP_CLIENT_NAME_ACTIVITI = "resetpasswordwf_clientName";
+}