From 473983491fd6159a46a9c857b1971c733fed508a Mon Sep 17 00:00:00 2001 From: vishesh92 Date: Tue, 14 Apr 2026 23:04:33 +0530 Subject: [PATCH] Address comments --- .../main/java/com/cloud/event/EventTypes.java | 1 + .../cloudstack/api/ResponseGenerator.java | 2 - .../admin/kms/MigrateVolumesToKMSCmd.java | 12 +- .../api/command/user/kms/DeleteKMSKeyCmd.java | 5 + .../api/command/user/kms/RotateKMSKeyCmd.java | 4 +- .../api/command/user/kms/UpdateKMSKeyCmd.java | 5 + ...ofileCmd.java => CreateHSMProfileCmd.java} | 20 ++- .../user/kms/hsm/DeleteHSMProfileCmd.java | 12 ++ .../user/kms/hsm/UpdateHSMProfileCmd.java | 11 ++ .../api/response/HSMProfileResponse.java | 8 +- .../api/response/VolumeResponse.java | 2 +- .../org/apache/cloudstack/kms/HSMProfile.java | 2 +- .../org/apache/cloudstack/kms/KMSManager.java | 4 +- .../orchestration/VolumeOrchestrator.java | 85 +++++------ .../apache/cloudstack/kms/HSMProfileVO.java | 16 +- .../META-INF/db/schema-42210to42300.sql | 16 +- .../kms/provider/DatabaseKMSProvider.java | 4 +- .../apache/cloudstack/kms/KMSManagerImpl.java | 141 ++++++++++++------ .../kms/KMSManagerImplAccessTest.java | 10 +- tools/apidoc/gen_toc.py | 4 +- ui/public/locales/en.json | 1 - ui/src/config/section/kms.js | 12 +- ui/src/config/section/storage.js | 2 +- 23 files changed, 227 insertions(+), 152 deletions(-) rename api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/{AddHSMProfileCmd.java => CreateHSMProfileCmd.java} (90%) diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 5dd80301845..ddc635b1842 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -1038,6 +1038,7 @@ public class EventTypes { entityEventDetails.put(EVENT_KMS_KEY_WRAP, KMSKey.class); entityEventDetails.put(EVENT_KMS_KEY_DELETE, KMSKey.class); entityEventDetails.put(EVENT_KMS_KEY_ROTATE, KMSKey.class); + entityEventDetails.put(EVENT_VOLUME_MIGRATE_TO_KMS, KMSKey.class); // HSM Profile Events entityEventDetails.put(EVENT_HSM_PROFILE_CREATE, HSMProfile.class); diff --git a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java index a243bac9b67..6e880c89432 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java +++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java @@ -23,7 +23,6 @@ import java.util.Map; import java.util.Set; import org.apache.cloudstack.api.response.ConsoleSessionResponse; -import org.apache.cloudstack.api.response.KMSKeyResponse; import org.apache.cloudstack.consoleproxy.ConsoleSession; import org.apache.cloudstack.acl.apikeypair.ApiKeyPair; import org.apache.cloudstack.acl.apikeypair.ApiKeyPairPermission; @@ -157,7 +156,6 @@ import org.apache.cloudstack.direct.download.DirectDownloadCertificate; import org.apache.cloudstack.direct.download.DirectDownloadCertificateHostMap; import org.apache.cloudstack.direct.download.DirectDownloadManager; import org.apache.cloudstack.gui.theme.GuiThemeJoin; -import org.apache.cloudstack.kms.KMSKey; import org.apache.cloudstack.management.ManagementServerHost; import org.apache.cloudstack.network.lb.ApplicationLoadBalancerRule; import org.apache.cloudstack.region.PortableIp; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/kms/MigrateVolumesToKMSCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/kms/MigrateVolumesToKMSCmd.java index eef4ef9af26..5b8dcf9cc21 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/kms/MigrateVolumesToKMSCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/kms/MigrateVolumesToKMSCmd.java @@ -39,14 +39,13 @@ import javax.inject.Inject; import java.util.List; @APICommand(name = "migrateVolumesToKMS", - description = "Migrates passphrase-based volumes to KMS (admin only)", + description = "Migrates encrypted volumes to KMS", responseObject = AsyncJobResponse.class, since = "4.23.0", authorized = {RoleType.Admin}, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) public class MigrateVolumesToKMSCmd extends BaseAsyncCmd { - private static final String s_name = "migratevolumestokmsresponse"; @Inject private KMSManager kmsManager; @@ -112,11 +111,6 @@ public class MigrateVolumesToKMSCmd extends BaseAsyncCmd { } } - @Override - public String getCommandName() { - return s_name; - } - @Override public long getEntityOwnerId() { KMSKey key = _entityMgr.findById(KMSKey.class, kmsKeyId); @@ -138,11 +132,11 @@ public class MigrateVolumesToKMSCmd extends BaseAsyncCmd { @Override public ApiCommandResourceType getApiResourceType() { - return ApiCommandResourceType.Zone; + return ApiCommandResourceType.KmsKey; } @Override public Long getApiResourceId() { - return zoneId; + return kmsKeyId; } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/DeleteKMSKeyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/DeleteKMSKeyCmd.java index 9963677c2f2..58f67ec60c4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/DeleteKMSKeyCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/DeleteKMSKeyCmd.java @@ -83,6 +83,11 @@ public class DeleteKMSKeyCmd extends BaseAsyncCmd implements UserCmd { return ApiCommandResourceType.KmsKey; } + @Override + public Long getApiResourceId() { + return getId(); + } + @Override public String getEventType() { return EventTypes.EVENT_KMS_KEY_DELETE; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/RotateKMSKeyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/RotateKMSKeyCmd.java index a370a1d5d59..e3c6c5822b2 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/RotateKMSKeyCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/RotateKMSKeyCmd.java @@ -36,7 +36,7 @@ import org.apache.cloudstack.kms.KMSManager; import javax.inject.Inject; @APICommand(name = "rotateKMSKey", - description = "Rotates KEK by creating new version and scheduling gradual re-encryption", + description = "Rotates KMS key (KEK) by creating new version and scheduling gradual re-encryption", responseObject = AsyncJobResponse.class, since = "4.23.0", authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, @@ -123,6 +123,6 @@ public class RotateKMSKeyCmd extends BaseAsyncCmd { @Override public Long getApiResourceId() { - return id; + return getId(); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/UpdateKMSKeyCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/UpdateKMSKeyCmd.java index 4eba649ecb4..ba7ad692de4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/UpdateKMSKeyCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/UpdateKMSKeyCmd.java @@ -104,6 +104,11 @@ public class UpdateKMSKeyCmd extends BaseAsyncCmd implements UserCmd { return ApiCommandResourceType.KmsKey; } + @Override + public Long getApiResourceId() { + return getId(); + } + @Override public String getEventType() { return EventTypes.EVENT_KMS_KEY_UPDATE; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/AddHSMProfileCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/CreateHSMProfileCmd.java similarity index 90% rename from api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/AddHSMProfileCmd.java rename to api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/CreateHSMProfileCmd.java index b7410e4cfea..34e1fc55c17 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/AddHSMProfileCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/CreateHSMProfileCmd.java @@ -25,6 +25,7 @@ import com.cloud.exception.ResourceUnavailableException; import com.cloud.utils.StringUtils; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; @@ -45,10 +46,10 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; -@APICommand(name = "addHSMProfile", description = "Adds a new HSM profile", responseObject = HSMProfileResponse.class, +@APICommand(name = "createHSMProfile", description = "Creates a new HSM profile", responseObject = HSMProfileResponse.class, requestHasSensitiveInfo = true, responseHasSensitiveInfo = true, since = "4.23.0", authorized = { RoleType.Admin }) -public class AddHSMProfileCmd extends BaseCmd { +public class CreateHSMProfileCmd extends BaseCmd { @Inject private KMSManager kmsManager; @@ -77,10 +78,10 @@ public class AddHSMProfileCmd extends BaseCmd { description = "the ID of the project to add the HSM profile for") private Long projectId; - @Parameter(name = "system", type = CommandType.BOOLEAN, - description = "whether this is a system HSM profile available to all users globally (root admin only). " + @Parameter(name = ApiConstants.IS_PUBLIC, type = CommandType.BOOLEAN, + description = "whether this is a public HSM profile available to all users globally (root admin only). " + "Default is false") - private Boolean system; + private Boolean isPublic; @Parameter(name = ApiConstants.VENDOR_NAME, type = CommandType.STRING, description = "the vendor name of the HSM") private String vendorName; @@ -116,8 +117,8 @@ public class AddHSMProfileCmd extends BaseCmd { return projectId; } - public Boolean isSystem() { - return system != null && system; + public Boolean getIsPublic() { + return isPublic != null && isPublic; } public String getVendorName() { @@ -159,4 +160,9 @@ public class AddHSMProfileCmd extends BaseCmd { } return CallContext.current().getCallingAccount().getId(); } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.HsmProfile; + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/DeleteHSMProfileCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/DeleteHSMProfileCmd.java index 5f70eb7a899..575a4447a69 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/DeleteHSMProfileCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/DeleteHSMProfileCmd.java @@ -24,6 +24,7 @@ import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; @@ -78,4 +79,15 @@ public class DeleteHSMProfileCmd extends BaseCmd { } return CallContext.current().getCallingAccount().getId(); } + + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.HsmProfile; + } + + @Override + public Long getApiResourceId() { + return getId(); + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/UpdateHSMProfileCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/UpdateHSMProfileCmd.java index a408c967362..fea7807d616 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/UpdateHSMProfileCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/kms/hsm/UpdateHSMProfileCmd.java @@ -24,6 +24,7 @@ import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.BaseCmd; @@ -90,4 +91,14 @@ public class UpdateHSMProfileCmd extends BaseCmd { } return CallContext.current().getCallingAccount().getId(); } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.HsmProfile; + } + + @Override + public Long getApiResourceId() { + return getId(); + } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/HSMProfileResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/HSMProfileResponse.java index 607d1f0c379..44b2038c934 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/HSMProfileResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/HSMProfileResponse.java @@ -90,9 +90,9 @@ public class HSMProfileResponse extends BaseResponse implements ControlledViewEn @Param(description = "whether the HSM profile is enabled") private Boolean enabled; - @SerializedName("system") + @SerializedName(ApiConstants.IS_PUBLIC) @Param(description = "whether this is a system HSM profile available to all users globally") - private Boolean system; + private Boolean isPublic; @SerializedName(ApiConstants.CREATED) @Param(description = "the date the HSM profile was created") @@ -168,8 +168,8 @@ public class HSMProfileResponse extends BaseResponse implements ControlledViewEn this.enabled = enabled; } - public void setSystem(Boolean system) { - this.system = system; + public void setIsPublic(Boolean isPublic) { + this.isPublic = isPublic; } public void setCreated(Date created) { diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java index fd0c8ca10f5..177a51678cc 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/VolumeResponse.java @@ -310,7 +310,7 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co private String encryptionFormat; @SerializedName(ApiConstants.KMS_KEY) - @Param(description = "KMS key id of the volume", since = "4.23.0") + @Param(description = "KMS key name of the volume", since = "4.23.0") private String kmsKey; @SerializedName(ApiConstants.KMS_KEY_ID) diff --git a/api/src/main/java/org/apache/cloudstack/kms/HSMProfile.java b/api/src/main/java/org/apache/cloudstack/kms/HSMProfile.java index 97a38f16ba9..bbb8617ff24 100644 --- a/api/src/main/java/org/apache/cloudstack/kms/HSMProfile.java +++ b/api/src/main/java/org/apache/cloudstack/kms/HSMProfile.java @@ -38,7 +38,7 @@ public interface HSMProfile extends Identity, InternalIdentity, ControlledEntity boolean isEnabled(); - boolean isSystem(); + boolean getIsPublic(); Date getCreated(); diff --git a/api/src/main/java/org/apache/cloudstack/kms/KMSManager.java b/api/src/main/java/org/apache/cloudstack/kms/KMSManager.java index 1c473b7f93c..aee59496dee 100644 --- a/api/src/main/java/org/apache/cloudstack/kms/KMSManager.java +++ b/api/src/main/java/org/apache/cloudstack/kms/KMSManager.java @@ -25,7 +25,7 @@ import org.apache.cloudstack.api.command.user.kms.CreateKMSKeyCmd; import org.apache.cloudstack.api.command.user.kms.DeleteKMSKeyCmd; import org.apache.cloudstack.api.command.user.kms.ListKMSKeysCmd; import org.apache.cloudstack.api.command.user.kms.UpdateKMSKeyCmd; -import org.apache.cloudstack.api.command.user.kms.hsm.AddHSMProfileCmd; +import org.apache.cloudstack.api.command.user.kms.hsm.CreateHSMProfileCmd; import org.apache.cloudstack.api.command.user.kms.hsm.DeleteHSMProfileCmd; import org.apache.cloudstack.api.command.user.kms.hsm.ListHSMProfilesCmd; import org.apache.cloudstack.api.command.user.kms.hsm.UpdateHSMProfileCmd; @@ -231,7 +231,7 @@ public interface KMSManager extends Manager, Configurable { * @return the created HSM profile * @throws KMSException if addition fails */ - HSMProfile addHSMProfile(AddHSMProfileCmd cmd) throws KMSException; + HSMProfile addHSMProfile(CreateHSMProfileCmd cmd) throws KMSException; /** * List HSM profiles diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index c69ac85a95b..e94b54348d9 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -1937,61 +1937,62 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati return kmsKeyDao.findById(volume.getKmsKeyId()); } - private VolumeVO setPassphraseForVolumeEncryption(VolumeVO volume) { - return setPassphraseForVolumeEncryption(volume, null, null); - } + private VolumeVO setKmsKeyForVolumeEncryption(VolumeVO volume, KMSKeyVO kmsKey, Long callerAccountId) { + // Determine caller account ID if not provided + if (callerAccountId == null) { + callerAccountId = volume.getAccountId(); + } + // Validate permission + if (!kmsManager.hasPermission(callerAccountId, kmsKey)) { + throw new CloudRuntimeException("No permission to use KMS key: " + kmsKey); + } + + try { + logger.debug("Generating and wrapping DEK for volume {} using KMS key {}", volume.getName(), kmsKey.getUuid()); + long startTime = System.currentTimeMillis(); + + // Generate and wrap DEK using active KEK version + WrappedKey wrappedKey = kmsManager.generateVolumeKeyWithKek(kmsKey, callerAccountId); + + // The wrapped key is already persisted by generateVolumeKeyWithKek, get its ID + KMSWrappedKeyVO wrappedKeyVO = kmsWrappedKeyDao.findByUuid(wrappedKey.getUuid()); + if (wrappedKeyVO == null) { + throw new CloudRuntimeException("Failed to find persisted wrapped key: " + wrappedKey.getUuid()); + } + + // Set the wrapped key ID on the volume + volume.setKmsWrappedKeyId(wrappedKeyVO.getId()); + + long finishTime = System.currentTimeMillis(); + logger.debug("Generating and persisting wrapped key took {} ms for volume: {}", + (finishTime - startTime), volume.getName()); + + return _volsDao.persist(volume); + + } catch (KMSException e) { + throw new CloudRuntimeException("KMS failure while setting up volume encryption: " + e.getMessage(), e); + } + } private VolumeVO setPassphraseForVolumeEncryption(VolumeVO volume, KMSKeyVO kmsKey, Long callerAccountId) { // If volume already has encryption set up, return it if (volume.getKmsWrappedKeyId() != null || volume.getPassphraseId() != null) { return volume; } - if (kmsKey != null) { - // Determine caller account ID if not provided - if (callerAccountId == null) { - callerAccountId = volume.getAccountId(); - } - - // Validate permission - if (!kmsManager.hasPermission(callerAccountId, kmsKey)) { - throw new CloudRuntimeException("No permission to use KMS key: " + kmsKey); - } - - try { - logger.debug("Generating and wrapping DEK for volume {} using KMS key {}", volume.getName(), kmsKey.getUuid()); - long startTime = System.currentTimeMillis(); - - // Generate and wrap DEK using active KEK version - WrappedKey wrappedKey = kmsManager.generateVolumeKeyWithKek(kmsKey, callerAccountId); - - // The wrapped key is already persisted by generateVolumeKeyWithKek, get its ID - KMSWrappedKeyVO wrappedKeyVO = kmsWrappedKeyDao.findByUuid(wrappedKey.getUuid()); - if (wrappedKeyVO == null) { - throw new CloudRuntimeException("Failed to find persisted wrapped key: " + wrappedKey.getUuid()); - } - - // Set the wrapped key ID on the volume - volume.setKmsWrappedKeyId(wrappedKeyVO.getId()); - - long finishTime = System.currentTimeMillis(); - logger.debug("Generating and persisting wrapped key took {} ms for volume: {}", - (finishTime - startTime), volume.getName()); - - return _volsDao.persist(volume); - - } catch (KMSException e) { - throw new CloudRuntimeException("KMS failure while setting up volume encryption: " + e.getMessage(), e); - } + return setKmsKeyForVolumeEncryption(volume, kmsKey, callerAccountId); } - // Legacy: passphrase-based encryption (fallback when KMS not enabled or KMS key not specified) - logger.debug("Creating passphrase for the volume: " + volume.getName()); + return setPassphraseForVolumeEncryption(volume); + } + + private VolumeVO setPassphraseForVolumeEncryption(VolumeVO volume) { + logger.debug("Creating passphrase for the volume: {}", volume.getName()); long startTime = System.currentTimeMillis(); PassphraseVO passphrase = passphraseDao.persist(new PassphraseVO(true)); volume.setPassphraseId(passphrase.getId()); long finishTime = System.currentTimeMillis(); - logger.debug("Creating and persisting passphrase took: " + (finishTime - startTime) + " ms for the volume: " + volume.toString()); + logger.debug("Creating and persisting passphrase took: {} ms for the volume: {}", finishTime - startTime, volume.toString()); return _volsDao.persist(volume); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/kms/HSMProfileVO.java b/engine/schema/src/main/java/org/apache/cloudstack/kms/HSMProfileVO.java index fdf1c78c693..86d57400c0d 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/kms/HSMProfileVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/kms/HSMProfileVO.java @@ -61,8 +61,8 @@ public class HSMProfileVO implements HSMProfile { @Column(name = "enabled") private boolean enabled; - @Column(name = "system") - private boolean system; + @Column(name = "is_public") + private boolean isPublic; @Column(name = "created") private Date created; @@ -73,7 +73,7 @@ public class HSMProfileVO implements HSMProfile { public HSMProfileVO() { this.uuid = UUID.randomUUID().toString(); this.created = new Date(); - this.system = false; + this.isPublic = false; } public HSMProfileVO(String name, String protocol, Long accountId, Long domainId, Long zoneId, String vendorName) { @@ -85,7 +85,7 @@ public class HSMProfileVO implements HSMProfile { this.zoneId = zoneId; this.vendorName = vendorName; this.enabled = true; - this.system = false; + this.isPublic = false; this.created = new Date(); } @@ -173,11 +173,11 @@ public class HSMProfileVO implements HSMProfile { } @Override - public boolean isSystem() { - return system; + public boolean getIsPublic() { + return isPublic; } - public void setSystem(boolean system) { - this.system = system; + public void setIsPublic(boolean isPublic) { + this.isPublic = isPublic; } } 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 d857247f134..f7eed41c669 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 @@ -121,7 +121,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`kms_hsm_profiles` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `uuid` VARCHAR(40) NOT NULL, `name` VARCHAR(255) NOT NULL, - `protocol` VARCHAR(32) NOT NULL COMMENT 'PKCS11, KMIP, AWS_KMS, etc.', + `protocol` VARCHAR(32) NOT NULL COMMENT 'Protocol for the HSM Profile', -- Scoping `account_id` BIGINT UNSIGNED COMMENT 'null = admin-provided (available to all accounts)', @@ -129,9 +129,9 @@ CREATE TABLE IF NOT EXISTS `cloud`.`kms_hsm_profiles` ( `zone_id` BIGINT UNSIGNED COMMENT 'null = global scope', -- Metadata - `vendor_name` VARCHAR(64) COMMENT 'HSM vendor (Thales, AWS, SoftHSM, etc.)', + `vendor_name` VARCHAR(64) COMMENT 'HSM vendor', `enabled` BOOLEAN NOT NULL DEFAULT TRUE, - `system` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'System profile (globally available, root admin only)', + `is_public` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Public profile. Available to all accounts', `created` DATETIME NOT NULL, `removed` DATETIME, @@ -159,7 +159,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`kms_hsm_profile_details` ( -- KMS Keys (Key Encryption Key Metadata) -- Account-scoped KEKs for envelope encryption CREATE TABLE IF NOT EXISTS `cloud`.`kms_keys` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique ID', + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `uuid` VARCHAR(40) NOT NULL COMMENT 'UUID - user-facing identifier', `name` VARCHAR(255) NOT NULL COMMENT 'User-friendly name', `description` VARCHAR(1024) COMMENT 'User description', @@ -182,13 +182,13 @@ CREATE TABLE IF NOT EXISTS `cloud`.`kms_keys` ( CONSTRAINT `fk_kms_keys__account_id` FOREIGN KEY (`account_id`) REFERENCES `account`(`id`) ON DELETE CASCADE, CONSTRAINT `fk_kms_keys__domain_id` FOREIGN KEY (`domain_id`) REFERENCES `domain`(`id`) ON DELETE CASCADE, CONSTRAINT `fk_kms_keys__zone_id` FOREIGN KEY (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE, - CONSTRAINT `fk_kms_keys__hsm_profile_id` FOREIGN KEY (`hsm_profile_id`) REFERENCES `kms_hsm_profiles`(`id`) + CONSTRAINT `fk_kms_keys__hsm_profile_id` FOREIGN KEY (`hsm_profile_id`) REFERENCES `kms_hsm_profiles`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KMS Key (KEK) metadata - account-scoped keys for envelope encryption'; -- KMS KEK Versions (multiple KEKs per KMS key for gradual rotation) -- Supports multiple KEK versions per logical KMS key during rotation CREATE TABLE IF NOT EXISTS `cloud`.`kms_kek_versions` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique ID', + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `uuid` VARCHAR(40) NOT NULL COMMENT 'UUID', `kms_key_id` BIGINT UNSIGNED NOT NULL COMMENT 'Reference to kms_keys table', `version_number` INT NOT NULL COMMENT 'Version number (1, 2, 3, ...)', @@ -203,13 +203,13 @@ CREATE TABLE IF NOT EXISTS `cloud`.`kms_kek_versions` ( INDEX `idx_kms_key_status` (`kms_key_id`, `status`, `removed`), INDEX `idx_kek_label` (`kek_label`), CONSTRAINT `fk_kms_kek_versions__kms_key_id` FOREIGN KEY (`kms_key_id`) REFERENCES `kms_keys`(`id`) ON DELETE CASCADE, - CONSTRAINT `fk_kms_kek_versions__hsm_profile_id` FOREIGN KEY (`hsm_profile_id`) REFERENCES `kms_hsm_profiles`(`id`) + CONSTRAINT `fk_kms_kek_versions__hsm_profile_id` FOREIGN KEY (`hsm_profile_id`) REFERENCES `kms_hsm_profiles`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KEK versions for a KMS key - supports gradual rotation'; -- KMS Wrapped Keys (Data Encryption Keys) -- Generic table for wrapped DEKs - references kms_keys for metadata and kek_versions for specific KEK version CREATE TABLE IF NOT EXISTS `cloud`.`kms_wrapped_key` ( - `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Unique ID', + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `uuid` VARCHAR(40) NOT NULL COMMENT 'UUID', `kms_key_id` BIGINT UNSIGNED COMMENT 'Reference to kms_keys table', `kek_version_id` BIGINT UNSIGNED COMMENT 'Reference to kms_kek_versions table', diff --git a/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/DatabaseKMSProvider.java b/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/DatabaseKMSProvider.java index 0d9ca1aa405..0366369614f 100644 --- a/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/DatabaseKMSProvider.java +++ b/plugins/kms/database/src/main/java/org/apache/cloudstack/kms/provider/DatabaseKMSProvider.java @@ -351,7 +351,7 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider { try { SearchBuilder sb = hsmProfileDao.createSearchBuilder(); sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); - sb.and("system", sb.entity().isSystem(), SearchCriteria.Op.EQ); + sb.and("system", sb.entity().getIsPublic(), SearchCriteria.Op.EQ); sb.and("protocol", sb.entity().getProtocol(), SearchCriteria.Op.EQ); sb.done(); @@ -369,7 +369,7 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider { HSMProfileVO profile = new HSMProfileVO(DEFAULT_PROFILE_NAME, PROVIDER_NAME, SYSTEM_ACCOUNT_ID, ROOT_DOMAIN_ID, null, null); profile.setEnabled(false); - profile.setSystem(true); + profile.setIsPublic(true); hsmProfileDao.persist(profile); logger.info("Seeded default database HSM profile (id={}, uuid={})", profile.getId(), profile.getUuid()); } catch (Exception e) { 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 3c41267032c..9a3f1338d99 100644 --- a/server/src/main/java/org/apache/cloudstack/kms/KMSManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/kms/KMSManagerImpl.java @@ -46,17 +46,17 @@ import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.Transaction; import com.cloud.utils.db.TransactionCallbackWithException; import com.cloud.utils.exception.CloudRuntimeException; -import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.command.admin.kms.MigrateVolumesToKMSCmd; import org.apache.cloudstack.api.command.user.kms.CreateKMSKeyCmd; import org.apache.cloudstack.api.command.user.kms.DeleteKMSKeyCmd; import org.apache.cloudstack.api.command.user.kms.ListKMSKeysCmd; import org.apache.cloudstack.api.command.user.kms.RotateKMSKeyCmd; import org.apache.cloudstack.api.command.user.kms.UpdateKMSKeyCmd; -import org.apache.cloudstack.api.command.user.kms.hsm.AddHSMProfileCmd; +import org.apache.cloudstack.api.command.user.kms.hsm.CreateHSMProfileCmd; import org.apache.cloudstack.api.command.user.kms.hsm.DeleteHSMProfileCmd; import org.apache.cloudstack.api.command.user.kms.hsm.ListHSMProfilesCmd; import org.apache.cloudstack.api.command.user.kms.hsm.UpdateHSMProfileCmd; +import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.response.HSMProfileResponse; import org.apache.cloudstack.api.response.KMSKeyResponse; import org.apache.cloudstack.api.response.ListResponse; @@ -196,6 +196,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable } @Override + @ActionEvent(eventType = EventTypes.EVENT_KMS_KEY_UNWRAP, eventDescription = "unwrapping key") public byte[] unwrapKey(Long wrappedKeyId) throws KMSException { KMSWrappedKeyVO wrappedVO = kmsWrappedKeyDao.findById(wrappedKeyId); if (wrappedVO == null) { @@ -212,8 +213,14 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable if (version != null && version.getStatus() != KMSKekVersionVO.Status.Archived) { try { byte[] dek = getUnwrappedKey(wrappedVO, kmsKey, version); - logger.debug("Successfully unwrapped key {} with KEK version {}", wrappedKeyId, - version.getVersionNumber()); + + CallContext.current().setEventResourceId(kmsKey.getId()); + CallContext.current().setEventResourceType(ApiCommandResourceType.KmsKey); + CallContext.current().setEventDetails(String.format( + "Unwrapped %s key (wrapped key uuid: %s) using KMS key: %s (uuid: %s) with KEK version %d", + kmsKey.getPurpose().getName(), wrappedVO.getUuid(), kmsKey.getName(), kmsKey.getUuid(), version.getVersionNumber())); + + logger.debug("Successfully unwrapped key {} with KEK version {}", wrappedKeyId, version.getVersionNumber()); return dek; } catch (Exception e) { logger.warn("Failed to unwrap with version {}: {}", version.getVersionNumber(), e.getMessage()); @@ -226,8 +233,15 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable for (KMSKekVersionVO version : versions) { try { byte[] dek = getUnwrappedKey(wrappedVO, kmsKey, version); - logger.info("Successfully unwrapped key {} with KEK version {} (fallback)", wrappedKeyId, - version.getVersionNumber()); + + CallContext.current().setEventResourceId(kmsKey.getId()); + CallContext.current().setEventResourceType(ApiCommandResourceType.KmsKey); + CallContext.current().setEventDetails(String.format( + "Unwrapped %s key (wrapped key uuid: %s) using KMS key: %s (uuid: %s) with KEK version %d (fallback)", + kmsKey.getPurpose().getName(), wrappedVO.getUuid(), kmsKey.getName(), kmsKey.getUuid(), version.getVersionNumber())); + + logger.info("Successfully unwrapped key {} with KEK version {} (fallback)", + wrappedKeyId, version.getVersionNumber()); return dek; } catch (Exception e) { logger.debug("Failed to unwrap with version {}: {}", version.getVersionNumber(), e.getMessage()); @@ -249,8 +263,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable } @Override - @ActionEvent(eventType = EventTypes.EVENT_KMS_KEY_WRAP, - eventDescription = "generating volume key with specified KEK") + @ActionEvent(eventType = EventTypes.EVENT_KMS_KEY_WRAP, eventDescription = "generating key with specified KEK") public WrappedKey generateVolumeKeyWithKek(KMSKey kmsKey, Long callerAccountId) throws KMSException { if (kmsKey == null) { throw KMSException.kekNotFound("KMS key not found"); @@ -299,8 +312,14 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable throw handleKmsException(e); } - logger.debug("Generated volume key using KMS key {} with KEK version {}, wrapped key UUID: {}", - kmsKey, activeVersion.getVersionNumber(), wrappedKey.getUuid()); + CallContext.current().setEventResourceId(kmsKey.getId()); + CallContext.current().setEventResourceType(ApiCommandResourceType.KmsKey); + CallContext.current().setEventDetails(String.format( + "Generated %s key (wrapped key uuid: %s) using KMS key: %s (uuid: %s) with KEK version %d", + kmsKey.getPurpose().getName(), wrappedKey.getUuid(), kmsKey.getName(), kmsKey.getUuid(), activeVersion.getVersionNumber())); + + logger.debug("Generated {} key using KMS key {} with KEK version {}, wrapped key UUID: {}", + kmsKey.getPurpose().getName(), kmsKey, activeVersion.getVersionNumber(), wrappedKey.getUuid()); return wrappedKey; } @@ -313,6 +332,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable } @Override + @ActionEvent(eventType = EventTypes.EVENT_KMS_KEY_CREATE, eventDescription = "creating user KMS key") public KMSKeyResponse createKMSKey(CreateKMSKeyCmd cmd) throws KMSException { Account caller = CallContext.current().getCallingAccount(); Account targetAccount = accountManager.finalizeOwner(caller, cmd.getAccountName(), cmd.getDomainId(), @@ -341,6 +361,10 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable bits, cmd.getHsmProfileId()); + CallContext.current().setEventResourceId(kmsKey.getId()); + CallContext.current().setEventResourceType(ApiCommandResourceType.KmsKey); + CallContext.current().setEventDetails(String.format("Created KMS key: %s (uuid: %s)", kmsKey.getName(), kmsKey.getUuid())); + return createKMSKeyResponse(kmsKey); } @@ -383,7 +407,6 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable return response; } - @ActionEvent(eventType = EventTypes.EVENT_KMS_KEY_CREATE, eventDescription = "creating user KMS key") KMSKey createUserKMSKey(Long accountId, Long domainId, Long zoneId, String name, String description, KeyPurpose purpose, Integer keyBits, long hsmProfileId) throws KMSException { @@ -419,10 +442,6 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable logger.info("Created KMS key ({}) with initial KEK version {} for account {} in zone {} (profile: {})", kmsKey, initialVersion.getVersionNumber(), accountId, zoneId, finalProfileId); - ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), kmsKey.getAccountId(), - EventVO.LEVEL_INFO, EventTypes.EVENT_KMS_KEY_CREATE, - String.format("Created KMS key: %s", kmsKey.getUuid()), - kmsKey.getId(), ApiCommandResourceType.KmsKey.toString(), CallContext.current().getStartEventId()); return kmsKey; } @@ -505,6 +524,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable Account caller = CallContext.current().getCallingAccount(); KMSKeyVO key = findKMSKeyAndCheckAccess(cmd.getId(), caller); KMSKey updatedKey = updateUserKMSKey(key, cmd.getName(), cmd.getDescription(), cmd.getEnabled()); + CallContext.current().setEventDetails(String.format("Updated KMS key: %s (uuid: %s)", updatedKey.getName(), updatedKey.getUuid())); return createKMSKeyResponse(updatedKey); } @@ -536,7 +556,9 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable Account caller = CallContext.current().getCallingAccount(); KMSKeyVO key = findKMSKeyAndCheckAccess(cmd.getId(), caller); - + CallContext.current().setEventResourceId(key.getId()); + CallContext.current().setEventResourceType(ApiCommandResourceType.KmsKey); + CallContext.current().setEventDetails(String.format("Deleted KMS key: %s (uuid: %s)", key.getName(), key.getUuid())); deleteUserKMSKey(key); return new SuccessResponse(); } @@ -566,6 +588,10 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable } catch (Exception e) { logger.warn("Failed to delete KEK {} (v{}) from provider during KMS key deletion: {}", kekVersion.getKekLabel(), kekVersion.getVersionNumber(), e.getMessage()); + ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), + key.getAccountId(), EventVO.LEVEL_WARN, EventTypes.EVENT_KMS_KEY_DELETE, true, + String.format("Failed to delete KEK %s from provider during KMS key deletion", kekVersion.getKekLabel()), + key.getId(), ApiCommandResourceType.KmsKey.toString(), CallContext.current().getStartEventId()); } } @@ -574,7 +600,6 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable } @Override - @ActionEvent(eventType = EventTypes.EVENT_KMS_KEY_ROTATE, eventDescription = "rotating KMS key", async = true) public String rotateKMSKey(RotateKMSKeyCmd cmd) throws KMSException { Account caller = CallContext.current().getCallingAccount(); Integer keyBits = cmd.getKeyBits(); @@ -609,18 +634,13 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable profile); KMSKekVersionVO newVersion = getActiveKekVersion(kmsKey.getId()); + CallContext.current().setEventDetails(String.format("Rotated KMS key: %s (uuid: %s) from version %d to %d", kmsKey.getName(), kmsKey.getUuid(), currentActive.getVersionNumber(), newVersion.getVersionNumber())); logger.info("KMS key rotation initiated: {} -> new KEK version {} (UUID: {}). " + "Background job will gradually rewrap {} wrapped key(s)", kmsKey, newVersion.getVersionNumber(), newVersion.getUuid(), kmsWrappedKeyDao.countByKmsKeyId(kmsKey.getId())); - ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), kmsKey.getAccountId(), - EventVO.LEVEL_INFO, EventTypes.EVENT_KMS_KEY_ROTATE, - String.format("KMS key rotation completed for KMS key from version %d to version %d", - currentActive.getVersionNumber(), newVersion.getVersionNumber()), - kmsKey.getId(), ApiCommandResourceType.KmsKey.toString(), CallContext.current().getStartEventId()); - return newVersion.getUuid(); } @@ -686,6 +706,10 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable } catch (KMSException ex) { logger.error("Failed to delete orphaned KEK {} from provider {} after DB failure: {}", newKekId, provider.getProviderName(), ex.getMessage()); + ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), + kmsKey.getAccountId(), EventVO.LEVEL_WARN, EventTypes.EVENT_KMS_KEY_ROTATE, true, + String.format("Failed to delete orphaned KEK %s from provider after DB rollback failure", newKekId), + kmsKey.getId(), ApiCommandResourceType.KmsKey.toString(), CallContext.current().getStartEventId()); } throw e; } @@ -717,9 +741,6 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable } @Override - @ActionEvent(eventType = EventTypes.EVENT_VOLUME_MIGRATE_TO_KMS, - eventDescription = "migrating volumes to KMS", - async = true) public int migrateVolumesToKMS(MigrateVolumesToKMSCmd cmd) throws KMSException { Account caller = CallContext.current().getCallingAccount(); Long zoneId = cmd.getZoneId(); @@ -826,6 +847,8 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable logger.info("Migration operation completed: {} total volumes processed, {} success, {} failures", successCount + failureCount, successCount, failureCount); + CallContext.current().setEventDetails(String.format("Migrated %d volumes to KMS key: %s (uuid: %s). Success: %d, Failures: %d", successCount + failureCount, kmsKey.getName(), kmsKey.getUuid(), successCount, failureCount)); + return successCount; } @@ -861,6 +884,11 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable volume.setKmsKeyId(kmsKey.getId()); volume.setPassphraseId(null); volumeDao.update(volume.getId(), volume); + + ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), + kmsKey.getAccountId(), EventVO.LEVEL_INFO, EventTypes.EVENT_VOLUME_MIGRATE_TO_KMS, true, + String.format("Successfully migrated volume encryption key to KMS key %s (v%d)", kmsKey.getUuid(), activeVersion.getVersionNumber()), + volume.getId(), ApiCommandResourceType.Volume.toString(), CallContext.current().getStartEventId()); return true; } finally { if (passphraseBytes != null) { @@ -903,6 +931,10 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable } catch (Exception e) { logger.warn("Failed to delete KEK {} from provider: {}", kekVersion.getKekLabel(), e.getMessage()); + ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), + key.getAccountId(), EventVO.LEVEL_WARN, EventTypes.EVENT_KMS_KEY_DELETE, true, + String.format("Failed to delete KEK %s from provider during account KMS cleanup", kekVersion.getKekLabel()), + key.getId(), ApiCommandResourceType.KmsKey.toString(), CallContext.current().getStartEventId()); } } } @@ -938,7 +970,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable @Override @ActionEvent(eventType = EventTypes.EVENT_HSM_PROFILE_CREATE, eventDescription = "Adding HSM profile") - public HSMProfile addHSMProfile(AddHSMProfileCmd cmd) throws KMSException { + public HSMProfile addHSMProfile(CreateHSMProfileCmd cmd) throws KMSException { Account caller = CallContext.current().getCallingAccount(); String protocol = cmd.getProtocol(); @@ -956,8 +988,8 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable Map details = cmd.getDetails() != null ? cmd.getDetails() : new HashMap<>(); provider.validateProfileConfig(details); - boolean isSystem = cmd.isSystem(); - if (isSystem && !accountManager.isRootAdmin(caller.getId())) { + boolean isPublic = cmd.getIsPublic(); + if (isPublic && !accountManager.isRootAdmin(caller.getId())) { throw new PermissionDeniedException("Only root admins can create system HSM profiles"); } @@ -974,7 +1006,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable domainId, cmd.getZoneId(), cmd.getVendorName()); - profile.setSystem(isSystem); + profile.setIsPublic(isPublic); profile = hsmProfileDao.persist(profile); if (cmd.getDetails() != null) { @@ -990,10 +1022,9 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable } } - ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), profile.getAccountId(), - EventVO.LEVEL_INFO, EventTypes.EVENT_HSM_PROFILE_CREATE, - String.format("created HSM profile with id: %s, name: %s", profile.getUuid(), profile.getName()), - profile.getId(), ApiCommandResourceType.HsmProfile.toString(), CallContext.current().getStartEventId()); + CallContext.current().setEventResourceId(profile.getId()); + CallContext.current().setEventResourceType(ApiCommandResourceType.HsmProfile); + CallContext.current().setEventDetails(String.format("Created HSM profile: %s (uuid: %s)", profile.getName(), profile.getUuid())); return profile; } @@ -1035,7 +1066,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable // profiles // If the profile is owned by the user, they should see full details even if it // is a system profile - boolean limited = profile.isSystem() && !isRootAdmin && !(cmd.getIsSystem() || cmd.listAll()) + boolean limited = profile.getIsPublic() && !isRootAdmin && !(cmd.getIsSystem() || cmd.listAll()) && profile.getAccountId() != caller.getId(); responses.add(createHSMProfileResponse(profile, limited)); } @@ -1055,7 +1086,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable sb.and("zoneId", sb.entity().getZoneId(), SearchCriteria.Op.EQ); sb.and("protocol", sb.entity().getProtocol(), SearchCriteria.Op.EQ); sb.and("enabled", sb.entity().isEnabled(), SearchCriteria.Op.EQ); - sb.and("system", sb.entity().isSystem(), SearchCriteria.Op.EQ); + sb.and("system", sb.entity().getIsPublic(), SearchCriteria.Op.EQ); sb.done(); return sb; } @@ -1148,12 +1179,12 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable getKMSProvider(profile.getProtocol()).invalidateProfileCache(profile.getId()); hsmProfileDetailsDao.deleteDetails(profile.getId()); + + CallContext.current().setEventResourceId(profile.getId()); + CallContext.current().setEventResourceType(ApiCommandResourceType.HsmProfile); + CallContext.current().setEventDetails(String.format("Deleted HSM profile: %s (uuid: %s)", profile.getName(), profile.getUuid())); + if (hsmProfileDao.remove(profile.getId())) { - ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), profile.getAccountId(), - EventVO.LEVEL_INFO, EventTypes.EVENT_HSM_PROFILE_DELETE, - String.format("Deleted HSM profile with id: %s, name: %s", profile.getUuid(), profile.getName()), - profile.getId(), ApiCommandResourceType.HsmProfile.toString(), - CallContext.current().getStartEventId()); return true; } return false; @@ -1175,10 +1206,10 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable hsmProfileDao.update(profile.getId(), profile); - ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), profile.getAccountId(), - EventVO.LEVEL_INFO, EventTypes.EVENT_HSM_PROFILE_UPDATE, - String.format("Updated HSM profile with id: %s, name: %s", profile.getUuid(), profile.getName()), - profile.getId(), ApiCommandResourceType.HsmProfile.toString(), CallContext.current().getStartEventId()); + CallContext.current().setEventResourceId(profile.getId()); + CallContext.current().setEventResourceType(ApiCommandResourceType.HsmProfile); + CallContext.current().setEventDetails(String.format("Updated HSM profile: %s (uuid: %s)", profile.getName(), profile.getUuid())); + return profile; } @@ -1192,7 +1223,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable response.setId(profile.getUuid()); response.setName(profile.getName()); response.setVendorName(profile.getVendorName()); - response.setSystem(profile.isSystem()); + response.setIsPublic(profile.getIsPublic()); if (profile.getZoneId() != null) { DataCenterVO zone = dataCenterDao.findById(profile.getZoneId()); @@ -1245,7 +1276,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable * For owned profiles: delegates to ACL checkAccess. */ void checkHSMProfileAccess(Account caller, HSMProfileVO profile, boolean requireModifyAccess) { - if (profile.isSystem()) { + if (profile.getIsPublic()) { if (requireModifyAccess && !accountManager.isRootAdmin(caller.getId())) { throw new PermissionDeniedException("Only root admins can modify system HSM profiles"); } @@ -1502,10 +1533,19 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable provider.deleteKek(oldVersion.getKekLabel()); logger.info("Deleted archived KEK {} (v{}) from provider {}", oldVersion.getKekLabel(), oldVersion.getVersionNumber(), provider.getProviderName()); + + ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), + kmsKey.getAccountId(), EventVO.LEVEL_INFO, EventTypes.EVENT_KMS_KEY_DELETE, true, + String.format("Deleted archived KEK %s from provider after all wrapped keys were rewrapped", oldVersion.getKekLabel()), + kmsKey.getId(), ApiCommandResourceType.KmsKey.toString(), CallContext.current().getStartEventId()); } } catch (Exception e) { logger.warn("Failed to delete archived KEK {} (v{}) from provider: {}", oldVersion.getKekLabel(), oldVersion.getVersionNumber(), e.getMessage()); + ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), + kmsKey.getAccountId(), EventVO.LEVEL_WARN, EventTypes.EVENT_KMS_KEY_DELETE, true, + String.format("Failed to delete archived KEK %s from provider: %s", oldVersion.getKekLabel(), e.getMessage()), + kmsKey.getId(), ApiCommandResourceType.KmsKey.toString(), CallContext.current().getStartEventId()); } return; @@ -1549,6 +1589,11 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable wrappedKeyVO.setKekVersionId(newVersion.getId()); wrappedKeyVO.setWrappedBlob(newWrapped.getWrappedKeyMaterial()); kmsWrappedKeyDao.update(wrappedKeyVO.getId(), wrappedKeyVO); + + ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), kmsKey.getAccountId(), + EventVO.LEVEL_INFO, EventTypes.EVENT_KMS_KEY_WRAP, true, + String.format("Rewrapped %s key (wrapped key uuid: %s) with new KEK version %d", kmsKey.getPurpose().getName(), wrappedKeyVO.getUuid(), newVersion.getVersionNumber()), + kmsKey.getId(), ApiCommandResourceType.KmsKey.toString(), CallContext.current().getStartEventId()); } finally { if (dek != null) { Arrays.fill(dek, (byte) 0); @@ -1592,7 +1637,7 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable cmdList.add(DeleteKMSKeyCmd.class); cmdList.add(RotateKMSKeyCmd.class); cmdList.add(MigrateVolumesToKMSCmd.class); - cmdList.add(AddHSMProfileCmd.class); + cmdList.add(CreateHSMProfileCmd.class); cmdList.add(ListHSMProfilesCmd.class); cmdList.add(UpdateHSMProfileCmd.class); cmdList.add(DeleteHSMProfileCmd.class); diff --git a/server/src/test/java/org/apache/cloudstack/kms/KMSManagerImplAccessTest.java b/server/src/test/java/org/apache/cloudstack/kms/KMSManagerImplAccessTest.java index 35e84ec20ca..a66c41b7d58 100644 --- a/server/src/test/java/org/apache/cloudstack/kms/KMSManagerImplAccessTest.java +++ b/server/src/test/java/org/apache/cloudstack/kms/KMSManagerImplAccessTest.java @@ -220,7 +220,7 @@ public class KMSManagerImplAccessTest { @Test(expected = PermissionDeniedException.class) public void testCheckHSMProfileAccess_DeniesNonRootModifyOfSystemProfile() { HSMProfileVO profile = mock(HSMProfileVO.class); - when(profile.isSystem()).thenReturn(true); + when(profile.getIsPublic()).thenReturn(true); Account caller = mock(Account.class); when(caller.getId()).thenReturn(1L); @@ -232,7 +232,7 @@ public class KMSManagerImplAccessTest { @Test public void testCheckHSMProfileAccess_AllowsRootModifyOfSystemProfile() { HSMProfileVO profile = mock(HSMProfileVO.class); - when(profile.isSystem()).thenReturn(true); + when(profile.getIsPublic()).thenReturn(true); Account caller = mock(Account.class); when(caller.getId()).thenReturn(1L); @@ -244,7 +244,7 @@ public class KMSManagerImplAccessTest { @Test public void testCheckHSMProfileAccess_AllowsReadAccessToSystemProfileForAllUsers() { HSMProfileVO profile = mock(HSMProfileVO.class); - when(profile.isSystem()).thenReturn(true); + when(profile.getIsPublic()).thenReturn(true); kmsManager.checkHSMProfileAccess(mock(Account.class), profile, false); } @@ -252,7 +252,7 @@ public class KMSManagerImplAccessTest { @Test public void testCheckHSMProfileAccess_DelegatesToAclForOwnedProfile() { HSMProfileVO profile = mock(HSMProfileVO.class); - when(profile.isSystem()).thenReturn(false); + when(profile.getIsPublic()).thenReturn(false); kmsManager.checkHSMProfileAccess(mock(Account.class), profile, true); } @@ -260,7 +260,7 @@ public class KMSManagerImplAccessTest { @Test(expected = PermissionDeniedException.class) public void testCheckHSMProfileAccess_ThrowsWhenAclDeniesOwnedProfile() { HSMProfileVO profile = mock(HSMProfileVO.class); - when(profile.isSystem()).thenReturn(false); + when(profile.getIsPublic()).thenReturn(false); Account caller = mock(Account.class); doThrow(new PermissionDeniedException("denied")) diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index 02c0a720f28..cf0443708e9 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -56,8 +56,8 @@ known_categories = { 'HypervisorGuestOsNames': 'Guest OS', 'Domain': 'Domain', 'Template': 'Template', - 'KMS': 'KMS', - 'HSM': 'KMS', + 'KMS': 'Key Management System', + 'HSM': 'Key Management System', 'Iso': 'ISO', 'Volume': 'Volume', 'Vlan': 'VLAN', diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index e442d21f3ec..942ac1a0c95 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -2515,7 +2515,6 @@ "label.suspend.project": "Suspend Project", "label.switch.type": "Switch type", "label.sync.storage": "Sync Storage Pool", -"label.system": "System", "label.system.ip.pool": "System Pool", "label.system.offering": "System Offering", "label.system.offerings": "System Offerings", diff --git a/ui/src/config/section/kms.js b/ui/src/config/section/kms.js index 31fc09d474e..6f4c6dd0422 100644 --- a/ui/src/config/section/kms.js +++ b/ui/src/config/section/kms.js @@ -77,6 +77,7 @@ export default { api: 'createKMSKey', icon: 'plus-outlined', label: 'label.create.kms.key', + docHelp: 'adminguide/kms.html#creating-a-kms-key', listView: true, popup: true, dataView: false, @@ -89,7 +90,6 @@ export default { { api: 'updateKMSKey', icon: 'edit-outlined', - docHelp: 'adminguide/storage.html#lifecycle-operations', label: 'label.update.kms.key', dataView: true, popup: true, @@ -103,7 +103,7 @@ export default { { api: 'rotateKMSKey', icon: 'sync-outlined', - docHelp: 'adminguide/storage.html#lifecycle-operations', + docHelp: 'adminguide/kms.html#rotating-a-kms-key', label: 'label.rotate.kms.key', dataView: true, popup: true, @@ -117,7 +117,7 @@ export default { { api: 'migrateVolumesToKMS', icon: 'swap-outlined', - docHelp: 'adminguide/storage.html#lifecycle-operations', + docHelp: 'adminguide/kms.html#migrating-existing-volumes-to-kms', label: 'label.migrate.volumes.to.kms', message: 'message.action.migrate.volumes.to.kms', dataView: true, @@ -141,7 +141,6 @@ export default { { api: 'deleteKMSKey', icon: 'delete-outlined', - docHelp: 'adminguide/storage.html#lifecycle-operations', label: 'label.delete.kms.key', message: 'message.action.delete.kms.key', dataView: true, @@ -204,8 +203,9 @@ export default { }, actions: [ { - api: 'addHSMProfile', + api: 'createHSMProfile', icon: 'plus-outlined', + docHelp: 'adminguide/kms.html#adding-an-hsm-profile', label: 'label.create.hsmprofile', listView: true, popup: true, @@ -227,7 +227,6 @@ export default { { api: 'updateHSMProfile', icon: 'edit-outlined', - docHelp: 'adminguide/storage.html#lifecycle-operations', label: 'label.update.hsm.profile', dataView: true, popup: true, @@ -244,7 +243,6 @@ export default { { api: 'deleteHSMProfile', icon: 'delete-outlined', - docHelp: 'adminguide/storage.html#lifecycle-operations', label: 'label.delete.hsm.profile', message: 'message.action.delete.hsm.profile', dataView: true, diff --git a/ui/src/config/section/storage.js b/ui/src/config/section/storage.js index 964c025ab5f..14f3cf821dc 100644 --- a/ui/src/config/section/storage.js +++ b/ui/src/config/section/storage.js @@ -224,7 +224,7 @@ export default { { api: 'migrateVolumesToKMS', icon: 'lock-outlined', - docHelp: 'adminguide/storage.html#lifecycle-operations', + docHelp: 'adminguide/kms.html#migrating-existing-volumes-to-kms', label: 'label.migrate.volume.to.kms', message: 'message.action.migrate.volume.to.kms', dataView: true,