4.19 fix saml account selector (#10311)

This commit is contained in:
Rene Glover 2025-04-14 05:59:43 -05:00 committed by GitHub
parent 99ea77dc83
commit f13cf597a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 214 additions and 64 deletions

View File

@ -133,10 +133,12 @@ public class ListAndSwitchSAMLAccountCmd extends BaseCmd implements APIAuthentic
} }
if (userUuid != null && domainUuid != null) { if (userUuid != null && domainUuid != null) {
s_logger.debug("User [" + currentUserAccount.getUsername() + "] is requesting to switch from user profile [" + currentUserAccount.getId() + "] to useraccount [" + userUuid + "] in domain [" + domainUuid + "]");
final User user = _userDao.findByUuid(userUuid); final User user = _userDao.findByUuid(userUuid);
final Domain domain = _domainDao.findByUuid(domainUuid); final Domain domain = _domainDao.findByUuid(domainUuid);
final UserAccount nextUserAccount = _accountService.getUserAccountById(user.getId()); final UserAccount nextUserAccount = _accountService.getUserAccountById(user.getId());
if (nextUserAccount != null && !nextUserAccount.getAccountState().equals(Account.State.ENABLED.toString())) { if (nextUserAccount != null && !nextUserAccount.getAccountState().equals(Account.State.ENABLED.toString())) {
s_logger.warn("User [" + currentUserAccount.getUsername() + "] is requesting to switch from user profile [" + currentUserId + "] to user profile [" + userUuid + "] in domain [" + domainUuid + "] but the associated target account [" + nextUserAccount.getAccountName() + "] is not enabled");
throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.PARAM_ERROR.getHttpCode(), 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.", "The requested user account is locked and cannot be switched to, please contact your administrator.",
params, responseType)); params, responseType));
@ -147,20 +149,26 @@ public class ListAndSwitchSAMLAccountCmd extends BaseCmd implements APIAuthentic
|| !nextUserAccount.getExternalEntity().equals(currentUserAccount.getExternalEntity()) || !nextUserAccount.getExternalEntity().equals(currentUserAccount.getExternalEntity())
|| (nextUserAccount.getDomainId() != domain.getId()) || (nextUserAccount.getDomainId() != domain.getId())
|| (nextUserAccount.getSource() != User.Source.SAML2)) { || (nextUserAccount.getSource() != User.Source.SAML2)) {
s_logger.warn("User [" + currentUserAccount.getUsername() + "] is requesting to switch from user profile [" + currentUserId + "] to user profile [" + userUuid + "] in domain [" + domainUuid + "] but the associated target account is not found or invalid");
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.PARAM_ERROR.getHttpCode(), throw new ServerApiException(ApiErrorCode.PARAM_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.PARAM_ERROR.getHttpCode(),
"User account is not allowed to switch to the requested account", "User account is not allowed to switch to the requested account",
params, responseType)); params, responseType));
} }
try { try {
if (_apiServer.verifyUser(nextUserAccount.getId())) { if (_apiServer.verifyUser(nextUserAccount.getId())) {
s_logger.info("User [" + currentUserAccount.getUsername() + "] user profile switch is accepted: from [" + currentUserId + "] to user profile [" + userUuid + "] in domain [" + domainUuid + "] with account [" + nextUserAccount.getAccountName() + "]");
// need to set a sessoin variable to inform the login function of the specific user to login as, rather than using email only (which could have multiple matches)
session.setAttribute("nextUserId", user.getId());
final LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, nextUserAccount.getUsername(), nextUserAccount.getUsername() + nextUserAccount.getSource().toString(), final LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, nextUserAccount.getUsername(), nextUserAccount.getUsername() + nextUserAccount.getSource().toString(),
nextUserAccount.getDomainId(), null, remoteAddress, params); nextUserAccount.getDomainId(), null, remoteAddress, params);
SAMLUtils.setupSamlUserCookies(loginResponse, resp); SAMLUtils.setupSamlUserCookies(loginResponse, resp);
resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value()); session.removeAttribute("nextUserId");
s_logger.debug("User [" + currentUserAccount.getUsername() + "] user profile switch cookies set: from [" + currentUserId + "] to user profile [" + userUuid + "] in domain [" + domainUuid + "] with account [" + nextUserAccount.getAccountName() + "]");
//resp.sendRedirect(SAML2AuthManager.SAMLCloudStackRedirectionUrl.value());
return ApiResponseSerializer.toSerializedString(loginResponse, responseType); return ApiResponseSerializer.toSerializedString(loginResponse, responseType);
} }
} catch (CloudAuthenticationException | IOException exception) { } catch (CloudAuthenticationException | IOException exception) {
s_logger.debug("Failed to switch to request SAML user account due to: " + exception.getMessage()); s_logger.debug("User [" + currentUserAccount.getUsername() + "] user profile switch cookies set FAILED: from [" + currentUserId + "] to user profile [" + userUuid + "] in domain [" + domainUuid + "] with account [" + nextUserAccount.getAccountName() + "]", exception);
} }
} else { } else {
List<UserAccountVO> switchableAccounts = _userAccountDao.getAllUsersByNameAndEntity(currentUserAccount.getUsername(), currentUserAccount.getExternalEntity()); List<UserAccountVO> switchableAccounts = _userAccountDao.getAllUsersByNameAndEntity(currentUserAccount.getUsername(), currentUserAccount.getExternalEntity());
@ -178,6 +186,9 @@ public class ListAndSwitchSAMLAccountCmd extends BaseCmd implements APIAuthentic
accountResponse.setAccountName(userAccount.getAccountName()); accountResponse.setAccountName(userAccount.getAccountName());
accountResponse.setIdpId(user.getExternalEntity()); accountResponse.setIdpId(user.getExternalEntity());
accountResponses.add(accountResponse); accountResponses.add(accountResponse);
if (s_logger.isDebugEnabled()) {
s_logger.debug("Returning available useraccount for [" + currentUserAccount.getUsername() + "]: UserUUID: [" + user.getUuid() + "], DomainUUID: [" + domain.getUuid() + "], Account: [" + userAccount.getAccountName() + "]");
}
} }
ListResponse<SamlUserAccountResponse> response = new ListResponse<SamlUserAccountResponse>(); ListResponse<SamlUserAccountResponse> response = new ListResponse<SamlUserAccountResponse>();
response.setResponses(accountResponses); response.setResponses(accountResponses);

