CLOUDSTACK-8701: Add listandswitchsamlaccount API test and add boundary checks

- Adds unit test for ListAndSwitchSAMLAccountCmd
- Checks and logs in user only if they are enabled
- If saml user switches to a locked account, send appropriate error message

Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
(cherry picked from commit b30977911d)
Signed-off-by: Rohit Yadav <rohit.yadav@shapeblue.com>
This commit is contained in:
Rohit Yadav 2015-08-12 14:04:08 +05:30
parent fcbee609b4
commit 25ccf4126d
3 changed files with 231 additions and 15 deletions

View File

@ -19,6 +19,7 @@ package org.apache.cloudstack.api.command;
import com.cloud.api.response.ApiResponseSerializer;
import com.cloud.domain.Domain;
import com.cloud.domain.dao.DomainDao;
import com.cloud.exception.CloudAuthenticationException;
import com.cloud.user.Account;
import com.cloud.user.User;
import com.cloud.user.UserAccount;
@ -50,6 +51,7 @@ import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -131,7 +133,14 @@ public class ListAndSwitchSAMLAccountCmd extends BaseCmd implements APIAuthentic
final User user = _userDao.findByUuid(userUuid);
final Domain domain = _domainDao.findByUuid(domainUuid);
final UserAccount nextUserAccount = _accountService.getUserAccountById(user.getId());
if (!nextUserAccount.getUsername().equals(currentUserAccount.getUsername())
if (nextUserAccount != null && !nextUserAccount.getAccountState().equals(Account.State.enabled.toString())) {
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.PARAM_ERROR.getHttpCode(),
"The requested user account is locked and cannot be switched to, please contact your administrator.",
params, responseType));
}
if (nextUserAccount == null
|| !nextUserAccount.getAccountState().equals(Account.State.enabled.toString())
|| !nextUserAccount.getUsername().equals(currentUserAccount.getUsername())
|| !nextUserAccount.getExternalEntity().equals(currentUserAccount.getExternalEntity())
|| (nextUserAccount.getDomainId() != domain.getId())
|| (nextUserAccount.getSource() != User.Source.SAML2)) {
@ -147,7 +156,8 @@ public class ListAndSwitchSAMLAccountCmd extends BaseCmd implements APIAuthentic
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
return ApiResponseSerializer.toSerializedString(loginResponse, responseType);
}
} catch (final Exception ignored) {
} catch (CloudAuthenticationException | IOException exception) {
s_logger.debug("Failed to switch to request SAML user account due to: " + exception.getMessage());
}
} else {
List<UserAccountVO> switchableAccounts = _userAccountDao.getAllUsersByNameAndEntity(currentUserAccount.getUsername(), currentUserAccount.getExternalEntity());

View File

@ -17,6 +17,7 @@
package org.apache.cloudstack.api.command;
import com.cloud.api.response.ApiResponseSerializer;
import com.cloud.exception.CloudAuthenticationException;
import com.cloud.user.Account;
import com.cloud.user.DomainManager;
import com.cloud.user.UserAccount;
@ -293,28 +294,32 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
UserAccount userAccount = null;
List<UserAccountVO> possibleUserAccounts = _userAccountDao.getAllUsersByNameAndEntity(username, issuer.getValue());
if (possibleUserAccounts != null && possibleUserAccounts.size() > 0) {
// By default, log into the first user account
// Log into the first enabled user account
// Users can switch to other allowed accounts later
userAccount = possibleUserAccounts.get(0);
for (UserAccountVO possibleUserAccount: possibleUserAccounts) {
if (possibleUserAccount.getAccountState().equals(Account.State.enabled.toString())) {
userAccount = possibleUserAccount;
break;
}
}
}
if (userAccount == null || userAccount.getExternalEntity() == null || !_samlAuthManager.isUserAuthorized(userAccount.getId(), issuer.getValue())) {
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
"Your authenticated user is not authorized, please contact your administrator",
"Your authenticated user is not authorized for SAML Single Sign-On, please contact your administrator",
params, responseType));
}
if (userAccount != null) {
try {
if (_apiServer.verifyUser(userAccount.getId())) {
LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, userAccount.getUsername(), userAccount.getUsername() + userAccount.getSource().toString(),
userAccount.getDomainId(), null, remoteAddress, params);
SAMLUtils.setupSamlUserCookies(loginResponse, resp);
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
return ApiResponseSerializer.toSerializedString(loginResponse, responseType);
}
} catch (final Exception ignored) {
try {
if (_apiServer.verifyUser(userAccount.getId())) {
LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, userAccount.getUsername(), userAccount.getUsername() + userAccount.getSource().toString(),
userAccount.getDomainId(), null, remoteAddress, params);
SAMLUtils.setupSamlUserCookies(loginResponse, resp);
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
return ApiResponseSerializer.toSerializedString(loginResponse, responseType);
}
} catch (CloudAuthenticationException | IOException exception) {
s_logger.debug("SAML Login failed to log in the user due to: " + exception.getMessage());
}
}
} catch (IOException e) {

View File

@ -0,0 +1,201 @@
/*
* 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;
import com.cloud.domain.DomainVO;
import com.cloud.domain.dao.DomainDao;
import com.cloud.user.Account;
import com.cloud.user.AccountService;
import com.cloud.user.User;
import com.cloud.user.UserAccountVO;
import com.cloud.user.UserVO;
import com.cloud.user.dao.UserAccountDao;
import com.cloud.user.dao.UserDao;
import com.cloud.utils.HttpUtils;
import junit.framework.TestCase;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ApiServerService;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.auth.APIAuthenticationType;
import org.apache.cloudstack.api.response.LoginCmdResponse;
import org.apache.cloudstack.saml.SAML2AuthManager;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
@RunWith(MockitoJUnitRunner.class)
public class ListAndSwitchSAMLAccountCmdTest extends TestCase {
@Mock
ApiServerService apiServer;
@Mock
SAML2AuthManager samlAuthManager;
@Mock
AccountService accountService;
@Mock
UserAccountDao userAccountDao;
@Mock
UserDao userDao;
@Mock
DomainDao domainDao;
@Mock
HttpSession session;
@Mock
HttpServletResponse resp;
@Mock
HttpServletRequest req;
@Test
public void testListAndSwitchSAMLAccountCmd() throws Exception {
// Setup
final Map<String, Object[]> params = new HashMap<String, Object[]>();
final String sessionKeyValue = "someSessionIDValue";
Mockito.when(session.getAttribute(ApiConstants.SESSIONKEY)).thenReturn(sessionKeyValue);
Mockito.when(session.getAttribute("userid")).thenReturn(2L);
params.put(ApiConstants.USER_ID, new String[]{"2"});
params.put(ApiConstants.DOMAIN_ID, new String[]{"1"});
Mockito.when(userDao.findByUuid(Mockito.anyString())).thenReturn(new UserVO(2L));
Mockito.when(domainDao.findByUuid(Mockito.anyString())).thenReturn(new DomainVO());
// Mock/field setup
ListAndSwitchSAMLAccountCmd cmd = new ListAndSwitchSAMLAccountCmd();
Field apiServerField = ListAndSwitchSAMLAccountCmd.class.getDeclaredField("_apiServer");
apiServerField.setAccessible(true);
apiServerField.set(cmd, apiServer);
Field managerField = ListAndSwitchSAMLAccountCmd.class.getDeclaredField("_samlAuthManager");
managerField.setAccessible(true);
managerField.set(cmd, samlAuthManager);
Field accountServiceField = BaseCmd.class.getDeclaredField("_accountService");
accountServiceField.setAccessible(true);
accountServiceField.set(cmd, accountService);
Field userAccountDaoField = ListAndSwitchSAMLAccountCmd.class.getDeclaredField("_userAccountDao");
userAccountDaoField.setAccessible(true);
userAccountDaoField.set(cmd, userAccountDao);
Field userDaoField = ListAndSwitchSAMLAccountCmd.class.getDeclaredField("_userDao");
userDaoField.setAccessible(true);
userDaoField.set(cmd, userDao);
Field domainDaoField = ListAndSwitchSAMLAccountCmd.class.getDeclaredField("_domainDao");
domainDaoField.setAccessible(true);
domainDaoField.set(cmd, domainDao);
// invalid session test
try {
cmd.authenticate("command", params, null, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
} catch (ServerApiException exception) {
assertEquals(exception.getErrorCode(), ApiErrorCode.UNAUTHORIZED);
} finally {
Mockito.verify(accountService, Mockito.times(0)).getUserAccountById(Mockito.anyLong());
}
// invalid sessionkey value test
params.put(ApiConstants.SESSIONKEY, new String[]{"someOtherValue"});
try {
Mockito.when(session.isNew()).thenReturn(false);
cmd.authenticate("command", params, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
} catch (ServerApiException exception) {
assertEquals(exception.getErrorCode(), ApiErrorCode.UNAUTHORIZED);
} finally {
Mockito.verify(accountService, Mockito.times(0)).getUserAccountById(Mockito.anyLong());
}
// valid sessionkey value test
params.put(ApiConstants.SESSIONKEY, new String[]{sessionKeyValue});
try {
cmd.authenticate("command", params, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
} catch (ServerApiException exception) {
assertEquals(exception.getErrorCode(), ApiErrorCode.ACCOUNT_ERROR);
} finally {
Mockito.verify(accountService, Mockito.times(1)).getUserAccountById(Mockito.anyLong());
}
// valid sessionkey, invalid useraccount type (non-saml) value test
UserAccountVO mockedUserAccount = new UserAccountVO();
mockedUserAccount.setId(2L);
mockedUserAccount.setAccountState(Account.State.enabled.toString());
mockedUserAccount.setUsername("someUsername");
mockedUserAccount.setExternalEntity("some IDP ID");
mockedUserAccount.setDomainId(0L);
mockedUserAccount.setSource(User.Source.UNKNOWN);
Mockito.when(accountService.getUserAccountById(Mockito.anyLong())).thenReturn(mockedUserAccount);
try {
cmd.authenticate("command", params, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
} catch (ServerApiException exception) {
assertEquals(exception.getErrorCode(), ApiErrorCode.ACCOUNT_ERROR);
} finally {
// accountService should have been called twice by now, for this case and the case above
Mockito.verify(accountService, Mockito.times(2)).getUserAccountById(Mockito.anyLong());
}
// all valid test
mockedUserAccount.setSource(User.Source.SAML2);
Mockito.when(accountService.getUserAccountById(Mockito.anyLong())).thenReturn(mockedUserAccount);
Mockito.when(apiServer.verifyUser(Mockito.anyLong())).thenReturn(true);
LoginCmdResponse loginCmdResponse = new LoginCmdResponse();
loginCmdResponse.setUserId("1");
loginCmdResponse.setDomainId("1");
loginCmdResponse.setType("1");
loginCmdResponse.setUsername("userName");
loginCmdResponse.setAccount("someAccount");
loginCmdResponse.setFirstName("firstName");
loginCmdResponse.setLastName("lastName");
loginCmdResponse.setSessionKey("newSessionKeyString");
Mockito.when(apiServer.loginUser(Mockito.any(HttpSession.class), Mockito.anyString(), Mockito.anyString(),
Mockito.anyLong(), Mockito.anyString(), Mockito.anyString(), Mockito.anyMap())).thenReturn(loginCmdResponse);
try {
cmd.authenticate("command", params, session, "random", HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
} catch (ServerApiException exception) {
fail("SAML list and switch account API failed to pass for all valid data: " + exception.getMessage());
} finally {
// accountService should have been called 4 times by now, for this case twice and 2 for cases above
Mockito.verify(accountService, Mockito.times(4)).getUserAccountById(Mockito.anyLong());
Mockito.verify(resp, Mockito.times(1)).sendRedirect(Mockito.anyString());
}
}
@Test
public void testGetAPIType() {
Assert.assertTrue(new ListAndSwitchSAMLAccountCmd().getAPIType() == APIAuthenticationType.READONLY_API);
}
}