You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by bh...@apache.org on 2015/08/13 14:56:27 UTC

[1/5] git commit: updated refs/heads/4.5-samlfixes to 38d6bae

Repository: cloudstack
Updated Branches:
  refs/heads/4.5-samlfixes ce02ab9f0 -> 38d6baee8 (forced update)


CLOUDSTACK-8701: Allow SAML users to switch accounts

SAML authorized accounts might be across various domains, this allows for
switching of accounts only in case of SAML authenticated user accounts across
other accounts with the same SAML uid/username.

Moves the previous switch account logic to its own ui-custom module

Signed-off-by: Rohit Yadav <ro...@shapeblue.com>


Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo
Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/a1a6bc7d
Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/a1a6bc7d
Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/a1a6bc7d

Branch: refs/heads/4.5-samlfixes
Commit: a1a6bc7dc9d06f73063ff03bb91cccacf84fa7d8
Parents: 6eb2e6b
Author: Rohit Yadav <ro...@shapeblue.com>
Authored: Mon Aug 3 12:36:14 2015 +0530
Committer: Rohit Yadav <ro...@shapeblue.com>
Committed: Thu Aug 13 18:25:45 2015 +0530

----------------------------------------------------------------------
 client/tomcatconf/commands.properties.in        |   1 +
 .../command/GetServiceProviderMetaDataCmd.java  |   2 +-
 .../command/ListAndSwitchSAMLAccountCmd.java    | 195 +++++++++++++++++++
 .../cloudstack/api/command/ListIdpsCmd.java     |   2 +-
 .../command/SAML2LoginAPIAuthenticatorCmd.java  |  36 +---
 .../api/response/SamlUserAccountResponse.java   |  99 ++++++++++
 .../cloudstack/saml/SAML2AuthManagerImpl.java   |   2 +
 .../cloudstack/saml/SAML2UserAuthenticator.java |  22 +--
 .../org/apache/cloudstack/saml/SAMLUtils.java   |  18 ++
 ui/css/cloudstack3.css                          |   6 +-
 ui/index.jsp                                    |   8 +-
 ui/scripts/ui-custom/login.js                   |   8 +-
 ui/scripts/ui-custom/saml.js                    |  96 +++++++++
 13 files changed, 424 insertions(+), 71 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a1a6bc7d/client/tomcatconf/commands.properties.in
----------------------------------------------------------------------
diff --git a/client/tomcatconf/commands.properties.in b/client/tomcatconf/commands.properties.in
index a66a3dc..a69605c 100644
--- a/client/tomcatconf/commands.properties.in
+++ b/client/tomcatconf/commands.properties.in
@@ -29,6 +29,7 @@ getSPMetadata=15
 listIdps=15
 authorizeSamlSso=7
 listSamlAuthorization=7
