diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/management/ListMgmtsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/management/ListMgmtsCmd.java index 6b72deb07757..7b7eae1d0e93 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/management/ListMgmtsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/management/ListMgmtsCmd.java @@ -42,7 +42,7 @@ public class ListMgmtsCmd extends BaseListCmd { @Parameter(name = ApiConstants.PEERS, type = CommandType.BOOLEAN, description = "Whether to return the management server peers or not. By default, the management server peers will not be returned.", - since = "4.20.0.0") + since = "4.20.1.0") private Boolean peers; ///////////////////////////////////////////////////// diff --git a/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDao.java b/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDao.java index 730182a1cb56..f4b2f6460020 100644 --- a/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDao.java +++ b/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDao.java @@ -47,6 +47,8 @@ public interface ProjectAccountDao extends GenericDao { void removeAccountFromProjects(long accountId); + void removeUserFromProjects(long userId); + boolean canUserModifyProject(long projectId, long accountId, long userId); List listUsersOrAccountsByRole(long id); diff --git a/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDaoImpl.java b/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDaoImpl.java index 8947cc600b38..b6eb6d44cea8 100644 --- a/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/projects/dao/ProjectAccountDaoImpl.java @@ -192,6 +192,17 @@ public void removeAccountFromProjects(long accountId) { } } + @Override + public void removeUserFromProjects(long userId) { + SearchCriteria sc = AllFieldsSearch.create(); + sc.setParameters("userId", userId); + + int removedCount = remove(sc); + if (removedCount > 0) { + logger.debug(String.format("Removed user [%s] from %s project(s).", userId, removedCount)); + } + } + @Override public boolean canUserModifyProject(long projectId, long accountId, long userId) { SearchCriteria sc = AllFieldsSearch.create(); diff --git a/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to41920.java b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to41920.java new file mode 100644 index 000000000000..6215021473ed --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41910to41920.java @@ -0,0 +1,66 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.upgrade.dao; + +import com.cloud.utils.exception.CloudRuntimeException; + +import java.io.InputStream; +import java.sql.Connection; + +public class Upgrade41910to41920 implements DbUpgrade { + + @Override + public String[] getUpgradableVersionRange() { + return new String[]{"4.19.1.0", "4.19.2.0"}; + } + + @Override + public String getUpgradedVersion() { + return "4.19.2.0"; + } + + @Override + public boolean supportsRollingUpgrade() { + return false; + } + + @Override + public InputStream[] getPrepareScripts() { + final String scriptFile = "META-INF/db/schema-41910to41920.sql"; + final InputStream script = Thread.currentThread().getContextClassLoader().getResourceAsStream(scriptFile); + if (script == null) { + throw new CloudRuntimeException("Unable to find " + scriptFile); + } + + return new InputStream[]{script}; + } + + @Override + public void performDataMigration(Connection conn) { + } + + @Override + public InputStream[] getCleanupScripts() { + final String scriptFile = "META-INF/db/schema-41910to41920-cleanup.sql"; + final InputStream script = Thread.currentThread().getContextClassLoader().getResourceAsStream(scriptFile); + if (script == null) { + throw new CloudRuntimeException("Unable to find " + scriptFile); + } + + return new InputStream[]{script}; + } +} diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41910to41920-cleanup.sql b/engine/schema/src/main/resources/META-INF/db/schema-41910to41920-cleanup.sql new file mode 100644 index 000000000000..cb317c69b796 --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/schema-41910to41920-cleanup.sql @@ -0,0 +1,23 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. + +--; +-- Schema upgrade cleanup from 4.19.1.0 to 4.19.2.0 +--; + +-- Delete `project_account` entries for users that were removed +DELETE FROM `cloud`.`project_account` WHERE `user_id` IN (SELECT `id` FROM `cloud`.`user` WHERE `removed`); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41910to41920.sql b/engine/schema/src/main/resources/META-INF/db/schema-41910to41920.sql new file mode 100644 index 000000000000..d4c23eee0ede --- /dev/null +++ b/engine/schema/src/main/resources/META-INF/db/schema-41910to41920.sql @@ -0,0 +1,20 @@ +-- Licensed to the Apache Software Foundation (ASF) under one +-- or more contributor license agreements. See the NOTICE file +-- distributed with this work for additional information +-- regarding copyright ownership. The ASF licenses this file +-- to you under the Apache License, Version 2.0 (the +-- "License"); you may not use this file except in compliance +-- with the License. You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, +-- software distributed under the License is distributed on an +-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +-- KIND, either express or implied. See the License for the +-- specific language governing permissions and limitations +-- under the License. + +--; +-- Schema upgrade from 4.19.1.0 to 4.19.2.0 +--; diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql b/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql index 97ee1df8b677..c36b71c2f250 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-41910to42000.sql @@ -425,10 +425,3 @@ INSERT IGNORE INTO `cloud`.`guest_os_hypervisor` (uuid, hypervisor_type, hypervi CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.vm_instance', 'delete_protection', 'boolean DEFAULT FALSE COMMENT "delete protection for vm" '); CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.volumes', 'delete_protection', 'boolean DEFAULT FALSE COMMENT "delete protection for volumes" '); - --- Modify index for mshost_peer -DELETE FROM `cloud`.`mshost_peer`; -CALL `cloud`.`IDEMPOTENT_DROP_FOREIGN_KEY`('cloud.mshost_peer','fk_mshost_peer__owner_mshost'); -CALL `cloud`.`IDEMPOTENT_DROP_INDEX`('i_mshost_peer__owner_peer_runid','mshost_peer'); -CALL `cloud`.`IDEMPOTENT_ADD_UNIQUE_KEY`('cloud.mshost_peer', 'i_mshost_peer__owner_peer', '(owner_mshost, peer_mshost)'); -CALL `cloud`.`IDEMPOTENT_ADD_FOREIGN_KEY`('cloud.mshost_peer', 'fk_mshost_peer__owner_mshost', '(owner_mshost)', '`mshost`(`id`)'); diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql b/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql index 31c4928d81bf..570627a8da55 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42000to42010.sql @@ -22,3 +22,10 @@ -- Add column api_key_access to user and account tables CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.user', 'api_key_access', 'boolean DEFAULT NULL COMMENT "is api key access allowed for the user" AFTER `secret_key`'); CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.account', 'api_key_access', 'boolean DEFAULT NULL COMMENT "is api key access allowed for the account" '); + +-- Modify index for mshost_peer +DELETE FROM `cloud`.`mshost_peer`; +CALL `cloud`.`IDEMPOTENT_DROP_FOREIGN_KEY`('cloud.mshost_peer','fk_mshost_peer__owner_mshost'); +CALL `cloud`.`IDEMPOTENT_DROP_INDEX`('i_mshost_peer__owner_peer_runid','mshost_peer'); +CALL `cloud`.`IDEMPOTENT_ADD_UNIQUE_KEY`('cloud.mshost_peer', 'i_mshost_peer__owner_peer', '(owner_mshost, peer_mshost)'); +CALL `cloud`.`IDEMPOTENT_ADD_FOREIGN_KEY`('cloud.mshost_peer', 'fk_mshost_peer__owner_mshost', '(owner_mshost)', '`mshost`(`id`)'); diff --git a/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java b/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java index 961c537d0da5..e96b0a008944 100644 --- a/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java +++ b/framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java @@ -656,7 +656,7 @@ private void migrateTemplateDeployAsIsDetails(Connection conn) throws SQLExcepti String sqlTemplateDeployAsIsDetails = "SELECT template_deploy_as_is_details.value " + "FROM template_deploy_as_is_details JOIN vm_instance " + "WHERE template_deploy_as_is_details.template_id = vm_instance.vm_template_id " + - "vm_instance.id = %s AND template_deploy_as_is_details.name = '%s' LIMIT 1"; + "AND vm_instance.id = %s AND template_deploy_as_is_details.name = '%s' LIMIT 1"; try (PreparedStatement selectPstmt = conn.prepareStatement("SELECT id, vm_id, name, value FROM user_vm_deploy_as_is_details"); ResultSet rs = selectPstmt.executeQuery(); PreparedStatement updatePstmt = conn.prepareStatement("UPDATE user_vm_deploy_as_is_details SET value=? WHERE id=?") diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index fa177428e513..bea799944bef 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -1500,6 +1500,8 @@ public UserAccount updateUser(UpdateUserCmd updateUserCmd) { *
    *
  • If 'password' is blank, we throw an {@link InvalidParameterValueException}; *
  • If 'current password' is not provided and user is not an Admin, we throw an {@link InvalidParameterValueException}; + *
  • If the user whose password is being changed has a source equal to {@link User.Source#SAML2}, {@link User.Source#SAML2DISABLED} or {@link User.Source#LDAP}, + * we throw an {@link InvalidParameterValueException}; *
  • If a normal user is calling this method, we use {@link #validateCurrentPassword(UserVO, String)} to check if the provided old password matches the database one; *
* @@ -1514,6 +1516,12 @@ public void validateUserPasswordAndUpdateIfNeeded(String newPassword, UserVO use throw new InvalidParameterValueException("Password cannot be empty or blank."); } + User.Source userSource = user.getSource(); + if (userSource == User.Source.SAML2 || userSource == User.Source.SAML2DISABLED || userSource == User.Source.LDAP) { + logger.warn(String.format("Unable to update the password for user [%d], as its source is [%s].", user.getId(), user.getSource().toString())); + throw new InvalidParameterValueException("CloudStack does not support updating passwords for SAML or LDAP users. Please contact your cloud administrator for assistance."); + } + passwordPolicy.verifyIfPasswordCompliesWithPasswordPolicies(newPassword, user.getUsername(), getAccount(user.getAccountId()).getDomainId()); Account callingAccount = getCurrentCallingAccount(); diff --git a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java index 11fc69c538ce..645c9e5aa675 100644 --- a/server/src/test/java/com/cloud/user/AccountManagerImplTest.java +++ b/server/src/test/java/com/cloud/user/AccountManagerImplTest.java @@ -874,6 +874,36 @@ public void validateUserPasswordAndUpdateIfNeededTestIfVerifyIfPasswordCompliesW accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(newPassword, userVoMock, currentPassword, false); } + @Test(expected = InvalidParameterValueException.class) + public void validateUserPasswordAndUpdateIfNeededTestSaml2UserShouldNotBeAllowedToUpdateTheirPassword() { + String newPassword = "newPassword"; + String currentPassword = "theCurrentPassword"; + + Mockito.when(userVoMock.getSource()).thenReturn(User.Source.SAML2); + + accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(newPassword, userVoMock, currentPassword, false); + } + + @Test(expected = InvalidParameterValueException.class) + public void validateUserPasswordAndUpdateIfNeededTestSaml2DisabledUserShouldNotBeAllowedToUpdateTheirPassword() { + String newPassword = "newPassword"; + String currentPassword = "theCurrentPassword"; + + Mockito.when(userVoMock.getSource()).thenReturn(User.Source.SAML2DISABLED); + + accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(newPassword, userVoMock, currentPassword, false); + } + + @Test(expected = InvalidParameterValueException.class) + public void validateUserPasswordAndUpdateIfNeededTestLdapUserShouldNotBeAllowedToUpdateTheirPassword() { + String newPassword = "newPassword"; + String currentPassword = "theCurrentPassword"; + + Mockito.when(userVoMock.getSource()).thenReturn(User.Source.LDAP); + + accountManagerImpl.validateUserPasswordAndUpdateIfNeeded(newPassword, userVoMock, currentPassword, false); + } + private String configureUserMockAuthenticators(String newPassword) { accountManagerImpl._userPasswordEncoders = new ArrayList<>(); UserAuthenticator authenticatorMock1 = Mockito.mock(UserAuthenticator.class); diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js index 5a50cb0b1ddb..e0fa72dff8a0 100644 --- a/ui/src/config/section/storage.js +++ b/ui/src/config/section/storage.js @@ -251,9 +251,7 @@ export default { label: 'label.action.create.template.from.volume', dataView: true, show: (record) => { - return !['Destroy', 'Destroyed', 'Expunging', 'Expunged', 'Migrating', 'Uploading', 'UploadError', 'Creating'].includes(record.state) && - ((record.type === 'ROOT' && record.vmstate === 'Stopped') || - (record.type !== 'ROOT' && !record.virtualmachineid && !['Allocated', 'Uploaded'].includes(record.state))) + return record.state === 'Ready' && (record.vmstate === 'Stopped' || !record.virtualmachineid) }, args: (record, store) => { var fields = ['volumeid', 'name', 'displaytext', 'ostypeid', 'isdynamicallyscalable', 'requireshvm', 'passwordenabled'] diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue index 3c4742d64b20..cb9f4a6dce76 100644 --- a/ui/src/views/compute/DeployVM.vue +++ b/ui/src/views/compute/DeployVM.vue @@ -1940,18 +1940,16 @@ export default { this.form.userdataid = undefined return } + this.form.userdataid = id this.userDataParams = [] api('listUserData', { id: id }).then(json => { const resp = json?.listuserdataresponse?.userdata || [] if (resp[0]) { - var params = resp[0].params - if (params) { - var dataParams = params.split(',') - } - var that = this - dataParams.forEach(function (val, index) { - that.userDataParams.push({ + const params = resp[0].params + const dataParams = params ? params.split(',') : [] + dataParams.forEach((val, index) => { + this.userDataParams.push({ id: index, key: val }) diff --git a/ui/src/views/compute/wizard/LoadBalancerSelection.vue b/ui/src/views/compute/wizard/LoadBalancerSelection.vue index e2ffb991e987..3ba66241d56b 100644 --- a/ui/src/views/compute/wizard/LoadBalancerSelection.vue +++ b/ui/src/views/compute/wizard/LoadBalancerSelection.vue @@ -30,6 +30,7 @@ :rowKey="record => record.id" :pagination="false" :rowSelection="rowSelection" + :customRow="onClickRow" size="middle" :scroll="{ y: 225 }">