This is an automated email from the ASF dual-hosted git repository.
dahn pushed a commit to branch 4.19
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/4.19 by this push:
new f13cf597a2e 4.19 fix saml account selector (#10311)
f13cf597a2e is described below
commit f13cf597a2e00054033ab5851db628680a64f8dc
Author: Rene Glover <[email protected]>
AuthorDate: Mon Apr 14 05:59:43 2025 -0500
4.19 fix saml account selector (#10311)
---
.../api/command/ListAndSwitchSAMLAccountCmd.java | 15 +++-
.../api/command/SAML2LoginAPIAuthenticatorCmd.java | 2 +-
.../apache/cloudstack/saml/SAML2AuthManager.java | 4 +
.../cloudstack/saml/SAML2AuthManagerImpl.java | 2 +-
.../java/org/apache/cloudstack/saml/SAMLUtils.java | 88 +++++++++++++---------
.../java/org/apache/cloudstack/SAMLUtilsTest.java | 6 +-
.../command/ListAndSwitchSAMLAccountCmdTest.java | 2 -
server/src/main/java/com/cloud/api/ApiServer.java | 9 ++-
.../java/com/cloud/user/AccountManagerImpl.java | 64 ++++++++++++----
.../com/cloud/user/AccountManagerImplTest.java | 71 +++++++++++++++++
ui/src/components/header/SamlDomainSwitcher.vue | 3 +
ui/src/store/modules/user.js | 4 +-
ui/vue.config.js | 2 +-
13 files changed, 211 insertions(+), 61 deletions(-)
diff --git
a/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmd.java
b/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmd.java
index c2f81cd3356..040d5414f26 100644
---
a/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmd.java
+++
b/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmd.java
@@ -133,10 +133,12 @@ public class ListAndSwitchSAMLAccountCmd extends BaseCmd
implements APIAuthentic
}
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 Domain domain = _domainDao.findByUuid(domainUuid);
final UserAccount nextUserAccount =
_accountService.getUserAccountById(user.getId());
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(),
"The requested user account is locked and cannot be
switched to, please contact your administrator.",
params, responseType));
@@ -147,20 +149,26 @@ public class ListAndSwitchSAMLAccountCmd extends BaseCmd
implements APIAuthentic
||
!nextUserAccount.getExternalEntity().equals(currentUserAccount.getExternalEntity())
|| (nextUserAccount.getDomainId() != domain.getId())
|| (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(),
"User account is not allowed to switch to the
requested account",
params, responseType));
}
try {
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(),
nextUserAccount.getDomainId(), null,
remoteAddress, params);
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);
}
} 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 {
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.setIdpId(user.getExternalEntity());
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>();
response.setResponses(accountResponses);
diff --git
a/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
b/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
index 332e0602784..0f25123ff88 100644
---
a/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
+++
b/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java
@@ -192,7 +192,7 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd
implements APIAuthent
String authnId = SAMLUtils.generateSecureRandomId();
samlAuthManager.saveToken(authnId, domainPath,
idpMetadata.getEntityId());
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);
return "";
} if (params.containsKey("SAMLart")) {
diff --git
a/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAML2AuthManager.java
b/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAML2AuthManager.java
index 4e8ba16c739..523f694d80b 100644
---
a/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAML2AuthManager.java
+++
b/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAML2AuthManager.java
@@ -79,6 +79,10 @@ public interface SAML2AuthManager extends
PluggableAPIAuthenticator, PluggableSe
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);
+ 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 getIdPMetadata(String entityId);
Collection<SAMLProviderMetadata> getAllIdPMetadata();
diff --git
a/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java
b/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java
index a7524ec63a7..92408141ef2 100644
---
a/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java
+++
b/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAML2AuthManagerImpl.java
@@ -543,6 +543,6 @@ public class SAML2AuthManagerImpl extends AdapterBase
implements SAML2AuthManage
SAMLCloudStackRedirectionUrl, SAMLUserAttributeName,
SAMLIdentityProviderMetadataURL, SAMLDefaultIdentityProviderId,
SAMLSignatureAlgorithm, SAMLAppendDomainSuffix, SAMLTimeout,
SAMLCheckSignature,
- SAMLForceAuthn, SAMLUserSessionKeyPathAttribute};
+ SAMLForceAuthn, SAMLUserSessionKeyPathAttribute,
SAMLRequirePasswordLogin};
}
}
diff --git
a/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAMLUtils.java
b/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAMLUtils.java
index fd68e2be1ae..2460e3826c6 100644
---
a/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAMLUtils.java
+++
b/plugins/user-authenticators/saml2/src/main/java/org/apache/cloudstack/saml/SAMLUtils.java
@@ -151,11 +151,11 @@ public class SAMLUtils {
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 = "";
try {
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;
if (spMetadata.getKeyPair() != null) {
privateKey = spMetadata.getKeyPair().getPrivate();
@@ -168,28 +168,21 @@ public class SAMLUtils {
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
IssuerBuilder issuerBuilder = new IssuerBuilder();
Issuer issuer = issuerBuilder.buildObject();
issuer.setValue(spId);
- // AuthnContextClass
- AuthnContextClassRefBuilder authnContextClassRefBuilder = new
AuthnContextClassRefBuilder();
- AuthnContextClassRef authnContextClassRef =
authnContextClassRefBuilder.buildObject(
- SAMLConstants.SAML20_NS,
- "AuthnContextClassRef", "saml");
-
authnContextClassRef.setAuthnContextClassRef(AuthnContext.PPT_AUTHN_CTX);
-
- // AuthnContext
- RequestedAuthnContextBuilder requestedAuthnContextBuilder = new
RequestedAuthnContextBuilder();
- RequestedAuthnContext requestedAuthnContext =
requestedAuthnContextBuilder.buildObject();
-
requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
-
requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef);
-
// 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);
@@ -200,11 +193,25 @@ public class SAMLUtils {
authnRequest.setAssertionConsumerServiceURL(consumerUrl);
authnRequest.setProviderName(spId);
authnRequest.setIssuer(issuer);
- authnRequest.setRequestedAuthnContext(requestedAuthnContext);
return authnRequest;
}
+ public static void setRequestedAuthnContext(AuthnRequest authnRequest,
boolean requirePasswordAuthentication) {
+ AuthnContextClassRefBuilder authnContextClassRefBuilder = new
AuthnContextClassRefBuilder();
+ AuthnContextClassRef authnContextClassRef =
authnContextClassRefBuilder.buildObject(
+ SAMLConstants.SAML20_NS,
+ "AuthnContextClassRef", "saml");
+
authnContextClassRef.setAuthnContextClassRef(AuthnContext.PPT_AUTHN_CTX);
+
+ // AuthnContext
+ RequestedAuthnContextBuilder requestedAuthnContextBuilder = new
RequestedAuthnContextBuilder();
+ RequestedAuthnContext requestedAuthnContext =
requestedAuthnContextBuilder.buildObject();
+
requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
+
requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef);
+ authnRequest.setRequestedAuthnContext(requestedAuthnContext);
+ }
+
public static LogoutRequest buildLogoutRequest(String logoutUrl, String
spId, String nameIdString) {
Issuer issuer = new IssuerBuilder().buildObject();
issuer.setValue(spId);
@@ -284,23 +291,6 @@ public class SAMLUtils {
}
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 path = SAML2AuthManager.SAMLUserSessionKeyPathAttribute.value();
String domain = null;
@@ -316,6 +306,18 @@ public class SAMLUtils {
} catch (URISyntaxException ex) {
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 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);
@@ -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));
}
+ 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
* @param key PublicKey
diff --git
a/plugins/user-authenticators/saml2/src/test/java/org/apache/cloudstack/SAMLUtilsTest.java
b/plugins/user-authenticators/saml2/src/test/java/org/apache/cloudstack/SAMLUtilsTest.java
index 752845edb64..891d028aebf 100644
---
a/plugins/user-authenticators/saml2/src/test/java/org/apache/cloudstack/SAMLUtilsTest.java
+++
b/plugins/user-authenticators/saml2/src/test/java/org/apache/cloudstack/SAMLUtilsTest.java
@@ -58,7 +58,7 @@ public class SAMLUtilsTest extends TestCase {
String idpUrl = "http://idp.domain.example";
String spId = "cloudstack";
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.getDestination(), idpUrl);
assertEquals(req.getIssuer().getValue(), spId);
@@ -86,7 +86,7 @@ public class SAMLUtilsTest extends TestCase {
idpMetadata.setSsoUrl(idpUrl);
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");
assertEquals(urlScheme, redirectUrl.getScheme());
assertEquals(idpDomain, redirectUrl.getHost());
@@ -115,7 +115,7 @@ public class SAMLUtilsTest extends TestCase {
idpMetadata.setSsoUrl(idpUrl);
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");
assertEquals(urlScheme, redirectUrl.getScheme());
assertEquals(idpDomain, redirectUrl.getHost());
diff --git
a/plugins/user-authenticators/saml2/src/test/java/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmdTest.java
b/plugins/user-authenticators/saml2/src/test/java/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmdTest.java
index 729334d22ce..bfee28a7e3b 100644
---
a/plugins/user-authenticators/saml2/src/test/java/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmdTest.java
+++
b/plugins/user-authenticators/saml2/src/test/java/org/apache/cloudstack/api/command/ListAndSwitchSAMLAccountCmdTest.java
@@ -213,7 +213,6 @@ public class ListAndSwitchSAMLAccountCmdTest extends
TestCase {
loginCmdResponse.set2FAenabled("false");
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);
- Mockito.doNothing().when(resp).sendRedirect(nullable(String.class));
try {
cmd.authenticate("command", params, session, null,
HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
} catch (ServerApiException exception) {
@@ -221,7 +220,6 @@ public class ListAndSwitchSAMLAccountCmdTest extends
TestCase {
} 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(anyString());
}
}
diff --git a/server/src/main/java/com/cloud/api/ApiServer.java
b/server/src/main/java/com/cloud/api/ApiServer.java
index c78f8e68c2b..afa5f07c826 100644
--- a/server/src/main/java/com/cloud/api/ApiServer.java
+++ b/server/src/main/java/com/cloud/api/ApiServer.java
@@ -1159,7 +1159,14 @@ public class ApiServer extends ManagerBase implements
HttpRequestHandler, ApiSer
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) {
final String timezone = userAcct.getTimezone();
float offsetInHrs = 0f;
diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java
b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
index 8e06c576881..4e3a2e98564 100644
--- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java
+++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
@@ -372,6 +372,14 @@ public class AccountManagerImpl extends ManagerBase
implements AccountManager, M
"totp",
"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() {
super();
}
@@ -1252,8 +1260,8 @@ public class AccountManagerImpl extends ManagerBase
implements AccountManager, M
// Check permissions
checkAccess(getCurrentCallingAccount(), domain);
- if (!_userAccountDao.validateUsernameInDomain(userName, domainId)) {
- throw new InvalidParameterValueException("The user " + userName +
" already exists in domain " + domainId);
+ if (!userAllowMultipleAccounts.valueInDomain(domainId) &&
!_userAccountDao.validateUsernameInDomain(userName, domainId)) {
+ throw new CloudRuntimeException("The user " + userName + " already
exists in domain " + domainId);
}
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");
}
- if (!_userAccountDao.validateUsernameInDomain(userName, domainId)) {
+ if (!userAllowMultipleAccounts.valueInDomain(domainId) &&
!_userAccountDao.validateUsernameInDomain(userName, 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;
user = createUser(account.getId(), userName, password, firstName,
lastName, email, timeZone, userUUID, source);
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.
* </ul>
*/
- protected void validateAndUpdateUsernameIfNeeded(UpdateUserCmd
updateUserCmd, UserVO user, Account account) {
+ protected void validateAndUpdateUsernameIfNeeded(UpdateUserCmd
updateUserCmd, UserVO newUser, Account newAccount) {
String userName = updateUserCmd.getUsername();
if (userName == null) {
return;
@@ -1572,18 +1587,21 @@ public class AccountManagerImpl extends ManagerBase
implements AccountManager, M
if (StringUtils.isBlank(userName)) {
throw new InvalidParameterValueException("Username cannot be
empty.");
}
- List<UserVO> duplicatedUsers = _userDao.findUsersByName(userName);
- for (UserVO duplicatedUser : duplicatedUsers) {
- if (duplicatedUser.getId() == user.getId()) {
+ List<UserVO> existingUsers = _userDao.findUsersByName(userName);
+ for (UserVO existingUser : existingUsers) {
+ if (existingUser.getId() == newUser.getId()) {
continue;
}
- Account duplicatedUserAccountWithUserThatHasTheSameUserName =
_accountDao.findById(duplicatedUser.getAccountId());
- if
(duplicatedUserAccountWithUserThatHasTheSameUserName.getDomainId() ==
account.getDomainId()) {
- DomainVO domain =
_domainDao.findById(duplicatedUserAccountWithUserThatHasTheSameUserName.getDomainId());
- throw new
InvalidParameterValueException(String.format("Username [%s] already exists in
domain [id=%s,name=%s]", duplicatedUser.getUsername(), domain.getUuid(),
domain.getName()));
+
+ // duplicate usernames cannot exist in same domain unless
explicitly configured
+ if
(!userAllowMultipleAccounts.valueInDomain(newAccount.getDomainId())) {
+ 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
// if the user is either locked already or disabled already, don't
change state...only lock currently enabled
-// users
+ // users
boolean success = true;
if (user.getState().equals(State.LOCKED)) {
// already locked...no-op
@@ -3317,7 +3335,8 @@ public class AccountManagerImpl extends ManagerBase
implements AccountManager, M
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[] {UseSecretKeyInResponse,
enableUserTwoFactorAuthentication,
- userTwoFactorAuthenticationDefaultProvider,
mandateUserTwoFactorAuthentication, userTwoFactorAuthenticationIssuer};
+ userTwoFactorAuthenticationDefaultProvider,
mandateUserTwoFactorAuthentication, userTwoFactorAuthenticationIssuer,
+ userAllowMultipleAccounts};
}
public List<UserTwoFactorAuthenticator>
getUserTwoFactorAuthenticationProviders() {
@@ -3502,4 +3521,21 @@ public class AccountManagerImpl extends ManagerBase
implements AccountManager, M
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()));
+ }
+ }
}
diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
index e68de194f01..f1cf0008676 100644
--- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
+++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java
@@ -1270,4 +1270,75 @@ public class AccountManagerImplTest extends
AccountManagetImplTestBase {
Assert.assertNull(updatedUser.getUser2faProvider());
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);
+ }
}
diff --git a/ui/src/components/header/SamlDomainSwitcher.vue
b/ui/src/components/header/SamlDomainSwitcher.vue
index 1d820dcbcff..082bab7bf13 100644
--- a/ui/src/components/header/SamlDomainSwitcher.vue
+++ b/ui/src/components/header/SamlDomainSwitcher.vue
@@ -88,6 +88,7 @@ export default {
this.showSwitcher = false
return
}
+ this.samlAccounts = samlAccounts
this.samlAccounts = _.orderBy(samlAccounts, ['domainPath'], ['asc'])
const currentAccount = this.samlAccounts.filter(x => {
return x.userId === store.getters.userInfo.id
@@ -109,6 +110,8 @@ export default {
this.$message.success(`Switched to "${account.accountName}
(${account.domainPath})"`)
this.$router.go()
})
+ }).else(error => {
+ console.log('error refreshing with new user context: ' + error)
})
}
}
diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js
index 46a1905619f..c0a45259a53 100644
--- a/ui/src/store/modules/user.js
+++ b/ui/src/store/modules/user.js
@@ -290,7 +290,7 @@ const user = {
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
- 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]
commit('SET_INFO', result)
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]
commit('SET_INFO', result)
commit('SET_NAME', result.firstname + ' ' + result.lastname)
diff --git a/ui/vue.config.js b/ui/vue.config.js
index 9cae2ff66fb..a0e795531fb 100644
--- a/ui/vue.config.js
+++ b/ui/vue.config.js
@@ -143,7 +143,7 @@ const vueConfig = {
ws: false,
changeOrigin: true,
proxyTimeout: 10 * 60 * 1000, // 10 minutes
- cookieDomainRewrite: '*',
+ cookieDomainRewrite: process.env.CS_COOKIE_HOST || 'localhost',
cookiePathRewrite: {
'/client': '/'
}