+listAndSwitchSamlAccount=15
 
 ### Account commands
 createAccount=7

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a1a6bc7d/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java
index ffaad7a..bed594f 100644
--- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java
@@ -266,7 +266,7 @@ public class GetServiceProviderMetaDataCmd extends BaseCmd implements APIAuthent
             }
         }
         if (_samlAuthManager == null) {
-            s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd");
+            s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 getSPMetadata Cmd");
         }
     }
 }

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a1a6bc7d/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmd.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmd.java
new file mode 100644
index 0000000..db0d6dc
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmd.java
@@ -0,0 +1,195 @@
+// 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.api.response.ApiResponseSerializer;
+import com.cloud.domain.Domain;
+import com.cloud.domain.dao.DomainDao;
+import com.cloud.user.Account;
+import com.cloud.user.User;
+import com.cloud.user.UserAccount;
+import com.cloud.user.UserAccountVO;
+import com.cloud.user.dao.UserAccountDao;
+import com.cloud.user.dao.UserDao;
+import com.cloud.utils.HttpUtils;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.ApiServerService;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.auth.APIAuthenticationType;
+import org.apache.cloudstack.api.auth.APIAuthenticator;
+import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
+import org.apache.cloudstack.api.response.DomainResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.api.response.LoginCmdResponse;
+import org.apache.cloudstack.api.response.SamlUserAccountResponse;
+import org.apache.cloudstack.api.response.SuccessResponse;
+import org.apache.cloudstack.api.response.UserResponse;
+import org.apache.cloudstack.saml.SAML2AuthManager;
+import org.apache.cloudstack.saml.SAMLUtils;
+import org.apache.log4j.Logger;
+
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@APICommand(name = "listAndSwitchSamlAccount", description = "Lists and switches to other SAML accounts owned by the SAML user", responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
+public class ListAndSwitchSAMLAccountCmd extends BaseCmd implements APIAuthenticator {
+    public static final Logger s_logger = Logger.getLogger(ListAndSwitchSAMLAccountCmd.class.getName());
+    private static final String s_name = "listandswitchsamlaccountresponse";
+
+    @Inject
+    ApiServerService _apiServer;
+
+    @Inject
+    private UserAccountDao _userAccountDao;
+    @Inject
+    private UserDao _userDao;
+    @Inject
+    private DomainDao _domainDao;
+
+    SAML2AuthManager _samlAuthManager;
+
+    /////////////////////////////////////////////////////
+    //////////////// API parameters /////////////////////
+    /////////////////////////////////////////////////////
+
+    @Parameter(name = ApiConstants.USER_ID, type = CommandType.UUID, entityType = UserResponse.class, required = false, description = "User uuid")
+    private Long userId;
+
+    @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, required = false, description = "Domain uuid")
+    private Long domainId;
+
+    @Override
+    public String getCommandName() {
+        return s_name;
+    }
+
+    @Override
+    public long getEntityOwnerId() {
+        return Account.ACCOUNT_ID_SYSTEM;
+    }
+
+    @Override
+    public void execute() {
+        throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication plugin api, cannot be used directly");
+    }
+
+    @Override
+    public String authenticate(final String command, final Map<String, Object[]> params, final HttpSession session, final String remoteAddress, final String responseType, final StringBuilder auditTrailSb, final HttpServletRequest req, final HttpServletResponse resp) throws ServerApiException {
+        if (session == null || session.isNew()) {
+            throw new ServerApiException(ApiErrorCode.UNAUTHORIZED, _apiServer.getSerializedApiError(ApiErrorCode.UNAUTHORIZED.getHttpCode(),
+                    "Only authenticated saml users can request this API",
+                    params, responseType));
+        }
+
+        if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY)) {
+            throw new ServerApiException(ApiErrorCode.UNAUTHORIZED, _apiServer.getSerializedApiError(ApiErrorCode.UNAUTHORIZED.getHttpCode(),
+                    "Unauthorized session, please re-login",
+                    params, responseType));
+        }
+
+        final long currentUserId = (Long) session.getAttribute("userid");
+        final UserAccount currentUserAccount = _accountService.getUserAccountById(currentUserId);
+        if (currentUserAccount == null || currentUserAccount.getSource() != User.Source.SAML2) {
+            throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
+                    "Only authenticated saml users can request this API",
+                    params, responseType));
+        }
+
+        String userUuid = null;
+        String domainUuid = null;
+        if (params.containsKey(ApiConstants.USER_ID)) {
+            userUuid = ((String[])params.get(ApiConstants.USER_ID))[0];
+        }
+        if (params.containsKey(ApiConstants.DOMAIN_ID)) {
+            domainUuid = ((String[])params.get(ApiConstants.DOMAIN_ID))[0];
+        }
+
+        if (userUuid != null && domainUuid != null) {
+            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())
+                    || !nextUserAccount.getExternalEntity().equals(currentUserAccount.getExternalEntity())
+                    || (nextUserAccount.getDomainId() != domain.getId())
+                    || (nextUserAccount.getSource() != User.Source.SAML2)) {
+                throw new ServerApiException(ApiErrorCode.PARAM_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.PARAM_ERROR.getHttpCode(),
+                        "User account is not allowed to switch to the requested account",
+                        params, responseType));
+            }
+            try {
+                if (_apiServer.verifyUser(nextUserAccount.getId())) {
+                    final LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, nextUserAccount.getUsername(), nextUserAccount.getUsername() + nextUserAccount.getSource().toString(),
+                            nextUserAccount.getDomainId(), null, remoteAddress, params);
+                    SAMLUtils.setupSamlUserCookies(loginResponse, resp);
+                    resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
+                    return ApiResponseSerializer.toSerializedString(loginResponse, responseType);
+                }
+            } catch (final Exception ignored) {
+            }
+        } else {
+            List<UserAccountVO> switchableAccounts = _userAccountDao.getAllUsersByNameAndEntity(currentUserAccount.getUsername(), currentUserAccount.getExternalEntity());
+            if (switchableAccounts != null && switchableAccounts.size() > 0 && currentUserId != User.UID_SYSTEM) {
+                List<SamlUserAccountResponse> accountResponses = new ArrayList<SamlUserAccountResponse>();
+                for (UserAccountVO userAccount: switchableAccounts) {
+                    User user = _userDao.getUser(userAccount.getId());
+                    Domain domain = _domainService.getDomain(userAccount.getDomainId());
+                    SamlUserAccountResponse accountResponse = new SamlUserAccountResponse();
+                    accountResponse.setUserId(user.getUuid());
+                    accountResponse.setUserName(user.getUsername());
+                    accountResponse.setDomainId(domain.getUuid());
+                    accountResponse.setDomainName(domain.getName());
+                    accountResponse.setAccountName(userAccount.getAccountName());
+                    accountResponse.setIdpId(user.getExternalEntity());
+                    accountResponses.add(accountResponse);
+                }
+                ListResponse<SamlUserAccountResponse> response = new ListResponse<SamlUserAccountResponse>();
+                response.setResponses(accountResponses);
+                response.setResponseName(getCommandName());
+                return ApiResponseSerializer.toSerializedString(response, responseType);
+            }
+        }
+        throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
+                "Unable to switch to requested SAML account. Please make sure your user/account is enabled. Please contact your administrator.",
+                params, responseType));
+    }
+
+    @Override
+    public APIAuthenticationType getAPIType() {
+        return APIAuthenticationType.READONLY_API;
+    }
+
+    @Override
+    public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
+        for (PluggableAPIAuthenticator authManager: authenticators) {
+            if (authManager != null && authManager instanceof SAML2AuthManager) {
+                _samlAuthManager = (SAML2AuthManager) authManager;
+            }
+        }
+        if (_samlAuthManager == null) {
+            s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 listAndSwitchSamlAccount Cmd");
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a1a6bc7d/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java
index 43ecfc5..4771342 100644
--- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java
@@ -111,4 +111,4 @@ public class ListIdpsCmd extends BaseCmd implements APIAuthenticator {
             s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd");
         }
     }
