Skip to content

Commit ae5dda8

Browse files
authored
Normalize encryption on global configurations values (#6812)
1 parent 8939ebb commit ae5dda8

7 files changed

Lines changed: 156 additions & 29 deletions

File tree

api/src/main/java/org/apache/cloudstack/api/command/admin/config/UpdateCfgCmd.java

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// under the License.
1717
package org.apache.cloudstack.api.command.admin.config;
1818

19+
import com.cloud.utils.crypt.DBEncryptionUtil;
1920
import org.apache.cloudstack.acl.RoleService;
2021
import org.apache.cloudstack.api.response.DomainResponse;
2122
import org.apache.log4j.Logger;
@@ -150,25 +151,50 @@ public void execute() {
150151
if (cfg != null) {
151152
ConfigurationResponse response = _responseGenerator.createConfigurationResponse(cfg);
152153
response.setResponseName(getCommandName());
153-
if (getZoneId() != null) {
154-
response.setScope("zone");
155-
}
156-
if (getClusterId() != null) {
157-
response.setScope("cluster");
158-
}
159-
if (getStoragepoolId() != null) {
160-
response.setScope("storagepool");
161-
}
162-
if (getAccountId() != null) {
163-
response.setScope("account");
164-
}
165-
if (getDomainId() != null) {
166-
response.setScope("domain");
167-
}
168-
response.setValue(value);
154+
response = setResponseScopes(response);
155+
response = setResponseValue(response, cfg);
169156
this.setResponseObject(response);
170157
} else {
171158
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update config");
172159
}
173160
}
161+
162+
/**
163+
* Sets the configuration value in the response. If the configuration is in the `Hidden` or `Secure` categories, the value is encrypted before being set in the response.
164+
* @param response to be set with the configuration `cfg` value
165+
* @param cfg to be used in setting the response value
166+
* @return the response with the configuration's value
167+
*/
168+
public ConfigurationResponse setResponseValue(ConfigurationResponse response, Configuration cfg) {
169+
if (cfg.isEncrypted()) {
170+
response.setValue(DBEncryptionUtil.encrypt(getValue()));
171+
} else {
172+
response.setValue(getValue());
173+
}
174+
return response;
175+
}
176+
177+
/**
178+
* Sets the scope for the Configuration response only if the field is not null.
179+
* @param response to be updated
180+
* @return the response updated with the scopes
181+
*/
182+
public ConfigurationResponse setResponseScopes(ConfigurationResponse response) {
183+
if (getZoneId() != null) {
184+
response.setScope("zone");
185+
}
186+
if (getClusterId() != null) {
187+
response.setScope("cluster");
188+
}
189+
if (getStoragepoolId() != null) {
190+
response.setScope("storagepool");
191+
}
192+
if (getAccountId() != null) {
193+
response.setScope("account");
194+
}
195+
if (getDomainId() != null) {
196+
response.setScope("domain");
197+
}
198+
return response;
199+
}
174200
}

engine/schema/src/main/java/com/cloud/domain/DomainDetailVO.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import javax.persistence.Id;
2424
import javax.persistence.Table;
2525

26-
import com.cloud.utils.db.Encrypt;
2726
import org.apache.cloudstack.api.InternalIdentity;
2827

