Skip to content

Commit 1065661

Browse files
committed
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 <rohit.yadav@shapeblue.com>
1 parent cb7dd7b commit 1065661

13 files changed

Lines changed: 424 additions & 71 deletions

File tree

client/tomcatconf/commands.properties.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ getSPMetadata=15
2929
listIdps=15
3030
authorizeSamlSso=7
3131
listSamlAuthorization=7
32+
listAndSwitchSamlAccount=15
3233

3334
### Account commands
3435
createAccount=7

plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/GetServiceProviderMetaDataCmd.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
266266
}
267267
}
268268
if (_samlAuthManager == null) {
269-
s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd");
269+
s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 getSPMetadata Cmd");
270270
}
271271
}
272272
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.api.command;
18+
19+
import com.cloud.api.response.ApiResponseSerializer;
20+
import com.cloud.domain.Domain;
21+
import com.cloud.domain.dao.DomainDao;
22+
import com.cloud.user.Account;
23+
import com.cloud.user.User;
24+
import com.cloud.user.UserAccount;
25+
import com.cloud.user.UserAccountVO;
26+
import com.cloud.user.dao.UserAccountDao;
27+
import com.cloud.user.dao.UserDao;
28+
import com.cloud.utils.HttpUtils;
29+
import org.apache.cloudstack.api.APICommand;
30+
import org.apache.cloudstack.api.ApiConstants;
31+
import org.apache.cloudstack.api.ApiErrorCode;
32+
import org.apache.cloudstack.api.ApiServerService;
33+
import org.apache.cloudstack.api.BaseCmd;
34+
import org.apache.cloudstack.api.Parameter;
35+
import org.apache.cloudstack.api.ServerApiException;
36+
import org.apache.cloudstack.api.auth.APIAuthenticationType;
37+
import org.apache.cloudstack.api.auth.APIAuthenticator;
38+
import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator;
39+
import org.apache.cloudstack.api.response.DomainResponse;
40+
import org.apache.cloudstack.api.response.ListResponse;
41+
import org.apache.cloudstack.api.response.LoginCmdResponse;
42+
import org.apache.cloudstack.api.response.SamlUserAccountResponse;
43+
import org.apache.cloudstack.api.response.SuccessResponse;
44+
import org.apache.cloudstack.api.response.UserResponse;
45+
import org.apache.cloudstack.saml.SAML2AuthManager;
46+
import org.apache.cloudstack.saml.SAMLUtils;
47+
import org.apache.log4j.Logger;
48+
49+
import javax.inject.Inject;
50+
import javax.servlet.http.HttpServletRequest;
51+
import javax.servlet.http.HttpServletResponse;
52+
import javax.servlet.http.HttpSession;
53+
import java.util.ArrayList;
54+
import java.util.List;
55+
import java.util.Map;
56+
57+
@APICommand(name = "listAndSwitchSamlAccount", description = "Lists and switches to other SAML accounts owned by the SAML user", responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
58+
public class ListAndSwitchSAMLAccountCmd extends BaseCmd implements APIAuthenticator {
59+
public static final Logger s_logger = Logger.getLogger(ListAndSwitchSAMLAccountCmd.class.getName());
60+
private static final String s_name = "listandswitchsamlaccountresponse";
61+
62+
@Inject
63+
ApiServerService _apiServer;
64+
65+
@Inject
66+
private UserAccountDao _userAccountDao;
67+
@Inject
68+
private UserDao _userDao;
69+
@Inject
70+
private DomainDao _domainDao;
71+
72+
SAML2AuthManager _samlAuthManager;
73+
74+
/////////////////////////////////////////////////////
75+
//////////////// API parameters /////////////////////
76+
/////////////////////////////////////////////////////
77+
78+
@Parameter(name = ApiConstants.USER_ID, type = CommandType.UUID, entityType = UserResponse.class, required = false, description = "User uuid")
79+
private Long userId;
80+
81+
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, required = false, description = "Domain uuid")
82+
private Long domainId;
83+
84+
@Override
85+
public String getCommandName() {
86+
return s_name;
87+
}
88+
89+
@Override
90+
public long getEntityOwnerId() {
91+
return Account.ACCOUNT_ID_SYSTEM;
92+
}
93+
94+
@Override
95+
public void execute() {
96+
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication plugin api, cannot be used directly");
97+
}
98+
99+
@Override
100+
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 {
101+
if (session == null || session.isNew()) {
102+
throw new ServerApiException(ApiErrorCode.UNAUTHORIZED, _apiServer.getSerializedApiError(ApiErrorCode.UNAUTHORIZED.getHttpCode(),
103+
"Only authenticated saml users can request this API",
104+
params, responseType));
105+
}
106+
107+
if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY)) {
108+
throw new ServerApiException(ApiErrorCode.UNAUTHORIZED, _apiServer.getSerializedApiError(ApiErrorCode.UNAUTHORIZED.getHttpCode(),
109+
"Unauthorized session, please re-login",
110+
params, responseType));
111+
}
112+
113+
final long currentUserId = (Long) session.getAttribute("userid");
114+
final UserAccount currentUserAccount = _accountService.getUserAccountById(currentUserId);
115+
if (currentUserAccount == null || currentUserAccount.getSource() != User.Source.SAML2) {
116+
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
117+
"Only authenticated saml users can request this API",
118+
params, responseType));
119+
}
120+
121+
String userUuid = null;
122+
String domainUuid = null;
123+
if (params.containsKey(ApiConstants.USER_ID)) {
124+
userUuid = ((String[])params.get(ApiConstants.USER_ID))[0];
125+
}
126+
if (params.containsKey(ApiConstants.DOMAIN_ID)) {
127+
domainUuid = ((String[])params.get(ApiConstants.DOMAIN_ID))[0];
128+
}
129+
130+
if (userUuid != null && domainUuid != null) {
131+
final User user = _userDao.findByUuid(userUuid);
132+
final Domain domain = _domainDao.findByUuid(domainUuid);
133+
final UserAccount nextUserAccount = _accountService.getUserAccountById(user.getId());
134+
if (!nextUserAccount.getUsername().equals(currentUserAccount.getUsername())
135+
|| !nextUserAccount.getExternalEntity().equals(currentUserAccount.getExternalEntity())
136+
|| (nextUserAccount.getDomainId() != domain.getId())
137+
|| (nextUserAccount.getSource() != User.Source.SAML2)) {
138+
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.PARAM_ERROR.getHttpCode(),
139+
"User account is not allowed to switch to the requested account",
140+
params, responseType));
141+
}
142+
try {
143+
if (_apiServer.verifyUser(nextUserAccount.getId())) {
144+
final LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, nextUserAccount.getUsername(), nextUserAccount.getUsername() + nextUserAccount.getSource().toString(),
145+
nextUserAccount.getDomainId(), null, remoteAddress, params);
146+
SAMLUtils.setupSamlUserCookies(loginResponse, resp);
147+
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
148+
return ApiResponseSerializer.toSerializedString(loginResponse, responseType);
149+
}
150+
} catch (final Exception ignored) {
151+
}
152+
} else {
153+
List<UserAccountVO> switchableAccounts = _userAccountDao.getAllUsersByNameAndEntity(currentUserAccount.getUsername(), currentUserAccount.getExternalEntity());
154+
if (switchableAccounts != null && switchableAccounts.size() > 0 && currentUserId != User.UID_SYSTEM) {
155+
List<SamlUserAccountResponse> accountResponses = new ArrayList<SamlUserAccountResponse>();
156+
for (UserAccountVO userAccount: switchableAccounts) {
157+
User user = _userDao.getUser(userAccount.getId());
158+
Domain domain = _domainService.getDomain(userAccount.getDomainId());
159+
SamlUserAccountResponse accountResponse = new SamlUserAccountResponse();
160+
accountResponse.setUserId(user.getUuid());
161+
accountResponse.setUserName(user.getUsername());
162+
accountResponse.setDomainId(domain.getUuid());
163+
accountResponse.setDomainName(domain.getName());
164+
accountResponse.setAccountName(userAccount.getAccountName());
165+
accountResponse.setIdpId(user.getExternalEntity());
166+
accountResponses.add(accountResponse);
167+
}
168+
ListResponse<SamlUserAccountResponse> response = new ListResponse<SamlUserAccountResponse>();
169+
response.setResponses(accountResponses);
170+
response.setResponseName(getCommandName());
171+
return ApiResponseSerializer.toSerializedString(response, responseType);
172+
}
173+
}
174+
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
175+
"Unable to switch to requested SAML account. Please make sure your user/account is enabled. Please contact your administrator.",
176+
params, responseType));
177+
}
178+
179+
@Override
180+
public APIAuthenticationType getAPIType() {
181+
return APIAuthenticationType.READONLY_API;
182+
}
183+
184+
@Override
185+
public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
186+
for (PluggableAPIAuthenticator authManager: authenticators) {
187+
if (authManager != null && authManager instanceof SAML2AuthManager) {
188+
_samlAuthManager = (SAML2AuthManager) authManager;
189+
}
190+
}
191+
if (_samlAuthManager == null) {
192+
s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 listAndSwitchSamlAccount Cmd");
193+
}
194+
}
195+
}

plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/ListIdpsCmd.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,4 @@ public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) {
111111
s_logger.error("No suitable Pluggable Authentication Manager found for SAML2 Login Cmd");
112112
}
113113
}
114-
}
114+
}

plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,11 @@
1717
package org.apache.cloudstack.api.command;
1818

1919
import com.cloud.api.response.ApiResponseSerializer;
20-
import com.cloud.exception.CloudAuthenticationException;
2120
import com.cloud.user.Account;
2221
import com.cloud.user.DomainManager;
2322
import com.cloud.user.UserAccount;
2423
import com.cloud.user.UserAccountVO;
2524
import com.cloud.user.dao.UserAccountDao;
26-
import com.cloud.utils.HttpUtils;
2725
import com.cloud.utils.db.EntityManager;
2826
import org.apache.cloudstack.api.APICommand;
2927
import org.apache.cloudstack.api.ApiConstants;
@@ -64,14 +62,12 @@
6462
import org.xml.sax.SAXException;
6563

6664
import javax.inject.Inject;
67-
import javax.servlet.http.Cookie;
6865
import javax.servlet.http.HttpServletRequest;
6966
import javax.servlet.http.HttpServletResponse;
7067
import javax.servlet.http.HttpSession;
7168
import javax.xml.parsers.ParserConfigurationException;
7269
import javax.xml.stream.FactoryConfigurationError;
7370
import java.io.IOException;
74-
import java.net.URLEncoder;
7571
import java.util.List;
7672
import java.util.Map;
7773