View File

@ -192,7 +192,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
String authnId = SAMLUtils.generateSecureRandomId(); String authnId = SAMLUtils.generateSecureRandomId();
samlAuthManager.saveToken(authnId, domainPath, idpMetadata.getEntityId()); samlAuthManager.saveToken(authnId, domainPath, idpMetadata.getEntityId());
s_logger.debug("Sending SAMLRequest id=" + authnId); s_logger.debug("Sending SAMLRequest id=" + authnId);
String redirectUrl = SAMLUtils.buildAuthnRequestUrl(authnId, spMetadata, idpMetadata, SAML2AuthManager.SAMLSignatureAlgorithm.value()); String redirectUrl = SAMLUtils.buildAuthnRequestUrl(authnId, spMetadata, idpMetadata, SAML2AuthManager.SAMLSignatureAlgorithm.value(), SAML2AuthManager.SAMLRequirePasswordLogin.value());
resp.sendRedirect(redirectUrl); resp.sendRedirect(redirectUrl);
return ""; return "";
} if (params.containsKey("SAMLart")) { } if (params.containsKey("SAMLart")) {

View File

@ -79,6 +79,10 @@ public interface SAML2AuthManager extends PluggableAPIAuthenticator, PluggableSe
ConfigKey<String> SAMLUserSessionKeyPathAttribute = new ConfigKey<String>("Advanced", String.class, "saml2.user.sessionkey.path", "", ConfigKey<String> SAMLUserSessionKeyPathAttribute = new ConfigKey<String>("Advanced", String.class, "saml2.user.sessionkey.path", "",
"The Path attribute of sessionkey cookie when SAML users have logged in. If not set, it will be set to the path of SAML redirection URL (saml2.redirect.url).", true); "The Path attribute of sessionkey cookie when SAML users have logged in. If not set, it will be set to the path of SAML redirection URL (saml2.redirect.url).", true);
ConfigKey<Boolean> SAMLRequirePasswordLogin = new ConfigKey<Boolean>("Advanced", Boolean.class, "saml2.require.password", "true",
"When enabled SAML2 will validate that the SAML login was performed with a password. If disabled, other forms of authentication are allowed (two-factor, certificate, etc) on the SAML Authentication Provider", true);
SAMLProviderMetadata getSPMetadata(); SAMLProviderMetadata getSPMetadata();
SAMLProviderMetadata getIdPMetadata(String entityId); SAMLProviderMetadata getIdPMetadata(String entityId);
Collection<SAMLProviderMetadata> getAllIdPMetadata(); Collection<SAMLProviderMetadata> getAllIdPMetadata();

View File

@ -543,6 +543,6 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage
SAMLCloudStackRedirectionUrl, SAMLUserAttributeName, SAMLCloudStackRedirectionUrl, SAMLUserAttributeName,
SAMLIdentityProviderMetadataURL, SAMLDefaultIdentityProviderId, SAMLIdentityProviderMetadataURL, SAMLDefaultIdentityProviderId,
SAMLSignatureAlgorithm, SAMLAppendDomainSuffix, SAMLTimeout, SAMLCheckSignature, SAMLSignatureAlgorithm, SAMLAppendDomainSuffix, SAMLTimeout, SAMLCheckSignature,
SAMLForceAuthn, SAMLUserSessionKeyPathAttribute}; SAMLForceAuthn, SAMLUserSessionKeyPathAttribute, SAMLRequirePasswordLogin};
} }
} }

