mirror of https://github.com/apache/cloudstack.git
fix list apis
This commit is contained in:
parent
529fd9d661
commit
6ba0af5ceb
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ public class ManagementServerMaintenanceManagerImplTest {
|
|||
Mockito.doReturn(expectedCount).when(jobManagerMock).countPendingNonPseudoJobs(1L);
|
||||
return expectedCount;
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void countPendingJobs() {
|
||||
long expectedCount = prepareCountPendingJobs();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue