fixups and some ui changes

This commit is contained in:
vishesh92 2026-01-21 14:27:08 +05:30
parent 0d39a7b0be
commit f354da4436
No known key found for this signature in database
GPG Key ID: 4E395186CBFA790B
28 changed files with 1523 additions and 765 deletions

View File

@ -868,6 +868,7 @@ public class ApiConstants {
public static final String SORT_BY = "sortby";
public static final String CHANGE_CIDR = "changecidr";
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_ID = "kmskeyid";
public static final String KMS_KEY_VERSION = "kmskeyversion";

View File

@ -27,6 +27,7 @@ import org.apache.cloudstack.api.Parameter;
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.ZoneResponse;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.kms.KMSManager;
@ -68,6 +69,13 @@ public class MigrateVolumesToKMSCmd extends BaseAsyncCmd {
description = "Domain ID")
private Long domainId;
@Parameter(name = ApiConstants.ID,
required = true,
type = CommandType.UUID,
entityType = KMSKeyResponse.class,
description = "KMS Key ID to use for migrating volumes")
private Long kmsKeyId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -84,6 +92,10 @@ public class MigrateVolumesToKMSCmd extends BaseAsyncCmd {
return domainId;
}
public Long getKmsKeyId() {
return kmsKeyId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////

View File

@ -26,6 +26,7 @@ import org.apache.cloudstack.api.BaseAsyncCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
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.framework.kms.KMSException;
import org.apache.cloudstack.kms.KMSManager;
@ -45,10 +46,6 @@ public class RotateKMSKeyCmd extends BaseAsyncCmd {
@Inject
private KMSManager kmsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID,
required = true,
type = CommandType.UUID,
@ -61,15 +58,12 @@ public class RotateKMSKeyCmd extends BaseAsyncCmd {
description = "Key size for new KEK (default: same as current)")
private Integer keyBits;
@Parameter(name = ApiConstants.HSM_PROFILE,
type = CommandType.STRING,
description = "The target HSM profile name for the new KEK version. If provided, migrates the key to this HSM.")
@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;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
@ -82,10 +76,6 @@ public class RotateKMSKeyCmd extends BaseAsyncCmd {
return hsmProfile;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
try {

View File

@ -31,6 +31,7 @@ import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
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.ZoneResponse;
import org.apache.cloudstack.context.CallContext;
@ -51,10 +52,6 @@ public class CreateKMSKeyCmd extends BaseCmd implements UserCmd {
@Inject
private KMSManager kmsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.NAME,
required = true,
type = CommandType.STRING,
@ -95,14 +92,12 @@ public class CreateKMSKeyCmd extends BaseCmd implements UserCmd {
description = "Key size in bits: 128, 192, or 256 (default: 256)")
private Integer keyBits;
@Parameter(name = ApiConstants.HSM_PROFILE,
type = CommandType.STRING,
description = "Name of HSM profile to create key in")
private String hsmProfile;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.HSM_PROFILE_ID,
type = CommandType.UUID,
entityType = HSMProfileResponse.class,
required = true,
description = "ID of HSM profile to create key in")
private Long hsmProfileId;
public String getName() {
return name;
@ -132,14 +127,10 @@ public class CreateKMSKeyCmd extends BaseCmd implements UserCmd {
return keyBits != null ? keyBits : 256; // Default to 256 bits
}
public String getHsmProfile() {
return hsmProfile;
public Long getHsmProfileId() {
return hsmProfileId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() throws ResourceAllocationException {
try {

View File

@ -49,10 +49,6 @@ public class DeleteKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
@Inject
private KMSManager kmsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID,
required = true,
type = CommandType.UUID,
@ -60,18 +56,10 @@ public class DeleteKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
description = "The UUID of the KMS key to delete")
private Long id;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
try {

View File

@ -47,10 +47,6 @@ public class ListKMSKeysCmd extends BaseListAccountResourcesCmd implements UserC
@Inject
private KMSManager kmsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID,
type = CommandType.UUID,
entityType = KMSKeyResponse.class,
@ -73,10 +69,6 @@ public class ListKMSKeysCmd extends BaseListAccountResourcesCmd implements UserC
description = "Filter by state: Enabled, Disabled")
private String state;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}

View File

@ -48,10 +48,6 @@ public class UpdateKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
@Inject
private KMSManager kmsManager;
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID,
required = true,
type = CommandType.UUID,
@ -74,10 +70,6 @@ public class UpdateKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
description = "New state: Enabled or Disabled")
private String state;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
@ -94,10 +86,6 @@ public class UpdateKMSKeyCmd extends BaseAsyncCmd implements UserCmd {
return state;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
try {

View File

@ -17,16 +17,18 @@
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.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.ZoneResponse;
@ -34,55 +36,56 @@ import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.kms.KMSException;
import org.apache.cloudstack.kms.HSMProfile;
import org.apache.cloudstack.kms.KMSManager;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
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 com.cloud.user.Account;
import javax.inject.Inject;
import java.util.Collection;
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.21.0")
requestHasSensitiveInfo = true, responseHasSensitiveInfo = true, since = "4.23.0")
public class AddHSMProfileCmd extends BaseCmd {
@Inject
private KMSManager kmsManager;
////////////////////////////////////////////////=====
// API parameters
////////////////////////////////////////////////=====
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, description = "the name of the HSM profile")
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true,
description = "the name of the HSM profile")
private String name;
@Parameter(name = ApiConstants.PROTOCOL, type = CommandType.STRING, required = true, description = "the protocol of the HSM profile (PKCS11, KMIP, etc.)")
@Parameter(name = ApiConstants.PROTOCOL, type = CommandType.STRING,
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)")
@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)")
private Long zoneId;
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "the domain ID where the HSM profile is available")
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class,
description = "the domain ID where the HSM profile is available")
private Long domainId;
@Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = DomainResponse.class, description = "the account ID of the HSM profile owner. If null, admin-provided (available to all accounts)")
@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.VENDOR_NAME, type = CommandType.STRING, description = "the vendor name of the HSM")
private String vendorName;
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, required = true, description = "HSM configuration details (protocol specific)")
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, description = "HSM configuration details (protocol specific)")
private Map<String, String> details;
////////////////////////////////////////////////=====
// Accessors
////////////////////////////////////////////////=====
public String getName() {
return name;
}
public String getProtocol() {
if (StringUtils.isBlank(protocol)) {
return "pkcs11";
}
return protocol;
}
@ -103,15 +106,22 @@ public class AddHSMProfileCmd extends BaseCmd {
}
public Map<String, String> getDetails() {
return details;
Map<String, String> detailsMap = new HashMap<>();
if (MapUtils.isNotEmpty(details)) {
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());
}
}
}
return detailsMap;
}
////////////////////////////////////////////////=====
// Implementation
////////////////////////////////////////////////=====
@Override
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
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)"

View File