View File

@ -151,11 +151,11 @@ public class SAMLUtils {
return null; return null;
} }
public static String buildAuthnRequestUrl(final String authnId, final SAMLProviderMetadata spMetadata, final SAMLProviderMetadata idpMetadata, final String signatureAlgorithm) { public static String buildAuthnRequestUrl(final String authnId, final SAMLProviderMetadata spMetadata, final SAMLProviderMetadata idpMetadata, final String signatureAlgorithm, boolean requirePasswordAuthentication) {
String redirectUrl = ""; String redirectUrl = "";
try { try {
DefaultBootstrap.bootstrap(); DefaultBootstrap.bootstrap();
AuthnRequest authnRequest = SAMLUtils.buildAuthnRequestObject(authnId, spMetadata.getEntityId(), idpMetadata.getSsoUrl(), spMetadata.getSsoUrl()); AuthnRequest authnRequest = SAMLUtils.buildAuthnRequestObject(authnId, spMetadata.getEntityId(), idpMetadata.getSsoUrl(), spMetadata.getSsoUrl(), requirePasswordAuthentication);
PrivateKey privateKey = null; PrivateKey privateKey = null;
if (spMetadata.getKeyPair() != null) { if (spMetadata.getKeyPair() != null) {
privateKey = spMetadata.getKeyPair().getPrivate(); privateKey = spMetadata.getKeyPair().getPrivate();
@ -168,13 +168,36 @@ public class SAMLUtils {
return redirectUrl; return redirectUrl;
} }
public static AuthnRequest buildAuthnRequestObject(final String authnId, final String spId, final String idpUrl, final String consumerUrl) { public static AuthnRequest buildAuthnRequestObject(final String authnId, final String spId, final String idpUrl, final String consumerUrl, boolean requirePasswordAuthentication) {
// Issuer object // Issuer object
IssuerBuilder issuerBuilder = new IssuerBuilder(); IssuerBuilder issuerBuilder = new IssuerBuilder();
Issuer issuer = issuerBuilder.buildObject(); Issuer issuer = issuerBuilder.buildObject();
issuer.setValue(spId); issuer.setValue(spId);
// AuthnContextClass // Creation of AuthRequestObject
AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder();
AuthnRequest authnRequest = authRequestBuilder.buildObject();
// AuthnContextClass. When this is false, the authentication requirements are defered to the SAML IDP and its default or configured workflow
if (requirePasswordAuthentication) {
setRequestedAuthnContext(authnRequest, requirePasswordAuthentication);
}
authnRequest.setID(authnId);
authnRequest.setDestination(idpUrl);
authnRequest.setVersion(SAMLVersion.VERSION_20);
authnRequest.setForceAuthn(SAML2AuthManager.SAMLForceAuthn.value());
authnRequest.setIsPassive(false);
authnRequest.setIssueInstant(new DateTime());
authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
authnRequest.setAssertionConsumerServiceURL(consumerUrl);
authnRequest.setProviderName(spId);
authnRequest.setIssuer(issuer);
return authnRequest;
}
public static void setRequestedAuthnContext(AuthnRequest authnRequest, boolean requirePasswordAuthentication) {
AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder(); AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder();
AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject( AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject(
SAMLConstants.SAML20_NS, SAMLConstants.SAML20_NS,
@ -186,23 +209,7 @@ public class SAMLUtils {
RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject(); RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject();
requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT); requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef); requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef);
// Creation of AuthRequestObject
AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder();
AuthnRequest authnRequest = authRequestBuilder.buildObject();
authnRequest.setID(authnId);
authnRequest.setDestination(idpUrl);
authnRequest.setVersion(SAMLVersion.VERSION_20);
authnRequest.setForceAuthn(SAML2AuthManager.SAMLForceAuthn.value());
authnRequest.setIsPassive(false);
authnRequest.setIssueInstant(new DateTime());
authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
authnRequest.setAssertionConsumerServiceURL(consumerUrl);
authnRequest.setProviderName(spId);
authnRequest.setIssuer(issuer);
authnRequest.setRequestedAuthnContext(requestedAuthnContext); authnRequest.setRequestedAuthnContext(requestedAuthnContext);
return authnRequest;
} }
public static LogoutRequest buildLogoutRequest(String logoutUrl, String spId, String nameIdString) { public static LogoutRequest buildLogoutRequest(String logoutUrl, String spId, String nameIdString) {
@ -284,23 +291,6 @@ public class SAMLUtils {
} }
public static void setupSamlUserCookies(final LoginCmdResponse loginResponse, final HttpServletResponse resp) throws IOException { 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)));
resp.addCookie(new Cookie("isSAML", URLEncoder.encode("true", HttpUtils.UTF_8)));
resp.addCookie(new Cookie("twoFaEnabled", URLEncoder.encode(loginResponse.is2FAenabled(), HttpUtils.UTF_8)));
String providerFor2FA = loginResponse.getProviderFor2FA();
if (StringUtils.isNotEmpty(providerFor2FA)) {
resp.addCookie(new Cookie("twoFaProvider", URLEncoder.encode(loginResponse.getProviderFor2FA(), 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")));
String redirectUrl = SAML2AuthManager.SAMLCloudStackRedirectionUrl.value(); String redirectUrl = SAML2AuthManager.SAMLCloudStackRedirectionUrl.value();
String path = SAML2AuthManager.SAMLUserSessionKeyPathAttribute.value(); String path = SAML2AuthManager.SAMLUserSessionKeyPathAttribute.value();
String domain = null; String domain = null;
@ -316,6 +306,18 @@ public class SAMLUtils {
} catch (URISyntaxException ex) { } catch (URISyntaxException ex) {
throw new CloudRuntimeException("Invalid URI: " + redirectUrl); throw new CloudRuntimeException("Invalid URI: " + redirectUrl);
} }
addBaseCookies(loginResponse, resp, domain, path);
String providerFor2FA = loginResponse.getProviderFor2FA();
if (StringUtils.isNotEmpty(providerFor2FA)) {
resp.addCookie(newCookie(domain, path,"twoFaProvider", URLEncoder.encode(loginResponse.getProviderFor2FA(), HttpUtils.UTF_8)));
}
String timezone = loginResponse.getTimeZone();
if (timezone != null) {
resp.addCookie(newCookie(domain, path,"timezone", URLEncoder.encode(timezone, HttpUtils.UTF_8)));
}
String sameSite = ApiServlet.getApiSessionKeySameSite(); String sameSite = ApiServlet.getApiSessionKeySameSite();
String sessionKeyCookie = String.format("%s=%s;Domain=%s;Path=%s;%s", ApiConstants.SESSIONKEY, loginResponse.getSessionKey(), domain, path, sameSite); String sessionKeyCookie = String.format("%s=%s;Domain=%s;Path=%s;%s", ApiConstants.SESSIONKEY, loginResponse.getSessionKey(), domain, path, sameSite);
s_logger.debug("Adding sessionkey cookie to response: " + sessionKeyCookie); s_logger.debug("Adding sessionkey cookie to response: " + sessionKeyCookie);
@ -323,6 +325,24 @@ public class SAMLUtils {
resp.addHeader("SET-COOKIE", String.format("%s=%s;HttpOnly;Path=/client/api;%s", ApiConstants.SESSIONKEY, loginResponse.getSessionKey(), sameSite)); resp.addHeader("SET-COOKIE", String.format("%s=%s;HttpOnly;Path=/client/api;%s", ApiConstants.SESSIONKEY, loginResponse.getSessionKey(), sameSite));
} }
private static void addBaseCookies(final LoginCmdResponse loginResponse, final HttpServletResponse resp, String domain, String path) throws IOException {
resp.addCookie(newCookie(domain, path, "userid", URLEncoder.encode(loginResponse.getUserId(), HttpUtils.UTF_8)));
resp.addCookie(newCookie(domain, path,"domainid", URLEncoder.encode(loginResponse.getDomainId(), HttpUtils.UTF_8)));
resp.addCookie(newCookie(domain, path,"role", URLEncoder.encode(loginResponse.getType(), HttpUtils.UTF_8)));
resp.addCookie(newCookie(domain, path,"username", URLEncoder.encode(loginResponse.getUsername(), HttpUtils.UTF_8)));
resp.addCookie(newCookie(domain, path,"account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8)));
resp.addCookie(newCookie(domain, path,"isSAML", URLEncoder.encode("true", HttpUtils.UTF_8)));
resp.addCookie(newCookie(domain, path,"twoFaEnabled", URLEncoder.encode(loginResponse.is2FAenabled(), HttpUtils.UTF_8)));
resp.addCookie(newCookie(domain, path,"userfullname", URLEncoder.encode(loginResponse.getFirstName() + " " + loginResponse.getLastName(), HttpUtils.UTF_8).replace("+", "%20")));
}
private static Cookie newCookie(final String domain, final String path, final String name, final String value) {
Cookie cookie = new Cookie(name, value);
cookie.setDomain(domain);
cookie.setPath(path);
return cookie;
}
/** /**
* Returns base64 encoded PublicKey * Returns base64 encoded PublicKey
* @param key PublicKey * @param key PublicKey

View File

@ -58,7 +58,7 @@ public class SAMLUtilsTest extends TestCase {
String idpUrl = "http://idp.domain.example"; String idpUrl = "http://idp.domain.example";
String spId = "cloudstack"; String spId = "cloudstack";
String authnId = SAMLUtils.generateSecureRandomId(); String authnId = SAMLUtils.generateSecureRandomId();
AuthnRequest req = SAMLUtils.buildAuthnRequestObject(authnId, spId, idpUrl, consumerUrl); AuthnRequest req = SAMLUtils.buildAuthnRequestObject(authnId, spId, idpUrl, consumerUrl, true);
assertEquals(req.getAssertionConsumerServiceURL(), consumerUrl); assertEquals(req.getAssertionConsumerServiceURL(), consumerUrl);
assertEquals(req.getDestination(), idpUrl); assertEquals(req.getDestination(), idpUrl);
assertEquals(req.getIssuer().getValue(), spId); assertEquals(req.getIssuer().getValue(), spId);
@ -86,7 +86,7 @@ public class SAMLUtilsTest extends TestCase {
idpMetadata.setSsoUrl(idpUrl); idpMetadata.setSsoUrl(idpUrl);
idpMetadata.setEntityId(idpId); idpMetadata.setEntityId(idpId);
URI redirectUrl = new URI(SAMLUtils.buildAuthnRequestUrl(authnId, spMetadata, idpMetadata, SAML2AuthManager.SAMLSignatureAlgorithm.value())); URI redirectUrl = new URI(SAMLUtils.buildAuthnRequestUrl(authnId, spMetadata, idpMetadata, SAML2AuthManager.SAMLSignatureAlgorithm.value(), true));
assertThat(redirectUrl).hasScheme(urlScheme).hasHost(idpDomain).hasParameter("SAMLRequest"); assertThat(redirectUrl).hasScheme(urlScheme).hasHost(idpDomain).hasParameter("SAMLRequest");
assertEquals(urlScheme, redirectUrl.getScheme()); assertEquals(urlScheme, redirectUrl.getScheme());
assertEquals(idpDomain, redirectUrl.getHost()); assertEquals(idpDomain, redirectUrl.getHost());
@ -115,7 +115,7 @@ public class SAMLUtilsTest extends TestCase {
idpMetadata.setSsoUrl(idpUrl); idpMetadata.setSsoUrl(idpUrl);
idpMetadata.setEntityId(idpId); idpMetadata.setEntityId(idpId);
URI redirectUrl = new URI(SAMLUtils.buildAuthnRequestUrl(authnId, spMetadata, idpMetadata, SAML2AuthManager.SAMLSignatureAlgorithm.value())); URI redirectUrl = new URI(SAMLUtils.buildAuthnRequestUrl(authnId, spMetadata, idpMetadata, SAML2AuthManager.SAMLSignatureAlgorithm.value(), true));
assertThat(redirectUrl).hasScheme(urlScheme).hasHost(idpDomain).hasParameter("idpid").hasParameter("SAMLRequest"); assertThat(redirectUrl).hasScheme(urlScheme).hasHost(idpDomain).hasParameter("idpid").hasParameter("SAMLRequest");
assertEquals(urlScheme, redirectUrl.getScheme()); assertEquals(urlScheme, redirectUrl.getScheme());
assertEquals(idpDomain, redirectUrl.getHost()); assertEquals(idpDomain, redirectUrl.getHost());

View File

@ -213,7 +213,6 @@ public class ListAndSwitchSAMLAccountCmdTest extends TestCase {
loginCmdResponse.set2FAenabled("false"); loginCmdResponse.set2FAenabled("false");
Mockito.when(apiServer.loginUser(nullable(HttpSession.class), nullable(String.class), nullable(String.class), Mockito.when(apiServer.loginUser(nullable(HttpSession.class), nullable(String.class), nullable(String.class),
nullable(Long.class), nullable(String.class), nullable(InetAddress.class), nullable(Map.class))).thenReturn(loginCmdResponse); nullable(Long.class), nullable(String.class), nullable(InetAddress.class), nullable(Map.class))).thenReturn(loginCmdResponse);
Mockito.doNothing().when(resp).sendRedirect(nullable(String.class));
try { try {
cmd.authenticate("command", params, session, null, HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp); cmd.authenticate("command", params, session, null, HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
} catch (ServerApiException exception) { } catch (ServerApiException exception) {
@ -221,7 +220,6 @@ public class ListAndSwitchSAMLAccountCmdTest extends TestCase {
} finally { } finally {
// accountService should have been called 4 times by now, for this case twice and 2 for cases above // 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(accountService, Mockito.times(4)).getUserAccountById(Mockito.anyLong());
Mockito.verify(resp, Mockito.times(1)).sendRedirect(anyString());
} }
} }

View File

@ -1159,7 +1159,14 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
domainId = userDomain.getId(); domainId = userDomain.getId();
} }
UserAccount userAcct = accountMgr.authenticateUser(username, password, domainId, loginIpAddress, requestParameters); Long userId = (Long)session.getAttribute("nextUserId");
UserAccount userAcct = null;
if (userId != null) {
userAcct = accountMgr.getUserAccountById(userId);
} else {
userAcct = accountMgr.authenticateUser(username, password, domainId, loginIpAddress, requestParameters);
}
if (userAcct != null) { if (userAcct != null) {
final String timezone = userAcct.getTimezone(); final String timezone = userAcct.getTimezone();
float offsetInHrs = 0f; float offsetInHrs = 0f;

View File

@ -372,6 +372,14 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
"totp", "totp",
"The default user two factor authentication provider. Eg. totp, staticpin", true, ConfigKey.Scope.Domain); "The default user two factor authentication provider. Eg. totp, staticpin", true, ConfigKey.Scope.Domain);
static ConfigKey<Boolean> userAllowMultipleAccounts = new ConfigKey<>("Advanced",
Boolean.class,
"user.allow.multiple.accounts",
"false",
"Determines if the same username can be added to more than one account in the same domain (SAML-only).",
true,
ConfigKey.Scope.Domain);
protected AccountManagerImpl() { protected AccountManagerImpl() {
super(); super();
} }
@ -1252,8 +1260,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
// Check permissions // Check permissions
checkAccess(getCurrentCallingAccount(), domain); checkAccess(getCurrentCallingAccount(), domain);
if (!_userAccountDao.validateUsernameInDomain(userName, domainId)) { if (!userAllowMultipleAccounts.valueInDomain(domainId) && !_userAccountDao.validateUsernameInDomain(userName, domainId)) {
throw new InvalidParameterValueException("The user " + userName + " already exists in domain " + domainId); throw new CloudRuntimeException("The user " + userName + " already exists in domain " + domainId);
} }
if (networkDomain != null && networkDomain.length() > 0) { if (networkDomain != null && networkDomain.length() > 0) {
@ -1436,9 +1444,16 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
throw new PermissionDeniedException("Account id : " + account.getId() + " is a system account, can't add a user to it"); throw new PermissionDeniedException("Account id : " + account.getId() + " is a system account, can't add a user to it");
} }
if (!_userAccountDao.validateUsernameInDomain(userName, domainId)) { if (!userAllowMultipleAccounts.valueInDomain(domainId) && !_userAccountDao.validateUsernameInDomain(userName, domainId)) {
throw new CloudRuntimeException("The user " + userName + " already exists in domain " + domainId); throw new CloudRuntimeException("The user " + userName + " already exists in domain " + domainId);
} }
List<UserVO> duplicatedUsers = _userDao.findUsersByName(userName);
for (UserVO duplicatedUser : duplicatedUsers) {
// users can't exist in same account
assertUserNotAlreadyInAccount(duplicatedUser, account);
}
UserVO user = null; UserVO user = null;
user = createUser(account.getId(), userName, password, firstName, lastName, email, timeZone, userUUID, source); user = createUser(account.getId(), userName, password, firstName, lastName, email, timeZone, userUUID, source);
return user; return user;
@ -1564,7 +1579,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
* <li> The username must be unique in each domain. Therefore, if there is already another user with the same username, an {@link InvalidParameterValueException} is thrown. * <li> The username must be unique in each domain. Therefore, if there is already another user with the same username, an {@link InvalidParameterValueException} is thrown.
* </ul> * </ul>
*/ */
protected void validateAndUpdateUsernameIfNeeded(UpdateUserCmd updateUserCmd, UserVO user, Account account) { protected void validateAndUpdateUsernameIfNeeded(UpdateUserCmd updateUserCmd, UserVO newUser, Account newAccount) {
String userName = updateUserCmd.getUsername(); String userName = updateUserCmd.getUsername();
if (userName == null) { if (userName == null) {
return; return;
@ -1572,18 +1587,21 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
if (StringUtils.isBlank(userName)) { if (StringUtils.isBlank(userName)) {
throw new InvalidParameterValueException("Username cannot be empty."); throw new InvalidParameterValueException("Username cannot be empty.");
} }
List<UserVO> duplicatedUsers = _userDao.findUsersByName(userName); List<UserVO> existingUsers = _userDao.findUsersByName(userName);
for (UserVO duplicatedUser : duplicatedUsers) { for (UserVO existingUser : existingUsers) {
if (duplicatedUser.getId() == user.getId()) { if (existingUser.getId() == newUser.getId()) {
continue; continue;
} }
Account duplicatedUserAccountWithUserThatHasTheSameUserName = _accountDao.findById(duplicatedUser.getAccountId());
if (duplicatedUserAccountWithUserThatHasTheSameUserName.getDomainId() == account.getDomainId()) { // duplicate usernames cannot exist in same domain unless explicitly configured
DomainVO domain = _domainDao.findById(duplicatedUserAccountWithUserThatHasTheSameUserName.getDomainId()); if (!userAllowMultipleAccounts.valueInDomain(newAccount.getDomainId())) {
throw new InvalidParameterValueException(String.format("Username [%s] already exists in domain [id=%s,name=%s]", duplicatedUser.getUsername(), domain.getUuid(), domain.getName())); assertUserNotAlreadyInDomain(existingUser, newAccount);
} }
// can't rename a username to an existing one in the same account
assertUserNotAlreadyInAccount(existingUser, newAccount);
} }
user.setUsername(userName); newUser.setUsername(userName);
} }
/** /**
@ -1820,7 +1838,7 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
// make sure the account is enabled too // make sure the account is enabled too
// if the user is either locked already or disabled already, don't change state...only lock currently enabled // if the user is either locked already or disabled already, don't change state...only lock currently enabled
// users // users
boolean success = true; boolean success = true;
if (user.getState().equals(State.LOCKED)) { if (user.getState().equals(State.LOCKED)) {
// already locked...no-op // already locked...no-op
@ -3317,7 +3335,8 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
@Override @Override
public ConfigKey<?>[] getConfigKeys() { public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[] {UseSecretKeyInResponse, enableUserTwoFactorAuthentication, return new ConfigKey<?>[] {UseSecretKeyInResponse, enableUserTwoFactorAuthentication,
userTwoFactorAuthenticationDefaultProvider, mandateUserTwoFactorAuthentication, userTwoFactorAuthenticationIssuer}; userTwoFactorAuthenticationDefaultProvider, mandateUserTwoFactorAuthentication, userTwoFactorAuthenticationIssuer,
userAllowMultipleAccounts};
} }
public List<UserTwoFactorAuthenticator> getUserTwoFactorAuthenticationProviders() { public List<UserTwoFactorAuthenticator> getUserTwoFactorAuthenticationProviders() {
@ -3502,4 +3521,21 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
return userAccountVO; return userAccountVO;
}); });
} }
void assertUserNotAlreadyInAccount(User existingUser, Account newAccount) {
System.out.println(existingUser.getAccountId());
System.out.println(newAccount.getId());
if (existingUser.getAccountId() == newAccount.getId()) {
AccountVO existingAccount = _accountDao.findById(newAccount.getId());
throw new InvalidParameterValueException(String.format("Username [%s] already exists in account [id=%s,name=%s]", existingUser.getUsername(), existingAccount.getUuid(), existingAccount.getAccountName()));
}
}
void assertUserNotAlreadyInDomain(User existingUser, Account originalAccount) {
Account existingAccount = _accountDao.findById(existingUser.getAccountId());
if (existingAccount.getDomainId() == originalAccount.getDomainId()) {
DomainVO existingDomain = _domainDao.findById(existingAccount.getDomainId());
throw new InvalidParameterValueException(String.format("Username [%s] already exists in domain [id=%s,name=%s] user account [id=%s,name=%s]", existingUser.getUsername(), existingDomain.getUuid(), existingDomain.getName(), existingAccount.getUuid(), existingAccount.getAccountName()));
}
}
} }

View File

@ -1270,4 +1270,75 @@ public class AccountManagerImplTest extends AccountManagetImplTestBase {
Assert.assertNull(updatedUser.getUser2faProvider()); Assert.assertNull(updatedUser.getUser2faProvider());
Assert.assertNull(updatedUser.getKeyFor2fa()); Assert.assertNull(updatedUser.getKeyFor2fa());
} }
@Test(expected = InvalidParameterValueException.class)
public void testAssertUserNotAlreadyInAccount_UserExistsInAccount() {
User existingUser = new UserVO();
existingUser.setUsername("testuser");
existingUser.setAccountId(1L);
Account newAccount = Mockito.mock(Account.class);
Mockito.when(newAccount.getId()).thenReturn(1L);
AccountVO existingAccount = Mockito.mock(AccountVO.class);
Mockito.when(existingAccount.getUuid()).thenReturn("existing-account-uuid");
Mockito.when(existingAccount.getAccountName()).thenReturn("existing-account");
Mockito.when(_accountDao.findById(1L)).thenReturn(existingAccount);
accountManagerImpl.assertUserNotAlreadyInAccount(existingUser, newAccount);
}
@Test
public void testAssertUserNotAlreadyInAccount_UserExistsInDiffAccount() {
User existingUser = new UserVO();
existingUser.setUsername("testuser");
existingUser.setAccountId(2L);
Account newAccount = Mockito.mock(Account.class);
Mockito.when(newAccount.getId()).thenReturn(1L);
accountManagerImpl.assertUserNotAlreadyInAccount(existingUser, newAccount);
}
@Test(expected = InvalidParameterValueException.class)
public void testAssertUserNotAlreadyInDomain_UserExistsInDomain() {
User existingUser = new UserVO();
existingUser.setUsername("testuser");
existingUser.setAccountId(1L);
Account originalAccount = Mockito.mock(Account.class);
Mockito.when(originalAccount.getDomainId()).thenReturn(1L);
AccountVO existingAccount = Mockito.mock(AccountVO.class);
Mockito.when(existingAccount.getDomainId()).thenReturn(1L);
Mockito.when(existingAccount.getUuid()).thenReturn("existing-account-uuid");
Mockito.when(existingAccount.getAccountName()).thenReturn("existing-account");
DomainVO existingDomain = Mockito.mock(DomainVO.class);
Mockito.when(existingDomain.getUuid()).thenReturn("existing-domain-uuid");
Mockito.when(existingDomain.getName()).thenReturn("existing-domain");
Mockito.when(_accountDao.findById(1L)).thenReturn(existingAccount);
Mockito.when(_domainDao.findById(1L)).thenReturn(existingDomain);
accountManagerImpl.assertUserNotAlreadyInDomain(existingUser, originalAccount);
}
@Test
public void testAssertUserNotAlreadyInDomain_UserExistsInDiffDomain() {
User existingUser = new UserVO();
existingUser.setUsername("testuser");
existingUser.setAccountId(1L);
Account originalAccount = Mockito.mock(Account.class);
Mockito.when(originalAccount.getDomainId()).thenReturn(1L);
AccountVO existingAccount = Mockito.mock(AccountVO.class);
Mockito.when(existingAccount.getDomainId()).thenReturn(2L);
Mockito.when(_accountDao.findById(1L)).thenReturn(existingAccount);
accountManagerImpl.assertUserNotAlreadyInDomain(existingUser, originalAccount);
}
} }

View File

@ -88,6 +88,7 @@ export default {
this.showSwitcher = false this.showSwitcher = false
return return
} }
this.samlAccounts = samlAccounts
this.samlAccounts = _.orderBy(samlAccounts, ['domainPath'], ['asc']) this.samlAccounts = _.orderBy(samlAccounts, ['domainPath'], ['asc'])
const currentAccount = this.samlAccounts.filter(x => { const currentAccount = this.samlAccounts.filter(x => {
return x.userId === store.getters.userInfo.id return x.userId === store.getters.userInfo.id
@ -109,6 +110,8 @@ export default {
this.$message.success(`Switched to "${account.accountName} (${account.domainPath})"`) this.$message.success(`Switched to "${account.accountName} (${account.domainPath})"`)
this.$router.go() this.$router.go()
}) })
}).else(error => {
console.log('error refreshing with new user context: ' + error)
}) })
} }
} }

View File

@ -290,7 +290,7 @@ const user = {
commit('SET_CUSTOM_COLUMNS', cachedCustomColumns) commit('SET_CUSTOM_COLUMNS', cachedCustomColumns)
// Ensuring we get the user info so that store.getters.user is never empty when the page is freshly loaded // Ensuring we get the user info so that store.getters.user is never empty when the page is freshly loaded
api('listUsers', { username: Cookies.get('username'), listall: true }).then(response => { api('listUsers', { id: Cookies.get('userid'), listall: true }).then(response => {
const result = response.listusersresponse.user[0] const result = response.listusersresponse.user[0]
commit('SET_INFO', result) commit('SET_INFO', result)
commit('SET_NAME', result.firstname + ' ' + result.lastname) commit('SET_NAME', result.firstname + ' ' + result.lastname)
@ -331,7 +331,7 @@ const user = {
}) })
} }
api('listUsers', { username: Cookies.get('username') }).then(response => { api('listUsers', { id: Cookies.get('userid') }).then(response => {
const result = response.listusersresponse.user[0] const result = response.listusersresponse.user[0]
commit('SET_INFO', result) commit('SET_INFO', result)
commit('SET_NAME', result.firstname + ' ' + result.lastname) commit('SET_NAME', result.firstname + ' ' + result.lastname)

View File

@ -143,7 +143,7 @@ const vueConfig = {
ws: false, ws: false,
changeOrigin: true, changeOrigin: true,
proxyTimeout: 10 * 60 * 1000, // 10 minutes proxyTimeout: 10 * 60 * 1000, // 10 minutes
cookieDomainRewrite: '*', cookieDomainRewrite: process.env.CS_COOKIE_HOST || 'localhost',
cookiePathRewrite: { cookiePathRewrite: {
'/client': '/' '/client': '/'
} }