From d5ba39387b690bf6f778e34ab5be0e9693afb0d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20De=20Marco=20Gon=C3=A7alves?= Date: Tue, 8 Apr 2025 07:16:18 -0300 Subject: [PATCH 01/21] fix projects metrics on dashboard (#10651) --- ui/src/views/dashboard/UsageDashboard.vue | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ui/src/views/dashboard/UsageDashboard.vue b/ui/src/views/dashboard/UsageDashboard.vue index 9df3b38c6a9a..5069968a4191 100644 --- a/ui/src/views/dashboard/UsageDashboard.vue +++ b/ui/src/views/dashboard/UsageDashboard.vue @@ -460,9 +460,13 @@ export default { }, listProject () { this.loading = true - api('listProjects', { id: store.getters.project.id }).then(json => { + const params = { + id: store.getters.project.id, + listall: true + } + api('listProjects', params).then(json => { this.loading = false - if (json && json.listprojectsresponse && json.listprojectsresponse.project) { + if (json?.listprojectsresponse?.project) { this.project = json.listprojectsresponse.project[0] } }) From e08f88f0ae6fd10fa27fda093be33d91b8113ecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20De=20Marco=20Gon=C3=A7alves?= Date: Tue, 8 Apr 2025 08:14:29 -0300 Subject: [PATCH 02/21] define the limit of projects through the UI (#10652) --- ui/src/components/view/ResourceLimitTab.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/view/ResourceLimitTab.vue b/ui/src/components/view/ResourceLimitTab.vue index 875d2cc3b402..2717a43eb070 100644 --- a/ui/src/components/view/ResourceLimitTab.vue +++ b/ui/src/components/view/ResourceLimitTab.vue @@ -27,7 +27,7 @@ >
Date: Wed, 9 Apr 2025 13:03:01 +0530 Subject: [PATCH 03/21] xenserver: do not destroy halted hypervisor vm (#9175) Signed-off-by: Abhishek Kumar --- .../resource/CitrixResourceBase.java | 38 ++++++++++++++----- .../xen56/XenServer56FenceCommandWrapper.java | 2 +- .../XenServer56FP1FenceCommandWrapper.java | 2 +- .../CitrixCreateVMSnapshotCommandWrapper.java | 9 ++--- .../CitrixDeleteVMSnapshotCommandWrapper.java | 2 +- ...itrixRevertToVMSnapshotCommandWrapper.java | 3 +- .../xenbase/CitrixStartCommandWrapper.java | 2 +- .../xenbase/CitrixStopCommandWrapper.java | 2 +- 8 files changed, 40 insertions(+), 20 deletions(-) diff --git a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/CitrixResourceBase.java b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/CitrixResourceBase.java index f53a70de7228..322f69c95319 100644 --- a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/CitrixResourceBase.java +++ b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/CitrixResourceBase.java @@ -51,10 +51,6 @@ import javax.naming.ConfigurationException; import javax.xml.parsers.ParserConfigurationException; -import com.trilead.ssh2.SFTPException; -import com.trilead.ssh2.SFTPv3Client; -import com.trilead.ssh2.SFTPv3DirectoryEntry; -import com.trilead.ssh2.SFTPv3FileAttributes; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.diagnostics.CopyToSecondaryStorageAnswer; import org.apache.cloudstack.diagnostics.CopyToSecondaryStorageCommand; @@ -72,6 +68,7 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; +import org.apache.maven.artifact.versioning.ComparableVersion; import org.apache.xmlrpc.XmlRpcException; import org.joda.time.Duration; import org.w3c.dom.Document; @@ -153,6 +150,10 @@ import com.cloud.vm.VirtualMachine.PowerState; import com.cloud.vm.VmDetailConstants; import com.trilead.ssh2.SCPClient; +import com.trilead.ssh2.SFTPException; +import com.trilead.ssh2.SFTPv3Client; +import com.trilead.ssh2.SFTPv3DirectoryEntry; +import com.trilead.ssh2.SFTPv3FileAttributes; import com.xensource.xenapi.Bond; import com.xensource.xenapi.Connection; import com.xensource.xenapi.Console; @@ -627,7 +628,7 @@ public boolean cleanupHaltedVms(final Connection conn) throws XenAPIException, X if (VmPowerState.HALTED.equals(vmRec.powerState) && vmRec.affinity.equals(host) && !isAlienVm(vm, conn)) { try { - vm.destroy(conn); + destroyVm(vm, conn); } catch (final Exception e) { s_logger.warn("Catch Exception " + e.getClass().getName() + ": unable to destroy VM " + vmRec.nameLabel + " due to ", e); success = false; @@ -1452,7 +1453,7 @@ public VM createVmFromTemplate(final Connection conn, final VirtualMachineTO vmS vm.setPVBootloader(conn, "pygrub"); vm.setPVBootloaderArgs(conn, CitrixHelper.getPVbootloaderArgs(guestOsTypeName)); } else { - vm.destroy(conn); + destroyVm(vm, conn, true); throw new CloudRuntimeException("Unable to handle boot loader type: " + vmSpec.getBootloader()); } } @@ -2038,7 +2039,7 @@ void forceShutdownVM(final Connection conn, final VM vm) { final Long domId = vm.getDomid(conn); callHostPlugin(conn, "vmopspremium", "forceShutdownVM", "domId", domId.toString()); vm.powerStateReset(conn); - vm.destroy(conn); + destroyVm(vm, conn); } catch (final Exception e) { final String msg = "forceShutdown failed due to " + e.toString(); s_logger.warn(msg, e); @@ -3690,7 +3691,7 @@ public String handleVmStartFailure(final Connection conn, final String vmName, f } if (vm.getPowerState(conn) == VmPowerState.HALTED) { try { - vm.destroy(conn); + destroyVm(vm, conn, true); } catch (final Exception e) { s_logger.warn("VM destroy failed due to ", e); } @@ -5208,7 +5209,7 @@ protected void startvmfailhandle(final Connection conn, final VM vm, final List< } if (vm.getPowerState(conn) == VmPowerState.HALTED) { try { - vm.destroy(conn); + destroyVm(vm, conn, true); } catch (final Exception e) { final String msg = "VM destroy failed due to " + e.toString(); s_logger.warn(msg, e); @@ -5875,4 +5876,23 @@ private void umountNfs(Connection conn, String remoteDir, String localDir) { s_logger.warn(errMsg); } } + + public boolean isDestroyHaltedVms() { + ComparableVersion version = new ComparableVersion(getHost().getProductVersion()); + if (version.compareTo(new ComparableVersion("8.0")) >= 0) { + return false; + } + return true; + } + + public void destroyVm(VM vm, Connection connection, boolean forced) throws XenAPIException, XmlRpcException { + if (!isDestroyHaltedVms() && !forced) { + return; + } + vm.destroy(connection); + } + + public void destroyVm(VM vm, Connection connection) throws XenAPIException, XmlRpcException { + destroyVm(vm, connection, false); + } } diff --git a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xen56/XenServer56FenceCommandWrapper.java b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xen56/XenServer56FenceCommandWrapper.java index 3cebbd9a342e..713807ffaeea 100644 --- a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xen56/XenServer56FenceCommandWrapper.java +++ b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xen56/XenServer56FenceCommandWrapper.java @@ -56,7 +56,7 @@ public Answer execute(final FenceCommand command, final XenServer56Resource xenS for (final VM vm : vms) { s_logger.info("Fence command for VM " + command.getVmName()); vm.powerStateReset(conn); - vm.destroy(conn); + xenServer56.destroyVm(vm, conn); } return new FenceAnswer(command); } catch (final XmlRpcException e) { diff --git a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xen56p1/XenServer56FP1FenceCommandWrapper.java b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xen56p1/XenServer56FP1FenceCommandWrapper.java index bc7a4434bd06..dda87101de77 100644 --- a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xen56p1/XenServer56FP1FenceCommandWrapper.java +++ b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xen56p1/XenServer56FP1FenceCommandWrapper.java @@ -68,7 +68,7 @@ public Answer execute(final FenceCommand command, final XenServer56Resource xenS } s_logger.info("Fence command for VM " + command.getVmName()); vm.powerStateReset(conn); - vm.destroy(conn); + xenServer56.destroyVm(vm, conn); for (final VDI vdi : vdis) { final Map smConfig = vdi.getSmConfig(conn); for (final String key : smConfig.keySet()) { diff --git a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixCreateVMSnapshotCommandWrapper.java b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixCreateVMSnapshotCommandWrapper.java index 68c295717c3e..47c08b0cebe4 100644 --- a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixCreateVMSnapshotCommandWrapper.java +++ b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixCreateVMSnapshotCommandWrapper.java @@ -100,6 +100,7 @@ public Answer execute(final CreateVMSnapshotCommand command, final CitrixResourc vm = citrixResourceBase.getVM(conn, vmName); vmState = vm.getPowerState(conn); } catch (final Exception e) { + s_logger.debug(String.format("Failed to find VM with name: %s due to:", vmName), e); if (!snapshotMemory) { vm = citrixResourceBase.createWorkingVM(conn, vmName, guestOSType, platformEmulator, listVolumeTo); } @@ -178,13 +179,11 @@ public Answer execute(final CreateVMSnapshotCommand command, final CitrixResourc vdi.destroy(conn); } } - vmSnapshot.destroy(conn); + citrixResourceBase.destroyVm(vmSnapshot, conn, true); } } - if (vmState == VmPowerState.HALTED) { - if (vm != null) { - vm.destroy(conn); - } + if (vmState == VmPowerState.HALTED && vm != null) { + citrixResourceBase.destroyVm(vm, conn); } } catch (final Exception e2) { s_logger.error("delete snapshot error due to " + e2.getMessage()); diff --git a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixDeleteVMSnapshotCommandWrapper.java b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixDeleteVMSnapshotCommandWrapper.java index b74111e8441c..0bcbe13c5580 100644 --- a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixDeleteVMSnapshotCommandWrapper.java +++ b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixDeleteVMSnapshotCommandWrapper.java @@ -68,7 +68,7 @@ public Answer execute(final DeleteVMSnapshotCommand command, final CitrixResourc if (command.getTarget().getType() == VMSnapshot.Type.DiskAndMemory) { vdiList.add(snapshot.getSuspendVDI(conn)); } - snapshot.destroy(conn); + citrixResourceBase.destroyVm(snapshot, conn, true); for (final VDI vdi : vdiList) { vdi.destroy(conn); } diff --git a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixRevertToVMSnapshotCommandWrapper.java b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixRevertToVMSnapshotCommandWrapper.java index f8bb1b892420..445e764a38c3 100644 --- a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixRevertToVMSnapshotCommandWrapper.java +++ b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixRevertToVMSnapshotCommandWrapper.java @@ -68,6 +68,7 @@ public Answer execute(final RevertToVMSnapshotCommand command, final CitrixResou try { vm = citrixResourceBase.getVM(conn, vmName); } catch (final Exception e) { + s_logger.debug(String.format("Failed to find VM with name: %s due to:", vmName), e); vm = citrixResourceBase.createWorkingVM(conn, vmName, command.getGuestOSType(), command.getPlatformEmulator(), listVolumeTo); } @@ -90,7 +91,7 @@ public Answer execute(final RevertToVMSnapshotCommand command, final CitrixResou } if (!snapshotMemory) { - vm.destroy(conn); + citrixResourceBase.destroyVm(vm, conn); vmState = PowerState.PowerOff; } else { vmState = PowerState.PowerOn; diff --git a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixStartCommandWrapper.java b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixStartCommandWrapper.java index ad76b7f4541a..5867a151c850 100644 --- a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixStartCommandWrapper.java +++ b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixStartCommandWrapper.java @@ -75,7 +75,7 @@ public Answer execute(final StartCommand command, final CitrixResourceBase citri for (final VM v : vms) { final VM.Record vRec = v.getRecord(conn); if (vRec.powerState == VmPowerState.HALTED) { - v.destroy(conn); + citrixResourceBase.destroyVm(v, conn, true); } else if (vRec.powerState == VmPowerState.RUNNING) { final String host = vRec.residentOn.getUuid(conn); final String msg = "VM " + vmName + " is runing on host " + host; diff --git a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixStopCommandWrapper.java b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixStopCommandWrapper.java index 8e7eb4caec29..87c2cb768229 100644 --- a/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixStopCommandWrapper.java +++ b/plugins/hypervisors/xenserver/src/main/java/com/cloud/hypervisor/xenserver/resource/wrapper/xenbase/CitrixStopCommandWrapper.java @@ -143,7 +143,7 @@ public Answer execute(final StopCommand command, final CitrixResourceBase citrix for (final VIF vif : vifs) { networks.add(vif.getNetwork(conn)); } - vm.destroy(conn); + citrixResourceBase.destroyVm(vm, conn); final SR sr = citrixResourceBase.getISOSRbyVmName(conn, command.getVmName(), false); citrixResourceBase.removeSR(conn, sr); final SR configDriveSR = citrixResourceBase.getISOSRbyVmName(conn, command.getVmName(), true); From d32065fd38a2b9216efa28a6890ae6dfc504daca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernardo=20De=20Marco=20Gon=C3=A7alves?= Date: Wed, 9 Apr 2025 09:20:51 -0300 Subject: [PATCH 04/21] backport #10500 framework/cluster: fix NPE for ms-host status when mgr stops (#10653) --- .../com/cloud/cluster/ClusterManagerImpl.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/framework/cluster/src/main/java/com/cloud/cluster/ClusterManagerImpl.java b/framework/cluster/src/main/java/com/cloud/cluster/ClusterManagerImpl.java index d601c094ca76..e684f6fd42f4 100644 --- a/framework/cluster/src/main/java/com/cloud/cluster/ClusterManagerImpl.java +++ b/framework/cluster/src/main/java/com/cloud/cluster/ClusterManagerImpl.java @@ -1052,12 +1052,24 @@ public boolean stop() { } if (_mshostId != null) { - final ManagementServerHostVO mshost = _mshostDao.findByMsid(_msId); - final ManagementServerStatusVO mshostStatus = mshostStatusDao.findByMsId(mshost.getUuid()); - mshost.setState(ManagementServerHost.State.Down); - mshostStatus.setLastJvmStop(new Date()); - _mshostDao.update(_mshostId, mshost); - mshostStatusDao.update(mshostStatus.getId(), mshostStatus); + ManagementServerHostVO mshost = _mshostDao.findByMsid(_msId); + if (mshost != null) { + ManagementServerStatusVO mshostStatus = mshostStatusDao.findByMsId(mshost.getUuid()); + if (mshostStatus != null) { + mshost.setState(ManagementServerHost.State.Down); + mshostStatus.setLastJvmStop(new Date()); + _mshostDao.update(_mshostId, mshost); + mshostStatusDao.update(mshostStatus.getId(), mshostStatus); + } else { + s_logger.warn(String.format("Found a management server host [%s] without a status. This should never happen!", mshost)); + mshostStatus = new ManagementServerStatusVO(); + mshostStatus.setMsId(mshost.getUuid()); + mshostStatus.setLastSystemBoot(new Date()); + mshostStatus.setLastJvmStart(new Date()); + mshostStatus.setUpdated(new Date()); + mshostStatusDao.persist(mshostStatus); + } + } } _heartbeatScheduler.shutdownNow(); From 99ea77dc83901fd88db4e1fcb2f7623bd343b5b1 Mon Sep 17 00:00:00 2001 From: dahn Date: Mon, 14 Apr 2025 10:24:35 +0200 Subject: [PATCH 05/21] Usage server: remove logging of prameters including secret keys (#10649) --- usage/src/main/java/com/cloud/usage/UsageManagerImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/usage/src/main/java/com/cloud/usage/UsageManagerImpl.java b/usage/src/main/java/com/cloud/usage/UsageManagerImpl.java index 6125f6f604de..63624cdc3c07 100644 --- a/usage/src/main/java/com/cloud/usage/UsageManagerImpl.java +++ b/usage/src/main/java/com/cloud/usage/UsageManagerImpl.java @@ -232,7 +232,6 @@ public boolean configure(String name, Map params) throws Configu if (params != null) { mergeConfigs(configs, params); - s_logger.info("configs = " + configs); } } catch (CloudRuntimeException e) { s_logger.error("Unhandled configuration exception: " + e.getMessage()); From f13cf597a2e00054033ab5851db628680a64f8dc Mon Sep 17 00:00:00 2001 From: Rene Glover Date: Mon, 14 Apr 2025 05:59:43 -0500 Subject: [PATCH 06/21] 4.19 fix saml account selector (#10311) --- .../command/ListAndSwitchSAMLAccountCmd.java | 15 +++- .../SAML2LoginAPIAuthenticatorCmd.java | 2 +- .../cloudstack/saml/SAML2AuthManager.java | 4 + .../cloudstack/saml/SAML2AuthManagerImpl.java | 2 +- .../org/apache/cloudstack/saml/SAMLUtils.java | 88 ++++++++++++------- .../org/apache/cloudstack/SAMLUtilsTest.java | 6 +- .../ListAndSwitchSAMLAccountCmdTest.java | 2 - .../main/java/com/cloud/api/ApiServer.java | 9 +- .../com/cloud/user/AccountManagerImpl.java | 64 +++++++++++--- .../cloud/user/AccountManagerImplTest.java | 71 +++++++++++++++ .../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 c2f81cd33563..040d5414f26f 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 String authenticate(final String command, final Map par } 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 String authenticate(final String command, final Map par || !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 switchableAccounts = _userAccountDao.getAllUsersByNameAndEntity(currentUserAccount.getUsername(), currentUserAccount.getExternalEntity()); @@ -178,6 +186,9 @@ public String authenticate(final String command, final Map par 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 response = new ListResponse(); 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 332e06027844..0f25123ff88b 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 String authenticate(final String command, final Map par 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 4e8ba16c7398..523f694d80b2 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 SAMLUserSessionKeyPathAttribute = new ConfigKey("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 SAMLRequirePasswordLogin = new ConfigKey("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 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 a7524ec63a7d..92408141ef29 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 ConfigKey[] getConfigKeys() { 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 fd68e2be1ae9..2460e3826c69 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 static String getValueFromAssertions(final List assertions, fi 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 static String buildAuthnRequestUrl(final String authnId, final SAMLProvid 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 static AuthnRequest buildAuthnRequestObject(final String authnId, final S 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 static String generateSAMLRequestSignature(final String urlEncodedString, } 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 static void setupSamlUserCookies(final LoginCmdResponse loginResponse, fi } 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 static void setupSamlUserCookies(final LoginCmdResponse loginResponse, fi 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 752845edb642..891d028aebfe 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 void testBuildAuthnRequestObject() throws Exception { 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 void testBuildAuthnRequestUrlWithoutQueryParam() throws Exception { 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 void testBuildAuthnRequestUrlWithQueryParam() throws Exception { 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 729334d22ce5..bfee28a7e3b8 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 void testListAndSwitchSAMLAccountCmd() throws Exception { 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 void testListAndSwitchSAMLAccountCmd() throws Exception { } 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 c78f8e68c2bb..afa5f07c826a 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 ResponseObject loginUser(final HttpSession session, final String username 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 8e06c5768818..4e3a2e98564d 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 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 UserAccount createUserAccount(final String userName, final String passwor // 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 UserVO createUser(String userName, String password, String firstName, Str 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 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 @@ protected void validateCurrentPassword(UserVO user, String currentPassword) { *
  • The username must be unique in each domain. Therefore, if there is already another user with the same username, an {@link InvalidParameterValueException} is thrown. * */ - 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 @@ protected void validateAndUpdateUsernameIfNeeded(UpdateUserCmd updateUserCmd, Us if (StringUtils.isBlank(userName)) { throw new InvalidParameterValueException("Username cannot be empty."); } - List duplicatedUsers = _userDao.findUsersByName(userName); - for (UserVO duplicatedUser : duplicatedUsers) { - if (duplicatedUser.getId() == user.getId()) { + List 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 UserAccount lockUser(long userId) { // 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 String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { return new ConfigKey[] {UseSecretKeyInResponse, enableUserTwoFactorAuthentication, - userTwoFactorAuthenticationDefaultProvider, mandateUserTwoFactorAuthentication, userTwoFactorAuthenticationIssuer}; + userTwoFactorAuthenticationDefaultProvider, mandateUserTwoFactorAuthentication, userTwoFactorAuthenticationIssuer, + userAllowMultipleAccounts}; } public List getUserTwoFactorAuthenticationProviders() { @@ -3502,4 +3521,21 @@ public UserAccount clearUserTwoFactorAuthenticationInSetupStateOnLogin(UserAccou 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 e68de194f019..f1cf00086764 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 void testClearUser2FA_When2FAInSetupState_Disable2FA() { 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 1d820dcbcffd..082bab7bf13c 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 46a1905619f9..c0a45259a536 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 9cae2ff66fb1..a0e795531fba 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': '/' } From 53d3d19606bcf332753f4b51b31b851fe53cbd66 Mon Sep 17 00:00:00 2001 From: dahn Date: Wed, 16 Apr 2025 17:26:26 +0200 Subject: [PATCH 07/21] server: check startip and endip of shared network (#10704) revert part of #10168 --- .../com/cloud/network/NetworkServiceImpl.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java index bdb928ae919d..7655e6486c50 100644 --- a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java @@ -1635,10 +1635,19 @@ public Network createGuestNetwork(CreateNetworkCmd cmd) throws InsufficientCapac throwInvalidIdException("Network offering with specified id doesn't support adding multiple ip ranges", ntwkOff.getUuid(), NETWORK_OFFERING_ID); } - if (GuestType.Shared == ntwkOff.getGuestType() && !ntwkOff.isSpecifyVlan() && Objects.isNull(associatedNetworkId)) { - throw new CloudRuntimeException("Associated network must be provided when creating Shared networks when specifyVlan is false"); - } + + if (GuestType.Shared == ntwkOff.getGuestType()) { + if (!ntwkOff.isSpecifyIpRanges()) { + throw new CloudRuntimeException("The 'specifyipranges' parameter should be true for Shared Networks"); + } + if (ipv4 && Objects.isNull(startIP)) { + throw new CloudRuntimeException("IPv4 address range needs to be provided"); + } + if (ipv6 && Objects.isNull(startIPv6)) { + throw new CloudRuntimeException("IPv6 address range needs to be provided"); + } + } Pair interfaceMTUs = validateMtuConfig(publicMtu, privateMtu, zone.getId()); mtuCheckForVpcNetwork(vpcId, interfaceMTUs, publicMtu); From 8db248e4b40b96c0f70a4eaaf41848087d1f24ca Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Thu, 17 Apr 2025 04:54:11 -0300 Subject: [PATCH 08/21] UI: Move templates creation date to the Zones tab (#10709) * UI: Move templates creation date to the Zones tab * Extend changes to ISOs --- ui/src/components/view/InfoCard.vue | 4 ++-- ui/src/views/image/IsoZones.vue | 8 ++++++++ ui/src/views/image/TemplateZones.vue | 8 ++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index d805f7b2719a..00cb4748a88c 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -629,7 +629,7 @@ {{ resource.podname || resource.pod || resource.podid }}
  • -
    +
    {{ $t('label.zone') }}
    @@ -700,7 +700,7 @@ {{ resource.managementserver || resource.managementserverid }}
    -
    +
    {{ $t('label.created') }}
    {{ $toLocaleDate(resource.created) }} diff --git a/ui/src/views/image/IsoZones.vue b/ui/src/views/image/IsoZones.vue index daf1e7e21e0f..75eac8fd97f5 100644 --- a/ui/src/views/image/IsoZones.vue +++ b/ui/src/views/image/IsoZones.vue @@ -48,6 +48,9 @@ {{ $t('label.yes') }} {{ $t('label.no') }} + +