diff --git a/repository/src/main/java/org/alfresco/repo/client/config/ClientAppConfig.java b/repository/src/main/java/org/alfresco/repo/client/config/ClientAppConfig.java index b6c651b3a4..bc6df000d8 100644 --- a/repository/src/main/java/org/alfresco/repo/client/config/ClientAppConfig.java +++ b/repository/src/main/java/org/alfresco/repo/client/config/ClientAppConfig.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2017 Alfresco Software Limited + * Copyright (C) 2005 - 2023 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -26,10 +26,12 @@ package org.alfresco.repo.client.config; +import org.alfresco.repo.admin.SysAdminParams; import org.alfresco.util.PropertyCheck; +import org.alfresco.util.UrlUtil; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEvent; import org.springframework.extensions.surf.util.AbstractLifecycleBean; @@ -41,6 +43,7 @@ import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.regex.Pattern; /** * This class picks up all the loaded properties passed to it and uses a naming @@ -59,25 +62,35 @@ import java.util.concurrent.ConcurrentMap; * for example, sharedLinkBaseUrl and templateAssetsUrl properties, then the following * needs to be put into a properties file. * * The default property file is alfresco/client/config/repo-clients-apps.properties which * could be overridden (or add new clients) by alfresco-global properties file. + *

+ * Note: The {@literal Url} is a special property which can be used as a placeholder. + * For example, + *

+ *

* * @author Jamal Kaabi-Mofrad */ public class ClientAppConfig extends AbstractLifecycleBean { - private static final Log logger = LogFactory.getLog(ClientAppConfig.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ClientAppConfig.class); public static final String PREFIX = "repo.client-app."; public static final String PROP_TEMPLATE_ASSETS_URL = "templateAssetsUrl"; + public static final String SHARE_PLACEHOLDER = "${shareUrl}"; + private Properties defaultProperties; private Properties globalProperties; - private ConcurrentMap clients = new ConcurrentHashMap<>(); + private final ConcurrentMap clients = new ConcurrentHashMap<>(); public ClientAppConfig() { @@ -117,6 +130,17 @@ public class ClientAppConfig extends AbstractLifecycleBean return clients.get(name); } + /** + * Checks whether a client with the given name exists or not. + * + * @param name the name of the client to check + * @return true if the client with the given name exists; false otherwise + */ + public boolean exists(String name) + { + return clients.containsKey(name); + } + @Override protected void onBootstrap(ApplicationEvent event) { @@ -127,9 +151,9 @@ public class ClientAppConfig extends AbstractLifecycleBean processPropertyKeys(mergedProperties, clientsNames, propsNames); clients.putAll(processClients(clientsNames, propsNames, mergedProperties)); - if (logger.isDebugEnabled()) + if (LOGGER.isDebugEnabled()) { - logger.debug("All bootstrapped repo clients apps: " + clients); + LOGGER.debug("All bootstrapped repo clients apps: " + clients); } } @@ -232,7 +256,7 @@ public class ClientAppConfig extends AbstractLifecycleBean } if (StringUtils.isEmpty(templateAssetsUrl) && config.isEmpty()) { - logger.warn("Client-app [" + clientName + "] can not be registered as it needs at least one property with a valid value."); + LOGGER.warn("Client-app [" + clientName + "] can not be registered as it needs at least one property with a valid value."); continue; } // As the required values are valid, create the client data @@ -275,7 +299,7 @@ public class ClientAppConfig extends AbstractLifecycleBean private void logMalformedPropertyKey(String propName, String reason) { reason = (StringUtils.isBlank(reason)) ? "" : " " + reason; - logger.warn("Ignoring client app config (malformed property key) [" + propName + "]." + reason); + LOGGER.warn("Ignoring client app config (malformed property key) [" + propName + "]." + reason); } private void logMalformedPropertyKey(String propName) @@ -285,9 +309,9 @@ public class ClientAppConfig extends AbstractLifecycleBean private void logInvalidPropertyValue(String propName, String propValue) { - if (logger.isDebugEnabled()) + if (LOGGER.isDebugEnabled()) { - logger.debug("Ignoring client app config (invalid value) [" + propValue + "] for the property:" + propName); + LOGGER.debug("Ignoring client app config (invalid value) [" + propValue + "] for the property:" + propName); } } @@ -298,13 +322,18 @@ public class ClientAppConfig extends AbstractLifecycleBean public static class ClientApp { + private static final Logger LOGGER = LoggerFactory.getLogger(ClientApp.class); private final String name; + private final String clientUrlPropKey; + private final Pattern clientUrlPlaceholderPattern; private final String templateAssetsUrl; private final Map properties; public ClientApp(String name, String templateAssetsUrl, Map properties) { this.name = name; + this.clientUrlPropKey = name + "Url"; // E.g. 'workspaceUrl' in 'repo.client-app.workspace.workspaceUrl' + this.clientUrlPlaceholderPattern = Pattern.compile("\\$\\{" + clientUrlPropKey + '}'); // E.g. ${workspaceUrl} this.templateAssetsUrl = templateAssetsUrl; this.properties = new HashMap<>(properties); } @@ -314,11 +343,77 @@ public class ClientAppConfig extends AbstractLifecycleBean return name; } + public String getClientUrlPropKey() + { + return clientUrlPropKey; + } + + public String getClientUrl() + { + String url = properties.get(getClientUrlPropKey()); + if (StringUtils.isEmpty(url) && "share".equals(name)) + { + return SHARE_PLACEHOLDER; + } + return url; + } + + /** + * Resolves the client URL that has one of the following defined URL placeholders: + *
    + *
  • {@literal ${repoBaseUrl}}
  • + *
  • {@literal ${shareUrl}}
  • + *
