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 javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
import com.cloud.domain.Domain;
|
||||||
import com.cloud.exception.CloudAuthenticationException;
|
import com.cloud.exception.CloudAuthenticationException;
|
||||||
|
import com.cloud.user.UserAccount;
|
||||||
|
|
||||||
public interface ApiServerService {
|
public interface ApiServerService {
|
||||||
public boolean verifyRequest(Map<String, Object[]> requestParameters, Long userId, InetAddress remoteAddress) throws ServerApiException;
|
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 String handleRequest(Map<String, Object[]> params, String responseType, StringBuilder auditTrailSb) throws ServerApiException;
|
||||||
|
|
||||||
public Class<?> getCmdClass(String cmdName);
|
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;
|
package org.apache.cloudstack.api.auth;
|
||||||
|
|
||||||
public enum APIAuthenticationType {
|
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;
|
package com.cloud.user;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import javax.persistence.Column;
|
import javax.persistence.Column;
|
||||||
@ -361,6 +362,9 @@ public class UserAccountVO implements UserAccount, InternalIdentity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> getDetails() {
|
public Map<String, String> getDetails() {
|
||||||
|
if (details == null) {
|
||||||
|
details = new HashMap<>();
|
||||||
|
}
|
||||||
return details;
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,6 +46,8 @@ public class UserDetailVO implements ResourceDetail {
|
|||||||
private boolean display = true;
|
private boolean display = true;
|
||||||
|
|
||||||
public static final String Setup2FADetail = "2FASetupStatus";
|
public static final String Setup2FADetail = "2FASetupStatus";
|
||||||
|
public static final String PasswordResetToken = "PasswordResetToken";
|
||||||
|
public static final String PasswordResetTokenExpiryDate = "PasswordResetTokenExpiryDate";
|
||||||
|
|
||||||
public UserDetailVO() {
|
public UserDetailVO() {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -515,6 +515,11 @@ public class MockAccountManager extends ManagerBase implements AccountManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void validateUserPasswordAndUpdateIfNeeded(String newPassword, UserVO user,
|
||||||
|
String currentPassword,
|
||||||
|
boolean skipCurrentPassValidation) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void checkApiAccess(Account account, String command) throws PermissionDeniedException {
|
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.kafka-clients.version>2.7.0</cs.kafka-clients.version>
|
||||||
<cs.libvirt-java.version>0.5.3</cs.libvirt-java.version>
|
<cs.libvirt-java.version>0.5.3</cs.libvirt-java.version>
|
||||||
<cs.mail.version>1.5.0-b01</cs.mail.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.mysql.version>8.0.33</cs.mysql.version>
|
||||||
<cs.neethi.version>2.0.4</cs.neethi.version>
|
<cs.neethi.version>2.0.4</cs.neethi.version>
|
||||||
<cs.nitro.version>10.1</cs.nitro.version>
|
<cs.nitro.version>10.1</cs.nitro.version>
|
||||||
|
|||||||
@ -101,6 +101,11 @@
|
|||||||
<artifactId>commons-math3</artifactId>
|
<artifactId>commons-math3</artifactId>
|
||||||
<version>${cs.commons-math3.version}</version>
|
<version>${cs.commons-math3.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.spullara.mustache.java</groupId>
|
||||||
|
<artifactId>compiler</artifactId>
|
||||||
|
<version>${cs.mustache.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.cloudstack</groupId>
|
<groupId>org.apache.cloudstack</groupId>
|
||||||
<artifactId>cloud-utils</artifactId>
|
<artifactId>cloud-utils</artifactId>
|
||||||
|
|||||||
@ -55,6 +55,13 @@ import javax.naming.ConfigurationException;
|
|||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.servlet.http.HttpSession;
|
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.acl.APIChecker;
|
||||||
import org.apache.cloudstack.api.APICommand;
|
import org.apache.cloudstack.api.APICommand;
|
||||||
import org.apache.cloudstack.api.ApiConstants;
|
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.MessageDispatcher;
|
||||||
import org.apache.cloudstack.framework.messagebus.MessageHandler;
|
import org.apache.cloudstack.framework.messagebus.MessageHandler;
|
||||||
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
|
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
|
||||||
|
import org.apache.cloudstack.user.UserPasswordResetManager;
|
||||||
import org.apache.commons.codec.binary.Base64;
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
import org.apache.commons.lang3.EnumUtils;
|
||||||
import org.apache.http.ConnectionClosedException;
|
import org.apache.http.ConnectionClosedException;
|
||||||
import org.apache.http.HttpException;
|
import org.apache.http.HttpException;
|
||||||
import org.apache.http.HttpRequest;
|
import org.apache.http.HttpRequest;
|
||||||
@ -157,13 +166,6 @@ import com.cloud.exception.ResourceUnavailableException;
|
|||||||
import com.cloud.exception.UnavailableCommandException;
|
import com.cloud.exception.UnavailableCommandException;
|
||||||
import com.cloud.projects.dao.ProjectDao;
|
import com.cloud.projects.dao.ProjectDao;
|
||||||
import com.cloud.storage.VolumeApiService;
|
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.ConstantTimeComparator;
|
||||||
import com.cloud.utils.DateUtil;
|
import com.cloud.utils.DateUtil;
|
||||||
import com.cloud.utils.HttpUtils;
|
import com.cloud.utils.HttpUtils;
|
||||||
@ -182,6 +184,8 @@ import com.cloud.utils.exception.ExceptionProxyObject;
|
|||||||
import com.cloud.utils.net.NetUtils;
|
import com.cloud.utils.net.NetUtils;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiServerService, Configurable {
|
public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiServerService, Configurable {
|
||||||
|
|
||||||
@ -214,6 +218,8 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
|
|||||||
private ProjectDao projectDao;
|
private ProjectDao projectDao;
|
||||||
@Inject
|
@Inject
|
||||||
private UUIDManager uuidMgr;
|
private UUIDManager uuidMgr;
|
||||||
|
@Inject
|
||||||
|
private UserPasswordResetManager userPasswordResetManager;
|
||||||
|
|
||||||
private List<PluggableService> pluggableServices;
|
private List<PluggableService> pluggableServices;
|
||||||
|
|
||||||
@ -1223,6 +1229,57 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
|
|||||||
return true;
|
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 {
|
private void checkCommandAvailable(final User user, final String commandName, final InetAddress remoteAddress) throws PermissionDeniedException {
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new PermissionDeniedException("User is null for role based API access check for command" + commandName);
|
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.ComponentContext;
|
||||||
import com.cloud.utils.component.ManagerBase;
|
import com.cloud.utils.component.ManagerBase;
|
||||||
|
|
||||||
|
import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuthenticationManager {
|
public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuthenticationManager {
|
||||||
|
|
||||||
@ -75,6 +77,10 @@ public class APIAuthenticationManagerImpl extends ManagerBase implements APIAuth
|
|||||||
List<Class<?>> cmdList = new ArrayList<Class<?>>();
|
List<Class<?>> cmdList = new ArrayList<Class<?>>();
|
||||||
cmdList.add(DefaultLoginAPIAuthenticatorCmd.class);
|
cmdList.add(DefaultLoginAPIAuthenticatorCmd.class);
|
||||||
cmdList.add(DefaultLogoutAPIAuthenticatorCmd.class);
|
cmdList.add(DefaultLogoutAPIAuthenticatorCmd.class);
|
||||||
|
if (UserPasswordResetEnabled.value()) {
|
||||||
|
cmdList.add(DefaultForgotPasswordAPIAuthenticatorCmd.class);
|
||||||
|
cmdList.add(DefaultResetPasswordAPIAuthenticatorCmd.class);
|
||||||
|
}
|
||||||
|
|
||||||
cmdList.add(ListUserTwoFactorAuthenticatorProvidersCmd.class);
|
cmdList.add(ListUserTwoFactorAuthenticatorProvidersCmd.class);
|
||||||
cmdList.add(ValidateUserTwoFactorAuthenticationCodeCmd.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();
|
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);
|
validateAndUpdateLastNameIfNeeded(updateUserCmd, user);
|
||||||
validateAndUpdateUsernameIfNeeded(updateUserCmd, user, account);
|
validateAndUpdateUsernameIfNeeded(updateUserCmd, user, account);
|
||||||
|
|
||||||
validateUserPasswordAndUpdateIfNeeded(updateUserCmd.getPassword(), user, updateUserCmd.getCurrentPassword());
|
validateUserPasswordAndUpdateIfNeeded(updateUserCmd.getPassword(), user, updateUserCmd.getCurrentPassword(), false);
|
||||||
String email = updateUserCmd.getEmail();
|
String email = updateUserCmd.getEmail();
|
||||||
if (StringUtils.isNotBlank(email)) {
|
if (StringUtils.isNotBlank(email)) {
|
||||||
user.setEmail(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}.
|
* 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) {
|
if (newPassword == null) {
|
||||||
logger.trace("No new password to update for user: " + user.getUuid());
|
logger.trace("No new password to update for user: " + user.getUuid());
|
||||||
return;
|
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 isRootAdminExecutingPasswordUpdate = callingAccount.getId() == Account.ACCOUNT_ID_SYSTEM || isRootAdmin(callingAccount.getId());
|
||||||
boolean isDomainAdmin = isDomainAdmin(callingAccount.getId());
|
boolean isDomainAdmin = isDomainAdmin(callingAccount.getId());
|
||||||
boolean isAdmin = isDomainAdmin || isRootAdminExecutingPasswordUpdate;
|
boolean isAdmin = isDomainAdmin || isRootAdminExecutingPasswordUpdate;
|
||||||
|
boolean skipValidation = isAdmin || skipCurrentPassValidation;
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
logger.trace(String.format("Admin account [uuid=%s] executing password update for user [%s] ", callingAccount.getUuid(), user.getUuid()));
|
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.");
|
throw new InvalidParameterValueException("To set a new password the current password must be provided.");
|
||||||
}
|
}
|
||||||
if (CollectionUtils.isEmpty(_userPasswordEncoders)) {
|
if (CollectionUtils.isEmpty(_userPasswordEncoders)) {
|
||||||
throw new CloudRuntimeException("No user authenticators configured!");
|
throw new CloudRuntimeException("No user authenticators configured!");
|
||||||
}
|
}
|
||||||
if (!isAdmin) {
|
if (!skipValidation) {
|
||||||
validateCurrentPassword(user, currentPassword);
|
validateCurrentPassword(user, currentPassword);
|
||||||
}
|
}
|
||||||
UserAuthenticator userAuthenticator = _userPasswordEncoders.get(0);
|
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>
|
||||||
|
|
||||||
<bean id="passwordPolicies" class="com.cloud.user.PasswordPolicyImpl" />
|
<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">
|
<bean id="managementServerImpl" class="com.cloud.server.ManagementServerImpl">
|
||||||
<property name="lockControllerListener" ref="lockControllerListener" />
|
<property name="lockControllerListener" ref="lockControllerListener" />
|
||||||
|
|||||||
@ -16,23 +16,53 @@
|
|||||||
// under the License.
|
// under the License.
|
||||||
package com.cloud.api;
|
package com.cloud.api;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import com.cloud.domain.Domain;
|
||||||
import java.util.List;
|
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.Assert;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockedConstruction;
|
import org.mockito.MockedConstruction;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
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)
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
public class ApiServerTest {
|
public class ApiServerTest {
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
ApiServer apiServer = new ApiServer();
|
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) {
|
private void runTestSetupIntegrationPortListenerInvalidPorts(Integer port) {
|
||||||
try (MockedConstruction<ApiServer.ListenerThread> mocked =
|
try (MockedConstruction<ApiServer.ListenerThread> mocked =
|
||||||
Mockito.mockConstruction(ApiServer.ListenerThread.class)) {
|
Mockito.mockConstruction(ApiServer.ListenerThread.class)) {
|
||||||
@ -61,4 +91,60 @@ public class ApiServerTest {
|
|||||||
Mockito.verify(listenerThread).start();
|
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).validateAndUpdateFirstNameIfNeeded(UpdateUserCmdMock, userVoMock);
|
||||||
Mockito.doNothing().when(accountManagerImpl).validateAndUpdateLastNameIfNeeded(UpdateUserCmdMock, userVoMock);
|
Mockito.doNothing().when(accountManagerImpl).validateAndUpdateLastNameIfNeeded(UpdateUserCmdMock, userVoMock);
|
||||||
Mockito.doNothing().when(accountManagerImpl).validateAndUpdateUsernameIfNeeded(UpdateUserCmdMock, userVoMock, accountMock);
|
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(true).when(userDaoMock).update(Mockito.anyLong(), Mockito.eq(userVoMock));
|
||||||
Mockito.doReturn(Mockito.mock(UserAccountVO.class)).when(userAccountDaoMock).findById(Mockito.anyLong());
|
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).validateAndUpdateFirstNameIfNeeded(UpdateUserCmdMock, userVoMock);
|
||||||
inOrder.verify(accountManagerImpl).validateAndUpdateLastNameIfNeeded(UpdateUserCmdMock, userVoMock);
|
inOrder.verify(accountManagerImpl).validateAndUpdateLastNameIfNeeded(UpdateUserCmdMock, userVoMock);
|
||||||
inOrder.verify(accountManagerImpl).validateAndUpdateUsernameIfNeeded(UpdateUserCmdMock, userVoMock, accountMock);
|
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)).setEmail(Mockito.anyString());
|
||||||
inOrder.verify(userVoMock, Mockito.times(numberOfExpectedCallsForSetEmailAndSetTimeZone)).setTimezone(Mockito.anyString());
|
inOrder.verify(userVoMock, Mockito.times(numberOfExpectedCallsForSetEmailAndSetTimeZone)).setTimezone(Mockito.anyString());
|
||||||
@ -707,14 +707,14 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void valiateUserPasswordAndUpdateIfNeededTestPasswordNull() {
|
public void valiateUserPasswordAndUpdateIfNeededTestPasswordNull() {
|
||||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(null, userVoMock, null);
|
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(null, userVoMock, null, false);
|
||||||
|
|
||||||
Mockito.verify(userVoMock, Mockito.times(0)).setPassword(Mockito.anyString());
|
Mockito.verify(userVoMock, Mockito.times(0)).setPassword(Mockito.anyString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = InvalidParameterValueException.class)
|
@Test(expected = InvalidParameterValueException.class)
|
||||||
public void valiateUserPasswordAndUpdateIfNeededTestBlankPassword() {
|
public void valiateUserPasswordAndUpdateIfNeededTestBlankPassword() {
|
||||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(" ", userVoMock, null);
|
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(" ", userVoMock, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = InvalidParameterValueException.class)
|
@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());
|
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)
|
@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());
|
Mockito.lenient().doNothing().when(passwordPolicyMock).verifyIfPasswordCompliesWithPasswordPolicies(Mockito.anyString(), Mockito.anyString(), Mockito.anyLong());
|
||||||
|
|
||||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded("newPassword", userVoMock, null);
|
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded("newPassword", userVoMock, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -762,7 +762,7 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
|
|||||||
|
|
||||||
Mockito.lenient().doNothing().when(passwordPolicyMock).verifyIfPasswordCompliesWithPasswordPolicies(Mockito.anyString(), Mockito.anyString(), Mockito.anyLong());
|
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(accountManagerImpl, Mockito.times(0)).validateCurrentPassword(Mockito.eq(userVoMock), Mockito.anyString());
|
||||||
Mockito.verify(userVoMock, Mockito.times(1)).setPassword(expectedUserPasswordAfterEncoded);
|
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());
|
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(accountManagerImpl, Mockito.times(0)).validateCurrentPassword(Mockito.eq(userVoMock), Mockito.anyString());
|
||||||
Mockito.verify(userVoMock, Mockito.times(1)).setPassword(expectedUserPasswordAfterEncoded);
|
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());
|
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(accountManagerImpl, Mockito.times(1)).validateCurrentPassword(userVoMock, currentPassword);
|
||||||
Mockito.verify(userVoMock, Mockito.times(1)).setPassword(expectedUserPasswordAfterEncoded);
|
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.doThrow(new InvalidParameterValueException("")).when(passwordPolicyMock).verifyIfPasswordCompliesWithPasswordPolicies(Mockito.anyString(), Mockito.anyString(),
|
||||||
Mockito.anyLong());
|
Mockito.anyLong());
|
||||||
|
|
||||||
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(newPassword, userVoMock, currentPassword);
|
accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(newPassword, userVoMock, currentPassword, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String configureUserMockAuthenticators(String newPassword) {
|
private String configureUserMockAuthenticators(String newPassword) {
|
||||||
|
|||||||
@ -487,4 +487,8 @@ public class MockAccountManagerImpl extends ManagerBase implements Manager, Acco
|
|||||||
public List<String> getApiNameList() {
|
public List<String> getApiNameList() {
|
||||||
return null;
|
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',
|
'Webhook': 'Webhook',
|
||||||
'Webhooks': 'Webhook',
|
'Webhooks': 'Webhook',
|
||||||
'purgeExpungedResources': 'Resource',
|
'purgeExpungedResources': 'Resource',
|
||||||
|
'forgotPassword': 'Authentication',
|
||||||
|
'resetPassword': 'Authentication',
|
||||||
'BgpPeer': 'BGP Peer',
|
'BgpPeer': 'BGP Peer',
|
||||||
'createASNRange': 'AS Number Range',
|
'createASNRange': 'AS Number Range',
|
||||||
'listASNRange': 'AS Number Range',
|
'listASNRange': 'AS Number Range',
|
||||||
'deleteASNRange': 'AS Number Range',
|
'deleteASNRange': 'AS Number Range',
|
||||||
'listASNumbers': 'AS Number',
|
'listASNumbers': 'AS Number',
|
||||||
'releaseASNumber': 'AS Number'
|
'releaseASNumber': 'AS Number',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -417,6 +417,7 @@
|
|||||||
"label.availableprocessors": "Available processor cores",
|
"label.availableprocessors": "Available processor cores",
|
||||||
"label.availablevirtualmachinecount": "Available Instances",
|
"label.availablevirtualmachinecount": "Available Instances",
|
||||||
"label.back": "Back",
|
"label.back": "Back",
|
||||||
|
"label.back.login": "Back to login",
|
||||||
"label.backup": "Backups",
|
"label.backup": "Backups",
|
||||||
"label.backup.attach.restore": "Restore and attach backup volume",
|
"label.backup.attach.restore": "Restore and attach backup volume",
|
||||||
"label.backup.configure.schedule": "Configure Backup Schedule",
|
"label.backup.configure.schedule": "Configure Backup Schedule",
|
||||||
@ -1002,6 +1003,7 @@
|
|||||||
"label.force.reboot": "Force reboot",
|
"label.force.reboot": "Force reboot",
|
||||||
"label.forceencap": "Force UDP encapsulation of ESP packets",
|
"label.forceencap": "Force UDP encapsulation of ESP packets",
|
||||||
"label.forgedtransmits": "Forged transmits",
|
"label.forgedtransmits": "Forged transmits",
|
||||||
|
"label.forgot.password": "Forgot password?",
|
||||||
"label.format": "Format",
|
"label.format": "Format",
|
||||||
"label.fornsx": "NSX",
|
"label.fornsx": "NSX",
|
||||||
"label.forvpc": "VPC",
|
"label.forvpc": "VPC",
|
||||||
@ -3174,6 +3176,7 @@
|
|||||||
"message.failed.to.add": "Failed to add",
|
"message.failed.to.add": "Failed to add",
|
||||||
"message.failed.to.assign.vms": "Failed to assign Instances",
|
"message.failed.to.assign.vms": "Failed to assign Instances",
|
||||||
"message.failed.to.remove": "Failed to remove",
|
"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.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": "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.",
|
"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.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.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.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": "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.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.",
|
"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',
|
path: 'login',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
component: () => import(/* webpackChunkName: "auth" */ '@/views/auth/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
|
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) => {
|
router.beforeEach((to, from, next) => {
|
||||||
// start progress bar
|
// start progress bar
|
||||||
|
|||||||
@ -51,7 +51,7 @@ const err = (error) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (response.status === 401) {
|
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
|
return
|
||||||
}
|
}
|
||||||
const originalPath = router.currentRoute.value.fullPath
|
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"
|
@click="handleSubmit"
|
||||||
>{{ $t('label.login') }}</a-button>
|
>{{ $t('label.login') }}</a-button>
|
||||||
</a-form-item>
|
</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">
|
<div class="content" v-if="socialLogin">
|
||||||
<p class="or">or</p>
|
<p class="or">or</p>
|
||||||
</div>
|
</div>
|
||||||
@ -220,7 +229,8 @@ export default {
|
|||||||
loginBtn: false,
|
loginBtn: false,
|
||||||
loginType: 0
|
loginType: 0
|
||||||
},
|
},
|
||||||
server: ''
|
server: '',
|
||||||
|
forgotPasswordEnabled: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
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
|
// handler
|
||||||
async handleUsernameOrEmail (rule, value) {
|
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 Session session = null;
|
||||||
protected SMTPSessionProperties sessionProps;
|
protected SMTPSessionProperties sessionProps;
|
||||||
|
|
||||||
protected static final String CONFIG_HOST = "host";
|
public static final String CONFIG_HOST = "host";
|
||||||
protected static final String CONFIG_PORT = "port";
|
public static final String CONFIG_PORT = "port";
|
||||||
protected static final String CONFIG_USE_AUTH = "useAuth";
|
public static final String CONFIG_USE_AUTH = "useAuth";
|
||||||
protected static final String CONFIG_USERNAME = "username";
|
public static final String CONFIG_USERNAME = "username";
|
||||||
protected static final String CONFIG_PASSWORD = "password";
|
public static final String CONFIG_PASSWORD = "password";
|
||||||
protected static final String CONFIG_DEBUG_MODE = "debug";
|
public static final String CONFIG_DEBUG_MODE = "debug";
|
||||||
protected static final String CONFIG_USE_STARTTLS = "useStartTLS";
|
public static final String CONFIG_USE_STARTTLS = "useStartTLS";
|
||||||
protected static final String CONFIG_ENABLED_SECURITY_PROTOCOLS = "enabledSecurityProtocols";
|
public static final String CONFIG_ENABLED_SECURITY_PROTOCOLS = "enabledSecurityProtocols";
|
||||||
protected static final String CONFIG_TIMEOUT = "timeout";
|
public static final String CONFIG_TIMEOUT = "timeout";
|
||||||
protected static final String CONFIG_CONNECTION_TIMEOUT = "connectiontimeout";
|
public static final String CONFIG_CONNECTION_TIMEOUT = "connectiontimeout";
|
||||||
|
|
||||||
protected Map<String, String> configs;
|
protected Map<String, String> configs;
|
||||||
protected String namespace;
|
protected String namespace;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user