2928
@Entity
@@ -40,7 +39,6 @@ public class DomainDetailVO implements InternalIdentity {
4039
@Column(name = "name")
4140
private String name;
4241

43-
@Encrypt
4442
@Column(name = "value")
4543
private String value;
4644

engine/schema/src/main/java/com/cloud/upgrade/dao/Upgrade41810to41900.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,19 @@
1717
package com.cloud.upgrade.dao;
1818

1919
import com.cloud.upgrade.SystemVmTemplateRegistration;
20+
import com.cloud.utils.crypt.DBEncryptionUtil;
2021
import com.cloud.utils.DateUtil;
2122
import com.cloud.utils.exception.CloudRuntimeException;
2223
import org.apache.log4j.Logger;
24+
import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
2325

2426
import java.io.InputStream;
2527
import java.sql.Connection;
2628
import java.sql.PreparedStatement;
2729
import java.sql.ResultSet;
2830
import java.sql.SQLException;
31+
import java.util.HashMap;
32+
import java.util.Map;
2933
import java.text.ParseException;
3034
import java.text.SimpleDateFormat;
3135
import java.util.Date;
@@ -34,6 +38,10 @@ public class Upgrade41810to41900 implements DbUpgrade, DbUpgradeSystemVmTemplate
3438
final static Logger LOG = Logger.getLogger(Upgrade41810to41900.class);
3539
private SystemVmTemplateRegistration systemVmTemplateRegistration;
3640

41+
private static final String ACCOUNT_DETAILS = "account_details";
42+
43+
private static final String DOMAIN_DETAILS = "domain_details";
44+
3745
private final SimpleDateFormat[] formats = {
3846
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"), new SimpleDateFormat("dd/MM/yyyy HH:mm:ss"),
3947
new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy")};
@@ -66,6 +74,7 @@ public InputStream[] getPrepareScripts() {
6674

6775
@Override
6876
public void performDataMigration(Connection conn) {
77+
decryptConfigurationValuesFromAccountAndDomainScopesNotInSecureHiddenCategories(conn);
6978
migrateBackupDates(conn);
7079
}
7180

@@ -95,6 +104,37 @@ public void updateSystemVmTemplates(Connection conn) {
95104
}
96105
}
97106

107+
protected void decryptConfigurationValuesFromAccountAndDomainScopesNotInSecureHiddenCategories(Connection conn) {
108+
LOG.info("Decrypting global configuration values from the following tables: account_details and domain_details.");
109+
110+
Map<Long, String> accountsMap = getConfigsWithScope(conn, ACCOUNT_DETAILS);
111+
updateConfigValuesWithScope(conn, accountsMap, ACCOUNT_DETAILS);
112+
LOG.info("Successfully decrypted configurations from account_details table.");
113+
114+
Map<Long, String> domainsMap = getConfigsWithScope(conn, DOMAIN_DETAILS);
115+
updateConfigValuesWithScope(conn, domainsMap, DOMAIN_DETAILS);
116+
LOG.info("Successfully decrypted configurations from domain_details table.");
117+
}
118+
119+
protected Map<Long, String> getConfigsWithScope(Connection conn, String table) {
120+
Map<Long, String> configsToBeUpdated = new HashMap<>();
121+
String selectDetails = String.format("SELECT details.id, details.value from cloud.%s details, cloud.configuration c " +
122+
"WHERE details.name = c.name AND c.category NOT IN ('Hidden', 'Secure') AND details.value <> \"\" ORDER BY details.id;", table);
123+
124+
try (PreparedStatement pstmt = conn.prepareStatement(selectDetails)) {
125+
try (ResultSet result = pstmt.executeQuery()) {
126+
while (result.next()) {
127+
configsToBeUpdated.put(result.getLong("id"), result.getString("value"));
128+
}
129+
}
130+
return configsToBeUpdated;
131+
} catch (SQLException e) {
132+
String message = String.format("Unable to retrieve data from table [%s] due to [%s].", table, e.getMessage());
133+
LOG.error(message, e);
134+
throw new CloudRuntimeException(message, e);
135+
}
136+
}
137+
98138
public void migrateBackupDates(Connection conn) {
99139
LOG.info("Trying to convert backups' date column from varchar(255) to datetime type.");
100140

@@ -125,6 +165,27 @@ private void modifyDateColumnNameAndCreateNewOne(Connection conn) {
125165
}
126166
}
127167

168+
protected void updateConfigValuesWithScope(Connection conn, Map<Long, String> configsToBeUpdated, String table) {
169+
String updateConfigValues = String.format("UPDATE cloud.%s SET value = ? WHERE id = ?;", table);
170+
171+
for (Map.Entry<Long, String> config : configsToBeUpdated.entrySet()) {
172+
try (PreparedStatement pstmt = conn.prepareStatement(updateConfigValues)) {
173+
String decryptedValue = DBEncryptionUtil.decrypt(config.getValue());
174+
175+
pstmt.setString(1, decryptedValue);
176+
pstmt.setLong(2, config.getKey());
177+
178+
LOG.info(String.format("Updating config with ID [%s] to value [%s].", config.getKey(), decryptedValue));
179+
pstmt.executeUpdate();
180+
} catch (SQLException | EncryptionOperationNotPossibleException e) {
181+
String message = String.format("Unable to update config value with ID [%s] on table [%s] due to [%s]. The config value may already be decrypted.",
182+
config.getKey(), table, e);
183+
LOG.error(message);
184+
throw new CloudRuntimeException(message, e);
185+
}
186+
}
187+
}
188+
128189
private void fetchDatesAndMigrateToNewColumn(Connection conn) {
129190
String selectBackupDates = "SELECT `id`, `old_date` FROM `cloud`.`backups` WHERE 1;";
130191
String date;

engine/schema/src/main/java/com/cloud/user/AccountDetailVO.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525

2626
import org.apache.cloudstack.api.InternalIdentity;
2727

28-
import com.cloud.utils.db.Encrypt;
29-
3028
@Entity
3129
@Table(name = "account_details")
3230
public class AccountDetailVO implements InternalIdentity {
@@ -41,7 +39,6 @@ public class AccountDetailVO implements InternalIdentity {
4139
@Column(name = "name")
4240
private String name;
4341

44-
@Encrypt
4542
@Column(name = "value", length=4096)
4643
private String value;
4744

framework/config/src/main/java/org/apache/cloudstack/framework/config/impl/ConfigurationVO.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ public void setValue(String value) {
170170

171171
@Override
172172
public boolean isEncrypted() {
173-
return "Hidden".equals(getCategory()) || "Secure".equals(getCategory());
173+
return StringUtils.equalsAny(getCategory(), "Hidden", "Secure");
174174
}
175175

176176
@Override

framework/db/src/main/java/com/cloud/utils/crypt/EncryptionSecretKeyChanger.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,8 @@ private boolean migrateData(String oldDBKey, String newDBKey, String oldEncrypto
475475

476476
// migrate resource details values
477477
migrateHostDetails(conn);
478+
migrateEncryptedAccountDetails(conn);
479+
migrateEncryptedDomainDetails(conn);
478480
migrateClusterDetails(conn);
479481
migrateImageStoreDetails(conn);
480482
migrateStoragePoolDetails(conn);
@@ -497,6 +499,30 @@ private boolean migrateData(String oldDBKey, String newDBKey, String oldEncrypto
497499
return true;
498500
}
499501

502+
private void migrateEncryptedAccountDetails(Connection conn) {
503+
System.out.println("Beginning migration of account_details encrypted values");
504+
505+
String tableName = "account_details";
506+
String selectSql = "SELECT details.id, details.value from account_details details, cloud.configuration c " +
507+
"WHERE details.name = c.name AND c.category IN ('Hidden', 'Secure') AND details.value <> \"\" ORDER BY details.id;";
508+
String updateSql = "UPDATE cloud.account_details SET value = ? WHERE id = ?;";
509+
migrateValueAndUpdateDatabaseById(conn, tableName, selectSql, updateSql, false);
510+
511+
System.out.println("End migration of account details values");
512+
}
513+
514+
private void migrateEncryptedDomainDetails(Connection conn) {
515+
System.out.println("Beginning migration of domain_details encrypted values");
516+
517+
String tableName = "domain_details";
518+
String selectSql = "SELECT details.id, details.value from domain_details details, cloud.configuration c " +
519+
"WHERE details.name = c.name AND c.category IN ('Hidden', 'Secure') AND details.value <> \"\" ORDER BY details.id;";
520+
String updateSql = "UPDATE cloud.domain_details SET value = ? WHERE id = ?;";
521+
migrateValueAndUpdateDatabaseById(conn, tableName, selectSql, updateSql, false);
522+
523+
System.out.println("End migration of domain details values");
524+
}
525+
500526
protected String migrateValue(String value) {
501527
if (StringUtils.isEmpty(value)) {
502528
return value;

server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747

4848

4949
import com.cloud.hypervisor.HypervisorGuru;
50+
import com.cloud.utils.crypt.DBEncryptionUtil;
5051
import org.apache.cloudstack.acl.SecurityChecker;
5152
import org.apache.cloudstack.affinity.AffinityGroup;
5253
import org.apache.cloudstack.affinity.AffinityGroupService;
@@ -664,7 +665,7 @@ public boolean stop() {
664665

665666
@Override
666667
@DB
667-
public String updateConfiguration(final long userId, final String name, final String category, final String value, final String scope, final Long resourceId) {
668+
public String updateConfiguration(final long userId, final String name, final String category, String value, final String scope, final Long resourceId) {
668669
final String validationMsg = validateConfigurationValue(name, value, scope);
669670

670671
if (validationMsg != null) {
@@ -677,6 +678,11 @@ public String updateConfiguration(final long userId, final String name, final St
677678
// if scope is mentioned as global or not mentioned then it is normal
678679
// global parameter updation
679680
if (scope != null && !scope.isEmpty() && !ConfigKey.Scope.Global.toString().equalsIgnoreCase(scope)) {
681+
boolean valueEncrypted = shouldEncryptValue(category);
682+
if (valueEncrypted) {
683+
value = DBEncryptionUtil.encrypt(value);
684+
}
685+
680686
switch (ConfigKey.Scope.valueOf(scope)) {
681687
case Zone:
682688
final DataCenterVO zone = _zoneDao.findById(resourceId);
@@ -767,7 +773,8 @@ public String updateConfiguration(final long userId, final String name, final St
767773
default:
768774
throw new InvalidParameterValueException("Scope provided is invalid");
769775
}
770-
return value;
776+
777+
return valueEncrypted ? DBEncryptionUtil.decrypt(value) : value;
771778
}
772779

773780
// Execute all updates in a single transaction
@@ -864,6 +871,10 @@ public String updateConfiguration(final long userId, final String name, final St
864871
return _configDao.getValue(name);
865872
}
866873

874+
private boolean shouldEncryptValue(String category) {
875+
return StringUtils.equalsAny(category, "Hidden", "Secure");
876+
}
877+
867878
/**
868879
* Updates the 'hypervisor.list' value to match the new custom hypervisor name set as newValue if the previous value was set
869880
*/
@@ -890,10 +901,11 @@ public Configuration updateConfiguration(final UpdateCfgCmd cmd) throws InvalidP
890901
final Long imageStoreId = cmd.getImageStoreId();
891902
Long accountId = cmd.getAccountId();
892903
Long domainId = cmd.getDomainId();
893-
CallContext.current().setEventDetails(" Name: " + name + " New Value: " + (name.toLowerCase().contains("password") ? "*****" : value == null ? "" : value));
894904
// check if config value exists
895905
final ConfigurationVO config = _configDao.findByName(name);
896-
String catergory = null;
906+
String category = null;
907+
String eventValue = encryptEventValueIfConfigIsEncrypted(config, value);
908+
CallContext.current().setEventDetails(String.format(" Name: %s New Value: %s", name, eventValue));
897909

898910
final Account caller = CallContext.current().getCallingAccount();
899911
if (_accountMgr.isDomainAdmin(caller.getId())) {
@@ -912,9 +924,9 @@ public Configuration updateConfiguration(final UpdateCfgCmd cmd) throws InvalidP
912924
s_logger.warn("Probably the component manager where configuration variable " + name + " is defined needs to implement Configurable interface");
913925
throw new InvalidParameterValueException("Config parameter with name " + name + " doesn't exist");
914926
}
915-
catergory = _configDepot.get(name).category();
927+
category = _configDepot.get(name).category();
916928
} else {
917-
catergory = config.getCategory();
929+
category = config.getCategory();
918930
}
919931

920932
validateIpAddressRelatedConfigValues(name, value);
@@ -971,14 +983,21 @@ public Configuration updateConfiguration(final UpdateCfgCmd cmd) throws InvalidP
971983
value = (id == null) ? null : "";
972984
}
973985

974-
final String updatedValue = updateConfiguration(userId, name, catergory, value, scope, id);
986+
final String updatedValue = updateConfiguration(userId, name, category, value, scope, id);
975987
if (value == null && updatedValue == null || updatedValue.equalsIgnoreCase(value)) {
976988
return _configDao.findByName(name);
977989
} else {
978990
throw new CloudRuntimeException("Unable to update configuration parameter " + name);
979991
}
980992
}
981993

994+
private String encryptEventValueIfConfigIsEncrypted(ConfigurationVO config, String value) {
995+
if (config != null && config.isEncrypted()) {
996+
return "*****";
997+
}
998+
return Objects.requireNonNullElse(value, "");
999+
}
1000+
9821001
private ParamCountPair getParamCount(Map<String, Long> scopeMap) {
9831002
Long id = null;
9841003
int paramCount = 0;

0 commit comments

Comments
 (0)