@@ -195,7 +191,6 @@ public String authenticate(final String command, final Map<String, Object[]> par
195191
}
196192

197193
String username = null;
198-
Long domainId = null;
199194
Issuer issuer = processedSAMLResponse.getIssuer();
200195
SAMLProviderMetadata spMetadata = _samlAuthManager.getSPMetadata();
201196
SAMLProviderMetadata idpMetadata = _samlAuthManager.getIdPMetadata(issuer.getValue());
@@ -204,9 +199,6 @@ public String authenticate(final String command, final Map<String, Object[]> par
204199
s_logger.debug("Received SAMLResponse in response to id=" + responseToId);
205200
SAMLTokenVO token = _samlAuthManager.getToken(responseToId);
206201
if (token != null) {
207-
if (token.getDomainId() != null) {
208-
domainId = token.getDomainId();
209-
}
210202
if (!(token.getEntity().equalsIgnoreCase(issuer.getValue()))) {
211203
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
212204
"The SAML response contains Issuer Entity ID that is different from the original SAML request",
@@ -298,17 +290,9 @@ public String authenticate(final String command, final Map<String, Object[]> par
298290
UserAccount userAccount = null;
299291
List<UserAccountVO> possibleUserAccounts = _userAccountDao.getAllUsersByNameAndEntity(username, issuer.getValue());
300292
if (possibleUserAccounts != null && possibleUserAccounts.size() > 0) {
301-
if (possibleUserAccounts.size() == 1) {
302-
userAccount = possibleUserAccounts.get(0);
303-
} else if (possibleUserAccounts.size() > 1) {
304-
if (domainId != null) {
305-
userAccount = _userAccountDao.getUserAccount(username, domainId);
306-
} else {
307-
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(),
308-
"You have accounts in multiple domains, please re-login by specifying the domain you want to log into.",
309-
params, responseType));
310-
}
311-
}
293+
// By default, log into the first user account
294+
// Users can switch to other allowed accounts later
295+
userAccount = possibleUserAccounts.get(0);
312296
}
313297

314298
if (userAccount == null || userAccount.getExternalEntity() == null || !_samlAuthManager.isUserAuthorized(userAccount.getId(), issuer.getValue())) {
@@ -322,21 +306,11 @@ public String authenticate(final String command, final Map<String, Object[]> par
322306
if (_apiServer.verifyUser(userAccount.getId())) {
323307
LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, userAccount.getUsername(), userAccount.getUsername() + userAccount.getSource().toString(),
324308
userAccount.getDomainId(), null, remoteAddress, params);
325-
resp.addCookie(new Cookie("userid", URLEncoder.encode(loginResponse.getUserId(), HttpUtils.UTF_8)));
326-
resp.addCookie(new Cookie("domainid", URLEncoder.encode(loginResponse.getDomainId(), HttpUtils.UTF_8)));
327-
resp.addCookie(new Cookie("role", URLEncoder.encode(loginResponse.getType(), HttpUtils.UTF_8)));
328-
resp.addCookie(new Cookie("username", URLEncoder.encode(loginResponse.getUsername(), HttpUtils.UTF_8)));
329-
resp.addCookie(new Cookie("account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8)));
330-
String timezone = loginResponse.getTimeZone();
331-
if (timezone != null) {
332-
resp.addCookie(new Cookie("timezone", URLEncoder.encode(timezone, HttpUtils.UTF_8)));
333-
}
334-
resp.addCookie(new Cookie("userfullname", URLEncoder.encode(loginResponse.getFirstName() + " " + loginResponse.getLastName(), HttpUtils.UTF_8).replace("+", "%20")));
335-
resp.addHeader("SET-COOKIE", String.format("%s=%s;HttpOnly", ApiConstants.SESSIONKEY, loginResponse.getSessionKey()));
309+
SAMLUtils.setupSamlUserCookies(loginResponse, resp);
336310
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
337311
return ApiResponseSerializer.toSerializedString(loginResponse, responseType);
338312
}
339-
} catch (final CloudAuthenticationException ignored) {
313+
} catch (final Exception ignored) {
340314
}
341315
}
342316
}

0 commit comments

Comments
 (0)