@ -39,31 +39,19 @@ import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
@APICommand(name = "deleteHSMProfile", description = "Deletes an HSM profile", responseObject = SuccessResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.21.0")
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.23.0")
public class DeleteHSMProfileCmd extends BaseCmd {
@Inject
private KMSManager kmsManager;
////////////////////////////////////////////////=====
// API parameters
////////////////////////////////////////////////=====
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = HSMProfileResponse.class, required = true, description = "the ID of the HSM profile")
private Long id;
////////////////////////////////////////////////=====
// Accessors
////////////////////////////////////////////////=====
public Long getId() {
return id;
}
////////////////////////////////////////////////=====
// Implementation
////////////////////////////////////////////////=====
@Override
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
try {

View File

@ -33,15 +33,14 @@ import org.apache.cloudstack.kms.HSMProfile;
import org.apache.cloudstack.kms.KMSManager;
@APICommand(name = "listHSMProfiles", description = "Lists HSM profiles", responseObject = HSMProfileResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = true, since = "4.21.0")
requestHasSensitiveInfo = false, responseHasSensitiveInfo = true, since = "4.23.0")
public class ListHSMProfilesCmd extends BaseListCmd {
@Inject
private KMSManager kmsManager;
////////////////////////////////////////////////=====
// API parameters
////////////////////////////////////////////////=====
@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")
private Long zoneId;
@ -52,9 +51,9 @@ public class ListHSMProfilesCmd extends BaseListCmd {
@Parameter(name = ApiConstants.ENABLED, type = CommandType.BOOLEAN, description = "list only enabled profiles")
private Boolean enabled;
////////////////////////////////////////////////=====
// Accessors
////////////////////////////////////////////////=====
public Long getId() {
return id;
}
public Long getZoneId() {
return zoneId;
@ -68,16 +67,12 @@ public class ListHSMProfilesCmd extends BaseListCmd {
return enabled;
}
////////////////////////////////////////////////=====
// Implementation
////////////////////////////////////////////////=====
@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);

View File

@ -40,16 +40,12 @@ import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
@APICommand(name = "updateHSMProfile", description = "Updates an HSM profile", responseObject = HSMProfileResponse.class,
requestHasSensitiveInfo = true, responseHasSensitiveInfo = true, since = "4.21.0")
requestHasSensitiveInfo = true, responseHasSensitiveInfo = true, since = "4.23.0")
public class UpdateHSMProfileCmd extends BaseCmd {
@Inject
private KMSManager kmsManager;
////////////////////////////////////////////////=====
// API parameters
////////////////////////////////////////////////=====
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = HSMProfileResponse.class, required = true, description = "the ID of the HSM profile")
private Long id;
@ -62,10 +58,6 @@ public class UpdateHSMProfileCmd extends BaseCmd {
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, description = "HSM configuration details to update (protocol specific)")
private Map<String, String> details;
////////////////////////////////////////////////=====
// Accessors
////////////////////////////////////////////////=====
public Long getId() {
return id;
}
@ -82,10 +74,6 @@ public class UpdateHSMProfileCmd extends BaseCmd {
return details;
}
////////////////////////////////////////////////=====
// Implementation
////////////////////////////////////////////////=====
@Override
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
try {

View File

@ -100,4 +100,6 @@ public interface KMSKey extends Identity, InternalIdentity, ControlledEntity {
/** Key is soft-deleted */
Deleted
}
Long getHsmProfileId();
}

View File

@ -50,20 +50,6 @@ public interface KMSManager extends Manager, Configurable {
// ==================== Configuration Keys ====================
/**
* Global: which KMS provider plugin to use by default
* Supported values: "database" (default), "pkcs11", or custom provider names
*/
ConfigKey<String> KMSProviderPlugin = new ConfigKey<>(
"Advanced",
String.class,
"kms.provider.plugin",
"database",
"The KMS provider plugin to use for cryptographic operations (database, pkcs11, etc.)",
true,
ConfigKey.Scope.Global
);
/**
* Zone-scoped: enable KMS for a specific zone
* When false (default), new volumes use legacy passphrase encryption
@ -209,23 +195,6 @@ public interface KMSManager extends Manager, Configurable {
// ==================== User KEK Management ====================
/**
* Create a new KMS key (KEK) for a user account
*
* @param accountId the account ID
* @param domainId the domain ID
* @param zoneId the zone ID
* @param name user-friendly name
* @param description optional description
* @param purpose key purpose
* @param keyBits key size in bits
* @return the created KMS key
* @throws KMSException if creation fails
*/
KMSKey createUserKMSKey(Long accountId, Long domainId, Long zoneId,
String name, String description, KeyPurpose purpose,
Integer keyBits) throws KMSException;
/**
* List KMS keys accessible to a user account
*
@ -341,7 +310,7 @@ public interface KMSManager extends Manager, Configurable {
/**
* Add a new HSM profile
*
*
* @param cmd the add command
* @return the created HSM profile
* @throws KMSException if addition fails
@ -350,7 +319,7 @@ public interface KMSManager extends Manager, Configurable {
/**
* List HSM profiles
*
*
* @param cmd the list command
* @return list of HSM profiles
*/
@ -358,7 +327,7 @@ public interface KMSManager extends Manager, Configurable {
/**
* Delete an HSM profile
*
*
* @param cmd the delete command
* @return true if deletion was successful
* @throws KMSException if deletion fails
@ -367,7 +336,7 @@ public interface KMSManager extends Manager, Configurable {
/**
* Update an HSM profile
*
*
* @param cmd the update command
* @return the updated HSM profile
* @throws KMSException if update fails
@ -376,7 +345,7 @@ public interface KMSManager extends Manager, Configurable {
/**
* Create a response object for an HSM profile
*
*
* @param profile the HSM profile
* @return the response object
*/

View File

@ -256,6 +256,11 @@
<artifactId>cloud-plugin-kms-database</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-kms-pkcs11</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-plugin-network-nvp</artifactId>

View File

@ -252,6 +252,7 @@ public class KMSKeyVO implements KMSKey {
this.state = state;
}
@Override
public Long getHsmProfileId() {
return hsmProfileId;
}

View File

@ -30,6 +30,10 @@ public class KMSException extends CloudRuntimeException {
*/
public enum ErrorType {
CONNECTION_FAILED(true),
/**
* Authentication failed (e.g., incorrect PIN)
*/
AUTHENTICATION_FAILED(false),
/**
* Provider not initialized or unavailable
*/

View File

@ -21,8 +21,6 @@
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
>

View File

@ -16,14 +16,17 @@
specific language governing permissions and limitations
under the License.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.apache.cloudstack.kms.provider.pkcs11" />
<context:component-scan base-package="org.apache.cloudstack.kms.provider.pkcs11"/>
<bean id="pkcs11HSMProvider" class="org.apache.cloudstack.kms.provider.pkcs11.PKCS11HSMProvider">
<property name="name" value="PKCS11HSMProvider"/>
</bean>
</beans>

View File

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

View File

@ -111,8 +111,9 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
@Override
public KMSProvider getKMSProvider(String name) {
// Default to database provider if no name specified
if (StringUtils.isEmpty(name)) {
return getConfiguredKmsProvider();
name = "database";
}
String providerName = name.toLowerCase();
@ -130,9 +131,9 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
@Override
public KMSProvider getKMSProviderForZone(Long zoneId) throws KMSException {
// For now, use global provider
// In the future, could support zone-specific providers via zone-scoped config
return getConfiguredKmsProvider();
// Default to database provider for backward compatibility
// HSM-based keys will use provider from HSM profile's protocol field
return getKMSProvider("database");
}
@Override
@ -225,40 +226,26 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
}
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_KMS_KEK_CREATE, eventDescription = "creating user KMS key")
public KMSKey createUserKMSKey(Long accountId, Long domainId, Long zoneId,
String name, String description, KeyPurpose purpose,
Integer keyBits) throws KMSException {
// Delegate to method with profileId
return createUserKMSKey(accountId, domainId, zoneId, name, description, purpose, keyBits, null);
}
KMSKey createUserKMSKey(Long accountId, Long domainId, Long zoneId,
String name, String description, KeyPurpose purpose,
Integer keyBits, String hsmProfileName) throws KMSException {
Integer keyBits, long hsmProfileId) throws KMSException {
validateKmsEnabled(zoneId);
KMSProvider provider = getKMSProviderForZone(zoneId);
// Resolve HSM Profile
Long hsmProfileId = null;
if (hsmProfileName != null) {
HSMProfileVO profile = hsmProfileDao.findByName(hsmProfileName);
if (profile == null) {
throw KMSException.invalidParameter("HSM Profile not found: " + hsmProfileName);
}
// Validate access
if (profile.getAccountId() != null && !profile.getAccountId().equals(accountId)) {
// Check if admin
// For simplicity, strict check for now. Ideally should check if user is admin.
// Assuming caller check happened upstream in createKMSKey(CreateKMSKeyCmd)
}
hsmProfileId = profile.getId();
} else {
// Auto-resolve based on hierarchy
hsmProfileId = resolveHSMProfile(accountId, zoneId, provider.getProviderName());
HSMProfileVO profile = hsmProfileDao.findById(hsmProfileId);
if (profile == null) {
throw KMSException.invalidParameter("HSM Profile not found");
}
// Validate access
if (profile.getAccountId() != null && !profile.getAccountId().equals(accountId)) {
// Check if admin
// For simplicity, strict check for now. Ideally should check if user is admin.
// Assuming caller check happened upstream in createKMSKey(CreateKMSKeyCmd)
throw KMSException.invalidParameter("HSM Profile not found");
}
// Determine provider from HSM profile or default to database
KMSProvider provider = getKMSProvider(profile.getProtocol());
// Generate unique KEK label
String kekLabel = purpose.getName() + "-kek-" + UUID.randomUUID().toString().substring(0, 8);
@ -290,46 +277,6 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
return kmsKey;
}
Long resolveHSMProfile(Long accountId, Long zoneId, String providerName) {
// Only applicable for providers that use profiles (pkcs11, kmip)
if ("database".equalsIgnoreCase(providerName)) {
return null;
}
// 1. User-provided profile
List<HSMProfileVO> userProfiles = hsmProfileDao.listByAccountId(accountId);
if (CollectionUtils.isNotEmpty(userProfiles)) {
// Filter by protocol/provider match if needed, for now pick first enabled
for (HSMProfileVO p : userProfiles) {
if (p.isEnabled() && isProviderMatch(p, providerName)) return p.getId();
}
}
// 2. Zone-scoped admin profile
List<HSMProfileVO> zoneProfiles = hsmProfileDao.listAdminProfiles(zoneId);
if (CollectionUtils.isNotEmpty(zoneProfiles)) {
for (HSMProfileVO p : zoneProfiles) {
if (p.isEnabled() && isProviderMatch(p, providerName)) return p.getId();
}
}
// 3. Global admin profile
List<HSMProfileVO> globalProfiles = hsmProfileDao.listAdminProfiles();
if (CollectionUtils.isNotEmpty(globalProfiles)) {
for (HSMProfileVO p : globalProfiles) {
if (p.isEnabled() && isProviderMatch(p, providerName)) return p.getId();
}
}
// If provider is not database, we must have a profile
throw new CloudRuntimeException("No suitable HSM profile found for provider " + providerName + " for account " + accountId);
}
boolean isProviderMatch(HSMProfileVO profile, String providerName) {
// Simple mapping: PKCS11 -> pkcs11, KMIP -> kmip
return profile.getProtocol().equalsIgnoreCase(providerName);
}
@Override
public List<? extends KMSKey> listUserKMSKeys(Long accountId, Long domainId, Long zoneId,
KeyPurpose purpose, KMSKey.State state) {
@ -490,7 +437,12 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
throw KMSException.invalidParameter("KMS key purpose is not VOLUME_ENCRYPTION: " + kmsKey);
}
KMSProvider provider = getKMSProviderForZone(kmsKey.getZoneId());
HSMProfileVO hsmProfile = hsmProfileDao.findById(kmsKey.getHsmProfileId());
if (hsmProfile == null) {
throw KMSException.invalidParameter("HSM profile not found: " + kmsKey.getHsmProfileId());
}
KMSProvider provider = getKMSProvider(hsmProfile.getProtocol());
// Get active KEK version
KMSKekVersionVO activeVersion = getActiveKekVersion(kmsKey.getId());
@ -588,7 +540,8 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
cmd.getName(),
cmd.getDescription(),
keyPurpose,
bits
bits,
cmd.getHsmProfileId()
);
return responseGenerator.createKMSKeyResponse(kmsKey);
@ -823,13 +776,47 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
Long zoneId = cmd.getZoneId();
String accountName = cmd.getAccountName();
Long domainId = cmd.getDomainId();
Long kmsKeyId = cmd.getKmsKeyId();
if (zoneId == null) {
throw KMSException.invalidParameter("zoneId must be specified");
}
if (kmsKeyId == null) {
throw KMSException.invalidParameter("kmsKeyId must be specified");
}
validateKmsEnabled(zoneId);
// Get and validate KMS key
KMSKeyVO kmsKey = kmsKeyDao.findById(kmsKeyId);
if (kmsKey == null) {
throw KMSException.kekNotFound("KMS key not found: " + kmsKeyId);
}
if (kmsKey.getState() != KMSKey.State.Enabled) {
throw KMSException.invalidParameter("KMS key is not enabled: " + kmsKey.getUuid());
}
if (kmsKey.getPurpose() != KeyPurpose.VOLUME_ENCRYPTION) {
throw KMSException.invalidParameter("KMS key purpose must be VOLUME_ENCRYPTION");
}
// Get provider from KMS key's HSM profile
KMSProvider provider;
if (kmsKey.getHsmProfileId() != null) {
HSMProfileVO profile = hsmProfileDao.findById(kmsKey.getHsmProfileId());
if (profile == null) {
throw KMSException.invalidParameter("HSM Profile not found for KMS key");
}
provider = getKMSProvider(profile.getProtocol());
} else {
provider = getKMSProvider("database");
}
// Get active KEK version
KMSKekVersionVO activeVersion = getActiveKekVersion(kmsKey.getId());
Long accountId = null;
if (accountName != null) {
accountId = accountManager.finalyzeAccountId(accountName, domainId, null, true);
@ -837,9 +824,6 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
int pageSize = 100; // Process 100 volumes per page to avoid OutOfMemoryError
// Get provider
KMSProvider provider = getKMSProviderForZone(zoneId);
int successCount = 0;
int failureCount = 0;
logger.info("Starting migration of volumes to KMS (zone: {}, account: {}, domain: {})",
@ -871,41 +855,13 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
// The KMS will store the same format, maintaining compatibility
byte[] passphraseBytes = passphrase.getPassphrase();
// Get or create KMS key for account
KMSKeyVO kmsKey;
List<? extends KMSKey> accountKeys = listUserKMSKeys(
volume.getAccountId(),
volume.getDomainId(),
zoneId,
KeyPurpose.VOLUME_ENCRYPTION,
KMSKey.State.Enabled
);
if (!accountKeys.isEmpty()) {
kmsKey = (KMSKeyVO) accountKeys.get(0); // Use first available key
} else {
// Create new KMS key for account
String keyName = "Volume-Encryption-Key-" + volume.getAccountId();
kmsKey = (KMSKeyVO) createUserKMSKey(
volume.getAccountId(),
volume.getDomainId(),
zoneId,
keyName,
"Auto-created for volume migration",
KeyPurpose.VOLUME_ENCRYPTION,
256 // Default to 256 bits
);
logger.info("Created KMS key {} for account {} during migration", kmsKey, volume.getAccountId());
}
// Get active KEK version
KMSKekVersionVO activeVersion = getActiveKekVersion(kmsKey.getId());
// Wrap existing passphrase bytes as DEK (don't generate new DEK)
// Wrap existing passphrase bytes as DEK using the specified KMS key
// Pass the HSM profile ID from the active version
WrappedKey wrappedKey = provider.wrapKey(
passphraseBytes,
KeyPurpose.VOLUME_ENCRYPTION,
activeVersion.getKekLabel()
activeVersion.getKekLabel(),
activeVersion.getHsmProfileId()
);
// Store wrapped key
@ -1011,16 +967,6 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
return KMSException.transientError("KMS operation failed: " + e.getMessage(), e);
}
private KMSProvider getConfiguredKmsProvider() {
String providerName = KMSProviderPlugin.value();
String providerKey = providerName != null ? providerName.toLowerCase() : null;
if (providerKey != null && kmsProviderMap.containsKey(providerKey) && kmsProviderMap.get(providerKey) != null) {
return kmsProviderMap.get(providerKey);
}
throw new CloudRuntimeException("Failed to find default configured KMS provider plugin: " + providerName);
}
public void setKmsProviders(List<KMSProvider> kmsProviders) {
this.kmsProviders = kmsProviders;
initializeKmsProviderMap();
@ -1055,30 +1001,20 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
super.start();
initializeKmsProviderMap();
String configuredProviderName = KMSProviderPlugin.value();
String providerKey = configuredProviderName != null ? configuredProviderName.toLowerCase() : null;
KMSProvider provider = null;
if (providerKey != null && kmsProviderMap.containsKey(providerKey)) {
provider = kmsProviderMap.get(providerKey);
logger.info("Configured KMS provider: {}", provider.getProviderName());
}
if (provider == null) {
logger.warn("No valid configured KMS provider found. KMS functionality will be unavailable.");
// Don't fail - KMS is optional
return true;
}
// Run health check on startup
try {
boolean healthy = provider.healthCheck();
if (healthy) {
logger.info("KMS provider {} health check passed", provider.getProviderName());
} else {
logger.warn("KMS provider {} health check failed", provider.getProviderName());
// Run health check on all registered providers
for (KMSProvider provider : kmsProviderMap.values()) {
if (provider != null) {
try {
boolean healthy = provider.healthCheck();
if (healthy) {
logger.info("KMS provider {} health check passed", provider.getProviderName());
} else {
logger.warn("KMS provider {} health check failed", provider.getProviderName());
}
} catch (Exception e) {
logger.warn("KMS provider {} health check error: {}", provider.getProviderName(), e.getMessage());
}
}
} catch (Exception e) {
logger.warn("KMS provider health check error: {}", e.getMessage());
}
// Schedule background rewrap worker
@ -1276,7 +1212,6 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[]{
KMSProviderPlugin,
KMSEnabled,
KMSDekSizeBits,
KMSRetryCount,
@ -1296,7 +1231,6 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
cmdList.add(DeleteKMSKeyCmd.class);
cmdList.add(RotateKMSKeyCmd.class);
cmdList.add(MigrateVolumesToKMSCmd.class);
cmdList.add(MigrateVolumesToKMSCmd.class);
cmdList.add(AddHSMProfileCmd.class);
cmdList.add(ListHSMProfilesCmd.class);
cmdList.add(UpdateHSMProfileCmd.class);
@ -1359,6 +1293,22 @@ public class KMSManagerImpl extends ManagerBase implements KMSManager, Pluggable
List<HSMProfile> result = new ArrayList<>();
if (cmd.getId() != null) {
HSMProfileVO key = hsmProfileDao.findById(cmd.getId());
if (key == null) {
return result;
}
// Validate the caller can list this profile
if (!isAdmin) {
Account caller = CallContext.current().getCallingAccount();
Account owner = accountManager.getAccount(key.getAccountId());
accountManager.checkAccess(caller, null, true, owner);
}
result.add(key);
return result;
}
// 1. User's own profiles
result.addAll(hsmProfileDao.listByAccountId(accountId));

View File

@ -66,8 +66,6 @@ public class KMSManagerImplHSMTest {
private AccountManager accountManager;
private Long testAccountId = 100L;
private Long testZoneId = 1L;
private String testProviderName = "pkcs11";
/**
* Test: isSensitiveKey correctly identifies "pin" as sensitive
@ -126,144 +124,6 @@ public class KMSManagerImplHSMTest {
assertTrue("'Password' (mixed case) should be detected as sensitive", resultMixed);
}
/**
* Test: resolveHSMProfile selects user profile when available
*/
@Test
public void testResolveHSMProfile_SelectsUserProfile() {
// Setup: User has a profile
HSMProfileVO userProfile = mock(HSMProfileVO.class);
when(userProfile.getId()).thenReturn(1L);
when(userProfile.isEnabled()).thenReturn(true);
when(userProfile.getProtocol()).thenReturn(testProviderName);
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(Arrays.asList(userProfile));
Long result = kmsManager.resolveHSMProfile(testAccountId, testZoneId, testProviderName);
assertNotNull("Should return user profile ID", result);
assertEquals("Should select user profile", userProfile.getId(), result.longValue());
verify(hsmProfileDao).listByAccountId(testAccountId);
}
/**
* Test: resolveHSMProfile falls back to zone admin profile when no user profile
*/
@Test
public void testResolveHSMProfile_FallbackToZoneAdmin() {
// Setup: No user profile, but zone admin profile exists
HSMProfileVO zoneProfile = mock(HSMProfileVO.class);
when(zoneProfile.getId()).thenReturn(2L);
when(zoneProfile.isEnabled()).thenReturn(true);
when(zoneProfile.getProtocol()).thenReturn(testProviderName);
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(new ArrayList<>());
when(hsmProfileDao.listAdminProfiles(testZoneId)).thenReturn(Arrays.asList(zoneProfile));
Long result = kmsManager.resolveHSMProfile(testAccountId, testZoneId, testProviderName);
assertNotNull("Should return zone admin profile ID", result);
assertEquals("Should select zone admin profile", zoneProfile.getId(), result.longValue());
verify(hsmProfileDao).listByAccountId(testAccountId);
verify(hsmProfileDao).listAdminProfiles(testZoneId);
}
/**
* Test: resolveHSMProfile falls back to global admin profile when no user or zone profile
*/
@Test
public void testResolveHSMProfile_FallbackToGlobal() {
// Setup: No user or zone profile, but global admin profile exists
HSMProfileVO globalProfile = mock(HSMProfileVO.class);
when(globalProfile.getId()).thenReturn(3L);
when(globalProfile.isEnabled()).thenReturn(true);
when(globalProfile.getProtocol()).thenReturn(testProviderName);
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(new ArrayList<>());
when(hsmProfileDao.listAdminProfiles(testZoneId)).thenReturn(new ArrayList<>());
when(hsmProfileDao.listAdminProfiles()).thenReturn(Arrays.asList(globalProfile));
Long result = kmsManager.resolveHSMProfile(testAccountId, testZoneId, testProviderName);
assertNotNull("Should return global admin profile ID", result);
assertEquals("Should select global admin profile", globalProfile.getId(), result.longValue());
verify(hsmProfileDao).listByAccountId(testAccountId);
verify(hsmProfileDao).listAdminProfiles(testZoneId);
verify(hsmProfileDao).listAdminProfiles();
}
/**
* Test: resolveHSMProfile throws exception when no profile found
*/
@Test(expected = CloudRuntimeException.class)
public void testResolveHSMProfile_ThrowsExceptionWhenNoneFound() {
// Setup: No profiles at any level
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(new ArrayList<>());
when(hsmProfileDao.listAdminProfiles(testZoneId)).thenReturn(new ArrayList<>());
when(hsmProfileDao.listAdminProfiles()).thenReturn(new ArrayList<>());
kmsManager.resolveHSMProfile(testAccountId, testZoneId, testProviderName);
}
/**
* Test: resolveHSMProfile skips disabled profiles
*/
@Test
public void testResolveHSMProfile_SkipsDisabledProfiles() {
// Setup: User has disabled profile, zone has enabled profile
HSMProfileVO disabledProfile = mock(HSMProfileVO.class);
when(disabledProfile.isEnabled()).thenReturn(false);
HSMProfileVO enabledZoneProfile = mock(HSMProfileVO.class);
when(enabledZoneProfile.getId()).thenReturn(5L);
when(enabledZoneProfile.isEnabled()).thenReturn(true);
when(enabledZoneProfile.getProtocol()).thenReturn(testProviderName);
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(Arrays.asList(disabledProfile));
when(hsmProfileDao.listAdminProfiles(testZoneId)).thenReturn(Arrays.asList(enabledZoneProfile));
Long result = kmsManager.resolveHSMProfile(testAccountId, testZoneId, testProviderName);
assertNotNull("Should return zone profile ID (skip disabled)", result);
assertEquals("Should select zone profile (not disabled user profile)", enabledZoneProfile.getId(), result.longValue());
}
/**
* Test: resolveHSMProfile returns null for database provider
*/
@Test
public void testResolveHSMProfile_ReturnsNullForDatabaseProvider() {
Long result = kmsManager.resolveHSMProfile(testAccountId, testZoneId, "database");
assertNull("Should return null for database provider", result);
verify(hsmProfileDao, never()).listByAccountId(anyLong());
}
/**
* Test: isProviderMatch correctly matches PKCS11 protocol
*/
@Test
public void testIsProviderMatch_MatchesPKCS11() {
HSMProfileVO profile = mock(HSMProfileVO.class);
when(profile.getProtocol()).thenReturn("PKCS11");
boolean result = kmsManager.isProviderMatch(profile, "pkcs11");
assertTrue("Should match PKCS11 (case-insensitive)", result);
}
/**
* Test: isProviderMatch is case-insensitive
*/
@Test
public void testIsProviderMatch_MatchesDifferentCases() {
HSMProfileVO profile = mock(HSMProfileVO.class);
when(profile.getProtocol()).thenReturn("pkcs11");
boolean resultUpper = kmsManager.isProviderMatch(profile, "PKCS11");
boolean resultMixed = kmsManager.isProviderMatch(profile, "Pkcs11");
assertTrue("Should match PKCS11 (uppercase)", resultUpper);
assertTrue("Should match Pkcs11 (mixed case)", resultMixed);
}
/**
* Test: createHSMProfileResponse populates details correctly
*/

View File

@ -111,7 +111,7 @@ public class KMSManagerImplKeyCreationTest {
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
KMSKey result = kmsManager.createUserKMSKey(testAccountId, testDomainId,
testZoneId, "test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, hsmProfileName);
testZoneId, "test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, hsmProfileId);
// Verify explicit profile was used
assertNotNull(result);
@ -125,52 +125,6 @@ public class KMSManagerImplKeyCreationTest {
assertEquals(hsmProfileId, createdKey.getHsmProfileId());
}
/**
* Test: createUserKMSKey auto-resolves profile when not provided
*/
@Test
public void testCreateUserKMSKey_AutoResolvesProfile() throws Exception {
// Setup: No explicit profile name, should auto-resolve
Long autoResolvedProfileId = 20L;
// Mock profile resolution hierarchy - user has a profile
HSMProfileVO userProfile = mock(HSMProfileVO.class);
when(userProfile.getId()).thenReturn(autoResolvedProfileId);
when(userProfile.isEnabled()).thenReturn(true);
when(userProfile.getProtocol()).thenReturn(testProviderName);
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(Arrays.asList(userProfile));
// Mock provider KEK creation
when(kmsProvider.createKek(any(KeyPurpose.class), anyString(), anyInt(), eq(autoResolvedProfileId)))
.thenReturn("test-kek-label");
// Mock DAO persist operations
KMSKeyVO mockKey = mock(KMSKeyVO.class);
when(mockKey.getId()).thenReturn(1L);
when(kmsKeyDao.persist(any(KMSKeyVO.class))).thenReturn(mockKey);
KMSKekVersionVO mockVersion = mock(KMSKekVersionVO.class);
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(mockVersion);
// Mock getKMSProviderForZone
doReturn(kmsProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
KMSKey result = kmsManager.createUserKMSKey(testAccountId, testDomainId,
testZoneId, "test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, null);
// Verify profile was auto-resolved
assertNotNull(result);
verify(hsmProfileDao).listByAccountId(testAccountId);
verify(kmsProvider).createKek(any(KeyPurpose.class), anyString(), eq(256), eq(autoResolvedProfileId));
// Verify KMSKeyVO was created with auto-resolved profile ID
ArgumentCaptor<KMSKeyVO> keyCaptor = ArgumentCaptor.forClass(KMSKeyVO.class);
verify(kmsKeyDao).persist(keyCaptor.capture());
KMSKeyVO createdKey = keyCaptor.getValue();
assertEquals(autoResolvedProfileId, createdKey.getHsmProfileId());
}
/**
* Test: createUserKMSKey throws exception when explicit profile not found
*/
@ -178,59 +132,14 @@ public class KMSManagerImplKeyCreationTest {
public void testCreateUserKMSKey_ThrowsExceptionWhenProfileNotFound() throws KMSException {
// Setup: Profile name provided but doesn't exist
String invalidProfileName = "non-existent-profile";
when(hsmProfileDao.findByName(invalidProfileName)).thenReturn(null);
long hsmProfileId = 1L;
when(hsmProfileDao.findById(hsmProfileId)).thenReturn(null);
doReturn(kmsProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
kmsManager.createUserKMSKey(testAccountId, testDomainId, testZoneId,
"test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, invalidProfileName);
}
/**
* Test: createUserKMSKey auto-resolves to zone admin profile when no user profile
*/
@Test
public void testCreateUserKMSKey_AutoResolvesToZoneAdmin() throws Exception {
// Setup: No user profile, but zone admin profile exists
Long zoneAdminProfileId = 30L;
HSMProfileVO zoneProfile = mock(HSMProfileVO.class);
when(zoneProfile.getId()).thenReturn(zoneAdminProfileId);
when(zoneProfile.isEnabled()).thenReturn(true);
when(zoneProfile.getProtocol()).thenReturn(testProviderName);
when(hsmProfileDao.listByAccountId(testAccountId)).thenReturn(new ArrayList<>());
when(hsmProfileDao.listAdminProfiles(testZoneId)).thenReturn(Arrays.asList(zoneProfile));
// Mock provider KEK creation
when(kmsProvider.createKek(any(KeyPurpose.class), anyString(), anyInt(), eq(zoneAdminProfileId)))
.thenReturn("test-kek-label");
// Mock DAO persist operations
KMSKeyVO mockKey = mock(KMSKeyVO.class);
when(mockKey.getId()).thenReturn(1L);
when(kmsKeyDao.persist(any(KMSKeyVO.class))).thenReturn(mockKey);
KMSKekVersionVO mockVersion = mock(KMSKekVersionVO.class);
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(mockVersion);
doReturn(kmsProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
KMSKey result = kmsManager.createUserKMSKey(testAccountId, testDomainId,
testZoneId, "test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, null);
// Verify zone admin profile was used
assertNotNull(result);
verify(hsmProfileDao).listByAccountId(testAccountId);
verify(hsmProfileDao).listAdminProfiles(testZoneId);
verify(kmsProvider).createKek(any(KeyPurpose.class), anyString(), eq(256), eq(zoneAdminProfileId));
// Verify KMSKeyVO was created with zone admin profile ID
ArgumentCaptor<KMSKeyVO> keyCaptor = ArgumentCaptor.forClass(KMSKeyVO.class);
verify(kmsKeyDao).persist(keyCaptor.capture());
assertEquals(zoneAdminProfileId, keyCaptor.getValue().getHsmProfileId());
"test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, hsmProfileId);
}
/**
@ -261,7 +170,7 @@ public class KMSManagerImplKeyCreationTest {
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
kmsManager.createUserKMSKey(testAccountId, testDomainId, testZoneId,
"test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, null);
"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);
@ -271,37 +180,4 @@ public class KMSManagerImplKeyCreationTest {
assertEquals(Integer.valueOf(1), Integer.valueOf(createdVersion.getVersionNumber()));
assertEquals("test-kek-label", createdVersion.getKekLabel());
}
/**
* Test: createUserKMSKey returns null profile ID for database provider
*/
@Test
public void testCreateUserKMSKey_NullProfileIdForDatabaseProvider() throws Exception {
// Setup: Database provider doesn't use profiles
KMSProvider databaseProvider = mock(KMSProvider.class);
when(databaseProvider.getProviderName()).thenReturn("database");
when(databaseProvider.createKek(any(KeyPurpose.class), anyString(), anyInt(), eq(null)))
.thenReturn("test-kek-label");
KMSKeyVO mockKey = mock(KMSKeyVO.class);
when(mockKey.getId()).thenReturn(1L);
when(kmsKeyDao.persist(any(KMSKeyVO.class))).thenReturn(mockKey);
KMSKekVersionVO mockVersion = mock(KMSKekVersionVO.class);
when(kmsKekVersionDao.persist(any(KMSKekVersionVO.class))).thenReturn(mockVersion);
doReturn(databaseProvider).when(kmsManager).getKMSProviderForZone(testZoneId);
doReturn(true).when(kmsManager).isKmsEnabled(testZoneId);
kmsManager.createUserKMSKey(testAccountId, testDomainId, testZoneId,
"test-key", "Test key", KeyPurpose.VOLUME_ENCRYPTION, 256, null);
// Verify KEK was created with null profile ID
verify(databaseProvider).createKek(any(KeyPurpose.class), anyString(), eq(256), eq(null));
// Verify KMSKeyVO has null profile ID
ArgumentCaptor<KMSKeyVO> keyCaptor = ArgumentCaptor.forClass(KMSKeyVO.class);
verify(kmsKeyDao).persist(keyCaptor.capture());
assertEquals(null, keyCaptor.getValue().getHsmProfileId());
}
}

View File

@ -1434,6 +1434,8 @@
"label.keyboardtype": "Keyboard type",
"label.keypair": "SSH key pair",
"label.keypairs": "SSH key pair(s)",
"label.kms.key": "KMS Key",
"label.select.kms.key.optional": "Select KMS Key (optional)",
"label.kubeconfig.cluster": "Kubernetes Cluster config",
"label.kubernetes": "Kubernetes",
"label.kubernetes.access.details": "The kubernetes nodes can be accessed via ssh using: <br> <code><b> ssh -i [ssh_key] -p [port_number] cloud@[public_ip_address] </b></code> <br><br> where, <br> <code><b>ssh_key:</b></code> points to the ssh private key file corresponding to the key that was associated while creating the Kubernetes Cluster. If no ssh key was provided during Kubernetes cluster creation, use the ssh private key of the management server. <br> <code><b>port_number:</b></code> can be obtained from the Port Forwarding Tab (Public Port column)",
@ -4224,5 +4226,6 @@
"Compute*Month": "Compute * Month",
"GB*Month": "GB * Month",
"IP*Month": "IP * Month",
"Policy*Month": "Policy * Month"
"Policy*Month": "Policy * Month",
"message.kms.key.optional": "Optional: Select a KMS key for encryption. If not selected, legacy passphrase encryption will be used."
}

View File

@ -28,6 +28,7 @@ import compute from '@/config/section/compute'
import storage from '@/config/section/storage'
import network from '@/config/section/network'
import image from '@/config/section/image'
import kms from '@/config/section/kms'
import project from '@/config/section/project'
import event from '@/config/section/event'
import user from '@/config/section/user'
@ -216,6 +217,7 @@ export function asyncRouterMap () {
generateRouterMap(compute),
generateRouterMap(storage),
generateRouterMap(kms),
generateRouterMap(network),
generateRouterMap(image),
generateRouterMap(event),

View File

@ -0,0 +1,148 @@
// 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.
import store from '@/store'
export default {
name: 'kms',
title: 'label.kms',
icon: 'hdd-outlined',
children: [
{
name: 'KMS key',
title: 'label.kms.keys',
icon: 'file-text-outlined',
permission: ['listKMSKeys'],
resourceType: 'KMSKey',
columns: () => {
const fields = ['name', 'state', 'account', 'domain', 'purpose']
return fields
},
details: ['id', 'name', 'description', 'state', 'account', 'domain', 'created'],
searchFilters: () => {
var filters = ['zoneid']
if (store.getters.userInfo.roletype === 'Admin') {
filters.push('accountid', 'domainid')
}
return filters
},
actions: [
{
api: 'createKMSKey',
icon: 'plus-outlined',
label: 'label.create.kms.key',
listView: true,
popup: true,
dataView: true,
args: (record, store, group) => {
var fields = ['zoneid', 'name', 'description', 'purpose', 'hsmprofileid', 'keybits']
return (['Admin'].includes(store.userInfo.roletype))
? fields.concat(['domainid', 'account']) : fields
}
},
{
api: 'updateKMSKey',
icon: 'edit-outlined',
docHelp: 'adminguide/storage.html#lifecycle-operations',
label: 'label.update.kms.ket',
dataView: true,
popup: true,
args: ['id', 'name', 'description', 'state'],
mapping: {
id: {
value: (record) => record.id
}
}
},
{
api: 'deleteKMSKey',
icon: 'delete-outlined',
docHelp: 'adminguide/storage.html#lifecycle-operations',
label: 'label.delete.kms.key',
message: 'message.action.delete.kms.key',
dataView: true,
popup: true,
args: ['id'],
mapping: {
id: {
value: (record) => record.id
}
}
}
]
},
{
name: 'hsmprofile',
title: 'label.hsm.profile',
icon: 'file-text-outlined',
permission: ['listHSMProfiles'],
resourceType: 'HSMProfile',
columns: () => {
const fields = ['name', 'state']
return fields
},
details: ['id', 'name', 'description', 'state', 'account', 'domain', 'created'],
searchFilters: () => {
var filters = ['zoneid']
return filters
},
actions: [
{
api: 'addHSMProfile',
icon: 'plus-outlined',
label: 'label.create.hsmprofile',
listView: true,
popup: true,
dataView: true,
args: (record, store, group) => {
return (['Admin'].includes(store.userInfo.roletype))
? ['zoneid', 'name', 'vendorname', 'domainid', 'accountid', 'details', 'protocol'] : ['zoneid', 'name', 'vendorname', 'details', 'protocol']
}
},
{
api: 'updateHSMProfile',
icon: 'edit-outlined',
docHelp: 'adminguide/storage.html#lifecycle-operations',
label: 'label.update.hsm.profile',
dataView: true,
popup: true,
args: ['id', 'name', 'details', 'enabled'],
mapping: {
id: {
value: (record) => record.id
}
}
},
{
api: 'deleteHSMProfile',
icon: 'delete-outlined',
docHelp: 'adminguide/storage.html#lifecycle-operations',
label: 'label.delete.hsm.profile',
message: 'message.action.delete.hsm.profile',
dataView: true,
popup: true,
args: ['id'],
mapping: {
id: {
value: (record) => record.id
}
}
}
]
}
]
}

View File

@ -341,15 +341,19 @@
@handle-search-filter="($event) => handleSearchFilter('diskOfferings', $event)"
></disk-offering-selection>
<disk-size-selection
v-if="overrideDiskOffering && (overrideDiskOffering.iscustomized || overrideDiskOffering.iscustomizediops)"
v-if="overrideDiskOffering && (overrideDiskOffering.iscustomized || overrideDiskOffering.iscustomizediops || overrideDiskOffering.encrypt || (serviceOffering && serviceOffering.encryptroot))"
input-decorator="rootdisksize"
:preFillContent="dataPreFill"
:minDiskSize="dataPreFill.minrootdisksize"
:rootDiskSelected="overrideDiskOffering"
:isCustomized="overrideDiskOffering.iscustomized"
:kmsKeys="options.kmsKeys"
:loadingKmsKeys="loading.kmsKeys"
:computeOfferingEncryptRoot="serviceOffering && serviceOffering.encryptroot"
@handler-error="handlerError"
@update-disk-size="updateFieldValue"
@update-root-disk-iops-value="updateIOPSValue"/>
@update-root-disk-iops-value="updateIOPSValue"
@update-root-kms-key="updateRootKmsKey"/>
<a-form-item class="form-item-hidden">
<a-input v-model:value="form.rootdisksize"/>
</a-form-item>
@ -394,14 +398,17 @@
@handle-search-filter="($event) => handleSearchFilter('diskOfferings', $event)"
></disk-offering-selection>
<disk-size-selection
v-if="diskOffering && (diskOffering.iscustomized || diskOffering.iscustomizediops)"
v-if="diskOffering && (diskOffering.iscustomized || diskOffering.iscustomizediops || diskOffering.encrypt)"
input-decorator="size"
:preFillContent="dataPreFill"
:diskSelected="diskSelected"
:isCustomized="diskOffering.iscustomized"
:kmsKeys="options.kmsKeys"
:loadingKmsKeys="loading.kmsKeys"
@handler-error="handlerError"
@update-disk-size="updateFieldValue"
@update-iops-value="updateIOPSValue"/>
@update-iops-value="updateIOPSValue"
@update-data-kms-key="updateDataKmsKey"/>
<a-form-item class="form-item-hidden">
<a-input v-model:value="form.size"/>
</a-form-item>
@ -1050,7 +1057,8 @@ export default {
keyboards: [],
bootTypes: [],
bootModes: [],
ioPolicyTypes: []
ioPolicyTypes: [],
kmsKeys: []
},
rowCount: {},
loading: {
@ -1071,7 +1079,8 @@ export default {
pods: false,
clusters: false,
hosts: false,
groups: false
groups: false,
kmsKeys: false
},
owner: {
projectid: store.getters.project?.id,
@ -1726,6 +1735,22 @@ export default {
serviceOffering (oldValue, newValue) {
if (oldValue && newValue && oldValue.id !== newValue.id) {
this.dynamicscalingenabled = this.isDynamicallyScalable()
// Fetch KMS keys if encryption is enabled
if (newValue && newValue.encryptroot && this.zoneId) {
this.fetchKmsKeys()
}
}
},
diskOffering (newValue) {
// Fetch KMS keys if encryption is enabled
if (newValue && newValue.encrypt && this.zoneId) {
this.fetchKmsKeys()
}
},
overrideDiskOffering (newValue) {
// Fetch KMS keys if encryption is enabled
if (newValue && newValue.encrypt && this.zoneId) {
this.fetchKmsKeys()
}
},
template (oldValue, newValue) {
@ -1993,6 +2018,25 @@ export default {
const param = this.params.networks
this.fetchOptions(param, 'networks')
},
fetchKmsKeys () {
if (!this.zoneId) {
return
}
this.loading.kmsKeys = true
this.options.kmsKeys = []
getAPI('listKMSKeys', {
zoneid: this.zoneId,
account: this.owner.account,
domainid: this.owner.domainid,
projectid: this.owner.projectid
}).then(response => {
this.options.kmsKeys = response.listkmskeysresponse.kmskey || []
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loading.kmsKeys = false
})
},
resetData () {
this.vm = {
name: null,
@ -2017,6 +2061,12 @@ export default {
this.formRef.value.resetFields()
this.fetchData()
},
updateRootKmsKey (value) {
this.form.rootkmskeyid = value
},
updateDataKmsKey (value) {
this.form.datakmskeyid = value
},
updateFieldValue (name, value) {
if (name === 'templateid') {
this.imageType = 'templateid'
@ -2380,6 +2430,10 @@ export default {
deployVmData['details[0].memory'] = values.memory
}
}
// Add root disk KMS key if selected (optional - falls back to legacy passphrase if not provided)
if (values.rootkmskeyid) {
deployVmData.rootdiskkmskeyid = values.rootkmskeyid
}
if (this.selectedTemplateConfiguration) {
deployVmData['details[0].configurationId'] = this.selectedTemplateConfiguration.id
}
@ -2406,12 +2460,29 @@ export default {
})
}
} else {
deployVmData.diskofferingid = values.diskofferingid
if (values.size) {
deployVmData.size = values.size
// When a KMS key is selected for data disk, we must use datadisksdetails format
if (values.datakmskeyid) {
deployVmData['datadisksdetails[0].diskofferingid'] = values.diskofferingid
deployVmData['datadisksdetails[0].deviceid'] = 1 // Device ID 1 for first data disk (0=root, 3=CD-ROM reserved)
if (values.size) {
deployVmData['datadisksdetails[0].size'] = values.size
}
deployVmData['datadisksdetails[0].kmskeyid'] = values.datakmskeyid
// Add IOPS if customized
if (this.isCustomizedDiskIOPS) {
deployVmData['datadisksdetails[0].miniops'] = this.diskIOpsMin
deployVmData['datadisksdetails[0].maxiops'] = this.diskIOpsMax
}
} else {
// Legacy format when no KMS key
deployVmData.diskofferingid = values.diskofferingid
if (values.size) {
deployVmData.size = values.size
}
}
}
if (this.isCustomizedDiskIOPS) {
// IOPS for non-KMS data disks (KMS data disks IOPS handled above in datadisksdetails)
if (this.isCustomizedDiskIOPS && !values.datakmskeyid) {
deployVmData['details[0].minIopsDo'] = this.diskIOpsMin
deployVmData['details[0].maxIopsDo'] = this.diskIOpsMax
}
@ -3087,6 +3158,7 @@ export default {
this.selectedBackupOffering = null
this.fetchZoneOptions()
this.updateZoneAllowsBackupOperations()
this.fetchKmsKeys()
},
onSelectPodId (value) {
this.podId = value

View File

@ -16,35 +16,62 @@
// under the License.
<template>
<a-row :span="24" :style="{ marginTop: '20px' }">
<a-col :span="isCustomizedDiskIOps || isCustomizedIOps ? 8 : 24" v-if="isCustomized">
<a-form-item
:label="inputDecorator === 'rootdisksize' ? $t('label.root.disk.size') : $t('label.disksize')"
class="form-item">
<span style="display: inline-flex">
<a-input-number
v-focus="true"
v-model:value="inputValue"
@change="($event) => updateDiskSize($event)"
/>
<span style="padding-top: 6px; margin-left: 5px">GB</span>
</span>
<p v-if="error" style="color: red"> {{ $t(error) }} </p>
</a-form-item>
</a-col>
<a-col :span="8" v-if="isCustomizedDiskIOps || isCustomizedIOps">
<a-form-item :label="$t('label.diskiopsmin')">
<a-input-number v-model:value="minIOps" @change="updateDiskIOps" />
<p v-if="errorMinIOps" style="color: red"> {{ $t(errorMinIOps) }} </p>
</a-form-item>
</a-col>
<a-col :span="8" v-if="isCustomizedDiskIOps || isCustomizedIOps">
<a-form-item :label="$t('label.diskiopsmax')">
<a-input-number v-model:value="maxIOps" @change="updateDiskIOps" />
<p v-if="errorMaxIOps" style="color: red"> {{ $t(errorMaxIOps) }} </p>
</a-form-item>
</a-col>
</a-row>
<div>
<a-row :span="24" :style="{ marginTop: '20px' }">
<a-col :span="isCustomizedDiskIOps || isCustomizedIOps ? 8 : 24" v-if="isCustomized">
<a-form-item
:label="inputDecorator === 'rootdisksize' ? $t('label.root.disk.size') : $t('label.disksize')"
class="form-item">
<span style="display: inline-flex">
<a-input-number
v-focus="true"
v-model:value="inputValue"
@change="($event) => updateDiskSize($event)"
/>
<span style="padding-top: 6px; margin-left: 5px">GB</span>
</span>
<p v-if="error" style="color: red"> {{ $t(error) }} </p>
</a-form-item>
</a-col>
<a-col :span="8" v-if="isCustomizedDiskIOps || isCustomizedIOps">
<a-form-item :label="$t('label.diskiopsmin')">
<a-input-number v-model:value="minIOps" @change="updateDiskIOps" />
<p v-if="errorMinIOps" style="color: red"> {{ $t(errorMinIOps) }} </p>
</a-form-item>
</a-col>
<a-col :span="8" v-if="isCustomizedDiskIOps || isCustomizedIOps">
<a-form-item :label="$t('label.diskiopsmax')">
<a-input-number v-model:value="maxIOps" @change="updateDiskIOps" />
<p v-if="errorMaxIOps" style="color: red"> {{ $t(errorMaxIOps) }} </p>
</a-form-item>
</a-col>
</a-row>
<a-row :span="24" v-if="showKmsKeySelector">
<a-col :span="24">
<a-form-item :label="$t('label.kms.key')" class="form-item">
<a-select
v-model:value="selectedKmsKey"
:loading="loadingKmsKeys"
:placeholder="$t('label.select.kms.key.optional')"
showSearch
optionFilterProp="label"
allowClear
@change="updateKmsKey">
<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>
</a-col>
</a-row>
</div>
</template>
<script>
@ -74,6 +101,18 @@ export default {
isCustomized: {
type: Boolean,
default: false
},
kmsKeys: {
type: Array,
default: () => []
},
loadingKmsKeys: {
type: Boolean,
default: false
},
computeOfferingEncryptRoot: {
type: Boolean,
default: false
}
},
watch: {
@ -90,6 +129,13 @@ export default {
},
isCustomizedIOps () {
return this.rootDiskSelected?.iscustomizediops || false
},
showKmsKeySelector () {
const isRootDisk = this.inputDecorator === 'rootdisksize'
const isDataDisk = this.inputDecorator === 'size'
return (isRootDisk && (this.computeOfferingEncryptRoot || this.rootDiskSelected?.encrypt)) ||
(isDataDisk && this.diskSelected?.encrypt)
}
},
data () {
@ -99,7 +145,8 @@ export default {
minIOps: null,
maxIOps: null,
errorMinIOps: false,
errorMaxIOps: false
errorMaxIOps: false,
selectedKmsKey: null
}
},
mounted () {
@ -153,6 +200,15 @@ export default {
this.$emit('update-root-disk-iops-value', 'minIops', this.minIOps)
this.$emit('update-root-disk-iops-value', 'maxIops', this.maxIOps)
this.$emit('handler-error', false)
},
updateKmsKey (value) {
// Emit the KMS key ID (or null if cleared)
// Use different event names for root vs data disk
if (this.inputDecorator === 'rootdisksize') {
this.$emit('update-root-kms-key', value)
} else if (this.inputDecorator === 'size') {
this.$emit('update-data-kms-key', value)
}
}
}
}