fix list apis

This commit is contained in:
vishesh92 2026-02-17 12:47:32 +05:30
parent 529fd9d661
commit 6ba0af5ceb
No known key found for this signature in database
GPG Key ID: 4E395186CBFA790B
72 changed files with 2617 additions and 2223 deletions

View File

@ -36,6 +36,8 @@ import org.apache.cloudstack.gpu.GpuCard;
import org.apache.cloudstack.gpu.GpuDevice;
import org.apache.cloudstack.gpu.VgpuProfile;
import org.apache.cloudstack.ha.HAConfig;
import org.apache.cloudstack.kms.HSMProfile;
import org.apache.cloudstack.kms.KMSKey;
import org.apache.cloudstack.network.BgpPeer;
import org.apache.cloudstack.network.Ipv4GuestSubnetNetworkMap;
import org.apache.cloudstack.quota.QuotaTariff;
@ -274,12 +276,17 @@ public class EventTypes {
// KMS (Key Management Service) events
public static final String EVENT_KMS_KEY_WRAP = "KMS.KEY.WRAP";
public static final String EVENT_KMS_KEY_UNWRAP = "KMS.KEY.UNWRAP";
public static final String EVENT_KMS_KEK_CREATE = "KMS.KEK.CREATE";
public static final String EVENT_KMS_KEK_ROTATE = "KMS.KEK.ROTATE";
public static final String EVENT_KMS_KEK_DELETE = "KMS.KEK.DELETE";
public static final String EVENT_KMS_HEALTH_CHECK = "KMS.HEALTH.CHECK";
public static final String EVENT_KMS_KEY_CREATE = "KMS.KEY.CREATE";
public static final String EVENT_KMS_KEY_UPDATE = "KMS.KEY.UPDATE";
public static final String EVENT_KMS_KEY_ROTATE = "KMS.KEY.ROTATE";
public static final String EVENT_KMS_KEY_DELETE = "KMS.KEY.DELETE";
public static final String EVENT_VOLUME_MIGRATE_TO_KMS = "VOLUME.MIGRATE.TO.KMS";
// HSM Profile events
public static final String EVENT_HSM_PROFILE_CREATE = "HSM.PROFILE.CREATE";
public static final String EVENT_HSM_PROFILE_UPDATE = "HSM.PROFILE.UPDATE";
public static final String EVENT_HSM_PROFILE_DELETE = "HSM.PROFILE.DELETE";
// Account events
public static final String EVENT_ACCOUNT_ENABLE = "ACCOUNT.ENABLE";
public static final String EVENT_ACCOUNT_DISABLE = "ACCOUNT.DISABLE";
@ -1024,6 +1031,19 @@ public class EventTypes {
entityEventDetails.put(EVENT_VOLUME_RECOVER, Volume.class);
entityEventDetails.put(EVENT_VOLUME_CHANGE_DISK_OFFERING, Volume.class);
// KMS Key Events
entityEventDetails.put(EVENT_KMS_KEY_CREATE, KMSKey.class);
entityEventDetails.put(EVENT_KMS_KEY_UPDATE, KMSKey.class);
entityEventDetails.put(EVENT_KMS_KEY_UNWRAP, KMSKey.class);
entityEventDetails.put(EVENT_KMS_KEY_WRAP, KMSKey.class);
entityEventDetails.put(EVENT_KMS_KEY_DELETE, KMSKey.class);
entityEventDetails.put(EVENT_KMS_KEY_ROTATE, KMSKey.class);
// HSM Profile Events
entityEventDetails.put(EVENT_HSM_PROFILE_CREATE, HSMProfile.class);
entityEventDetails.put(EVENT_HSM_PROFILE_UPDATE, HSMProfile.class);
entityEventDetails.put(EVENT_HSM_PROFILE_DELETE, HSMProfile.class);
// Domains
entityEventDetails.put(EVENT_DOMAIN_CREATE, Domain.class);
entityEventDetails.put(EVENT_DOMAIN_DELETE, Domain.class);

View File

@ -90,7 +90,8 @@ public enum ApiCommandResourceType {
SharedFS(org.apache.cloudstack.storage.sharedfs.SharedFS.class),
Extension(org.apache.cloudstack.extension.Extension.class),
ExtensionCustomAction(org.apache.cloudstack.extension.ExtensionCustomAction.class),
KmsKey(org.apache.cloudstack.kms.KMSKey.class);
KmsKey(org.apache.cloudstack.kms.KMSKey.class),
HsmProfile(org.apache.cloudstack.kms.HSMProfile.class);
private final Class<?> clazz;

View File

@ -870,6 +870,7 @@ public class ApiConstants {
public static final String HSM_PROFILE = "hsmprofile";
public static final String HSM_PROFILE_ID = "hsmprofileid";
public static final String PURPOSE = "purpose";
public static final String KMS_KEY = "kmskey";
public static final String KMS_KEY_ID = "kmskeyid";
public static final String KMS_KEY_VERSION = "kmskeyversion";
public static final String KEK_LABEL = "keklabel";

View File

@ -23,6 +23,7 @@ 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;
@ -76,8 +77,6 @@ import org.apache.cloudstack.api.response.HypervisorGuestOsNamesResponse;
import org.apache.cloudstack.api.response.IPAddressResponse;
import org.apache.cloudstack.api.response.ImageStoreResponse;
import org.apache.cloudstack.api.response.InstanceGroupResponse;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.kms.KMSKey;
import org.apache.cloudstack.api.response.InternalLoadBalancerElementResponse;
import org.apache.cloudstack.api.response.IpForwardingRuleResponse;
import org.apache.cloudstack.api.response.IpQuarantineResponse;
@ -158,6 +157,7 @@ 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;
@ -283,7 +283,8 @@ public interface ResponseGenerator {
List<UserVmResponse> createUserVmResponse(ResponseView view, String objectName, UserVm... userVms);
List<UserVmResponse> createUserVmResponse(ResponseView view, String objectName, EnumSet<VMDetails> details, UserVm... userVms);
List<UserVmResponse> createUserVmResponse(ResponseView view, String objectName, EnumSet<VMDetails> details,
UserVm... userVms);
SystemVmResponse createSystemVmResponse(VirtualMachine systemVM);
@ -309,11 +310,13 @@ public interface ResponseGenerator {
LoadBalancerResponse createLoadBalancerResponse(LoadBalancer loadBalancer);
LBStickinessResponse createLBStickinessPolicyResponse(List<? extends StickinessPolicy> stickinessPolicies, LoadBalancer lb);
LBStickinessResponse createLBStickinessPolicyResponse(List<? extends StickinessPolicy> stickinessPolicies,
LoadBalancer lb);
LBStickinessResponse createLBStickinessPolicyResponse(StickinessPolicy stickinessPolicy, LoadBalancer lb);
LBHealthCheckResponse createLBHealthCheckPolicyResponse(List<? extends HealthCheckPolicy> healthcheckPolicies, LoadBalancer lb);
LBHealthCheckResponse createLBHealthCheckPolicyResponse(List<? extends HealthCheckPolicy> healthcheckPolicies,
LoadBalancer lb);
LBHealthCheckResponse createLBHealthCheckPolicyResponse(HealthCheckPolicy healthcheckPolicy, LoadBalancer lb);
@ -321,7 +324,8 @@ public interface ResponseGenerator {
PodResponse createMinimalPodResponse(Pod pod);
ZoneResponse createZoneResponse(ResponseView view, DataCenter dataCenter, Boolean showCapacities, Boolean showResourceIcon);
ZoneResponse createZoneResponse(ResponseView view, DataCenter dataCenter, Boolean showCapacities,
Boolean showResourceIcon);
DataCenterGuestIpv6PrefixResponse createDataCenterGuestIpv6PrefixResponse(DataCenterGuestIpv6Prefix prefix);
@ -361,7 +365,8 @@ public interface ResponseGenerator {
List<TemplateResponse> createTemplateResponses(ResponseView view, long templateId, Long zoneId, boolean readyOnly);
List<TemplateResponse> createTemplateResponses(ResponseView view, long templateId, Long snapshotId, Long volumeId, boolean readyOnly);
List<TemplateResponse> createTemplateResponses(ResponseView view, long templateId, Long snapshotId, Long volumeId,
boolean readyOnly);
SecurityGroupResponse createSecurityGroupResponseFromSecurityGroupRule(List<? extends SecurityRule> securityRules);
@ -380,14 +385,15 @@ public interface ResponseGenerator {
TemplateResponse createTemplateUpdateResponse(ResponseView view, VirtualMachineTemplate result);
List<TemplateResponse> createTemplateResponses(ResponseView view, VirtualMachineTemplate result,
Long zoneId, boolean readyOnly);
Long zoneId, boolean readyOnly);
List<TemplateResponse> createTemplateResponses(ResponseView view, VirtualMachineTemplate result,
List<Long> zoneIds, boolean readyOnly);
List<Long> zoneIds, boolean readyOnly);
List<CapacityResponse> createCapacityResponse(List<? extends Capacity> result, DecimalFormat format);
TemplatePermissionsResponse createTemplatePermissionsResponse(ResponseView view, List<String> accountNames, Long id);
TemplatePermissionsResponse createTemplatePermissionsResponse(ResponseView view, List<String> accountNames,
Long id);
AsyncJobResponse queryJobResult(QueryAsyncJobResultCmd cmd);
@ -401,7 +407,8 @@ public interface ResponseGenerator {
Long getSecurityGroupId(String groupName, long accountId);
List<TemplateResponse> createIsoResponses(ResponseView view, VirtualMachineTemplate iso, Long zoneId, boolean readyOnly);
List<TemplateResponse> createIsoResponses(ResponseView view, VirtualMachineTemplate iso, Long zoneId,
boolean readyOnly);
ProjectResponse createProjectResponse(Project project);
@ -502,13 +509,15 @@ public interface ResponseGenerator {
GuestOsMappingResponse createGuestOSMappingResponse(GuestOSHypervisor osHypervisor);
HypervisorGuestOsNamesResponse createHypervisorGuestOSNamesResponse(List<Pair<String, String>> hypervisorGuestOsNames);
HypervisorGuestOsNamesResponse createHypervisorGuestOSNamesResponse(
List<Pair<String, String>> hypervisorGuestOsNames);
SnapshotScheduleResponse createSnapshotScheduleResponse(SnapshotSchedule sched);
UsageRecordResponse createUsageResponse(Usage usageRecord);
UsageRecordResponse createUsageResponse(Usage usageRecord, Map<String, Set<ResourceTagResponse>> resourceTagResponseMap, boolean oldFormat);
UsageRecordResponse createUsageResponse(Usage usageRecord,
Map<String, Set<ResourceTagResponse>> resourceTagResponseMap, boolean oldFormat);
public Map<String, Set<ResourceTagResponse>> getUsageResourceTags();
@ -520,7 +529,8 @@ public interface ResponseGenerator {
public NicResponse createNicResponse(Nic result);
ApplicationLoadBalancerResponse createLoadBalancerContainerReponse(ApplicationLoadBalancerRule lb, Map<Ip, UserVm> lbInstances);
ApplicationLoadBalancerResponse createLoadBalancerContainerReponse(ApplicationLoadBalancerRule lb,
Map<Ip, UserVm> lbInstances);
AffinityGroupResponse createAffinityGroupResponse(AffinityGroup group);
@ -546,9 +556,12 @@ public interface ResponseGenerator {
ManagementServerResponse createManagementResponse(ManagementServerHost mgmt);
List<RouterHealthCheckResultResponse> createHealthCheckResponse(VirtualMachine router, List<RouterHealthCheckResult> healthCheckResults);
List<RouterHealthCheckResultResponse> createHealthCheckResponse(VirtualMachine router,
List<RouterHealthCheckResult> healthCheckResults);
RollingMaintenanceResponse createRollingMaintenanceResponse(Boolean success, String details, List<RollingMaintenanceManager.HostUpdated> hostsUpdated, List<RollingMaintenanceManager.HostSkipped> hostsSkipped);
RollingMaintenanceResponse createRollingMaintenanceResponse(Boolean success, String details,
List<RollingMaintenanceManager.HostUpdated> hostsUpdated,
List<RollingMaintenanceManager.HostSkipped> hostsSkipped);
ResourceIconResponse createResourceIconResponse(ResourceIcon resourceIcon);
@ -558,11 +571,14 @@ public interface ResponseGenerator {
DirectDownloadCertificateResponse createDirectDownloadCertificateResponse(DirectDownloadCertificate certificate);
List<DirectDownloadCertificateHostStatusResponse> createDirectDownloadCertificateHostMapResponse(List<DirectDownloadCertificateHostMap> hostMappings);
List<DirectDownloadCertificateHostStatusResponse> createDirectDownloadCertificateHostMapResponse(
List<DirectDownloadCertificateHostMap> hostMappings);
DirectDownloadCertificateHostStatusResponse createDirectDownloadCertificateHostStatusResponse(DirectDownloadManager.HostCertificateStatus status);
DirectDownloadCertificateHostStatusResponse createDirectDownloadCertificateHostStatusResponse(
DirectDownloadManager.HostCertificateStatus status);
DirectDownloadCertificateHostStatusResponse createDirectDownloadCertificateProvisionResponse(Long certificateId, Long hostId, Pair<Boolean, String> result);
DirectDownloadCertificateHostStatusResponse createDirectDownloadCertificateProvisionResponse(Long certificateId,
Long hostId, Pair<Boolean, String> result);
FirewallResponse createIpv6FirewallRuleResponse(FirewallRule acl);

View File

@ -17,7 +17,6 @@
package org.apache.cloudstack.api.command.admin.kms;
import com.cloud.dc.DataCenter;
import com.cloud.user.Account;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
@ -29,11 +28,15 @@ import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.AsyncJobResponse;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.VolumeResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.kms.KMSKey;
import org.apache.cloudstack.kms.KMSManager;
import javax.inject.Inject;
import java.util.List;
@APICommand(name = "migrateVolumesToKMS",
description = "Migrates passphrase-based volumes to KMS (admin only)",
@ -48,12 +51,7 @@ public class MigrateVolumesToKMSCmd extends BaseAsyncCmd {
@Inject
private KMSManager kmsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ZONE_ID,
required = true,
type = CommandType.UUID,
entityType = ZoneResponse.class,
description = "Zone ID")
@ -70,17 +68,20 @@ public class MigrateVolumesToKMSCmd extends BaseAsyncCmd {
description = "Domain ID")
private Long domainId;
@Parameter(name = ApiConstants.ID,
@Parameter(name = ApiConstants.VOLUME_IDS,
type = CommandType.LIST,
collectionType = CommandType.UUID,
entityType = VolumeResponse.class,
description = "List of volume IDs to migrate")
private List<Long> volumeIds;
@Parameter(name = ApiConstants.KMS_KEY_ID,
required = true,
type = CommandType.UUID,
entityType = KMSKeyResponse.class,
description = "KMS Key ID to use for migrating volumes")
private Long kmsKeyId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getZoneId() {
return zoneId;
}
@ -93,14 +94,14 @@ public class MigrateVolumesToKMSCmd extends BaseAsyncCmd {
return domainId;
}
public List<Long> getVolumeIds() {
return volumeIds;
}
public Long getKmsKeyId() {
return kmsKeyId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
try {
@ -118,7 +119,11 @@ public class MigrateVolumesToKMSCmd extends BaseAsyncCmd {
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
KMSKey key = _entityMgr.findById(KMSKey.class, kmsKeyId);
if (key != null) {
return key.getAccountId();
}
return CallContext.current().getCallingAccount().getId();
}
@Override

View File

@ -20,7 +20,6 @@
package org.apache.cloudstack.api.command.user.kms;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.user.Account;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
@ -33,6 +32,7 @@ import org.apache.cloudstack.api.command.user.UserCmd;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.HSMProfileResponse;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.ProjectResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.kms.KMSException;
@ -64,9 +64,8 @@ public class CreateKMSKeyCmd extends BaseCmd implements UserCmd {
private String description;
@Parameter(name = ApiConstants.PURPOSE,
required = true,
type = CommandType.STRING,
description = "Purpose of the key: volume, tls")
description = "Purpose of the key: volume, tls. (default: volume)")
private String purpose;
@Parameter(name = ApiConstants.ZONE_ID,
@ -87,6 +86,12 @@ public class CreateKMSKeyCmd extends BaseCmd implements UserCmd {
description = "Domain ID (for creating keys for child accounts - requires domain admin or admin)")
private Long domainId;
@Parameter(name = ApiConstants.PROJECT_ID,
type = CommandType.UUID,
entityType = ProjectResponse.class,
description = "ID of the project to create the KMS key for")
private Long projectId;
@Parameter(name = ApiConstants.KEY_BITS,
type = CommandType.INTEGER,
description = "Key size in bits: 128, 192, or 256 (default: 256)")
@ -108,7 +113,7 @@ public class CreateKMSKeyCmd extends BaseCmd implements UserCmd {
}
public String getPurpose() {
return purpose;
return purpose == null ? "volume" : purpose;
}
public Long getZoneId() {
@ -123,8 +128,12 @@ public class CreateKMSKeyCmd extends BaseCmd implements UserCmd {
return domainId;
}
public Long getProjectId() {
return projectId;
}
public Integer getKeyBits() {
return keyBits != null ? keyBits : 256; // Default to 256 bits
return keyBits != null ? keyBits : 256;
}
public Long getHsmProfileId() {
@ -145,11 +154,11 @@ public class CreateKMSKeyCmd extends BaseCmd implements UserCmd {
@Override
public long getEntityOwnerId() {
Account caller = CallContext.current().getCallingAccount();
if (accountName != null || domainId != null) {
return _accountService.finalyzeAccountId(accountName, domainId, null, true);
Long accountId = _accountService.finalizeAccountId(accountName, domainId, projectId, true);
if (accountId != null) {
return accountId;
}
return caller.getId();
return CallContext.current().getCallingAccount().getId();
}
@Override

View File

@ -33,6 +33,7 @@ import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.kms.KMSKey;
import org.apache.cloudstack.kms.KMSManager;
import javax.inject.Inject;
@ -56,10 +57,6 @@ public class DeleteKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
description = "The UUID of the KMS key to delete")
private Long id;
public Long getId() {
return id;
}
@Override
public void execute() {
try {
@ -74,12 +71,21 @@ public class DeleteKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
@Override
public long getEntityOwnerId() {
KMSKey key = _entityMgr.findById(KMSKey.class, id);
if (key != null) {
return key.getAccountId();
}
return CallContext.current().getCallingAccount().getId();
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.KmsKey;
}
@Override
public String getEventType() {
return EventTypes.EVENT_KMS_KEK_DELETE;
return EventTypes.EVENT_KMS_KEY_DELETE;
}
@Override
@ -87,8 +93,7 @@ public class DeleteKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
return "deleting KMS key: " + getId();
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.KmsKey;
public Long getId() {
return id;
}
}

View File

@ -22,10 +22,11 @@ package org.apache.cloudstack.api.command.user.kms;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseListAccountResourcesCmd;
import org.apache.cloudstack.api.BaseListProjectAndAccountResourcesCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.command.user.UserCmd;
import org.apache.cloudstack.api.response.HSMProfileResponse;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
@ -41,7 +42,7 @@ import javax.inject.Inject;
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
public class ListKMSKeysCmd extends BaseListAccountResourcesCmd implements UserCmd {
public class ListKMSKeysCmd extends BaseListProjectAndAccountResourcesCmd implements UserCmd {
private static final String s_name = "listkmskeysresponse";
@Inject
@ -64,10 +65,16 @@ public class ListKMSKeysCmd extends BaseListAccountResourcesCmd implements UserC
description = "Filter by zone ID")
private Long zoneId;
@Parameter(name = ApiConstants.STATE,
type = CommandType.STRING,
description = "Filter by state: Enabled, Disabled")
private String state;
@Parameter(name = ApiConstants.ENABLED,
type = CommandType.BOOLEAN,
description = "Filter by enabled status")
private Boolean enabled;
@Parameter(name = ApiConstants.HSM_PROFILE_ID,
type = CommandType.UUID,
entityType = HSMProfileResponse.class,
description = "Filter by HSM profile ID")
private Long hsmProfileId;
public Long getId() {
return id;
@ -81,13 +88,13 @@ public class ListKMSKeysCmd extends BaseListAccountResourcesCmd implements UserC
return zoneId;
}
public String getState() {
return state;
public Boolean getEnabled() {
return enabled;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
public Long getHsmProfileId() {
return hsmProfileId;
}
@Override
public void execute() {

View File

@ -14,9 +14,8 @@
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.command.admin.kms;
package org.apache.cloudstack.api.command.user.kms;
import com.cloud.user.Account;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
@ -29,6 +28,7 @@ import org.apache.cloudstack.api.response.AsyncJobResponse;
import org.apache.cloudstack.api.response.HSMProfileResponse;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.kms.KMSKey;
import org.apache.cloudstack.kms.KMSManager;
@ -36,10 +36,10 @@ 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 (admin only)",
description = "Rotates KEK by creating new version and scheduling gradual re-encryption",
responseObject = AsyncJobResponse.class,
since = "4.23.0",
authorized = {RoleType.Admin},
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
public class RotateKMSKeyCmd extends BaseAsyncCmd {
@ -61,10 +61,11 @@ public class RotateKMSKeyCmd extends BaseAsyncCmd {
private Integer keyBits;
@Parameter(name = ApiConstants.HSM_PROFILE_ID,
type = CommandType.UUID,
entityType = HSMProfileResponse.class,
description = "The target HSM profile ID for the new KEK version. If provided, migrates the key to this HSM.")
private String hsmProfile;
type = CommandType.UUID,
entityType = HSMProfileResponse.class,
description = "The target HSM profile ID for the new KEK version. If provided, migrates the key to "
+ "this HSM.")
private Long hsmProfileId;
public Long getId() {
return id;
@ -74,8 +75,8 @@ public class RotateKMSKeyCmd extends BaseAsyncCmd {
return keyBits;
}
public String getHsmProfile() {
return hsmProfile;
public Long getHsmProfileId() {
return hsmProfileId;
}
@Override
@ -98,12 +99,16 @@ public class RotateKMSKeyCmd extends BaseAsyncCmd {
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
KMSKey key = _entityMgr.findById(KMSKey.class, id);
if (key != null) {
return key.getAccountId();
}
return CallContext.current().getCallingAccount().getId();
}
@Override
public String getEventType() {
return com.cloud.event.EventTypes.EVENT_KMS_KEK_ROTATE;
return com.cloud.event.EventTypes.EVENT_KMS_KEY_ROTATE;
}
@Override

View File

@ -37,7 +37,7 @@ import org.apache.cloudstack.kms.KMSManager;
import javax.inject.Inject;
@APICommand(name = "updateKMSKey",
description = "Updates KMS key name, description, or state",
description = "Updates KMS key name, description, or enabled status",
responseObject = KMSKeyResponse.class,
since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
@ -65,14 +65,10 @@ public class UpdateKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
description = "New description for the key")
private String description;
@Parameter(name = ApiConstants.STATE,
type = CommandType.STRING,
description = "New state: Enabled or Disabled")
private String state;
public Long getId() {
return id;
}
@Parameter(name = ApiConstants.ENABLED,
type = CommandType.BOOLEAN,
description = "whether the key should be enabled")
private Boolean enabled;
public String getName() {
return name;
@ -82,8 +78,8 @@ public class UpdateKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
return description;
}
public String getState() {
return state;
public Boolean getEnabled() {
return enabled;
}
@Override
@ -103,9 +99,14 @@ public class UpdateKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
return CallContext.current().getCallingAccount().getId();
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.KmsKey;
}
@Override
public String getEventType() {
return EventTypes.EVENT_KMS_KEK_CREATE; // Reuse create event type for updates
return EventTypes.EVENT_KMS_KEY_UPDATE;
}
@Override
@ -113,8 +114,7 @@ public class UpdateKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
return "updating KMS key: " + getId();
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.KmsKey;
public Long getId() {
return id;
}
}

View File

@ -23,15 +23,16 @@ import com.cloud.exception.NetworkRuleConflictException;
import com.cloud.exception.ResourceAllocationException;
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.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.AccountResponse;
import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.HSMProfileResponse;
import org.apache.cloudstack.api.response.ProjectResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.kms.KMSException;
@ -45,37 +46,47 @@ import java.util.HashMap;
import java.util.Map;
@APICommand(name = "addHSMProfile", description = "Adds a new HSM profile", responseObject = HSMProfileResponse.class,
requestHasSensitiveInfo = true, responseHasSensitiveInfo = true, since = "4.23.0")
requestHasSensitiveInfo = true, responseHasSensitiveInfo = true, since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class AddHSMProfileCmd extends BaseCmd {
@Inject
private KMSManager kmsManager;
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true,
description = "the name of the HSM profile")
description = "the name of the HSM profile")
private String name;
@Parameter(name = ApiConstants.PROTOCOL, type = CommandType.STRING,
description = "the protocol of the HSM profile (PKCS11, KMIP, etc.). Default is 'pkcs11'")
description = "the protocol of the HSM profile (PKCS11, KMIP, etc.). Default is 'pkcs11'")
private String protocol;
@Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class,
description = "the zone ID where the HSM profile is available. If null, global scope (for admin only)")
description = "the zone ID where the HSM profile is available. If null, global scope (for admin only)")
private Long zoneId;
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class,
description = "the domain ID where the HSM profile is available")
description = "the domain ID where the HSM profile is available")
private Long domainId;
@Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class,
description = "the account ID of the HSM profile owner. If null, admin-provided (available to all "
+ "accounts)")
private Long accountId;
@Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING,
description = "the account name of the HSM profile owner. Must be used with domainId.")
private String accountName;
@Parameter(name = ApiConstants.PROJECT_ID, type = CommandType.UUID, entityType = ProjectResponse.class,
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). "
+ "Default is false")
private Boolean system;
@Parameter(name = ApiConstants.VENDOR_NAME, type = CommandType.STRING, description = "the vendor name of the HSM")
private String vendorName;
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, description = "HSM configuration details (protocol specific)")
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP,
description = "HSM configuration details (protocol specific)")
private Map<String, String> details;
public String getName() {
@ -97,8 +108,16 @@ public class AddHSMProfileCmd extends BaseCmd {
return domainId;
}
public Long getAccountId() {
return accountId;
public String getAccountName() {
return accountName;
}
public Long getProjectId() {
return projectId;
}
public Boolean isSystem() {
return system != null && system;
}
public String getVendorName() {
@ -111,8 +130,8 @@ public class AddHSMProfileCmd extends BaseCmd {
Collection<?> props = details.values();
for (Object prop : props) {
HashMap<String, String> detail = (HashMap<String, String>) prop;
for (Map.Entry<String, String> entry: detail.entrySet()) {
detailsMap.put(entry.getKey(),entry.getValue());
for (Map.Entry<String, String> entry : detail.entrySet()) {
detailsMap.put(entry.getKey(), entry.getValue());
}
}
}
@ -123,12 +142,6 @@ public class AddHSMProfileCmd extends BaseCmd {
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException,
ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
try {
// Default to caller account if not admin and accountId not specified
// But wait, the plan says: "No accountId parameter means account_id = NULL (admin-provided)"
// However, regular users can add their own profiles.
// So if caller is normal user, accountId should be forced to their account.
// Logic handled in KMSManagerImpl
HSMProfile profile = kmsManager.addHSMProfile(this);
HSMProfileResponse response = kmsManager.createHSMProfileResponse(profile);
response.setResponseName(getCommandName());
@ -140,6 +153,7 @@ public class AddHSMProfileCmd extends BaseCmd {
@Override
public long getEntityOwnerId() {
Long accountId = _accountService.finalizeAccountId(accountName, domainId, projectId, true);
if (accountId != null) {
return accountId;
}

View File

@ -17,8 +17,12 @@
package org.apache.cloudstack.api.command.user.kms.hsm;
import javax.inject.Inject;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.NetworkRuleConflictException;
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.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
@ -32,20 +36,18 @@ import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.kms.HSMProfile;
import org.apache.cloudstack.kms.KMSManager;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.NetworkRuleConflictException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import javax.inject.Inject;
@APICommand(name = "deleteHSMProfile", description = "Deletes an HSM profile", responseObject = SuccessResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0")
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class DeleteHSMProfileCmd extends BaseCmd {
@Inject
private KMSManager kmsManager;
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = HSMProfileResponse.class, required = true, description = "the ID of the HSM profile")
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = HSMProfileResponse.class, required = true,
description = "the ID of the HSM profile")
private Long id;
public Long getId() {
@ -53,7 +55,8 @@ public class DeleteHSMProfileCmd extends BaseCmd {
}
@Override
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException,
ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
try {
boolean result = kmsManager.deleteHSMProfile(this);
if (result) {
@ -70,7 +73,7 @@ public class DeleteHSMProfileCmd extends BaseCmd {
@Override
public long getEntityOwnerId() {
HSMProfile profile = _entityMgr.findById(HSMProfile.class, id);
if (profile != null && profile.getAccountId() != null) {
if (profile != null && profile.getAccountId() > 0) {
return profile.getAccountId();
}
return CallContext.current().getCallingAccount().getId();

View File

@ -17,32 +17,32 @@
package org.apache.cloudstack.api.command.user.kms.hsm;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.BaseListProjectAndAccountResourcesCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.HSMProfileResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.kms.HSMProfile;
import org.apache.cloudstack.kms.KMSManager;
import javax.inject.Inject;
@APICommand(name = "listHSMProfiles", description = "Lists HSM profiles", responseObject = HSMProfileResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = true, since = "4.23.0")
public class ListHSMProfilesCmd extends BaseListCmd {
requestHasSensitiveInfo = false, responseHasSensitiveInfo = true, since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class ListHSMProfilesCmd extends BaseListProjectAndAccountResourcesCmd {
@Inject
private KMSManager kmsManager;
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = HSMProfileResponse.class, description = "the HSM profile ID")
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = HSMProfileResponse.class,
description = "the HSM profile ID")
private Long id;
@Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, description = "the zone ID")
@Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class,
description = "the zone ID")
private Long zoneId;
@Parameter(name = ApiConstants.PROTOCOL, type = CommandType.STRING, description = "the protocol of the HSM profile")
@ -51,6 +51,11 @@ public class ListHSMProfilesCmd extends BaseListCmd {
@Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "list only enabled profiles")
private Boolean enabled;
@Parameter(name = ApiConstants.IS_SYSTEM,
type = CommandType.BOOLEAN,
description = "when true, non-admin users see only system (global) profiles")
private Boolean isSystem;
public Long getId() {
return id;
}
@ -67,18 +72,13 @@ public class ListHSMProfilesCmd extends BaseListCmd {
return enabled;
}
public Boolean getIsSystem() {
return isSystem;
}
@Override
public void execute() {
List<HSMProfile> profiles = kmsManager.listHSMProfiles(this);
ListResponse<HSMProfileResponse> response = new ListResponse<>();
List<HSMProfileResponse> profileResponses = new ArrayList<>();
for (HSMProfile profile : profiles) {
HSMProfileResponse profileResponse = kmsManager.createHSMProfileResponse(profile);
profileResponses.add(profileResponse);
}
response.setResponses(profileResponses);
ListResponse<HSMProfileResponse> response = kmsManager.listHSMProfiles(this);
response.setResponseName(getCommandName());
setResponseObject(response);
}

View File

@ -17,10 +17,12 @@
package org.apache.cloudstack.api.command.user.kms.hsm;
import java.util.Map;
import javax.inject.Inject;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.NetworkRuleConflictException;
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.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
@ -33,31 +35,28 @@ import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.kms.HSMProfile;
import org.apache.cloudstack.kms.KMSManager;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.NetworkRuleConflictException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import javax.inject.Inject;
@APICommand(name = "updateHSMProfile", description = "Updates an HSM profile", responseObject = HSMProfileResponse.class,
requestHasSensitiveInfo = true, responseHasSensitiveInfo = true, since = "4.23.0")
@APICommand(name = "updateHSMProfile", description = "Updates an HSM profile",
responseObject = HSMProfileResponse.class,
requestHasSensitiveInfo = true, responseHasSensitiveInfo = true, since = "4.23.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
public class UpdateHSMProfileCmd extends BaseCmd {
@Inject
private KMSManager kmsManager;
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = HSMProfileResponse.class, required = true, description = "the ID of the HSM profile")
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = HSMProfileResponse.class, required = true,
description = "the ID of the HSM profile")
private Long id;
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "the name of the HSM profile")
private String name;
@Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "whether the HSM profile is enabled")
@Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN,
description = "whether the HSM profile is enabled")
private Boolean enabled;
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, description = "HSM configuration details to update (protocol specific)")
private Map<String, String> details;
public Long getId() {
return id;
}
@ -70,12 +69,9 @@ public class UpdateHSMProfileCmd extends BaseCmd {
return enabled;
}
public Map<String, String> getDetails() {
return details;
}
@Override
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException,
ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
try {
HSMProfile profile = kmsManager.updateHSMProfile(this);
HSMProfileResponse response = kmsManager.createHSMProfileResponse(profile);
@ -89,7 +85,7 @@ public class UpdateHSMProfileCmd extends BaseCmd {
@Override
public long getEntityOwnerId() {
HSMProfile profile = _entityMgr.findById(HSMProfile.class, id);
if (profile != null && profile.getAccountId() != null) {
if (profile != null && profile.getAccountId() > 0) {
return profile.getAccountId();
}
return CallContext.current().getCallingAccount().getId();

View File

@ -29,6 +29,7 @@ import org.apache.cloudstack.api.command.user.UserCmd;
import org.apache.cloudstack.api.response.ClusterResponse;
import org.apache.cloudstack.api.response.DiskOfferingResponse;
import org.apache.cloudstack.api.response.HostResponse;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.PodResponse;
import org.apache.cloudstack.api.response.ServiceOfferingResponse;
@ -90,6 +91,9 @@ public class ListVolumesCmd extends BaseListRetrieveOnlyResourceCountCmd impleme
@Parameter(name = ApiConstants.DISK_OFFERING_ID, type = CommandType.UUID, entityType = DiskOfferingResponse.class, description = "List volumes by disk offering", since = "4.4")
private Long diskOfferingId;
@Parameter(name = ApiConstants.KMS_KEY_ID, type = CommandType.UUID, entityType = KMSKeyResponse.class, description = "List volumes by KMS Key", since = "4.23")
private Long kmsKeyId;
@Parameter(name = ApiConstants.DISPLAY_VOLUME, type = CommandType.BOOLEAN, description = "List resources by display flag; only ROOT admin is eligible to pass this parameter", since = "4.4", authorized = {
RoleType.Admin})
private Boolean display;
@ -136,6 +140,10 @@ public class ListVolumesCmd extends BaseListRetrieveOnlyResourceCountCmd impleme
return diskOfferingId;
}
public Long getKmsKeyId() {
return kmsKeyId;
}
public String getType() {
return type;
}

View File

@ -29,7 +29,7 @@ import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
@EntityReference(value = HSMProfile.class)
public class HSMProfileResponse extends BaseResponse {
public class HSMProfileResponse extends BaseResponse implements ControlledViewEntityResponse {
@SerializedName(ApiConstants.ID)
@Param(description = "the ID of the HSM profile")
private String id;
@ -58,6 +58,18 @@ public class HSMProfileResponse extends BaseResponse {
@Param(description = "the domain name of the HSM profile owner")
private String domainName;
@SerializedName(ApiConstants.DOMAIN_PATH)
@Param(description = "the domain path of the HSM profile owner")
private String domainPath;
@SerializedName(ApiConstants.PROJECT_ID)
@Param(description = "the project ID of the HSM profile owner")
private String projectId;
@SerializedName(ApiConstants.PROJECT)
@Param(description = "the project name of the HSM profile owner")
private String projectName;
@SerializedName(ApiConstants.ZONE_ID)
@Param(description = "the zone ID where the HSM profile is available")
private String zoneId;
@ -78,6 +90,10 @@ public class HSMProfileResponse extends BaseResponse {
@Param(description = "whether the HSM profile is enabled")
private Boolean enabled;
@SerializedName("system")
@Param(description = "whether this is a system HSM profile available to all users globally")
private Boolean system;
@SerializedName(ApiConstants.CREATED)
@Param(description = "the date the HSM profile was created")
private Date created;
@ -102,18 +118,36 @@ public class HSMProfileResponse extends BaseResponse {
this.accountId = accountId;
}
@Override
public void setAccountName(String accountName) {
this.accountName = accountName;
}
@Override
public void setDomainId(String domainId) {
this.domainId = domainId;
}
@Override
public void setDomainName(String domainName) {
this.domainName = domainName;
}
@Override
public void setDomainPath(String domainPath) {
this.domainPath = domainPath;
}
@Override
public void setProjectId(String projectId) {
this.projectId = projectId;
}
@Override
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public void setZoneId(String zoneId) {
this.zoneId = zoneId;
}
@ -134,6 +168,10 @@ public class HSMProfileResponse extends BaseResponse {
this.enabled = enabled;
}
public void setSystem(Boolean system) {
this.system = system;
}
public void setCreated(Date created) {
this.created = created;
}

View File

@ -30,7 +30,7 @@ import org.apache.cloudstack.kms.KMSKey;
import java.util.Date;
@EntityReference(value = KMSKey.class)
public class KMSKeyResponse extends BaseResponse implements ControlledEntityResponse {
public class KMSKeyResponse extends BaseResponse implements ControlledViewEntityResponse {
@SerializedName(ApiConstants.ID)
@Param(description = "the UUID of the key")
@ -76,9 +76,13 @@ public class KMSKeyResponse extends BaseResponse implements ControlledEntityResp
@Param(description = "the zone name where the key is valid")
private String zoneName;
@SerializedName(ApiConstants.PROVIDER)
@Param(description = "the KMS provider (database, pkcs11, etc.)")
private String provider;
@SerializedName(ApiConstants.HSM_PROFILE_ID)
@Param(description = "the zone ID where the key is valid")
private String hsmProfileId;
@SerializedName(ApiConstants.HSM_PROFILE)
@Param(description = "the zone name where the key is valid")
private String hsmProfileName;
@SerializedName(ApiConstants.ALGORITHM)
@Param(description = "the encryption algorithm")
@ -88,20 +92,29 @@ public class KMSKeyResponse extends BaseResponse implements ControlledEntityResp
@Param(description = "the key size in bits")
private Integer keyBits;
@SerializedName(ApiConstants.STATE)
@Param(description = "the state of the key (Enabled, Disabled, Deleted)")
private String state;
@SerializedName(ApiConstants.VERSION)
@Param(description = "the key size in bits")
private Integer version;
@SerializedName(ApiConstants.ENABLED)
@Param(description = "whether the key is enabled")
private Boolean enabled;
@SerializedName(ApiConstants.CREATED)
@Param(description = "the creation timestamp")
private Date created;
// KEK label is admin-only for security
@SerializedName(ApiConstants.KEK_LABEL)
@Param(description = "the provider-specific KEK label (admin only)", authorized = {RoleType.Admin})
private String kekLabel;
@SerializedName(ApiConstants.PROJECT_ID)
@Param(description = "the project ID of the key")
private String projectId;
// Getters and Setters
@SerializedName(ApiConstants.PROJECT)
@Param(description = "the project name of the key")
private String projectName;
@SerializedName(ApiConstants.KEK_LABEL)
@Param(description = "the provider-specific KEK label (admin only)", authorized = { RoleType.Admin })
private String kekLabel;
public String getId() {
return id;
@ -146,12 +159,12 @@ public class KMSKeyResponse extends BaseResponse implements ControlledEntityResp
@Override
public void setProjectId(String projectId) {
// KMS keys are not project-scoped
this.projectId = projectId;
}
@Override
public void setProjectName(String projectName) {
// KMS keys are not project-scoped
this.projectName = projectName;
}
public String getAccountId() {
@ -205,12 +218,20 @@ public class KMSKeyResponse extends BaseResponse implements ControlledEntityResp
this.zoneName = zoneName;
}
public String getProvider() {
return provider;
public String getHsmProfileId() {
return hsmProfileId;
}
public void setProvider(String provider) {
this.provider = provider;
public void setHsmProfileId(String hsmProfileId) {
this.hsmProfileId = hsmProfileId;
}
public String getHsmProfileName() {
return hsmProfileName;
}
public void setHsmProfileName(String hsmProfileName) {
this.hsmProfileName = hsmProfileName;
}
public String getAlgorithm() {
@ -229,12 +250,20 @@ public class KMSKeyResponse extends BaseResponse implements ControlledEntityResp
this.keyBits = keyBits;
}
public String getState() {
return state;
public Integer getVersion() {
return version;
}
public void setState(String state) {
this.state = state;
public void setVersion(Integer version) {
this.version = version;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public Date getCreated() {

View File

@ -309,6 +309,10 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co
@Param(description = "the format of the disk encryption if applicable", since = "4.19.1")
private String encryptionFormat;
@SerializedName(ApiConstants.KMS_KEY)
@Param(description = "KMS key id of the volume", since = "4.23.0")
private String kmsKey;
@SerializedName(ApiConstants.KMS_KEY_ID)
@Param(description = "KMS key id of the volume", since = "4.23.0")
private String kmsKeyId;
@ -880,6 +884,14 @@ public class VolumeResponse extends BaseResponseWithTagInformation implements Co
this.encryptionFormat = encryptionFormat;
}
public String getKmsKey() {
return kmsKey;
}
public void setKmsKey(String kmsKey) {
this.kmsKey = kmsKey;
}
public String getKmsKeyId() {
return kmsKeyId;
}

View File

@ -17,19 +17,20 @@
package org.apache.cloudstack.kms;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
import java.util.Date;
public interface HSMProfile extends Identity, InternalIdentity {
public interface HSMProfile extends Identity, InternalIdentity, ControlledEntity {
String getName();
String getProtocol();
Long getAccountId();
long getAccountId();
Long getDomainId();
long getDomainId();
Long getZoneId();
@ -37,6 +38,8 @@ public interface HSMProfile extends Identity, InternalIdentity {
boolean isEnabled();
boolean isSystem();
Date getCreated();
Date getRemoved();

View File

@ -33,73 +33,28 @@ import java.util.Date;
*/
public interface KMSKey extends Identity, InternalIdentity, ControlledEntity {
/**
* Get the user-friendly name of the key
*/
String getName();
/**
* Get the description of the key
*/
String getDescription();
/**
* Get the provider-specific KEK label/ID
* (internal identifier used by the KMS provider)
* Provider-specific KEK label/ID (internal identifier used by the KMS provider)
*/
String getKekLabel();
/**
* Get the purpose of this key
*/
KeyPurpose getPurpose();
/**
* Get the zone ID where this key is valid
*/
Long getZoneId();
/**
* Get the KMS provider name (e.g., "database", "pkcs11")
*/
String getProviderName();
/**
* Get the encryption algorithm (e.g., "AES/GCM/NoPadding")
*/
String getAlgorithm();
/**
* Get the key size in bits (e.g., 128, 192, 256)
*/
Integer getKeyBits();
/**
* Get the current state of the key
*/
State getState();
boolean isEnabled();
/**
* Get the creation timestamp
*/
Date getCreated();
/**
* Get the removal timestamp (null if not removed)
*/
Date getRemoved();
/**
* Key state enumeration
*/
enum State {
/** Key is active and can be used for encryption/decryption */
Enabled,
/** Key is disabled and cannot be used for new operations */
Disabled,
/** Key is soft-deleted */
Deleted
}
Long getHsmProfileId();
}

View File

@ -17,11 +17,10 @@
package org.apache.cloudstack.kms;
import com.cloud.user.Account;
import com.cloud.utils.component.Manager;
import org.apache.cloudstack.api.command.admin.kms.MigrateVolumesToKMSCmd;
import org.apache.cloudstack.api.command.admin.kms.RotateKMSKeyCmd;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.api.command.user.kms.RotateKMSKeyCmd;
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;
@ -34,41 +33,16 @@ import org.apache.cloudstack.api.response.HSMProfileResponse;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.framework.kms.KMSProvider;
import org.apache.cloudstack.framework.kms.KeyPurpose;
import org.apache.cloudstack.framework.kms.WrappedKey;
import java.util.List;
/**
* Manager interface for Key Management Service operations.
* Provides high-level API for cryptographic key management with zone-scoping,
* provider abstraction, and integration with CloudStack's configuration system.
*/
public interface KMSManager extends Manager, Configurable {
// ==================== Configuration Keys ====================
/**
* Zone-scoped: enable KMS for a specific zone
* When false (default), new volumes use legacy passphrase encryption
* When true, new volumes use KMS envelope encryption
*/
ConfigKey<Boolean> KMSEnabled = new ConfigKey<>(
"Advanced",
Boolean.class,
"kms.enabled",
"false",
"Enable Key Management Service for disk encryption in this zone",
true,
ConfigKey.Scope.Zone
);
/**
* Global: DEK size in bits for volume encryption
* Supported: 128, 192, 256
*/
ConfigKey<Integer> KMSDekSizeBits = new ConfigKey<>(
"Advanced",
Integer.class,
@ -79,9 +53,6 @@ public interface KMSManager extends Manager, Configurable {
ConfigKey.Scope.Global
);
/**
* Global: retry count for transient KMS failures
*/
ConfigKey<Integer> KMSRetryCount = new ConfigKey<>(
"Advanced",
Integer.class,
@ -92,9 +63,6 @@ public interface KMSManager extends Manager, Configurable {
ConfigKey.Scope.Global
);
/**
* Global: retry delay in milliseconds
*/
ConfigKey<Integer> KMSRetryDelayMs = new ConfigKey<>(
"Advanced",
Integer.class,
@ -105,9 +73,6 @@ public interface KMSManager extends Manager, Configurable {
ConfigKey.Scope.Global
);
/**
* Global: timeout for KMS operations in seconds
*/
ConfigKey<Integer> KMSOperationTimeoutSec = new ConfigKey<>(
"Advanced",
Integer.class,
@ -118,9 +83,6 @@ public interface KMSManager extends Manager, Configurable {
ConfigKey.Scope.Global
);
/**
* Global: batch size for background rewrap operations
*/
ConfigKey<Integer> KMSRewrapBatchSize = new ConfigKey<>(
"Advanced",
Integer.class,
@ -131,9 +93,6 @@ public interface KMSManager extends Manager, Configurable {
ConfigKey.Scope.Global
);
/**
* Global: interval for background rewrap job
*/
ConfigKey<Long> KMSRewrapIntervalMs = new ConfigKey<>(
"Advanced",
Long.class,
@ -144,8 +103,6 @@ public interface KMSManager extends Manager, Configurable {
ConfigKey.Scope.Global
);
// ==================== Provider Management ====================
/**
* List all registered KMS providers
*
@ -161,16 +118,6 @@ public interface KMSManager extends Manager, Configurable {
*/
KMSProvider getKMSProvider(String name);
/**
* Check if KMS is enabled for a zone
*
* @param zoneId the zone ID
* @return true if KMS is enabled
*/
boolean isKmsEnabled(Long zoneId);
// ==================== DEK Operations ====================
/**
* Unwrap a DEK from a wrapped key
* SECURITY: Caller must zeroize returned byte array after use!
@ -182,32 +129,27 @@ public interface KMSManager extends Manager, Configurable {
*/
byte[] unwrapVolumeKey(WrappedKey wrappedKey, Long zoneId) throws KMSException;
// ==================== Health & Status ====================
// ==================== User KEK Management ====================
/**
* List KMS keys accessible to a user account
*
* @param accountId the account ID
* @param domainId the domain ID
* @param zoneId optional zone filter
* @param purpose optional purpose filter
* @param state optional state filter
* @return list of accessible KMS keys
*/
List<? extends KMSKey> listUserKMSKeys(Long accountId, Long domainId, Long zoneId,
KeyPurpose purpose, KMSKey.State state);
/**
* Check if caller has permission to use a KMS key
*
* @param callerAccountId the caller's account ID
* @param key the KMS key
* @param key the KMS key
* @return true if caller has permission
*/
boolean hasPermission(Long callerAccountId, KMSKey key);
/**
* Validates that the KMS key can be used for volume encryption: key exists, not deleted,
* caller has access, key state is Enabled, and key purpose is VOLUME_ENCRYPTION.
* No-op if kmsKeyId is null.
*
* @param caller the caller's account
* @param kmsKeyId the KMS key database ID
* @param zoneId the zone ID of the target resource (volume/VM)
* @throws InvalidParameterValueException if key not found, deleted, disabled, wrong purpose, or zone mismatch
* @throws PermissionDeniedException if caller lacks access
*/
void checkKmsKeyForVolumeEncryption(Account caller, Long kmsKeyId, Long zoneId);
/**
* Unwrap a DEK by wrapped key ID, trying multiple KEK versions if needed
@ -221,15 +163,13 @@ public interface KMSManager extends Manager, Configurable {
/**
* Generate and wrap a DEK using a specific KMS key UUID
*
* @param kmsKey the KMS key
* @param kmsKey the KMS key
* @param callerAccountId the caller's account ID
* @return wrapped key ready for database storage
* @throws KMSException if operation fails
*/
WrappedKey generateVolumeKeyWithKek(KMSKey kmsKey, Long callerAccountId) throws KMSException;
// ==================== API Response Methods ====================
/**
* Create a KMS key and return the response object.
* Handles validation, account resolution, and permission checks.
@ -269,8 +209,6 @@ public interface KMSManager extends Manager, Configurable {
*/
SuccessResponse deleteKMSKey(DeleteKMSKeyCmd cmd) throws KMSException;
// ==================== Admin Operations ====================
/**
* Rotate KEK by creating new version and scheduling gradual re-encryption
*
@ -297,8 +235,6 @@ public interface KMSManager extends Manager, Configurable {
*/
boolean deleteKMSKeysByAccountId(Long accountId);
// ==================== HSM Profile Management ====================
/**
* Add a new HSM profile
*
@ -314,7 +250,7 @@ public interface KMSManager extends Manager, Configurable {
* @param cmd the list command
* @return list of HSM profiles
*/
List<HSMProfile> listHSMProfiles(ListHSMProfilesCmd cmd);
ListResponse<HSMProfileResponse> listHSMProfiles(ListHSMProfilesCmd cmd);
/**
* Delete an HSM profile

View File

@ -1947,10 +1947,7 @@ public class VolumeOrchestrator extends ManagerBase implements VolumeOrchestrati
return volume;
}
Long zoneId = volume.getDataCenterId();
// Check if KMS is enabled for zone AND KMS key is provided
if (kmsManager != null && kmsManager.isKmsEnabled(zoneId) && kmsKey != null) {
if (kmsKey != null) {
// Determine caller account ID if not provided
if (callerAccountId == null) {
callerAccountId = volume.getAccountId();

View File

@ -177,4 +177,6 @@ public interface VolumeDao extends GenericDao<VolumeVO, Long>, StateDao<Volume.S
int getVolumeCountByOfferingId(long diskOfferingId);
VolumeVO findByLastIdAndState(long lastVolumeId, Volume.State...states);
boolean existsWithKmsKey(long kmsKeyId);
}

View File

@ -755,9 +755,7 @@ public class VolumeDaoImpl extends GenericDaoBase<VolumeVO, Long> implements Vol
if (domainId != null) {
sc.setParameters("domainId", domainId);
}
Integer count = getCount(sc);
List<VolumeVO> volumes = listBy(sc, filter);
return new Pair<>(volumes, count);
return searchAndCount(sc, filter);
}
@Override
@ -973,4 +971,12 @@ public class VolumeDaoImpl extends GenericDaoBase<VolumeVO, Long> implements Vol
sc.and(sc.entity().getState(), SearchCriteria.Op.IN, (Object[]) states);
return sc.find();
}
@Override
public boolean existsWithKmsKey(long kmsKeyId) {
SearchCriteria<VolumeVO> sc = AllFieldsSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
sc.setParameters("notDestroyed", Volume.State.Expunged, Volume.State.Destroy);
return findOneBy(sc) != null;
}
}

View File

@ -17,6 +17,8 @@
package org.apache.cloudstack.kms;
import org.apache.cloudstack.api.ResourceDetail;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
@ -24,8 +26,6 @@ import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.apache.cloudstack.api.ResourceDetail;
@Entity
@Table(name = "kms_hsm_profile_details")
public class HSMProfileDetailsVO implements ResourceDetail {
@ -77,7 +77,7 @@ public class HSMProfileDetailsVO implements ResourceDetail {
public boolean isDisplay() {
return true;
}
public void setValue(String value) {
this.value = value;
}

View File

@ -17,8 +17,7 @@
package org.apache.cloudstack.kms;
import java.util.Date;
import java.util.UUID;
import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils;
import javax.persistence.Column;
import javax.persistence.Entity;
@ -26,6 +25,8 @@ import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
import java.util.UUID;
@Entity
@Table(name = "kms_hsm_profiles")
@ -60,6 +61,9 @@ public class HSMProfileVO implements HSMProfile {
@Column(name = "enabled")
private boolean enabled;
@Column(name = "system")
private boolean system;
@Column(name = "created")
private Date created;
@ -69,6 +73,7 @@ public class HSMProfileVO implements HSMProfile {
public HSMProfileVO() {
this.uuid = UUID.randomUUID().toString();
this.created = new Date();
this.system = false;
}
public HSMProfileVO(String name, String protocol, Long accountId, Long domainId, Long zoneId, String vendorName) {
@ -80,9 +85,17 @@ public class HSMProfileVO implements HSMProfile {
this.zoneId = zoneId;
this.vendorName = vendorName;
this.enabled = true;
this.system = false;
this.created = new Date();
}
@Override
public String toString() {
return String.format("HSMProfileVO %s",
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(
this, "id", "uuid", "name", "protocol", "system", "enabled"));
}
@Override
public long getId() {
return id;
@ -104,13 +117,13 @@ public class HSMProfileVO implements HSMProfile {
}
@Override
public Long getAccountId() {
return accountId;
public long getAccountId() {
return accountId == null ? -1 : accountId;
}
@Override
public Long getDomainId() {
return domainId;
public long getDomainId() {
return domainId == null ? -1 : domainId;
}
@Override
@ -138,8 +151,13 @@ public class HSMProfileVO implements HSMProfile {
return removed;
}
public void setName(String name) {
this.name = name;
@Override
public Class<?> getEntityType() {
return HSMProfile.class;
}
public void setRemoved(Date removed) {
this.removed = removed;
}
public void setEnabled(boolean enabled) {
@ -150,7 +168,16 @@ public class HSMProfileVO implements HSMProfile {
this.vendorName = vendorName;
}
public void setRemoved(Date removed) {
this.removed = removed;
public void setName(String name) {
this.name = name;
}
@Override
public boolean isSystem() {
return system;
}
public void setSystem(boolean system) {
this.system = system;
}
}

View File

@ -43,73 +43,47 @@ import java.util.UUID;
@Table(name = "kms_kek_versions")
public class KMSKekVersionVO {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "uuid", nullable = false, unique = true)
private String uuid;
@Column(name = "kms_key_id", nullable = false)
private Long kmsKeyId;
@Column(name = "version_number", nullable = false)
private Integer versionNumber;
@Column(name = "kek_label", nullable = false)
private String kekLabel;
@Column(name = "status", nullable = false, length = 32)
@Enumerated(EnumType.STRING)
private Status status;
@Column(name = "hsm_profile_id")
private Long hsmProfileId;
@Column(name = "hsm_key_label")
private String hsmKeyLabel;
@Column(name = GenericDao.CREATED_COLUMN, nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private Date created;
@Column(name = GenericDao.REMOVED_COLUMN)
@Temporal(TemporalType.TIMESTAMP)
private Date removed;
/**
* Status of a KEK version
*/
public enum Status {
/**
* Active version - used for new encryption operations
* Used for new encryption operations
*/
Active,
/**
* Previous version - still usable for decryption during rotation
* Still usable for decryption during key rotation
*/
Previous,
/**
* Archived version - no longer used (after re-encryption complete)
* No longer used; all wrapped keys have been re-encrypted
*/
Archived
}
public KMSKekVersionVO() {
this.uuid = UUID.randomUUID().toString();
this.created = new Date();
this.status = Status.Active;
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "uuid", nullable = false)
private String uuid;
@Column(name = "kms_key_id", nullable = false)
private Long kmsKeyId;
@Column(name = "version_number", nullable = false)
private Integer versionNumber;
@Column(name = "kek_label", nullable = false)
private String kekLabel;
@Column(name = "status", nullable = false, length = 32)
@Enumerated(EnumType.STRING)
private Status status;
@Column(name = "hsm_profile_id")
private Long hsmProfileId;
@Column(name = "hsm_key_label")
private String hsmKeyLabel;
@Column(name = GenericDao.CREATED_COLUMN, nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private Date created;
@Column(name = GenericDao.REMOVED_COLUMN)
@Temporal(TemporalType.TIMESTAMP)
private Date removed;
/**
* Constructor for creating a new KEK version
*
* @param kmsKeyId the KMS key ID this version belongs to
* @param versionNumber the version number (1, 2, 3, ...)
* @param kekLabel the provider-specific KEK label
* @param status the status (typically Active for new versions)
*/
public KMSKekVersionVO(Long kmsKeyId, Integer versionNumber, String kekLabel, Status status) {
this();
this.kmsKeyId = kmsKeyId;
@ -118,6 +92,12 @@ public class KMSKekVersionVO {
this.status = status;
}
public KMSKekVersionVO() {
this.uuid = UUID.randomUUID().toString();
this.created = new Date();
this.status = Status.Active;
}
public Long getId() {
return id;
}

View File

@ -47,7 +47,7 @@ public class KMSKeyVO implements KMSKey {
@Column(name = "id")
private Long id;
@Column(name = "uuid", nullable = false, unique = true)
@Column(name = "uuid", nullable = false)
private String uuid;
@Column(name = "name", nullable = false)
@ -72,18 +72,14 @@ public class KMSKeyVO implements KMSKey {
@Column(name = "zone_id", nullable = false)
private Long zoneId;
@Column(name = "provider_name", nullable = false, length = 64)
private String providerName;
@Column(name = "algorithm", nullable = false, length = 64)
private String algorithm;
@Column(name = "key_bits", nullable = false)
private Integer keyBits;
@Column(name = "state", nullable = false, length = 32)
@Enumerated(EnumType.STRING)
private State state;
@Column(name = "enabled", nullable = false)
private boolean enabled;
@Column(name = "hsm_profile_id")
private Long hsmProfileId;
@ -96,15 +92,9 @@ public class KMSKeyVO implements KMSKey {
@Temporal(TemporalType.TIMESTAMP)
private Date removed;
public KMSKeyVO() {
this.uuid = UUID.randomUUID().toString();
this.created = new Date();
this.state = State.Enabled;
}
public KMSKeyVO(String name, String description, String kekLabel, KeyPurpose purpose,
Long accountId, Long domainId, Long zoneId, String providerName,
String algorithm, Integer keyBits) {
Long accountId, Long domainId, Long zoneId,
String algorithm, Integer keyBits) {
this();
this.name = name;
this.description = description;
@ -113,21 +103,34 @@ public class KMSKeyVO implements KMSKey {
this.accountId = accountId;
this.domainId = domainId;
this.zoneId = zoneId;
this.providerName = providerName;
this.algorithm = algorithm;
this.keyBits = keyBits;
}
public KMSKeyVO() {
this.uuid = UUID.randomUUID().toString();
this.created = new Date();
this.enabled = true;
}
@Override
public long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
@Override
public String getName() {
return name;
@ -153,11 +156,6 @@ public class KMSKeyVO implements KMSKey {
return zoneId;
}
@Override
public String getProviderName() {
return providerName;
}
@Override
public String getAlgorithm() {
return algorithm;
@ -169,8 +167,8 @@ public class KMSKeyVO implements KMSKey {
}
@Override
public State getState() {
return state;
public boolean isEnabled() {
return enabled;
}
@Override
@ -183,75 +181,6 @@ public class KMSKeyVO implements KMSKey {
return removed;
}
// ControlledEntity interface methods
@Override
public long getAccountId() {
return accountId;
}
@Override
public long getDomainId() {
return domainId;
}
@Override
public Class<?> getEntityType() {
return KMSKey.class;
}
public void setId(Long id) {
this.id = id;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public void setName(String name) {
this.name = name;
}
public void setDescription(String description) {
this.description = description;
}
public void setKekLabel(String kekLabel) {
this.kekLabel = kekLabel;
}
public void setPurpose(KeyPurpose purpose) {
this.purpose = purpose;
}
public void setAccountId(Long accountId) {
this.accountId = accountId;
}
public void setDomainId(Long domainId) {
this.domainId = domainId;
}
public void setZoneId(Long zoneId) {
this.zoneId = zoneId;
}
public void setProviderName(String providerName) {
this.providerName = providerName;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public void setKeyBits(Integer keyBits) {
this.keyBits = keyBits;
}
public void setState(State state) {
this.state = state;
}
@Override
public Long getHsmProfileId() {
return hsmProfileId;
@ -261,17 +190,73 @@ public class KMSKeyVO implements KMSKey {
this.hsmProfileId = hsmProfileId;
}
public void setRemoved(Date removed) {
this.removed = removed;
}
public void setCreated(Date created) {
this.created = created;
}
public void setRemoved(Date removed) {
this.removed = removed;
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setKeyBits(Integer keyBits) {
this.keyBits = keyBits;
}
public void setAlgorithm(String algorithm) {
this.algorithm = algorithm;
}
public void setZoneId(Long zoneId) {
this.zoneId = zoneId;
}
public void setPurpose(KeyPurpose purpose) {
this.purpose = purpose;
}
public void setKekLabel(String kekLabel) {
this.kekLabel = kekLabel;
}
public void setDescription(String description) {
this.description = description;
}
public void setName(String name) {
this.name = name;
}
@Override
public long getAccountId() {
return accountId;
}
public void setAccountId(Long accountId) {
this.accountId = accountId;
}
@Override
public long getDomainId() {
return domainId;
}
public void setDomainId(Long domainId) {
this.domainId = domainId;
}
@Override
public Class<?> getEntityType() {
return KMSKey.class;
}
@Override
public String toString() {
return String.format("KMSKey %s",
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "name", "purpose", "accountId", "zoneId", "state"));
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "uuid", "name", "purpose",
"accountId", "zoneId", "enabled"));
}
}

View File

@ -46,7 +46,7 @@ public class KMSWrappedKeyVO {
@Column(name = "id")
private Long id;
@Column(name = "uuid", nullable = false, unique = true)
@Column(name = "uuid", nullable = false)
private String uuid;
@Column(name = "kms_key_id")
@ -58,7 +58,7 @@ public class KMSWrappedKeyVO {
@Column(name = "zone_id", nullable = false)
private Long zoneId;
@Column(name = "wrapped_blob", nullable = false, columnDefinition = "VARBINARY(4096)")
@Column(name = "wrapped_blob", nullable = false)
private byte[] wrappedBlob;
@Column(name = GenericDao.CREATED_COLUMN, nullable = false)
@ -76,12 +76,16 @@ public class KMSWrappedKeyVO {
this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
}
public KMSWrappedKeyVO() {
this.uuid = UUID.randomUUID().toString();
this.created = new Date();
}
public KMSWrappedKeyVO(KMSKeyVO kmsKey, Long kekVersionId, byte[] wrappedBlob) {
this();
this.kmsKeyId = kmsKey.getId();
this.kekVersionId = kekVersionId;
this.zoneId = kmsKey.getZoneId();
// Defensive copy
this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
}
@ -100,11 +104,6 @@ public class KMSWrappedKeyVO {
this.wrappedBlob = wrappedBlob != null ? Arrays.copyOf(wrappedBlob, wrappedBlob.length) : null;
}
public KMSWrappedKeyVO() {
this.uuid = UUID.randomUUID().toString();
this.created = new Date();
}
public Long getId() {
return id;
}
@ -173,6 +172,7 @@ public class KMSWrappedKeyVO {
public String toString() {
return String.format("KMSWrappedKey %s",
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(
this, "id", "uuid", "kmsKeyId", "kekVersionId", "accountId", "zoneId", "state", "created", "removed"));
this, "id", "uuid", "kmsKeyId", "kekVersionId", "accountId", "zoneId", "state", "created",
"removed"));
}
}

View File

@ -17,15 +17,8 @@
package org.apache.cloudstack.kms.dao;
import java.util.List;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.kms.HSMProfileVO;
import com.cloud.utils.db.GenericDao;
public interface HSMProfileDao extends GenericDao<HSMProfileVO, Long> {
List<HSMProfileVO> listByAccountId(Long accountId);
List<HSMProfileVO> listAdminProfiles();
List<HSMProfileVO> listAdminProfiles(Long zoneId);
HSMProfileVO findByName(String name);
}

View File

@ -17,69 +17,13 @@
package org.apache.cloudstack.kms.dao;
import java.util.List;
import com.cloud.utils.db.GenericDaoBase;
import org.apache.cloudstack.kms.HSMProfileVO;
import org.springframework.stereotype.Component;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.SearchCriteria.Op;
@Component
public class HSMProfileDaoImpl extends GenericDaoBase<HSMProfileVO, Long> implements HSMProfileDao {
protected SearchBuilder<HSMProfileVO> AccountSearch;
protected SearchBuilder<HSMProfileVO> AdminSearch;
protected SearchBuilder<HSMProfileVO> NameSearch;
public HSMProfileDaoImpl() {
super();
AccountSearch = createSearchBuilder();
AccountSearch.and("accountId", AccountSearch.entity().getAccountId(), Op.EQ);
AccountSearch.and("removed", AccountSearch.entity().getRemoved(), Op.NULL);
AccountSearch.done();
AdminSearch = createSearchBuilder();
AdminSearch.and("accountId", AdminSearch.entity().getAccountId(), Op.NULL);
AdminSearch.and("zoneId", AdminSearch.entity().getZoneId(), Op.EQ);
AdminSearch.and("removed", AdminSearch.entity().getRemoved(), Op.NULL);
AdminSearch.done();
NameSearch = createSearchBuilder();
NameSearch.and("name", NameSearch.entity().getName(), Op.EQ);
NameSearch.and("removed", NameSearch.entity().getRemoved(), Op.NULL);
NameSearch.done();
}
@Override
public List<HSMProfileVO> listByAccountId(Long accountId) {
SearchCriteria<HSMProfileVO> sc = AccountSearch.create();
sc.setParameters("accountId", accountId);
return listBy(sc);
}
@Override
public List<HSMProfileVO> listAdminProfiles() {
SearchCriteria<HSMProfileVO> sc = AdminSearch.create();
// Global admin profiles have zone_id = NULL
sc.setParameters("zoneId", (Object)null);
return listBy(sc);
}
@Override
public List<HSMProfileVO> listAdminProfiles(Long zoneId) {
SearchCriteria<HSMProfileVO> sc = AdminSearch.create();
sc.setParameters("zoneId", zoneId);
return listBy(sc);
}
@Override
public HSMProfileVO findByName(String name) {
SearchCriteria<HSMProfileVO> sc = NameSearch.create();
sc.setParameters("name", name);
return findOneBy(sc);
}
}

View File

@ -17,14 +17,17 @@
package org.apache.cloudstack.kms.dao;
import java.util.List;
import org.apache.cloudstack.kms.HSMProfileDetailsVO;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.kms.HSMProfileDetailsVO;
import java.util.List;
public interface HSMProfileDetailsDao extends GenericDao<HSMProfileDetailsVO, Long> {
List<HSMProfileDetailsVO> listByProfileId(long profileId);
void persist(long profileId, String name, String value);
HSMProfileDetailsVO findDetail(long profileId, String name);
void deleteDetails(long profileId);
}

View File

@ -17,15 +17,14 @@
package org.apache.cloudstack.kms.dao;
import java.util.List;
import org.apache.cloudstack.kms.HSMProfileDetailsVO;
import org.springframework.stereotype.Component;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.SearchCriteria.Op;
import org.apache.cloudstack.kms.HSMProfileDetailsVO;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class HSMProfileDetailsDaoImpl extends GenericDaoBase<HSMProfileDetailsVO, Long> implements HSMProfileDetailsDao {
@ -35,11 +34,11 @@ public class HSMProfileDetailsDaoImpl extends GenericDaoBase<HSMProfileDetailsVO
public HSMProfileDetailsDaoImpl() {
super();
ProfileSearch = createSearchBuilder();
ProfileSearch.and("profileId", ProfileSearch.entity().getResourceId(), Op.EQ);
ProfileSearch.done();
DetailSearch = createSearchBuilder();
DetailSearch.and("profileId", DetailSearch.entity().getResourceId(), Op.EQ);
DetailSearch.and("name", DetailSearch.entity().getName(), Op.EQ);

View File

@ -23,34 +23,21 @@ import org.apache.cloudstack.kms.KMSKekVersionVO;
import java.util.List;
public interface KMSKekVersionDao extends GenericDao<KMSKekVersionVO, Long> {
/**
* Get the active version for a KMS key
*/
KMSKekVersionVO getActiveVersion(Long kmsKeyId);
/**
* Get all versions that can be used for decryption (Active and Previous)
* Returns Active and Previous versions (usable for decryption)
*/
List<KMSKekVersionVO> getVersionsForDecryption(Long kmsKeyId);
/**
* List all versions for a KMS key
*/
List<KMSKekVersionVO> listByKmsKeyId(Long kmsKeyId);
/**
* Find a specific version by KMS key ID and version number
*/
KMSKekVersionVO findByKmsKeyIdAndVersion(Long kmsKeyId, Integer versionNumber);
/**
* Find a KEK version by KEK label
*/
KMSKekVersionVO findByKekLabel(String kekLabel);
/**
* Find all KEK versions with a specific status
* (useful for background jobs to find versions needing processing)
*/
List<KMSKekVersionVO> findByStatus(KMSKekVersionVO.Status status);
List<KMSKekVersionVO> listByHsmProfileId(Long hsmProfileId);
}

View File

@ -23,6 +23,7 @@ import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.kms.KMSKekVersionVO;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
@ -36,6 +37,7 @@ public class KMSKekVersionDaoImpl extends GenericDaoBase<KMSKekVersionVO, Long>
allFieldSearch.and("status", allFieldSearch.entity().getStatus(), SearchCriteria.Op.IN);
allFieldSearch.and("versionNumber", allFieldSearch.entity().getVersionNumber(), SearchCriteria.Op.EQ);
allFieldSearch.and("kekLabel", allFieldSearch.entity().getKekLabel(), SearchCriteria.Op.EQ);
allFieldSearch.and("hsmProfileId", allFieldSearch.entity().getHsmProfileId(), SearchCriteria.Op.EQ);
allFieldSearch.done();
}
@ -83,4 +85,14 @@ public class KMSKekVersionDaoImpl extends GenericDaoBase<KMSKekVersionVO, Long>
sc.setParameters("status", status);
return listBy(sc);
}
@Override
public List<KMSKekVersionVO> listByHsmProfileId(Long hsmProfileId) {
if (hsmProfileId == null) {
return new ArrayList<>();
}
SearchCriteria<KMSKekVersionVO> sc = allFieldSearch.create();
sc.setParameters("hsmProfileId", hsmProfileId);
return listBy(sc);
}
}

View File

@ -19,40 +19,15 @@ package org.apache.cloudstack.kms.dao;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.framework.kms.KeyPurpose;
import org.apache.cloudstack.kms.KMSKey;
import org.apache.cloudstack.kms.KMSKeyVO;
import java.util.List;
public interface KMSKeyDao extends GenericDao<KMSKeyVO, Long> {
/**
* Find a KMS key by KEK label and provider
*/
KMSKeyVO findByKekLabel(String kekLabel, String providerName);
List<KMSKeyVO> listByAccount(Long accountId, KeyPurpose purpose, Boolean enabled);
/**
* List KMS keys owned by an account
*/
List<KMSKeyVO> listByAccount(Long accountId, KeyPurpose purpose, KMSKey.State state);
List<KMSKeyVO> listByZone(Long zoneId, KeyPurpose purpose, Boolean enabled);
/**
* List KMS keys in a zone
*/
List<KMSKeyVO> listByZone(Long zoneId, KeyPurpose purpose, KMSKey.State state);
/**
* List KMS keys accessible to an account (owns or in parent domain)
*/
List<KMSKeyVO> listAccessibleKeys(Long accountId, Long domainId, Long zoneId, KeyPurpose purpose, KMSKey.State state);
/**
* Count how many wrapped keys reference this KEK
*/
long countWrappedKeysByKmsKey(Long kmsKeyId);
/**
* Count KEKs by label (to check for duplicates)
*/
long countByKekLabel(String kekLabel, String providerName);
long countByHsmProfileId(Long hsmProfileId);
}

View File

@ -21,11 +21,9 @@ import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.framework.kms.KeyPurpose;
import org.apache.cloudstack.kms.KMSKey;
import org.apache.cloudstack.kms.KMSKeyVO;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.util.List;
@Component
@ -33,85 +31,44 @@ public class KMSKeyDaoImpl extends GenericDaoBase<KMSKeyVO, Long> implements KMS
private final SearchBuilder<KMSKeyVO> allFieldSearch;
@Inject
private KMSWrappedKeyDao kmsWrappedKeyDao;
public KMSKeyDaoImpl() {
allFieldSearch = createSearchBuilder();
allFieldSearch.and("kekLabel", allFieldSearch.entity().getKekLabel(), SearchCriteria.Op.EQ);
allFieldSearch.and("providerName", allFieldSearch.entity().getProviderName(), SearchCriteria.Op.EQ);
allFieldSearch.and("domainId", allFieldSearch.entity().getDomainId(), SearchCriteria.Op.EQ);
allFieldSearch.and("accountId", allFieldSearch.entity().getAccountId(), SearchCriteria.Op.EQ);
allFieldSearch.and("purpose", allFieldSearch.entity().getPurpose(), SearchCriteria.Op.EQ);
allFieldSearch.and("state", allFieldSearch.entity().getState(), SearchCriteria.Op.EQ);
allFieldSearch.and("enabled", allFieldSearch.entity().isEnabled(), SearchCriteria.Op.EQ);
allFieldSearch.and("zoneId", allFieldSearch.entity().getZoneId(), SearchCriteria.Op.EQ);
allFieldSearch.and("hsmProfileId", allFieldSearch.entity().getHsmProfileId(), SearchCriteria.Op.EQ);
allFieldSearch.done();
}
@Override
public KMSKeyVO findByKekLabel(String kekLabel, String providerName) {
SearchCriteria<KMSKeyVO> sc = allFieldSearch.create();
sc.setParameters("kekLabel", kekLabel);
sc.setParameters("providerName", providerName);
return findOneBy(sc);
}
@Override
public List<KMSKeyVO> listByAccount(Long accountId, KeyPurpose purpose, KMSKey.State state) {
public List<KMSKeyVO> listByAccount(Long accountId, KeyPurpose purpose, Boolean enabled) {
SearchCriteria<KMSKeyVO> sc = allFieldSearch.create();
sc.setParameters("accountId", accountId);
if (purpose != null) {
sc.setParameters("purpose", purpose);
}
if (state != null) {
sc.setParameters("state", state);
}
sc.setParametersIfNotNull("purpose", purpose);
sc.setParametersIfNotNull("enabled", enabled);
return listBy(sc);
}
@Override
public List<KMSKeyVO> listByZone(Long zoneId, KeyPurpose purpose, KMSKey.State state) {
public List<KMSKeyVO> listByZone(Long zoneId, KeyPurpose purpose, Boolean enabled) {
SearchCriteria<KMSKeyVO> sc = allFieldSearch.create();
sc.setParameters("zoneId", zoneId);
if (purpose != null) {
sc.setParameters("purpose", purpose);
}
if (state != null) {
sc.setParameters("state", state);
}
sc.setParametersIfNotNull("purpose", purpose);
sc.setParametersIfNotNull("enabled", enabled);
return listBy(sc);
}
@Override
public List<KMSKeyVO> listAccessibleKeys(Long accountId, Long domainId, Long zoneId, KeyPurpose purpose, KMSKey.State state) {
SearchCriteria<KMSKeyVO> sc = allFieldSearch.create();
sc.setParameters("accountId", accountId);
if (zoneId != null) {
sc.setParameters("zoneId", zoneId);
}
if (purpose != null) {
sc.setParameters("purpose", purpose);
}
if (state != null) {
sc.setParameters("state", state);
}
return listBy(sc);
}
@Override
public long countWrappedKeysByKmsKey(Long kmsKeyId) {
if (kmsKeyId == null) {
public long countByHsmProfileId(Long hsmProfileId) {
if (hsmProfileId == null) {
return 0;
}
return kmsWrappedKeyDao.countByKmsKeyId(kmsKeyId);
}
@Override
public long countByKekLabel(String kekLabel, String providerName) {
SearchCriteria<KMSKeyVO> sc = allFieldSearch.create();
sc.setParameters("kekLabel", kekLabel);
sc.setParameters("providerName", providerName);
sc.setParameters("hsmProfileId", hsmProfileId);
Integer count = getCount(sc);
return count != null ? count.longValue() : 0L;
return count != null ? count : 0;
}
}

View File

@ -22,62 +22,14 @@ import org.apache.cloudstack.kms.KMSWrappedKeyVO;
import java.util.List;
/**
* Data Access Object for KMS Wrapped Keys.
* This DAO is purpose-agnostic and can be used for any key purpose
* (volumes, TLS certs, config secrets, etc.)
*/
public interface KMSWrappedKeyDao extends GenericDao<KMSWrappedKeyVO, Long> {
/**
* List all wrapped keys using a specific KMS key
* (useful for key rotation)
*
* @param kmsKeyId the KMS key ID (FK to kms_keys)
* @return list of wrapped keys
*/
List<KMSWrappedKeyVO> listByKmsKeyId(Long kmsKeyId);
/**
* List all wrapped keys in a zone
*
* @param zoneId the zone ID
* @return list of wrapped keys
*/
List<KMSWrappedKeyVO> listByZone(Long zoneId);
/**
* Count wrapped keys using a specific KMS key
*
* @param kmsKeyId the KMS key ID (FK to kms_keys)
* @return count of keys
*/
long countByKmsKeyId(Long kmsKeyId);
/**
* List all wrapped keys using a specific KEK version
*
* @param kekVersionId the KEK version ID (FK to kms_kek_versions)
* @return list of wrapped keys
*/
List<KMSWrappedKeyVO> listByKekVersionId(Long kekVersionId);
/**
* List wrapped keys using a specific KEK version with pagination limit
* (useful for batch processing in background jobs)
*
* @param kekVersionId the KEK version ID (FK to kms_kek_versions)
* @param limit maximum number of keys to return
* @return list of wrapped keys (limited to specified count)
* Limited variant for batch processing during key rotation
*/
List<KMSWrappedKeyVO> listByKekVersionId(Long kekVersionId, int limit);
/**
* List wrapped keys for a KMS key that need re-encryption (not using specified version)
*
* @param kmsKeyId the KMS key ID
* @param excludeKekVersionId the KEK version ID to exclude (keys using this version don't need rewrap)
* @return list of wrapped keys that need re-encryption
*/
List<KMSWrappedKeyVO> listWrappedKeysForRewrap(long kmsKeyId, long excludeKekVersionId);
long countByKekVersionId(Long kekVersionId);
}

View File

@ -30,41 +30,16 @@ import java.util.List;
public class KMSWrappedKeyDaoImpl extends GenericDaoBase<KMSWrappedKeyVO, Long> implements KMSWrappedKeyDao {
private final SearchBuilder<KMSWrappedKeyVO> allFieldSearch;
private final SearchBuilder<KMSWrappedKeyVO> rewrapExcludeVersionSearch;
public KMSWrappedKeyDaoImpl() {
super();
// Search by UUID
allFieldSearch = createSearchBuilder();
allFieldSearch.and("kmsKeyId", allFieldSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
allFieldSearch.and("kekVersionId", allFieldSearch.entity().getKekVersionId(), SearchCriteria.Op.EQ);
allFieldSearch.and("zoneId", allFieldSearch.entity().getZoneId(), SearchCriteria.Op.EQ);
allFieldSearch.and("kmsKeyId", allFieldSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
allFieldSearch.done();
// Search builder for excluding specific version using OR condition
rewrapExcludeVersionSearch = createSearchBuilder();
rewrapExcludeVersionSearch.and("kmsKeyId", rewrapExcludeVersionSearch.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
// OR group: (kekVersionId != excludeKekVersionId OR kekVersionId IS NULL)
rewrapExcludeVersionSearch.and().op("kekVersionId", rewrapExcludeVersionSearch.entity().getKekVersionId(), SearchCriteria.Op.NEQ);
rewrapExcludeVersionSearch.or("kekVersionIdNull", rewrapExcludeVersionSearch.entity().getKekVersionId(), SearchCriteria.Op.NULL);
rewrapExcludeVersionSearch.cp();
rewrapExcludeVersionSearch.done();
}
@Override
public List<KMSWrappedKeyVO> listByKmsKeyId(Long kmsKeyId) {
SearchCriteria<KMSWrappedKeyVO> sc = allFieldSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
return listBy(sc);
}
@Override
public List<KMSWrappedKeyVO> listByZone(Long zoneId) {
SearchCriteria<KMSWrappedKeyVO> sc = allFieldSearch.create();
sc.setParameters("zoneId", zoneId);
return listBy(sc);
}
@Override
@ -75,13 +50,6 @@ public class KMSWrappedKeyDaoImpl extends GenericDaoBase<KMSWrappedKeyVO, Long>
return count != null ? count.longValue() : 0L;
}
@Override
public List<KMSWrappedKeyVO> listByKekVersionId(Long kekVersionId) {
SearchCriteria<KMSWrappedKeyVO> sc = allFieldSearch.create();
sc.setParameters("kekVersionId", kekVersionId);
return listBy(sc);
}
@Override
public List<KMSWrappedKeyVO> listByKekVersionId(Long kekVersionId, int limit) {
SearchCriteria<KMSWrappedKeyVO> sc = allFieldSearch.create();
@ -91,10 +59,13 @@ public class KMSWrappedKeyDaoImpl extends GenericDaoBase<KMSWrappedKeyVO, Long>
}
@Override
public List<KMSWrappedKeyVO> listWrappedKeysForRewrap(long kmsKeyId, long excludeKekVersionId) {
SearchCriteria<KMSWrappedKeyVO> sc = rewrapExcludeVersionSearch.create();
sc.setParameters("kmsKeyId", kmsKeyId);
sc.setParameters("kekVersionId", excludeKekVersionId);
return listBy(sc);
public long countByKekVersionId(Long kekVersionId) {
if (kekVersionId == null) {
return 0;
}
SearchCriteria<KMSWrappedKeyVO> sc = allFieldSearch.create();
sc.setParameters("kekVersionId", kekVersionId);
Integer count = getCount(sc);
return count != null ? count.longValue() : 0L;
}
}

View File

@ -131,6 +131,7 @@ CREATE TABLE IF NOT EXISTS `cloud`.`kms_hsm_profiles` (
-- Metadata
`vendor_name` VARCHAR(64) COMMENT 'HSM vendor (Thales, AWS, SoftHSM, etc.)',
`enabled` BOOLEAN NOT NULL DEFAULT TRUE,
`system` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'System profile (globally available, root admin only)',
`created` DATETIME NOT NULL,
`removed` DATETIME,
@ -167,19 +168,17 @@ CREATE TABLE IF NOT EXISTS `cloud`.`kms_keys` (
`account_id` BIGINT UNSIGNED NOT NULL COMMENT 'Owning account',
`domain_id` BIGINT UNSIGNED NOT NULL COMMENT 'Owning domain',
`zone_id` BIGINT UNSIGNED NOT NULL COMMENT 'Zone where key is valid',
`provider_name` VARCHAR(64) NOT NULL COMMENT 'KMS provider (database, pkcs11, etc.)',
`algorithm` VARCHAR(64) NOT NULL DEFAULT 'AES/GCM/NoPadding' COMMENT 'Encryption algorithm',
`key_bits` INT NOT NULL DEFAULT 256 COMMENT 'Key size in bits',
`state` VARCHAR(32) NOT NULL DEFAULT 'Enabled' COMMENT 'Enabled, Disabled, or Deleted',
`hsm_profile_id` BIGINT UNSIGNED COMMENT 'Current HSM profile ID for this key',
`enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Whether the key is enabled for new cryptographic operations',
`hsm_profile_id` BIGINT UNSIGNED NOT NULL COMMENT 'Current HSM profile ID for this key',
`created` DATETIME NOT NULL COMMENT 'Creation timestamp',
`removed` DATETIME COMMENT 'Removal timestamp for soft delete',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_uuid` (`uuid`),
INDEX `idx_account_purpose` (`account_id`, `purpose`, `state`),
INDEX `idx_domain_purpose` (`domain_id`, `purpose`, `state`),
INDEX `idx_zone_state` (`zone_id`, `state`),
INDEX `idx_kek_label_provider` (`kek_label`, `provider_name`),
INDEX `idx_account_purpose` (`account_id`, `purpose`, `enabled`),
INDEX `idx_domain_purpose` (`domain_id`, `purpose`, `enabled`),
INDEX `idx_zone_enabled` (`zone_id`, `enabled`),
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,

View File

@ -41,6 +41,8 @@ SELECT
`volumes`.`external_uuid` AS `external_uuid`,
`volumes`.`encrypt_format` AS `encrypt_format`,
`volumes`.`kms_key_id` AS `kms_key_id`,
`kms_keys`.`uuid` AS `kms_key_uuid`,
`kms_keys`.`name` AS `kms_key_name`,
`volumes`.`kms_wrapped_key_id` AS `kms_wrapped_key_id`,
`volumes`.`delete_protection` AS `delete_protection`,
`account`.`id` AS `account_id`,
@ -118,7 +120,7 @@ SELECT
`resource_tag_domain`.`uuid` AS `tag_domain_uuid`,
`resource_tag_domain`.`name` AS `tag_domain_name`
FROM
((((((((((((((((((`volumes`
(((((((((((((((((((`volumes`
JOIN `account`ON
((`volumes`.`account_id` = `account`.`id`)))
JOIN `domain`ON
@ -131,8 +133,10 @@ LEFT JOIN `vm_instance`ON
((`volumes`.`instance_id` = `vm_instance`.`id`)))
LEFT JOIN `user_vm`ON
((`user_vm`.`id` = `vm_instance`.`id`)))
LEFT JOIN `volume_store_ref`ON
LEFT JOIN `volume_store_ref` ON
((`volumes`.`id` = `volume_store_ref`.`volume_id`)))
LEFT JOIN `kms_keys` ON
((`volumes`.`kms_key_id` = `kms_keys`.`id`)))
LEFT JOIN `service_offering`ON
((`vm_instance`.`service_offering_id` = `service_offering`.`id`)))
LEFT JOIN `disk_offering`ON

View File

@ -122,8 +122,6 @@ public class KMSException extends CloudRuntimeException {
"KEK not found: " + kekId);
}
// Static factory methods for common error types
public static KMSException keyAlreadyExists(String details) {
return new KMSException(ErrorType.KEY_ALREADY_EXISTS,
"Key already exists: " + details);

View File

@ -17,9 +17,8 @@
package org.apache.cloudstack.framework.kms;
import org.apache.cloudstack.framework.config.Configurable;
import com.cloud.utils.component.Adapter;
import org.apache.cloudstack.framework.config.Configurable;
/**
* Abstract provider contract for Key Management Service operations.
@ -44,14 +43,12 @@ public interface KMSProvider extends Configurable, Adapter {
*/
String getProviderName();
// ==================== KEK Management ====================
/**
* Create a new Key Encryption Key (KEK) in the secure backend with explicit HSM profile.
*
* @param purpose the purpose/scope for this KEK
* @param label human-readable label for the KEK (must be unique within purpose)
* @param keyBits key size in bits (typically 128, 192, or 256)
* @param purpose the purpose/scope for this KEK
* @param label human-readable label for the KEK (must be unique within purpose)
* @param keyBits key size in bits (typically 128, 192, or 256)
* @param hsmProfileId optional HSM profile ID to create the KEK in (null for auto-resolution/default)
* @return the KEK identifier (label or handle) for later reference
* @throws KMSException if KEK creation fails
@ -91,14 +88,12 @@ public interface KMSProvider extends Configurable, Adapter {
*/
boolean isKekAvailable(String kekId) throws KMSException;
// ==================== DEK Operations ====================
/**
* Wrap (encrypt) a plaintext Data Encryption Key with a KEK using explicit HSM profile.
*
* @param plainDek the plaintext DEK to wrap (caller must zeroize after call)
* @param purpose the intended purpose of this DEK
* @param kekLabel the label of the KEK to use for wrapping
* @param plainDek the plaintext DEK to wrap (caller must zeroize after call)
* @param purpose the intended purpose of this DEK
* @param kekLabel the label of the KEK to use for wrapping
* @param hsmProfileId optional HSM profile ID to use (null for auto-resolution/default)
* @return WrappedKey containing the encrypted DEK and metadata
* @throws KMSException if wrapping fails or KEK not found
@ -124,7 +119,7 @@ public interface KMSProvider extends Configurable, Adapter {
* <p>
* SECURITY: Caller MUST zeroize the returned byte array after use
*
* @param wrappedKey the wrapped key to decrypt
* @param wrappedKey the wrapped key to decrypt
* @param hsmProfileId optional HSM profile ID to use (null for auto-resolution/default)
* @return plaintext DEK (caller must zeroize!)
* @throws KMSException if unwrapping fails or KEK not found
@ -149,14 +144,15 @@ public interface KMSProvider extends Configurable, Adapter {
* Generate a new random DEK and immediately wrap it with a KEK using explicit HSM profile.
* (convenience method combining generation + wrapping)
*
* @param purpose the intended purpose of the new DEK
* @param kekLabel the label of the KEK to use for wrapping
* @param keyBits DEK size in bits (typically 128, 192, or 256)
* @param purpose the intended purpose of the new DEK
* @param kekLabel the label of the KEK to use for wrapping
* @param keyBits DEK size in bits (typically 128, 192, or 256)
* @param hsmProfileId optional HSM profile ID to use (null for auto-resolution/default)
* @return WrappedKey containing the newly generated and wrapped DEK
* @throws KMSException if generation or wrapping fails
*/
WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits, Long hsmProfileId) throws KMSException;
WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits,
Long hsmProfileId) throws KMSException;
/**
* Generate a new random DEK and immediately wrap it with a KEK.
@ -177,8 +173,8 @@ public interface KMSProvider extends Configurable, Adapter {
* Rewrap a DEK with a different KEK (used during key rotation) using explicit target HSM profile.
* This unwraps with the old KEK and wraps with the new KEK without exposing the plaintext DEK.
*
* @param oldWrappedKey the currently wrapped key
* @param newKekLabel the label of the new KEK to wrap with
* @param oldWrappedKey the currently wrapped key
* @param newKekLabel the label of the new KEK to wrap with
* @param targetHsmProfileId optional target HSM profile ID to wrap with (null for auto-resolution/default)
* @return new WrappedKey encrypted with the new KEK
* @throws KMSException if rewrapping fails
@ -199,8 +195,6 @@ public interface KMSProvider extends Configurable, Adapter {
return rewrapKey(oldWrappedKey, newKekLabel, null);
}
// ==================== Health & Status ====================
/**
* Perform health check on the provider backend
*
@ -208,4 +202,18 @@ public interface KMSProvider extends Configurable, Adapter {
* @throws KMSException if health check fails with critical error
*/
boolean healthCheck() throws KMSException;
/**
* Invalidates any cached state (config, sessions) associated with the given HSM profile.
* Must be called after an HSM profile is updated or deleted so that the next operation
* re-reads the profile details from the database instead of using stale cached values.
*
* <p>Providers that do not cache per-profile state (e.g. the database provider) can
* leave this as a no-op.
*
* @param profileId the HSM profile ID whose cache should be evicted
*/
default void invalidateProfileCache(Long profileId) {
// no-op for providers that don't cache per-profile state
}
}

View File

@ -18,6 +18,7 @@
package org.apache.cloudstack.kms.provider;
import com.cloud.utils.component.AdapterBase;
import com.cloud.utils.crypt.DBEncryptionUtil;
import com.google.crypto.tink.subtle.AesGcmJce;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.kms.KMSException;
@ -26,7 +27,6 @@ import org.apache.cloudstack.framework.kms.KeyPurpose;
import org.apache.cloudstack.framework.kms.WrappedKey;
import org.apache.cloudstack.kms.provider.database.KMSDatabaseKekObjectVO;
import org.apache.cloudstack.kms.provider.database.dao.KMSDatabaseKekObjectDao;
import com.cloud.utils.crypt.DBEncryptionUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -34,14 +34,10 @@ import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Database-backed KMS provider that stores master KEKs in a PKCS#11-like object table.
@ -52,26 +48,14 @@ import java.util.concurrent.ConcurrentHashMap;
* CloudStack's existing DBEncryptionUtil, with PKCS#11-compatible attributes.
*/
public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
// Configuration keys
public static final ConfigKey<Boolean> CacheEnabled = new ConfigKey<>(
"Advanced",
Boolean.class,
"kms.database.cache.enabled",
"true",
"Enable in-memory caching of KEKs for better performance",
true,
ConfigKey.Scope.Global
);
private static final Logger logger = LogManager.getLogger(DatabaseKMSProvider.class);
private static final String PROVIDER_NAME = "database";
private static final int GCM_IV_LENGTH = 12; // 96 bits recommended for GCM
private static final int GCM_TAG_LENGTH = 16; // 128 bits
private static final String ALGORITHM = "AES/GCM/NoPadding";
// PKCS#11 constants
private static final String CKO_SECRET_KEY = "CKO_SECRET_KEY";
private static final String CKK_AES = "CKK_AES";
// In-memory cache of KEKs (encrypted form cached, decrypted on demand)
private final Map<String, byte[]> kekCache = new ConcurrentHashMap<>();
private final SecureRandom secureRandom = new SecureRandom();
@Inject
private KMSDatabaseKekObjectDao kekObjectDao;
@ -97,28 +81,24 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
label = generateKekLabel(purpose);
}
// Check if KEK already exists
if (kekObjectDao.existsByLabel(label)) {
throw KMSException.keyAlreadyExists("KEK with label " + label + " already exists");
}
byte[] kekBytes = new byte[keyBits / 8];
try {
// Generate random KEK
byte[] kekBytes = new byte[keyBits / 8];
secureRandom.nextBytes(kekBytes);
// Encrypt the KEK material using DBEncryptionUtil (Base64 encode first, then encrypt)
// Base64 encode then encrypt the KEK material using DBEncryptionUtil
String kekBase64 = Base64.getEncoder().encodeToString(kekBytes);
String encryptedKek = DBEncryptionUtil.encrypt(kekBase64);
byte[] encryptedKekBytes = encryptedKek.getBytes(StandardCharsets.UTF_8);
// Create PKCS#11-like object
KMSDatabaseKekObjectVO kekObject = new KMSDatabaseKekObjectVO(label, purpose, keyBits, encryptedKekBytes);
kekObject.setObjectClass(CKO_SECRET_KEY);
kekObject.setKeyType(CKK_AES);
kekObject.setObjectId(label.getBytes(StandardCharsets.UTF_8));
kekObject.setAlgorithm(ALGORITHM);
// PKCS#11 attributes for KEK
kekObject.setIsSensitive(true);
kekObject.setIsExtractable(false);
kekObject.setIsToken(true);
@ -131,31 +111,17 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
kekObjectDao.persist(kekObject);
// Cache the KEK
if (CacheEnabled.value()) {
kekCache.put(label, kekBytes);
}
logger.info("Created KEK with label {} for purpose {} (PKCS#11 object ID: {})", label, purpose, kekObject.getId());
logger.info("Created KEK with label {} for purpose {} (PKCS#11 object ID: {})", label, purpose,
kekObject.getId());
return label;
} catch (Exception e) {
throw KMSException.kekOperationFailed("Failed to create KEK: " + e.getMessage(), e);
} finally {
Arrays.fill(kekBytes, (byte) 0);
}
}
@Override
public String getConfigComponentName() {
return DatabaseKMSProvider.class.getSimpleName();
}
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[]{
CacheEnabled
};
}
@Override
public void deleteKek(String kekId) throws KMSException {
KMSDatabaseKekObjectVO kekObject = kekObjectDao.findByLabel(kekId);
@ -166,13 +132,6 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
try {
kekObjectDao.remove(kekObject.getId());
// Remove from cache
byte[] cachedKek = kekCache.remove(kekId);
if (cachedKek != null) {
Arrays.fill(cachedKek, (byte) 0); // Zeroize
}
// Zeroize key material in database object
if (kekObject.getKeyMaterial() != null) {
Arrays.fill(kekObject.getKeyMaterial(), (byte) 0);
}
@ -195,7 +154,8 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
}
@Override
public WrappedKey wrapKey(byte[] plainKey, KeyPurpose purpose, String kekLabel, Long hsmProfileId) throws KMSException {
public WrappedKey wrapKey(byte[] plainKey, KeyPurpose purpose, String kekLabel,
Long hsmProfileId) throws KMSException {
// Database provider ignores hsmProfileId
return wrapKey(plainKey, purpose, kekLabel);
}
@ -209,14 +169,12 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
byte[] kekBytes = loadKek(kekLabel);
try {
// Create AES-GCM cipher with the KEK
// Tink's AesGcmJce automatically generates a random IV and prepends it to the ciphertext
AesGcmJce aesgcm = new AesGcmJce(kekBytes);
byte[] wrappedBlob = aesgcm.encrypt(plainKey, new byte[0]);
// Encrypt the DEK (Tink's encrypt returns [IV][ciphertext+tag] format)
byte[] wrappedBlob = aesgcm.encrypt(plainKey, new byte[0]); // Empty associated data
WrappedKey wrapped = new WrappedKey(kekLabel, purpose, ALGORITHM, wrappedBlob, PROVIDER_NAME, new Date(), null);
WrappedKey wrapped = new WrappedKey(kekLabel, purpose, ALGORITHM, wrappedBlob, PROVIDER_NAME, new Date(),
null);
logger.debug("Wrapped {} key with KEK {}", purpose, kekLabel);
return wrapped;
@ -243,9 +201,7 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
byte[] kekBytes = loadKek(wrappedKey.getKekId());
try {
// Create AES-GCM cipher with the KEK
AesGcmJce aesgcm = new AesGcmJce(kekBytes);
// Tink's decrypt expects [IV][ciphertext+tag] format (same as encrypt returns)
byte[] blob = wrappedKey.getWrappedKeyMaterial();
if (blob.length < GCM_IV_LENGTH + GCM_TAG_LENGTH) {
@ -253,8 +209,7 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
"Invalid wrapped key format: too short");
}
// Decrypt the DEK (Tink extracts IV from the blob automatically)
byte[] plainKey = aesgcm.decrypt(blob, new byte[0]); // Empty associated data
byte[] plainKey = aesgcm.decrypt(blob, new byte[0]);
logger.debug("Unwrapped {} key with KEK {}", wrappedKey.getPurpose(), wrappedKey.getKekId());
return plainKey;
@ -270,7 +225,8 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
}
@Override
public WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits, Long hsmProfileId) throws KMSException {
public WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits,
Long hsmProfileId) throws KMSException {
// Database provider ignores hsmProfileId
return generateAndWrapDek(purpose, kekLabel, keyBits);
}
@ -281,7 +237,6 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
throw KMSException.invalidParameter("DEK size must be 128, 192, or 256 bits");
}
// Generate random DEK
byte[] dekBytes = new byte[keyBits / 8];
secureRandom.nextBytes(dekBytes);
@ -294,18 +249,16 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
}
@Override
public WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel, Long targetHsmProfileId) throws KMSException {
public WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel,
Long targetHsmProfileId) throws KMSException {
// Database provider ignores targetHsmProfileId
return rewrapKey(oldWrappedKey, newKekLabel);
}
@Override
public WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel) throws KMSException {
// Unwrap with old KEK
byte[] plainKey = unwrapKey(oldWrappedKey);
try {
// Wrap with new KEK
return wrapKey(plainKey, oldWrappedKey.getPurpose(), newKekLabel);
} finally {
// Zeroize plaintext DEK
@ -316,7 +269,6 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
@Override
public boolean healthCheck() throws KMSException {
try {
// Verify we can access KEK object DAO
if (kekObjectDao == null) {
logger.error("KMSDatabaseKekObjectDao is not initialized");
return false;
@ -329,16 +281,6 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
}
private byte[] loadKek(String kekLabel) throws KMSException {
// Check cache first
if (CacheEnabled.value()) {
byte[] cached = kekCache.get(kekLabel);
if (cached != null) {
updateLastUsed(kekLabel);
return Arrays.copyOf(cached, cached.length); // Return copy
}
}
// Load from database
KMSDatabaseKekObjectVO kekObject = kekObjectDao.findByLabel(kekLabel);
if (kekObject == null || kekObject.getRemoved() != null) {
@ -346,23 +288,15 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
}
try {
// Decrypt the key material
byte[] encryptedKekBytes = kekObject.getKeyMaterial();
if (encryptedKekBytes == null || encryptedKekBytes.length == 0) {
throw KMSException.kekNotFound("KEK value is empty for label " + kekLabel);
}
// Decrypt using DBEncryptionUtil
String encryptedKek = new String(encryptedKekBytes, StandardCharsets.UTF_8);
String kekBase64 = DBEncryptionUtil.decrypt(encryptedKek);
byte[] kekBytes = Base64.getDecoder().decode(kekBase64);
// Cache for future use
if (CacheEnabled.value()) {
kekCache.put(kekLabel, Arrays.copyOf(kekBytes, kekBytes.length));
}
// Update last used timestamp
updateLastUsed(kekLabel);
return kekBytes;
@ -370,7 +304,8 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
} catch (IllegalArgumentException e) {
throw KMSException.kekOperationFailed("Invalid KEK encoding for label " + kekLabel, e);
} catch (Exception e) {
throw KMSException.kekOperationFailed("Failed to decrypt KEK for label " + kekLabel + ": " + e.getMessage(), e);
throw KMSException.kekOperationFailed("Failed to decrypt KEK for label " + kekLabel + ": " + e.getMessage(),
e);
}
}
@ -389,4 +324,14 @@ public class DatabaseKMSProvider extends AdapterBase implements KMSProvider {
private String generateKekLabel(KeyPurpose purpose) {
return purpose.getName() + "-kek-" + UUID.randomUUID().toString().substring(0, 8);
}
@Override
public String getConfigComponentName() {
return DatabaseKMSProvider.class.getSimpleName();
}
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[0];
}
}

View File

@ -50,7 +50,7 @@ public class KMSDatabaseKekObjectVO {
@Column(name = "id")
private Long id;
@Column(name = "uuid", nullable = false, unique = true)
@Column(name = "uuid", nullable = false)
private String uuid;
// PKCS#11 Object Class (CKA_CLASS)
@ -134,17 +134,12 @@ public class KMSDatabaseKekObjectVO {
@Temporal(TemporalType.TIMESTAMP)
private Date removed;
public KMSDatabaseKekObjectVO() {
this.uuid = UUID.randomUUID().toString();
this.created = new Date();
}
/**
* Constructor for creating a new KEK object
*
* @param label PKCS#11 label (CKA_LABEL)
* @param purpose key purpose
* @param keyBits key size in bits
* @param label PKCS#11 label (CKA_LABEL)
* @param purpose key purpose
* @param keyBits key size in bits
* @param keyMaterial encrypted key material (CKA_VALUE)
*/
public KMSDatabaseKekObjectVO(String label, KeyPurpose purpose, Integer keyBits, byte[] keyMaterial) {
@ -157,6 +152,11 @@ public class KMSDatabaseKekObjectVO {
this.startDate = new Date();
}
public KMSDatabaseKekObjectVO() {
this.uuid = UUID.randomUUID().toString();
this.created = new Date();
}
public Long getId() {
return id;
}

View File

@ -14,6 +14,5 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
name=database-kms
parent=kms

View File

@ -69,22 +69,19 @@ import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
private static final Logger logger = LogManager.getLogger(PKCS11HSMProvider.class);
private static final String PROVIDER_NAME = "pkcs11";
// AES-CBC with PKCS5Padding: FIPS-compliant (NIST SP 800-38A) with universal PKCS#11 support
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
// Constants for session management
private static final long SESSION_ACQUIRE_TIMEOUT_MS = 5000L;
private static final int MAX_SESSION_RETRIES = 3;
private static final long RETRY_BACKOFF_BASE_MS = 100L;
// Valid key sizes for AES
private static final int[] VALID_KEY_SIZES = {128, 192, 256};
// Session pool per HSM profile
private final Map<Long, HSMSessionPool> sessionPools = new ConcurrentHashMap<>();
// Profile configuration caching
private final Map<Long, Map<String, String>> profileConfigCache = new ConcurrentHashMap<>();
@Inject
private HSMProfileDao hsmProfileDao;
@Inject
@ -107,19 +104,8 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
if (hsmProfileId == null) {
throw KMSException.invalidParameter("HSM Profile ID is required for PKCS#11 provider");
}
if (StringUtils.isEmpty(label)) {
label = generateKekLabel(purpose);
}
HSMSessionPool pool = getSessionPool(hsmProfileId);
PKCS11Session session = null;
try {
session = pool.acquireSession(5000);
return session.generateKey(label, keyBits, purpose);
} finally {
pool.releaseSession(session);
}
final String kekLabel = StringUtils.isEmpty(label) ? generateKekLabel(purpose) : label;
return executeWithSession(hsmProfileId, session -> session.generateKey(kekLabel, keyBits, purpose));
}
@Override
@ -133,10 +119,8 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
@Override
public boolean isKekAvailable(String kekId) throws KMSException {
Long hsmProfileId = resolveProfileId(kekId);
if (hsmProfileId == null) return false;
try {
Long hsmProfileId = resolveProfileId(kekId);
return executeWithSession(hsmProfileId, session -> session.checkKeyExists(kekId));
} catch (Exception e) {
return false;
@ -151,7 +135,7 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
}
byte[] wrappedBlob = executeWithSession(hsmProfileId, session -> session.wrapKey(plainDek, kekLabel));
return new WrappedKey(kekLabel, purpose, "AES/GCM/NoPadding", wrappedBlob, PROVIDER_NAME, new Date(), null);
return new WrappedKey(kekLabel, purpose, CIPHER_ALGORITHM, wrappedBlob, PROVIDER_NAME, new Date(), null);
}
@Override
@ -167,34 +151,25 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
@Override
public WrappedKey generateAndWrapDek(KeyPurpose purpose, String kekLabel, int keyBits,
Long hsmProfileId) throws KMSException {
// Generate random DEK
byte[] dekBytes = new byte[keyBits / 8];
new SecureRandom().nextBytes(dekBytes);
try {
return wrapKey(dekBytes, purpose, kekLabel, hsmProfileId);
} finally {
java.util.Arrays.fill(dekBytes, (byte) 0);
Arrays.fill(dekBytes, (byte) 0);
}
}
@Override
public WrappedKey rewrapKey(WrappedKey oldWrappedKey, String newKekLabel,
Long targetHsmProfileId) throws KMSException {
// 1. Unwrap with old KEK
byte[] plainKey = unwrapKey(oldWrappedKey, null); // Auto-resolve old profile
byte[] plainKey = unwrapKey(oldWrappedKey, null);
try {
// 2. Wrap with new KEK
Long profileId = targetHsmProfileId;
if (profileId == null) {
profileId = resolveProfileId(newKekLabel);
}
Long profileId = targetHsmProfileId != null ? targetHsmProfileId : resolveProfileId(newKekLabel);
return wrapKey(plainKey, oldWrappedKey.getPurpose(), newKekLabel, profileId);
} finally {
// Zeroize plaintext key
java.util.Arrays.fill(plainKey, (byte) 0);
Arrays.fill(plainKey, (byte) 0);
}
}
@ -216,17 +191,14 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
*/
@Override
public boolean healthCheck() throws KMSException {
// Test connectivity to at least one configured HSM profile
if (sessionPools.isEmpty()) {
logger.debug("No HSM profiles configured for health check");
return true; // No profiles means nothing to check
return true;
}
boolean allHealthy = true;
for (Map.Entry<Long, HSMSessionPool> entry : sessionPools.entrySet()) {
Long profileId = entry.getKey();
HSMSessionPool pool = entry.getValue();
if (!checkProfileHealth(profileId, pool)) {
for (Long profileId : sessionPools.keySet()) {
if (!checkProfileHealth(profileId)) {
allHealthy = false;
}
}
@ -235,35 +207,21 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
throw KMSException.healthCheckFailed("One or more HSM profiles failed health check", null);
}
return allHealthy;
return true;
}
/**
* Checks health of a single HSM profile.
*
* @param profileId HSM profile ID
* @param pool Session pool for the profile
* @return true if profile is healthy, false otherwise
*/
private boolean checkProfileHealth(Long profileId, HSMSessionPool pool) {
private boolean checkProfileHealth(Long profileId) {
try {
PKCS11Session testSession = pool.acquireSession(SESSION_ACQUIRE_TIMEOUT_MS);
if (testSession == null || !testSession.isValid()) {
logger.warn("Health check failed for HSM profile {}: Could not acquire valid session", profileId);
return false;
}
try {
if (testSession.keyStore != null) {
testSession.keyStore.size(); // Lightweight operation
logger.debug("Health check passed for HSM profile {}", profileId);
return true;
} else {
logger.warn("Health check failed for HSM profile {}: KeyStore is null", profileId);
Boolean result = executeWithSession(profileId, session -> {
try {
session.keyStore.size(); // Verify the HSM token is currently reachable
} catch (KeyStoreException e) {
return false;
}
} finally {
pool.releaseSession(testSession);
}
return true;
});
logger.debug("Health check {} for HSM profile {}", result ? "passed" : "failed", profileId);
return result;
} catch (Exception e) {
logger.warn("Health check failed for HSM profile {}: {}", profileId, e.getMessage(), e);
return false;
@ -299,25 +257,25 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
}
HSMSessionPool getSessionPool(Long profileId) {
return sessionPools.computeIfAbsent(profileId,
id -> new HSMSessionPool(id, loadProfileConfig(id)));
return sessionPools.computeIfAbsent(profileId, id -> {
Map<String, String> config = loadProfileConfig(id);
int maxSessions = Integer.parseInt(config.getOrDefault("max_sessions", "10"));
return new HSMSessionPool(id, maxSessions, this);
});
}
Map<String, String> loadProfileConfig(Long profileId) {
return profileConfigCache.computeIfAbsent(profileId, id -> {
List<HSMProfileDetailsVO> details = hsmProfileDetailsDao.listByProfileId(id);
Map<String, String> config = new HashMap<>();
for (HSMProfileDetailsVO detail : details) {
String value = detail.getValue();
if (isSensitiveKey(detail.getName())) {
value = DBEncryptionUtil.decrypt(value);
}
config.put(detail.getName(), value);
List<HSMProfileDetailsVO> details = hsmProfileDetailsDao.listByProfileId(profileId);
Map<String, String> config = new HashMap<>();
for (HSMProfileDetailsVO detail : details) {
String value = detail.getValue();
if (isSensitiveKey(detail.getName())) {
value = DBEncryptionUtil.decrypt(value);
}
// Validate configuration
validateProfileConfig(config);
return config;
});
config.put(detail.getName(), value);
}
validateProfileConfig(config);
return config;
}
/**
@ -329,27 +287,23 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
* <li>{@code slot} or {@code token_label}: At least one required</li>
* <li>{@code pin}: Required for HSM authentication</li>
* <li>{@code max_sessions}: Optional, must be positive integer if provided</li>
* <li>{@code min_idle_sessions}: Optional, must be non-negative integer if provided</li>
* </ul>
*
* @param config Configuration map from HSM profile details
* @throws KMSException with {@code INVALID_PARAMETER} if validation fails
*/
void validateProfileConfig(Map<String, String> config) throws KMSException {
// Validate required config keys
String libraryPath = config.get("library");
if (StringUtils.isEmpty(libraryPath)) {
throw KMSException.invalidParameter("library is required for PKCS#11 HSM profile");
}
// Validate slot or token_label (at least one required)
String slot = config.get("slot");
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");
}
// Validate slot is numeric if provided
if (!StringUtils.isEmpty(slot)) {
try {
Integer.parseInt(slot);
@ -358,24 +312,19 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
}
}
// Validate PIN is present
String pin = config.get("pin");
if (StringUtils.isEmpty(pin)) {
throw KMSException.invalidParameter("pin is required for PKCS#11 HSM profile");
}
// Validate library points to existing file (if accessible)
File libraryFile = new File(libraryPath);
if (!libraryFile.exists() && !libraryFile.isAbsolute()) {
// Try to find in common library paths, but don't fail if not found
// The HSM library might be in system library path
// The HSM library might be in the system library path
logger.debug("Library path {} does not exist as absolute path, will rely on system library path",
libraryPath);
}
// Validate max_sessions and min_idle_sessions if provided
parsePositiveInteger(config, "max_sessions", "max_sessions");
parseNonNegativeInteger(config, "min_idle_sessions", "min_idle_sessions");
}
/**
@ -403,32 +352,6 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
}
}
/**
* Parses a non-negative 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 negative
*/
private int parseNonNegativeInteger(Map<String, String> 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 non-negative");
}
return parsed;
} catch (NumberFormatException e) {
throw KMSException.invalidParameter(errorPrefix + " must be a valid integer: " + value);
}
}
boolean isSensitiveKey(String key) {
return key.equalsIgnoreCase("pin") ||
key.equalsIgnoreCase("password") ||
@ -440,11 +363,6 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
return purpose.getName() + "-kek-" + UUID.randomUUID().toString().substring(0, 8);
}
/**
* @return The name of the component that provided this configuration
* variable. This value is saved in the database so someone can easily
* identify who provides this variable.
**/
@Override
public String getConfigComponentName() {
return PKCS11HSMProvider.class.getSimpleName();
@ -455,87 +373,105 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
return new ConfigKey<?>[0];
}
/**
* Functional interface for operations that require a PKCS#11 session.
*/
@Override
public void invalidateProfileCache(Long profileId) {
HSMSessionPool pool = sessionPools.remove(profileId);
if (pool != null) {
pool.invalidate();
}
logger.info("Invalidated HSM session pool for profile {}", profileId);
}
@FunctionalInterface
private interface SessionOperation<T> {
T execute(PKCS11Session session) throws KMSException;
}
// Inner class for session pooling
private static class HSMSessionPool {
private final BlockingQueue<PKCS11Session> availableSessions;
private final Long profileId;
private final Map<String, String> config;
private final PKCS11HSMProvider provider;
private final int maxSessions;
private final int minIdleSessions;
// Counts total sessions (idle + active). Acquired on creation, released on close.
private final Semaphore sessionPermits;
private volatile boolean invalidated = false;
HSMSessionPool(Long profileId, Map<String, String> config) {
HSMSessionPool(Long profileId, int maxSessions, PKCS11HSMProvider provider) {
this.profileId = profileId;
this.config = config;
this.maxSessions = Integer.parseInt(config.getOrDefault("max_sessions", "10"));
this.minIdleSessions = Integer.parseInt(config.getOrDefault("min_idle_sessions", "2"));
this.provider = provider;
this.maxSessions = maxSessions;
this.sessionPermits = new Semaphore(maxSessions);
this.availableSessions = new ArrayBlockingQueue<>(maxSessions);
// Pre-warm
for (int i = 0; i < minIdleSessions; i++) {
try {
availableSessions.offer(createNewSession());
} catch (Exception e) {
logger.warn("Failed to pre-warm session for profile {}: {}", profileId, e.getMessage());
}
}
}
private PKCS11Session createNewSession() throws KMSException {
return new PKCS11Session(config);
// Config (including decrypted PIN) is loaded fresh each time and not stored.
return new PKCS11Session(provider.loadProfileConfig(profileId));
}
PKCS11Session acquireSession(long timeoutMs) throws KMSException {
// Retry logic for session creation
Exception lastException = null;
for (int attempt = 0; attempt < MAX_SESSION_RETRIES; attempt++) {
try {
PKCS11Session session = availableSessions.poll();
if (session == null || !session.isValid()) {
if (session != null) {
session.close();
}
session = createNewSession();
}
// Try to get an existing idle session first (no semaphore change: it already owns a permit).
PKCS11Session session = availableSessions.poll();
if (session != null) {
if (session.isValid()) {
return session;
} catch (Exception e) {
lastException = e;
if (attempt < MAX_SESSION_RETRIES - 1) {
// Exponential backoff: 100ms, 200ms, 400ms
long backoffMs = RETRY_BACKOFF_BASE_MS * (1L << attempt);
try {
Thread.sleep(backoffMs);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new KMSException(KMSException.ErrorType.CONNECTION_FAILED,
"Interrupted while waiting to retry HSM session acquisition", ie);
}
logger.debug("Retrying HSM session acquisition for profile {} (attempt {}/{})",
profileId, attempt + 2, MAX_SESSION_RETRIES);
}
}
// Stale idle session: discard it and free its permit so a new one can be created.
session.close();
sessionPermits.release();
}
// All retries failed
logger.error("Failed to acquire HSM session for profile {} after {} attempts", profileId,
MAX_SESSION_RETRIES);
throw new KMSException(KMSException.ErrorType.CONNECTION_FAILED,
"Failed to acquire HSM session after " + MAX_SESSION_RETRIES + " attempts", lastException);
// Acquire a permit to create a new session, blocking up to timeoutMs if at capacity.
try {
if (!sessionPermits.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS)) {
// One last try: a session may have been returned while we were waiting.
session = availableSessions.poll();
if (session != null && session.isValid()) {
return session;
}
if (session != null) {
session.close();
sessionPermits.release();
}
throw new KMSException(KMSException.ErrorType.CONNECTION_FAILED,
"Timed out waiting for an available HSM session for profile " + profileId
+ " (max=" + maxSessions + ", timeout=" + timeoutMs + "ms)");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new KMSException(KMSException.ErrorType.CONNECTION_FAILED,
"Interrupted while waiting to acquire HSM session for profile " + profileId, e);
}
try {
return createNewSession();
} catch (KMSException e) {
sessionPermits.release();
throw e;
}
}
void releaseSession(PKCS11Session session) {
if (session != null && session.isValid()) {
if (!availableSessions.offer(session)) {
session.close(); // Pool full
}
if (session == null) return;
if (!invalidated && session.isValid() && availableSessions.offer(session)) {
return; // session returned to the idle pool; permit stays consumed
}
// Pool is invalidated, session is stale, or the idle queue is full: close immediately.
session.close();
sessionPermits.release();
}
/**
* Marks the pool as invalidated and closes all idle sessions.
* Any session currently checked out will be closed (and its permit released) when
* it is returned via {@link #releaseSession} the invalidated flag prevents re-pooling.
*/
void invalidate() {
invalidated = true;
PKCS11Session session;
while ((session = availableSessions.poll()) != null) {
session.close();
sessionPermits.release();
}
}
}
@ -547,8 +483,8 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
* <p>Key operations supported:
* <ul>
* <li>Key generation: Generate AES keys directly in the HSM</li>
* <li>Key wrapping: Encrypt DEKs using KEKs stored in the HSM (AES-GCM)</li>
* <li>Key unwrapping: Decrypt DEKs using KEKs stored in the HSM (AES-GCM)</li>
* <li>Key wrapping: Encrypt DEKs using KEKs stored in the HSM (AES-CBC/PKCS5Padding)</li>
* <li>Key unwrapping: Decrypt DEKs using KEKs stored in the HSM (AES-CBC/PKCS5Padding)</li>
* <li>Key deletion: Remove keys from the HSM</li>
* <li>Key existence check: Verify if a key exists in the HSM</li>
* </ul>
@ -564,13 +500,8 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
* {@link KMSException.ErrorType} values for proper retry logic and error reporting.
*/
private static class PKCS11Session {
// Use AES-CBC with PKCS5Padding for key wrapping
// This is FIPS-compliant (NIST SP 800-38A) and has universal PKCS#11 support
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final int IV_LENGTH = 16; // 128 bits for CBC
private static final String PROVIDER_PREFIX = "CloudStackPKCS11-";
private static final int IV_LENGTH = 16; // 128 bits for CBC mode
private final Map<String, String> config;
private KeyStore keyStore;
private Provider provider;
private String providerName;
@ -578,13 +509,14 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
/**
* Creates a new PKCS#11 session and connects to the HSM.
* The config map (including any sensitive values such as the PIN) is used only
* during connection setup and is not retained as a field.
*
* @param config HSM profile configuration containing library, slot/token_label, and pin
* @throws KMSException if connection fails or configuration is invalid
*/
PKCS11Session(Map<String, String> config) throws KMSException {
this.config = config;
connect();
connect(config);
}
/**
@ -611,55 +543,54 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
* <li>{@code CONNECTION_FAILED} if HSM is unreachable or device error occurs</li>
* </ul>
*/
private void connect() throws KMSException {
private void connect(Map<String, String> config) throws KMSException {
try {
// Create unique provider name to avoid conflicts
providerName = PROVIDER_PREFIX + UUID.randomUUID().toString().substring(0, 8);
// Unique suffix ensures each session gets its own provider name in java.security.Security,
// allowing Security.removeProvider() in close() to target exactly this session's provider.
String nameSuffix = UUID.randomUUID().toString().substring(0, 8);
String configString = buildSunPKCS11Config(config);
String configString = buildSunPKCS11Config(config, nameSuffix);
// For Java 9+, use the recommended approach: get provider and configure with file
// Write config to temporary file (required by Java 9+ API)
// Java 9+ API: write config to temp file, then configure the provider
tempConfigFile = Files.createTempFile("pkcs11-config-", ".cfg");
try (FileWriter writer = new FileWriter(tempConfigFile.toFile(), StandardCharsets.UTF_8)) {
writer.write(configString);
}
// Get the base SunPKCS11 provider and configure it
Provider baseProvider = Security.getProvider("SunPKCS11");
if (baseProvider == null) {
throw new KMSException(KMSException.ErrorType.CONNECTION_FAILED,
"SunPKCS11 provider not available in this JVM");
}
// Configure the provider with the config file (Java 9+ API)
provider = baseProvider.configure(tempConfigFile.toAbsolutePath().toString());
// Set the provider name (it will be based on the 'name' field in config)
// Add provider to Security if not already present
if (Security.getProvider(providerName) == null) {
Security.addProvider(provider);
} else {
provider = Security.getProvider(providerName);
// Use the actual provider name so Security.removeProvider() in close() works correctly.
providerName = provider.getName();
// Security.addProvider returns -1 if a provider with this name is already registered.
// With the UUID-based suffix this should be impossible in practice; guard defensively.
if (Security.addProvider(provider) < 0) {
throw new KMSException(KMSException.ErrorType.CONNECTION_FAILED,
"Failed to register PKCS#11 provider '" + providerName + "': name already in use");
}
// Load PKCS#11 KeyStore
keyStore = KeyStore.getInstance("PKCS11", provider);
// Get PIN for authentication
String pin = config.get("pin");
if (StringUtils.isEmpty(pin)) {
throw KMSException.invalidParameter("pin is required");
}
char[] pinChars = pin.toCharArray();
// Load KeyStore with PIN (this authenticates to the HSM)
keyStore.load(null, pinChars);
// Zeroize PIN from memory
Arrays.fill(pinChars, '\0');
logger.debug("aSuccessfully connected to PKCS#11 HSM at {}", config.get("library"));
// The temp file is only needed during configure()/load(); delete it immediately
// rather than holding it until the session is eventually closed.
Files.deleteIfExists(tempConfigFile);
tempConfigFile = null;
logger.debug("Successfully connected to PKCS#11 HSM at {}", config.get("library"));
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException e) {
handlePKCS11Exception(e, "Failed to initialize PKCS#11 connection");
} catch (IOException e) {
@ -684,14 +615,17 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
* @return Configuration string for SunPKCS11 provider
* @throws KMSException if required configuration is missing
*/
private String buildSunPKCS11Config(Map<String, String> config) throws KMSException {
private String buildSunPKCS11Config(Map<String, String> config, String nameSuffix) throws KMSException {
String libraryPath = config.get("library");
if (StringUtils.isEmpty(libraryPath)) {
throw KMSException.invalidParameter("library is required");
}
StringBuilder configBuilder = new StringBuilder();
configBuilder.append("name=CloudStackHSM\n");
// Include the unique suffix so that each session is registered under a distinct
// provider name (SunPKCS11-CloudStackHSM-{suffix}), preventing name collisions
// across concurrent sessions and allowing clean removal via Security.removeProvider().
configBuilder.append("name=CloudStackHSM-").append(nameSuffix).append("\n");
configBuilder.append("library=").append(libraryPath).append("\n");
String tokenLabel = config.get("token_label");
@ -734,7 +668,6 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
}
logger.warn("PKCS#11 error: {} - {}", errorMsg, context, e);
// Map PKCS#11 error codes to KMSException types
if (errorMsg.contains("CKR_PIN_INCORRECT") || errorMsg.contains("PIN_INCORRECT")) {
throw new KMSException(KMSException.ErrorType.AUTHENTICATION_FAILED,
context + ": Incorrect PIN", e);
@ -773,17 +706,14 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
*/
boolean isValid() {
try {
// Check if KeyStore object is not null
if (keyStore == null) {
return false;
}
// Check if Provider is still registered in Security
if (provider == null || Security.getProvider(provider.getName()) == null) {
return false;
}
// Test with a lightweight HSM operation (get KeyStore size)
keyStore.size();
return true;
} catch (Exception e) {
@ -808,15 +738,10 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
*/
void close() {
try {
// Close KeyStore if it implements Closeable
if (keyStore instanceof Closeable) {
((Closeable) keyStore).close();
}
// Logout from HSM token (if supported)
// Note: SunPKCS11 KeyStore doesn't have explicit logout, but closing should handle it
// Remove provider from Security
if (provider != null && providerName != null) {
try {
Security.removeProvider(providerName);
@ -825,7 +750,6 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
}
}
// Clean up temporary config file
if (tempConfigFile != null) {
try {
Files.deleteIfExists(tempConfigFile);
@ -1060,7 +984,7 @@ public class PKCS11HSMProvider extends AdapterBase implements KMSProvider {
// Minimum size: IV (16) + at least one block of ciphertext (16)
if (wrappedBlob.length < IV_LENGTH + 16) {
throw KMSException.invalidParameter("Wrapped blob too short: expected at least " +
(IV_LENGTH + 16) + " bytes");
(IV_LENGTH + 16) + " bytes");
}
SecretKey kek = null;

View File

@ -17,18 +17,6 @@
package org.apache.cloudstack.kms.provider.pkcs11;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.Map;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.framework.kms.KeyPurpose;
import org.apache.cloudstack.kms.HSMProfileDetailsVO;
@ -43,6 +31,18 @@ import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Arrays;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Unit tests for PKCS11HSMProvider
* Tests provider-specific logic: config loading, profile resolution, sensitive key detection
@ -128,11 +128,11 @@ public class PKCS11HSMProviderTest {
when(detail2.getValue()).thenReturn("ENC(encrypted_pin)");
HSMProfileDetailsVO detail3 = mock(HSMProfileDetailsVO.class);
when(detail3.getName()).thenReturn("slot_id");
when(detail3.getName()).thenReturn("slot");
when(detail3.getValue()).thenReturn("0");
when(hsmProfileDetailsDao.listByProfileId(testProfileId)).thenReturn(
Arrays.asList(detail1, detail2, detail3));
Arrays.asList(detail1, detail2, detail3));
// Test
Map<String, String> config = provider.loadProfileConfig(testProfileId);
@ -144,7 +144,7 @@ public class PKCS11HSMProviderTest {
// Note: In real code, DBEncryptionUtil.decrypt would be called
// Here we just verify the structure is correct
assertTrue("Config should contain pin", config.containsKey("pin"));
assertEquals("0", config.get("slot_id"));
assertEquals("0", config.get("slot"));
verify(hsmProfileDetailsDao).listByProfileId(testProfileId);
}
@ -152,17 +152,13 @@ public class PKCS11HSMProviderTest {
/**
* Test: loadProfileConfig handles empty details
*/
@Test
@Test(expected = KMSException.class)
public void testLoadProfileConfig_HandlesEmptyDetails() {
// Setup
when(hsmProfileDetailsDao.listByProfileId(testProfileId)).thenReturn(Arrays.asList());
// Test
Map<String, String> config = provider.loadProfileConfig(testProfileId);
// Verify
assertNotNull("Config should not be null", config);
assertEquals(0, config.size());
}
/**
@ -201,7 +197,8 @@ public class PKCS11HSMProviderTest {
// Verify
assertNotNull("Label should not be null", label);
assertTrue("Label should start with purpose", label.startsWith(KeyPurpose.VOLUME_ENCRYPTION.getName()));
assertTrue("Label should contain UUID", label.length() > (KeyPurpose.VOLUME_ENCRYPTION.getName() + "-kek-").length());
assertTrue("Label should contain UUID",
label.length() > (KeyPurpose.VOLUME_ENCRYPTION.getName() + "-kek-").length());
}
/**
@ -218,42 +215,30 @@ public class PKCS11HSMProviderTest {
@Test(expected = KMSException.class)
public void testCreateKek_RequiresProfileId() throws KMSException {
provider.createKek(
KeyPurpose.VOLUME_ENCRYPTION,
"test-label",
256,
null // null profile ID should throw exception
KeyPurpose.VOLUME_ENCRYPTION,
"test-label",
256,
null // null profile ID should throw exception
);
}
/**
* Test: loadProfileConfig caches configuration
*/
@Test
public void testLoadProfileConfig_CachesConfiguration() {
// Setup
HSMProfileDetailsVO detail = mock(HSMProfileDetailsVO.class);
when(detail.getName()).thenReturn("library");
when(detail.getValue()).thenReturn("/path/to/lib.so");
when(hsmProfileDetailsDao.listByProfileId(testProfileId)).thenReturn(Arrays.asList(detail));
// Load twice
provider.loadProfileConfig(testProfileId);
provider.loadProfileConfig(testProfileId);
// DAO should only be called once due to caching
verify(hsmProfileDetailsDao, times(1)).listByProfileId(testProfileId);
}
/**
* Test: getSessionPool creates pool for new profile
*/
@Test
public void testGetSessionPool_CreatesPoolForNewProfile() {
// Setup
HSMProfileDetailsVO detail = mock(HSMProfileDetailsVO.class);
when(detail.getName()).thenReturn("library");
when(detail.getValue()).thenReturn("/path/to/lib.so");
when(hsmProfileDetailsDao.listByProfileId(testProfileId)).thenReturn(Arrays.asList(detail));
HSMProfileDetailsVO libraryDetail = mock(HSMProfileDetailsVO.class);
when(libraryDetail.getName()).thenReturn("library");
when(libraryDetail.getValue()).thenReturn("/path/to/lib.so");
HSMProfileDetailsVO slotDetail = mock(HSMProfileDetailsVO.class);
when(slotDetail.getName()).thenReturn("slot");
when(slotDetail.getValue()).thenReturn("1");
HSMProfileDetailsVO pinDetail = mock(HSMProfileDetailsVO.class);
when(pinDetail.getName()).thenReturn("pin");
when(pinDetail.getValue()).thenReturn("1234");
when(hsmProfileDetailsDao.listByProfileId(testProfileId)).thenReturn(
Arrays.asList(libraryDetail, slotDetail, pinDetail));
// Test
Object pool = provider.getSessionPool(testProfileId);
@ -269,10 +254,17 @@ public class PKCS11HSMProviderTest {
@Test
public void testGetSessionPool_ReusesPoolForSameProfile() {
// Setup
HSMProfileDetailsVO detail = mock(HSMProfileDetailsVO.class);
when(detail.getName()).thenReturn("library");
when(detail.getValue()).thenReturn("/path/to/lib.so");
when(hsmProfileDetailsDao.listByProfileId(testProfileId)).thenReturn(Arrays.asList(detail));
HSMProfileDetailsVO libraryDetail = mock(HSMProfileDetailsVO.class);
when(libraryDetail.getName()).thenReturn("library");
when(libraryDetail.getValue()).thenReturn("/path/to/lib.so");
HSMProfileDetailsVO slotDetail = mock(HSMProfileDetailsVO.class);
when(slotDetail.getName()).thenReturn("slot");
when(slotDetail.getValue()).thenReturn("1");
HSMProfileDetailsVO pinDetail = mock(HSMProfileDetailsVO.class);
when(pinDetail.getName()).thenReturn("pin");
when(pinDetail.getValue()).thenReturn("1234");
when(hsmProfileDetailsDao.listByProfileId(testProfileId)).thenReturn(
Arrays.asList(libraryDetail, slotDetail, pinDetail));
// Test
Object pool1 = provider.getSessionPool(testProfileId);

View File

@ -81,7 +81,7 @@ public class ManagementServerMaintenanceManagerImplTest {
Mockito.doReturn(expectedCount).when(jobManagerMock).countPendingNonPseudoJobs(1L);
return expectedCount;
}
@Test
public void countPendingJobs() {
long expectedCount = prepareCountPendingJobs();

View File

@ -115,7 +115,6 @@ import org.apache.cloudstack.api.response.HypervisorGuestOsResponse;
import org.apache.cloudstack.api.response.IPAddressResponse;
import org.apache.cloudstack.api.response.ImageStoreResponse;
import org.apache.cloudstack.api.response.InstanceGroupResponse;
import org.apache.cloudstack.api.response.KMSKeyResponse;
import org.apache.cloudstack.api.response.InternalLoadBalancerElementResponse;
import org.apache.cloudstack.api.response.IpForwardingRuleResponse;
import org.apache.cloudstack.api.response.IpQuarantineResponse;
@ -219,6 +218,7 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
import org.apache.cloudstack.framework.jobs.AsyncJob;
import org.apache.cloudstack.framework.jobs.AsyncJobManager;
import org.apache.cloudstack.gui.theme.GuiThemeJoin;
import org.apache.cloudstack.kms.dao.HSMProfileDao;
import org.apache.cloudstack.management.ManagementServerHost;
import org.apache.cloudstack.network.BgpPeerVO;
import org.apache.cloudstack.network.RoutedIpv4Manager;
@ -425,7 +425,6 @@ import com.cloud.user.AccountManager;
import com.cloud.user.SSHKeyPair;
import com.cloud.user.User;
import com.cloud.user.UserAccount;
import org.apache.cloudstack.kms.KMSKey;
import com.cloud.user.UserData;
import com.cloud.user.UserStatisticsVO;
import com.cloud.user.dao.UserDataDao;
@ -531,6 +530,8 @@ public class ApiResponseHelper implements ResponseGenerator {
private ASNumberRangeDao asNumberRangeDao;
@Inject
private ASNumberDao asNumberDao;
@Inject
private HSMProfileDao hsmProfileDao;
@Inject
ObjectStoreDao _objectStoreDao;

View File

@ -2678,6 +2678,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
Long clusterId = cmd.getClusterId();
Long serviceOfferingId = cmd.getServiceOfferingId();
Long diskOfferingId = cmd.getDiskOfferingId();
Long kmsKeyId = cmd.getKmsKeyId();
Boolean display = cmd.getDisplay();
String state = cmd.getState();
boolean shouldListSystemVms = shouldListSystemVms(cmd, caller.getId());
@ -2714,6 +2715,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
volumeSearchBuilder.and("uuid", volumeSearchBuilder.entity().getUuid(), SearchCriteria.Op.NNULL);
volumeSearchBuilder.and("instanceId", volumeSearchBuilder.entity().getInstanceId(), SearchCriteria.Op.EQ);
volumeSearchBuilder.and("dataCenterId", volumeSearchBuilder.entity().getDataCenterId(), SearchCriteria.Op.EQ);
volumeSearchBuilder.and("kmsKeyId", volumeSearchBuilder.entity().getKmsKeyId(), SearchCriteria.Op.EQ);
if (cmd.isEncrypted() != null) {
if (cmd.isEncrypted()) {
volumeSearchBuilder.and("encryptFormat", volumeSearchBuilder.entity().getEncryptFormat(), SearchCriteria.Op.NNULL);
@ -2838,6 +2840,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
if (vmInstanceId != null) {
sc.setParameters("instanceId", vmInstanceId);
}
if (kmsKeyId != null) {
sc.setParameters("kmsKeyId", kmsKeyId);
}
if (zoneId != null) {
sc.setParameters("dataCenterId", zoneId);
}

View File

@ -30,7 +30,6 @@ import org.apache.cloudstack.api.response.VolumeResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.kms.KMSKekVersionVO;
import org.apache.cloudstack.kms.KMSKeyVO;
import org.apache.cloudstack.kms.KMSWrappedKeyVO;
import org.apache.cloudstack.kms.dao.KMSKekVersionDao;
import org.apache.cloudstack.kms.dao.KMSKeyDao;
@ -296,12 +295,9 @@ public class VolumeJoinDaoImpl extends GenericDaoBaseWithTagInformation<VolumeJo
volResponse.setObjectName("volume");
volResponse.setExternalUuid(volume.getExternalUuid());
volResponse.setEncryptionFormat(volume.getEncryptionFormat());
if (volume.getKmsKeyId() != null) {
KMSKeyVO kmsKey = kmsKeyDao.findById(volume.getKmsKeyId());
if (kmsKey != null) {
volResponse.setKmsKeyId(kmsKey.getUuid());
}
}
volResponse.setKmsKeyId(volume.getKmsKeyUuid());
volResponse.setKmsKey(volume.getKmsKeyName());
if (volume.getKmsWrappedKeyId() != null) {
KMSWrappedKeyVO wrappedKey = kmsWrappedKeyDao.findById(volume.getKmsWrappedKeyId());
if (wrappedKey != null) {

View File

@ -283,6 +283,12 @@ public class VolumeJoinVO extends BaseViewWithTagInformationVO implements Contro
@Column(name = "kms_key_id")
private Long kmsKeyId;
@Column(name = "kms_key_uuid")
private String kmsKeyUuid;
@Column(name = "kms_key_name")
private String kmsKeyName;
@Column(name = "kms_wrapped_key_id")
private Long kmsWrappedKeyId;
@ -632,6 +638,14 @@ public class VolumeJoinVO extends BaseViewWithTagInformationVO implements Contro
return kmsKeyId;
}
public String getKmsKeyName() {
return kmsKeyName;
}
public String getKmsKeyUuid() {
return kmsKeyUuid;
}
public Long getKmsWrappedKeyId() {
return kmsWrappedKeyId;
}

View File

@ -98,6 +98,7 @@ import org.apache.cloudstack.resourcedetail.DiskOfferingDetailVO;
import org.apache.cloudstack.resourcedetail.SnapshotPolicyDetailVO;
import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao;
import org.apache.cloudstack.resourcedetail.dao.SnapshotPolicyDetailsDao;
import org.apache.cloudstack.kms.KMSManager;
import org.apache.cloudstack.snapshot.SnapshotHelper;
import org.apache.cloudstack.storage.command.AttachAnswer;
import org.apache.cloudstack.storage.command.AttachCommand;
@ -370,6 +371,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
@Inject
private VMSnapshotDetailsDao vmSnapshotDetailsDao;
@Inject
private KMSManager kmsManager;
public static final String KVM_FILE_BASED_STORAGE_SNAPSHOT = "kvmFileBasedStorageSnapshot";
@ -962,6 +965,10 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
String userSpecifiedName = getVolumeNameFromCommand(cmd);
if (cmd.getKmsKeyId() != null) {
kmsManager.checkKmsKeyForVolumeEncryption(caller, cmd.getKmsKeyId(), zoneId);
}
return commitVolume(cmd.getSnapshotId(), caller, owner, displayVolume, zoneId, diskOfferingId, provisioningType, size, minIops, maxIops, parentVolume, userSpecifiedName,
_uuidMgr.generateUuid(Volume.class, cmd.getCustomId()), details, cmd.getKmsKeyId());
}

View File

@ -111,6 +111,7 @@ import org.apache.cloudstack.backup.BackupVO;
import org.apache.cloudstack.backup.dao.BackupDao;
import org.apache.cloudstack.backup.dao.BackupScheduleDao;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.kms.KMSManager;
import org.apache.cloudstack.engine.cloud.entity.api.VirtualMachineEntity;
import org.apache.cloudstack.engine.cloud.entity.api.db.dao.VMNetworkMapDao;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
@ -475,6 +476,8 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
@Inject
private AccountManager _accountMgr;
@Inject
private KMSManager kmsManager;
@Inject
private AccountService _accountService;
@Inject
private ClusterDao _clusterDao;
@ -4222,6 +4225,13 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
throw new InvalidParameterValueException("Root volume encryption is not supported for hypervisor type " + hypervisorType);
}
kmsManager.checkKmsKeyForVolumeEncryption(caller, rootDiskKmsKeyId, zone.getId());
if (dataDiskInfoList != null) {
for (VmDiskInfo diskInfo : dataDiskInfoList) {
kmsManager.checkKmsKeyForVolumeEncryption(caller, diskInfo.getKmsKeyId(), zone.getId());
}
}
UserVm vm = getCheckedUserVmResource(zone, hostName, displayName, owner, diskOfferingId, diskSize, dataDiskInfoList, networkList, securityGroupIdList, group, httpmethod, userData, userDataId, userDataDetails, sshKeyPairs, caller, requestedIps, defaultIps, isDisplayVm, keyboard, affinityGroupIdList, customParameters, customId, dhcpOptionMap, datadiskTemplateToDiskOfferringMap, userVmOVFPropertiesMap, dynamicScalingEnabled, vmType, template, hypervisorType, accountId, offering, isIso, rootDiskOfferingId, rootDiskKmsKeyId, volumesSize, volume, snapshot);
_securityGroupMgr.addInstanceToGroups(vm, securityGroupIdList);

View File

@ -17,15 +17,9 @@
package org.apache.cloudstack.kms;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.domain.dao.DomainDao;
import com.cloud.user.AccountManager;
import org.apache.cloudstack.api.response.HSMProfileResponse;
import org.apache.cloudstack.kms.dao.HSMProfileDao;
import org.apache.cloudstack.kms.dao.HSMProfileDetailsDao;
@ -36,7 +30,14 @@ import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import com.cloud.user.AccountManager;
import java.util.Arrays;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Unit tests for HSM-related business logic in KMSManagerImpl
@ -45,20 +46,20 @@ import com.cloud.user.AccountManager;
@RunWith(MockitoJUnitRunner.class)
public class KMSManagerImplHSMTest {
private final Long testAccountId = 100L;
@Spy
@InjectMocks
private KMSManagerImpl kmsManager;
@Mock
private HSMProfileDao hsmProfileDao;
@Mock
private HSMProfileDetailsDao hsmProfileDetailsDao;
@Mock
private AccountManager accountManager;
private Long testAccountId = 100L;
@Mock
private DataCenterDao dataCenterDao;
@Mock
private DomainDao domainDao;
/**
* Test: isSensitiveKey correctly identifies "pin" as sensitive

View File

@ -17,6 +17,23 @@
package org.apache.cloudstack.kms;
import com.cloud.event.ActionEventUtils;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.framework.kms.KMSProvider;
import org.apache.cloudstack.framework.kms.KeyPurpose;
import org.apache.cloudstack.kms.dao.HSMProfileDao;
import org.apache.cloudstack.kms.dao.KMSKekVersionDao;
import org.apache.cloudstack.kms.dao.KMSKeyDao;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
@ -28,21 +45,6 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.framework.kms.KMSProvider;
import org.apache.cloudstack.framework.kms.KeyPurpose;
import org.apache.cloudstack.kms.dao.HSMProfileDao;
import org.apache.cloudstack.kms.dao.KMSKekVersionDao;
import org.apache.cloudstack.kms.dao.KMSKeyDao;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
/**
* Unit tests for KMS key creation logic in KMSManagerImpl
* Tests key creation with explicit and auto-resolved HSM profiles
@ -50,33 +52,22 @@ import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
public class KMSManagerImplKeyCreationTest {
private final Long testAccountId = 100L;
private final Long testDomainId = 1L;
private final Long testZoneId = 1L;
private final String testProviderName = "pkcs11";
@Spy
@InjectMocks
private KMSManagerImpl kmsManager;
@Mock
private KMSKeyDao kmsKeyDao;
@Mock
private KMSKekVersionDao kmsKekVersionDao;
@Mock
private HSMProfileDao hsmProfileDao;
@Mock
private KMSProvider kmsProvider;
private Long testAccountId = 100L;
private Long testDomainId = 1L;
private Long testZoneId = 1L;
private String testProviderName = "pkcs11";
@Before
public void setUp() {
// Setup provider
when(kmsProvider.getProviderName()).thenReturn(testProviderName);
}
/**
* Test: createUserKMSKey uses explicit HSM profile when provided
*/
@ -87,13 +78,12 @@ public class KMSManagerImplKeyCreationTest {
Long hsmProfileId = 10L;
HSMProfileVO profile = mock(HSMProfileVO.class);
when(profile.getAccountId()).thenReturn(testAccountId);
when(profile.getProtocol()).thenReturn(testProviderName);
when(hsmProfileDao.findById(hsmProfileId)).thenReturn(profile);
// Mock provider KEK creation
when(kmsProvider.createKek(any(KeyPurpose.class), anyString(), anyInt(), eq(hsmProfileId)))
.thenReturn("test-kek-label");
.thenReturn("test-kek-label");
// Mock DAO persist operations
KMSKeyVO mockKey = mock(KMSKeyVO.class);
@ -103,23 +93,27 @@ public class KMSManagerImplKeyCreationTest {
KMSKekVersionVO mockVersion = mock(KMSKekVersionVO.class);
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(mockVersion);
// Mock getKMSProvider to return our mock provider
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
doReturn(kmsProvider).when(kmsManager).getKMSProvider(testProviderName);
KMSKey result = kmsManager.createUserKMSKey(testAccountId, testDomainId,
testZoneId, "test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, hsmProfileId);
try (MockedStatic<ActionEventUtils> actionEventUtils = Mockito.mockStatic(ActionEventUtils.class)) {
actionEventUtils.when(() -> ActionEventUtils.onCompletedActionEvent(
Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyString(), Mockito.anyLong(),
Mockito.anyString(), Mockito.anyInt())).thenReturn(2L);
KMSKey result = kmsManager.createUserKMSKey(testAccountId, testDomainId,
testZoneId, "test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, hsmProfileId);
// Verify explicit profile was used
assertNotNull(result);
verify(hsmProfileDao).findById(hsmProfileId);
verify(kmsProvider).createKek(any(KeyPurpose.class), anyString(), eq(256), eq(hsmProfileId));
// Verify explicit profile was used
assertNotNull(result);
verify(hsmProfileDao).findById(hsmProfileId);
verify(kmsProvider).createKek(any(KeyPurpose.class), anyString(), eq(256), eq(hsmProfileId));
// Verify KMSKeyVO was created with correct profile ID
ArgumentCaptor<KMSKeyVO> keyCaptor = ArgumentCaptor.forClass(KMSKeyVO.class);
verify(kmsKeyDao).persist(keyCaptor.capture());
KMSKeyVO createdKey = keyCaptor.getValue();
assertEquals(hsmProfileId, createdKey.getHsmProfileId());
// Verify KMSKeyVO was created with correct profile ID
ArgumentCaptor<KMSKeyVO> keyCaptor = ArgumentCaptor.forClass(KMSKeyVO.class);
verify(kmsKeyDao).persist(keyCaptor.capture());
KMSKeyVO createdKey = keyCaptor.getValue();
assertEquals(hsmProfileId, createdKey.getHsmProfileId());
}
}
/**
@ -132,10 +126,8 @@ public class KMSManagerImplKeyCreationTest {
long hsmProfileId = 1L;
when(hsmProfileDao.findById(hsmProfileId)).thenReturn(null);
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
kmsManager.createUserKMSKey(testAccountId, testDomainId, testZoneId,
"test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, hsmProfileId);
"test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, hsmProfileId);
}
/**
@ -148,11 +140,10 @@ public class KMSManagerImplKeyCreationTest {
HSMProfileVO profile = mock(HSMProfileVO.class);
when(profile.getProtocol()).thenReturn(testProviderName);
when(profile.getAccountId()).thenReturn(testAccountId);
when(hsmProfileDao.findById(hsmProfileId)).thenReturn(profile);
when(kmsProvider.createKek(any(KeyPurpose.class), anyString(), anyInt(), eq(hsmProfileId)))
.thenReturn("test-kek-label");
.thenReturn("test-kek-label");
KMSKeyVO mockKey = mock(KMSKeyVO.class);
when(mockKey.getId()).thenReturn(1L);
@ -161,18 +152,24 @@ public class KMSManagerImplKeyCreationTest {
KMSKekVersionVO mockVersion = mock(KMSKekVersionVO.class);
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(mockVersion);
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
doReturn(kmsProvider).when(kmsManager).getKMSProvider(testProviderName);
kmsManager.createUserKMSKey(testAccountId, testDomainId, testZoneId,
"test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, hsmProfileId);
try (MockedStatic<ActionEventUtils> actionEventUtils = Mockito.mockStatic(ActionEventUtils.class)) {
actionEventUtils.when(() -> ActionEventUtils.onCompletedActionEvent(
Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyString(), Mockito.anyLong(),
Mockito.anyString(), Mockito.anyInt())).thenReturn(2L);
// Verify KEK version was created with correct profile ID
ArgumentCaptor<KMSKekVersionVO> versionCaptor = ArgumentCaptor.forClass(KMSKekVersionVO.class);
verify(kmsKekVersionDao).persist(versionCaptor.capture());
KMSKekVersionVO createdVersion = versionCaptor.getValue();
assertEquals(hsmProfileId, createdVersion.getHsmProfileId());
assertEquals(Integer.valueOf(1), Integer.valueOf(createdVersion.getVersionNumber()));
assertEquals("test-kek-label", createdVersion.getKekLabel());
kmsManager.createUserKMSKey(testAccountId, testDomainId, testZoneId,
"test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, hsmProfileId);
// Verify KEK version was created with correct profile ID
ArgumentCaptor<KMSKekVersionVO> versionCaptor = ArgumentCaptor.forClass(KMSKekVersionVO.class);
verify(kmsKekVersionDao).persist(versionCaptor.capture());
KMSKekVersionVO createdVersion = versionCaptor.getValue();
assertEquals(hsmProfileId, createdVersion.getHsmProfileId());
assertEquals(Integer.valueOf(1), createdVersion.getVersionNumber());
assertEquals("test-kek-label", createdVersion.getKekLabel());
}
}
}

View File

@ -34,7 +34,7 @@ import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@ -74,8 +74,8 @@ public class KMSManagerImplKeyRotationTest {
@Mock
private KMSProvider kmsProvider;
private Long testZoneId = 1L;
private String testProviderName = "pkcs11";
private final Long testZoneId = 1L;
private final String testProviderName = "pkcs11";
@Before
public void setUp() {
@ -94,7 +94,6 @@ public class KMSManagerImplKeyRotationTest {
KMSKeyVO kmsKey = mock(KMSKeyVO.class);
when(kmsKey.getId()).thenReturn(kmsKeyId);
when(kmsKey.getZoneId()).thenReturn(testZoneId);
when(kmsKey.getHsmProfileId()).thenReturn(oldProfileId);
when(kmsKey.getPurpose()).thenReturn(KeyPurpose.VOLUME_ENCRYPTION);
@ -108,7 +107,7 @@ public class KMSManagerImplKeyRotationTest {
when(oldVersion.getVersionNumber()).thenReturn(1);
when(oldVersion.getId()).thenReturn(10L);
when(kmsKekVersionDao.getActiveVersion(kmsKeyId)).thenReturn(oldVersion);
when(kmsKekVersionDao.listByKmsKeyId(kmsKeyId)).thenReturn(Arrays.asList(oldVersion));
when(kmsKekVersionDao.listByKmsKeyId(kmsKeyId)).thenReturn(List.of(oldVersion));
// Provider creates new KEK
when(kmsProvider.createKek(any(KeyPurpose.class), eq(newKekLabel), anyInt(), eq(oldProfileId)))
@ -119,7 +118,6 @@ public class KMSManagerImplKeyRotationTest {
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(newVersion);
doReturn(kmsProvider).when(kmsManager).getKMSProvider(testProviderName);
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
String result = kmsManager.rotateKek(kmsKey, oldKekLabel, newKekLabel, 256, null);
@ -135,7 +133,7 @@ public class KMSManagerImplKeyRotationTest {
ArgumentCaptor<KMSKekVersionVO> versionCaptor = ArgumentCaptor.forClass(KMSKekVersionVO.class);
verify(kmsKekVersionDao).persist(versionCaptor.capture());
KMSKekVersionVO createdVersion = versionCaptor.getValue();
assertEquals(Integer.valueOf(2), Integer.valueOf(createdVersion.getVersionNumber()));
assertEquals(Integer.valueOf(2), createdVersion.getVersionNumber());
assertEquals(oldProfileId, createdVersion.getHsmProfileId());
}
@ -153,7 +151,6 @@ public class KMSManagerImplKeyRotationTest {
KMSKeyVO kmsKey = mock(KMSKeyVO.class);
when(kmsKey.getId()).thenReturn(kmsKeyId);
when(kmsKey.getZoneId()).thenReturn(testZoneId);
when(kmsKey.getHsmProfileId()).thenReturn(oldProfileId);
when(kmsKey.getPurpose()).thenReturn(KeyPurpose.VOLUME_ENCRYPTION);
@ -165,7 +162,7 @@ public class KMSManagerImplKeyRotationTest {
when(oldVersion.getVersionNumber()).thenReturn(1);
when(oldVersion.getId()).thenReturn(10L);
when(kmsKekVersionDao.getActiveVersion(kmsKeyId)).thenReturn(oldVersion);
when(kmsKekVersionDao.listByKmsKeyId(kmsKeyId)).thenReturn(Arrays.asList(oldVersion));
when(kmsKekVersionDao.listByKmsKeyId(kmsKeyId)).thenReturn(List.of(oldVersion));
// Provider creates new KEK in different HSM
when(kmsProvider.createKek(any(KeyPurpose.class), eq(newKekLabel), anyInt(), eq(newProfileId)))
@ -176,7 +173,6 @@ public class KMSManagerImplKeyRotationTest {
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(newVersion);
doReturn(kmsProvider).when(kmsManager).getKMSProvider(testProviderName);
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
String result = kmsManager.rotateKek(kmsKey, oldKekLabel, newKekLabel, 256, hsmProfile);
@ -257,7 +253,6 @@ public class KMSManagerImplKeyRotationTest {
when(kmsKey.getId()).thenReturn(kmsKeyId);
when(kmsKey.getHsmProfileId()).thenReturn(oldProfileId);
when(kmsKey.getPurpose()).thenReturn(KeyPurpose.VOLUME_ENCRYPTION);
when(kmsKey.getZoneId()).thenReturn(testZoneId);
HSMProfileVO hsmProfile = mock(HSMProfileVO.class);
when(hsmProfile.getId()).thenReturn(oldProfileId);
@ -268,7 +263,7 @@ public class KMSManagerImplKeyRotationTest {
when(oldVersion.getVersionNumber()).thenReturn(1);
when(oldVersion.getId()).thenReturn(10L);
when(kmsKekVersionDao.getActiveVersion(kmsKeyId)).thenReturn(oldVersion);
when(kmsKekVersionDao.listByKmsKeyId(kmsKeyId)).thenReturn(Arrays.asList(oldVersion));
when(kmsKekVersionDao.listByKmsKeyId(kmsKeyId)).thenReturn(List.of(oldVersion));
// Provider creates new KEK - will accept any label
when(kmsProvider.createKek(any(KeyPurpose.class), anyString(), anyInt(), eq(oldProfileId)))
@ -279,7 +274,6 @@ public class KMSManagerImplKeyRotationTest {
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(newVersion);
doReturn(kmsProvider).when(kmsManager).getKMSProvider(testProviderName);
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
kmsManager.rotateKek(kmsKey, oldKekLabel, null, 256, null);
@ -291,15 +285,14 @@ public class KMSManagerImplKeyRotationTest {
}
/**
* Test: rotateKek throws exception when old KEK not found
* Test: rotateKek throws exception when old KEK not found (provider rejects the rotation)
*/
@Test(expected = KMSException.class)
public void testRotateKek_ThrowsExceptionWhenOldKekNotFound() throws KMSException {
// Setup: Old KEK doesn't exist
Long oldProfileId = 10L;
Long kmsKeyId = 1L;
KMSKeyVO kmsKey = mock(KMSKeyVO.class);
when(kmsKey.getZoneId()).thenReturn(testZoneId);
when(kmsKey.getHsmProfileId()).thenReturn(oldProfileId);
when(kmsKey.getPurpose()).thenReturn(KeyPurpose.VOLUME_ENCRYPTION);
@ -308,11 +301,13 @@ public class KMSManagerImplKeyRotationTest {
when(hsmProfile.getProtocol()).thenReturn(testProviderName);
when(hsmProfileDao.findById(oldProfileId)).thenReturn(hsmProfile);
doReturn(kmsProvider).when(kmsManager).getKMSProvider(testProviderName);
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
// Provider throws because the old KEK label doesn't exist in the HSM
when(kmsProvider.createKek(any(KeyPurpose.class), eq("new-label"), eq(256), eq(oldProfileId)))
.thenThrow(KMSException.kekNotFound("Old KEK not found: non-existent-label"));
kmsManager.rotateKek(kmsKey,
"non-existent-label", "new-label", 256, null);
doReturn(kmsProvider).when(kmsManager).getKMSProvider(testProviderName);
kmsManager.rotateKek(kmsKey, "non-existent-label", "new-label", 256, null);
}
/**
@ -327,7 +322,6 @@ public class KMSManagerImplKeyRotationTest {
KMSKeyVO kmsKey = mock(KMSKeyVO.class);
when(kmsKey.getId()).thenReturn(kmsKeyId);
when(kmsKey.getZoneId()).thenReturn(testZoneId);
when(kmsKey.getHsmProfileId()).thenReturn(currentProfileId);
when(kmsKey.getPurpose()).thenReturn(KeyPurpose.VOLUME_ENCRYPTION);
@ -340,7 +334,7 @@ public class KMSManagerImplKeyRotationTest {
when(oldVersion.getVersionNumber()).thenReturn(1);
when(oldVersion.getId()).thenReturn(10L);
when(kmsKekVersionDao.getActiveVersion(kmsKeyId)).thenReturn(oldVersion);
when(kmsKekVersionDao.listByKmsKeyId(kmsKeyId)).thenReturn(Arrays.asList(oldVersion));
when(kmsKekVersionDao.listByKmsKeyId(kmsKeyId)).thenReturn(List.of(oldVersion));
when(kmsProvider.createKek(any(KeyPurpose.class), anyString(), anyInt(), eq(currentProfileId)))
.thenReturn("new-kek-id");
@ -350,7 +344,6 @@ public class KMSManagerImplKeyRotationTest {
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(newVersion);
doReturn(kmsProvider).when(kmsManager).getKMSProvider(testProviderName);
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
kmsManager.rotateKek(kmsKey, oldKekLabel, "new-label", 256, null);

View File

@ -0,0 +1,159 @@
// 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 org.apache.cloudstack.kms;
import org.apache.cloudstack.framework.kms.KMSException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.doReturn;
/**
* Unit tests for KMSManagerImpl's retryOperation() logic, covering
* timeout enforcement, retry-on-transient-failure, and non-retryable fast-fail.
*
* Config values (retry count, delay, timeout) are spied on so tests remain
* fast without needing a full management-server config context.
*/
@RunWith(MockitoJUnitRunner.class)
public class KMSManagerImplRetryTest {
@Spy
@InjectMocks
private KMSManagerImpl kmsManager;
/** Configure the spy to use a 1-second timeout, the given retry count, and no delay. */
private void useShortConfig(int retries) {
doReturn(1).when(kmsManager).getOperationTimeoutSec();
doReturn(retries).when(kmsManager).getRetryCount();
doReturn(0).when(kmsManager).getRetryDelayMs();
}
/**
* Normal path: operation completes immediately, result returned.
*/
@Test
public void testRetryOperation_succeedsImmediately() throws Exception {
useShortConfig(0);
String result = kmsManager.retryOperation(() -> "ok");
assertEquals("ok", result);
}
/**
* Timeout path: operation never finishes within the configured timeout.
* retryOperation() must unblock and throw a retryable KMSException.
*/
@Test
public void testRetryOperation_timesOutAndThrowsKMSException() {
useShortConfig(0);
try {
kmsManager.retryOperation(() -> {
Thread.sleep(5_000);
return "should never reach here";
});
fail("Expected KMSException due to timeout");
} catch (KMSException e) {
assertTrue("Exception should be retryable (transient timeout)", e.isRetryable());
assertTrue("Message should mention timeout", e.getMessage().contains("timed out"));
} catch (Exception e) {
fail("Expected KMSException, got: " + e.getClass().getName() + ": " + e.getMessage());
}
}
/**
* Retry path: operation fails with a retryable KMSException on the first
* attempt and succeeds on the second. retryOperation() should return the
* successful result.
*/
@Test
public void testRetryOperation_retriesOnTransientFailureAndSucceeds() throws Exception {
useShortConfig(2);
AtomicInteger attempts = new AtomicInteger(0);
String result = kmsManager.retryOperation(() -> {
if (attempts.getAndIncrement() == 0) {
throw KMSException.transientError("temporary HSM error", null);
}
return "recovered";
});
assertEquals("recovered", result);
assertEquals("Should have taken exactly 2 attempts", 2, attempts.get());
}
/**
* Non-retryable path: a KMSException with isRetryable() == false must be
* re-thrown immediately without consuming any retry budget.
*/
@Test
public void testRetryOperation_nonRetryableExceptionFastFails() {
useShortConfig(3);
AtomicInteger attempts = new AtomicInteger(0);
try {
kmsManager.retryOperation(() -> {
attempts.incrementAndGet();
throw KMSException.invalidParameter("bad key label");
});
fail("Expected non-retryable KMSException");
} catch (KMSException e) {
assertFalse("Exception should NOT be retryable", e.isRetryable());
} catch (Exception e) {
fail("Expected KMSException, got: " + e.getClass().getName());
}
assertEquals("Non-retryable exception must not trigger retries", 1, attempts.get());
}
/**
* Retry exhaustion on timeout: all attempts time out; retryOperation()
* must eventually throw after exhausting the retry budget.
*/
@Test
public void testRetryOperation_exhaustsRetriesOnRepeatedTimeout() {
useShortConfig(2); // 3 total attempts (initial + 2 retries), each timing out after 1s
AtomicInteger attempts = new AtomicInteger(0);
try {
kmsManager.retryOperation(() -> {
attempts.incrementAndGet();
Thread.sleep(5_000);
return "never";
});
fail("Expected KMSException after retry exhaustion");
} catch (KMSException e) {
assertTrue("Final exception should be retryable (timeout)", e.isRetryable());
} catch (Exception e) {
fail("Expected KMSException, got: " + e.getClass().getName());
}
assertEquals("Should have attempted exactly 3 times (1 initial + 2 retries)", 3, attempts.get());
}
}

View File

@ -1434,7 +1434,20 @@
"label.keyboardtype": "Keyboard type",
"label.keypair": "SSH key pair",
"label.keypairs": "SSH key pair(s)",
"label.kms": "Key Management",
"label.kms.key": "KMS Key",
"label.kmskey": "KMS Key",
"label.kms.keys": "KMS Keys",
"label.create.kms.key": "Create KMS Key",
"label.update.kms.key": "Update KMS Key",
"label.rotate.kms.key": "Rotate KMS Key",
"label.delete.kms.key": "Delete KMS Key",
"label.migrate.volumes.to.kms": "Migrate Volumes to KMS",
"label.hsm.profile": "HSM Profile",
"label.hsmprofile": "HSM Profile",
"label.create.hsmprofile": "Add HSM Profile",
"label.update.hsm.profile": "Update HSM Profile",
"label.delete.hsm.profile": "Delete HSM Profile",
"label.select.kms.key.optional": "Select KMS Key (optional)",
"label.kubeconfig.cluster": "Kubernetes Cluster config",
"label.kubernetes": "Kubernetes",
@ -1633,6 +1646,7 @@
"label.migrate.instance.specific.storages": "Migrate volume(s) of the Instance to specific primary storages",
"label.migrate.systemvm.to": "Migrate System VM to",
"label.migrate.volume": "Migrate Volume",
"label.migrate.volume.to.kms": "Migrate Volume Encryption to KMS",
"message.memory.usage.info.hypervisor.additionals": "The data shown may not reflect the actual memory usage if the Instance does not have the additional hypervisor tools installed",
"message.memory.usage.info.negative.value": "If the Instance's memory usage cannot be obtained from the hypervisor, the lines for free memory in the raw data graph and memory usage in the percentage graph will be disabled",
"message.migrate.volume.tooltip": "Volume can be migrated to any suitable storage pool. Admin has to choose the appropriate disk offering to replace, that supports the new storage pool",
@ -2969,9 +2983,11 @@
"message.action.delete.guest.os": "Please confirm that you want to delete this guest os. System defined entry cannot be deleted.",
"message.action.delete.guest.os.category": "Please confirm that you want to delete this guest os category.",
"message.action.delete.guest.os.hypervisor.mapping": "Please confirm that you want to delete this guest os hypervisor mapping. System defined entry cannot be deleted.",
"message.action.delete.hsm.profile": "Please confirm that you want to delete this HSM profile.",
"message.action.delete.instance.group": "Please confirm that you want to delete the Instance group.",
"message.action.delete.interface.static.route": "Please confirm that you want to remove this interface Static Route?",
"message.action.delete.iso": "Please confirm that you want to delete this ISO.",
"message.action.delete.kms.key": "Please confirm that you want to delete this KMS key.",
"message.action.delete.network": "Please confirm that you want to delete this Network.",
"message.action.delete.network.static.route": "Please confirm that you want to remove this Network Static Route",
"message.action.delete.nexusvswitch": "Please confirm that you want to delete this nexus 1000v",
@ -3709,6 +3725,8 @@
"message.migrate.volume.failed": "Migrating volume failed.",
"message.migrate.volume.pool.auto.assign": "Primary storage for the volume will be automatically chosen based on the suitability and Instance destination",
"message.migrate.volume.processing": "Migrating volume...",
"message.action.migrate.volume.to.kms": "Please confirm that you want to migrate this volume's passphrase encryption to KMS. This operation re-encrypts the volume key using the selected KMS key and cannot be undone.",
"message.action.migrate.volumes.to.kms": "Please confirm that you want to migrate volumes to KMS encryption. This operation re-encrypts volume keys using the selected KMS key and cannot be undone.",
"message.migrate.with.storage": "Specify storage pool for volumes of the Instance.",
"message.migrating.failed": "Migration failed.",
"message.migrating.processing": "Migration in progress for",

View File

@ -189,7 +189,7 @@
<div>{{ dataResource[item].rbd_default_data_pool }}</div>
</div>
</a-list-item>
<a-list-item v-else-if="item === 'details' && ['extension', 'customaction'].includes($route.meta.name) && dataResource[item] && Object.keys(dataResource[item]).length > 0">
<a-list-item v-else-if="item === 'details' && ['extension', 'customaction', 'hsmprofile'].includes($route.meta.name) && dataResource[item] && Object.keys(dataResource[item]).length > 0">
<div>
<strong>{{ $t('label.configuration.details') }}</strong>
<br/>

View File

@ -413,6 +413,30 @@
</span>
</div>
</div>
<div class="resource-detail-item" v-if="$route.meta.name === 'volume' && resource.kmskey">
<div class="resource-detail-item__label">{{ $t('label.kms.key') }}</div>
<div class="resource-detail-item__details">
<safety-outlined />
<router-link
v-if="resource.kmskeyid && $router.resolve('/kmskey/' + resource.kmskeyid).matched[0].redirect !== '/exception/404'"
:to="{ path: '/kmskey/' + resource.kmskeyid }">
{{ resource.kmskey }}
</router-link>
<span v-else>{{ resource.kmskey }}</span>
</div>
</div>
<div class="resource-detail-item" v-if="$route.meta.name === 'kmskey' && resource.hsmprofile">
<div class="resource-detail-item__label">{{ $t('label.hsm.profile') }}</div>
<div class="resource-detail-item__details">
<safety-outlined />
<router-link
v-if="resource.hsmprofileid && $router.resolve('/hsmprofile/' + resource.hsmprofileid).matched[0].redirect !== '/exception/404'"
:to="{ path: '/hsmprofile/' + resource.hsmprofileid }">
{{ resource.hsmprofile }}
</router-link>
<span v-else>{{ resource.hsmprofile }}</span>
</div>
</div>
<div class="resource-detail-item" v-if="resource.nic || ('networkkbsread' in resource && 'networkkbswrite' in resource)">
<div class="resource-detail-item__label">{{ $t('label.network') }}</div>
<div class="resource-detail-item__details resource-detail-item__details--start">

View File

@ -650,6 +650,10 @@
<template v-if="column.key === 'objectstore'">
<router-link :to="{ path: '/objectstore/' + record.objectstorageid }">{{ text }}</router-link>
</template>
<template v-if="column.key === 'hsmprofile'">
<router-link v-if="record.hsmprofileid" :to="{ path: '/hsmprofile/' + record.hsmprofileid }">{{ text }}</router-link>
<span v-else>{{ text }}</span>
</template>
<template v-if="column.key === 'podname'">
<router-link :to="{ path: '/pod/' + record.podid }">{{ text }}</router-link>
</template>
@ -1219,7 +1223,8 @@ export default {
'/zone', '/pod', '/cluster', '/host', '/storagepool', '/imagestore', '/systemvm', '/router', '/ilbvm', '/annotation',
'/computeoffering', '/systemoffering', '/diskoffering', '/backupoffering', '/networkoffering', '/vpcoffering',
'/tungstenfabric', '/oauthsetting', '/guestos', '/guestoshypervisormapping', '/webhook', 'webhookdeliveries', 'webhookfilters', '/quotatariff', '/sharedfs',
'/ipv4subnets', '/managementserver', '/gpucard', '/gpudevices', '/vgpuprofile', '/extension', '/snapshotpolicy', '/backupschedule'].join('|'))
'/ipv4subnets', '/managementserver', '/gpucard', '/gpudevices', '/vgpuprofile', '/extension', '/snapshotpolicy', '/backupschedule',
'/kmskey', '/hsmprofile'].join('|'))
.test(this.$route.path)
},
enableGroupAction () {

View File

@ -166,6 +166,18 @@ export default {
responseKey1: 'listnetworksresponse',
responseKey2: 'network',
field: 'name'
},
hsmprofileid: {
apiName: 'listHSMProfiles',
responseKey1: 'listhsmprofilesresponse',
responseKey2: 'hsmprofile',
field: 'name'
},
kmskeyid: {
apiName: 'listKMSKeys',
responseKey1: 'listkmskeysresponse',
responseKey2: 'kmskey',
field: 'name'
}
}
}
@ -217,6 +229,12 @@ export default {
if (fieldName === 'groupid') {
fieldName = 'group'
}
if (fieldName === 'hsmprofileid') {
fieldName = 'hsm.profile'
}
if (fieldName === 'kmskeyid') {
fieldName = 'kms.key'
}
if (fieldName === 'keyword') {
if ('listAnnotations' in this.$store.getters.apis) {
return this.$t('label.annotation')

View File

@ -275,6 +275,12 @@ export default {
if (fieldName === 'groupid') {
fieldName = 'group'
}
if (fieldName === 'hsmprofileid') {
fieldName = 'hsm.profile'
}
if (fieldName === 'kmskeyid') {
fieldName = 'kms.key'
}
if (fieldName === 'keyword') {
if ('listAnnotations' in this.$store.getters.apis) {
return this.$t('label.annotation')
@ -320,12 +326,18 @@ export default {
if (item === 'backupofferingid' && !('listBackupOfferings' in this.$store.getters.apis)) {
return true
}
if (item === 'hsmprofileid' && !('listHSMProfiles' in this.$store.getters.apis)) {
return true
}
if (item === 'kmskeyid' && !('listKMSKeys' in this.$store.getters.apis)) {
return true
}
if (['zoneid', 'domainid', 'imagestoreid', 'storageid', 'state', 'account', 'hypervisor', 'level',
'clusterid', 'podid', 'groupid', 'entitytype', 'accounttype', 'systemvmtype', 'scope', 'provider',
'type', 'scope', 'managementserverid', 'serviceofferingid',
'diskofferingid', 'networkid', 'usagetype', 'restartrequired', 'gpuenabled',
'displaynetwork', 'guestiptype', 'usersource', 'arch', 'oscategoryid', 'templatetype', 'gpucardid', 'vgpuprofileid',
'extensionid', 'backupoffering', 'volumeid', 'virtualmachineid'].includes(item)
'extensionid', 'backupoffering', 'volumeid', 'virtualmachineid', 'hsmprofileid', 'kmskeyid'].includes(item)
) {
type = 'list'
} else if (item === 'tags') {
@ -516,6 +528,8 @@ export default {
let gpuCardIndex = -1
let vgpuProfileIndex = -1
let extensionIndex = -1
let hsmProfileIndex = -1
let kmsKeyIndex = -1
if (arrayField.includes('type')) {
if (this.$route.path === '/alert') {
@ -661,6 +675,18 @@ export default {
promises.push(await this.fetchVolumes(searchKeyword))
}
if (arrayField.includes('hsmprofileid')) {
hsmProfileIndex = this.fields.findIndex(item => item.name === 'hsmprofileid')
this.fields[hsmProfileIndex].loading = true
promises.push(await this.fetchHSMProfiles(searchKeyword))
}
if (arrayField.includes('kmskeyid')) {
kmsKeyIndex = this.fields.findIndex(item => item.name === 'kmskeyid')
this.fields[kmsKeyIndex].loading = true
promises.push(await this.fetchKMSKeys(searchKeyword))
}
Promise.all(promises).then(response => {
if (typeIndex > -1) {
const types = response.filter(item => item.type === 'type')
@ -805,6 +831,20 @@ export default {
this.fields[virtualmachineIndex].opts = this.sortArray(virtualMachines[0].data)
}
}
if (hsmProfileIndex > -1) {
const hsmProfiles = response.filter(item => item.type === 'hsmprofileid')
if (hsmProfiles && hsmProfiles.length > 0) {
this.fields[hsmProfileIndex].opts = this.sortArray(hsmProfiles[0].data)
}
}
if (kmsKeyIndex > -1) {
const kmsKeys = response.filter(item => item.type === 'kmskeyid')
if (kmsKeys && kmsKeys.length > 0) {
this.fields[kmsKeyIndex].opts = this.sortArray(kmsKeys[0].data)
}
}
}).finally(() => {
if (typeIndex > -1) {
this.fields[typeIndex].loading = false
@ -872,6 +912,12 @@ export default {
if (virtualmachineIndex > -1) {
this.fields[virtualmachineIndex].loading = false
}
if (hsmProfileIndex > -1) {
this.fields[hsmProfileIndex].loading = false
}
if (kmsKeyIndex > -1) {
this.fields[kmsKeyIndex].loading = false
}
if (Array.isArray(arrayField)) {
this.fillFormFieldValues()
}
@ -1590,6 +1636,32 @@ export default {
})
})
},
fetchHSMProfiles (searchKeyword) {
return new Promise((resolve, reject) => {
getAPI('listHSMProfiles', { listAll: true, keyword: searchKeyword }).then(json => {
const hsmProfiles = json.listhsmprofilesresponse.hsmprofile
resolve({
type: 'hsmprofileid',
data: hsmProfiles || []
})
}).catch(error => {
reject(error.response.headers['x-description'])
})
})
},
fetchKMSKeys (searchKeyword) {
return new Promise((resolve, reject) => {
getAPI('listKMSKeys', { listAll: true, keyword: searchKeyword }).then(json => {
const kmsKeys = json.listkmskeysresponse.kmskey
resolve({
type: 'kmskeyid',
data: kmsKeys || []
})
}).catch(error => {
reject(error.response.headers['x-description'])
})
})
},
onSearch (value) {
this.paramsFilter = {}
this.searchQuery = value

View File

@ -80,7 +80,7 @@ export default {
return {
vm: {},
volumes: [],
defaultColumns: ['name', 'state', 'type', 'size'],
defaultColumns: ['name', 'state', 'type', 'size', 'kmskey'],
allColumns: [
{
key: 'name',
@ -101,6 +101,11 @@ export default {
title: this.$t('label.size'),
dataIndex: 'size'
},
{
key: 'kmskey',
title: this.$t('label.kms.key'),
dataIndex: 'kmskey'
},
{
key: 'storage',
title: this.$t('label.storage'),

View File

@ -19,7 +19,15 @@
<div>
<div class="input-row">
<a-form-item no-style>
<a-input v-model:value="newKey" :placeholder="$t('label.key')" class="input-field" />
<a-auto-complete
v-if="optionalKeys && optionalKeys.length > 0"
v-model:value="newKey"
:placeholder="$t('label.key')"
class="input-field"
:options="optionalKeyOptions"
:filterOption="(input, option) => option.value.toLowerCase().includes(input.toLowerCase())"
/>
<a-input v-else v-model:value="newKey" :placeholder="$t('label.key')" class="input-field" />
</a-form-item>
<a-form-item no-style>
<a-input v-model:value="newValue" :placeholder="$t('label.value')" class="input-field" />
@ -82,6 +90,15 @@ export default {
showTableHeaders: {
type: Boolean,
default: true
},
optionalKeys: {
type: Array,
default: () => []
}
},
computed: {
optionalKeyOptions () {
return this.optionalKeys.map(k => ({ value: k }))
}
},
data () {

View File

@ -15,6 +15,7 @@
// specific language governing permissions and limitations
// under the License.
import { shallowRef, defineAsyncComponent } from 'vue'
import store from '@/store'
export default {
@ -23,20 +24,48 @@ export default {
icon: 'hdd-outlined',
children: [
{
name: 'KMS key',
name: 'kmskey',
title: 'label.kms.keys',
icon: 'file-text-outlined',
permission: ['listKMSKeys'],
resourceType: 'KMSKey',
columns: () => {
const fields = ['name', 'state', 'account', 'domain', 'purpose']
const fields = ['name', 'enabled', 'purpose', 'hsmprofile']
if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
fields.push('account')
}
if (store.getters.listAllProjects) {
fields.push('project')
}
fields.push('domain')
return fields
},
details: ['id', 'name', 'description', 'state', 'account', 'domain', 'created'],
details: ['id', 'name', 'description', 'version', 'enabled', 'account', 'domain', 'project', 'created', 'hsmprofile'],
related: [
{
name: 'volume',
title: 'label.volumes',
param: 'kmskeyid'
}
],
tabs: [
{
name: 'details',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue')))
},
{
name: 'events',
resourceType: 'KmsKey',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/EventsTab.vue'))),
show: () => {
return 'listEvents' in store.getters.apis
}
}
],
searchFilters: () => {
var filters = ['zoneid']
var filters = ['zoneid', 'hsmprofileid']
if (store.getters.userInfo.roletype === 'Admin') {
filters.push('accountid', 'domainid')
filters.push('account', 'domainid', 'projectid')
}
return filters
},
@ -47,27 +76,65 @@ export default {
label: 'label.create.kms.key',
listView: true,
popup: true,
dataView: true,
dataView: false,
args: (record, store, group) => {
var fields = ['zoneid', 'name', 'description', 'purpose', 'hsmprofileid', 'keybits']
return (['Admin'].includes(store.userInfo.roletype))
? fields.concat(['domainid', 'account']) : fields
return ['Admin'].includes(store.userInfo.roletype)
? ['zoneid', 'domainid', 'account', 'projectid', 'name', 'description', 'hsmprofileid', 'keybits']
: ['zoneid', 'name', 'description', 'hsmprofileid', 'keybits']
}
},
{
api: 'updateKMSKey',
icon: 'edit-outlined',
docHelp: 'adminguide/storage.html#lifecycle-operations',
label: 'label.update.kms.ket',
label: 'label.update.kms.key',
dataView: true,
popup: true,
args: ['id', 'name', 'description', 'state'],
args: ['id', 'name', 'description', 'enabled'],
mapping: {
id: {
value: (record) => record.id
}
}
},
{
api: 'rotateKMSKey',
icon: 'sync-outlined',
docHelp: 'adminguide/storage.html#lifecycle-operations',
label: 'label.rotate.kms.key',
dataView: true,
popup: true,
args: ['id', 'keybits', 'hsmprofileid'],
mapping: {
id: {
value: (record) => record.id
}
}
},
{
api: 'migrateVolumesToKMS',
icon: 'swap-outlined',
docHelp: 'adminguide/storage.html#lifecycle-operations',
label: 'label.migrate.volumes.to.kms',
message: 'message.action.migrate.volumes.to.kms',
dataView: true,
popup: true,
show: (record, store) => {
return ['Admin'].includes(store.userInfo.roletype)
},
args: (record, store) => {
var fields = ['zoneid', 'kmskeyid', 'volumeids']
if (['Admin'].includes(store.userInfo.roletype)) {
fields = fields.concat(['account', 'domainid'])
}
return fields
},
mapping: {
kmskeyid: {
value: (record) => record.id
}
}
},
{
api: 'deleteKMSKey',
icon: 'delete-outlined',
@ -88,16 +155,47 @@ export default {
{
name: 'hsmprofile',
title: 'label.hsm.profile',
icon: 'file-text-outlined',
icon: 'safety-outlined',
permission: ['listHSMProfiles'],
resourceType: 'HSMProfile',
columns: () => {
const fields = ['name', 'state']
const fields = ['name', 'enabled']
if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
fields.push('account')
}
if (store.getters.listAllProjects) {
fields.push('project')
}
fields.push('domain')
return fields
},
details: ['id', 'name', 'description', 'state', 'account', 'domain', 'created'],
details: ['id', 'name', 'description', 'enabled', 'account', 'domain', 'project', 'created', 'details'],
related: [
{
name: 'kmskey',
title: 'label.kms.keys',
param: 'hsmprofileid'
}
],
tabs: [
{
name: 'details',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/DetailsTab.vue')))
},
{
name: 'events',
resourceType: 'HsmProfile',
component: shallowRef(defineAsyncComponent(() => import('@/components/view/EventsTab.vue'))),
show: () => {
return 'listEvents' in store.getters.apis
}
}
],
searchFilters: () => {
var filters = ['zoneid']
if (store.getters.userInfo.roletype === 'Admin') {
filters.push('account', 'domainid', 'projectid')
}
return filters
},
actions: [
@ -107,10 +205,16 @@ export default {
label: 'label.create.hsmprofile',
listView: true,
popup: true,
dataView: true,
dataView: false,
args: (record, store, group) => {
return (['Admin'].includes(store.userInfo.roletype))
? ['zoneid', 'name', 'vendorname', 'domainid', 'accountid', 'details', 'protocol'] : ['zoneid', 'name', 'vendorname', 'details', 'protocol']
return ['Admin'].includes(store.userInfo.roletype)
? ['name', 'zoneid', 'vendorname', 'domainid', 'account', 'projectid', 'details', 'system']
: ['name', 'zoneid', 'vendorname', 'details']
},
mapping: {
details: {
optionalKeys: ['pin', 'library', 'slot', 'token_label']
}
}
},
{
@ -120,7 +224,7 @@ export default {
label: 'label.update.hsm.profile',
dataView: true,
popup: true,
args: ['id', 'name', 'details', 'enabled'],
args: ['id', 'name', 'enabled'],
mapping: {
id: {
value: (record) => record.id

View File

@ -63,7 +63,7 @@ export default {
return fields
},
details: ['name', 'id', 'type', 'storagetype', 'diskofferingdisplaytext', 'deviceid', 'sizegb', 'physicalsize', 'provisioningtype', 'utilization', 'diskkbsread', 'diskkbswrite', 'diskioread', 'diskiowrite', 'diskiopstotal', 'miniops', 'maxiops', 'path', 'deleteprotection'],
details: ['name', 'id', 'type', 'storagetype', 'diskofferingdisplaytext', 'kmskey', 'deviceid', 'sizegb', 'physicalsize', 'provisioningtype', 'utilization', 'diskkbsread', 'diskkbswrite', 'diskioread', 'diskiowrite', 'diskiopstotal', 'miniops', 'maxiops', 'path', 'deleteprotection'],
related: [{
name: 'snapshot',
title: 'label.snapshots',
@ -92,7 +92,7 @@ export default {
}
],
searchFilters: () => {
const filters = ['name', 'zoneid', 'domainid', 'account', 'state', 'tags', 'serviceofferingid', 'diskofferingid', 'isencrypted']
const filters = ['name', 'zoneid', 'domainid', 'account', 'state', 'tags', 'serviceofferingid', 'diskofferingid', 'kmskeyid', 'isencrypted']
if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
filters.push('storageid')
}
@ -221,6 +221,25 @@ export default {
popup: true,
component: shallowRef(defineAsyncComponent(() => import('@/views/storage/MigrateVolume.vue')))
},
{
api: 'migrateVolumesToKMS',
icon: 'lock-outlined',
docHelp: 'adminguide/storage.html#lifecycle-operations',
label: 'label.migrate.volume.to.kms',
message: 'message.action.migrate.volume.to.kms',
dataView: true,
popup: true,
show: (record, store) => {
return record.encryptformat && !record.kmskeyid &&
['Ready', 'Allocated'].includes(record.state)
},
args: ['kmskeyid'],
mapping: {
volumeids: {
value: (record) => { return record.id }
}
}
},
{
api: 'changeOfferingForVolume',
icon: 'swap-outlined',

View File

@ -486,7 +486,8 @@
</a-select>
<details-input
v-else-if="field.type==='map'"
v-model:value="form[field.name]" />
v-model:value="form[field.name]"
:optionalKeys="currentAction.mapping?.[field.name]?.optionalKeys || []" />
<a-input-number
v-else-if="field.type==='long'"
v-focus="fieldIndex === firstIndex"
@ -999,7 +1000,7 @@ export default {
this.projectView = Boolean(store.getters.project && store.getters.project.id)
this.hasProjectId = ['vm', 'vmgroup', 'ssh', 'affinitygroup', 'userdata', 'volume', 'snapshot', 'buckets', 'vmsnapshot', 'guestnetwork',
'vpc', 'securitygroups', 'publicip', 'vpncustomergateway', 'template', 'iso', 'event', 'kubernetes', 'sharedfs',
'autoscalevmgroup', 'vnfapp', 'webhook'].includes(this.$route.name)
'autoscalevmgroup', 'vnfapp', 'webhook', 'kmskey', 'hsmprofile'].includes(this.$route.name)
if (this.dataView && !refreshed) {
this.resource = {}

View File

@ -116,6 +116,31 @@
:placeholder="apiParams.maxiops.description"/>
</a-form-item>
</span>
<span v-if="diskOfferingSupportsEncryption">
<a-form-item ref="kmskeyid" name="kmskeyid">
<template #label>
<tooltip-label :title="$t('label.kms.key')" :tooltip="apiParams.kmskeyid.description"/>
</template>
<a-select
v-model:value="form.kmskeyid"
:loading="loadingKmsKeys"
:placeholder="$t('label.select.kms.key.optional')"
showSearch
optionFilterProp="label"
allowClear>
<a-select-option
v-for="key in kmsKeys"
:key="key.id"
:value="key.id"
:label="key.name">
{{ key.name }}
</a-select-option>
</a-select>
<p style="color: gray; font-size: 12px; margin-top: 5px">
{{ $t('message.kms.key.optional') }}
</p>
</a-form-item>
</span>
<a-form-item name="attachVolume" ref="attachVolume" v-if="!createVolumeFromVM">
<template #label>
<tooltip-label :title="$t('label.action.attach.to.instance')" :tooltip="$t('label.attach.vol.to.instance')" />
@ -204,10 +229,20 @@ export default {
isCustomizedDiskIOps: false,
virtualmachines: [],
attachVolume: false,
vmidtoattach: null
vmidtoattach: null,
kmsKeys: [],
loadingKmsKeys: false,
kmsKeysZoneId: null
}
},
computed: {
selectedDiskOffering () {
if (!this.form.diskofferingid || !this.offerings.length) return null
return this.offerings.find(o => o.id === this.form.diskofferingid) || null
},
diskOfferingSupportsEncryption () {
return this.selectedDiskOffering?.encrypt === true
},
createVolumeFromVM () {
return this.$route.path.startsWith('/vm/')
},
@ -327,6 +362,11 @@ export default {
})
},
fetchDiskOfferings (zoneId) {
if (zoneId !== this.kmsKeysZoneId) {
this.kmsKeys = []
this.kmsKeysZoneId = null
this.form.kmskeyid = undefined
}
this.loading = true
var params = {
zoneid: zoneId,
@ -351,6 +391,12 @@ export default {
}
this.customDiskOffering = this.offerings[0].iscustomized || false
this.isCustomizedDiskIOps = this.offerings[0]?.iscustomizediops || false
if (this.offerings[0]?.encrypt) {
this.fetchKmsKeys()
} else {
this.form.kmskeyid = undefined
this.kmsKeys = []
}
}).finally(() => {
this.loading = false
})
@ -401,6 +447,9 @@ export default {
this.vmidtoattach = values.virtualmachineid
values.virtualmachineid = null
}
if (!this.diskOfferingSupportsEncryption && 'kmskeyid' in values) {
delete values.kmskeyid
}
values.domainid = this.owner.domainid
if (this.owner.projectid) {
values.projectid = this.owner.projectid
@ -459,6 +508,33 @@ export default {
const offering = this.offerings.filter(x => x.id === id)
this.customDiskOffering = offering[0]?.iscustomized || false
this.isCustomizedDiskIOps = offering[0]?.iscustomizediops || false
if (offering[0]?.encrypt) {
this.fetchKmsKeys()
} else {
this.form.kmskeyid = undefined
}
},
fetchKmsKeys () {
const zoneId = this.form.zoneid || (this.createVolumeFromVM && this.resource?.zoneid)
if (!zoneId) return
if (zoneId === this.kmsKeysZoneId) return
this.kmsKeysZoneId = zoneId
this.loadingKmsKeys = true
this.kmsKeys = []
const params = {
zoneid: zoneId,
account: this.owner.account,
domainid: this.owner.domainid,
projectid: this.owner.projectid,
purpose: 'volume'
}
getAPI('listKMSKeys', params).then(response => {
this.kmsKeys = response.listkmskeysresponse.kmskey || []
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loadingKmsKeys = false
})
},
onChangeAttachToVM (zone) {
this.attachVolume = !this.attachVolume