+ * + * @param sysAdminParams SysAdminParams object to retrieve configurable system parameters + * @return the resolved property value if applicable + */ + public String getResolvedClientUrl(SysAdminParams sysAdminParams) + { + String resolvedUrl; + String clientUrl = getClientUrl(); + if (!StringUtils.isEmpty(clientUrl) && clientUrl.contains(SHARE_PLACEHOLDER)) + { + resolvedUrl = UrlUtil.replaceShareUrlPlaceholder(clientUrl, sysAdminParams); + } + else + { + resolvedUrl = UrlUtil.replaceRepoBaseUrlPlaceholder(clientUrl, sysAdminParams); + } + + LOGGER.debug("Resolved clientUrl [" + clientUrl + "] to [" + resolvedUrl + "] for the client: " + name); + return resolvedUrl; + } + + public Pattern getClientUrlPlaceholderPattern() + { + return clientUrlPlaceholderPattern; + } + public String getTemplateAssetsUrl() { return templateAssetsUrl; } + /** + * Resolves the template assets URL that has the following defined URL placeholder: + *
    + *
  • {@literal ${Url}}
  • + *
+ * + * @param sysAdminParams SysAdminParams object to retrieve configurable system parameters + * @return the resolved property value if applicable + */ + public String getResolvedTemplateAssetsUrl(SysAdminParams sysAdminParams) + { + String resolvedUrl = UrlUtil.replaceUrlPlaceholder(getClientUrlPlaceholderPattern(), getTemplateAssetsUrl(), + getResolvedClientUrl(sysAdminParams)); + + LOGGER.debug("Resolved template assets [" + getTemplateAssetsUrl() + "] URL to [" + resolvedUrl + + "] for the client: " + name); + return resolvedUrl; + } + public Map getProperties() { return Collections.unmodifiableMap(properties); @@ -329,6 +424,27 @@ public class ClientAppConfig extends AbstractLifecycleBean return properties.get(propName); } + /** + * Resolves the property that has the following defined URL placeholder: + *
    + *
  • {@literal ${Url}}
  • + *
+ * + * @param propName the property name to search for + * @param sysAdminParams SysAdminParams object to retrieve configurable system parameters + * @return the resolved property value if applicable + */ + public String getResolvedProperty(String propName, SysAdminParams sysAdminParams) + { + String property = getProperty(propName); + String resolvedProp = UrlUtil.replaceUrlPlaceholder(getClientUrlPlaceholderPattern(), property, + getResolvedClientUrl(sysAdminParams)); + + LOGGER.debug("Resolved [" + property + "] URL to [" + resolvedProp + "] for the [" + propName + + "] property of the client: " + name); + return resolvedProp; + } + @Override public boolean equals(Object o) { @@ -356,6 +472,8 @@ public class ClientAppConfig extends AbstractLifecycleBean { final StringBuilder sb = new StringBuilder(250); sb.append("ClientApp [name=").append(name) + .append(", clientUrlPropKey=").append(clientUrlPropKey) + .append(", clientUrlPlaceholderPattern=").append(clientUrlPlaceholderPattern.pattern()) .append(", templateAssetsUrl=").append(templateAssetsUrl) .append(", properties=").append(properties) .append(']'); diff --git a/repository/src/main/java/org/alfresco/repo/invitation/InvitationServiceImpl.java b/repository/src/main/java/org/alfresco/repo/invitation/InvitationServiceImpl.java index bb8ea5d91d..87737da627 100644 --- a/repository/src/main/java/org/alfresco/repo/invitation/InvitationServiceImpl.java +++ b/repository/src/main/java/org/alfresco/repo/invitation/InvitationServiceImpl.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2023 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -1875,7 +1875,6 @@ public class InvitationServiceImpl implements InvitationService, NodeServicePoli * @param siteName * @param role * @param runAsUser - * @param siteService * @param overrideExisting */ public void addSiteMembership(final String invitee, final String siteName, final String role, final String runAsUser, final boolean overrideExisting) @@ -2111,11 +2110,11 @@ public class InvitationServiceImpl implements InvitationService, NodeServicePoli workflowProps.put(WorkflowModelModeratedInvitation.WF_PROP_RESOURCE_TYPE, resourceType.toString()); workflowProps.put(WorkflowModelModeratedInvitation.WF_PROP_CLIENT_NAME, clientName); - if(clientName != null && clientAppConfig.getClient(clientName) != null) + if(clientName != null && clientAppConfig.exists(clientName)) { ClientAppConfig.ClientApp client = clientAppConfig.getClient(clientName); - workflowProps.put(WorkflowModelModeratedInvitation.WF_TEMPLATE_ASSETS_URL, client.getTemplateAssetsUrl()); - workflowProps.put(WorkflowModelModeratedInvitation.WF_WORKSPACE_URL, client.getProperty("workspaceUrl")); + workflowProps.put(WorkflowModelModeratedInvitation.WF_TEMPLATE_ASSETS_URL, client.getResolvedTemplateAssetsUrl(sysAdminParams)); + workflowProps.put(WorkflowModelModeratedInvitation.WF_WORKSPACE_URL, client.getResolvedClientUrl(sysAdminParams)); } // get the moderated workflow diff --git a/repository/src/main/java/org/alfresco/repo/invitation/activiti/SendModeratedInviteDelegate.java b/repository/src/main/java/org/alfresco/repo/invitation/activiti/SendModeratedInviteDelegate.java index add6dd7557..bea8a969ab 100644 --- a/repository/src/main/java/org/alfresco/repo/invitation/activiti/SendModeratedInviteDelegate.java +++ b/repository/src/main/java/org/alfresco/repo/invitation/activiti/SendModeratedInviteDelegate.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2016 Alfresco Software Limited + * Copyright (C) 2005 - 2023 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -72,7 +72,7 @@ public class SendModeratedInviteDelegate extends AbstractInvitationDelegate Map variables = execution.getVariables(); String clientName = (String) variables.get(WorkflowModelModeratedInvitation.wfVarClientName); - if(clientName != null && clientAppConfig.getClient(clientName) != null) + if(clientName != null && clientAppConfig.exists(clientName)) { ClientAppConfig.ClientApp clientApp = clientAppConfig.getClient(clientName); final String path = clientApp.getProperty("inviteModeratedTemplatePath"); diff --git a/repository/src/main/java/org/alfresco/repo/quickshare/QuickShareServiceImpl.java b/repository/src/main/java/org/alfresco/repo/quickshare/QuickShareServiceImpl.java index 2102a2bf93..50eccb8849 100644 --- a/repository/src/main/java/org/alfresco/repo/quickshare/QuickShareServiceImpl.java +++ b/repository/src/main/java/org/alfresco/repo/quickshare/QuickShareServiceImpl.java @@ -2,7 +2,7 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2017 Alfresco Software Limited + * Copyright (C) 2005 - 2023 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of @@ -934,13 +934,13 @@ public class QuickShareServiceImpl implements QuickShareService, Map templateModel = new HashMap<>(6); templateModel.put(FTL_SENDER_FIRST_NAME, senderFirstName); templateModel.put(FTL_SENDER_LAST_NAME, senderLastName); - final String sharedNodeUrl = getUrl(clientApp.getProperty(CONFIG_SHARED_LINK_BASE_URL), CONFIG_SHARED_LINK_BASE_URL) - + '/' + emailRequest.getSharedId(); + String resolvedUrl = getUrl(clientApp.getResolvedProperty(CONFIG_SHARED_LINK_BASE_URL, sysAdminParams), CONFIG_SHARED_LINK_BASE_URL); + final String sharedNodeUrl = resolvedUrl + '/' + emailRequest.getSharedId(); templateModel.put(FTL_SHARED_NODE_URL, sharedNodeUrl); templateModel.put(FTL_SHARED_NODE_NAME, emailRequest.getSharedNodeName()); templateModel.put(FTL_SENDER_MESSAGE, emailRequest.getSenderMessage()); - final String templateAssetsUrl = getUrl(clientApp.getTemplateAssetsUrl(), ClientAppConfig.PROP_TEMPLATE_ASSETS_URL); + final String templateAssetsUrl = getUrl(clientApp.getResolvedTemplateAssetsUrl(sysAdminParams), ClientAppConfig.PROP_TEMPLATE_ASSETS_URL); templateModel.put(FTL_TEMPLATE_ASSETS_URL, templateAssetsUrl); // Set the email details @@ -1048,8 +1048,7 @@ public class QuickShareServiceImpl implements QuickShareService, { url = url.substring(0, url.length() - 1); } - // Replace '${shareUrl} placeholder if it does exist. - return UrlUtil.replaceShareUrlPlaceholder(url, sysAdminParams); + return url; } private T getDefaultIfNull(T defaultValue, T newValue) diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/ResetPasswordService.java b/repository/src/main/java/org/alfresco/repo/security/authentication/ResetPasswordService.java index b8781b2486..d7f1fe3629 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/ResetPasswordService.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/ResetPasswordService.java @@ -32,13 +32,11 @@ import org.alfresco.repo.client.config.ClientAppNotFoundException; import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetPasswordDetails; /** - * @deprecated from 7.1.0 * Reset password service. * * @author Jamal Kaabi-Mofrad * @since 5.2.1 */ -@Deprecated public interface ResetPasswordService { /** diff --git a/repository/src/main/java/org/alfresco/repo/security/authentication/ResetPasswordServiceImpl.java b/repository/src/main/java/org/alfresco/repo/security/authentication/ResetPasswordServiceImpl.java index 530002af89..c74e8cf738 100644 --- a/repository/src/main/java/org/alfresco/repo/security/authentication/ResetPasswordServiceImpl.java +++ b/repository/src/main/java/org/alfresco/repo/security/authentication/ResetPasswordServiceImpl.java @@ -2,23 +2,23 @@ * #%L * Alfresco Remote API * %% - * Copyright (C) 2005 - 2017 Alfresco Software Limited + * Copyright (C) 2005 - 2023 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 + * 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% @@ -61,8 +61,8 @@ import org.alfresco.util.ParameterCheck; import org.alfresco.util.PropertyCheck; import org.alfresco.util.UrlUtil; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.extensions.surf.util.I18NUtil; import org.springframework.extensions.webscripts.WebScriptException; @@ -74,17 +74,15 @@ import java.util.Locale; import java.util.Map; /** - * @deprecated from 7.1.0 - * * + * * Reset password implementation based on workflow. * * @author Jamal Kaabi-Mofrad * @since 5.2.1 */ -@Deprecated public class ResetPasswordServiceImpl implements ResetPasswordService { - private static final Log LOGGER = LogFactory.getLog(ResetPasswordServiceImpl.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ResetPasswordServiceImpl.class); private static final String TIMER_END = "PT1H"; private static final String WORKFLOW_DESCRIPTION_KEY = "resetpasswordwf_resetpassword.resetpassword.workflow.description"; @@ -196,6 +194,7 @@ public class ResetPasswordServiceImpl implements ResetPasswordService ParameterCheck.mandatoryString("clientName", clientName); String userEmail = validateUserAndGetEmail(userId); + validateClient(clientName); // Get the (latest) workflow definition for reset-password. WorkflowDefinition wfDefinition = workflowService.getDefinitionByName(WorkflowModelResetPassword.WORKFLOW_DEFINITION_NAME); @@ -342,8 +341,9 @@ public class ResetPasswordServiceImpl implements ResetPasswordService } else if (!username.equals(userId)) { - throw new InvalidResetPasswordWorkflowException("The given user id [" + userId + "] does not match the person's user id [" + username - + "] who requested the password reset."); + throw new InvalidResetPasswordWorkflowException( + "The given user id [" + userId + "] does not match the person's user id [" + username + + "] who requested the password reset."); } } @@ -352,12 +352,16 @@ public class ResetPasswordServiceImpl implements ResetPasswordService { ParameterCheck.mandatoryString("clientName", clientName); - ClientApp clientApp = clientAppConfig.getClient(clientName); - if (clientApp == null) + validateClient(clientName); + return clientAppConfig.getClient(clientName); + } + + private void validateClient(String clientName) + { + if (!clientAppConfig.exists(clientName)) { throw new ClientAppNotFoundException("Client was not found [" + clientName + "]"); } - return clientApp; } @@ -383,9 +387,9 @@ public class ResetPasswordServiceImpl implements ResetPasswordService .setUserName(userName) .setUserEmail(toEmail) .setTemplatePath(templatePath) - .setTemplateAssetsUrl(clientApp.getTemplateAssetsUrl()) .setEmailSubject(emailSubject) - .setTemplateModel(emailTemplateModel); + .setTemplateModel(emailTemplateModel) + .setClientApp(clientApp); sendEmail(emailRequest); } @@ -400,7 +404,7 @@ public class ResetPasswordServiceImpl implements ResetPasswordService final String userName = (String) execution.getVariable(WorkflowModelResetPassword.WF_PROP_USERNAME_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. + // 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) @@ -448,9 +452,9 @@ public class ResetPasswordServiceImpl implements ResetPasswordService .setUserName(userName) .setUserEmail(userEmail) .setTemplatePath(templatePath) - .setTemplateAssetsUrl(clientApp.getTemplateAssetsUrl()) .setEmailSubject(emailSubject) - .setTemplateModel(emailTemplateModel); + .setTemplateModel(emailTemplateModel) + .setClientApp(clientApp); sendEmail(emailRequest); } @@ -460,7 +464,9 @@ public class ResetPasswordServiceImpl implements ResetPasswordService // 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); + ClientApp clientApp = emailRequest.getClientApp(); + final String templateAssetsUrl = getUrl(clientApp.getResolvedTemplateAssetsUrl(sysAdminParams), + ClientAppConfig.PROP_TEMPLATE_ASSETS_URL, clientApp.getName()); templateModel.put(FTL_TEMPLATE_ASSETS_URL, templateAssetsUrl); if (emailRequest.getTemplateModel() != null) { @@ -489,11 +495,11 @@ public class ResetPasswordServiceImpl implements ResetPasswordService actionService.executeAction(mailAction, null, false, sendEmailAsynchronously); } - private String getUrl(String url, String propName) + private String getUrl(String url, String propName, String clientName) { - if (url == null) + if (StringUtils.isEmpty(url)) { - LOGGER.warn("The url for the property [" + propName + "] is not configured."); + LOGGER.warn("The url for the property [" + propName + "] is not configured for the client: " + clientName); return ""; } @@ -501,7 +507,7 @@ public class ResetPasswordServiceImpl implements ResetPasswordService { url = url.substring(0, url.length() - 1); } - return UrlUtil.replaceShareUrlPlaceholder(url, sysAdminParams); + return url; } protected String getResetPasswordEmailTemplate(ClientApp clientApp) @@ -521,22 +527,23 @@ public class ResetPasswordServiceImpl implements ResetPasswordService { StringBuilder sb = new StringBuilder(100); - String pageUrl = clientApp.getProperty("resetPasswordPageUrl"); + String pageUrl = clientApp.getResolvedProperty("resetPasswordPageUrl", sysAdminParams); 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 + "]"); - 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(getUrl(pageUrl, "resetPasswordPageUrl", clientApp.getName())); } - sb.append("?key=").append(key) - .append("&id=").append(BPMEngineRegistry.createGlobalId(ActivitiConstants.ENGINE_ID, id)); + sb.append("?key=") + .append(key) + .append("&id=") + .append(BPMEngineRegistry.createGlobalId(ActivitiConstants.ENGINE_ID, id)); return sb.toString(); } @@ -616,10 +623,10 @@ public class ResetPasswordServiceImpl implements ResetPasswordService private String userEmail; private String fromEmail; private String templatePath; - private String templateAssetsUrl; private Map templateModel; private String emailSubject; private boolean ignoreSendFailure = true; + private ClientApp clientApp; public String getUserName() { @@ -665,17 +672,6 @@ public class ResetPasswordServiceImpl implements ResetPasswordService return this; } - public String getTemplateAssetsUrl() - { - return templateAssetsUrl; - } - - public ResetPasswordEmailDetails setTemplateAssetsUrl(String templateAssetsUrl) - { - this.templateAssetsUrl = templateAssetsUrl; - return this; - } - public Map getTemplateModel() { return templateModel; @@ -709,18 +705,29 @@ public class ResetPasswordServiceImpl implements ResetPasswordService return this; } + public ClientApp getClientApp() + { + return clientApp; + } + + public ResetPasswordEmailDetails setClientApp(ClientApp clientApp) + { + this.clientApp = clientApp; + return this; + } + @Override public String toString() { - final StringBuilder sb = new StringBuilder(250); + final StringBuilder sb = new StringBuilder(300); 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(", clientApp=").append(clientApp) .append(']'); return sb.toString(); } diff --git a/repository/src/main/java/org/alfresco/repo/workflow/WorkflowModelResetPassword.java b/repository/src/main/java/org/alfresco/repo/workflow/WorkflowModelResetPassword.java index c44017ab38..3dc24fe4a7 100644 --- a/repository/src/main/java/org/alfresco/repo/workflow/WorkflowModelResetPassword.java +++ b/repository/src/main/java/org/alfresco/repo/workflow/WorkflowModelResetPassword.java @@ -29,12 +29,10 @@ package org.alfresco.repo.workflow; import org.alfresco.service.namespace.QName; /** - * @deprecated from 7.1.0 * * @author Jamal Kaabi-Mofrad * @since 5.2.1 */ -@Deprecated public interface WorkflowModelResetPassword { // namespace diff --git a/repository/src/main/java/org/alfresco/util/UrlUtil.java b/repository/src/main/java/org/alfresco/util/UrlUtil.java index f040c31858..8d9490d13a 100644 --- a/repository/src/main/java/org/alfresco/util/UrlUtil.java +++ b/repository/src/main/java/org/alfresco/util/UrlUtil.java @@ -2,23 +2,23 @@ * #%L * Alfresco Repository * %% - * Copyright (C) 2005 - 2017 Alfresco Software Limited + * Copyright (C) 2005 - 2023 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 + * 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% @@ -32,43 +32,62 @@ import java.util.regex.Pattern; /** * Alfresco URL related utility functions. - * + * * @since 3.5 */ public class UrlUtil { // ${shareUrl} placeholder public static final Pattern PATTERN = Pattern.compile("\\$\\{shareUrl\\}"); + // ${alfrescoUrl} placeholder public static final Pattern REPO_PATTERN = Pattern.compile("\\$\\{alfrescoUrl\\}"); + + // ${repoBaseUrl} placeholder + public static final Pattern REPO_BASE_PATTERN = Pattern.compile("\\$\\{repoBaseUrl\\}"); + /** - * Builds up the Url to Alfresco based on the settings in the - * {@link SysAdminParams}. - * @return Alfresco Url such as https://col.ab.or.ate/alfresco/ - * or http://localhost:8080/alfresco/ + * Builds up the Url to Alfresco root url based on the settings in the + * {@link SysAdminParams}. + * @return Alfresco base Url such as {@code https://col.ab.or.ate} + * or {@code http://localhost:8080} + */ + public static String getAlfrescoBaseUrl(SysAdminParams sysAdminParams) + { + return buildBaseUrl( + sysAdminParams.getAlfrescoProtocol(), + sysAdminParams.getAlfrescoHost(), + sysAdminParams.getAlfrescoPort()); + } + + /** + * Builds up the Url to Alfresco context based on the settings in the + * {@link SysAdminParams}. + * @return Alfresco Url such as {@code https://col.ab.or.ate/alfresco} + * or {@code http://localhost:8080/alfresco} */ public static String getAlfrescoUrl(SysAdminParams sysAdminParams) { return buildUrl( - sysAdminParams.getAlfrescoProtocol(), - sysAdminParams.getAlfrescoHost(), - sysAdminParams.getAlfrescoPort(), - sysAdminParams.getAlfrescoContext()); + sysAdminParams.getAlfrescoProtocol(), + sysAdminParams.getAlfrescoHost(), + sysAdminParams.getAlfrescoPort(), + sysAdminParams.getAlfrescoContext()); } - + /** - * Builds up the Url to Share based on the settings in the - * {@link SysAdminParams}. - * @return Alfresco Url such as https://col.ab.or.ate/share/ - * or http://localhost:8081/share/ + * Builds up the Url to Share based on the settings in the + * {@link SysAdminParams}. + * @return Alfresco Url such as {@code https://col.ab.or.ate/share} + * or {@code http://localhost:8081/share} */ public static String getShareUrl(SysAdminParams sysAdminParams) { return buildUrl( - sysAdminParams.getShareProtocol(), - sysAdminParams.getShareHost(), - sysAdminParams.getSharePort(), - sysAdminParams.getShareContext()); + sysAdminParams.getShareProtocol(), + sysAdminParams.getShareHost(), + sysAdminParams.getSharePort(), + sysAdminParams.getShareContext()); } @@ -80,8 +99,8 @@ public class UrlUtil /** * Builds URL to Api-Explorer based on the request only if the URL property is not provided * {@link SysAdminParams}. - * @return Rest-Api Url such as https://col.ab.or.ate/api-explorer/ - * or http://localhost:8082/api-explorer/ + * @return Rest-Api Url such as {@code https://col.ab.or.ate/api-explorer} + * or {@code http://localhost:8082/api-explorer} */ public static String getApiExplorerUrl(SysAdminParams sysAdminParams, String requestURL, String requestURI) { @@ -124,6 +143,12 @@ public class UrlUtil } protected static String buildUrl(String protocol, String host, int port, String context) + { + String baseUrl = buildBaseUrl(protocol, host, port); + return baseUrl + '/' + context; + } + + protected static String buildBaseUrl(String protocol, String host, int port) { StringBuilder url = new StringBuilder(); url.append(protocol); @@ -142,8 +167,33 @@ public class UrlUtil url.append(':'); url.append(port); } - url.append('/'); - url.append(context); return url.toString(); } + + /** + * Replaces the repo base url placeholder, namely {@literal ${repoBaseUrl}}, with value based on the settings in the + * {@link SysAdminParams}. + * + * @param value the string value which contains the repoBase url placeholder + * @param sysAdminParams the {@code SysAdminParams} object + * @return if the given {@code value} contains repoBase url placeholder, + * the placeholder is replaced with repoBase url; otherwise, the given {@code value} is simply returned + */ + public static String replaceRepoBaseUrlPlaceholder(String value, SysAdminParams sysAdminParams) + { + if (value != null) + { + return REPO_BASE_PATTERN.matcher(value).replaceAll(getAlfrescoBaseUrl(sysAdminParams)); + } + return value; + } + + public static String replaceUrlPlaceholder(Pattern pattern, String value, String replacement) + { + if (value != null) + { + return pattern.matcher(value).replaceAll(replacement); + } + return null; + } } diff --git a/repository/src/main/resources/alfresco/client/config/repo-clients-apps.properties b/repository/src/main/resources/alfresco/client/config/repo-clients-apps.properties index 16e27a74e1..c2f71a4802 100644 --- a/repository/src/main/resources/alfresco/client/config/repo-clients-apps.properties +++ b/repository/src/main/resources/alfresco/client/config/repo-clients-apps.properties @@ -5,8 +5,8 @@ # a NodeRef of the template # a class path of the template -# template assets url for share client -repo.client-app.share.templateAssetsUrl=${shareUrl}/res/components/images/ +# template assets url for share client. i.e. The source url for the images/logs within the email +repo.client-app.share.templateAssetsUrl=${shareUrl}/res/components/images # shared-link (quickShare) base url repo.client-app.share.sharedLinkBaseUrl=${shareUrl}/s # shared-link email template path @@ -20,6 +20,14 @@ repo.client-app.share.resetPasswordPageUrl=${shareUrl}/page/reset-password repo.client-app.share.confirmResetPasswordTemplatePath= ### Digital workspace template configurations +repo.client-app.workspace.workspaceUrl=${repoBaseUrl}/workspace repo.client-app.workspace.inviteModeratedTemplatePath= -repo.client-app.workspace.workspaceUrl=workspace -repo.client-app.workspace.templateAssetsUrl=${alfrescoUrl}/images +# template assets url for workspace client. i.e. The source url for the images/logs within the email +repo.client-app.workspace.templateAssetsUrl=${workspaceUrl}/images + +# reset password request email template path +repo.client-app.workspace.requestResetPasswordTemplatePath=alfresco/templates/reset-password-email-templates/forgot-password-email-template.ftl +# reset password UI page url +repo.client-app.workspace.resetPasswordPageUrl=${workspaceUrl}/reset-password +# reset password confirmation email template path +repo.client-app.workspace.confirmResetPasswordTemplatePath= diff --git a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java index 999f2e16ca..0f128d287f 100644 --- a/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java +++ b/repository/src/test/java/org/alfresco/AllUnitTestsSuite.java @@ -250,7 +250,8 @@ import org.junit.runners.Suite; org.alfresco.repo.event2.RepoEvent2UnitSuite.class, org.alfresco.util.schemacomp.SchemaDifferenceHelperUnitTest.class, - org.alfresco.repo.tagging.TaggingServiceImplUnitTest.class + org.alfresco.repo.tagging.TaggingServiceImplUnitTest.class, + org.alfresco.repo.client.config.ClientAppConfigUnitTest.class }) public class AllUnitTestsSuite { diff --git a/repository/src/test/java/org/alfresco/repo/client/config/ClientAppConfigTest.java b/repository/src/test/java/org/alfresco/repo/client/config/ClientAppConfigTest.java index b111486301..cce090c81f 100644 --- a/repository/src/test/java/org/alfresco/repo/client/config/ClientAppConfigTest.java +++ b/repository/src/test/java/org/alfresco/repo/client/config/ClientAppConfigTest.java @@ -31,16 +31,12 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import org.alfresco.repo.client.config.ClientAppConfig.ClientApp; -import org.alfresco.service.cmr.repository.TemporalSourceOptions; import org.alfresco.util.ApplicationContextHelper; import org.alfresco.util.testing.category.LuceneTests; -import org.junit.AfterClass; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import org.junit.experimental.categories.Category; import org.springframework.context.ApplicationContext; -import org.springframework.context.support.ClassPathXmlApplicationContext; import java.util.Collections; import java.util.Map; @@ -53,15 +49,15 @@ import java.util.Map; @Category(LuceneTests.class) public class ClientAppConfigTest { - private ApplicationContext context; private ClientAppConfig clientAppConfig; @Before public void setUp() throws Exception { ApplicationContextHelper.closeApplicationContext(); - context = ApplicationContextHelper.getApplicationContext(new String[] { ApplicationContextHelper.CONFIG_LOCATIONS[0], - "classpath:org/alfresco/repo/client/config/test-repo-clients-apps-context.xml" }); + ApplicationContext context = ApplicationContextHelper.getApplicationContext( + new String[] { ApplicationContextHelper.CONFIG_LOCATIONS[0], + "classpath:org/alfresco/repo/client/config/test-repo-clients-apps-context.xml" }); clientAppConfig = context.getBean("clientAppConfigTest", ClientAppConfig.class); } diff --git a/repository/src/test/java/org/alfresco/repo/client/config/ClientAppConfigUnitTest.java b/repository/src/test/java/org/alfresco/repo/client/config/ClientAppConfigUnitTest.java new file mode 100644 index 0000000000..0f431eb880 --- /dev/null +++ b/repository/src/test/java/org/alfresco/repo/client/config/ClientAppConfigUnitTest.java @@ -0,0 +1,266 @@ +/* + * #%L + * Alfresco Repository + * %% + * Copyright (C) 2005 - 2023 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.client.config; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.alfresco.repo.admin.SysAdminParams; +import org.alfresco.repo.client.config.ClientAppConfig.ClientApp; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * @author Jamal Kaabi-Mofrad + */ +@RunWith(MockitoJUnitRunner.class) +public class ClientAppConfigUnitTest +{ + private ClientAppConfig clientAppConfig; + @Mock + private SysAdminParams sysAdminParams; + + private AtomicBoolean initialised; + + @Before + public void setUp() throws Exception + + { // This in not initialised yet. i.e. the properties are not processed yet. + // The processing will start when you call the 'onBootstrap()' method + clientAppConfig = buildClientAppConfig(); + + initialised = new AtomicBoolean(false); + + when(sysAdminParams.getAlfrescoProtocol()).thenReturn("http"); + when(sysAdminParams.getAlfrescoHost()).thenReturn("localhost"); + when(sysAdminParams.getAlfrescoPort()).thenReturn(8080); + + when(sysAdminParams.getShareProtocol()).thenReturn("http"); + when(sysAdminParams.getShareHost()).thenReturn("localhost"); + when(sysAdminParams.getSharePort()).thenReturn(8081); + when(sysAdminParams.getShareContext()).thenReturn("share"); + } + + private ClientAppConfig buildClientAppConfig() + { + Properties defaultProps = getWorkspaceAppProperties(); + defaultProps.putAll(getCoolAppProperties()); + defaultProps.putAll(getShareProperties()); + Properties globalProps = new Properties(); + + ClientAppConfig config = new ClientAppConfig(); + config.setDefaultProperties(defaultProps); + config.setGlobalProperties(globalProps); + return config; + } + + @Test + public void testWorkspaceClient() + { + ClientApp client = getClientApp("workspace"); + assertEquals("workspace", client.getName()); + assertEquals("${workspaceUrl}/images", client.getTemplateAssetsUrl()); + assertEquals("${repoBaseUrl}/workspace", client.getClientUrl()); + assertEquals("workspaceUrl", client.getClientUrlPropKey()); + assertEquals("\\$\\{workspaceUrl}", client.getClientUrlPlaceholderPattern() + .pattern()); + + Map properties = client.getProperties(); + assertNotNull(properties); + assertNull("Not Set", properties.get("inviteModeratedTemplatePath")); + assertEquals("alfresco/templates/test-email-templates/test-email-template.ftl", + properties.get("requestResetPasswordTemplatePath")); + assertEquals("${workspaceUrl}/reset-password", properties.get("resetPasswordPageUrl")); + assertEquals("some/path", properties.get("confirmResetPasswordTemplatePath")); + } + + @Test + public void testCoolAppClient() + { + ClientApp client = getClientApp("coolApp"); + assertEquals("coolApp", client.getName()); + assertEquals("${coolAppUrl}/images", client.getTemplateAssetsUrl()); + assertEquals("http://localhost:8090/cool-app", client.getClientUrl()); + assertEquals("coolAppUrl", client.getClientUrlPropKey()); + assertEquals("\\$\\{coolAppUrl}", client.getClientUrlPlaceholderPattern() + .pattern()); + + Map properties = client.getProperties(); + assertNotNull(properties); + assertEquals("${coolAppUrl}/page-one/page-two", properties.get("testPropUrl")); + } + + @Test + public void resolveWorkspacePlaceholders() + { + ClientApp client = getClientApp("workspace"); + // Raw properties + assertEquals("${repoBaseUrl}/workspace", client.getClientUrl()); + assertEquals("workspaceUrl", client.getClientUrlPropKey()); + assertEquals("\\$\\{workspaceUrl}", client.getClientUrlPlaceholderPattern() + .pattern()); + assertEquals("${workspaceUrl}/images", client.getTemplateAssetsUrl()); + assertEquals("${workspaceUrl}/reset-password", client.getProperty("resetPasswordPageUrl")); + + // Resolved properties + // String clientUrl = UrlUtil.replaceRepoBaseUrlPlaceholder(client.getClientUrl(), sysAdminParams); + assertEquals("http://localhost:8080/workspace", client.getResolvedClientUrl(sysAdminParams)); + assertEquals("http://localhost:8080/workspace/images", client.getResolvedTemplateAssetsUrl(sysAdminParams)); + assertEquals("http://localhost:8080/workspace/reset-password", + client.getResolvedProperty("resetPasswordPageUrl", sysAdminParams)); + } + + @Test + public void resolveCoolAppPlaceholders() + { + ClientApp client = getClientApp("coolApp"); + // Resolved properties + assertEquals("http://localhost:8090/cool-app", client.getResolvedClientUrl(sysAdminParams)); + assertEquals("http://localhost:8090/cool-app/images", client.getResolvedTemplateAssetsUrl(sysAdminParams)); + assertEquals("http://localhost:8090/cool-app/page-one/page-two", + client.getResolvedProperty("testPropUrl", sysAdminParams)); + } + + @Test + public void resolveSharePlaceholders() + { + ClientApp client = getClientApp("share"); + // Resolved properties + assertEquals("http://localhost:8081/share", client.getResolvedClientUrl(sysAdminParams)); + assertEquals("http://localhost:8081/share/res/components/images", + client.getResolvedTemplateAssetsUrl(sysAdminParams)); + assertEquals("http://localhost:8081/share/page/reset-password", + client.getResolvedProperty("resetPasswordPageUrl", sysAdminParams)); + assertEquals("http://localhost:8081/share/s", client.getResolvedProperty("sharedLinkBaseUrl", sysAdminParams)); + } + + @Test + public void testClientsPropertiesOverride() + { + Properties globalProps = new Properties(); + globalProps.put("repo.client-app.workspace.workspaceUrl", "https://develop.envalfresco.com/#"); + globalProps.put("repo.client-app.share.shareUrl", "https://develop.envalfresco.com/share"); + globalProps.put("repo.client-app.coolApp.coolAppUrl", "https://develop.envalfresco.com/eval/cool-app"); + + clientAppConfig.setGlobalProperties(globalProps); + + /* + * Workspace client URL Override + */ + ClientApp workspaceClient = getClientApp("workspace"); + // Resolved properties + assertEquals("https://develop.envalfresco.com/#", workspaceClient.getResolvedClientUrl(sysAdminParams)); + assertEquals("https://develop.envalfresco.com/#/images", + workspaceClient.getResolvedTemplateAssetsUrl(sysAdminParams)); + assertEquals("https://develop.envalfresco.com/#/reset-password", + workspaceClient.getResolvedProperty("resetPasswordPageUrl", sysAdminParams)); + + /* + * Share client URL Override + */ + ClientApp shareClient = getClientApp("share"); + // Resolved properties + assertEquals("https://develop.envalfresco.com/share", shareClient.getResolvedClientUrl(sysAdminParams)); + assertEquals("https://develop.envalfresco.com/share/res/components/images", + shareClient.getResolvedTemplateAssetsUrl(sysAdminParams)); + assertEquals("https://develop.envalfresco.com/share/page/reset-password", + shareClient.getResolvedProperty("resetPasswordPageUrl", sysAdminParams)); + assertEquals("https://develop.envalfresco.com/share/s", + shareClient.getResolvedProperty("sharedLinkBaseUrl", sysAdminParams)); + + /* + * coolApp client URL Override + */ + ClientApp coolAppClient = getClientApp("coolApp"); + // Resolved properties + assertEquals("https://develop.envalfresco.com/eval/cool-app", + coolAppClient.getResolvedClientUrl(sysAdminParams)); + assertEquals("https://develop.envalfresco.com/eval/cool-app/images", + coolAppClient.getResolvedTemplateAssetsUrl(sysAdminParams)); + assertEquals("https://develop.envalfresco.com/eval/cool-app/page-one/page-two", + coolAppClient.getResolvedProperty("testPropUrl", sysAdminParams)); + } + + private Properties getWorkspaceAppProperties() + { + Properties props = new Properties(); + props.put("repo.client-app.workspace.inviteModeratedTemplatePath", ""); + props.put("repo.client-app.workspace.workspaceUrl", "${repoBaseUrl}/workspace"); + props.put("repo.client-app.workspace.templateAssetsUrl", "${workspaceUrl}/images"); + props.put("repo.client-app.workspace.requestResetPasswordTemplatePath", + "alfresco/templates/test-email-templates/test-email-template.ftl"); + props.put("repo.client-app.workspace.resetPasswordPageUrl", "${workspaceUrl}/reset-password"); + props.put("repo.client-app.workspace.confirmResetPasswordTemplatePath", "some/path"); + + return props; + } + + private Properties getShareProperties() + { + Properties props = new Properties(); + props.put("repo.client-app.share.templateAssetsUrl", "${shareUrl}/res/components/images"); + props.put("repo.client-app.share.resetPasswordPageUrl", "${shareUrl}/page/reset-password"); + props.put("repo.client-app.share.sharedLinkBaseUrl", "${shareUrl}/s"); + + return props; + } + + private Properties getCoolAppProperties() + { + Properties props = new Properties(); + props.put("repo.client-app.coolApp.coolAppUrl", "http://localhost:8090/cool-app"); + props.put("repo.client-app.coolApp.templateAssetsUrl", "${coolAppUrl}/images"); + props.put("repo.client-app.coolApp.testPropUrl", "${coolAppUrl}/page-one/page-two"); + + return props; + } + + private ClientApp getClientApp(String clientName) + { + if (!initialised.get()) + { + clientAppConfig.onBootstrap(null); + initialised.set(true); + } + Map clients = clientAppConfig.getClients(); + assertFalse(clients.isEmpty()); + assertTrue(clientName + " client is expected.", clientAppConfig.exists(clientName)); + ClientApp client = clients.get(clientName); + assertNotNull(clientName + " client can't be null.", client); + return client; + } +} diff --git a/repository/src/test/java/org/alfresco/repo/security/authentication/ResetPasswordServiceImplTest.java b/repository/src/test/java/org/alfresco/repo/security/authentication/ResetPasswordServiceImplTest.java index a4b4e94c54..ff27ce82fa 100644 --- a/repository/src/test/java/org/alfresco/repo/security/authentication/ResetPasswordServiceImplTest.java +++ b/repository/src/test/java/org/alfresco/repo/security/authentication/ResetPasswordServiceImplTest.java @@ -30,6 +30,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import org.alfresco.model.ContentModel; +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.security.authentication.ResetPasswordServiceImpl.InvalidResetPasswordWorkflowException; import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetPasswordDetails; @@ -47,6 +50,7 @@ import org.alfresco.service.namespace.QName; import org.alfresco.util.GUID; import org.alfresco.util.Pair; import org.alfresco.util.TestHelper; +import org.alfresco.util.UrlUtil; import org.alfresco.util.email.EmailUtil; import org.alfresco.util.test.junitrules.ApplicationContextInit; import org.alfresco.util.test.junitrules.RunAsFullyAuthenticatedRule; @@ -88,9 +92,12 @@ public class ResetPasswordServiceImplTest private static PersonService personService; private static Properties globalProperties; private static WorkflowService workflowService; + private static ClientAppConfig clientAppConfig; + private static SysAdminParams sysAdminParams; - private static TestPerson testPerson; - private static EmailUtil emailUtil; + private static TestPerson testPerson1; + private static TestPerson testPerson2; + private static EmailUtil emailUtil; @BeforeClass public static void initStaticData() throws Exception @@ -103,36 +110,57 @@ public class ResetPasswordServiceImplTest personService = APP_CONTEXT_INIT.getApplicationContext().getBean("personService", PersonService.class); globalProperties = APP_CONTEXT_INIT.getApplicationContext().getBean("global-properties", Properties.class); workflowService = APP_CONTEXT_INIT.getApplicationContext().getBean("WorkflowService", WorkflowService.class); + clientAppConfig = APP_CONTEXT_INIT.getApplicationContext().getBean("clientAppConfig", ClientAppConfig.class); + sysAdminParams = APP_CONTEXT_INIT.getApplicationContext().getBean("sysAdminParams", SysAdminParams.class); emailUtil = new EmailUtil(APP_CONTEXT_INIT.getApplicationContext()); emailUtil.reset(); - String userName = "jane.doe" + System.currentTimeMillis(); - testPerson = new TestPerson() - .setUserName(userName) + String userName1 = "jane.doe" + System.currentTimeMillis(); + testPerson1 = new TestPerson() + .setUserName(userName1) .setFirstName("Jane") - .setLastName("doe") + .setLastName("Doe") .setPassword("password") - .setEmail(userName + "@example.com"); + .setEmail(userName1 + "@example.com"); + String userName2 = "sara.blogs" + System.currentTimeMillis(); + testPerson2 = new TestPerson() + .setUserName(userName2) + .setFirstName("Sara") + .setLastName("Blogs") + .setPassword("password") + .setEmail(userName2 + "@example.com"); + + AuthenticationUtil.setRunAsUserSystem(); transactionHelper.doInTransaction((RetryingTransactionCallback) () -> { - createUser(testPerson); + createUser(testPerson1); + createUser(testPerson2); return null; }); - + // Restore authentication to pre-test state. + try + { + AuthenticationUtil.popAuthentication(); + } + catch(EmptyStackException e) + { + // Nothing to do. + } } @AfterClass public static void cleanUp() { - resetPasswordService.setSendEmailAsynchronously(Boolean.valueOf( + resetPasswordService.setSendEmailAsynchronously(Boolean.parseBoolean( globalProperties.getProperty("system.reset-password.sendEmailAsynchronously"))); resetPasswordService.setDefaultEmailSender((String) globalProperties.get("system.email.sender.default")); AuthenticationUtil.setRunAsUserSystem(); transactionHelper.doInTransaction(() -> { - personService.deletePerson(testPerson.userName); + personService.deletePerson(testPerson1.userName); + personService.deletePerson(testPerson2.userName); return null; }); @@ -157,21 +185,21 @@ public class ResetPasswordServiceImplTest public void testResetPassword() throws Exception { // Try the credential before change of password - authenticateUser(testPerson.userName, testPerson.password); + authenticateUser(testPerson1.userName, testPerson1.password); // Make sure to run as system AuthenticationUtil.clearCurrentSecurityContext(); AuthenticationUtil.setRunAsUserSystem(); // Request password reset - resetPasswordService.requestReset(testPerson.userName, "share"); + resetPasswordService.requestReset(testPerson1.userName, "share"); assertEquals("A reset password email should have been sent.", 1, emailUtil.getSentCount()); // Check the email MimeMessage msg = emailUtil.getLastEmail(); assertNotNull("There should be an email.", msg); assertEquals("Should've been only one email recipient.", 1, msg.getAllRecipients().length); // Check the recipient is the person who requested the reset password - assertEquals(testPerson.email, msg.getAllRecipients()[0].toString()); + assertEquals(testPerson1.email, msg.getAllRecipients()[0].toString()); //Check the sender is what we set as default assertEquals(DEFAULT_SENDER, msg.getFrom()[0].toString()); // There should be a subject @@ -192,7 +220,7 @@ public class ResetPasswordServiceImplTest emailUtil.reset(); // Now that we have got the email, try to reset the password ResetPasswordDetails passwordDetails = new ResetPasswordDetails() - .setUserId(testPerson.userName) + .setUserId(testPerson1.userName) .setPassword("newPassword") .setWorkflowId(pair.getFirst()) .setWorkflowKey(pair.getSecond()); @@ -204,7 +232,7 @@ public class ResetPasswordServiceImplTest assertNotNull("There should be an email.", msg); assertEquals("Should've been only one email recipient.", 1, msg.getAllRecipients().length); // Check the recipient is the person who requested the reset password - assertEquals(testPerson.email, msg.getAllRecipients()[0].toString()); + assertEquals(testPerson1.email, msg.getAllRecipients()[0].toString()); // Check the sender is what we set as default assertEquals(DEFAULT_SENDER, msg.getFrom()[0].toString()); // There should be a subject @@ -215,12 +243,12 @@ public class ResetPasswordServiceImplTest assertEquals(msg.getSubject(), I18NUtil.getMessage(emailSubjectKey)); // Try the old credential - TestHelper.assertThrows(() -> authenticateUser(testPerson.userName, testPerson.password), + TestHelper.assertThrows(() -> authenticateUser(testPerson1.userName, testPerson1.password), AuthenticationException.class, "As the user changed her password, the authentication should have failed."); // Try the new credential - authenticateUser(testPerson.userName, "newPassword"); + authenticateUser(testPerson1.userName, "newPassword"); // Make sure to run as system AuthenticationUtil.clearCurrentSecurityContext(); @@ -237,12 +265,12 @@ public class ResetPasswordServiceImplTest public void testRequestResetPasswordInvalid() throws Exception { // Request password reset - TestHelper.assertThrows(() -> resetPasswordService.requestReset(testPerson.userName, null), + TestHelper.assertThrows(() -> resetPasswordService.requestReset(testPerson1.userName, null), IllegalArgumentException.class, "Client name is mandatory."); // Request password reset - TestHelper.assertThrows(() -> resetPasswordService.requestReset(testPerson.userName, "TestClient" + System.currentTimeMillis()), + TestHelper.assertThrows(() -> resetPasswordService.requestReset(testPerson1.userName, "TestClient" + System.currentTimeMillis()), ClientAppNotFoundException.class, "Client is not found."); assertEquals("No email should have been sent.", 0, emailUtil.getSentCount()); @@ -260,23 +288,23 @@ public class ResetPasswordServiceImplTest assertEquals("No email should have been sent.", 0, emailUtil.getSentCount()); // Disable the user - enableUser(testPerson.userName, false); + enableUser(testPerson1.userName, false); // Request password reset - TestHelper.assertThrows(() -> resetPasswordService.requestReset(testPerson.userName, "share"), + TestHelper.assertThrows(() -> resetPasswordService.requestReset(testPerson1.userName, "share"), ResetPasswordWorkflowInvalidUserException.class, "user is disabled."); assertEquals("No email should have been sent.", 0, emailUtil.getSentCount()); // Enable the user - enableUser(testPerson.userName, true); + enableUser(testPerson1.userName, true); } @Test public void testResetPasswordInvalid() throws Exception { // Request password reset - resetPasswordService.requestReset(testPerson.userName, "share"); + resetPasswordService.requestReset(testPerson1.userName, "share"); assertEquals("A reset password email should have been sent.", 1, emailUtil.getSentCount()); // Check the email MimeMessage msg = emailUtil.getLastEmail(); @@ -307,7 +335,7 @@ public class ResetPasswordServiceImplTest IllegalArgumentException.class, "User id is mandatory."); - passwordDetails.setUserId(testPerson.userName) + passwordDetails.setUserId(testPerson1.userName) .setPassword(null); // Password is not provided TestHelper.assertThrows(() -> resetPasswordService.initiateResetPassword(passwordDetails), IllegalArgumentException.class, @@ -363,7 +391,7 @@ public class ResetPasswordServiceImplTest // Set the duration for 1 second resetPasswordService.setTimerEnd("PT1S"); // Request password reset - resetPasswordService.requestReset(testPerson.userName, "share"); + resetPasswordService.requestReset(testPerson1.userName, "share"); assertEquals("A reset password email should have been sent.", 1, emailUtil.getSentCount()); // Check the reset password url. @@ -377,7 +405,7 @@ public class ResetPasswordServiceImplTest emailUtil.reset(); // Now that we have got the email, try to reset the password ResetPasswordDetails passwordDetails = new ResetPasswordDetails() - .setUserId(testPerson.userName) + .setUserId(testPerson1.userName) .setPassword("newPassword") .setWorkflowId(pair.getFirst()) .setWorkflowKey(pair.getSecond()); @@ -396,6 +424,110 @@ public class ResetPasswordServiceImplTest } } + @Test + public void testResetPasswordForClientWorkspace() throws Exception + { + // Try the credential before change of password + authenticateUser(testPerson2.userName, testPerson2.password); + + // Make sure to run as system + AuthenticationUtil.clearCurrentSecurityContext(); + AuthenticationUtil.setRunAsUserSystem(); + + // Request password reset + resetPasswordService.requestReset(testPerson2.userName, "workspace"); + assertEquals("A reset password email should have been sent.", 1, emailUtil.getSentCount()); + // Check the email + MimeMessage msg = emailUtil.getLastEmail(); + assertNotNull("There should be an email.", msg); + assertEquals("Should've been only one email recipient.", 1, msg.getAllRecipients().length); + // Check the recipient is the person who requested the reset password + assertEquals(testPerson2.email, msg.getAllRecipients()[0].toString()); + //Check the sender is what we set as default + assertEquals(DEFAULT_SENDER, msg.getFrom()[0].toString()); + // There should be a subject + assertNotNull("There should be a subject.", msg.getSubject()); + // Check the default email subject - (check that we are sending the right email) + String emailSubjectKey = getDeclaredField(SendResetPasswordEmailDelegate.class, "EMAIL_SUBJECT_KEY"); + assertNotNull(emailSubjectKey); + assertEquals(msg.getSubject(), I18NUtil.getMessage(emailSubjectKey)); + + // Check the reset password url. + String resetPasswordUrl = (String) emailUtil.getLastEmailTemplateModelValue("reset_password_url"); + assertNotNull("Wrong email is sent.", resetPasswordUrl); + // Get the workflow id and key + Pair pair = getWorkflowIdAndKeyFromUrl(resetPasswordUrl); + assertNotNull("Workflow Id can't be null.", pair.getFirst()); + assertNotNull("Workflow Key can't be null.", pair.getSecond()); + + emailUtil.reset(); + // Now that we have got the email, try to reset the password + ResetPasswordDetails passwordDetails = new ResetPasswordDetails() + .setUserId(testPerson2.userName) + .setPassword("strongPassword") + .setWorkflowId(pair.getFirst()) + .setWorkflowKey(pair.getSecond()); + + resetPasswordService.initiateResetPassword(passwordDetails); + assertEquals("A reset password confirmation email should have been sent.", 1, emailUtil.getSentCount()); + // Check the email + msg = emailUtil.getLastEmail(); + assertNotNull("There should be an email.", msg); + assertEquals("Should've been only one email recipient.", 1, msg.getAllRecipients().length); + // Check the recipient is the person who requested the reset password + assertEquals(testPerson2.email, msg.getAllRecipients()[0].toString()); + // Check the sender is what we set as default + assertEquals(DEFAULT_SENDER, msg.getFrom()[0].toString()); + // There should be a subject + assertNotNull("There should be a subject.", msg.getSubject()); + // Check the default email subject - (check that we are sending the right email) + emailSubjectKey = getDeclaredField(SendResetPasswordConfirmationEmailDelegate.class, "EMAIL_SUBJECT_KEY"); + assertNotNull(emailSubjectKey); + assertEquals(msg.getSubject(), I18NUtil.getMessage(emailSubjectKey)); + + // Try the old credential + TestHelper.assertThrows(() -> authenticateUser(testPerson2.userName, testPerson2.password), + AuthenticationException.class, + "As the user changed her password, the authentication should have failed."); + + // Try the new credential + authenticateUser(testPerson2.userName, "strongPassword"); + + // Make sure to run as system + AuthenticationUtil.clearCurrentSecurityContext(); + AuthenticationUtil.setRunAsUserSystem(); + emailUtil.reset(); + // Try reset again with the used workflow + TestHelper.assertThrows(() -> resetPasswordService.initiateResetPassword(passwordDetails), + InvalidResetPasswordWorkflowException.class, + "The workflow instance is not active (it has already been used)."); + assertEquals("No email should have been sent.", 0, emailUtil.getSentCount()); + } + + @Test + public void testCreateResetPasswordUrl() + { + /* Out of the box we have share and workspace registered as the clients. + * See: alfresco/client/config/repo-clients-apps.properties file. + */ + + // Share client + ClientApp share = clientAppConfig.getClient("share"); + String shareResetPasswordUrl = + resetPasswordService.createResetPasswordUrl(share, "workflow-id-123", "workflow-key-123"); + String shareExpectedUrl = UrlUtil.getShareUrl(sysAdminParams) + + "/page/reset-password?key=workflow-key-123&id=activiti$workflow-id-123"; + assertEquals(shareExpectedUrl, shareResetPasswordUrl); + + // Workspace client + ClientApp workspace = clientAppConfig.getClient("workspace"); + String workspaceResetPasswordUrl = + resetPasswordService.createResetPasswordUrl(workspace, "workflow-id-456", "workflow-key-456"); + String workspaceExpectedUrl = UrlUtil.getAlfrescoBaseUrl(sysAdminParams) + + "/workspace/reset-password?key=workflow-key-456&id=activiti$workflow-id-456"; + assertEquals(workspaceExpectedUrl, workspaceResetPasswordUrl); + } + private boolean isActive(String workflowId) { WorkflowInstance workflowInstance = workflowService.getWorkflowById(workflowId);