Skip to content

Commit f24c534

Browse files
committed
Feature: Forgot password
1 parent cc1dcf5 commit f24c534

File tree

1,644 files changed

+659672
-35
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,644 files changed

+659672
-35
lines changed

api/src/main/java/org/apache/cloudstack/api/ApiServerService.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import javax.servlet.http.HttpSession;
2323

2424
import com.cloud.exception.CloudAuthenticationException;
25+
import com.cloud.user.UserAccount;
2526

2627
public interface ApiServerService {
2728
public boolean verifyRequest(Map<String, Object[]> requestParameters, Long userId, InetAddress remoteAddress) throws ServerApiException;
@@ -42,4 +43,8 @@ public ResponseObject loginUser(HttpSession session, String username, String pas
4243
public String handleRequest(Map<String, Object[]> params, String responseType, StringBuilder auditTrailSb) throws ServerApiException;
4344

4445
public Class<?> getCmdClass(String cmdName);
46+
47+
boolean forgotPassword(UserAccount userAccount);
48+
49+
boolean resetPassword(UserAccount userAccount, String token, String password);
4550
}

api/src/main/java/org/apache/cloudstack/api/auth/APIAuthenticationType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717
package org.apache.cloudstack.api.auth;
1818

1919
public enum APIAuthenticationType {
20-
LOGIN_API, LOGOUT_API, READONLY_API, LOGIN_2FA_API
20+
LOGIN_API, LOGOUT_API, READONLY_API, LOGIN_2FA_API, PASSWORD_RESET
2121
}

engine/schema/src/main/java/com/cloud/user/UserAccountVO.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.cloud.user;
1818

1919
import java.util.Date;
20+
import java.util.HashMap;
2021
import java.util.Map;
2122

2223
import javax.persistence.Column;
@@ -361,6 +362,9 @@ public void setUser2faProvider(String user2faProvider) {
361362

362363
@Override
363364
public Map<String, String> getDetails() {
365+
if (details == null) {
366+
details = new HashMap<>();
367+
}
364368
return details;
365369
}
366370

engine/schema/src/main/java/org/apache/cloudstack/resourcedetail/UserDetailVO.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public class UserDetailVO implements ResourceDetail {
4646
private boolean display = true;
4747

4848
public static final String Setup2FADetail = "2FASetupStatus";
49+
public static final String PasswordResetToken = "PasswordResetToken";
50+
public static final String PasswordResetTokenExpiryDate = "PasswordResetTokenExpiryDate";
4951

5052
public UserDetailVO() {
5153
}

plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,4 +514,10 @@ public String getConfigComponentName() {
514514
public ConfigKey<?>[] getConfigKeys() {
515515
return null;
516516
}
517+
518+
public void validateUserPasswordAndUpdateIfNeeded(String newPassword, UserVO user,
519+
String currentPassword,
520+
boolean skipCurrentPassValidation) {
521+
522+
}
517523
}

server/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@
101101
<artifactId>commons-math3</artifactId>
102102
<version>${cs.commons-math3.version}</version>
103103
</dependency>
104+
<dependency>
105+
<groupId>com.github.spullara.mustache.java</groupId>
106+
<artifactId>compiler</artifactId>
107+
<version>0.9.14</version>
108+
</dependency>
104109
<dependency>
105110
<groupId>org.apache.cloudstack</groupId>
106111
<artifactId>cloud-utils</artifactId>

server/src/main/java/com/cloud/api/ApiServer.java

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@
5555
import javax.servlet.http.HttpServletResponse;
5656
import javax.servlet.http.HttpSession;
5757

58+
import com.cloud.user.Account;
59+
import com.cloud.user.AccountManager;
60+
import com.cloud.user.AccountManagerImpl;
61+
import com.cloud.user.DomainManager;
62+
import com.cloud.user.User;
63+
import com.cloud.user.UserAccount;
64+
import com.cloud.user.UserVO;
5865
import org.apache.cloudstack.acl.APIChecker;
5966
import org.apache.cloudstack.api.APICommand;
6067
import org.apache.cloudstack.api.ApiConstants;
@@ -103,6 +110,7 @@
103110
import org.apache.cloudstack.framework.messagebus.MessageDispatcher;
104111
import org.apache.cloudstack.framework.messagebus.MessageHandler;
105112
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
113+
import org.apache.cloudstack.user.PasswordReset;
106114
import org.apache.commons.codec.binary.Base64;
107115
import org.apache.http.ConnectionClosedException;
108116
import org.apache.http.HttpException;
@@ -157,13 +165,6 @@
157165
import com.cloud.exception.UnavailableCommandException;
158166
import com.cloud.projects.dao.ProjectDao;
159167
import com.cloud.storage.VolumeApiService;
160-
import com.cloud.user.Account;
161-
import com.cloud.user.AccountManager;
162-
import com.cloud.user.AccountManagerImpl;
163-
import com.cloud.user.DomainManager;
164-
import com.cloud.user.User;
165-
import com.cloud.user.UserAccount;
166-
import com.cloud.user.UserVO;
167168
import com.cloud.utils.ConstantTimeComparator;
168169
import com.cloud.utils.DateUtil;
169170
import com.cloud.utils.HttpUtils;
@@ -182,6 +183,9 @@
182183
import com.cloud.utils.net.NetUtils;
183184
import com.google.gson.reflect.TypeToken;
184185

186+
import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordResetToken;
187+
import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordResetTokenExpiryDate;
188+
185189
@Component
186190
public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiServerService, Configurable {
187191

@@ -214,6 +218,8 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
214218
private ProjectDao projectDao;
215219
@Inject
216220
private UUIDManager uuidMgr;
221+
@Inject
222+
private PasswordReset passwordReset;
217223

218224
private List<PluggableService> pluggableServices;
219225

@@ -1223,6 +1229,28 @@ public boolean verifyUser(final Long userId) {
12231229
return true;
12241230
}
12251231

1232+
@Override
1233+
public boolean forgotPassword(UserAccount userAccount) {
1234+
String resetToken = userAccount.getDetails().get(PasswordResetToken);
1235+
String resetTokenExpiryTimeString = userAccount.getDetails().getOrDefault(PasswordResetTokenExpiryDate, "0");
1236+
if (StringUtils.isNotEmpty(resetToken) && StringUtils.isNotEmpty(resetTokenExpiryTimeString)) {
1237+
final Date resetTokenExpiryTime = new Date(Long.parseLong(resetTokenExpiryTimeString));
1238+
final Date currentTime = new Date();
1239+
if (currentTime.after(resetTokenExpiryTime)) {
1240+
passwordReset.setResetTokenAndSend(userAccount);
1241+
}
1242+
} else if (StringUtils.isEmpty(resetToken)) {
1243+
passwordReset.setResetTokenAndSend(userAccount);
1244+
}
1245+
return true;
1246+
}
1247+
1248+
@Override
1249+
public boolean resetPassword(UserAccount userAccount, String token, String password) {
1250+
passwordReset.validateAndResetPassword(userAccount, token, password);
1251+
return true;
1252+
}
1253+
12261254
private void checkCommandAvailable(final User user, final String commandName, final InetAddress remoteAddress) throws PermissionDeniedException {
12271255
if (user == null) {
12281256
throw new PermissionDeniedException("User is null for role based API access check for command" + commandName);

server/src/main/java/com/cloud/api/auth/APIAuthenticationManagerImpl.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ public List<Class<?>> getCommands() {
7575
List<Class<?>> cmdList = new ArrayList<Class<?>>();
7676
cmdList.add(DefaultLoginAPIAuthenticatorCmd.class);
7777
cmdList.add(DefaultLogoutAPIAuthenticatorCmd.class);
78+
cmdList.add(DefaultForgotPasswordAPIAuthenticatorCmd.class);
79+
cmdList.add(DefaultResetPasswordAPIAuthenticatorCmd.class);
7880

7981
cmdList.add(ListUserTwoFactorAuthenticatorProvidersCmd.class);
8082
cmdList.add(ValidateUserTwoFactorAuthenticationCodeCmd.class);
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package com.cloud.api.auth;
18+
19+
import com.cloud.api.ApiServlet;
20+
import com.cloud.api.response.ApiResponseSerializer;
21+
import com.cloud.domain.Domain;
22+
import com.cloud.exception.CloudAuthenticationException;
23+
import com.cloud.user.Account;
24+
import com.cloud.user.User;
25+
import com.cloud.user.UserAccount;
26+
import com.cloud.utils.exception.CloudRuntimeException;
27+
import org.apache.cloudstack.api.APICommand;
28+
import org.apache.cloudstack.api.ApiConstants;
29+
import org.apache.cloudstack.api.ApiErrorCode;
30+
import org.apache.cloudstack.api.ApiServerService;
31+
import org.apache.cloudstack.api.BaseCmd;
32+
import org.apache.cloudstack.api.Parameter;
33+
import org.apache.cloudstack.api.ServerApiException;
34+
import org.apache.cloudstack.api.auth.APIAuthenticationType;
35+
import org.apache.cloudstack.api.auth.APIAuthenticator;
36+
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
37+
import org.apache.cloudstack.api.response.LoginCmdResponse;
38+
import org.apache.cloudstack.api.response.SuccessResponse;
39+
import org.jetbrains.annotations.Nullable;
40+
41+
import javax.inject.Inject;
42+
import javax.servlet.http.HttpServletRequest;
43+
import javax.servlet.http.HttpServletResponse;
44+
import javax.servlet.http.HttpSession;
45+
import java.net.InetAddress;
46+
import java.util.List;
47+
import java.util.Map;
48+
49+
@APICommand(name = "forgotPassword",
50+
description = "Sends an email to the user with a token to reset the password using resetPassword command.",
51+
requestHasSensitiveInfo = true,
52+
responseObject = SuccessResponse.class)
53+
public class DefaultForgotPasswordAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator {
54+
55+
56+
/////////////////////////////////////////////////////
57+
//////////////// API parameters /////////////////////
58+
/////////////////////////////////////////////////////
59+
@Parameter(name = ApiConstants.USERNAME, type = CommandType.STRING, description = "Username", required = true)
60+
private String username;
61+
62+
@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.")
63+
private String domain;
64+
65+
@Inject
66+
ApiServerService _apiServer;
67+
68+
/////////////////////////////////////////////////////
69+
/////////////////// Accessors ///////////////////////
70+
/////////////////////////////////////////////////////
71+
72+
public String getUsername() {
73+
return username;
74+
}
75+
76+
public String getDomainName() {
77+
return domain;
78+
}
79+
80+
81+
/////////////////////////////////////////////////////
82+
/////////////// API Implementation///////////////////
83+
/////////////////////////////////////////////////////
84+
85+
@Override
86+
public long getEntityOwnerId() {
87+
return Account.Type.NORMAL.ordinal();
88+
}
89+
90+
@Override
91+
public void execute() throws ServerApiException {
92+
// We should never reach here
93+
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly");
94+
}
95+
96+
@Override
97+
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 {
98+
final String[] username = (String[])params.get(ApiConstants.USERNAME);
99+
final String[] domainName = (String[])params.get(ApiConstants.DOMAIN);
100+
101+
Long domainId = null;
102+
String domain = null;
103+
domain = getDomainName(auditTrailSb, domainName, domain);
104+
105+
String serializedResponse = null;
106+
if (username != null) {
107+
try {
108+
final Domain userDomain = _domainService.findDomainByPath(domain);
109+
if (userDomain != null) {
110+
domainId = userDomain.getId();
111+
} else {
112+
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find the domain from the path %s", domain));
113+
}
114+
final UserAccount userAccount = _accountService.getActiveUserAccount(username[0], domainId);
115+
if (userAccount != null && List.of(User.Source.SAML2, User.Source.OAUTH2, User.Source.LDAP).contains(userAccount.getSource())) {
116+
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Forgot Password is not allowed for this user");
117+
}
118+
boolean success = _apiServer.forgotPassword(userAccount);
119+
SuccessResponse successResponse = new SuccessResponse();
120+
successResponse.setSuccess(success);
121+
successResponse.setResponseName(getCommandName());
122+
return ApiResponseSerializer.toSerializedString(successResponse, responseType);
123+
} catch (final CloudRuntimeException ex) {
124+
ApiServlet.invalidateHttpSession(session, "fall through to API key,");
125+
String msg = String.format("%s", ex.getMessage() != null ?
126+
ex.getMessage() :
127+
"forgot password request failed for user, check if username/domain are correct");
128+
auditTrailSb.append(" " + ApiErrorCode.ACCOUNT_ERROR + " " + msg);
129+
serializedResponse = _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), msg, params, responseType);
130+
if (logger.isTraceEnabled()) {
131+
logger.trace(msg);
132+
}
133+
}
134+
}
135+
// We should not reach here and if we do we throw an exception
136+
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, serializedResponse);
137+
}
138+
139+
@Nullable
140+
private String getDomainName(StringBuilder auditTrailSb, String[] domainName, String domain) {
141+
if (domainName != null) {
142+
domain = domainName[0];
143+
auditTrailSb.append(" domain=" + domain);
144+
if (domain != null) {
145+
// ensure domain starts with '/' and ends with '/'
146+
if (!domain.endsWith("/")) {
147+
domain += '/';
148+
}
149+
if (!domain.startsWith("/")) {
150+
domain = "/" + domain;
151+
}
152+
}
153+
}
154+
return domain;
155+
}
156+
157+
@Override
158+
public APIAuthenticationType getAPIType() {
159+
return APIAuthenticationType.PASSWORD_RESET;
160+
}
161+
162+
@Override
163+
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
164+
}
165+
}

0 commit comments

Comments
 (0)