-}
\ No newline at end of file
+}

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a1a6bc7d/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
index d4940ec..188eb6b 100644
--- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
@@ -17,13 +17,11 @@
 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;
 import com.cloud.user.UserAccountVO;
 import com.cloud.user.dao.UserAccountDao;
-import com.cloud.utils.HttpUtils;
 import com.cloud.utils.db.EntityManager;
 import org.apache.cloudstack.api.APICommand;
 import org.apache.cloudstack.api.ApiConstants;
@@ -64,14 +62,12 @@ import org.opensaml.xml.validation.ValidationException;
 import org.xml.sax.SAXException;
 
 import javax.inject.Inject;
-import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.stream.FactoryConfigurationError;
 import java.io.IOException;
-import java.net.URLEncoder;
 import java.util.List;
 import java.util.Map;
 
@@ -195,7 +191,6 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
                 }
 
                 String username = null;
-                Long domainId = null;
                 Issuer issuer = processedSAMLResponse.getIssuer();
                 SAMLProviderMetadata spMetadata = _samlAuthManager.getSPMetadata();
                 SAMLProviderMetadata idpMetadata = _samlAuthManager.getIdPMetadata(issuer.getValue());
@@ -204,9 +199,6 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
                 s_logger.debug("Received SAMLResponse in response to id=" + responseToId);
                 SAMLTokenVO token = _samlAuthManager.getToken(responseToId);
                 if (token != null) {
-                    if (token.getDomainId() != null) {
-                        domainId = token.getDomainId();
-                    }
                     if (!(token.getEntity().equalsIgnoreCase(issuer.getValue()))) {
                         throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
                                 "The SAML response contains Issuer Entity ID that is different from the original SAML request",
@@ -298,17 +290,9 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
                 UserAccount userAccount = null;
                 List<UserAccountVO> possibleUserAccounts = _userAccountDao.getAllUsersByNameAndEntity(username, issuer.getValue());
                 if (possibleUserAccounts != null && possibleUserAccounts.size() > 0) {
-                    if (possibleUserAccounts.size() == 1) {
-                        userAccount = possibleUserAccounts.get(0);
-                    } else if (possibleUserAccounts.size() > 1) {
-                        if (domainId != null) {
-                            userAccount = _userAccountDao.getUserAccount(username, domainId);
-                        } else {
-                            throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
-                                    "You have accounts in multiple domains, please re-login by specifying the domain you want to log into.",
-                                    params, responseType));
-                        }
-                    }
+                    // By default, log into the first user account
+                    // Users can switch to other allowed accounts later
+                    userAccount = possibleUserAccounts.get(0);
                 }
 
                 if (userAccount == null || userAccount.getExternalEntity() == null || !_samlAuthManager.isUserAuthorized(userAccount.getId(), issuer.getValue())) {
@@ -322,21 +306,11 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
                         if (_apiServer.verifyUser(userAccount.getId())) {
                             LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, userAccount.getUsername(), userAccount.getUsername() + userAccount.getSource().toString(),
                                     userAccount.getDomainId(), null, remoteAddress, params);
-                            resp.addCookie(new Cookie("userid", URLEncoder.encode(loginResponse.getUserId(), HttpUtils.UTF_8)));
-                            resp.addCookie(new Cookie("domainid", URLEncoder.encode(loginResponse.getDomainId(), HttpUtils.UTF_8)));
-                            resp.addCookie(new Cookie("role", URLEncoder.encode(loginResponse.getType(), HttpUtils.UTF_8)));
-                            resp.addCookie(new Cookie("username", URLEncoder.encode(loginResponse.getUsername(), HttpUtils.UTF_8)));
-                            resp.addCookie(new Cookie("account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8)));
-                            String timezone = loginResponse.getTimeZone();
-                            if (timezone != null) {
-                                resp.addCookie(new Cookie("timezone", URLEncoder.encode(timezone, HttpUtils.UTF_8)));
-                            }
-                            resp.addCookie(new Cookie("userfullname", URLEncoder.encode(loginResponse.getFirstName() + " " + loginResponse.getLastName(), HttpUtils.UTF_8).replace("+", "%20")));
-                            resp.addHeader("SET-COOKIE", String.format("%s=%s;HttpOnly", ApiConstants.SESSIONKEY, loginResponse.getSessionKey()));
+                            SAMLUtils.setupSamlUserCookies(loginResponse, resp);
                             resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
                             return ApiResponseSerializer.toSerializedString(loginResponse, responseType);
                         }
-                    } catch (final CloudAuthenticationException ignored) {
+                    } catch (final Exception ignored) {
                     }
                 }
             }

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a1a6bc7d/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlUserAccountResponse.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlUserAccountResponse.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlUserAccountResponse.java
new file mode 100644
index 0000000..f0927e3
--- /dev/null
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/response/SamlUserAccountResponse.java
@@ -0,0 +1,99 @@
+// 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 com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+
+public class SamlUserAccountResponse extends AuthenticationCmdResponse {
+    @SerializedName("userId")
+    @Param(description = "The User Id")
+    private String userId;
+
+    @SerializedName("domainId")
+    @Param(description = "The Domain Id")
+    private String domainId;
+
+    @SerializedName("userName")
+    @Param(description = "The User Name")
+    private String userName;
+
+    @SerializedName("accountName")
+    @Param(description = "The Account Name")
+    private String accountName;
+
+    @SerializedName("domainName")
+    @Param(description = "The Domain Name")
+    private String domainName;
+
+    @SerializedName("idpId")
+    @Param(description = "The IDP ID")
+    private String idpId;
+
+    public SamlUserAccountResponse() {
+        super();
+        setObjectName("samluseraccount");
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getDomainId() {
+        return domainId;
+    }
+
+    public void setDomainId(String domainId) {
+        this.domainId = domainId;
+    }
+
+    public String getUserName() {
+        return userName;
+    }
+
+    public void setUserName(String userName) {
+        this.userName = userName;
+    }
+
+    public String getAccountName() {
+        return accountName;
+    }
+
+    public void setAccountName(String accountName) {
+        this.accountName = accountName;
+    }
+
+    public String getDomainName() {
+        return domainName;
+    }
+
+    public void setDomainName(String domainName) {
+        this.domainName = domainName;
+    }
+
+    public String getIdpId() {
+        return idpId;
+    }
+
+    public void setIdpId(String idpId) {
+        this.idpId = idpId;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a1a6bc7d/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java
index 7232ac9..46b1c40 100644
--- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java
@@ -26,6 +26,7 @@ import com.cloud.utils.component.AdapterBase;
 import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
 import org.apache.cloudstack.api.command.AuthorizeSAMLSSOCmd;
 import org.apache.cloudstack.api.command.GetServiceProviderMetaDataCmd;
+import org.apache.cloudstack.api.command.ListAndSwitchSAMLAccountCmd;
 import org.apache.cloudstack.api.command.ListIdpsCmd;
 import org.apache.cloudstack.api.command.ListSamlAuthorizationCmd;
 import org.apache.cloudstack.api.command.SAML2LoginAPIAuthenticatorCmd;
@@ -491,6 +492,7 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage
         cmdList.add(SAML2LogoutAPIAuthenticatorCmd.class);
         cmdList.add(GetServiceProviderMetaDataCmd.class);
         cmdList.add(ListIdpsCmd.class);
+        cmdList.add(ListAndSwitchSAMLAccountCmd.class);
         return cmdList;
     }
 

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a1a6bc7d/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java
index 5c8a390..65a7959 100644
--- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java
@@ -23,18 +23,9 @@ import com.cloud.user.dao.UserDao;
 import com.cloud.utils.Pair;
 import org.apache.cxf.common.util.StringUtils;
 import org.apache.log4j.Logger;
-import org.opensaml.DefaultBootstrap;
-import org.opensaml.saml2.core.Response;
-import org.opensaml.saml2.core.StatusCode;
-import org.opensaml.xml.ConfigurationException;
-import org.opensaml.xml.io.UnmarshallingException;
-import org.xml.sax.SAXException;
 
 import javax.ejb.Local;
 import javax.inject.Inject;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.stream.FactoryConfigurationError;
-import java.io.IOException;
 import java.util.Map;
 
 @Local(value = {UserAuthenticator.class})
@@ -63,18 +54,7 @@ public class SAML2UserAuthenticator extends DefaultUserAuthenticator {
             return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
         } else {
             User user = _userDao.getUser(userAccount.getId());
-            if (user != null && requestParameters != null && requestParameters.containsKey(SAMLPluginConstants.SAML_RESPONSE)) {
-                final String samlResponse = ((String[])requestParameters.get(SAMLPluginConstants.SAML_RESPONSE))[0];
-                Response responseObject = null;
-                try {
-                    DefaultBootstrap.bootstrap();
-                    responseObject = SAMLUtils.decodeSAMLResponse(samlResponse);
-                } catch (ConfigurationException | FactoryConfigurationError | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) {
-                    return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
-                }
-                if (!responseObject.getStatus().getStatusCode().getValue().equals(StatusCode.SUCCESS_URI)) {
-                    return new Pair<Boolean, ActionOnFailedAuthentication>(false, null);
-                }
+            if (user != null && user.getSource() == User.Source.SAML2 && user.getExternalEntity() != null) {
                 return new Pair<Boolean, ActionOnFailedAuthentication>(true, null);
             }
         }

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a1a6bc7d/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java
index 0216ad7..fb2d960 100644
--- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAMLUtils.java
@@ -20,6 +20,8 @@
 package org.apache.cloudstack.saml;
 
 import com.cloud.utils.HttpUtils;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.response.LoginCmdResponse;
 import org.apache.log4j.Logger;
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.bouncycastle.x509.X509V1CertificateGenerator;
@@ -62,6 +64,8 @@ import org.w3c.dom.Element;
 import org.xml.sax.SAXException;
 
 import javax.security.auth.x500.X500Principal;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
@@ -351,4 +355,18 @@ public class SAMLUtils {
         return certGen.generate(keyPair.getPrivate(), "BC");
     }
 
+    public static void setupSamlUserCookies(final LoginCmdResponse loginResponse, final HttpServletResponse resp) throws IOException {
+        resp.addCookie(new Cookie("userid", URLEncoder.encode(loginResponse.getUserId(), HttpUtils.UTF_8)));
+        resp.addCookie(new Cookie("domainid", URLEncoder.encode(loginResponse.getDomainId(), HttpUtils.UTF_8)));
+        resp.addCookie(new Cookie("role", URLEncoder.encode(loginResponse.getType(), HttpUtils.UTF_8)));
+        resp.addCookie(new Cookie("username", URLEncoder.encode(loginResponse.getUsername(), HttpUtils.UTF_8)));
+        resp.addCookie(new Cookie("account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8)));
+        String timezone = loginResponse.getTimeZone();
+        if (timezone != null) {
+            resp.addCookie(new Cookie("timezone", URLEncoder.encode(timezone, HttpUtils.UTF_8)));
+        }
+        resp.addCookie(new Cookie("userfullname", URLEncoder.encode(loginResponse.getFirstName() + " " + loginResponse.getLastName(), HttpUtils.UTF_8).replace("+", "%20")));
+        resp.addHeader("SET-COOKIE", String.format("%s=%s;HttpOnly", ApiConstants.SESSIONKEY, loginResponse.getSessionKey()));
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a1a6bc7d/ui/css/cloudstack3.css
----------------------------------------------------------------------
diff --git a/ui/css/cloudstack3.css b/ui/css/cloudstack3.css
index ef33dea..06abbb8 100644
--- a/ui/css/cloudstack3.css
+++ b/ui/css/cloudstack3.css
@@ -9651,7 +9651,7 @@ div.container div.panel div#details-tab-addloadBalancer.detail-group div.loadBal
 }
 
 /*** View switcher (drop-down)*/
-.project-switcher {
+.project-switcher, .domain-switcher {
   float: left;
   width: 223px;
   padding: 9px 17px 0 19px;
@@ -9662,7 +9662,7 @@ div.container div.panel div#details-tab-addloadBalancer.detail-group div.loadBal
   border-radius: 4px;
 }
 
-.project-switcher label {
+.project-switcher label, .domain-switcher label {
   top: 29px;
   color: #FFFFFF;
   font-size: 13px;
@@ -9671,7 +9671,7 @@ div.container div.panel div#details-tab-addloadBalancer.detail-group div.loadBal
   margin-top: 5px;
 }
 
-.project-switcher select {
+.project-switcher select, .domain-switcher select {
   width: 70%;
   float: left;
   margin-top: 0px;

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a1a6bc7d/ui/index.jsp
----------------------------------------------------------------------
diff --git a/ui/index.jsp b/ui/index.jsp
index c3bae19..60f3cc3 100644
--- a/ui/index.jsp
+++ b/ui/index.jsp
@@ -75,13 +75,6 @@
                             </div>
                         </div>
 
-                        <div id="saml-login">
-                            <div class="field domain">
-                                <label for="saml-domain"><fmt:message key="label.domain"/></label>
-                                <input id="saml-domain" type="text" name="saml-domain" />
-                            </div>
-                        </div>
-
                         <div id="login-submit">
                             <!-- Submit (login) -->
                             <input id="login-submit" type="submit" value="<fmt:message key="label.login"/>" />
@@ -1805,6 +1798,7 @@
         <script type="text/javascript" src="scripts/docs.js?t=<%=now%>"></script>
         <script type="text/javascript" src="scripts/vm_snapshots.js?t=<%=now%>"></script>
         <script type="text/javascript" src="scripts/ui-custom/projectSelect.js?t=<%=now%>"></script>
+        <script type="text/javascript" src="scripts/ui-custom/saml.js?t=<%=now%>"></script>
 
         <!-- Plugin/module API -->
         <script type="text/javascript" src="scripts/ui-custom/pluginListing.js?t=<%=now%>"></script>

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a1a6bc7d/ui/scripts/ui-custom/login.js
----------------------------------------------------------------------
diff --git a/ui/scripts/ui-custom/login.js b/ui/scripts/ui-custom/login.js
index e112958..5825529 100644
--- a/ui/scripts/ui-custom/login.js
+++ b/ui/scripts/ui-custom/login.js
@@ -122,8 +122,7 @@
             } else if (selectedLogin === 'saml') {
                 // SAML
                 args.samlLoginAction({
-                    data: {'idpid': $login.find('#login-options').find(':selected').val(),
-                          'domain': $login.find('#saml-domain').val()}
+                    data: {'idpid': $login.find('#login-options').find(':selected').val()}
                 });
             }
             return false;
@@ -133,16 +132,13 @@
         var toggleLoginView = function (selectedOption) {
             $login.find('#login-submit').show();
             if (selectedOption === '') {
-                    $login.find('#saml-login').hide();
                     $login.find('#cloudstack-login').hide();
                     $login.find('#login-submit').hide();
                     selectedLogin = 'none';
             } else if (selectedOption === 'cloudstack-login') {
-                    $login.find('#saml-login').hide();
                     $login.find('#cloudstack-login').show();
                     selectedLogin = 'cloudstack';
             } else {
-                    $login.find('#saml-login').show();
                     $login.find('#cloudstack-login').hide();
                     selectedLogin = 'saml';
             }
@@ -160,14 +156,12 @@
         $login.find('#login-dropdown').hide();
         $login.find('#login-submit').show();
         $login.find('#cloudstack-login').show();
-        $login.find('#saml-login').hide();
 
         // If any IdP servers were set, SAML is enabled
         if (g_idpList && g_idpList.length > 0) {
             $login.find('#login-dropdown').show();
             $login.find('#login-submit').hide();
             $login.find('#cloudstack-login').hide();
-            $login.find('#saml-login').hide();
 
             $login.find('#login-options')
                 .append($('<option>', {

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a1a6bc7d/ui/scripts/ui-custom/saml.js
----------------------------------------------------------------------
diff --git a/ui/scripts/ui-custom/saml.js b/ui/scripts/ui-custom/saml.js
new file mode 100644
index 0000000..391e7f8
--- /dev/null
+++ b/ui/scripts/ui-custom/saml.js
@@ -0,0 +1,96 @@
+// 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.
+
+(function($, cloudStack) {
+    $(window).bind('cloudStack.ready', function() {
+        var showSamlDomainSwitcher = false;
+        if (g_idpList) {
+            showSamlDomainSwitcher = true;
+        }
+        if (!showSamlDomainSwitcher) {
+            return;
+        }
+
+        var $label = $('<label>').html('Domain:');
+        var $header = $('#header .controls');
+        var $domainSwitcher = $('<div>').addClass('domain-switcher');
+        var $domainSelect = $('<select>');
+        $domainSwitcher.append($label, $domainSelect);
+
+        var switchAccount = function(userId, domainId) {
+            var toReload = true;
+            $.ajax({
+                url: createURL('listAndSwitchSamlAccount'),
+                type: 'POST',
+                async: false,
+                data: {
+                    userid: userId,
+                    domainid: domainId
+                },
+                success: function(data, textStatus) {
+                    document.location.reload(true);
+                },
+                error: function(data) {
+                    cloudStack.dialog.notice({
+                        message: parseXMLHttpResponse(data)
+                    });
+                    if (data.status !== 200) {
+                        toReload = false;
+                    }
+                },
+                complete: function() {
+                    if (toReload) {
+                        document.location.reload(true);
+                    }
+                    toReload = true;
+                }
+            });
+        };
+
+        $domainSelect.change(function() {
+            var selectedOption = $domainSelect.val();
+            var userId = selectedOption.split('/')[0];
+            var domainId = selectedOption.split('/')[1];
+            switchAccount(userId, domainId);
+        });
+
+        $.ajax({
+            url: createURL('listAndSwitchSamlAccount'),
+            success: function(json) {
+                var accounts = json.listandswitchsamlaccountresponse.samluseraccount;
+                if (accounts.length < 2) {
+                    return;
+                };
+                $domainSelect.empty();
+                for (var i = 0; i < accounts.length; i++) {
+                    var option = $('<option>');
+                    option.data("userId", accounts[i].userId);
+                    option.data("domainId", accounts[i].domainId);
+                    option.val(accounts[i].userId + '/' + accounts[i].domainId);
+                    option.html(accounts[i].accountName + "/" + accounts[i].domainName);
+                    option.appendTo($domainSelect);
+                }
+                var currentAccountDomain = g_userid + '/' + g_domainid;
+                $domainSelect.find('option[value="' + currentAccountDomain + '"]').attr("selected", "selected");
+                $domainSwitcher.insertAfter($header.find('.region-switcher'));
+            },
+            error: function(data) {
+                // if call fails, the logged in user in not a SAML authenticated user
+            }
+        });
+    });
+}(jQuery, cloudStack));


[3/5] git commit: updated refs/heads/4.5-samlfixes to 38d6bae

Posted by bh...@apache.org.
CLOUDSTACK-8701: Add administrative contact block as per SAML IDP expectations

Signed-off-by: Rohit Yadav <ro...@shapeblue.com>


Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo
Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/3a8785b7
Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/3a8785b7
Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/3a8785b7

Branch: refs/heads/4.5-samlfixes
Commit: 3a8785b7c05a20b0b33bc506c5be743333a845a6
Parents: a1a6bc7
Author: Rohit Yadav <ro...@shapeblue.com>
Authored: Sun Aug 9 00:13:51 2015 +0530
Committer: Rohit Yadav <ro...@shapeblue.com>
Committed: Thu Aug 13 18:25:58 2015 +0530

----------------------------------------------------------------------
 .../api/command/GetServiceProviderMetaDataCmd.java    | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cloudstack/blob/3a8785b7/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java
index bed594f..80d38fd 100644
--- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java
@@ -205,16 +205,28 @@ public class GetServiceProviderMetaDataCmd extends BaseCmd implements APIAuthent
         spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS);
         spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor);
 
-        ContactPerson contactPerson = new ContactPersonBuilder().buildObject();
+        // Add technical contact
         GivenName givenName = new GivenNameBuilder().buildObject();
         givenName.setName(spMetadata.getContactPersonName());
         EmailAddress emailAddress = new EmailAddressBuilder().buildObject();
         emailAddress.setAddress(spMetadata.getContactPersonEmail());
+        ContactPerson contactPerson = new ContactPersonBuilder().buildObject();
         contactPerson.setType(ContactPersonTypeEnumeration.TECHNICAL);
         contactPerson.setGivenName(givenName);
         contactPerson.getEmailAddresses().add(emailAddress);
         spEntityDescriptor.getContactPersons().add(contactPerson);
 
+        // Add administrative/support contact
+        GivenName givenNameAdmin = new GivenNameBuilder().buildObject();
+        givenNameAdmin.setName(spMetadata.getContactPersonName());
+        EmailAddress emailAddressAdmin = new EmailAddressBuilder().buildObject();
+        emailAddressAdmin.setAddress(spMetadata.getContactPersonEmail());
+        ContactPerson contactPersonAdmin = new ContactPersonBuilder().buildObject();
+        contactPersonAdmin.setType(ContactPersonTypeEnumeration.ADMINISTRATIVE);
+        contactPersonAdmin.setGivenName(givenNameAdmin);
+        contactPersonAdmin.getEmailAddresses().add(emailAddressAdmin);
+        spEntityDescriptor.getContactPersons().add(contactPersonAdmin);
+
         Organization organization = new OrganizationBuilder().buildObject();
         OrganizationName organizationName = new OrganizationNameBuilder().buildObject();
         organizationName.setName(new LocalizedString(spMetadata.getOrganizationName(), Locale.getDefault().getLanguage()));


[4/5] git commit: updated refs/heads/4.5-samlfixes to 38d6bae

Posted by bh...@apache.org.
engine: fix accountState setter name

Signed-off-by: Rohit Yadav <ro...@shapeblue.com>


Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo
Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/220f35c8
Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/220f35c8
Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/220f35c8

Branch: refs/heads/4.5-samlfixes
Commit: 220f35c8bd8bb30ae0fd685dc1989996e13116ec
Parents: a675d02
Author: Rohit Yadav <ro...@shapeblue.com>
Authored: Wed Aug 12 14:03:17 2015 +0530
Committer: Rohit Yadav <ro...@shapeblue.com>
Committed: Thu Aug 13 18:25:58 2015 +0530

----------------------------------------------------------------------
 engine/schema/src/com/cloud/user/UserAccountVO.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cloudstack/blob/220f35c8/engine/schema/src/com/cloud/user/UserAccountVO.java
----------------------------------------------------------------------
diff --git a/engine/schema/src/com/cloud/user/UserAccountVO.java b/engine/schema/src/com/cloud/user/UserAccountVO.java
index 80ee873..5ce0eb7 100644
--- a/engine/schema/src/com/cloud/user/UserAccountVO.java
+++ b/engine/schema/src/com/cloud/user/UserAccountVO.java
@@ -251,7 +251,7 @@ public class UserAccountVO implements UserAccount, InternalIdentity {
         return accountState;
     }
 
-    public void setAccountDisabled(String accountState) {
+    public void setAccountState(String accountState) {
         this.accountState = accountState;
     }
 


[2/5] git commit: updated refs/heads/4.5-samlfixes to 38d6bae

Posted by bh...@apache.org.
utils: add HttpUtils test

Signed-off-by: Rohit Yadav <ro...@shapeblue.com>


Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo
Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/a675d027
Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/a675d027
Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/a675d027

Branch: refs/heads/4.5-samlfixes
Commit: a675d0272ed8734bfadcfd8dbf81ed4912783452
Parents: 3a8785b
Author: Rohit Yadav <ro...@shapeblue.com>
Authored: Wed Aug 12 14:02:57 2015 +0530
Committer: Rohit Yadav <ro...@shapeblue.com>
Committed: Thu Aug 13 18:25:58 2015 +0530

----------------------------------------------------------------------
 utils/src/com/cloud/utils/HttpUtils.java      | 32 +++++---
 utils/test/com/cloud/utils/HttpUtilsTest.java | 94 ++++++++++++++++++++++
 2 files changed, 113 insertions(+), 13 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a675d027/utils/src/com/cloud/utils/HttpUtils.java
----------------------------------------------------------------------
diff --git a/utils/src/com/cloud/utils/HttpUtils.java b/utils/src/com/cloud/utils/HttpUtils.java
index 379b117..d2e844a 100644
--- a/utils/src/com/cloud/utils/HttpUtils.java
+++ b/utils/src/com/cloud/utils/HttpUtils.java
@@ -52,18 +52,6 @@ public class HttpUtils {
         }
     }
 
-    public static String findCookie(final Cookie[] cookies, final String key) {
-        if (cookies == null || key == null || key.isEmpty()) {
-            return null;
-        }
-        for (Cookie cookie: cookies) {
-            if (cookie != null && cookie.getName().equals(key)) {
-                return cookie.getValue();
-            }
-        }
-        return null;
-    }
-
     public static void writeHttpResponse(final HttpServletResponse resp, final String response,
                                          final Integer responseCode, final String responseType, final String jsonContentType) {
         try {
@@ -92,10 +80,28 @@ public class HttpUtils {
         }
     }
 
+    public static String findCookie(final Cookie[] cookies, final String key) {
+        if (cookies == null || key == null || key.isEmpty()) {
+            return null;
+        }
+        for (Cookie cookie: cookies) {
+            if (cookie != null && cookie.getName().equals(key)) {
+                return cookie.getValue();
+            }
+        }
+        return null;
+    }
+
     public static boolean validateSessionKey(final HttpSession session, final Map<String, Object[]> params, final Cookie[] cookies, final String sessionKeyString) {
+        if (session == null || sessionKeyString == null) {
+            return false;
+        }
         final String sessionKey = (String) session.getAttribute(sessionKeyString);
         final String sessionKeyFromCookie = HttpUtils.findCookie(cookies, sessionKeyString);
-        final String[] sessionKeyFromParams = (String[]) params.get(sessionKeyString);
+        String[] sessionKeyFromParams = null;
+        if (params != null) {
+            sessionKeyFromParams = (String[]) params.get(sessionKeyString);
+        }
         if ((sessionKey == null)
                 || (sessionKeyFromParams == null && sessionKeyFromCookie == null)
                 || (sessionKeyFromParams != null && !sessionKey.equals(sessionKeyFromParams[0]))

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/a675d027/utils/test/com/cloud/utils/HttpUtilsTest.java
----------------------------------------------------------------------
diff --git a/utils/test/com/cloud/utils/HttpUtilsTest.java b/utils/test/com/cloud/utils/HttpUtilsTest.java
new file mode 100644
index 0000000..e10a5a3
--- /dev/null
+++ b/utils/test/com/cloud/utils/HttpUtilsTest.java
@@ -0,0 +1,94 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+//
+
+package com.cloud.utils;
+
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpSession;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpSession;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class HttpUtilsTest {
+
+    @Test
+    public void findCookieTest() {
+        Cookie[] cookies = null;
+        String cookieName = null;
+
+        // null test
+        assertNull(HttpUtils.findCookie(cookies, cookieName));
+        cookieName = "";
+        assertNull(HttpUtils.findCookie(cookies, cookieName));
+
+        // value test
+        cookieName = "daakuBandar";
+        cookies = new Cookie[]{new Cookie(cookieName, "someValue")};
+        assertNull(HttpUtils.findCookie(cookies, "aalasiLangur"));
+        assertNotNull(HttpUtils.findCookie(cookies, cookieName));
+    }
+
+    @Test
+    public void validateSessionKeyTest() {
+        HttpSession session = null;
+        Map<String, Object[]> params = null;
+        String sessionKeyString = null;
+        Cookie[] cookies = null;
+        final String sessionKeyValue = "randomUniqueSessionID";
+
+        // session and sessionKeyString null test
+        assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
+        sessionKeyString =  "sessionkey";
+        assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
+
+        // param and cookie null test
+        session = new MockHttpSession();
+        session.setAttribute(sessionKeyString, sessionKeyValue);
+        assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
+
+        // param null, cookies not null test
+        params = null;
+        cookies = new Cookie[]{new Cookie(sessionKeyString, sessionKeyValue)};
+        assertFalse(HttpUtils.validateSessionKey(session, params, cookies, "randomString"));
+        assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
+
+        // param not null, cookies null test
+        params = new HashMap<String, Object[]>();
+        params.put(sessionKeyString, new String[]{"randomString"});
+        cookies = null;
+        assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
+        params.put(sessionKeyString, new String[]{sessionKeyValue});
+        assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
+
+        // both param and cookies not null test
+        params = new HashMap<String, Object[]>();
+        cookies = new Cookie[]{new Cookie(sessionKeyString, sessionKeyValue)};
+        params.put(sessionKeyString, new String[]{"incorrectValue"});
+        assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
+        params.put(sessionKeyString, new String[]{sessionKeyValue});
+        assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
+    }
+}


[5/5] git commit: updated refs/heads/4.5-samlfixes to 38d6bae

Posted by bh...@apache.org.
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 <ro...@shapeblue.com>


Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo
Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/38d6baee
Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/38d6baee
Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/38d6baee

Branch: refs/heads/4.5-samlfixes
Commit: 38d6baee862a87e05c793fbc26f167469dca7aae
Parents: 220f35c
Author: Rohit Yadav <ro...@shapeblue.com>
Authored: Wed Aug 12 14:04:08 2015 +0530
Committer: Rohit Yadav <ro...@shapeblue.com>
Committed: Thu Aug 13 18:25:59 2015 +0530

----------------------------------------------------------------------
 .../command/ListAndSwitchSAMLAccountCmd.java    |  14 +-
 .../command/SAML2LoginAPIAuthenticatorCmd.java  |  31 +--
 .../ListAndSwitchSAMLAccountCmdTest.java        | 201 +++++++++++++++++++
 3 files changed, 231 insertions(+), 15 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cloudstack/blob/38d6baee/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmd.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmd.java
index db0d6dc..80aee1b 100644
--- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmd.java
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmd.java
@@ -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());

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/38d6baee/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
index 188eb6b..2c1b292 100644
--- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
+++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
@@ -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;
@@ -290,28 +291,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) {

http://git-wip-us.apache.org/repos/asf/cloudstack/blob/38d6baee/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmdTest.java
----------------------------------------------------------------------
diff --git a/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmdTest.java b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmdTest.java
new file mode 100644
index 0000000..1423243
--- /dev/null
+++ b/plugins/user-authenticators/saml2/test/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmdTest.java
@@ -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);
+    }
+
+}