Skip to content

Commit 092390b

Browse files
committed
Feature: Forgot password
1 parent cc1dcf5 commit 092390b

File tree

25 files changed

+1354
-35
lines changed

25 files changed

+1354
-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: 40 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,33 @@ 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+
1237+
if (StringUtils.isBlank(userAccount.getEmail())) {
1238+
throw new CloudRuntimeException("Email is not set for the user. Please contact your administrator.");
1239+
}
1240+
1241+
if (StringUtils.isNotEmpty(resetToken) && StringUtils.isNotEmpty(resetTokenExpiryTimeString)) {
1242+
final Date resetTokenExpiryTime = new Date(Long.parseLong(resetTokenExpiryTimeString));
1243+
final Date currentTime = new Date();
1244+
if (currentTime.after(resetTokenExpiryTime)) {
1245+
passwordReset.setResetTokenAndSend(userAccount);
1246+
}
1247+
} else if (StringUtils.isEmpty(resetToken)) {
1248+
passwordReset.setResetTokenAndSend(userAccount);
1249+
}
1250+
return true;
1251+
}
1252+
1253+
@Override
1254+
public boolean resetPassword(UserAccount userAccount, String token, String password) {
1255+
passwordReset.validateAndResetPassword(userAccount, token, password);
1256+
return true;
1257+
}
1258+
12261259
private void checkCommandAvailable(final User user, final String commandName, final InetAddress remoteAddress) throws PermissionDeniedException {
12271260
if (user == null) {
12281261
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: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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.user.Account;
23+
import com.cloud.user.User;
24+
import com.cloud.user.UserAccount;
25+
import com.cloud.utils.exception.CloudRuntimeException;
26+
import org.apache.cloudstack.api.APICommand;
27+
import org.apache.cloudstack.api.ApiConstants;
28+
import org.apache.cloudstack.api.ApiErrorCode;
29+
import org.apache.cloudstack.api.ApiServerService;
30+
import org.apache.cloudstack.api.BaseCmd;
31+
import org.apache.cloudstack.api.Parameter;
32+
import org.apache.cloudstack.api.ServerApiException;
33+
import org.apache.cloudstack.api.auth.APIAuthenticationType;
34+
import org.apache.cloudstack.api.auth.APIAuthenticator;
35+
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
36+
import org.apache.cloudstack.api.response.SuccessResponse;
37+
import org.jetbrains.annotations.Nullable;
38+
39+
import javax.inject.Inject;
40+
import javax.servlet.http.HttpServletRequest;
41+
import javax.servlet.http.HttpServletResponse;
42+
import javax.servlet.http.HttpSession;
43+
import java.net.InetAddress;
44+
import java.util.List;
45+
import java.util.Map;
46+
47+
@APICommand(name = "forgotPassword",
48+
description = "Sends an email to the user with a token to reset the password using resetPassword command.",
49+
requestHasSensitiveInfo = true,
50+
responseObject = SuccessResponse.class)
51+
public class DefaultForgotPasswordAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator {
52+
53+
54+
/////////////////////////////////////////////////////
55+
//////////////// API parameters /////////////////////
56+
/////////////////////////////////////////////////////
57+
@Parameter(name = ApiConstants.USERNAME, type = CommandType.STRING, description = "Username", required = true)
58+
private String username;
59+
60+
@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.")
61+
private String domain;
62+
63+
@Inject
64+
ApiServerService _apiServer;
65+
66+
/////////////////////////////////////////////////////
67+
/////////////////// Accessors ///////////////////////
68+
/////////////////////////////////////////////////////
69+
70+
public String getUsername() {
71+
return username;
72+
}
73+
74+
public String getDomainName() {
75+
return domain;
76+
}
77+
78+
79+
/////////////////////////////////////////////////////
80+
/////////////// API Implementation///////////////////
81+
/////////////////////////////////////////////////////
82+
83+
@Override
84+
public long getEntityOwnerId() {
85+
return Account.Type.NORMAL.ordinal();
86+
}
87+
88+
@Override
89+
public void execute() throws ServerApiException {
90+
// We should never reach here
91+
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly");
92+
}
93+
94+
@Override
95+
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 {
96+
final String[] username = (String[])params.get(ApiConstants.USERNAME);
97+
final String[] domainName = (String[])params.get(ApiConstants.DOMAIN);
98+
99+
Long domainId = null;
100+
String domain = null;
101+
domain = getDomainName(auditTrailSb, domainName, domain);
102+
103+
String serializedResponse = null;
104+
if (username != null) {
105+
try {
106+
final Domain userDomain = _domainService.findDomainByPath(domain);
107+
if (userDomain != null) {
108+
domainId = userDomain.getId();
109+
} else {
110+
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find the domain from the path %s", domain));
111+
}
112+
final UserAccount userAccount = _accountService.getActiveUserAccount(username[0], domainId);
113+
if (userAccount != null && List.of(User.Source.SAML2, User.Source.OAUTH2, User.Source.LDAP).contains(userAccount.getSource())) {
114+
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Forgot Password is not allowed for this user");
115+
}
116+
boolean success = _apiServer.forgotPassword(userAccount);
117+
SuccessResponse successResponse = new SuccessResponse();
118+
successResponse.setSuccess(success);
119+
successResponse.setResponseName(getCommandName());
120+
return ApiResponseSerializer.toSerializedString(successResponse, responseType);
121+
} catch (final CloudRuntimeException ex) {
122+
ApiServlet.invalidateHttpSession(session, "fall through to API key,");
123+
String msg = String.format("%s", ex.getMessage() != null ?
124+
ex.getMessage() :
125+
"forgot password request failed for user, check if username/domain are correct");
126+
auditTrailSb.append(" " + ApiErrorCode.ACCOUNT_ERROR + " " + msg);
127+
serializedResponse = _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), msg, params, responseType);
128+
if (logger.isTraceEnabled()) {
129+
logger.trace(msg);
130+
}
131+
}
132+
}
133+
// We should not reach here and if we do we throw an exception
134+
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, serializedResponse);
135+
}
136+
137+
@Nullable
138+
private String getDomainName(StringBuilder auditTrailSb, String[] domainName, String domain) {
139+
if (domainName != null) {
140+
domain = domainName[0];
141+
auditTrailSb.append(" domain=" + domain);
142+
if (domain != null) {
143+
// ensure domain starts with '/' and ends with '/'
144+
if (!domain.endsWith("/")) {
145+
domain += '/';
146+
}
147+
if (!domain.startsWith("/")) {
148+
domain = "/" + domain;
149+
}
150+
}
151+
}
152+
return domain;
153+
}
154+
155+
@Override
156+
public APIAuthenticationType getAPIType() {
157+
return APIAuthenticationType.PASSWORD_RESET;
158+
}
159+
160+
@Override
161+
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
162+
}
163+
}

0 commit comments

Comments
 (0)