mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
Feature: Forgot password (#9509)
* Feature: Forgot password * Address comments * fixups * Make forgot password disabled by default * Apply suggestions from code review * Address comments
This commit is contained in:
parent
638c1526d0
commit
0655075f51
@ -21,7 +21,9 @@ import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import com.cloud.domain.Domain;
|
||||
import com.cloud.exception.CloudAuthenticationException;
|
||||
import com.cloud.user.UserAccount;
|
||||
|
||||
public interface ApiServerService {
|
||||
public boolean verifyRequest(Map<String, Object[]> requestParameters, Long userId, InetAddress remoteAddress) throws ServerApiException;
|
||||
@ -42,4 +44,8 @@ public interface ApiServerService {
|
||||
public String handleRequest(Map<String, Object[]> params, String responseType, StringBuilder auditTrailSb) throws ServerApiException;
|
||||
|
||||
public Class<?> getCmdClass(String cmdName);
|
||||
|
||||
boolean forgotPassword(UserAccount userAccount, Domain domain);
|
||||
|
||||
boolean resetPassword(UserAccount userAccount, String token, String password);
|
||||
}
|
||||
|
||||
@ -17,5 +17,5 @@
|
||||
package org.apache.cloudstack.api.auth;
|
||||
|
||||
public enum APIAuthenticationType {
|
||||
LOGIN_API, LOGOUT_API, READONLY_API, LOGIN_2FA_API
|
||||
LOGIN_API, LOGOUT_API, READONLY_API, LOGIN_2FA_API, PASSWORD_RESET
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
package com.cloud.user;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.persistence.Column;
|
||||
@ -361,6 +362,9 @@ public class UserAccountVO implements UserAccount, InternalIdentity {
|
||||
|
||||
@Override
|
||||
public Map<String, String> getDetails() {
|
||||
if (details == null) {
|
||||
details = new HashMap<>();
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
|
||||
@ -46,6 +46,8 @@ public class UserDetailVO implements ResourceDetail {
|
||||
private boolean display = true;
|
||||
|
||||
public static final String Setup2FADetail = "2FASetupStatus";
|
||||
public static final String PasswordResetToken = "PasswordResetToken";
|
||||
public static final String PasswordResetTokenExpiryDate = "PasswordResetTokenExpiryDate";
|
||||
|
||||
public UserDetailVO() {
|
||||
}
|
||||
|
||||
@ -515,6 +515,11 @@ public class MockAccountManager extends ManagerBase implements AccountManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void validateUserPasswordAndUpdateIfNeeded(String newPassword, UserVO user,
|
||||
String currentPassword,
|
||||
boolean skipCurrentPassValidation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkApiAccess(Account account, String command) throws PermissionDeniedException {
|
||||
|
||||
|
||||
1
pom.xml
1
pom.xml
@ -169,6 +169,7 @@
|
||||
<cs.kafka-clients.version>2.7.0</cs.kafka-clients.version>
|
||||
<cs.libvirt-java.version>0.5.3</cs.libvirt-java.version>
|
||||
<cs.mail.version>1.5.0-b01</cs.mail.version>
|
||||
<cs.mustache.version>0.9.14</cs.mustache.version>
|
||||
<cs.mysql.version>8.0.33</cs.mysql.version>
|
||||
<cs.neethi.version>2.0.4</cs.neethi.version>
|
||||
<cs.nitro.version>10.1</cs.nitro.version>
|
||||
|
||||
@ -101,6 +101,11 @@
|
||||
<artifactId>commons-math3</artifactId>
|
||||
<version>${cs.commons-math3.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.spullara.mustache.java</groupId>
|
||||
<artifactId>compiler</artifactId>
|
||||
<version>${cs.mustache.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.cloudstack</groupId>
|
||||
<artifactId>cloud-utils</artifactId>
|
||||
|
||||
@ -55,6 +55,13 @@ import javax.naming.ConfigurationException;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountManager;
|
||||
import com.cloud.user.AccountManagerImpl;
|
||||
import com.cloud.user.DomainManager;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.user.UserAccount;
|
||||
import com.cloud.user.UserVO;
|
||||
import org.apache.cloudstack.acl.APIChecker;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
@ -103,7 +110,9 @@ import org.apache.cloudstack.framework.messagebus.MessageBus;
|
||||
import org.apache.cloudstack.framework.messagebus.MessageDispatcher;
|
||||
import org.apache.cloudstack.framework.messagebus.MessageHandler;
|
||||
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
|
||||
import org.apache.cloudstack.user.UserPasswordResetManager;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.lang3.EnumUtils;
|
||||
import org.apache.http.ConnectionClosedException;
|
||||
import org.apache.http.HttpException;
|
||||
import org.apache.http.HttpRequest;
|
||||
@ -157,13 +166,6 @@ import com.cloud.exception.ResourceUnavailableException;
|
||||
import com.cloud.exception.UnavailableCommandException;
|
||||
import com.cloud.projects.dao.ProjectDao;
|
||||
import com.cloud.storage.VolumeApiService;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountManager;
|
||||
import com.cloud.user.AccountManagerImpl;
|
||||
import com.cloud.user.DomainManager;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.user.UserAccount;
|
||||
import com.cloud.user.UserVO;
|
||||
import com.cloud.utils.ConstantTimeComparator;
|
||||
import com.cloud.utils.DateUtil;
|
||||
import com.cloud.utils.HttpUtils;
|
||||
@ -182,6 +184,8 @@ import com.cloud.utils.exception.ExceptionProxyObject;
|
||||
import com.cloud.utils.net.NetUtils;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled;
|
||||
|
||||
@Component
|
||||
public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiServerService, Configurable {
|
||||
|
||||
@ -214,6 +218,8 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
|
||||
private ProjectDao projectDao;
|
||||
@Inject
|
||||
private UUIDManager uuidMgr;
|
||||
@Inject
|
||||
private UserPasswordResetManager userPasswordResetManager;
|
||||
|
||||
private List<PluggableService> pluggableServices;
|
||||
|
||||
@ -1223,6 +1229,57 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forgotPassword(UserAccount userAccount, Domain domain) {
|
||||
if (!UserPasswordResetEnabled.value()) {
|
||||
String errorMessage = String.format("%s is false. Password reset for the user is not allowed.",
|
||||
UserPasswordResetEnabled.key());
|
||||
logger.error(errorMessage);
|
||||
throw new CloudRuntimeException(errorMessage);
|
||||
}
|
||||
if (StringUtils.isBlank(userAccount.getEmail())) {
|
||||
logger.error(String.format(
|
||||
"Email is not set. username: %s account id: %d domain id: %d",
|
||||
userAccount.getUsername(), userAccount.getAccountId(), userAccount.getDomainId()));
|
||||
throw new CloudRuntimeException("Email is not set for the user.");
|
||||
}
|
||||
|
||||
if (!EnumUtils.getEnumIgnoreCase(Account.State.class, userAccount.getState()).equals(Account.State.ENABLED)) {
|
||||
logger.error(String.format(
|
||||
"User is not enabled. username: %s account id: %d domain id: %s",
|
||||
userAccount.getUsername(), userAccount.getAccountId(), domain.getUuid()));
|
||||
throw new CloudRuntimeException("User is not enabled.");
|
||||
}
|
||||
|
||||
if (!EnumUtils.getEnumIgnoreCase(Account.State.class, userAccount.getAccountState()).equals(Account.State.ENABLED)) {
|
||||
logger.error(String.format(
|
||||
"Account is not enabled. username: %s account id: %d domain id: %s",
|
||||
userAccount.getUsername(), userAccount.getAccountId(), domain.getUuid()));
|
||||
throw new CloudRuntimeException("Account is not enabled.");
|
||||
}
|
||||
|
||||
if (!domain.getState().equals(Domain.State.Active)) {
|
||||
logger.error(String.format(
|
||||
"Domain is not active. username: %s account id: %d domain id: %s",
|
||||
userAccount.getUsername(), userAccount.getAccountId(), domain.getUuid()));
|
||||
throw new CloudRuntimeException("Domain is not active.");
|
||||
}
|
||||
|
||||
userPasswordResetManager.setResetTokenAndSend(userAccount);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean resetPassword(UserAccount userAccount, String token, String password) {
|
||||
if (!UserPasswordResetEnabled.value()) {
|
||||
String errorMessage = String.format("%s is false. Password reset for the user is not allowed.",
|
||||
UserPasswordResetEnabled.key());
|
||||
logger.error(errorMessage);
|
||||
throw new CloudRuntimeException(errorMessage);
|
||||
}
|
||||
return userPasswordResetManager.validateAndResetPassword(userAccount, token, password);
|
||||
}
|
||||
|
||||
private void checkCommandAvailable(final User user, final String commandName, final InetAddress remoteAddress) throws PermissionDeniedException {
|
||||
if (user == null) {
|
||||
throw new PermissionDeniedException("User is null for role based API access check for command" + commandName);
|
||||
|
||||
@ -31,6 +31,8 @@ import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
|
||||
import com.cloud.utils.component.ComponentContext;
|
||||
import com.cloud.utils.component.ManagerBase;
|
||||
|
||||
import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuthenticationManager {
|
||||
|
||||
@ -75,6 +77,10 @@ public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuth
|
||||
List<Class<?>> cmdList = new ArrayList<Class<?>>();
|
||||
cmdList.add(DefaultLoginAPIAuthenticatorCmd.class);
|
||||
cmdList.add(DefaultLogoutAPIAuthenticatorCmd.class);
|
||||
if (UserPasswordResetEnabled.value()) {
|
||||
cmdList.add(DefaultForgotPasswordAPIAuthenticatorCmd.class);
|
||||
cmdList.add(DefaultResetPasswordAPIAuthenticatorCmd.class);
|
||||
}
|
||||
|
||||
cmdList.add(ListUserTwoFactorAuthenticatorProvidersCmd.class);
|
||||
cmdList.add(ValidateUserTwoFactorAuthenticationCodeCmd.class);
|
||||
|
||||
@ -0,0 +1,165 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
package com.cloud.api.auth;
|
||||
|
||||
import com.cloud.api.ApiServlet;
|
||||
import com.cloud.api.response.ApiResponseSerializer;
|
||||
import com.cloud.domain.Domain;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.user.UserAccount;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.ApiServerService;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
import org.apache.cloudstack.api.Parameter;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationType;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticator;
|
||||
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
|
||||
import org.apache.cloudstack.api.response.SuccessResponse;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.net.InetAddress;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@APICommand(name = "forgotPassword",
|
||||
description = "Sends an email to the user with a token to reset the password using resetPassword command.",
|
||||
since = "4.20.0.0",
|
||||
requestHasSensitiveInfo = true,
|
||||
responseObject = SuccessResponse.class)
|
||||
public class DefaultForgotPasswordAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator {
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
//////////////// API parameters /////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
@Parameter(name = ApiConstants.USERNAME, type = CommandType.STRING, description = "Username", required = true)
|
||||
private String username;
|
||||
|
||||
@Parameter(name = ApiConstants.DOMAIN, type = CommandType.STRING, description = "Path of the domain that the user belongs to. Example: domain=/com/cloud/internal. If no domain is passed in, the ROOT (/) domain is assumed.")
|
||||
private String domain;
|
||||
|
||||
@Inject
|
||||
ApiServerService _apiServer;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getDomainName() {
|
||||
return domain;
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////// API Implementation///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public long getEntityOwnerId() {
|
||||
return Account.Type.NORMAL.ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws ServerApiException {
|
||||
// We should never reach here
|
||||
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String authenticate(String command, Map<String, Object[]> params, HttpSession session, InetAddress remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException {
|
||||
final String[] username = (String[])params.get(ApiConstants.USERNAME);
|
||||
final String[] domainName = (String[])params.get(ApiConstants.DOMAIN);
|
||||
|
||||
Long domainId = null;
|
||||
String domain = null;
|
||||
domain = getDomainName(auditTrailSb, domainName, domain);
|
||||
|
||||
String serializedResponse = null;
|
||||
if (username != null) {
|
||||
try {
|
||||
final Domain userDomain = _domainService.findDomainByPath(domain);
|
||||
if (userDomain != null) {
|
||||
domainId = userDomain.getId();
|
||||
} else {
|
||||
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find the domain from the path %s", domain));
|
||||
}
|
||||
final UserAccount userAccount = _accountService.getActiveUserAccount(username[0], domainId);
|
||||
if (userAccount != null && List.of(User.Source.SAML2, User.Source.OAUTH2, User.Source.LDAP).contains(userAccount.getSource())) {
|
||||
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Forgot Password is not allowed for this user");
|
||||
}
|
||||
boolean success = _apiServer.forgotPassword(userAccount, userDomain);
|
||||
logger.debug("Forgot password request for user " + username[0] + " in domain " + domain + " is successful: " + success);
|
||||
} catch (final CloudRuntimeException ex) {
|
||||
ApiServlet.invalidateHttpSession(session, "fall through to API key,");
|
||||
String msg = String.format("%s", ex.getMessage() != null ?
|
||||
ex.getMessage() :
|
||||
"forgot password request failed for user, check if username/domain are correct");
|
||||
auditTrailSb.append(" " + ApiErrorCode.ACCOUNT_ERROR + " " + msg);
|
||||
serializedResponse = _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), msg, params, responseType);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(msg);
|
||||
}
|
||||
}
|
||||
SuccessResponse successResponse = new SuccessResponse();
|
||||
successResponse.setSuccess(true);
|
||||
successResponse.setResponseName(getCommandName());
|
||||
return ApiResponseSerializer.toSerializedString(successResponse, responseType);
|
||||
}
|
||||
// We should not reach here and if we do we throw an exception
|
||||
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, serializedResponse);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getDomainName(StringBuilder auditTrailSb, String[] domainName, String domain) {
|
||||
if (domainName != null) {
|
||||
domain = domainName[0];
|
||||
auditTrailSb.append(" domain=" + domain);
|
||||
if (domain != null) {
|
||||
// ensure domain starts with '/' and ends with '/'
|
||||
if (!domain.endsWith("/")) {
|
||||
domain += '/';
|
||||
}
|
||||
if (!domain.startsWith("/")) {
|
||||
domain = "/" + domain;
|
||||
}
|
||||
}
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public APIAuthenticationType getAPIType() {
|
||||
return APIAuthenticationType.PASSWORD_RESET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,193 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
package com.cloud.api.auth;
|
||||
|
||||
import com.cloud.api.ApiServlet;
|
||||
import com.cloud.api.response.ApiResponseSerializer;
|
||||
import com.cloud.domain.Domain;
|
||||
import com.cloud.exception.CloudAuthenticationException;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.user.UserAccount;
|
||||
import com.cloud.utils.UuidUtils;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
import org.apache.cloudstack.api.APICommand;
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.ApiServerService;
|
||||
import org.apache.cloudstack.api.BaseCmd;
|
||||
import org.apache.cloudstack.api.Parameter;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticationType;
|
||||
import org.apache.cloudstack.api.auth.APIAuthenticator;
|
||||
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
|
||||
import org.apache.cloudstack.api.response.SuccessResponse;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
import java.net.InetAddress;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@APICommand(name = "resetPassword",
|
||||
description = "Resets the password for the user using the token generated via forgotPassword command.",
|
||||
since = "4.20.0.0",
|
||||
requestHasSensitiveInfo = true,
|
||||
responseObject = SuccessResponse.class)
|
||||
public class DefaultResetPasswordAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator {
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
//////////////// API parameters /////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
@Parameter(name = ApiConstants.USERNAME,
|
||||
type = CommandType.STRING,
|
||||
description = "Username", required = true)
|
||||
private String username;
|
||||
|
||||
@Parameter(name = ApiConstants.DOMAIN,
|
||||
type = CommandType.STRING,
|
||||
description = "Path of the domain that the user belongs to. Example: domain=/com/cloud/internal. If no domain is passed in, the ROOT (/) domain is assumed.")
|
||||
private String domain;
|
||||
|
||||
@Parameter(name = ApiConstants.TOKEN,
|
||||
type = CommandType.STRING,
|
||||
required = true,
|
||||
description = "Token generated via forgotPassword command.")
|
||||
private String token;
|
||||
|
||||
@Parameter(name = ApiConstants.PASSWORD,
|
||||
type = CommandType.STRING,
|
||||
required = true,
|
||||
description = "New password in clear text (Default hashed to SHA256SALT).")
|
||||
private String password;
|
||||
|
||||
@Inject
|
||||
ApiServerService _apiServer;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getDomainName() {
|
||||
return domain;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////// API Implementation///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public long getEntityOwnerId() {
|
||||
return Account.Type.NORMAL.ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws ServerApiException {
|
||||
// We should never reach here
|
||||
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String authenticate(String command, Map<String, Object[]> params, HttpSession session, InetAddress remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException {
|
||||
final String[] username = (String[])params.get(ApiConstants.USERNAME);
|
||||
final String[] password = (String[])params.get(ApiConstants.PASSWORD);
|
||||
final String[] domainName = (String[])params.get(ApiConstants.DOMAIN);
|
||||
final String[] token = (String[])params.get(ApiConstants.TOKEN);
|
||||
|
||||
Long domainId = null;
|
||||
String domain = null;
|
||||
domain = getDomainName(auditTrailSb, domainName, domain);
|
||||
|
||||
String serializedResponse = null;
|
||||
|
||||
if (!UuidUtils.isUuid(token[0])) {
|
||||
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid token");
|
||||
}
|
||||
|
||||
if (username != null) {
|
||||
final String pwd = ((password == null) ? null : password[0]);
|
||||
try {
|
||||
final Domain userDomain = _domainService.findDomainByPath(domain);
|
||||
if (userDomain != null) {
|
||||
domainId = userDomain.getId();
|
||||
} else {
|
||||
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find the domain from the path %s", domain));
|
||||
}
|
||||
final UserAccount userAccount = _accountService.getActiveUserAccount(username[0], domainId);
|
||||
if (userAccount != null && List.of(User.Source.SAML2, User.Source.OAUTH2, User.Source.LDAP).contains(userAccount.getSource())) {
|
||||
throw new CloudAuthenticationException("Password reset is not allowed for CloudStack login");
|
||||
}
|
||||
boolean success = _apiServer.resetPassword(userAccount, token[0], pwd);
|
||||
SuccessResponse successResponse = new SuccessResponse();
|
||||
successResponse.setSuccess(success);
|
||||
successResponse.setResponseName(getCommandName());
|
||||
return ApiResponseSerializer.toSerializedString(successResponse, responseType);
|
||||
} catch (final CloudRuntimeException ex) {
|
||||
ApiServlet.invalidateHttpSession(session, "fall through to API key,");
|
||||
String msg = String.format("%s", ex.getMessage() != null ?
|
||||
ex.getMessage() :
|
||||
"failed to reset password for user, check your inputs");
|
||||
auditTrailSb.append(" " + ApiErrorCode.ACCOUNT_ERROR + " " + msg);
|
||||
serializedResponse = _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), msg, params, responseType);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
// We should not reach here and if we do we throw an exception
|
||||
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, serializedResponse);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getDomainName(StringBuilder auditTrailSb, String[] domainName, String domain) {
|
||||
if (domainName != null) {
|
||||
domain = domainName[0];
|
||||
auditTrailSb.append(" domain=" + domain);
|
||||
if (domain != null) {
|
||||
// ensure domain starts with '/' and ends with '/'
|
||||
if (!domain.endsWith("/")) {
|
||||
domain += '/';
|
||||
}
|
||||
if (!domain.startsWith("/")) {
|
||||
domain = "/" + domain;
|
||||
}
|
||||
}
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public APIAuthenticationType getAPIType() {
|
||||
return APIAuthenticationType.PASSWORD_RESET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
|
||||
}
|
||||
}
|
||||
@ -200,5 +200,7 @@ public interface AccountManager extends AccountService, Configurable {
|
||||
|
||||
List<String> getApiNameList();
|
||||
|
||||
void checkApiAccess(Account caller, String command);
|
||||
void validateUserPasswordAndUpdateIfNeeded(String newPassword, UserVO user, String currentPassword, boolean skipCurrentPassValidation);
|
||||
|
||||
void checkApiAccess(Account caller, String command);
|
||||
}
|
||||
|
||||
@ -1455,7 +1455,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
|
||||
validateAndUpdateLastNameIfNeeded(updateUserCmd, user);
|
||||
validateAndUpdateUsernameIfNeeded(updateUserCmd, user, account);
|
||||
|
||||
validateUserPasswordAndUpdateIfNeeded(updateUserCmd.getPassword(), user, updateUserCmd.getCurrentPassword());
|
||||
validateUserPasswordAndUpdateIfNeeded(updateUserCmd.getPassword(), user, updateUserCmd.getCurrentPassword(), false);
|
||||
String email = updateUserCmd.getEmail();
|
||||
if (StringUtils.isNotBlank(email)) {
|
||||
user.setEmail(email);
|
||||
@ -1483,7 +1483,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
|
||||
*
|
||||
* If all checks pass, we encode the given password with the most preferable password mechanism given in {@link #_userPasswordEncoders}.
|
||||
*/
|
||||
protected void validateUserPasswordAndUpdateIfNeeded(String newPassword, UserVO user, String currentPassword) {
|
||||
public void validateUserPasswordAndUpdateIfNeeded(String newPassword, UserVO user, String currentPassword, boolean skipCurrentPassValidation) {
|
||||
if (newPassword == null) {
|
||||
logger.trace("No new password to update for user: " + user.getUuid());
|
||||
return;
|
||||
@ -1498,16 +1498,17 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
|
||||
boolean isRootAdminExecutingPasswordUpdate = callingAccount.getId() == Account.ACCOUNT_ID_SYSTEM || isRootAdmin(callingAccount.getId());
|
||||
boolean isDomainAdmin = isDomainAdmin(callingAccount.getId());
|
||||
boolean isAdmin = isDomainAdmin || isRootAdminExecutingPasswordUpdate;
|
||||
boolean skipValidation = isAdmin || skipCurrentPassValidation;
|
||||
if (isAdmin) {
|
||||
logger.trace(String.format("Admin account [uuid=%s] executing password update for user [%s] ", callingAccount.getUuid(), user.getUuid()));
|
||||
}
|
||||
if (!isAdmin && StringUtils.isBlank(currentPassword)) {
|
||||
if (!skipValidation && StringUtils.isBlank(currentPassword)) {
|
||||
throw new InvalidParameterValueException("To set a new password the current password must be provided.");
|
||||
}
|
||||
if (CollectionUtils.isEmpty(_userPasswordEncoders)) {
|
||||
throw new CloudRuntimeException("No user authenticators configured!");
|
||||
}
|
||||
if (!isAdmin) {
|
||||
if (!skipValidation) {
|
||||
validateCurrentPassword(user, currentPassword);
|
||||
}
|
||||
UserAuthenticator userAuthenticator = _userPasswordEncoders.get(0);
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package org.apache.cloudstack.user;
|
||||
|
||||
import com.cloud.user.UserAccount;
|
||||
import org.apache.cloudstack.framework.config.ConfigKey;
|
||||
|
||||
public interface UserPasswordResetManager {
|
||||
ConfigKey<Boolean> UserPasswordResetEnabled = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED,
|
||||
Boolean.class,
|
||||
"user.password.reset.enabled", "false",
|
||||
"Setting this to true allows the ACS user to request an email to reset their password",
|
||||
false,
|
||||
ConfigKey.Scope.Global);
|
||||
|
||||
ConfigKey<Long> UserPasswordResetTtl = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Long.class,
|
||||
"user.password.reset.ttl", "30",
|
||||
"TTL in minutes for the token generated to reset the ACS user's password", true,
|
||||
ConfigKey.Scope.Global);
|
||||
|
||||
ConfigKey<String> UserPasswordResetEmailSender = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED,
|
||||
String.class, "user.password.reset.email.sender", null,
|
||||
"Sender for emails sent to the user to reset ACS user's password ", true,
|
||||
ConfigKey.Scope.Global);
|
||||
|
||||
ConfigKey<String> UserPasswordResetSMTPHost = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED,
|
||||
String.class, "user.password.reset.smtp.host", null,
|
||||
"Host for SMTP server for sending emails for resetting password for ACS users",
|
||||
false,
|
||||
ConfigKey.Scope.Global);
|
||||
|
||||
ConfigKey<Integer> UserPasswordResetSMTPPort = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED,
|
||||
Integer.class, "user.password.reset.smtp.port", "25",
|
||||
"Port for SMTP server for sending emails for resetting password for ACS users",
|
||||
false,
|
||||
ConfigKey.Scope.Global);
|
||||
|
||||
ConfigKey<Boolean> UserPasswordResetSMTPUseAuth = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED,
|
||||
Boolean.class, "user.password.reset.smtp.useAuth", "false",
|
||||
"Use auth in the SMTP server for sending emails for resetting password for ACS users",
|
||||
false, ConfigKey.Scope.Global);
|
||||
|
||||
ConfigKey<String> UserPasswordResetSMTPUsername = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED,
|
||||
String.class, "user.password.reset.smtp.username", null,
|
||||
"Username for SMTP server for sending emails for resetting password for ACS users",
|
||||
false, ConfigKey.Scope.Global);
|
||||
|
||||
ConfigKey<String> UserPasswordResetSMTPPassword = new ConfigKey<>("Secure", String.class,
|
||||
"user.password.reset.smtp.password", null,
|
||||
"Password for SMTP server for sending emails for resetting password for ACS users",
|
||||
false, ConfigKey.Scope.Global);
|
||||
|
||||
void setResetTokenAndSend(UserAccount userAccount);
|
||||
|
||||
boolean validateAndResetPassword(UserAccount user, String token, String password);
|
||||
}
|
||||
@ -0,0 +1,312 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
package org.apache.cloudstack.user;
|
||||
|
||||
import com.cloud.user.AccountManager;
|
||||
import com.cloud.user.UserAccount;
|
||||
import com.cloud.user.UserVO;
|
||||
import com.cloud.user.dao.UserDao;
|
||||
import com.cloud.utils.StringUtils;
|
||||
import com.cloud.utils.component.ManagerBase;
|
||||
import com.github.mustachejava.DefaultMustacheFactory;
|
||||
import com.github.mustachejava.Mustache;
|
||||
import com.github.mustachejava.MustacheFactory;
|
||||
import org.apache.cloudstack.api.ApiErrorCode;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.framework.config.ConfigKey;
|
||||
import org.apache.cloudstack.framework.config.Configurable;
|
||||
import org.apache.cloudstack.resourcedetail.UserDetailVO;
|
||||
import org.apache.cloudstack.resourcedetail.dao.UserDetailsDao;
|
||||
import org.apache.cloudstack.utils.mailing.MailAddress;
|
||||
import org.apache.cloudstack.utils.mailing.SMTPMailProperties;
|
||||
import org.apache.cloudstack.utils.mailing.SMTPMailSender;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.naming.ConfigurationException;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.apache.cloudstack.config.ApiServiceConfiguration.ManagementServerAddresses;
|
||||
import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordResetToken;
|
||||
import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordResetTokenExpiryDate;
|
||||
|
||||
public class UserPasswordResetManagerImpl extends ManagerBase implements UserPasswordResetManager, Configurable {
|
||||
|
||||
@Inject
|
||||
private AccountManager accountManager;
|
||||
|
||||
@Inject
|
||||
private UserDetailsDao userDetailsDao;
|
||||
|
||||
@Inject
|
||||
private UserDao userDao;
|
||||
|
||||
private SMTPMailSender mailSender;
|
||||
|
||||
public static ConfigKey<String> PasswordResetMailTemplate =
|
||||
new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, String.class,
|
||||
"user.password.reset.mail.template", "Hello {{username}}!\n" +
|
||||
"You have requested to reset your password. Please click the following link to reset your password:\n" +
|
||||
"http://{{{resetLink}}}\n" +
|
||||
"If you did not request a password reset, please ignore this email.\n" +
|
||||
"\n" +
|
||||
"Regards,\n" +
|
||||
"The CloudStack Team",
|
||||
"Password reset mail template. This uses mustache template engine. Available " +
|
||||
"variables are: username, firstName, lastName, resetLink, token",
|
||||
true,
|
||||
ConfigKey.Scope.Global);
|
||||
|
||||
@Override
|
||||
public String getConfigComponentName() {
|
||||
return UserPasswordResetManagerImpl.class.getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConfigKey<?>[] getConfigKeys() {
|
||||
return new ConfigKey<?>[]{
|
||||
UserPasswordResetEnabled,
|
||||
UserPasswordResetTtl,
|
||||
UserPasswordResetEmailSender,
|
||||
UserPasswordResetSMTPHost,
|
||||
UserPasswordResetSMTPPort,
|
||||
UserPasswordResetSMTPUseAuth,
|
||||
UserPasswordResetSMTPUsername,
|
||||
UserPasswordResetSMTPPassword,
|
||||
PasswordResetMailTemplate
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
|
||||
String smtpHost = UserPasswordResetSMTPHost.value();
|
||||
Integer smtpPort = UserPasswordResetSMTPPort.value();
|
||||
Boolean useAuth = UserPasswordResetSMTPUseAuth.value();
|
||||
String username = UserPasswordResetSMTPUsername.value();
|
||||
String password = UserPasswordResetSMTPPassword.value();
|
||||
|
||||
if (!StringUtils.isEmpty(smtpHost) && smtpPort != null && smtpPort > 0) {
|
||||
String namespace = "password.reset.smtp";
|
||||
|
||||
Map<String, String> configs = new HashMap<>();
|
||||
|
||||
configs.put(getKey(namespace, SMTPMailSender.CONFIG_HOST), smtpHost);
|
||||
configs.put(getKey(namespace, SMTPMailSender.CONFIG_PORT), smtpPort.toString());
|
||||
configs.put(getKey(namespace, SMTPMailSender.CONFIG_USE_AUTH), useAuth.toString());
|
||||
configs.put(getKey(namespace, SMTPMailSender.CONFIG_USERNAME), username);
|
||||
configs.put(getKey(namespace, SMTPMailSender.CONFIG_PASSWORD), password);
|
||||
|
||||
mailSender = new SMTPMailSender(configs, namespace);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String getKey(String namespace, String config) {
|
||||
return String.format("%s.%s", namespace, config);
|
||||
}
|
||||
|
||||
|
||||
protected boolean validateExistingToken(UserAccount userAccount) {
|
||||
|
||||
Map<String, String> details = userDetailsDao.listDetailsKeyPairs(userAccount.getId());
|
||||
|
||||
String resetToken = details.get(PasswordResetToken);
|
||||
String resetTokenExpiryTimeString = details.getOrDefault(PasswordResetTokenExpiryDate, "0");
|
||||
|
||||
|
||||
if (StringUtils.isNotEmpty(resetToken) && StringUtils.isNotEmpty(resetTokenExpiryTimeString)) {
|
||||
final Date resetTokenExpiryTime = new Date(Long.parseLong(resetTokenExpiryTimeString));
|
||||
final Date currentTime = new Date();
|
||||
if (currentTime.after(resetTokenExpiryTime)) {
|
||||
return true;
|
||||
}
|
||||
} else if (StringUtils.isEmpty(resetToken)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setResetTokenAndSend(UserAccount userAccount) {
|
||||
if (mailSender == null) {
|
||||
logger.debug("Failed to reset token and send email. SMTP mail sender is not configured.");
|
||||
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR,
|
||||
"Failed to reset token and send email. SMTP mail sender is not configured");
|
||||
}
|
||||
|
||||
if (!validateExistingToken(userAccount)) {
|
||||
logger.debug(String.format(
|
||||
"Failed to reset token and send email. Password reset token is already set for user %s in " +
|
||||
"domain id: %s with account %s and email %s",
|
||||
userAccount.getUsername(), userAccount.getDomainId(),
|
||||
userAccount.getAccountName(), userAccount.getEmail()));
|
||||
return;
|
||||
}
|
||||
|
||||
final String resetToken = UUID.randomUUID().toString();
|
||||
final Date resetTokenExpiryTime = new Date(System.currentTimeMillis() + UserPasswordResetTtl.value() * 60 * 1000);
|
||||
|
||||
userDetailsDao.addDetail(userAccount.getId(), PasswordResetToken, resetToken, false);
|
||||
userDetailsDao.addDetail(userAccount.getId(), PasswordResetTokenExpiryDate, String.valueOf(resetTokenExpiryTime.getTime()), false);
|
||||
|
||||
final String email = userAccount.getEmail();
|
||||
final String username = userAccount.getUsername();
|
||||
final String subject = "Password Reset Request";
|
||||
|
||||
String resetLink = String.format("%s/client/#/user/resetPassword?username=%s&token=%s",
|
||||
ManagementServerAddresses.value().split(",")[0], username, resetToken);
|
||||
String content = getMessageBody(userAccount, resetToken, resetLink);
|
||||
|
||||
SMTPMailProperties mailProperties = new SMTPMailProperties();
|
||||
|
||||
mailProperties.setSender(new MailAddress(UserPasswordResetEmailSender.value()));
|
||||
mailProperties.setSubject(subject);
|
||||
mailProperties.setContent(content);
|
||||
mailProperties.setContentType("text/html; charset=utf-8");
|
||||
|
||||
Set<MailAddress> addresses = new HashSet<>();
|
||||
|
||||
addresses.add(new MailAddress(email));
|
||||
|
||||
mailProperties.setRecipients(addresses);
|
||||
|
||||
mailSender.sendMail(mailProperties);
|
||||
logger.debug(String.format(
|
||||
"User password reset email for user id: %d username: %s account id: %d" +
|
||||
" domain id:%d sent to %s with token expiry at %s",
|
||||
userAccount.getId(), username, userAccount.getAccountId(),
|
||||
userAccount.getDomainId(), email, resetTokenExpiryTime));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateAndResetPassword(UserAccount user, String token, String password) {
|
||||
UserDetailVO resetTokenDetail = userDetailsDao.findDetail(user.getId(), PasswordResetToken);
|
||||
UserDetailVO resetTokenExpiryDate = userDetailsDao.findDetail(user.getId(), PasswordResetTokenExpiryDate);
|
||||
|
||||
if (resetTokenDetail == null || resetTokenExpiryDate == null) {
|
||||
logger.debug(String.format(
|
||||
"Failed to reset password. No reset token found for user id: %d username: %s account" +
|
||||
" id: %d domain id: %d",
|
||||
user.getId(), user.getUsername(), user.getAccountId(), user.getDomainId()));
|
||||
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("No reset token found for user %s", user.getUsername()));
|
||||
}
|
||||
|
||||
Date resetTokenExpiryTime = new Date(Long.parseLong(resetTokenExpiryDate.getValue()));
|
||||
|
||||
Date now = new Date();
|
||||
String resetToken = resetTokenDetail.getValue();
|
||||
if (StringUtils.isEmpty(resetToken)) {
|
||||
logger.debug(String.format(
|
||||
"Failed to reset password. No reset token found for user id: %d username: %s account" +
|
||||
" id: %d domain id: %d",
|
||||
user.getId(), user.getUsername(), user.getAccountId(), user.getDomainId()));
|
||||
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("No reset token found for user %s", user.getUsername()));
|
||||
}
|
||||
if (!resetToken.equals(token)) {
|
||||
logger.debug(String.format(
|
||||
"Failed to reset password. Invalid reset token for user id: %d username: %s " +
|
||||
"account id: %d domain id: %d",
|
||||
user.getId(), user.getUsername(), user.getAccountId(), user.getDomainId()));
|
||||
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Invalid reset token for user %s", user.getUsername()));
|
||||
}
|
||||
if (now.after(resetTokenExpiryTime)) {
|
||||
logger.debug(String.format(
|
||||
"Failed to reset password. Reset token has expired for user id: %d username: %s " +
|
||||
"account id: %d domain id: %d",
|
||||
user.getId(), user.getUsername(), user.getAccountId(), user.getDomainId()));
|
||||
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Reset token has expired for user %s", user.getUsername()));
|
||||
}
|
||||
|
||||
resetPassword(user, password);
|
||||
logger.debug(String.format(
|
||||
"Password reset successful for user id: %d username: %s account id: %d domain id: %d",
|
||||
user.getId(), user.getUsername(), user.getAccountId(), user.getDomainId()));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void resetPassword(UserAccount userAccount, String password) {
|
||||
UserVO user = userDao.getUser(userAccount.getId());
|
||||
|
||||
accountManager.validateUserPasswordAndUpdateIfNeeded(password, user, "", true);
|
||||
|
||||
userDetailsDao.removeDetail(userAccount.getId(), PasswordResetToken);
|
||||
userDetailsDao.removeDetail(userAccount.getId(), PasswordResetTokenExpiryDate);
|
||||
|
||||
userDao.persist(user);
|
||||
}
|
||||
|
||||
String getMessageBody(UserAccount userAccount, String token, String resetLink) {
|
||||
MustacheFactory mf = new DefaultMustacheFactory();
|
||||
Mustache mustache = mf.compile(new StringReader(PasswordResetMailTemplate.value()), "password.reset.mail");
|
||||
StringWriter writer = new StringWriter();
|
||||
|
||||
PasswordResetMail values = new PasswordResetMail(userAccount.getUsername(), userAccount.getFirstname(), userAccount.getLastname(), resetLink, token);
|
||||
|
||||
try {
|
||||
mustache.execute(writer, values).flush();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return writer.toString();
|
||||
|
||||
}
|
||||
|
||||
static class PasswordResetMail {
|
||||
private String username;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String resetLink;
|
||||
private String token;
|
||||
|
||||
|
||||
public PasswordResetMail(String username, String firstName, String lastName, String resetLink, String token) {
|
||||
this.username = username;
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
this.resetLink = resetLink;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public String getResetLink() {
|
||||
return resetLink;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,6 +56,7 @@
|
||||
</bean>
|
||||
|
||||
<bean id="passwordPolicies" class="com.cloud.user.PasswordPolicyImpl" />
|
||||
<bean id="passwordReset" class="org.apache.cloudstack.user.UserPasswordResetManagerImpl" />
|
||||
|
||||
<bean id="managementServerImpl" class="com.cloud.server.ManagementServerImpl">
|
||||
<property name="lockControllerListener" ref="lockControllerListener" />
|
||||
|
||||
@ -16,23 +16,53 @@
|
||||
// under the License.
|
||||
package com.cloud.api;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.cloud.domain.Domain;
|
||||
import com.cloud.user.UserAccount;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
import org.apache.cloudstack.framework.config.ConfigKey;
|
||||
import org.apache.cloudstack.user.UserPasswordResetManager;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedConstruction;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ApiServerTest {
|
||||
|
||||
@InjectMocks
|
||||
ApiServer apiServer = new ApiServer();
|
||||
|
||||
@Mock
|
||||
UserPasswordResetManager userPasswordResetManager;
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass() throws Exception {
|
||||
overrideDefaultConfigValue(UserPasswordResetEnabled, "_value", true);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void afterClass() throws Exception {
|
||||
overrideDefaultConfigValue(UserPasswordResetEnabled, "_value", false);
|
||||
}
|
||||
|
||||
private static void overrideDefaultConfigValue(final ConfigKey configKey, final String name, final Object o) throws IllegalAccessException, NoSuchFieldException {
|
||||
Field f = ConfigKey.class.getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
f.set(configKey, o);
|
||||
}
|
||||
|
||||
private void runTestSetupIntegrationPortListenerInvalidPorts(Integer port) {
|
||||
try (MockedConstruction<ApiServer.ListenerThread> mocked =
|
||||
Mockito.mockConstruction(ApiServer.ListenerThread.class)) {
|
||||
@ -61,4 +91,60 @@ public class ApiServerTest {
|
||||
Mockito.verify(listenerThread).start();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForgotPasswordSuccess() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Domain domain = Mockito.mock(Domain.class);
|
||||
|
||||
Mockito.when(userAccount.getEmail()).thenReturn("test@test.com");
|
||||
Mockito.when(userAccount.getState()).thenReturn("ENABLED");
|
||||
Mockito.when(userAccount.getAccountState()).thenReturn("ENABLED");
|
||||
Mockito.when(domain.getState()).thenReturn(Domain.State.Active);
|
||||
Mockito.doNothing().when(userPasswordResetManager).setResetTokenAndSend(userAccount);
|
||||
Assert.assertTrue(apiServer.forgotPassword(userAccount, domain));
|
||||
Mockito.verify(userPasswordResetManager).setResetTokenAndSend(userAccount);
|
||||
}
|
||||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void testForgotPasswordFailureNoEmail() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Domain domain = Mockito.mock(Domain.class);
|
||||
|
||||
Mockito.when(userAccount.getEmail()).thenReturn("");
|
||||
apiServer.forgotPassword(userAccount, domain);
|
||||
}
|
||||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void testForgotPasswordFailureDisabledUser() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Domain domain = Mockito.mock(Domain.class);
|
||||
|
||||
Mockito.when(userAccount.getEmail()).thenReturn("test@test.com");
|
||||
Mockito.when(userAccount.getState()).thenReturn("DISABLED");
|
||||
apiServer.forgotPassword(userAccount, domain);
|
||||
}
|
||||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void testForgotPasswordFailureDisabledAccount() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Domain domain = Mockito.mock(Domain.class);
|
||||
|
||||
Mockito.when(userAccount.getEmail()).thenReturn("test@test.com");
|
||||
Mockito.when(userAccount.getState()).thenReturn("ENABLED");
|
||||
Mockito.when(userAccount.getAccountState()).thenReturn("DISABLED");
|
||||
apiServer.forgotPassword(userAccount, domain);
|
||||
}
|
||||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void testForgotPasswordFailureInactiveDomain() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Domain domain = Mockito.mock(Domain.class);
|
||||
|
||||
Mockito.when(userAccount.getEmail()).thenReturn("test@test.com");
|
||||
Mockito.when(userAccount.getState()).thenReturn("ENABLED");
|
||||
Mockito.when(userAccount.getAccountState()).thenReturn("ENABLED");
|
||||
Mockito.when(domain.getState()).thenReturn(Domain.State.Inactive);
|
||||
apiServer.forgotPassword(userAccount, domain);
|
||||
}
|
||||
}
|
||||
|
||||
@ -405,7 +405,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
|
||||
Mockito.doNothing().when(accountManagerImpl).validateAndUpdateFirstNameIfNeeded(UpdateUserCmdMock, userVoMock);
|
||||
Mockito.doNothing().when(accountManagerImpl).validateAndUpdateLastNameIfNeeded(UpdateUserCmdMock, userVoMock);
|
||||
Mockito.doNothing().when(accountManagerImpl).validateAndUpdateUsernameIfNeeded(UpdateUserCmdMock, userVoMock, accountMock);
|
||||
Mockito.doNothing().when(accountManagerImpl).validateUserPasswordAndUpdateIfNeeded(Mockito.anyString(), Mockito.eq(userVoMock), Mockito.anyString());
|
||||
Mockito.doNothing().when(accountManagerImpl).validateUserPasswordAndUpdateIfNeeded(Mockito.anyString(), Mockito.eq(userVoMock), Mockito.anyString(), Mockito.eq(false));
|
||||
|
||||
Mockito.doReturn(true).when(userDaoMock).update(Mockito.anyLong(), Mockito.eq(userVoMock));
|
||||
Mockito.doReturn(Mockito.mock(UserAccountVO.class)).when(userAccountDaoMock).findById(Mockito.anyLong());
|
||||
@ -421,7 +421,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
|
||||
inOrder.verify(accountManagerImpl).validateAndUpdateFirstNameIfNeeded(UpdateUserCmdMock, userVoMock);
|
||||
inOrder.verify(accountManagerImpl).validateAndUpdateLastNameIfNeeded(UpdateUserCmdMock, userVoMock);
|
||||
inOrder.verify(accountManagerImpl).validateAndUpdateUsernameIfNeeded(UpdateUserCmdMock, userVoMock, accountMock);
|
||||
inOrder.verify(accountManagerImpl).validateUserPasswordAndUpdateIfNeeded(UpdateUserCmdMock.getPassword(), userVoMock, UpdateUserCmdMock.getCurrentPassword());
|
||||
inOrder.verify(accountManagerImpl).validateUserPasswordAndUpdateIfNeeded(UpdateUserCmdMock.getPassword(), userVoMock, UpdateUserCmdMock.getCurrentPassword(), false);
|
||||
|
||||
inOrder.verify(userVoMock, Mockito.times(numberOfExpectedCallsForSetEmailAndSetTimeZone)).setEmail(Mockito.anyString());
|
||||
inOrder.verify(userVoMock, Mockito.times(numberOfExpectedCallsForSetEmailAndSetTimeZone)).setTimezone(Mockito.anyString());
|
||||
@ -707,14 +707,14 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
|
||||
|
||||
@Test
|
||||
public void valiateUserPasswordAndUpdateIfNeededTestPasswordNull() {
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(null, userVoMock, null);
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(null, userVoMock, null, false);
|
||||
|
||||
Mockito.verify(userVoMock, Mockito.times(0)).setPassword(Mockito.anyString());
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
public void valiateUserPasswordAndUpdateIfNeededTestBlankPassword() {
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(" ", userVoMock, null);
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(" ", userVoMock, null, false);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
@ -728,7 +728,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
|
||||
|
||||
Mockito.lenient().doNothing().when(passwordPolicyMock).verifyIfPasswordCompliesWithPasswordPolicies(Mockito.anyString(), Mockito.anyString(), Mockito.anyLong());
|
||||
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded("newPassword", userVoMock, " ");
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded("newPassword", userVoMock, " ", false);
|
||||
}
|
||||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
@ -743,7 +743,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
|
||||
|
||||
Mockito.lenient().doNothing().when(passwordPolicyMock).verifyIfPasswordCompliesWithPasswordPolicies(Mockito.anyString(), Mockito.anyString(), Mockito.anyLong());
|
||||
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded("newPassword", userVoMock, null);
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded("newPassword", userVoMock, null, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -762,7 +762,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
|
||||
|
||||
Mockito.lenient().doNothing().when(passwordPolicyMock).verifyIfPasswordCompliesWithPasswordPolicies(Mockito.anyString(), Mockito.anyString(), Mockito.anyLong());
|
||||
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(newPassword, userVoMock, null);
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(newPassword, userVoMock, null, false);
|
||||
|
||||
Mockito.verify(accountManagerImpl, Mockito.times(0)).validateCurrentPassword(Mockito.eq(userVoMock), Mockito.anyString());
|
||||
Mockito.verify(userVoMock, Mockito.times(1)).setPassword(expectedUserPasswordAfterEncoded);
|
||||
@ -784,7 +784,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
|
||||
|
||||
Mockito.lenient().doNothing().when(passwordPolicyMock).verifyIfPasswordCompliesWithPasswordPolicies(Mockito.anyString(), Mockito.anyString(), Mockito.anyLong());
|
||||
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(newPassword, userVoMock, null);
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(newPassword, userVoMock, null, false);
|
||||
|
||||
Mockito.verify(accountManagerImpl, Mockito.times(0)).validateCurrentPassword(Mockito.eq(userVoMock), Mockito.anyString());
|
||||
Mockito.verify(userVoMock, Mockito.times(1)).setPassword(expectedUserPasswordAfterEncoded);
|
||||
@ -807,7 +807,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
|
||||
|
||||
Mockito.lenient().doNothing().when(passwordPolicyMock).verifyIfPasswordCompliesWithPasswordPolicies(Mockito.anyString(), Mockito.anyString(), Mockito.anyLong());
|
||||
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(newPassword, userVoMock, currentPassword);
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(newPassword, userVoMock, currentPassword, false);
|
||||
|
||||
Mockito.verify(accountManagerImpl, Mockito.times(1)).validateCurrentPassword(userVoMock, currentPassword);
|
||||
Mockito.verify(userVoMock, Mockito.times(1)).setPassword(expectedUserPasswordAfterEncoded);
|
||||
@ -826,7 +826,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
|
||||
Mockito.doThrow(new InvalidParameterValueException("")).when(passwordPolicyMock).verifyIfPasswordCompliesWithPasswordPolicies(Mockito.anyString(), Mockito.anyString(),
|
||||
Mockito.anyLong());
|
||||
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(newPassword, userVoMock, currentPassword);
|
||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(newPassword, userVoMock, currentPassword, false);
|
||||
}
|
||||
|
||||
private String configureUserMockAuthenticators(String newPassword) {
|
||||
|
||||
@ -487,4 +487,8 @@ public class MockAccountManagerImpl extends ManagerBase implements Manager, Acco
|
||||
public List<String> getApiNameList() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateUserPasswordAndUpdateIfNeeded(String newPassword, UserVO user, String currentPassword, boolean skipCurrentPassValidation) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,150 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
package org.apache.cloudstack.user;
|
||||
|
||||
import com.cloud.user.UserAccount;
|
||||
import org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.framework.config.ConfigKey;
|
||||
import org.apache.cloudstack.resourcedetail.UserDetailVO;
|
||||
import org.apache.cloudstack.resourcedetail.dao.UserDetailsDao;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordResetToken;
|
||||
import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordResetTokenExpiryDate;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class UserPasswordResetManagerImplTest {
|
||||
@Spy
|
||||
@InjectMocks
|
||||
UserPasswordResetManagerImpl passwordReset;
|
||||
|
||||
@Mock
|
||||
private UserDetailsDao userDetailsDao;
|
||||
|
||||
@Test
|
||||
public void testGetMessageBody() {
|
||||
ConfigKey<String> passwordResetMailTemplate = Mockito.mock(ConfigKey.class);
|
||||
UserPasswordResetManagerImpl.PasswordResetMailTemplate = passwordResetMailTemplate;
|
||||
Mockito.when(passwordResetMailTemplate.value()).thenReturn("Hello {{username}}!\n" +
|
||||
"You have requested to reset your password. Please click the following link to reset your password:\n" +
|
||||
"{{{resetLink}}}\n" +
|
||||
"If you did not request a password reset, please ignore this email.\n" +
|
||||
"\n" +
|
||||
"Regards,\n" +
|
||||
"The CloudStack Team");
|
||||
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Mockito.when(userAccount.getUsername()).thenReturn("test_user");
|
||||
|
||||
String messageBody = passwordReset.getMessageBody(userAccount, "reset_token", "reset_link");
|
||||
String expectedMessageBody = "Hello test_user!\n" +
|
||||
"You have requested to reset your password. Please click the following link to reset your password:\n" +
|
||||
"reset_link\n" +
|
||||
"If you did not request a password reset, please ignore this email.\n" +
|
||||
"\n" +
|
||||
"Regards,\n" +
|
||||
"The CloudStack Team";
|
||||
Assert.assertEquals("Message body doesn't match", expectedMessageBody, messageBody);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateAndResetPassword() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Mockito.when(userAccount.getId()).thenReturn(1L);
|
||||
Mockito.when(userAccount.getUsername()).thenReturn("test_user");
|
||||
|
||||
Mockito.doNothing().when(passwordReset).resetPassword(userAccount, "new_password");
|
||||
|
||||
UserDetailVO resetTokenDetail = Mockito.mock(UserDetailVO.class);
|
||||
UserDetailVO resetTokenExpiryDate = Mockito.mock(UserDetailVO.class);
|
||||
Mockito.when(userDetailsDao.findDetail(1L, PasswordResetToken)).thenReturn(resetTokenDetail);
|
||||
Mockito.when(userDetailsDao.findDetail(1L, PasswordResetTokenExpiryDate)).thenReturn(resetTokenExpiryDate);
|
||||
Mockito.when(resetTokenExpiryDate.getValue()).thenReturn(String.valueOf(System.currentTimeMillis() - 5 * 60 * 1000));
|
||||
|
||||
try {
|
||||
passwordReset.validateAndResetPassword(userAccount, "reset_token", "new_password");
|
||||
Assert.fail("Should have thrown exception");
|
||||
} catch (ServerApiException e) {
|
||||
Assert.assertEquals("No reset token found for user test_user", e.getMessage());
|
||||
}
|
||||
|
||||
Mockito.when(resetTokenDetail.getValue()).thenReturn("reset_token_XXX");
|
||||
|
||||
try {
|
||||
passwordReset.validateAndResetPassword(userAccount, "reset_token", "new_password");
|
||||
Assert.fail("Should have thrown exception");
|
||||
} catch (ServerApiException e) {
|
||||
Assert.assertEquals("Invalid reset token for user test_user", e.getMessage());
|
||||
}
|
||||
|
||||
Mockito.when(resetTokenDetail.getValue()).thenReturn("reset_token");
|
||||
|
||||
try {
|
||||
passwordReset.validateAndResetPassword(userAccount, "reset_token", "new_password");
|
||||
Assert.fail("Should have thrown exception");
|
||||
} catch (ServerApiException e) {
|
||||
Assert.assertEquals("Reset token has expired for user test_user", e.getMessage());
|
||||
}
|
||||
|
||||
Mockito.when(resetTokenExpiryDate.getValue()).thenReturn(String.valueOf(System.currentTimeMillis() + 5 * 60 * 1000));
|
||||
|
||||
Assert.assertTrue(passwordReset.validateAndResetPassword(userAccount, "reset_token", "new_password"));
|
||||
Mockito.verify(passwordReset, Mockito.times(1)).resetPassword(userAccount, "new_password");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateExistingTokenFirstRequest() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Mockito.when(userAccount.getId()).thenReturn(1L);
|
||||
Mockito.when(userDetailsDao.listDetailsKeyPairs(1L)).thenReturn(Collections.emptyMap());
|
||||
|
||||
Assert.assertTrue(passwordReset.validateExistingToken(userAccount));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateExistingTokenSecondRequestExpired() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Mockito.when(userAccount.getId()).thenReturn(1L);
|
||||
Mockito.when(userDetailsDao.listDetailsKeyPairs(1L)).thenReturn(Map.of(
|
||||
PasswordResetToken, "reset_token",
|
||||
PasswordResetTokenExpiryDate, String.valueOf(System.currentTimeMillis() - 5 * 60 * 1000)));
|
||||
|
||||
Assert.assertTrue(passwordReset.validateExistingToken(userAccount));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testValidateExistingTokenSecondRequestUnexpired() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Mockito.when(userAccount.getId()).thenReturn(1L);
|
||||
Mockito.when(userDetailsDao.listDetailsKeyPairs(1L)).thenReturn(Map.of(
|
||||
PasswordResetToken, "reset_token",
|
||||
PasswordResetTokenExpiryDate, String.valueOf(System.currentTimeMillis() + 5 * 60 * 1000)));
|
||||
|
||||
Assert.assertFalse(passwordReset.validateExistingToken(userAccount));
|
||||
}
|
||||
}
|
||||
@ -282,12 +282,14 @@ known_categories = {
|
||||
'Webhook': 'Webhook',
|
||||
'Webhooks': 'Webhook',
|
||||
'purgeExpungedResources': 'Resource',
|
||||
'forgotPassword': 'Authentication',
|
||||
'resetPassword': 'Authentication',
|
||||
'BgpPeer': 'BGP Peer',
|
||||
'createASNRange': 'AS Number Range',
|
||||
'listASNRange': 'AS Number Range',
|
||||
'deleteASNRange': 'AS Number Range',
|
||||
'listASNumbers': 'AS Number',
|
||||
'releaseASNumber': 'AS Number'
|
||||
'releaseASNumber': 'AS Number',
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -417,6 +417,7 @@
|
||||
"label.availableprocessors": "Available processor cores",
|
||||
"label.availablevirtualmachinecount": "Available Instances",
|
||||
"label.back": "Back",
|
||||
"label.back.login": "Back to login",
|
||||
"label.backup": "Backups",
|
||||
"label.backup.attach.restore": "Restore and attach backup volume",
|
||||
"label.backup.configure.schedule": "Configure Backup Schedule",
|
||||
@ -1002,6 +1003,7 @@
|
||||
"label.force.reboot": "Force reboot",
|
||||
"label.forceencap": "Force UDP encapsulation of ESP packets",
|
||||
"label.forgedtransmits": "Forged transmits",
|
||||
"label.forgot.password": "Forgot password?",
|
||||
"label.format": "Format",
|
||||
"label.fornsx": "NSX",
|
||||
"label.forvpc": "VPC",
|
||||
@ -3174,6 +3176,7 @@
|
||||
"message.failed.to.add": "Failed to add",
|
||||
"message.failed.to.assign.vms": "Failed to assign Instances",
|
||||
"message.failed.to.remove": "Failed to remove",
|
||||
"message.forgot.password.success": "An email has been sent to your email address with instructions on how to reset your password.",
|
||||
"message.generate.keys": "Please confirm that you would like to generate new API/Secret keys for this User.",
|
||||
"message.chart.statistic.info": "The shown charts are self-adjustable, that means, if the value gets close to the limit or overpass it, it will grow to adjust the shown value",
|
||||
"message.chart.statistic.info.hypervisor.additionals": "The metrics data depend on the hypervisor plugin used for each hypervisor. The behavior can vary across different hypervisors. For instance, with KVM, metrics are real-time statistics provided by libvirt. In contrast, with VMware, the metrics are averaged data for a given time interval controlled by configuration.",
|
||||
@ -3301,6 +3304,8 @@
|
||||
"message.offering.internet.protocol.warning": "WARNING: IPv6 supported Networks use static routing and will require upstream routes to be configured manually.",
|
||||
"message.offering.ipv6.warning": "Please refer documentation for creating IPv6 enabled Network/VPC offering <a href='http://docs.cloudstack.apache.org/en/latest/plugins/ipv6.html#isolated-network-and-vpc-tier'>IPv6 support in CloudStack - Isolated Networks and VPC Network Tiers</a>",
|
||||
"message.ovf.configurations": "OVF configurations available for the selected appliance. Please select the desired value. Incompatible compute offerings will get disabled.",
|
||||
"message.password.reset.failed": "Failed to reset password.",
|
||||
"message.password.reset.success": "Password has been reset successfully. Please login using your new credentials.",
|
||||
"message.path": "Path : ",
|
||||
"message.path.description": "NFS: exported path from the server. VMFS: /datacenter name/datastore name. SharedMountPoint: path where primary storage is mounted, such as /mnt/primary.",
|
||||
"message.please.confirm.remove.ssh.key.pair": "Please confirm that you want to remove this SSH key pair.",
|
||||
|
||||
@ -300,6 +300,16 @@ export const constantRouterMap = [
|
||||
path: 'login',
|
||||
name: 'login',
|
||||
component: () => import(/* webpackChunkName: "auth" */ '@/views/auth/Login')
|
||||
},
|
||||
{
|
||||
path: 'forgotPassword',
|
||||
name: 'forgotPassword',
|
||||
component: () => import(/* webpackChunkName: "auth" */ '@/views/auth/ForgotPassword')
|
||||
},
|
||||
{
|
||||
path: 'resetPassword',
|
||||
name: 'resetPassword',
|
||||
component: () => import(/* webpackChunkName: "auth" */ '@/views/auth/ResetPassword')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -30,7 +30,7 @@ import { ACCESS_TOKEN, APIS, SERVER_MANAGER, CURRENT_PROJECT } from '@/store/mut
|
||||
|
||||
NProgress.configure({ showSpinner: false }) // NProgress Configuration
|
||||
|
||||
const allowList = ['login', 'VerifyOauth'] // no redirect allowlist
|
||||
const allowList = ['login', 'VerifyOauth', 'forgotPassword', 'resetPassword'] // no redirect allowlist
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
// start progress bar
|
||||
|
||||
@ -51,7 +51,7 @@ const err = (error) => {
|
||||
})
|
||||
}
|
||||
if (response.status === 401) {
|
||||
if (response.config && response.config.params && ['listIdps', 'cloudianIsEnabled'].includes(response.config.params.command)) {
|
||||
if (response.config && response.config.params && ['forgotPassword', 'listIdps', 'cloudianIsEnabled'].includes(response.config.params.command)) {
|
||||
return
|
||||
}
|
||||
const originalPath = router.currentRoute.value.fullPath
|
||||
|
||||
260
ui/src/views/auth/ForgotPassword.vue
Normal file
260
ui/src/views/auth/ForgotPassword.vue
Normal file
@ -0,0 +1,260 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
<template>
|
||||
<a-form
|
||||
id="formForgotPassword"
|
||||
class="user-layout-forgot-password"
|
||||
:ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
@finish="handleSubmit"
|
||||
v-ctrl-enter="handleSubmit"
|
||||
>
|
||||
<a-form-item v-if="$config.multipleServer" name="server" ref="server">
|
||||
<a-select
|
||||
size="large"
|
||||
:placeholder="$t('server')"
|
||||
v-model:value="form.server"
|
||||
@change="onChangeServer"
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
:filterOption="(input, option) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}">
|
||||
<a-select-option v-for="item in $config.servers" :key="(item.apiHost || '') + item.apiBase" :label="item.name">
|
||||
<template #prefix>
|
||||
<database-outlined />
|
||||
</template>
|
||||
{{ item.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item ref="username" name="username">
|
||||
<a-input
|
||||
size="large"
|
||||
type="text"
|
||||
v-focus="true"
|
||||
:placeholder="$t('label.username')"
|
||||
v-model:value="form.username"
|
||||
>
|
||||
<template #prefix>
|
||||
<user-outlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item ref="domain" name="domain">
|
||||
<a-input
|
||||
size="large"
|
||||
type="text"
|
||||
:placeholder="$t('label.domain')"
|
||||
v-model:value="form.domain"
|
||||
>
|
||||
<template #prefix>
|
||||
<block-outlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
size="large"
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
class="submit-button"
|
||||
:loading="submitBtn"
|
||||
:disabled="submitBtn"
|
||||
ref="submit"
|
||||
@click="handleSubmit"
|
||||
>{{ $t('label.submit') }}</a-button>
|
||||
</a-form-item>
|
||||
<a-row justify="space-between">
|
||||
<a-col>
|
||||
<translation-menu/>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<router-link :to="{ name: 'login' }">
|
||||
{{ $t('label.back.login') }}
|
||||
</router-link>
|
||||
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, toRaw } from 'vue'
|
||||
import { api } from '@/api'
|
||||
import store from '@/store'
|
||||
import { SERVER_MANAGER } from '@/store/mutation-types'
|
||||
import TranslationMenu from '@/components/header/TranslationMenu'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TranslationMenu
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
idps: [],
|
||||
customActiveKey: 'cs',
|
||||
customActiveKeyOauth: false,
|
||||
submitBtn: false,
|
||||
email: '',
|
||||
secretcode: '',
|
||||
oauthexclude: '',
|
||||
server: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.$config.multipleServer) {
|
||||
this.server = this.$localStorage.get(SERVER_MANAGER) || this.$config.servers[0]
|
||||
}
|
||||
this.initForm()
|
||||
},
|
||||
methods: {
|
||||
initForm () {
|
||||
this.formRef = ref()
|
||||
this.form = reactive({
|
||||
server: (this.server.apiHost || '') + this.server.apiBase
|
||||
})
|
||||
this.rules = {
|
||||
username: [{
|
||||
required: true,
|
||||
message: this.$t('message.error.username'),
|
||||
trigger: 'change'
|
||||
}]
|
||||
}
|
||||
},
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
if (this.submitBtn) return
|
||||
this.formRef.value.validate().then(() => {
|
||||
this.submitBtn = true
|
||||
|
||||
const values = toRaw(this.form)
|
||||
if (this.$config.multipleServer) {
|
||||
this.axios.defaults.baseURL = (this.server.apiHost || '') + this.server.apiBase
|
||||
store.dispatch('SetServer', this.server)
|
||||
}
|
||||
const loginParams = { ...values }
|
||||
delete loginParams.username
|
||||
loginParams.username = values.username
|
||||
loginParams.domain = values.domain
|
||||
if (!loginParams.domain) {
|
||||
loginParams.domain = '/'
|
||||
}
|
||||
api('forgotPassword', {}, 'POST', loginParams)
|
||||
.finally(() => {
|
||||
this.$message.success(this.$t('message.forgot.password.success'))
|
||||
this.$router.push({ path: '/login' }).catch(() => {})
|
||||
})
|
||||
}).catch(error => {
|
||||
this.formRef.value.scrollToField(error.errorFields[0].name)
|
||||
})
|
||||
},
|
||||
requestFailed (err) {
|
||||
if (err && err.response && err.response.data && err.response.data.forgotpasswordresponse) {
|
||||
const error = err.response.data.forgotpasswordresponse.errorcode + ': ' + err.response.data.forgotpasswordresponse.errortext
|
||||
this.$message.error(`${this.$t('label.error')} ${error}`)
|
||||
} else {
|
||||
this.$message.error(this.$t('message.password.reset.failed'))
|
||||
}
|
||||
},
|
||||
onChangeServer (server) {
|
||||
const servers = this.$config.servers || []
|
||||
const serverFilter = servers.filter(ser => (ser.apiHost || '') + ser.apiBase === server)
|
||||
this.server = serverFilter[0] || {}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.user-layout-forgot-password {
|
||||
min-width: 260px;
|
||||
width: 368px;
|
||||
margin: 0 auto;
|
||||
|
||||
.mobile & {
|
||||
max-width: 368px;
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button.submit-button {
|
||||
margin-top: 8px;
|
||||
padding: 0 15px;
|
||||
font-size: 16px;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-login-other {
|
||||
text-align: left;
|
||||
margin-top: 24px;
|
||||
line-height: 22px;
|
||||
|
||||
.item-icon {
|
||||
font-size: 24px;
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
margin-left: 16px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.register {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.g-btn-wrapper {
|
||||
background-color: rgb(221, 75, 57);
|
||||
height: 40px;
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
.center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 10px auto;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.or {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
background:
|
||||
linear-gradient(#CCC 0 0) left,
|
||||
linear-gradient(#CCC 0 0) right;
|
||||
background-size: 40% 1px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -152,7 +152,16 @@
|
||||
@click="handleSubmit"
|
||||
>{{ $t('label.login') }}</a-button>
|
||||
</a-form-item>
|
||||
<translation-menu/>
|
||||
<a-row justify="space-between">
|
||||
<a-col>
|
||||
<translation-menu/>
|
||||
</a-col>
|
||||
<a-col v-if="forgotPasswordEnabled">
|
||||
<router-link :to="{ name: 'forgotPassword' }">
|
||||
{{ $t('label.forgot.password') }}
|
||||
</router-link>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<div class="content" v-if="socialLogin">
|
||||
<p class="or">or</p>
|
||||
</div>
|
||||
@ -220,7 +229,8 @@ export default {
|
||||
loginBtn: false,
|
||||
loginType: 0
|
||||
},
|
||||
server: ''
|
||||
server: '',
|
||||
forgotPasswordEnabled: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
@ -303,6 +313,15 @@ export default {
|
||||
})
|
||||
}
|
||||
})
|
||||
api('forgotPassword', {}).then(response => {
|
||||
this.forgotPasswordEnabled = response.forgotpasswordresponse.enabled
|
||||
}).catch((err) => {
|
||||
if (err?.response?.data === null) {
|
||||
this.forgotPasswordEnabled = true
|
||||
} else {
|
||||
this.forgotPasswordEnabled = false
|
||||
}
|
||||
})
|
||||
},
|
||||
// handler
|
||||
async handleUsernameOrEmail (rule, value) {
|
||||
|
||||
318
ui/src/views/auth/ResetPassword.vue
Normal file
318
ui/src/views/auth/ResetPassword.vue
Normal file
@ -0,0 +1,318 @@
|
||||
// Licensed to the Apache Software Foundation (ASF) under one
|
||||
// or more contributor license agreements. See the NOTICE file
|
||||
// distributed with this work for additional information
|
||||
// regarding copyright ownership. The ASF licenses this file
|
||||
// to you under the Apache License, Version 2.0 (the
|
||||
// "License"); you may not use this file except in compliance
|
||||
// with the License. You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing,
|
||||
// software distributed under the License is distributed on an
|
||||
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
// KIND, either express or implied. See the License for the
|
||||
// specific language governing permissions and limitations
|
||||
// under the License.
|
||||
|
||||
<template>
|
||||
<a-form
|
||||
id="formResetPassword"
|
||||
class="user-layout-reset-password"
|
||||
:ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
@finish="handleSubmit"
|
||||
v-ctrl-enter="handleSubmit"
|
||||
>
|
||||
<a-form-item v-if="$config.multipleServer" name="server" ref="server">
|
||||
<a-select
|
||||
size="large"
|
||||
:placeholder="$t('server')"
|
||||
v-model:value="form.server"
|
||||
@change="onChangeServer"
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
:filterOption="(input, option) => {
|
||||
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}">
|
||||
<a-select-option v-for="item in $config.servers" :key="(item.apiHost || '') + item.apiBase" :label="item.name">
|
||||
<template #prefix>
|
||||
<database-outlined />
|
||||
</template>
|
||||
{{ item.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item ref="username" name="username">
|
||||
<a-input
|
||||
size="large"
|
||||
type="text"
|
||||
v-focus="true"
|
||||
:placeholder="$t('label.username')"
|
||||
v-model:value="form.username"
|
||||
>
|
||||
<template #prefix>
|
||||
<user-outlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item ref="domain" name="domain">
|
||||
<a-input
|
||||
size="large"
|
||||
type="text"
|
||||
:placeholder="$t('label.domain')"
|
||||
v-model:value="form.domain"
|
||||
>
|
||||
<template #prefix>
|
||||
<block-outlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item ref="password" name="password">
|
||||
<a-input-password
|
||||
size="large"
|
||||
type="password"
|
||||
autocomplete="false"
|
||||
:placeholder="$t('label.password')"
|
||||
v-model:value="form.password"
|
||||
>
|
||||
<template #prefix>
|
||||
<lock-outlined />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item ref="confirmpassword" name="confirmpassword">
|
||||
<a-input-password
|
||||
size="large"
|
||||
type="password"
|
||||
autocomplete="false"
|
||||
:placeholder="$t('label.confirmpassword.description')"
|
||||
v-model:value="form.confirmpassword"
|
||||
>
|
||||
<template #prefix>
|
||||
<lock-outlined />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
size="large"
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
class="reset-button"
|
||||
:loading="resetBtn"
|
||||
:disabled="resetBtn"
|
||||
ref="submit"
|
||||
@click="handleSubmit"
|
||||
>{{ $t('label.action.reset.password') }}</a-button>
|
||||
</a-form-item>
|
||||
<a-row justify="space-between">
|
||||
<a-col>
|
||||
<translation-menu/>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<router-link :to="{ name: 'login' }">
|
||||
{{ $t('label.back.login') }}
|
||||
</router-link>
|
||||
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, toRaw } from 'vue'
|
||||
import { api } from '@/api'
|
||||
import store from '@/store'
|
||||
import { SERVER_MANAGER } from '@/store/mutation-types'
|
||||
import TranslationMenu from '@/components/header/TranslationMenu'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TranslationMenu
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
idps: [],
|
||||
customActiveKey: 'cs',
|
||||
customActiveKeyOauth: false,
|
||||
resetBtn: false,
|
||||
email: '',
|
||||
secretcode: '',
|
||||
oauthexclude: '',
|
||||
server: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.$config.multipleServer) {
|
||||
this.server = this.$localStorage.get(SERVER_MANAGER) || this.$config.servers[0]
|
||||
}
|
||||
this.initForm()
|
||||
},
|
||||
methods: {
|
||||
initForm () {
|
||||
this.formRef = ref()
|
||||
this.form = reactive({
|
||||
server: (this.server.apiHost || '') + this.server.apiBase,
|
||||
username: this.$route.query?.username || '',
|
||||
token: this.$route.query?.token || ''
|
||||
})
|
||||
this.rules = {
|
||||
username: [{
|
||||
required: true,
|
||||
message: this.$t('message.error.username'),
|
||||
trigger: 'change'
|
||||
}],
|
||||
password: [{
|
||||
required: true,
|
||||
message: this.$t('message.error.password'),
|
||||
trigger: 'change'
|
||||
}],
|
||||
confirmpassword: [{
|
||||
required: true,
|
||||
message: this.$t('message.error.password'),
|
||||
trigger: 'change'
|
||||
},
|
||||
{
|
||||
validator: this.validateConfirmPassword,
|
||||
trigger: 'change'
|
||||
}]
|
||||
}
|
||||
},
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
if (this.resetBtn) return
|
||||
this.formRef.value.validate().then(() => {
|
||||
this.resetBtn = true
|
||||
|
||||
const values = toRaw(this.form)
|
||||
if (this.$config.multipleServer) {
|
||||
this.axios.defaults.baseURL = (this.server.apiHost || '') + this.server.apiBase
|
||||
store.dispatch('SetServer', this.server)
|
||||
}
|
||||
const loginParams = { ...values }
|
||||
loginParams.username = values.username
|
||||
loginParams.domain = values.domain
|
||||
if (!loginParams.domain) {
|
||||
loginParams.domain = '/'
|
||||
}
|
||||
|
||||
api('resetPassword', {}, 'POST', loginParams)
|
||||
.then((res) => {
|
||||
if (res?.resetpasswordresponse?.success) {
|
||||
this.$message.success(this.$t('message.password.reset.success'))
|
||||
this.$router.push({ name: 'login' })
|
||||
} else {
|
||||
this.$message.error(this.$t('message.password.reset.failed'))
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
this.$message.error(`${this.$t('message.password.reset.failed')} ${err?.response?.data}`)
|
||||
}).finally(() => {
|
||||
this.resetBtn = false
|
||||
})
|
||||
}).catch(error => {
|
||||
this.formRef.value.scrollToField(error.errorFields[0].name)
|
||||
})
|
||||
},
|
||||
onChangeServer (server) {
|
||||
const servers = this.$config.servers || []
|
||||
const serverFilter = servers.filter(ser => (ser.apiHost || '') + ser.apiBase === server)
|
||||
this.server = serverFilter[0] || {}
|
||||
},
|
||||
async validateConfirmPassword (rule, value) {
|
||||
if (!value || value.length === 0) {
|
||||
return Promise.resolve()
|
||||
} else if (rule.field === 'confirmpassword') {
|
||||
const messageConfirm = this.$t('error.password.not.match')
|
||||
const passwordVal = this.form.password
|
||||
if (passwordVal && passwordVal !== value) {
|
||||
return Promise.reject(messageConfirm)
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.user-layout-reset-password {
|
||||
min-width: 260px;
|
||||
width: 368px;
|
||||
margin: 0 auto;
|
||||
|
||||
.mobile & {
|
||||
max-width: 368px;
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button.reset-button {
|
||||
margin-top: 8px;
|
||||
padding: 0 15px;
|
||||
font-size: 16px;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-login-other {
|
||||
text-align: left;
|
||||
margin-top: 24px;
|
||||
line-height: 22px;
|
||||
|
||||
.item-icon {
|
||||
font-size: 24px;
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
margin-left: 16px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.register {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.g-btn-wrapper {
|
||||
background-color: rgb(221, 75, 57);
|
||||
height: 40px;
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
.center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 10px auto;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.or {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
background:
|
||||
linear-gradient(#CCC 0 0) left,
|
||||
linear-gradient(#CCC 0 0) right;
|
||||
background-size: 40% 1px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -48,16 +48,16 @@ public class SMTPMailSender {
|
||||
protected Session session = null;
|
||||
protected SMTPSessionProperties sessionProps;
|
||||
|
||||
protected static final String CONFIG_HOST = "host";
|
||||
protected static final String CONFIG_PORT = "port";
|
||||
protected static final String CONFIG_USE_AUTH = "useAuth";
|
||||
protected static final String CONFIG_USERNAME = "username";
|
||||
protected static final String CONFIG_PASSWORD = "password";
|
||||
protected static final String CONFIG_DEBUG_MODE = "debug";
|
||||
protected static final String CONFIG_USE_STARTTLS = "useStartTLS";
|
||||
protected static final String CONFIG_ENABLED_SECURITY_PROTOCOLS = "enabledSecurityProtocols";
|
||||
protected static final String CONFIG_TIMEOUT = "timeout";
|
||||
protected static final String CONFIG_CONNECTION_TIMEOUT = "connectiontimeout";
|
||||
public static final String CONFIG_HOST = "host";
|
||||
public static final String CONFIG_PORT = "port";
|
||||
public static final String CONFIG_USE_AUTH = "useAuth";
|
||||
public static final String CONFIG_USERNAME = "username";
|
||||
public static final String CONFIG_PASSWORD = "password";
|
||||
public static final String CONFIG_DEBUG_MODE = "debug";
|
||||
public static final String CONFIG_USE_STARTTLS = "useStartTLS";
|
||||
public static final String CONFIG_ENABLED_SECURITY_PROTOCOLS = "enabledSecurityProtocols";
|
||||
public static final String CONFIG_TIMEOUT = "timeout";
|
||||
public static final String CONFIG_CONNECTION_TIMEOUT = "connectiontimeout";
|
||||
|
||||
protected Map<String, String> configs;
|
||||
protected String namespace;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user