diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 74b010e05fd..875a144bfa7 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -145,6 +145,10 @@ CREATE TABLE IF NOT EXISTS `cloud`.`kms_hsm_profiles` ( CONSTRAINT `fk_kms_hsm_profiles__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='HSM profiles for KMS providers'; +-- Add default database HSM profile (disabled by default) +INSERT INTO `cloud`.`kms_hsm_profiles` (`uuid`, `name`, `protocol`, `account_id`, `domain_id`, `enabled`, `system`, `created`) +VALUES (UUID(), 'default', 'database', 1, 1, 0, 1, NOW()); + -- KMS HSM Profile Details (Protocol-specific configuration) CREATE TABLE IF NOT EXISTS `cloud`.`kms_hsm_profile_details` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, diff --git a/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSProvider.java b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSProvider.java index 2b793fe560a..388d464caa7 100644 --- a/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSProvider.java +++ b/framework/kms/src/main/java/org/apache/cloudstack/framework/kms/KMSProvider.java @@ -103,6 +103,18 @@ public interface KMSProvider extends Configurable, Adapter { */ void deleteKek(String kekId) throws KMSException; + /** + * Validates the configuration details for this provider before saving an HSM + * profile. + * Implementations should override this to perform provider-specific validation. + * + * @param details the configuration details to validate + * @throws KMSException if validation fails + */ + default void validateProfileConfig(java.util.Map details) throws KMSException { + // default no-op + } + /** * Check if a KEK exists and is accessible * diff --git a/plugins/kms/pkcs11/src/main/java/org/apache/cloudstack/kms/provider/pkcs11/PKCS11HSMProvider.java b/plugins/kms/pkcs11/src/main/java/org/apache/cloudstack/kms/provider/pkcs11/PKCS11HSMProvider.java index 54376f55c08..16203dd7f1d 100644 --- a/plugins/kms/pkcs11/src/main/java/org/apache/cloudstack/kms/provider/pkcs11/PKCS11HSMProvider.java +++ b/plugins/kms/pkcs11/src/main/java/org/apache/cloudstack/kms/provider/pkcs11/PKCS11HSMProvider.java @@ -296,30 +296,35 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider { /** * Validates HSM profile configuration for PKCS#11 provider. * - *

Validates: + *

+ * Validates: *

* * @param config Configuration map from HSM profile details * @throws KMSException with {@code INVALID_PARAMETER} if validation fails */ - void validateProfileConfig(Map config) throws KMSException { + @Override + public void validateProfileConfig(Map config) throws KMSException { String libraryPath = config.get("library"); - if (StringUtils.isEmpty(libraryPath)) { + if (StringUtils.isBlank(libraryPath)) { throw KMSException.invalidParameter("library is required for PKCS#11 HSM profile"); } String slot = config.get("slot"); + String slotListIndex = config.get("slot_list_index"); String tokenLabel = config.get("token_label"); - if (StringUtils.isEmpty(slot) && StringUtils.isEmpty(tokenLabel)) { - throw KMSException.invalidParameter("Either 'slot' or 'token_label' is required for PKCS#11 HSM profile"); + if (StringUtils.isAllBlank(slot, slotListIndex, tokenLabel)) { + throw KMSException.invalidParameter( + "One of 'slot', 'slot_list_index', or 'token_label' is required for PKCS#11 HSM profile"); } - if (!StringUtils.isEmpty(slot)) { + if (StringUtils.isNotBlank(slot)) { try { Integer.parseInt(slot); } catch (NumberFormatException e) { @@ -327,6 +332,17 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider { } } + if (StringUtils.isNotBlank(slotListIndex)) { + try { + int idx = Integer.parseInt(slotListIndex); + if (idx < 0) { + throw KMSException.invalidParameter("slot_list_index must be a non-negative integer"); + } + } catch (NumberFormatException e) { + throw KMSException.invalidParameter("slot_list_index must be a valid integer: " + slotListIndex); + } + } + File libraryFile = new File(libraryPath); if (!libraryFile.exists() && !libraryFile.isAbsolute()) { // The HSM library might be in the system library path @@ -334,31 +350,16 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider { libraryPath); } - parsePositiveInteger(config, "max_sessions", "max_sessions"); - } - - /** - * Parses a positive integer from configuration. - * - * @param config Configuration map - * @param key Configuration key - * @param errorPrefix Prefix for error messages - * @return Parsed integer value, or -1 if not provided - * @throws KMSException if value is invalid or not positive - */ - private int parsePositiveInteger(Map config, String key, String errorPrefix) throws KMSException { - String value = config.get(key); - if (StringUtils.isEmpty(value)) { - return -1; // Not provided - } - try { - int parsed = Integer.parseInt(value); - if (parsed <= 0) { - throw KMSException.invalidParameter(errorPrefix + " must be greater than 0"); + String max_sessions = config.get("max_sessions"); + if (StringUtils.isNotBlank(max_sessions)) { + try { + int idx = Integer.parseInt(max_sessions); + if (idx <= 0) { + throw KMSException.invalidParameter("max_sessions must be greater than 0"); + } + } catch (NumberFormatException e) { + throw KMSException.invalidParameter("max_sessions must be a valid integer: " + max_sessions); } - return parsed; - } catch (NumberFormatException e) { - throw KMSException.invalidParameter(errorPrefix + " must be a valid integer: " + value); } } @@ -615,7 +616,7 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider { */ private String buildSunPKCS11Config(Map config, String nameSuffix) throws KMSException { String libraryPath = config.get("library"); - if (StringUtils.isEmpty(libraryPath)) { + if (StringUtils.isBlank(libraryPath)) { throw KMSException.invalidParameter("library is required"); } @@ -627,14 +628,17 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider { configBuilder.append("library=").append(libraryPath).append("\n"); String tokenLabel = config.get("token_label"); + String slotListIndex = config.get("slot_list_index"); String slot = config.get("slot"); - if (!StringUtils.isEmpty(tokenLabel)) { + if (StringUtils.isNotBlank(tokenLabel)) { configBuilder.append("tokenLabel=").append(tokenLabel).append("\n"); - } else if (!StringUtils.isEmpty(slot)) { + } else if (StringUtils.isNotBlank(slotListIndex)) { + configBuilder.append("slotListIndex=").append(slotListIndex).append("\n"); + } else if (StringUtils.isNotBlank(slot)) { configBuilder.append("slot=").append(slot).append("\n"); } else { - throw KMSException.invalidParameter("Either 'slot' or 'token_label' is required"); + throw KMSException.invalidParameter("One of 'slot', 'slot_list_index', or 'token_label' is required"); } return configBuilder.toString(); diff --git a/server/src/main/java/org/apache/cloudstack/kms/KMSManagerImpl.java b/server/src/main/java/org/apache/cloudstack/kms/KMSManagerImpl.java index 9f791d75131..451638fc2f6 100644 --- a/server/src/main/java/org/apache/cloudstack/kms/KMSManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/kms/KMSManagerImpl.java @@ -969,12 +969,16 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable throw new InvalidParameterValueException("Protocol cannot be empty"); } + KMSProvider provider; try { - getKMSProvider(protocol); + provider = getKMSProvider(protocol); } catch (CloudRuntimeException e) { throw new InvalidParameterValueException("No provider found for protocol: " + protocol); } + Map details = cmd.getDetails() != null ? cmd.getDetails() : new HashMap<>(); + provider.validateProfileConfig(details); + boolean isSystem = cmd.isSystem(); if (isSystem && !accountManager.isRootAdmin(caller.getId())) { throw new PermissionDeniedException("Only root admins can create system HSM profiles"); diff --git a/ui/src/config/section/kms.js b/ui/src/config/section/kms.js index 9802b3dd9bf..7ab1e82e5e7 100644 --- a/ui/src/config/section/kms.js +++ b/ui/src/config/section/kms.js @@ -22,6 +22,9 @@ export default { name: 'kms', title: 'label.kms', icon: 'hdd-outlined', + show: (record, store) => { + return ['Admin'].includes(store.getters.userInfo.roletype) || store.getters.features.hashsmprofiles + }, children: [ { name: 'kmskey', @@ -157,6 +160,7 @@ export default { title: 'label.hsm.profile', icon: 'safety-outlined', permission: ['listHSMProfiles'], + show: (record, route, user) => { return ['Admin'].includes(user.roletype) }, resourceType: 'HSMProfile', columns: () => { const fields = ['name', 'enabled'] diff --git a/ui/src/store/modules/user.js b/ui/src/store/modules/user.js index 6a818d58723..9cffe85b81f 100644 --- a/ui/src/store/modules/user.js +++ b/ui/src/store/modules/user.js @@ -480,6 +480,16 @@ const user = { commit('SET_CLOUDIAN', cloudian) }).catch(ignored => { }) + + if ('listHSMProfiles' in store.getters.apis) { + getAPI('listHSMProfiles', { listall: true }).then(response => { + const hasHsmProfiles = (response.listhsmprofilesresponse.count > 0) + const features = Object.assign({}, store.getters.features) + features.hashsmprofiles = hasHsmProfiles + commit('SET_FEATURES', features) + }).catch(ignored => { + }) + } }).catch(error => { console.error(error) }) diff --git a/ui/src/views/compute/DeployVM.vue b/ui/src/views/compute/DeployVM.vue index ca6299b68b5..0b2f778c539 100644 --- a/ui/src/views/compute/DeployVM.vue +++ b/ui/src/views/compute/DeployVM.vue @@ -2030,9 +2030,14 @@ export default { domainid: this.owner.domainid, projectid: this.owner.projectid }).then(response => { - this.options.kmsKeys = response.listkmskeysresponse.kmskey || [] - }).catch(error => { - this.$notifyError(error) + const kmskeyMap = response.listkmskeysresponse.kmskey || [] + if (kmskeyMap.length > 0) { + this.options.kmsKeys = kmskeyMap + } else { + this.options.kmsKeys = null + } + }).catch(() => { + this.options.kmsKeys = null }).finally(() => { this.loading.kmsKeys = false }) diff --git a/ui/src/views/compute/wizard/DiskSizeSelection.vue b/ui/src/views/compute/wizard/DiskSizeSelection.vue index 43fa556b8ef..baae69ea1a1 100644 --- a/ui/src/views/compute/wizard/DiskSizeSelection.vue +++ b/ui/src/views/compute/wizard/DiskSizeSelection.vue @@ -131,6 +131,9 @@ export default { return this.rootDiskSelected?.iscustomizediops || false }, showKmsKeySelector () { + if (this.kmsKeys === null) { + return false + } const isRootDisk = this.inputDecorator === 'rootdisksize' const isDataDisk = this.inputDecorator === 'size' diff --git a/ui/src/views/storage/CreateVolume.vue b/ui/src/views/storage/CreateVolume.vue index 6d1c1d15930..65b3ba24c51 100644 --- a/ui/src/views/storage/CreateVolume.vue +++ b/ui/src/views/storage/CreateVolume.vue @@ -116,7 +116,7 @@ :placeholder="apiParams.maxiops.description"/> - +