feat: mask encrypted config values in SCT logs

Keep plaintext out of refresh and group-load logs by tracking encrypted keys and replacing values with length plus a per-process salted fingerprint.

Co-authored-by: Cursor <cursoragent@cursor.com>
pull/1818/head
evelynwei 1 week ago
parent 49bae9ff89
commit 333d575215

@ -16,3 +16,4 @@
- [feat: support audit log](https://github.com/Tencent/spring-cloud-tencent/pull/1812)
- [feat: support config effective value ](https://github.com/Tencent/spring-cloud-tencent/pull/1815)
- [feat: support encoding and decoding TSF headers without TSF Consul](https://github.com/Tencent/spring-cloud-tencent/pull/1816)
- [feat: mask config values of encrypted config files in logs](https://github.com/Tencent/spring-cloud-tencent/pull/1817)

@ -17,6 +17,10 @@
package com.tencent.cloud.polaris.config.adapter;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.HashSet;
@ -66,6 +70,24 @@ public abstract class PolarisConfigPropertyAutoRefresher implements ApplicationL
private static final Logger LOGGER = LoggerFactory.getLogger(PolarisConfigPropertyAutoRefresher.class);
private static final Set<String> registeredPolarisPropertySets = Sets.newConcurrentHashSet();
/**
* Property keys contributed by encrypted config files. Values of these keys must never be
* written to logs in plain text.
* <p>
* Grow-only on purpose: once a key is known to be sensitive, keep masking it even after the
* encrypted file drops it. Over-masking only costs troubleshooting convenience, while
* under-masking is a leak.
*/
private static final Set<String> encryptedPropertyKeys = Sets.newConcurrentHashSet();
private static final String FINGERPRINT_ALGORITHM = "SHA-256";
/**
* Number of digest bytes kept in a fingerprint. 4 bytes are enough to tell two values apart.
*/
private static final int FINGERPRINT_BYTES = 4;
/**
* Random per JVM: see {@link #fingerprint(String)} for why the digest must be salted.
*/
private static final byte[] FINGERPRINT_SALT = newFingerprintSalt();
private final PolarisConfigProperties polarisConfigProperties;
private final AtomicBoolean registered = new AtomicBoolean(false);
// this class provides customized logic for some customers to configure special business group files
@ -191,6 +213,10 @@ public abstract class PolarisConfigPropertyAutoRefresher implements ApplicationL
LOGGER.info("[SCT Config] received polaris config change event and will refresh spring context." + " namespace = {}, group = {}, fileName = {}",
listenPolarisPropertySource.getNamespace(), listenPolarisPropertySource.getGroup(), listenPolarisPropertySource.getFileName());
// the change event is the only place carrying the plugin-level ConfigFile,
// which is where the server-side per-file encrypted flag can be read
markEncryptedKeys(listenPolarisPropertySource.getConfigKVFile(), configKVFileChangeEvent.getConfigFile());
Map<String, Object> effectSource = effectPolarisPropertySource.getSource();
Map<String, Object> listenSource = listenPolarisPropertySource.getSource();
boolean isGroupRefresh = !listenPolarisPropertySource.equals(effectPolarisPropertySource);
@ -204,7 +230,15 @@ public abstract class PolarisConfigPropertyAutoRefresher implements ApplicationL
for (String changedKey : configKVFileChangeEvent.changedKeys()) {
ConfigPropertyChangeInfo configPropertyChangeInfo = configKVFileChangeEvent.getChangeInfo(changedKey);
LOGGER.info("[SCT Config] changed property = {}", configPropertyChangeInfo);
if (isEncryptedKey(changedKey)) {
LOGGER.info("[SCT Config] changed property = [key={}, changeType={}, oldValue={}, newValue={}]",
configPropertyChangeInfo.getPropertyName(), configPropertyChangeInfo.getChangeType(),
maskValue(configPropertyChangeInfo.getOldValue()),
maskValue(configPropertyChangeInfo.getNewValue()));
}
else {
LOGGER.info("[SCT Config] changed property = {}", configPropertyChangeInfo);
}
// new ability to dynamically change log levels
try {
@ -267,6 +301,96 @@ public abstract class PolarisConfigPropertyAutoRefresher implements ApplicationL
polarisConfigCustomExtensionLayer.executeRegisterPublishChangeListener(listenPolarisPropertySource, effectPolarisPropertySource);
}
/**
* Registers the property keys of an encrypted config file, so that their values can be masked
* in logs afterwards.
* <p>
* {@code configFile} must come from {@link ConfigKVFileChangeEvent#getConfigFile()}: that
* object belongs to the response chain, where {@code encrypted} is the per-file value pushed
* by the server. The request-side object is not usable as a criterion, because the crypto
* filter unconditionally sets it to true to declare crypto support.
*
* @param kvFile the config file whose property names will be registered
* @param configFile the plugin-level config file carrying the encrypted flag, may be null
*/
private void markEncryptedKeys(ConfigKVFile kvFile, ConfigFile configFile) {
if (kvFile == null || configFile == null || !configFile.isEncrypted()) {
return;
}
Set<String> propertyNames = kvFile.getPropertyNames();
if (!CollectionUtils.isEmpty(propertyNames)) {
encryptedPropertyKeys.addAll(propertyNames);
}
}
/**
* @param key the property key
* @return whether the value of the given key comes from an encrypted config file
*/
protected boolean isEncryptedKey(String key) {
return encryptedPropertyKeys.contains(key);
}
/**
* Masks a property value of an encrypted config file. The length and a fingerprint are kept as
* hints for troubleshooting, the content is not exposed.
* <p>
* Takes an Object rather than a String: both {@code ConfigPropertyChangeInfo#getOldValue()}
* and the resolved {@code @Value} result are declared as Object.
*
* @param value the raw value
* @return the masked value
*/
protected static String maskValue(Object value) {
if (value == null) {
return null;
}
String text = String.valueOf(value);
if (text.isEmpty()) {
return "";
}
return "***(len=" + text.length() + ", fp=" + fingerprint(text) + ")";
}
/**
* Salted and truncated digest of a value, so that two masked values can be told apart even when
* their lengths are equal (e.g. an old and a new password of the same length).
* <p>
* The salt is random per JVM on purpose. An unsalted digest of a single config value would be
* reversible by dictionary attack, since config values carry little entropy - that would defeat
* the masking. With a per-process salt the fingerprint stays comparable within one log file,
* which is what change diagnosis needs, and carries no information outside it.
* <p>
* Truncated to 4 bytes: a collision only makes two different values look alike, it never
* exposes a value.
*
* @param text the raw value
* @return an 8-char hex fingerprint
*/
private static byte[] newFingerprintSalt() {
byte[] salt = new byte[16];
new SecureRandom().nextBytes(salt);
return salt;
}
private static String fingerprint(String text) {
try {
MessageDigest digest = MessageDigest.getInstance(FINGERPRINT_ALGORITHM);
digest.update(FINGERPRINT_SALT);
byte[] hash = digest.digest(text.getBytes(StandardCharsets.UTF_8));
StringBuilder builder = new StringBuilder(FINGERPRINT_BYTES * 2);
for (int i = 0; i < FINGERPRINT_BYTES; i++) {
builder.append(Character.forDigit((hash[i] >> 4) & 0xF, 16));
builder.append(Character.forDigit(hash[i] & 0xF, 16));
}
return builder.toString();
}
catch (NoSuchAlgorithmException e) {
// SHA-256 is mandated by the JDK spec, so this is unreachable in practice
return "unavailable";
}
}
private Map<String, ConfigFileMetadata> calculateUnregister(List<ConfigFileMetadata> oldConfigFileMetadataList,
List<ConfigFileMetadata> newConfigFileMetadataList) {
@ -334,4 +458,12 @@ public abstract class PolarisConfigPropertyAutoRefresher implements ApplicationL
public void setRegistered(boolean registered) {
this.registered.set(registered);
}
/**
* Just for junit test. {@code encryptedPropertyKeys} is static and grow-only, so it has to be
* reset between test methods.
*/
public static void clearEncryptedPropertyKeys() {
encryptedPropertyKeys.clear();
}
}

@ -79,7 +79,7 @@ public class PolarisRefreshAffectedContextRefresher extends PolarisConfigPropert
}
// update the attribute with @Value annotation
for (SpringValue val : targetValues) {
updateSpringValue(val);
updateSpringValue(changedKey, val);
}
}
@ -101,15 +101,20 @@ public class PolarisRefreshAffectedContextRefresher extends PolarisConfigPropert
}
}
private void updateSpringValue(SpringValue springValue) {
private void updateSpringValue(String changedKey, SpringValue springValue) {
try {
Object value = resolvePropertyValue(springValue);
springValue.update(value);
LOGGER.info("[SCT Config] Auto update polaris changed value successfully, new value: {}, {}", value,
// values of encrypted config files must not be logged in plain text
Object displayValue = isEncryptedKey(changedKey) ? maskValue(value) : value;
LOGGER.info("[SCT Config] Auto update polaris changed value successfully, new value: {}, {}", displayValue,
springValue);
}
catch (Throwable ex) {
// SpringValue.toString() carries no property value, so it is safe to log as is.
// The stack trace may still embed the raw value (e.g. unresolvable placeholder),
// which is kept on purpose: losing it would make refresh failures undiagnosable.
LOGGER.error("[SCT Config] Auto update polaris changed value failed, {}", springValue.toString(), ex);
}
}

@ -82,9 +82,10 @@ public final class PolarisPropertySourceUtils {
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("namespace='" + namespace + '\''
+ ", group='" + group + '\'' + ", fileName='" + compositeConfigFile + '\''
+ ", map='" + map + '\'');
// only coordinates and key names: values of encrypted config files must not be logged.
// This method cannot tell whether the group holds encrypted files, so no value is logged at all.
LOGGER.debug("[SCT Config] load group property source. namespace = {}, group = {}, propertyCount = {}, keys = {}",
namespace, group, map.size(), map.keySet());
}
return new PolarisPropertySource(namespace, group, "", compositeConfigFile, map);

@ -0,0 +1,425 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.polaris.config.adapter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.tencent.cloud.polaris.config.config.PolarisConfigProperties;
import com.tencent.cloud.polaris.config.spring.property.PlaceholderHelper;
import com.tencent.cloud.polaris.config.spring.property.SpringValue;
import com.tencent.cloud.polaris.config.spring.property.SpringValueRegistry;
import com.tencent.cloud.polaris.config.utils.PolarisPropertySourceUtils;
import com.tencent.polaris.api.plugin.common.ValueContext;
import com.tencent.polaris.api.plugin.compose.Extensions;
import com.tencent.polaris.api.plugin.configuration.ConfigFile;
import com.tencent.polaris.client.api.SDKContext;
import com.tencent.polaris.configuration.api.core.ChangeType;
import com.tencent.polaris.configuration.api.core.ConfigFileService;
import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeEvent;
import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.slf4j.LoggerFactory;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.context.ConfigurableApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Test for sensitive data masking of encrypted config files.
*
* <p>Covers the log-masking behaviour of {@link PolarisConfigPropertyAutoRefresher},
* {@link PolarisRefreshAffectedContextRefresher} and {@link PolarisPropertySourceUtils}.
*
* @author evelynwei
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class PolarisConfigSensitiveDataMaskingTest {
private static final String SENSITIVE_VALUE = "root-password-1234";
private static final String PLAIN_VALUE = "plain-value";
private final String testNamespace = "testNamespace";
private final String testFileGroup = "testFileGroup";
private final String testFileName = "application.properties";
@Mock
private PolarisConfigProperties polarisConfigProperties;
@Mock
private SpringValueRegistry springValueRegistry;
@Mock
private PlaceholderHelper placeholderHelper;
@Mock
private ConfigFileService configFileService;
@Mock
private ContextRefresher contextRefresher;
@Mock
private SDKContext sdkContext;
@Mock
private Extensions extensions;
@Mock
private ValueContext valueContext;
private final List<ListAppender<ILoggingEvent>> appenders = new ArrayList<>();
@BeforeEach
public void setUp() {
PolarisPropertySourceManager.clearPropertySources();
PolarisConfigPropertyAutoRefresher.clearEncryptedPropertyKeys();
}
@AfterEach
public void tearDown() {
PolarisConfigPropertyAutoRefresher.clearEncryptedPropertyKeys();
for (ListAppender<ILoggingEvent> appender : appenders) {
appender.stop();
}
appenders.clear();
}
/**
* Item 5: maskValue keeps the length and fingerprint hints and never exposes the content.
*/
@Test
public void testMaskValue() {
assertThat(PolarisConfigPropertyAutoRefresher.maskValue(null)).isNull();
assertThat(PolarisConfigPropertyAutoRefresher.maskValue("")).isEmpty();
assertThat(PolarisConfigPropertyAutoRefresher.maskValue(SENSITIVE_VALUE))
.matches("\\*\\*\\*\\(len=" + SENSITIVE_VALUE.length() + ", fp=[0-9a-f]{8}\\)")
.doesNotContain(SENSITIVE_VALUE);
// non-String Object must not blow up
assertThat(PolarisConfigPropertyAutoRefresher.maskValue(12345)).matches("\\*\\*\\*\\(len=5, fp=[0-9a-f]{8}\\)");
assertThat(PolarisConfigPropertyAutoRefresher.maskValue(Boolean.TRUE))
.matches("\\*\\*\\*\\(len=4, fp=[0-9a-f]{8}\\)");
}
/**
* Item 10: the fingerprint is stable for the same value and differs for different values of the
* same length. This is what makes an old/new pair distinguishable when their lengths match.
*/
@Test
public void testFingerprintDistinguishesEqualLengthValues() {
String oldValue = "passwordAAAA";
String newValue = "passwordBBBB";
assertThat(oldValue).hasSameSizeAs(newValue);
String maskedOld = PolarisConfigPropertyAutoRefresher.maskValue(oldValue);
String maskedNew = PolarisConfigPropertyAutoRefresher.maskValue(newValue);
assertThat(maskedOld).isNotEqualTo(maskedNew);
// stable within the same process, so repeated logging of one value reads consistently
assertThat(PolarisConfigPropertyAutoRefresher.maskValue(oldValue)).isEqualTo(maskedOld);
}
/**
* Item 3: keys of an encrypted file are registered, keys of a plain file are not.
*/
@Test
public void testEncryptedKeysRegisteredOnlyForEncryptedFile() {
PolarisConfigPropertyAutoRefresher refresher = buildRefresher();
MockedConfigKVFile encryptedFile = new MockedConfigKVFile(contentOf("encrypted.key", SENSITIVE_VALUE));
fireChange(refresher, encryptedFile, encryptedConfigFile(), "encrypted.key",
new ConfigPropertyChangeInfo("encrypted.key", "old", SENSITIVE_VALUE, ChangeType.MODIFIED));
assertThat(refresher.isEncryptedKey("encrypted.key")).isTrue();
MockedConfigKVFile plainFile = new MockedConfigKVFile(contentOf("plain.key", PLAIN_VALUE));
fireChange(refresher, plainFile, plainConfigFile(), "plain.key",
new ConfigPropertyChangeInfo("plain.key", "old", PLAIN_VALUE, ChangeType.MODIFIED));
assertThat(refresher.isEncryptedKey("plain.key")).isFalse();
}
/**
* Item 4: a null ConfigFile (ConfigKVFileChangeEvent#getConfigFile may be null) must neither
* throw nor register anything.
*/
@Test
public void testNullConfigFileNeitherThrowsNorRegisters() {
PolarisConfigPropertyAutoRefresher refresher = buildRefresher();
MockedConfigKVFile file = new MockedConfigKVFile(contentOf("some.key", PLAIN_VALUE));
fireChange(refresher, file, null, "some.key",
new ConfigPropertyChangeInfo("some.key", "old", PLAIN_VALUE, ChangeType.MODIFIED));
assertThat(refresher.isEncryptedKey("some.key")).isFalse();
}
/**
* Item 1: the change log of an encrypted config file carries no raw value.
*/
@Test
public void testEncryptedConfigChangeLogIsMasked() {
PolarisConfigPropertyAutoRefresher refresher = buildRefresher();
ListAppender<ILoggingEvent> appender = attachAppender(PolarisConfigPropertyAutoRefresher.class);
MockedConfigKVFile file = new MockedConfigKVFile(contentOf("db.password", SENSITIVE_VALUE));
fireChange(refresher, file, encryptedConfigFile(), "db.password",
new ConfigPropertyChangeInfo("db.password", SENSITIVE_VALUE, SENSITIVE_VALUE + "-new",
ChangeType.MODIFIED));
String logs = renderLogs(appender);
assertThat(logs).doesNotContain(SENSITIVE_VALUE);
// key and change type stay observable for troubleshooting
assertThat(logs).contains("db.password").contains("MODIFIED").contains("***(len=");
}
/**
* Item 2: a plain config file keeps the original behaviour and logs the raw value.
*/
@Test
public void testPlainConfigChangeLogKeepsRawValue() {
PolarisConfigPropertyAutoRefresher refresher = buildRefresher();
ListAppender<ILoggingEvent> appender = attachAppender(PolarisConfigPropertyAutoRefresher.class);
MockedConfigKVFile file = new MockedConfigKVFile(contentOf("app.name", PLAIN_VALUE));
fireChange(refresher, file, plainConfigFile(), "app.name",
new ConfigPropertyChangeInfo("app.name", "old-name", PLAIN_VALUE, ChangeType.MODIFIED));
String logs = renderLogs(appender);
assertThat(logs).contains(PLAIN_VALUE).doesNotContain("***(len=");
}
/**
* Item 6: updateSpringValue masks the new value when the changed key is encrypted, and keeps it
* as is otherwise.
*/
@Test
public void testSpringValueRefreshLogRespectsEncryptedKey() throws Exception {
PolarisRefreshAffectedContextRefresher refresher = buildAffectedRefresher(SENSITIVE_VALUE);
ListAppender<ILoggingEvent> appender = attachAppender(PolarisRefreshAffectedContextRefresher.class);
MockedConfigKVFile encryptedFile = new MockedConfigKVFile(contentOf("db.password", SENSITIVE_VALUE));
fireChange(refresher, encryptedFile, encryptedConfigFile(), "db.password",
new ConfigPropertyChangeInfo("db.password", "old", SENSITIVE_VALUE, ChangeType.MODIFIED));
String logs = renderLogs(appender);
assertThat(logs).contains("Auto update polaris changed value successfully");
assertThat(logs).doesNotContain(SENSITIVE_VALUE);
assertThat(logs).contains("***(len=" + SENSITIVE_VALUE.length() + ", fp=");
}
@Test
public void testSpringValueRefreshLogKeepsPlainValue() throws Exception {
PolarisRefreshAffectedContextRefresher refresher = buildAffectedRefresher(PLAIN_VALUE);
ListAppender<ILoggingEvent> appender = attachAppender(PolarisRefreshAffectedContextRefresher.class);
MockedConfigKVFile plainFile = new MockedConfigKVFile(contentOf("app.name", PLAIN_VALUE));
fireChange(refresher, plainFile, plainConfigFile(), "app.name",
new ConfigPropertyChangeInfo("app.name", "old", PLAIN_VALUE, ChangeType.MODIFIED));
String logs = renderLogs(appender);
assertThat(logs).contains(PLAIN_VALUE).doesNotContain("***(len=");
}
/**
* Item 9: SpringValue#toString carries no property value, which is why the refresh failure log
* needs no masking.
*/
@Test
public void testSpringValueToStringCarriesNoValue() throws Exception {
MockedConfigChange bean = new MockedConfigChange();
bean.setK1(SENSITIVE_VALUE);
Field field = bean.getClass().getDeclaredField("k1");
SpringValue springValue = new SpringValue("db.password", "${db.password}", bean, "mockedConfigChange", field);
assertThat(springValue.toString())
.doesNotContain(SENSITIVE_VALUE)
.contains("db.password")
.contains("mockedConfigChange");
}
/**
* Item 7: the group-dimension DEBUG log carries key names only, never values.
*/
@Test
public void testGroupPropertySourceDebugLogCarriesNoValue() {
ch.qos.logback.classic.Logger logger =
(ch.qos.logback.classic.Logger) LoggerFactory.getLogger(PolarisPropertySourceUtils.class);
Level originalLevel = logger.getLevel();
logger.setLevel(Level.DEBUG);
ListAppender<ILoggingEvent> appender = attachAppender(PolarisPropertySourceUtils.class);
try {
MockedConfigKVFile file = new MockedConfigKVFile(contentOf("db.password", SENSITIVE_VALUE),
testFileName, testFileGroup, testNamespace);
com.tencent.polaris.configuration.api.core.ConfigFileGroup group =
new com.tencent.polaris.configuration.client.internal.RevisableConfigFileGroup(
testNamespace, testFileGroup, java.util.Collections.singletonList(file), "v1");
when(configFileService.getConfigFileGroup(testNamespace, testFileGroup)).thenReturn(group);
when(configFileService.getConfigPropertiesFile(testNamespace, testFileGroup, testFileName))
.thenReturn(file);
PolarisPropertySource source = PolarisPropertySourceUtils
.loadGroupPolarisPropertySource(configFileService, testNamespace, testFileGroup);
assertThat(source).isNotNull();
String logs = renderLogs(appender);
assertThat(logs).doesNotContain(SENSITIVE_VALUE);
assertThat(logs).contains("db.password").contains("propertyCount = 1");
}
finally {
logger.setLevel(originalLevel);
}
}
/**
* Item 8: locks the known boundary of the grow-only key set. A key that has never been seen in
* a file-dimension change event is not masked on its first refresh; once registered it stays
* masked even when the very same key later arrives from a plain file.
*/
@Test
public void testGrowOnlyKeySetBoundary() {
PolarisConfigPropertyAutoRefresher refresher = buildRefresher();
// never seen before -> not masked
assertThat(refresher.isEncryptedKey("shared.key")).isFalse();
MockedConfigKVFile encryptedFile = new MockedConfigKVFile(contentOf("shared.key", SENSITIVE_VALUE));
fireChange(refresher, encryptedFile, encryptedConfigFile(), "shared.key",
new ConfigPropertyChangeInfo("shared.key", "old", SENSITIVE_VALUE, ChangeType.MODIFIED));
assertThat(refresher.isEncryptedKey("shared.key")).isTrue();
// a later plain file carrying the same key does not un-register it
MockedConfigKVFile plainFile = new MockedConfigKVFile(contentOf("shared.key", PLAIN_VALUE));
fireChange(refresher, plainFile, plainConfigFile(), "shared.key",
new ConfigPropertyChangeInfo("shared.key", SENSITIVE_VALUE, PLAIN_VALUE, ChangeType.MODIFIED));
assertThat(refresher.isEncryptedKey("shared.key")).isTrue();
}
/**
* The affected-context refresher is used throughout: it is the default implementation and the
* only one whose {@code refreshConfigurationProperties} works against a mocked context.
*/
private PolarisConfigPropertyAutoRefresher buildRefresher() {
try {
return buildAffectedRefresher(PLAIN_VALUE);
}
catch (Exception e) {
throw new IllegalStateException("failed to build refresher", e);
}
}
private PolarisRefreshAffectedContextRefresher buildAffectedRefresher(String resolvedValue) throws Exception {
when(polarisConfigProperties.isAutoRefresh()).thenReturn(true);
when(sdkContext.getExtensions()).thenReturn(extensions);
when(extensions.getValueContext()).thenReturn(valueContext);
when(valueContext.getClientId()).thenReturn("mockClientId");
when(valueContext.getHost()).thenReturn("mockHost");
PolarisRefreshAffectedContextRefresher refresher = new PolarisRefreshAffectedContextRefresher(
polarisConfigProperties, springValueRegistry, placeholderHelper, configFileService, contextRefresher,
sdkContext);
ConfigurableApplicationContext applicationContext = mock(ConfigurableApplicationContext.class);
ConfigurableListableBeanFactory beanFactory = mock(ConfigurableListableBeanFactory.class);
TypeConverter typeConverter = mock(TypeConverter.class);
when(beanFactory.getTypeConverter()).thenReturn(typeConverter);
when(applicationContext.getBeanFactory()).thenReturn(beanFactory);
refresher.setApplicationContext(applicationContext);
when(typeConverter.convertIfNecessary(any(), any(), (Field) any())).thenReturn(resolvedValue);
MockedConfigChange bean = new MockedConfigChange();
Field field = bean.getClass().getDeclaredField("k1");
Collection<SpringValue> springValues = new ArrayList<>();
springValues.add(new SpringValue("db.password", "${db.password}", bean, "mockedConfigChange", field));
when(springValueRegistry.get(any(), any())).thenReturn(springValues);
return refresher;
}
/**
* Registers the property source, wires the change listener and fires one change event.
*/
private void fireChange(PolarisConfigPropertyAutoRefresher refresher, MockedConfigKVFile file,
ConfigFile configFile, String changedKey, ConfigPropertyChangeInfo changeInfo) {
Map<String, Object> source = new HashMap<>(contentOf(changedKey, changeInfo.getOldValue()));
PolarisPropertySource propertySource = new PolarisPropertySource(file.getNamespace(), file.getFileGroup(),
file.getFileName(), file, source);
refresher.registerPolarisConfigPublishChangeListener(propertySource);
Map<String, ConfigPropertyChangeInfo> changeInfos = new HashMap<>();
changeInfos.put(changedKey, changeInfo);
file.fireChangeListener(new ConfigKVFileChangeEvent(changeInfos, configFile));
}
private ConfigFile encryptedConfigFile() {
ConfigFile configFile = new ConfigFile(testNamespace, testFileGroup, testFileName);
configFile.setEncrypted(true);
return configFile;
}
private ConfigFile plainConfigFile() {
ConfigFile configFile = new ConfigFile(testNamespace, testFileGroup, testFileName);
configFile.setEncrypted(false);
return configFile;
}
private Map<String, Object> contentOf(String key, Object value) {
Map<String, Object> content = new HashMap<>();
content.put(key, value);
return content;
}
private ListAppender<ILoggingEvent> attachAppender(Class<?> loggerClass) {
ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(loggerClass);
ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.setContext(logger.getLoggerContext());
appender.start();
logger.addAppender(appender);
appenders.add(appender);
return appender;
}
private String renderLogs(ListAppender<ILoggingEvent> appender) {
StringBuilder builder = new StringBuilder();
for (ILoggingEvent event : appender.list) {
builder.append(event.getFormattedMessage()).append('\n');
}
return builder.toString();
}
}
Loading…
Cancel
Save