mirror of https://github.com/apache/cloudstack.git
Allow enforcing password change for a user after reset by admin (root/domain) (#12294)
* API modifications for passwordchangerequired * ui login flow for passwordchangerequired * add passwordchangerequired in listUsers API response, it will be used in UI to render reset password form * cleanup redundant LOGIN_SOURCE and limiting apis for first time login * address copilot comments * allow enforcing password change for all role types and update reset pwd flow for passwordchangerequired * address review comments * add unit tests * cleanup ispasswordchangerequired from user_view * address review comments * 1. Allow enforcing password change while creating user 2. Admin can enforce password change on next login with out resetting password * address review comment, add unit test * improve code coverage * fix pre-commit license issue * 1. allow enter key to submit change password form 2. hide force password reset for disabled/locked user in ui * 1. throw exception when force reset password is done for locked/disabled user/account 2. ui validation on current and new password being same 3. allow enforce change password for add user until saml is not enabled * allow oauth login to skip force password change
This commit is contained in:
parent
b1edfb8d60
commit
c79b33c1fb
|
|
@ -59,7 +59,8 @@ public interface AccountService {
|
|||
|
||||
User getSystemUser();
|
||||
|
||||
User createUser(String userName, String password, String firstName, String lastName, String email, String timeZone, String accountName, Long domainId, String userUUID);
|
||||
User createUser(String userName, String password, String firstName, String lastName, String email, String timeZone,
|
||||
String accountName, Long domainId, String userUUID, boolean isPasswordChangeRequired);
|
||||
|
||||
User createUser(String userName, String password, String firstName, String lastName, String email, String timeZone, String accountName, Long domainId, String userUUID,
|
||||
User.Source source);
|
||||
|
|
|
|||
|
|
@ -1261,6 +1261,7 @@ public class ApiConstants {
|
|||
public static final String PROVIDER_FOR_2FA = "providerfor2fa";
|
||||
public static final String ISSUER_FOR_2FA = "issuerfor2fa";
|
||||
public static final String MANDATE_2FA = "mandate2fa";
|
||||
public static final String PASSWORD_CHANGE_REQUIRED = "passwordchangerequired";
|
||||
public static final String SECRET_CODE = "secretcode";
|
||||
public static final String LOGIN = "login";
|
||||
public static final String LOGOUT = "logout";
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import org.apache.cloudstack.api.ServerApiException;
|
|||
import org.apache.cloudstack.api.response.DomainResponse;
|
||||
import org.apache.cloudstack.api.response.UserResponse;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.cloud.user.Account;
|
||||
|
|
@ -78,6 +79,12 @@ public class CreateUserCmd extends BaseCmd {
|
|||
@Parameter(name = ApiConstants.USER_ID, type = CommandType.STRING, description = "User UUID, required for adding account from external provisioning system")
|
||||
private String userUUID;
|
||||
|
||||
@Parameter(name = ApiConstants.PASSWORD_CHANGE_REQUIRED,
|
||||
type = CommandType.BOOLEAN,
|
||||
description = "Provide true to mandate the User to reset password on next login.",
|
||||
since = "4.23.0")
|
||||
private Boolean passwordChangeRequired;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
|
@ -118,6 +125,10 @@ public class CreateUserCmd extends BaseCmd {
|
|||
return userUUID;
|
||||
}
|
||||
|
||||
public Boolean isPasswordChangeRequired() {
|
||||
return BooleanUtils.isTrue(passwordChangeRequired);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////// API Implementation///////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
|
@ -147,7 +158,7 @@ public class CreateUserCmd extends BaseCmd {
|
|||
CallContext.current().setEventDetails("UserName: " + getUserName() + ", FirstName :" + getFirstName() + ", LastName: " + getLastName());
|
||||
User user =
|
||||
_accountService.createUser(getUserName(), getPassword(), getFirstName(), getLastName(), getEmail(), getTimezone(), getAccountName(), getDomainId(),
|
||||
getUserUUID());
|
||||
getUserUUID(), isPasswordChangeRequired());
|
||||
if (user != null) {
|
||||
UserResponse response = _responseGenerator.createUserResponse(user);
|
||||
response.setResponseName(getCommandName());
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import org.apache.cloudstack.api.ServerApiException;
|
|||
import org.apache.cloudstack.api.response.UserResponse;
|
||||
import org.apache.cloudstack.context.CallContext;
|
||||
import org.apache.cloudstack.region.RegionService;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.User;
|
||||
|
|
@ -38,6 +39,8 @@ import com.cloud.user.UserAccount;
|
|||
requestHasSensitiveInfo = true, responseHasSensitiveInfo = true)
|
||||
public class UpdateUserCmd extends BaseCmd {
|
||||
|
||||
@Inject
|
||||
private RegionService _regionService;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
//////////////// API parameters /////////////////////
|
||||
|
|
@ -85,8 +88,11 @@ public class UpdateUserCmd extends BaseCmd {
|
|||
"This parameter is only used to mandate 2FA, not to disable 2FA", since = "4.18.0.0")
|
||||
private Boolean mandate2FA;
|
||||
|
||||
@Inject
|
||||
private RegionService _regionService;
|
||||
@Parameter(name = ApiConstants.PASSWORD_CHANGE_REQUIRED,
|
||||
type = CommandType.BOOLEAN,
|
||||
description = "Provide true to mandate the User to reset password on next login.",
|
||||
since = "4.23.0")
|
||||
private Boolean passwordChangeRequired;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
|
|
@ -193,4 +199,8 @@ public class UpdateUserCmd extends BaseCmd {
|
|||
public ApiCommandResourceType getApiResourceType() {
|
||||
return ApiCommandResourceType.User;
|
||||
}
|
||||
|
||||
public Boolean isPasswordChangeRequired() {
|
||||
return BooleanUtils.isTrue(passwordChangeRequired);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@ public class LoginCmdResponse extends AuthenticationCmdResponse {
|
|||
@Param(description = "Management Server ID that the user logged to", since = "4.21.0.0")
|
||||
private String managementServerId;
|
||||
|
||||
@SerializedName(value = ApiConstants.PASSWORD_CHANGE_REQUIRED)
|
||||
@Param(description = "Indicates whether the User is required to change password on next login.", since = "4.23.0")
|
||||
private Boolean passwordChangeRequired;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
|
@ -223,4 +227,12 @@ public class LoginCmdResponse extends AuthenticationCmdResponse {
|
|||
public void setManagementServerId(String managementServerId) {
|
||||
this.managementServerId = managementServerId;
|
||||
}
|
||||
|
||||
public Boolean getPasswordChangeRequired() {
|
||||
return passwordChangeRequired;
|
||||
}
|
||||
|
||||
public void setPasswordChangeRequired(Boolean passwordChangeRequired) {
|
||||
this.passwordChangeRequired = passwordChangeRequired;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ public class CreateUserCmdTest {
|
|||
} catch (ServerApiException e) {
|
||||
Assert.assertTrue("Received exception as the mock accountService createUser returns null user", true);
|
||||
}
|
||||
Mockito.verify(accountService, Mockito.times(1)).createUser(null, "Test", null, null, null, null, null, null, null);
|
||||
Mockito.verify(accountService, Mockito.times(1)).createUser(null, "Test", null, null, null, null, null, null, null, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -82,7 +82,7 @@ public class CreateUserCmdTest {
|
|||
Assert.assertEquals(ApiErrorCode.PARAM_ERROR,e.getErrorCode());
|
||||
Assert.assertEquals("Empty passwords are not allowed", e.getMessage());
|
||||
}
|
||||
Mockito.verify(accountService, Mockito.never()).createUser(null, null, null, null, null, null, null, null, null);
|
||||
Mockito.verify(accountService, Mockito.never()).createUser(null, null, null, null, null, null, null, null, null, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -95,6 +95,6 @@ public class CreateUserCmdTest {
|
|||
Assert.assertEquals(ApiErrorCode.PARAM_ERROR,e.getErrorCode());
|
||||
Assert.assertEquals("Empty passwords are not allowed", e.getMessage());
|
||||
}
|
||||
Mockito.verify(accountService, Mockito.never()).createUser(null, null, null, null, null, null, null, null, null);
|
||||
Mockito.verify(accountService, Mockito.never()).createUser(null, null, null, null, null, null, null, null, null, true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.api.command.admin.user;
|
||||
|
||||
import org.apache.cloudstack.api.ApiCommandResourceType;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class UpdateUserCmdTest {
|
||||
@InjectMocks
|
||||
private UpdateUserCmd cmd;
|
||||
|
||||
@Test
|
||||
public void testGetApiResourceId() {
|
||||
Long userId = 99L;
|
||||
cmd.setId(userId);
|
||||
Assert.assertEquals(userId, cmd.getApiResourceId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetApiResourceType() {
|
||||
Assert.assertEquals(ApiCommandResourceType.User, cmd.getApiResourceType());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsPasswordChangeRequired_True() {
|
||||
ReflectionTestUtils.setField(cmd, "passwordChangeRequired", Boolean.TRUE);
|
||||
Assert.assertTrue(cmd.isPasswordChangeRequired());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsPasswordChangeRequired_False() {
|
||||
ReflectionTestUtils.setField(cmd, "passwordChangeRequired", Boolean.FALSE);
|
||||
Assert.assertFalse(cmd.isPasswordChangeRequired());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsPasswordChangeRequired_Null() {
|
||||
ReflectionTestUtils.setField(cmd, "passwordChangeRequired", null);
|
||||
Assert.assertFalse(cmd.isPasswordChangeRequired());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
// 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.api.response;
|
||||
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class LoginCmdResponseTest {
|
||||
|
||||
@Test
|
||||
public void testAllGettersAndSetters() {
|
||||
LoginCmdResponse response = new LoginCmdResponse();
|
||||
|
||||
response.setUsername("user1");
|
||||
response.setUserId("100");
|
||||
response.setDomainId("200");
|
||||
response.setTimeout(3600);
|
||||
response.setAccount("account1");
|
||||
response.setFirstName("John");
|
||||
response.setLastName("Doe");
|
||||
response.setType("admin");
|
||||
response.setTimeZone("UTC");
|
||||
response.setTimeZoneOffset("+00:00");
|
||||
response.setRegistered("true");
|
||||
response.setSessionKey("session-key");
|
||||
response.set2FAenabled("true");
|
||||
response.set2FAverfied("false");
|
||||
response.setProviderFor2FA("totp");
|
||||
response.setIssuerFor2FA("cloudstack");
|
||||
response.setManagementServerId("ms-1");
|
||||
|
||||
Assert.assertEquals("user1", response.getUsername());
|
||||
Assert.assertEquals("100", response.getUserId());
|
||||
Assert.assertEquals("200", response.getDomainId());
|
||||
Assert.assertEquals(Integer.valueOf(3600), response.getTimeout());
|
||||
Assert.assertEquals("account1", response.getAccount());
|
||||
Assert.assertEquals("John", response.getFirstName());
|
||||
Assert.assertEquals("Doe", response.getLastName());
|
||||
Assert.assertEquals("admin", response.getType());
|
||||
Assert.assertEquals("UTC", response.getTimeZone());
|
||||
Assert.assertEquals("+00:00", response.getTimeZoneOffset());
|
||||
Assert.assertEquals("true", response.getRegistered());
|
||||
Assert.assertEquals("session-key", response.getSessionKey());
|
||||
Assert.assertEquals("true", response.is2FAenabled());
|
||||
Assert.assertEquals("false", response.is2FAverfied());
|
||||
Assert.assertEquals("totp", response.getProviderFor2FA());
|
||||
Assert.assertEquals("cloudstack", response.getIssuerFor2FA());
|
||||
Assert.assertEquals("ms-1", response.getManagementServerId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordChangeRequired_True() {
|
||||
LoginCmdResponse response = new LoginCmdResponse();
|
||||
response.setPasswordChangeRequired(true);
|
||||
Assert.assertTrue(response.getPasswordChangeRequired());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordChangeRequired_False() {
|
||||
LoginCmdResponse response = new LoginCmdResponse();
|
||||
response.setPasswordChangeRequired(false);
|
||||
Assert.assertFalse(response.getPasswordChangeRequired());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordChangeRequired_Null() {
|
||||
LoginCmdResponse response = new LoginCmdResponse();
|
||||
response.setPasswordChangeRequired(null);
|
||||
Assert.assertNull("Boolean.parseBoolean(null) should return null", response.getPasswordChangeRequired());
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +48,8 @@ public class UserDetailVO implements ResourceDetail {
|
|||
public static final String Setup2FADetail = "2FASetupStatus";
|
||||
public static final String PasswordResetToken = "PasswordResetToken";
|
||||
public static final String PasswordResetTokenExpiryDate = "PasswordResetTokenExpiryDate";
|
||||
public static final String PasswordChangeRequired = "PasswordChangeRequired";
|
||||
public static final String OauthLogin = "OauthLogin";
|
||||
|
||||
public UserDetailVO() {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,8 +44,10 @@ import org.apache.cloudstack.api.response.ApiDiscoveryResponse;
|
|||
import org.apache.cloudstack.api.response.ApiParameterResponse;
|
||||
import org.apache.cloudstack.api.response.ApiResponseResponse;
|
||||
import org.apache.cloudstack.api.response.ListResponse;
|
||||
import org.apache.cloudstack.resourcedetail.UserDetailVO;
|
||||
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.collections.MapUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.reflections.ReflectionUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
|
@ -56,6 +58,7 @@ import com.cloud.serializer.Param;
|
|||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountService;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.user.UserAccount;
|
||||
import com.cloud.utils.ReflectUtil;
|
||||
import com.cloud.utils.component.ComponentLifecycleBase;
|
||||
import com.cloud.utils.component.PluggableService;
|
||||
|
|
@ -67,6 +70,7 @@ public class ApiDiscoveryServiceImpl extends ComponentLifecycleBase implements A
|
|||
List<APIChecker> _apiAccessCheckers = null;
|
||||
List<PluggableService> _services = null;
|
||||
protected static Map<String, ApiDiscoveryResponse> s_apiNameDiscoveryResponseMap = null;
|
||||
public static final List<String> APIS_ALLOWED_FOR_PASSWORD_CHANGE = Arrays.asList("login", "logout", "updateUser", "listApis");
|
||||
|
||||
@Inject
|
||||
AccountService accountService;
|
||||
|
|
@ -287,12 +291,20 @@ public class ApiDiscoveryServiceImpl extends ComponentLifecycleBase implements A
|
|||
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(account, "accountName", "uuid")));
|
||||
}
|
||||
|
||||
if (role.getRoleType() == RoleType.Admin && role.getId() == RoleType.Admin.getId()) {
|
||||
logger.info(String.format("Account [%s] is Root Admin, all APIs are allowed.",
|
||||
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(account, "accountName", "uuid")));
|
||||
// Limit APIs on first login requiring password change
|
||||
UserAccount userAccount = accountService.getUserAccountById(user.getId());
|
||||
Map<String, String> userAccDetails = userAccount.getDetails();
|
||||
if (MapUtils.isNotEmpty(userAccDetails) && !userAccDetails.containsKey(UserDetailVO.OauthLogin) &&
|
||||
"true".equalsIgnoreCase(userAccDetails.get(UserDetailVO.PasswordChangeRequired))) {
|
||||
apisAllowed = APIS_ALLOWED_FOR_PASSWORD_CHANGE;
|
||||
} else {
|
||||
for (APIChecker apiChecker : _apiAccessCheckers) {
|
||||
apisAllowed = apiChecker.getApisAllowedToUser(role, user, apisAllowed);
|
||||
if (role.getRoleType() == RoleType.Admin && role.getId() == RoleType.Admin.getId()) {
|
||||
logger.info(String.format("Account [%s] is Root Admin, all APIs are allowed.",
|
||||
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(account, "accountName", "uuid")));
|
||||
} else {
|
||||
for (APIChecker apiChecker : _apiAccessCheckers) {
|
||||
apisAllowed = apiChecker.getApisAllowedToUser(role, user, apisAllowed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import com.cloud.user.Account;
|
|||
import com.cloud.user.AccountService;
|
||||
import com.cloud.user.AccountVO;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.user.UserAccount;
|
||||
import com.cloud.user.UserAccountVO;
|
||||
import com.cloud.user.UserVO;
|
||||
|
||||
import org.apache.cloudstack.acl.APIChecker;
|
||||
|
|
@ -29,6 +31,8 @@ import org.apache.cloudstack.acl.RoleService;
|
|||
import org.apache.cloudstack.acl.RoleType;
|
||||
import org.apache.cloudstack.acl.RoleVO;
|
||||
import org.apache.cloudstack.api.response.ApiDiscoveryResponse;
|
||||
import org.apache.cloudstack.api.response.ListResponse;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
|
@ -39,11 +43,15 @@ import org.mockito.Spy;
|
|||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordChangeRequired;
|
||||
import static org.apache.cloudstack.resourcedetail.UserDetailVO.Setup2FADetail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ApiDiscoveryTest {
|
||||
|
|
@ -66,12 +74,17 @@ public class ApiDiscoveryTest {
|
|||
@InjectMocks
|
||||
ApiDiscoveryServiceImpl discoveryServiceSpy;
|
||||
|
||||
@Mock
|
||||
UserAccount mockUserAccount;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
discoveryServiceSpy.s_apiNameDiscoveryResponseMap = apiNameDiscoveryResponseMapMock;
|
||||
discoveryServiceSpy._apiAccessCheckers = apiAccessCheckersMock;
|
||||
|
||||
Mockito.when(discoveryServiceSpy._apiAccessCheckers.iterator()).thenReturn(Arrays.asList(apiCheckerMock).iterator());
|
||||
Mockito.when(mockUserAccount.getDetails()).thenReturn(null);
|
||||
Mockito.when(accountServiceMock.getUserAccountById(anyLong())).thenReturn(mockUserAccount);
|
||||
}
|
||||
|
||||
private User getTestUser() {
|
||||
|
|
@ -131,4 +144,29 @@ public class ApiDiscoveryTest {
|
|||
|
||||
Mockito.verify(apiCheckerMock, Mockito.times(1)).getApisAllowedToUser(any(Role.class), any(User.class), anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listApisForUserWithoutEnforcedPwdChange() throws PermissionDeniedException {
|
||||
RoleVO userRoleVO = new RoleVO(4L, "name", RoleType.User, "description");
|
||||
Map<String, String> userDetails = new HashMap<>();
|
||||
userDetails.put(Setup2FADetail, UserAccountVO.Setup2FAstatus.ENABLED.name());
|
||||
Mockito.when(mockUserAccount.getDetails()).thenReturn(userDetails);
|
||||
Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(getNormalAccount());
|
||||
Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(userRoleVO);
|
||||
discoveryServiceSpy.listApis(getTestUser(), null);
|
||||
Mockito.verify(apiCheckerMock, Mockito.times(1)).getApisAllowedToUser(any(Role.class), any(User.class), anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listApisForUserEnforcedPwdChange() throws PermissionDeniedException {
|
||||
RoleVO userRoleVO = new RoleVO(4L, "name", RoleType.User, "description");
|
||||
Map<String, String> userDetails = new HashMap<>();
|
||||
userDetails.put(PasswordChangeRequired, "true");
|
||||
Mockito.when(mockUserAccount.getDetails()).thenReturn(userDetails);
|
||||
Mockito.when(accountServiceMock.getAccount(Mockito.anyLong())).thenReturn(getNormalAccount());
|
||||
Mockito.when(roleServiceMock.findRole(Mockito.anyLong())).thenReturn(userRoleVO);
|
||||
Mockito.when(apiNameDiscoveryResponseMapMock.get(Mockito.anyString())).thenReturn(Mockito.mock(ApiDiscoveryResponse.class));
|
||||
ListResponse<ApiDiscoveryResponse> response = (ListResponse<ApiDiscoveryResponse>) discoveryServiceSpy.listApis(getTestUser(), null);
|
||||
Assert.assertEquals(4, response.getResponses().size());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ public class MockAccountManager extends ManagerBase implements AccountManager {
|
|||
}
|
||||
|
||||
@Override
|
||||
public User createUser(String arg0, String arg1, String arg2, String arg3, String arg4, String arg5, String arg6, Long arg7, String arg8) {
|
||||
public User createUser(String arg0, String arg1, String arg2, String arg3, String arg4, String arg5, String arg6, Long arg7, String arg8, boolean arg9) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ 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.LoginCmdResponse;
|
||||
import org.apache.cloudstack.resourcedetail.UserDetailVO;
|
||||
import org.apache.cloudstack.resourcedetail.dao.UserDetailsDao;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
|
@ -74,6 +76,9 @@ public class OauthLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
|
|||
@Inject
|
||||
ApiServerService _apiServer;
|
||||
|
||||
@Inject
|
||||
UserDetailsDao userDetailsDao;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
|
@ -157,8 +162,10 @@ public class OauthLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
|
|||
if (userAccount != null && User.Source.SAML2 == userAccount.getSource()) {
|
||||
throw new CloudAuthenticationException("User is not allowed CloudStack login");
|
||||
}
|
||||
return ApiResponseSerializer.toSerializedString(_apiServer.loginUser(session, userAccount.getUsername(), null, domainId, domain, remoteAddress, params),
|
||||
serializedResponse = ApiResponseSerializer.toSerializedString(_apiServer.loginUser(session, userAccount.getUsername(), null, domainId, domain, remoteAddress, params),
|
||||
responseType);
|
||||
userDetailsDao.addDetail(userAccount.getId(), UserDetailVO.OauthLogin, "true", false);
|
||||
return serializedResponse;
|
||||
} catch (final CloudAuthenticationException ex) {
|
||||
ApiServlet.invalidateHttpSession(session, "fall through to API key,");
|
||||
String msg = String.format("%s", ex.getMessage() != null ?
|
||||
|
|
|
|||
|
|
@ -116,9 +116,11 @@ import org.apache.cloudstack.framework.messagebus.MessageBus;
|
|||
import org.apache.cloudstack.framework.messagebus.MessageDispatcher;
|
||||
import org.apache.cloudstack.framework.messagebus.MessageHandler;
|
||||
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
|
||||
import org.apache.cloudstack.resourcedetail.UserDetailVO;
|
||||
import org.apache.cloudstack.user.UserPasswordResetManager;
|
||||
import org.apache.cloudstack.utils.identity.ManagementServerNode;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.commons.collections.MapUtils;
|
||||
import org.apache.commons.lang3.EnumUtils;
|
||||
import org.apache.http.ConnectionClosedException;
|
||||
import org.apache.http.HttpException;
|
||||
|
|
@ -194,6 +196,7 @@ import com.cloud.utils.net.NetUtils;
|
|||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import static com.cloud.user.AccountManagerImpl.apiKeyAccess;
|
||||
import static org.apache.cloudstack.api.ApiConstants.PASSWORD_CHANGE_REQUIRED;
|
||||
import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled;
|
||||
|
||||
@Component
|
||||
|
|
@ -1227,6 +1230,9 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
|
|||
if (ApiConstants.MANAGEMENT_SERVER_ID.equalsIgnoreCase(attrName)) {
|
||||
response.setManagementServerId(attrObj.toString());
|
||||
}
|
||||
if (PASSWORD_CHANGE_REQUIRED.equalsIgnoreCase(attrName) && attrObj instanceof Boolean) {
|
||||
response.setPasswordChangeRequired((Boolean) attrObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
response.setResponseName("loginresponse");
|
||||
|
|
@ -1327,6 +1333,13 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
|
|||
final String sessionKey = Base64.encodeBase64URLSafeString(sessionKeyBytes);
|
||||
session.setAttribute(ApiConstants.SESSIONKEY, sessionKey);
|
||||
|
||||
Map<String, String> userAccDetails = userAcct.getDetails();
|
||||
if (MapUtils.isNotEmpty(userAccDetails)) {
|
||||
String needPwdChangeStr = userAccDetails.get(UserDetailVO.PasswordChangeRequired);
|
||||
if ("true".equalsIgnoreCase(needPwdChangeStr)) {
|
||||
session.setAttribute(PASSWORD_CHANGE_REQUIRED, true);
|
||||
}
|
||||
}
|
||||
return createLoginResponse(session);
|
||||
}
|
||||
throw new CloudAuthenticationException("Failed to authenticate user " + username + " in domain " + domainId + "; please provide valid credentials");
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ 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.LoginCmdResponse;
|
||||
import org.apache.cloudstack.resourcedetail.UserDetailVO;
|
||||
import org.apache.cloudstack.resourcedetail.dao.UserDetailsDao;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
|
@ -66,6 +68,9 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe
|
|||
@Inject
|
||||
ApiServerService _apiServer;
|
||||
|
||||
@Inject
|
||||
UserDetailsDao userDetailsDao;
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
/////////////////// Accessors ///////////////////////
|
||||
/////////////////////////////////////////////////////
|
||||
|
|
@ -151,8 +156,10 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe
|
|||
if (userAccount != null && User.Source.SAML2 == userAccount.getSource()) {
|
||||
throw new CloudAuthenticationException("User is not allowed CloudStack login");
|
||||
}
|
||||
return ApiResponseSerializer.toSerializedString(_apiServer.loginUser(session, username[0], pwd, domainId, domain, remoteAddress, params),
|
||||
serializedResponse = ApiResponseSerializer.toSerializedString(_apiServer.loginUser(session, username[0], pwd, domainId, domain, remoteAddress, params),
|
||||
responseType);
|
||||
userDetailsDao.removeDetail(userAccount.getId(), UserDetailVO.OauthLogin);
|
||||
return serializedResponse;
|
||||
} catch (final CloudAuthenticationException ex) {
|
||||
ApiServlet.invalidateHttpSession(session, "fall through to API key,");
|
||||
// TODO: fall through to API key, or just fail here w/ auth error? (HTTP 401)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
// under the License.
|
||||
package com.cloud.user;
|
||||
|
||||
import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordChangeRequired;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.URLEncoder;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
|
@ -1509,12 +1511,24 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
|
|||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_USER_CREATE, eventDescription = "creating User")
|
||||
public UserVO createUser(String userName, String password, String firstName, String lastName, String email, String timeZone, String accountName, Long domainId, String userUUID,
|
||||
User.Source source) {
|
||||
User.Source source) {
|
||||
return createUser(userName, password, firstName, lastName, email, timeZone, accountName, domainId, userUUID, source, false);
|
||||
}
|
||||
|
||||
|
||||
@ActionEvent(eventType = EventTypes.EVENT_USER_CREATE, eventDescription = "creating User")
|
||||
public UserVO createUser(String userName, String password, String firstName, String lastName, String email, String timeZone, String accountName, Long domainId, String userUUID,
|
||||
User.Source source, boolean isPasswordChangeRequired) {
|
||||
// default domain to ROOT if not specified
|
||||
if (domainId == null) {
|
||||
domainId = Domain.ROOT_DOMAIN;
|
||||
}
|
||||
|
||||
if (isPasswordChangeRequired && (source == User.Source.SAML2 || source == User.Source.SAML2DISABLED || source == User.Source.LDAP)) {
|
||||
logger.warn("Enforcing password change is not permitted for source [{}].", source);
|
||||
throw new InvalidParameterValueException("CloudStack does not support enforcing password change for SAML or LDAP users.");
|
||||
}
|
||||
|
||||
Domain domain = _domainMgr.getDomain(domainId);
|
||||
if (domain == null) {
|
||||
throw new CloudRuntimeException("The domain " + domainId + " does not exist; unable to create user");
|
||||
|
|
@ -1545,14 +1559,21 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
|
|||
verifyCallerPrivilegeForUserOrAccountOperations(account);
|
||||
UserVO user;
|
||||
user = createUser(account.getId(), userName, password, firstName, lastName, email, timeZone, userUUID, source);
|
||||
if (isPasswordChangeRequired) {
|
||||
long callerAccountId = CallContext.current().getCallingAccountId();
|
||||
if ((isRootAdmin(callerAccountId) || isDomainAdmin(callerAccountId))) {
|
||||
_userDetailsDao.addDetail(user.getId(), PasswordChangeRequired, "true", false);
|
||||
}
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ActionEvent(eventType = EventTypes.EVENT_USER_CREATE, eventDescription = "creating User")
|
||||
public UserVO createUser(String userName, String password, String firstName, String lastName, String email, String timeZone, String accountName, Long domainId, String userUUID) {
|
||||
public UserVO createUser(String userName, String password, String firstName, String lastName, String email,
|
||||
String timeZone, String accountName, Long domainId, String userUUID, boolean isPasswordChangeRequired) {
|
||||
|
||||
return createUser(userName, password, firstName, lastName, email, timeZone, accountName, domainId, userUUID, User.Source.UNKNOWN);
|
||||
return createUser(userName, password, firstName, lastName, email, timeZone, accountName, domainId, userUUID, User.Source.UNKNOWN, isPasswordChangeRequired);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -1586,10 +1607,41 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
|
|||
if (mandate2FA != null && mandate2FA) {
|
||||
user.setUser2faEnabled(true);
|
||||
}
|
||||
validateAndUpdatePasswordChangeRequired(caller, updateUserCmd, user, account);
|
||||
_userDao.update(user.getId(), user);
|
||||
return _userAccountDao.findById(user.getId());
|
||||
}
|
||||
|
||||
private void validateAndUpdatePasswordChangeRequired(User caller, UpdateUserCmd updateUserCmd, UserVO user, Account account) {
|
||||
if (updateUserCmd.isPasswordChangeRequired()) {
|
||||
if (user.getState() != State.ENABLED || account.getState() != State.ENABLED) {
|
||||
throw new CloudRuntimeException("CloudStack does not support enforcing password change for locked/disabled User or Account.");
|
||||
}
|
||||
|
||||
User.Source userSource = user.getSource();
|
||||
if (userSource == User.Source.SAML2 || userSource == User.Source.SAML2DISABLED || userSource == User.Source.LDAP) {
|
||||
logger.warn("Enforcing password change is not permitted for source [{}].", user.getSource());
|
||||
throw new InvalidParameterValueException("CloudStack does not support enforcing password change for SAML or LDAP users.");
|
||||
}
|
||||
}
|
||||
|
||||
boolean isCallerSameAsUser = user.getId() == caller.getId();
|
||||
boolean isPasswordResetRequired = updateUserCmd.isPasswordChangeRequired() && !isCallerSameAsUser;
|
||||
// Admins only can enforce passwordChangeRequired for user
|
||||
if (isRootAdmin(caller.getAccountId()) || isDomainAdmin(caller.getAccountId())) {
|
||||
if (isPasswordResetRequired) {
|
||||
_userDetailsDao.addDetail(user.getId(), PasswordChangeRequired, "true", false);
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(updateUserCmd.getPassword())) {
|
||||
// Remove passwordChangeRequired if user updating own pwd or admin has not enforced it
|
||||
if (isCallerSameAsUser || !isPasswordResetRequired) {
|
||||
_userDetailsDao.removeDetail(user.getId(), PasswordChangeRequired);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verifyCallerPrivilegeForUserOrAccountOperations(Account userAccount) {
|
||||
logger.debug(String.format("Verifying whether the caller has the correct privileges based on the user's role type and API permissions: %s", userAccount));
|
||||
|
|
@ -2848,6 +2900,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
|
|||
logger.debug(String.format("User: %s in domain %d has successfully logged in, auth time duration - %d ms", username, domainId, validUserLastAuthTimeDurationInMs));
|
||||
}
|
||||
|
||||
user.setDetails(_userDetailsDao.listDetailsKeyPairs(user.getId()));
|
||||
|
||||
return user;
|
||||
} else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import java.util.Set;
|
|||
import java.util.UUID;
|
||||
|
||||
import static org.apache.cloudstack.config.ApiServiceConfiguration.ManagementServerAddresses;
|
||||
import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordChangeRequired;
|
||||
import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordResetToken;
|
||||
import static org.apache.cloudstack.resourcedetail.UserDetailVO.PasswordResetTokenExpiryDate;
|
||||
|
||||
|
|
@ -265,6 +266,8 @@ public class UserPasswordResetManagerImpl extends ManagerBase implements UserPas
|
|||
|
||||
userDetailsDao.removeDetail(userAccount.getId(), PasswordResetToken);
|
||||
userDetailsDao.removeDetail(userAccount.getId(), PasswordResetTokenExpiryDate);
|
||||
// remove password change required if user reset password
|
||||
userDetailsDao.removeDetail(userAccount.getId(), PasswordChangeRequired);
|
||||
|
||||
userDao.persist(user);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,21 @@
|
|||
package com.cloud.api;
|
||||
|
||||
import com.cloud.domain.Domain;
|
||||
import com.cloud.domain.DomainVO;
|
||||
import com.cloud.exception.CloudAuthenticationException;
|
||||
import com.cloud.user.Account;
|
||||
import com.cloud.user.AccountManager;
|
||||
import com.cloud.user.DomainManager;
|
||||
import com.cloud.user.User;
|
||||
import com.cloud.user.UserAccount;
|
||||
import com.cloud.user.UserVO;
|
||||
import com.cloud.utils.exception.CloudRuntimeException;
|
||||
|
||||
import org.apache.cloudstack.api.ApiConstants;
|
||||
import org.apache.cloudstack.api.ResponseObject;
|
||||
import org.apache.cloudstack.api.response.LoginCmdResponse;
|
||||
import org.apache.cloudstack.framework.config.ConfigKey;
|
||||
import org.apache.cloudstack.resourcedetail.UserDetailVO;
|
||||
import org.apache.cloudstack.user.UserPasswordResetManager;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Assert;
|
||||
|
|
@ -35,10 +45,22 @@ import org.mockito.Mockito;
|
|||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.apache.cloudstack.api.ApiConstants.PASSWORD_CHANGE_REQUIRED;
|
||||
import static org.apache.cloudstack.user.UserPasswordResetManager.UserPasswordResetEnabled;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ApiServerTest {
|
||||
|
|
@ -49,6 +71,15 @@ public class ApiServerTest {
|
|||
@Mock
|
||||
UserPasswordResetManager userPasswordResetManager;
|
||||
|
||||
@Mock
|
||||
DomainManager domainManager;
|
||||
|
||||
@Mock
|
||||
AccountManager accountManager;
|
||||
|
||||
@Mock
|
||||
HttpSession session;
|
||||
|
||||
@BeforeClass
|
||||
public static void beforeClass() throws Exception {
|
||||
overrideDefaultConfigValue(UserPasswordResetEnabled, "_value", true);
|
||||
|
|
@ -96,8 +127,8 @@ public class ApiServerTest {
|
|||
|
||||
@Test
|
||||
public void testForgotPasswordSuccess() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Domain domain = Mockito.mock(Domain.class);
|
||||
UserAccount userAccount = mock(UserAccount.class);
|
||||
Domain domain = mock(Domain.class);
|
||||
|
||||
Mockito.when(userAccount.getEmail()).thenReturn("test@test.com");
|
||||
Mockito.when(userAccount.getState()).thenReturn("ENABLED");
|
||||
|
|
@ -110,8 +141,8 @@ public class ApiServerTest {
|
|||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void testForgotPasswordFailureNoEmail() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Domain domain = Mockito.mock(Domain.class);
|
||||
UserAccount userAccount = mock(UserAccount.class);
|
||||
Domain domain = mock(Domain.class);
|
||||
|
||||
Mockito.when(userAccount.getEmail()).thenReturn("");
|
||||
apiServer.forgotPassword(userAccount, domain);
|
||||
|
|
@ -119,8 +150,8 @@ public class ApiServerTest {
|
|||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void testForgotPasswordFailureDisabledUser() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Domain domain = Mockito.mock(Domain.class);
|
||||
UserAccount userAccount = mock(UserAccount.class);
|
||||
Domain domain = mock(Domain.class);
|
||||
|
||||
Mockito.when(userAccount.getEmail()).thenReturn("test@test.com");
|
||||
Mockito.when(userAccount.getState()).thenReturn("DISABLED");
|
||||
|
|
@ -129,8 +160,8 @@ public class ApiServerTest {
|
|||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void testForgotPasswordFailureDisabledAccount() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Domain domain = Mockito.mock(Domain.class);
|
||||
UserAccount userAccount = mock(UserAccount.class);
|
||||
Domain domain = mock(Domain.class);
|
||||
|
||||
Mockito.when(userAccount.getEmail()).thenReturn("test@test.com");
|
||||
Mockito.when(userAccount.getState()).thenReturn("ENABLED");
|
||||
|
|
@ -140,8 +171,8 @@ public class ApiServerTest {
|
|||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void testForgotPasswordFailureInactiveDomain() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
Domain domain = Mockito.mock(Domain.class);
|
||||
UserAccount userAccount = mock(UserAccount.class);
|
||||
Domain domain = mock(Domain.class);
|
||||
|
||||
Mockito.when(userAccount.getEmail()).thenReturn("test@test.com");
|
||||
Mockito.when(userAccount.getState()).thenReturn("ENABLED");
|
||||
|
|
@ -153,8 +184,8 @@ public class ApiServerTest {
|
|||
@Test
|
||||
public void testVerifyApiKeyAccessAllowed() {
|
||||
Long domainId = 1L;
|
||||
User user = Mockito.mock(User.class);
|
||||
Account account = Mockito.mock(Account.class);
|
||||
User user = mock(User.class);
|
||||
Account account = mock(Account.class);
|
||||
|
||||
Mockito.when(user.getApiKeyAccess()).thenReturn(true);
|
||||
Assert.assertEquals(true, apiServer.verifyApiKeyAccessAllowed(user, account));
|
||||
|
|
@ -176,4 +207,73 @@ public class ApiServerTest {
|
|||
Mockito.when(account.getApiKeyAccess()).thenReturn(null);
|
||||
Assert.assertEquals(true, apiServer.verifyApiKeyAccessAllowed(user, account));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoginUserSuccess() throws Exception {
|
||||
String username = "user";
|
||||
String password = "password";
|
||||
Long domainId = 1L;
|
||||
String domainPath = "/";
|
||||
InetAddress loginIp = InetAddress.getByName("127.0.0.1");
|
||||
Map<String, Object[]> requestParams = new HashMap<>();
|
||||
|
||||
DomainVO domain = mock(DomainVO.class);
|
||||
Mockito.when(domain.getId()).thenReturn(domainId);
|
||||
Mockito.when(domain.getUuid()).thenReturn("domain-uuid");
|
||||
|
||||
Mockito.when(domainManager.findDomainByIdOrPath(domainId, domainPath)).thenReturn(domain);
|
||||
Mockito.when(domainManager.getDomain(domainId)).thenReturn(domain);
|
||||
|
||||
UserAccount userAccount = mock(UserAccount.class);
|
||||
Mockito.when(userAccount.getId()).thenReturn(100L);
|
||||
Mockito.when(userAccount.getAccountId()).thenReturn(200L);
|
||||
Mockito.when(userAccount.getUsername()).thenReturn(username);
|
||||
Mockito.when(userAccount.getFirstname()).thenReturn("First");
|
||||
Mockito.when(userAccount.getLastname()).thenReturn("Last");
|
||||
Mockito.when(userAccount.getTimezone()).thenReturn("UTC");
|
||||
Mockito.when(userAccount.getRegistrationToken()).thenReturn("token");
|
||||
Mockito.when(userAccount.isRegistered()).thenReturn(true);
|
||||
Mockito.when(userAccount.getDomainId()).thenReturn(domainId);
|
||||
Map<String, String> userAccDetails = new HashMap<>();
|
||||
userAccDetails.put(UserDetailVO.PasswordChangeRequired, "true");
|
||||
Mockito.when(userAccount.getDetails()).thenReturn(userAccDetails);
|
||||
|
||||
Mockito.when(accountManager.authenticateUser(username, password, domainId, loginIp, requestParams)).thenReturn(userAccount);
|
||||
Mockito.when(accountManager.clearUserTwoFactorAuthenticationInSetupStateOnLogin(userAccount)).thenReturn(userAccount);
|
||||
|
||||
Account account = mock(Account.class);
|
||||
Mockito.when(account.getAccountName()).thenReturn("account");
|
||||
Mockito.when(account.getDomainId()).thenReturn(domainId);
|
||||
Mockito.when(account.getType()).thenReturn(Account.Type.NORMAL);
|
||||
Mockito.when(account.getType()).thenReturn(Account.Type.NORMAL);
|
||||
Mockito.when(accountManager.getAccount(200L)).thenReturn(account);
|
||||
|
||||
UserVO userVO = mock(UserVO.class);
|
||||
Mockito.when(userVO.getUuid()).thenReturn("user-uuid");
|
||||
Mockito.when(accountManager.getActiveUser(100L)).thenReturn(userVO);
|
||||
|
||||
Mockito.when(session.getAttributeNames()).thenReturn(Collections.enumeration(List.of(PASSWORD_CHANGE_REQUIRED)));
|
||||
Mockito.when(session.getAttribute(PASSWORD_CHANGE_REQUIRED)).thenReturn(Boolean.TRUE);
|
||||
|
||||
ResponseObject response = apiServer.loginUser(session, username, password, domainId, domainPath, loginIp, requestParams);
|
||||
Assert.assertNotNull(response);
|
||||
Assert.assertTrue(response instanceof LoginCmdResponse);
|
||||
Mockito.verify(session).setAttribute(eq("userid"), eq(100L));
|
||||
Mockito.verify(session).setAttribute(eq(ApiConstants.SESSIONKEY), anyString());
|
||||
}
|
||||
|
||||
@Test(expected = CloudAuthenticationException.class)
|
||||
public void testLoginUserDomainNotFound() throws Exception {
|
||||
Mockito.when(domainManager.findDomainByIdOrPath(anyLong(), anyString())).thenReturn(null);
|
||||
apiServer.loginUser(session, "user", "pass", 1L, "/", null, null);
|
||||
}
|
||||
|
||||
@Test(expected = CloudAuthenticationException.class)
|
||||
public void testLoginUserAuthFailed() throws Exception {
|
||||
DomainVO domain = mock(DomainVO.class);
|
||||
Mockito.when(domain.getId()).thenReturn(1L);
|
||||
Mockito.when(domainManager.findDomainByIdOrPath(anyLong(), anyString())).thenReturn(domain);
|
||||
Mockito.when(accountManager.authenticateUser(anyString(), anyString(), anyLong(), any(), any())).thenReturn(null);
|
||||
apiServer.loginUser(session, "user", "pass", 1L, "/", null, null);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import java.net.InetAddress;
|
|||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -432,6 +433,44 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
|
|||
prepareMockAndExecuteUpdateUserTest(1);
|
||||
}
|
||||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void updateUserTestPwdChangeDisabledUser() {
|
||||
Mockito.when(userVoMock.getState()).thenReturn(State.DISABLED);
|
||||
updateUserPwdChange();
|
||||
}
|
||||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void updateUserTestPwdChangeLockedUser() {
|
||||
Mockito.when(userVoMock.getState()).thenReturn(State.LOCKED);
|
||||
updateUserPwdChange();
|
||||
}
|
||||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void updateUserTestPwdChangeDisabledAccount() {
|
||||
Mockito.when(userVoMock.getState()).thenReturn(State.ENABLED);
|
||||
Mockito.when(accountMock.getState()).thenReturn(State.LOCKED);
|
||||
updateUserPwdChange();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateUserTestPwdChange() {
|
||||
Mockito.when(userVoMock.getState()).thenReturn(State.ENABLED);
|
||||
Mockito.when(accountMock.getState()).thenReturn(State.ENABLED);
|
||||
updateUserPwdChange();
|
||||
}
|
||||
|
||||
private void updateUserPwdChange() {
|
||||
Mockito.doReturn(true).when(UpdateUserCmdMock).isPasswordChangeRequired();
|
||||
Mockito.when(userVoMock.getAccountId()).thenReturn(10L);
|
||||
Mockito.doReturn(accountMock).when(accountManagerImpl).getAccount(10L);
|
||||
Mockito.when(accountMock.getAccountId()).thenReturn(10L);
|
||||
Mockito.doReturn(false).when(accountManagerImpl).isRootAdmin(10L);
|
||||
Mockito.lenient().when(accountManagerImpl.getRoleType(Mockito.eq(accountMock))).thenReturn(RoleType.User);
|
||||
Mockito.when(callingUser.getAccountId()).thenReturn(1L);
|
||||
Mockito.doReturn(true).when(accountManagerImpl).isRootAdmin(1L);
|
||||
prepareMockAndExecuteUpdateUserTest(0);
|
||||
}
|
||||
|
||||
private void prepareMockAndExecuteUpdateUserTest(int numberOfExpectedCallsForSetEmailAndSetTimeZone) {
|
||||
Mockito.doReturn("password").when(UpdateUserCmdMock).getPassword();
|
||||
Mockito.doReturn("newpassword").when(UpdateUserCmdMock).getCurrentPassword();
|
||||
|
|
@ -1592,4 +1631,104 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
|
|||
|
||||
accountManagerImpl.checkCallerApiPermissionsForUserOrAccountOperations(accountMock);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
public void testPasswordChangeRequiredWithSamlThrowsException() {
|
||||
accountManagerImpl.createUser(
|
||||
"user", "pass", "fn", "ln", "e@mail.com",
|
||||
"UTC", "acct", 1L, null,
|
||||
User.Source.SAML2, true
|
||||
);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
public void testPasswordChangeRequiredWithLdapSourceThrows() {
|
||||
accountManagerImpl.createUser(
|
||||
"user", "pass", "fn", "ln", "e@mail.com",
|
||||
"UTC", "acct", 1L, null,
|
||||
User.Source.LDAP, true);
|
||||
}
|
||||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void testDomainNotFound() {
|
||||
Mockito.when(_domainMgr.getDomain(1L)).thenReturn(null);
|
||||
accountManagerImpl.createUser(
|
||||
"user", "pass", "fn", "ln", "e@mail.com",
|
||||
"UTC", "acct", 1L, null,
|
||||
User.Source.UNKNOWN, false);
|
||||
}
|
||||
|
||||
@Test(expected = CloudRuntimeException.class)
|
||||
public void testCreateUserInactiveDomain() {
|
||||
Mockito.when(domainVoMock.getState()).thenReturn(Domain.State.Inactive);
|
||||
Mockito.when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domainVoMock);
|
||||
accountManagerImpl.createUser(
|
||||
"user", "pass", "fn", "ln", "e@mail.com",
|
||||
"UTC", "acct", 1L, null,
|
||||
User.Source.NATIVE, false);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
public void testCreateUserCheckAccess() {
|
||||
Mockito.when(domainVoMock.getState()).thenReturn(Domain.State.Active);
|
||||
Mockito.when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domainVoMock);
|
||||
Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class));
|
||||
accountManagerImpl.createUser(
|
||||
"user", "pass", "fn", "ln", "e@mail.com",
|
||||
"UTC", "acct", 1L, null,
|
||||
User.Source.NATIVE, false);
|
||||
}
|
||||
|
||||
@Test(expected = InvalidParameterValueException.class)
|
||||
public void testCreateUserMissingOrProjectAccount() {
|
||||
Mockito.when(domainVoMock.getState()).thenReturn(Domain.State.Active);
|
||||
Mockito.when(_accountDao.findEnabledAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(accountMock);
|
||||
Mockito.when(accountMock.getType()).thenReturn(Account.Type.PROJECT);
|
||||
Mockito.when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domainVoMock);
|
||||
Mockito.doNothing().when(accountManagerImpl).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class));
|
||||
accountManagerImpl.createUser(
|
||||
"user", "pass", "fn", "ln", "e@mail.com",
|
||||
"UTC", "acct", 1L, null,
|
||||
User.Source.NATIVE, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateUserSuccess() {
|
||||
Account rootAdminAccount = Mockito.mock(Account.class);
|
||||
Mockito.when(rootAdminAccount.getId()).thenReturn(1L);
|
||||
Mockito.when(accountManagerImpl.isRootAdmin(1L)).thenReturn(true);
|
||||
User callingUser = Mockito.mock(User.class);
|
||||
CallContext.register(callingUser, rootAdminAccount);
|
||||
|
||||
String newPassword = "newPassword";
|
||||
configureUserMockAuthenticators(newPassword);
|
||||
Mockito.doNothing().when(accountManagerImpl).checkAccess(any(Account.class), any(Domain.class));
|
||||
Mockito.doReturn(accountMock).when(accountManagerImpl).getAccount(Mockito.anyLong());
|
||||
Mockito.doNothing().when(passwordPolicyMock).verifyIfPasswordCompliesWithPasswordPolicies(Mockito.anyString(), Mockito.anyString(), Mockito.anyLong());
|
||||
Mockito.when(_domainMgr.getDomain(Mockito.anyLong())).thenReturn(domainVoMock);
|
||||
Mockito.when(domainVoMock.getState()).thenReturn(Domain.State.Active);
|
||||
|
||||
Mockito.when(_accountDao.findEnabledAccount(Mockito.anyString(), Mockito.anyLong())).thenReturn(accountMock);
|
||||
Mockito.when(accountMock.getId()).thenReturn(10L);
|
||||
Mockito.when(accountMock.getType()).thenReturn(Account.Type.NORMAL);
|
||||
|
||||
Mockito.when(userAccountDaoMock.validateUsernameInDomain(Mockito.anyString(), Mockito.anyLong())).thenReturn(true);
|
||||
Mockito.when(userDaoMock.findUsersByName(Mockito.anyString())).thenReturn(Collections.emptyList());
|
||||
UserVO createdUser = new UserVO();
|
||||
String userMockUUID = "userMockUUID";
|
||||
createdUser.setUuid(userMockUUID);
|
||||
Mockito.when(userDaoMock.persist(Mockito.any(UserVO.class))).thenReturn(createdUser);
|
||||
UserVO userResultVO = accountManagerImpl.createUser(
|
||||
"user", newPassword, "fn", "ln", "e@mail.com",
|
||||
"UTC", "acct", 1L, null,
|
||||
User.Source.NATIVE, false
|
||||
);
|
||||
Assert.assertNotNull(userResultVO);
|
||||
UserVO userResultPasswordChangeVO = accountManagerImpl.createUser(
|
||||
"user", newPassword, "fn", "ln", "e@mail.com",
|
||||
"UTC", "acct", 1L, null,
|
||||
User.Source.NATIVE, true
|
||||
);
|
||||
Assert.assertNotNull(userResultVO);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,11 @@
|
|||
// 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 org.apache.cloudstack.api.ServerApiException;
|
||||
import org.apache.cloudstack.framework.config.ConfigKey;
|
||||
import org.apache.cloudstack.resourcedetail.UserDetailVO;
|
||||
|
|
@ -45,6 +49,12 @@ public class UserPasswordResetManagerImplTest {
|
|||
@Mock
|
||||
private UserDetailsDao userDetailsDao;
|
||||
|
||||
@Mock
|
||||
AccountManager accountManager;
|
||||
|
||||
@Mock
|
||||
UserDao userDao;
|
||||
|
||||
@Test
|
||||
public void testGetMessageBody() {
|
||||
ConfigKey<String> passwordResetMailTemplate = Mockito.mock(ConfigKey.class);
|
||||
|
|
@ -147,4 +157,21 @@ public class UserPasswordResetManagerImplTest {
|
|||
|
||||
Assert.assertFalse(passwordReset.validateExistingToken(userAccount));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResetPassword() {
|
||||
UserAccount userAccount = Mockito.mock(UserAccount.class);
|
||||
UserVO userVO = Mockito.mock(UserVO.class);
|
||||
long userId = 1L;
|
||||
String newPassword = "newPassword";
|
||||
Mockito.when(userAccount.getId()).thenReturn(userId);
|
||||
Mockito.when(userDao.getUser(userId)).thenReturn(userVO);
|
||||
passwordReset.resetPassword(userAccount, newPassword);
|
||||
Mockito.verify(userDao).getUser(userId);
|
||||
Mockito.verify(accountManager).validateUserPasswordAndUpdateIfNeeded(newPassword, userVO, "", true);
|
||||
Mockito.verify(userDetailsDao).removeDetail(userId, PasswordResetToken);
|
||||
Mockito.verify(userDetailsDao).removeDetail(userId, PasswordResetTokenExpiryDate);
|
||||
Mockito.verify(userDetailsDao).removeDetail(userId, UserDetailVO.PasswordChangeRequired);
|
||||
Mockito.verify(userDao).persist(userVO);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -539,6 +539,8 @@
|
|||
"label.change.ipaddress": "Change IP address for NIC",
|
||||
"label.change.disk.offering": "Change disk offering",
|
||||
"label.change.offering.for.volume": "Change disk offering for the volume",
|
||||
"label.change.password.onlogin": "User must change password at next login",
|
||||
"label.change.password.reset": "Force password reset",
|
||||
"label.change.service.offering": "Change service offering",
|
||||
"label.character": "Character",
|
||||
"label.checksum": "Checksum",
|
||||
|
|
@ -3169,6 +3171,8 @@
|
|||
"message.change.offering.for.volume.failed": "Change offering for the volume failed",
|
||||
"message.change.offering.for.volume.processing": "Changing offering for the volume...",
|
||||
"message.change.password": "Please change your password.",
|
||||
"message.change.password.required": "You are required to change your password.",
|
||||
"message.change.password.reset": "Force password reset on next login.",
|
||||
"message.change.scope.failed": "Scope change failed",
|
||||
"message.change.scope.processing": "Scope change in progress",
|
||||
"message.change.service.offering.sharedfs.failed": "Failed to change service offering for the Shared FileSystem.",
|
||||
|
|
@ -3419,6 +3423,7 @@
|
|||
"message.error.apply.tungsten.tag": "Applying Tag failed",
|
||||
"message.error.binaries.iso.url": "Please enter binaries ISO URL.",
|
||||
"message.error.bucket": "Please enter bucket",
|
||||
"message.error.change.password": "Failed to change password.",
|
||||
"message.error.cidr": "CIDR is required",
|
||||
"message.error.cidr.or.cidrsize": "CIDR or cidr size is required",
|
||||
"message.error.cloudian.console": "Single-Sign-On failed for Cloudian management console. Please ask your administrator to fix integration issues.",
|
||||
|
|
@ -3482,6 +3487,7 @@
|
|||
"message.error.netmask": "Please enter Netmask.",
|
||||
"message.error.network.offering": "Please select Network offering.",
|
||||
"message.error.new.password": "Please enter new password.",
|
||||
"message.error.newpassword.sameascurrent": "New password cannot be the same as the current password.",
|
||||
"message.error.nexus1000v.ipaddress": "Please enter Nexus 1000v IP address.",
|
||||
"message.error.nexus1000v.password": "Please enter Nexus 1000v password.",
|
||||
"message.error.nexus1000v.username": "Please enter Nexus 1000v username.",
|
||||
|
|
@ -3726,6 +3732,7 @@
|
|||
"message.please.confirm.remove.user.data": "Please confirm that you want to remove this User Data",
|
||||
"message.please.enter.valid.value": "Please enter a valid value.",
|
||||
"message.please.enter.value": "Please enter values.",
|
||||
"message.please.login.new.password": "Please log in again with your new password",
|
||||
"message.please.wait.while.autoscale.vmgroup.is.being.created": "Please wait while your AutoScaling Group is being created; this may take a while...",
|
||||
"message.please.wait.while.zone.is.being.created": "Please wait while your Zone is being created; this may take a while...",
|
||||
"message.pod.dedicated": "Pod dedicated.",
|
||||
|
|
|
|||
|
|
@ -318,6 +318,11 @@ export const constantRouterMap = [
|
|||
path: 'resetPassword',
|
||||
name: 'resetPassword',
|
||||
component: () => import(/* webpackChunkName: "auth" */ '@/views/auth/ResetPassword')
|
||||
},
|
||||
{
|
||||
path: 'forceChangePassword',
|
||||
name: 'forceChangePassword',
|
||||
component: () => import(/* webpackChunkName: "auth" */ '@/views/iam/ForceChangePassword')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -82,6 +82,24 @@ export default {
|
|||
popup: true,
|
||||
component: shallowRef(defineAsyncComponent(() => import('@/views/iam/EditUser.vue')))
|
||||
},
|
||||
{
|
||||
api: 'updateUser',
|
||||
icon: 'redo-outlined',
|
||||
label: 'label.change.password.reset',
|
||||
message: 'message.change.password.reset',
|
||||
dataView: true,
|
||||
args: ['passwordchangerequired'],
|
||||
mapping: {
|
||||
passwordchangerequired: {
|
||||
value: (record) => { return true }
|
||||
}
|
||||
},
|
||||
popup: true,
|
||||
show: (record, store) => {
|
||||
return ['Admin', 'DomainAdmin'].includes(store.userInfo.roletype) && !record.isdefault &&
|
||||
store.userInfo.id !== record.id && record.state === 'enabled' && record.usersource === 'native'
|
||||
}
|
||||
},
|
||||
{
|
||||
api: 'updateUser',
|
||||
icon: 'key-outlined',
|
||||
|
|
|
|||
|
|
@ -94,6 +94,16 @@ router.beforeEach((to, from, next) => {
|
|||
}
|
||||
store.commit('SET_LOGIN_FLAG', true)
|
||||
}
|
||||
// store already loaded
|
||||
if (store.getters.passwordChangeRequired) {
|
||||
if (to.path === '/user/forceChangePassword') {
|
||||
next()
|
||||
} else {
|
||||
next({ path: '/user/forceChangePassword' })
|
||||
NProgress.done()
|
||||
}
|
||||
return
|
||||
}
|
||||
if (Object.keys(store.getters.apis).length === 0) {
|
||||
const cachedApis = vueProps.$localStorage.get(APIS, {})
|
||||
if (Object.keys(cachedApis).length > 0) {
|
||||
|
|
@ -102,6 +112,19 @@ router.beforeEach((to, from, next) => {
|
|||
store
|
||||
.dispatch('GetInfo')
|
||||
.then(apis => {
|
||||
// Essential for Page Refresh scenarios
|
||||
if (store.getters.passwordChangeRequired) {
|
||||
// Only allow the Change Password page
|
||||
if (to.path === '/user/forceChangePassword') {
|
||||
next()
|
||||
} else {
|
||||
// Redirect everything else (including dashboard, wildcards) to Change Password
|
||||
next({ path: '/user/forceChangePassword' })
|
||||
NProgress.done()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
store.dispatch('GenerateRoutes', { apis }).then(() => {
|
||||
store.getters.addRouters.map(route => {
|
||||
router.addRoute(route)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ const getters = {
|
|||
loginFlag: state => state.user.loginFlag,
|
||||
allProjects: (state) => state.app.allProjects,
|
||||
customHypervisorName: state => state.user.customHypervisorName,
|
||||
readyForShutdownPollingJob: state => state.user.readyForShutdownPollingJob
|
||||
readyForShutdownPollingJob: state => state.user.readyForShutdownPollingJob,
|
||||
passwordChangeRequired: state => state.user.passwordChangeRequired
|
||||
}
|
||||
|
||||
export default getters
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ import {
|
|||
MS_ID,
|
||||
OAUTH_DOMAIN,
|
||||
OAUTH_PROVIDER,
|
||||
LATEST_CS_VERSION
|
||||
LATEST_CS_VERSION,
|
||||
PASSWORD_CHANGE_REQUIRED
|
||||
} from '@/store/mutation-types'
|
||||
|
||||
import {
|
||||
|
|
@ -80,7 +81,8 @@ const user = {
|
|||
twoFaProvider: '',
|
||||
twoFaIssuer: '',
|
||||
customHypervisorName: 'Custom',
|
||||
readyForShutdownPollingJob: ''
|
||||
readyForShutdownPollingJob: '',
|
||||
passwordChangeRequired: false
|
||||
},
|
||||
|
||||
mutations: {
|
||||
|
|
@ -196,6 +198,14 @@ const user = {
|
|||
vueProps.$localStorage.set(LATEST_CS_VERSION, version)
|
||||
state.latestVersion = version
|
||||
}
|
||||
},
|
||||
SET_PASSWORD_CHANGE_REQUIRED: (state, required) => {
|
||||
state.passwordChangeRequired = required
|
||||
if (required) {
|
||||
vueProps.$localStorage.set(PASSWORD_CHANGE_REQUIRED, true)
|
||||
} else {
|
||||
vueProps.$localStorage.remove(PASSWORD_CHANGE_REQUIRED)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -244,6 +254,13 @@ const user = {
|
|||
if (result && result.managementserverid) {
|
||||
commit('SET_MS_ID', result.managementserverid)
|
||||
}
|
||||
if (result.passwordchangerequired) {
|
||||
commit('SET_PASSWORD_CHANGE_REQUIRED', true)
|
||||
commit('SET_APIS', {})
|
||||
vueProps.$localStorage.remove(APIS)
|
||||
} else {
|
||||
commit('SET_PASSWORD_CHANGE_REQUIRED', false)
|
||||
}
|
||||
const latestVersion = vueProps.$localStorage.get(LATEST_CS_VERSION, { version: '', fetchedTs: 0 })
|
||||
commit('SET_LATEST_VERSION', latestVersion)
|
||||
notification.destroy()
|
||||
|
|
@ -323,6 +340,15 @@ const user = {
|
|||
commit('SET_DOMAIN_STORE', domainStore)
|
||||
commit('SET_DARK_MODE', darkMode)
|
||||
commit('SET_LATEST_VERSION', latestVersion)
|
||||
|
||||
// This block is to enforce password change for first time login after admin resets password
|
||||
const isPwdChangeRequired = vueProps.$localStorage.get(PASSWORD_CHANGE_REQUIRED)
|
||||
commit('SET_PASSWORD_CHANGE_REQUIRED', isPwdChangeRequired)
|
||||
if (isPwdChangeRequired) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
if (hasAuth) {
|
||||
console.log('Login detected, using cached APIs')
|
||||
commit('SET_ZONES', cachedZones)
|
||||
|
|
@ -485,6 +511,8 @@ const user = {
|
|||
vueProps.$localStorage.remove(ACCESS_TOKEN)
|
||||
vueProps.$localStorage.remove(HEADER_NOTICES)
|
||||
|
||||
commit('SET_PASSWORD_CHANGE_REQUIRED', false)
|
||||
|
||||
logout(state.token).then(() => {
|
||||
message.destroy()
|
||||
if (cloudianUrl) {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export const RELOAD_ALL_PROJECTS = 'RELOAD_ALL_PROJECTS'
|
|||
export const MS_ID = 'MS_ID'
|
||||
export const OAUTH_DOMAIN = 'OAUTH_DOMAIN'
|
||||
export const OAUTH_PROVIDER = 'OAUTH_PROVIDER'
|
||||
export const PASSWORD_CHANGE_REQUIRED = 'PASSWORD_CHANGE_REQUIRED'
|
||||
|
||||
export const CONTENT_WIDTH_TYPE = {
|
||||
Fluid: 'Fluid',
|
||||
|
|
|
|||
|
|
@ -133,11 +133,16 @@
|
|||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="isAdminOrDomainAdmin() && !samlEnable" name="passwordChangeRequired" ref="passwordChangeRequired">
|
||||
<a-checkbox v-model:checked="form.passwordChangeRequired">
|
||||
{{ $t('label.change.password.onlogin') }}
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
<div v-if="samlAllowed">
|
||||
<a-form-item name="samlenable" ref="samlenable" :label="$t('label.samlenable')">
|
||||
<a-switch v-model:checked="form.samlenable" />
|
||||
<a-form-item name="samlEnable" ref="samlEnable" :label="$t('label.samlenable')">
|
||||
<a-switch v-model:checked="samlEnable" />
|
||||
</a-form-item>
|
||||
<a-form-item name="samlentity" ref="samlentity" v-if="form.samlenable">
|
||||
<a-form-item name="samlentity" ref="samlentity" v-if="samlEnable">
|
||||
<template #label>
|
||||
<tooltip-label :title="$t('label.samlentity')" :tooltip="apiParams.entityid.description"/>
|
||||
</template>
|
||||
|
|
@ -198,6 +203,13 @@ export default {
|
|||
this.initForm()
|
||||
this.fetchData()
|
||||
},
|
||||
watch: {
|
||||
samlEnable (newVal) {
|
||||
if (newVal) {
|
||||
this.form.passwordChangeRequired = false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
samlAllowed () {
|
||||
return 'authorizeSamlSso' in this.$store.getters.apis
|
||||
|
|
@ -291,9 +303,9 @@ export default {
|
|||
})
|
||||
|
||||
const user = userCreationResponse?.createuserresponse?.user
|
||||
if (values.samlenable && user) {
|
||||
if (this.samlEnable && user) {
|
||||
await postAPI('authorizeSamlSso', {
|
||||
enable: values.samlenable,
|
||||
enable: this.samlEnable,
|
||||
entityid: values.samlentity,
|
||||
userid: user.id
|
||||
})
|
||||
|
|
@ -347,6 +359,9 @@ export default {
|
|||
if (this.isValidValueForKey(rawParams, 'timezone') && rawParams.timezone.length > 0) {
|
||||
params.timezone = rawParams.timezone
|
||||
}
|
||||
if (this.isAdminOrDomainAdmin() && rawParams.passwordChangeRequired === true) {
|
||||
params.passwordchangerequired = rawParams.passwordChangeRequired
|
||||
}
|
||||
|
||||
return postAPI('createUser', params)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -49,6 +49,11 @@
|
|||
v-model:value="form.confirmpassword"
|
||||
:placeholder="$t('label.confirmpassword.description')"/>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="isAdminOrDomainAdmin() && isCallerNotSameAsUser()" name="passwordChangeRequired" ref="passwordChangeRequired">
|
||||
<a-checkbox v-model:checked="form.passwordChangeRequired">
|
||||
{{ $t('label.change.password.onlogin') }}
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
|
||||
<div :span="24" class="action-button">
|
||||
<a-button @click="closeAction">{{ $t('label.cancel') }}</a-button>
|
||||
|
|
@ -102,6 +107,11 @@ export default {
|
|||
isAdminOrDomainAdmin () {
|
||||
return ['Admin', 'DomainAdmin'].includes(this.$store.getters.userInfo.roletype)
|
||||
},
|
||||
isCallerNotSameAsUser () {
|
||||
const userId = this.$store.getters.userInfo.id
|
||||
const resourceId = this.resource?.id ?? null
|
||||
return userId !== resourceId
|
||||
},
|
||||
isValidValueForKey (obj, key) {
|
||||
return key in obj && obj[key] != null
|
||||
},
|
||||
|
|
@ -134,6 +144,10 @@ export default {
|
|||
if (this.isValidValueForKey(values, 'currentpassword') && values.currentpassword.length > 0) {
|
||||
params.currentpassword = values.currentpassword
|
||||
}
|
||||
|
||||
if (this.isAdminOrDomainAdmin() && values.passwordChangeRequired === true) {
|
||||
params.passwordchangerequired = values.passwordChangeRequired
|
||||
}
|
||||
postAPI('updateUser', params).then(json => {
|
||||
this.$notification.success({
|
||||
message: this.$t('label.action.change.password'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,285 @@
|
|||
// 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>
|
||||
<div class="user-layout-wrapper">
|
||||
<div class="container">
|
||||
<div class="user-layout-content">
|
||||
<a-card :bordered="false" class="force-password-card">
|
||||
<template #title>
|
||||
<div style="text-align: center; font-size: 18px; font-weight: bold;">
|
||||
{{ $t('label.action.change.password') }}
|
||||
</div>
|
||||
<div v-if="!isSubmitted" style="text-align: center; font-size: 14px; color: #666; margin-top: 5px;">
|
||||
{{ $t('message.change.password.required') }}
|
||||
</div>
|
||||
</template>
|
||||
<a-spin :spinning="loading">
|
||||
<div v-if="isSubmitted" class="success-state">
|
||||
<check-outlined class="success-icon" />
|
||||
<div class="success-text">
|
||||
{{ $t('message.success.change.password') }}
|
||||
</div>
|
||||
<div class="success-subtext">
|
||||
{{ $t('message.please.login.new.password') }}
|
||||
</div>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
@click="redirectToLogin()"
|
||||
style="margin-top: 20px;"
|
||||
>
|
||||
{{ $t('label.login') }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-form
|
||||
v-else
|
||||
:ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
@finish="handleSubmit"
|
||||
v-ctrl-enter="handleSubmit"
|
||||
>
|
||||
<a-form-item name="currentpassword" :label="$t('label.currentpassword')">
|
||||
<a-input-password
|
||||
v-model:value="form.currentpassword"
|
||||
:placeholder="$t('label.currentpassword')"
|
||||
size="large"
|
||||
v-focus="true"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="password" :label="$t('label.new.password')">
|
||||
<a-input-password
|
||||
v-model:value="form.password"
|
||||
:placeholder="$t('label.new.password')"
|
||||
size="large"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="confirmpassword" :label="$t('label.confirmpassword')">
|
||||
<a-input-password
|
||||
v-model:value="form.confirmpassword"
|
||||
:placeholder="$t('label.confirmpassword')"
|
||||
size="large"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
html-type="submit"
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
:disabled="loading"
|
||||
:loading="loading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ $t('label.ok') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
|
||||
<div class="actions">
|
||||
<a @click="logoutAndRedirectToLogin()">{{ $t('label.logout') }}</a>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, toRaw } from 'vue'
|
||||
import { postAPI } from '@/api'
|
||||
import Cookies from 'js-cookie'
|
||||
import { PASSWORD_CHANGE_REQUIRED } from '@/store/mutation-types'
|
||||
|
||||
export default {
|
||||
name: 'ForceChangePassword',
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
isSubmitted: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.formRef = ref()
|
||||
this.form = reactive({})
|
||||
this.isPasswordChangeRequired()
|
||||
},
|
||||
computed: {
|
||||
rules () {
|
||||
return {
|
||||
currentpassword: [{ required: true, message: this.$t('message.error.current.password') }],
|
||||
password: [
|
||||
{ required: true, message: this.$t('message.error.new.password') },
|
||||
{ validator: this.validateNewPassword, trigger: 'change' }
|
||||
],
|
||||
confirmpassword: [
|
||||
{ required: true, message: this.$t('message.error.confirm.password') },
|
||||
{ validator: this.validateTwoPassword, trigger: 'change' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async validateNewPassword (rule, value) {
|
||||
const currentPassword = this.form.currentpassword
|
||||
if (!value || value.length === 0) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
// Ensure new password is different from current password
|
||||
if (currentPassword && value === currentPassword) {
|
||||
return Promise.reject(this.$t('message.error.newpassword.sameascurrent'))
|
||||
}
|
||||
return Promise.resolve()
|
||||
},
|
||||
async validateTwoPassword (rule, value) {
|
||||
if (!value || value.length === 0) {
|
||||
return Promise.resolve()
|
||||
} else if (rule.field === 'confirmpassword') {
|
||||
const form = this.form
|
||||
const messageConfirm = this.$t('message.validate.equalto')
|
||||
const passwordVal = form.password
|
||||
if (passwordVal && passwordVal !== value) {
|
||||
return Promise.reject(messageConfirm)
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
} else {
|
||||
return Promise.resolve()
|
||||
}
|
||||
},
|
||||
handleSubmit (e) {
|
||||
e.preventDefault()
|
||||
if (this.loading) return
|
||||
this.formRef.value.validate().then(() => {
|
||||
this.loading = true
|
||||
const values = toRaw(this.form)
|
||||
const userId = Cookies.get('userid')
|
||||
|
||||
const params = {
|
||||
id: userId,
|
||||
password: values.password,
|
||||
currentpassword: values.currentpassword
|
||||
}
|
||||
postAPI('updateUser', params).then(async () => {
|
||||
this.$localStorage.remove(PASSWORD_CHANGE_REQUIRED)
|
||||
await this.handleLogout()
|
||||
this.isSubmitted = true
|
||||
}).catch(error => {
|
||||
console.error(error)
|
||||
this.$message.error(this.$t('message.error.change.password'))
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}).catch(error => {
|
||||
console.log('Validation failed:', error)
|
||||
})
|
||||
},
|
||||
async handleLogout () {
|
||||
try {
|
||||
await this.$store.dispatch('Logout')
|
||||
} catch (e) {
|
||||
console.error('Logout failed:', e)
|
||||
} finally {
|
||||
Cookies.remove('userid')
|
||||
Cookies.remove('token')
|
||||
}
|
||||
},
|
||||
redirectToLogin () {
|
||||
this.$router.replace('/user/login')
|
||||
},
|
||||
logoutAndRedirectToLogin () {
|
||||
this.handleLogout().then(() => {
|
||||
this.redirectToLogin()
|
||||
})
|
||||
},
|
||||
async isPasswordChangeRequired () {
|
||||
const passwordChangeRequired = this.$localStorage.get(PASSWORD_CHANGE_REQUIRED)
|
||||
this.isSubmitted = !passwordChangeRequired
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.user-layout-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
|
||||
.user-layout-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.force-password-card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
|
||||
a {
|
||||
color: #1890ff;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.success-state {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
|
||||
.success-icon {
|
||||
font-size: 48px;
|
||||
color: #52c41a;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.success-text {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.success-subtext {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue