fix: attribute encrypted config by file for logs and ACK

Pass EffectiveValue.sourceFile to the SDK, register keys from ConfigKVFile.isEncrypted() on group add, and omit group DEBUG maps only when the composite file is encrypted.

Co-authored-by: Cursor <cursoragent@cursor.com>
pull/1818/head
evelynwei 5 days ago
parent 35501bccfb
commit f717d9edd5

@ -265,9 +265,27 @@ public class ConfigurationModifier implements PolarisConfigurationConfigModifier
if (StringUtils.isBlank(rootPath)) {
return false;
}
// Polaris config cache file names follow namespace#group#fileName.yaml.
File[] files = new File(rootPath).listFiles(file -> file.isFile()
&& file.getName().endsWith(".yaml") && file.getName().contains("#"));
File[] files = new File(rootPath).listFiles(this::isPolarisConfigCacheFile);
return files != null && files.length > 0;
}
/**
* Polaris persist files use {@code encodedNamespace#encodedFileGroup#encodedFileName.yaml}.
* Empty files and names that only happen to contain {@code #} are ignored.
*/
private boolean isPolarisConfigCacheFile(File file) {
if (file == null || !file.isFile() || file.length() <= 0) {
return false;
}
String name = file.getName();
if (!name.endsWith(".yaml")) {
return false;
}
String stem = name.substring(0, name.length() - ".yaml".length());
String[] parts = stem.split("#", -1);
return parts.length == 3
&& StringUtils.isNotBlank(parts[0])
&& StringUtils.isNotBlank(parts[1])
&& StringUtils.isNotBlank(parts[2]);
}
}

@ -185,6 +185,9 @@ public abstract class PolarisConfigPropertyAutoRefresher implements ApplicationL
changedKeys.addAll(p.getSource().keySet());
this.registerPolarisConfigPublishChangeListener(p, polarisPropertySource);
PolarisPropertySourceManager.addPropertySource(p);
// the loaded file carries the per-file encrypted flag, so register its keys
// right here rather than waiting for the first change event on that file
markEncryptedKeys(p.getConfigKVFile());
for (String changedKey : p.getSource().keySet()) {
polarisPropertySource.getSource().put(changedKey, p.getSource().get(changedKey));
refreshSpringValue(changedKey);
@ -215,7 +218,8 @@ public abstract class PolarisConfigPropertyAutoRefresher implements ApplicationL
// 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());
markEncryptedKeys(listenPolarisPropertySource.getConfigKVFile(),
configKVFileChangeEvent.getConfigFile(), configKVFileChangeEvent.changedKeys());
Map<String, Object> effectSource = effectPolarisPropertySource.getSource();
Map<String, Object> listenSource = listenPolarisPropertySource.getSource();
@ -245,6 +249,8 @@ public abstract class PolarisConfigPropertyAutoRefresher implements ApplicationL
if (changedKey.startsWith("logging.level") && changedKey.length() >= 14) {
String loggerName = changedKey.substring(14);
String newValue = (String) configPropertyChangeInfo.getNewValue();
// not masked even in an encrypted file: encryption is per file, and
// the value here is a log level, never a secret
LOGGER.info("[SCT Config] set logging.level loggerName:{}, newValue:{}", loggerName, newValue);
PolarisConfigLoggerContext.setLevel(loggerName, newValue);
}
@ -310,11 +316,37 @@ public abstract class PolarisConfigPropertyAutoRefresher implements ApplicationL
* 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
* @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
* @param changedKeys keys from the change event; an ADDED key may not be in
* {@code kvFile.getPropertyNames()} yet
*/
private void markEncryptedKeys(ConfigKVFile kvFile, ConfigFile configFile) {
if (kvFile == null || configFile == null || !configFile.isEncrypted()) {
private void markEncryptedKeys(ConfigKVFile kvFile, ConfigFile configFile, Set<String> changedKeys) {
if (configFile == null || !configFile.isEncrypted()) {
return;
}
if (kvFile != null) {
Set<String> propertyNames = kvFile.getPropertyNames();
if (!CollectionUtils.isEmpty(propertyNames)) {
encryptedPropertyKeys.addAll(propertyNames);
}
}
if (!CollectionUtils.isEmpty(changedKeys)) {
encryptedPropertyKeys.addAll(changedKeys);
}
}
/**
* Registers the property keys of a config file that reports itself as encrypted.
* <p>
* Used where no change event is available, e.g. a file newly added to a watched group.
* {@code ConfigKVFile#isEncrypted()} resolves to the server-pushed per-file flag, so it is
* usable from the very first load.
*
* @param kvFile the loaded config file, may be null
*/
private void markEncryptedKeys(ConfigKVFile kvFile) {
if (kvFile == null || !kvFile.isEncrypted()) {
return;
}
Set<String> propertyNames = kvFile.getPropertyNames();

@ -28,6 +28,7 @@ import com.tencent.polaris.configuration.api.core.ConfigKVFile;
import com.tencent.polaris.configuration.api.core.ConfigKeyConflict;
import com.tencent.polaris.configuration.api.core.EffectiveValue;
import com.tencent.polaris.configuration.client.internal.CompositeConfigFile;
import com.tencent.polaris.configuration.client.internal.DefaultConfigFileMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -103,18 +104,23 @@ public class SpringConfigEffectiveValueProvider implements ConfigEffectiveValueP
}
String effectiveValue = null;
String propertySource = null;
ConfigFileMetadata sourceFile = null;
try {
// Effective value: Environment has already converged by precedence and
// resolves ${} placeholders, i.e. the value the application actually reads.
effectiveValue = environment.getProperty(key);
propertySource = resolvePropertySourceName(key);
SourceMatch match = resolveSourceMatch(key);
if (match != null) {
propertySource = match.getName();
sourceFile = match.getFile();
}
}
catch (Throwable t) {
// Only degrade the effective-value dimension; the file value already in hand is returned.
LOG.warn("[SCT Config] Resolve effective value failed, key = {}, error = {}", key,
t.getClass().getSimpleName());
}
return new EffectiveValue(fileValue, effectiveValue, propertySource);
return new EffectiveValue(fileValue, effectiveValue, propertySource, sourceFile);
}
@Override
@ -138,15 +144,15 @@ public class SpringConfigEffectiveValueProvider implements ConfigEffectiveValueP
}
/**
* Walks the ordered PropertySource chain of the Environment and returns the source
* identity of the first one containing the key. Iteration order of
* MutablePropertySources is exactly Spring's precedence order.
* Walks the ordered PropertySource chain of the Environment and returns the match for
* the first source containing the key. Iteration order of MutablePropertySources is
* exactly Spring's precedence order.
*/
private String resolvePropertySourceName(String key) {
private SourceMatch resolveSourceMatch(String key) {
for (PropertySource<?> source : environment.getPropertySources()) {
String name = matchSource(source, key);
if (name != null) {
return name;
SourceMatch match = matchSource(source, key);
if (match != null) {
return match;
}
}
return null;
@ -157,17 +163,18 @@ public class SpringConfigEffectiveValueProvider implements ConfigEffectiveValueP
* in bootstrap mode polaris sources are wrapped into a CompositePropertySource
* ("polaris-config") and then into BootstrapPropertySource, so recursion is needed
* to reach the real PolarisPropertySource. Polaris sources are translated to a
* normalized coordinate {@code polaris:namespace/group/fileName}.
* normalized coordinate {@code polaris:namespace/group/fileName} plus the structured
* file coordinate the SDK needs to tell whether that source file is encrypted.
*/
private String matchSource(PropertySource<?> source, String key) {
private SourceMatch matchSource(PropertySource<?> source, String key) {
if (CONFIGURATION_PROPERTIES_SOURCE_NAME.equals(source.getName())) {
return null;
}
if (source instanceof CompositePropertySource) {
for (PropertySource<?> sub : ((CompositePropertySource) source).getPropertySources()) {
String name = matchSource(sub, key);
if (name != null) {
return name;
SourceMatch match = matchSource(sub, key);
if (match != null) {
return match;
}
}
return null;
@ -186,21 +193,22 @@ public class SpringConfigEffectiveValueProvider implements ConfigEffectiveValueP
// file containing the key is the effective one.
for (ConfigKVFile sub : ((CompositeConfigFile) file).getConfigKVFiles()) {
if (sub.getProperty(key, null) != null) {
return formatCoordinate(sub);
return toMatch(sub);
}
}
// Fallback: the composite's sub list is frozen at startup and does not cover
// files added to the group at runtime; look them up by group coordinate.
ConfigKVFile added = findInGroup(polarisSource.getNamespace(), polarisSource.getGroup(), key);
if (added != null) {
return formatCoordinate(added);
return toMatch(added);
}
}
else {
return formatCoordinate(file);
return toMatch(file);
}
}
return source.getName();
// Not a polaris config file: the SDK cannot judge encryption, so no coordinate.
return new SourceMatch(source.getName(), null);
}
/**
@ -275,6 +283,11 @@ public class SpringConfigEffectiveValueProvider implements ConfigEffectiveValueP
&& Objects.equals(candidate.getFileName(), metadata.getFileName());
}
private SourceMatch toMatch(ConfigKVFile file) {
return new SourceMatch(formatCoordinate(file),
new DefaultConfigFileMetadata(file.getNamespace(), file.getFileGroup(), file.getFileName()));
}
private String formatCoordinate(ConfigKVFile file) {
return SOURCE_PREFIX + file.getNamespace() + "/" + file.getFileGroup() + "/" + file.getFileName();
}
@ -285,4 +298,30 @@ public class SpringConfigEffectiveValueProvider implements ConfigEffectiveValueP
}
return metadata.getNamespace() + "/" + metadata.getFileGroup() + "/" + metadata.getFileName();
}
/**
* A matched property source: the display identity plus, for polaris config files, the
* structured coordinate. The coordinate lets the SDK look up that file's own snapshot to
* decide whether the effective value came from an encrypted file; it is consumed inside
* the SDK and never reported.
*/
private static final class SourceMatch {
private final String name;
private final ConfigFileMetadata file;
SourceMatch(String name, ConfigFileMetadata file) {
this.name = name;
this.file = file;
}
String getName() {
return name;
}
ConfigFileMetadata getFile() {
return file;
}
}
}

@ -82,10 +82,19 @@ public final class PolarisPropertySourceUtils {
}
if (LOGGER.isDebugEnabled()) {
// 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());
// Encryption is a per-file flag and a group merges several files, so a single
// encrypted member is enough to keep every value out of the log: the merged map
// gives no per-key attribution here. With none encrypted the original behaviour
// is preserved and values are logged as before.
if (compositeConfigFile.isEncrypted()) {
LOGGER.debug("[SCT Config] load group property source. namespace = {}, group = {}, "
+ "propertyCount = {}, keys = {} (values omitted: group holds encrypted files)",
namespace, group, map.size(), map.keySet());
}
else {
LOGGER.debug("[SCT Config] load group property source. namespace = {}, group = {}, "
+ "propertyCount = {}, map = {}", namespace, group, map.size(), map);
}
}
return new PolarisPropertySource(namespace, group, "", compositeConfigFile, map);

@ -718,7 +718,8 @@ class ConfigurationModifierTest {
void testModify_CheckAddressNotAccessibleWithLocalCacheFallback() throws Exception {
ConfigurationImpl configuration = buildMockConfiguration();
ConnectorConfigImpl connectorConfig = configuration.getConfigFile().getServerConnector();
Path cacheFile = Files.createFile(tempDir.resolve("default#group#application.properties.yaml"));
Path cacheFile = Files.writeString(tempDir.resolve("default#group#application.properties.yaml"),
"content: cached");
when(polarisContextProperties.getEnabled()).thenReturn(true);
when(polarisConfigProperties.isEnabled()).thenReturn(true);
@ -755,6 +756,72 @@ class ConfigurationModifierTest {
}
}
/**
* Fallback is enabled but the persist directory has no usable cache file.
* Expect: startup still aborts.
*/
@DisplayName("modify should still abort when fallback is enabled but cache dir is empty")
@Test
void testModify_CheckAddressNotAccessibleWithEmptyCacheDir() {
ConfigurationImpl configuration = buildMockConfiguration();
ConnectorConfigImpl connectorConfig = configuration.getConfigFile().getServerConnector();
when(polarisContextProperties.getEnabled()).thenReturn(true);
when(polarisConfigProperties.isEnabled()).thenReturn(true);
when(polarisConfigProperties.getDataSource()).thenReturn("polaris");
when(polarisConfigProperties.getLocalFileRootPath()).thenReturn(tempDir.toString());
when(polarisConfigProperties.getAddress()).thenReturn("grpc://127.0.0.1:1");
when(polarisConfigProperties.isCheckAddress()).thenReturn(true);
when(polarisConfigProperties.isShutdownIfConnectToConfigServerFailed()).thenReturn(true);
when(connectorConfig.getFallbackToLocalCache()).thenReturn(true);
List<String> parsedAddresses = Collections.singletonList("127.0.0.1:1");
try (MockedStatic<AddressUtils> mockedAddressUtils = Mockito.mockStatic(AddressUtils.class)) {
mockedAddressUtils.when(() -> AddressUtils.parseAddressList("grpc://127.0.0.1:1"))
.thenReturn(parsedAddresses);
mockedAddressUtils.when(() -> AddressUtils.accessible("127.0.0.1", 1, 3000))
.thenReturn(false);
assertThatThrownBy(() -> configurationModifier.modify(configuration))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("can not be connected");
}
}
/**
* A leftover yaml that only contains '#' must not count as a Polaris persist cache.
*/
@DisplayName("modify should ignore non-cache yaml files when deciding local fallback")
@Test
void testModify_CheckAddressNotAccessibleWithInvalidCacheFileName() throws Exception {
ConfigurationImpl configuration = buildMockConfiguration();
ConnectorConfigImpl connectorConfig = configuration.getConfigFile().getServerConnector();
Files.writeString(tempDir.resolve("not-a-cache.yaml"), "content");
Files.writeString(tempDir.resolve("only#two.yaml"), "content");
Files.createFile(tempDir.resolve("default#group#application.properties.yaml"));
when(polarisContextProperties.getEnabled()).thenReturn(true);
when(polarisConfigProperties.isEnabled()).thenReturn(true);
when(polarisConfigProperties.getDataSource()).thenReturn("polaris");
when(polarisConfigProperties.getLocalFileRootPath()).thenReturn(tempDir.toString());
when(polarisConfigProperties.getAddress()).thenReturn("grpc://127.0.0.1:1");
when(polarisConfigProperties.isCheckAddress()).thenReturn(true);
when(polarisConfigProperties.isShutdownIfConnectToConfigServerFailed()).thenReturn(true);
when(connectorConfig.getFallbackToLocalCache()).thenReturn(true);
List<String> parsedAddresses = Collections.singletonList("127.0.0.1:1");
try (MockedStatic<AddressUtils> mockedAddressUtils = Mockito.mockStatic(AddressUtils.class)) {
mockedAddressUtils.when(() -> AddressUtils.parseAddressList("grpc://127.0.0.1:1"))
.thenReturn(parsedAddresses);
mockedAddressUtils.when(() -> AddressUtils.accessible("127.0.0.1", 1, 3000))
.thenReturn(false);
assertThatThrownBy(() -> configurationModifier.modify(configuration))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("can not be connected");
}
}
/**
* Test modify with checkAddress enabled, address not accessible but shutdown disabled.
* Scenario: checkAddress is true, accessible returns false, shutdownIfConnectToConfigServerFailed is false.

@ -45,6 +45,8 @@ public class MockedConfigKVFile implements ConfigKVFile {
private final List<ConfigKVFileChangeListener> listeners = new ArrayList<>();
private boolean encrypted;
public MockedConfigKVFile(Map<String, Object> properties) {
this.properties = properties;
}
@ -197,4 +199,13 @@ public class MockedConfigKVFile implements ConfigKVFile {
public String getFileVersion() {
return "";
}
@Override
public boolean isEncrypted() {
return encrypted;
}
public void setEncrypted(boolean encrypted) {
this.encrypted = encrypted;
}
}

@ -19,10 +19,13 @@ package com.tencent.cloud.polaris.config.adapter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent;
@ -40,6 +43,8 @@ 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 com.tencent.polaris.configuration.client.internal.CompositeConfigFile;
import com.tencent.polaris.configuration.client.internal.RevisableConfigFileGroup;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -210,10 +215,14 @@ public class PolarisConfigSensitiveDataMaskingTest {
}
/**
* Item 2: a plain config file keeps the original behaviour and logs the raw value.
* Item 2: a plain config file keeps the original behaviour, i.e. the SDK's own
* ConfigPropertyChangeInfo rendering, with no masking applied by us.
* <p>
* That rendering carries the key and the change type but no values, so this asserts the
* absence of the mask marker rather than the presence of the raw value.
*/
@Test
public void testPlainConfigChangeLogKeepsRawValue() {
public void testPlainConfigChangeLogIsNotMasked() {
PolarisConfigPropertyAutoRefresher refresher = buildRefresher();
ListAppender<ILoggingEvent> appender = attachAppender(PolarisConfigPropertyAutoRefresher.class);
@ -222,7 +231,7 @@ public class PolarisConfigSensitiveDataMaskingTest {
new ConfigPropertyChangeInfo("app.name", "old-name", PLAIN_VALUE, ChangeType.MODIFIED));
String logs = renderLogs(appender);
assertThat(logs).contains(PLAIN_VALUE).doesNotContain("***(len=");
assertThat(logs).contains("app.name").contains("MODIFIED").doesNotContain("***(len=");
}
/**
@ -275,18 +284,40 @@ public class PolarisConfigSensitiveDataMaskingTest {
}
/**
* Item 7: the group-dimension DEBUG log carries key names only, never values.
* Item 7: a group holding an encrypted file logs key names only, never values.
*/
@Test
public void testGroupPropertySourceDebugLogCarriesNoValueWhenEncrypted() {
String logs = loadGroupAndRenderDebugLogs("db.password", SENSITIVE_VALUE, true);
assertThat(logs).doesNotContain(SENSITIVE_VALUE);
assertThat(logs).contains("db.password").contains("propertyCount = 1").contains("values omitted");
}
/**
* A group with no encrypted file keeps the original behaviour and logs the merged map, so the
* unencrypted scenario loses no diagnosability.
*/
@Test
public void testGroupPropertySourceDebugLogCarriesNoValue() {
public void testGroupPropertySourceDebugLogKeepsMapWhenNotEncrypted() {
String logs = loadGroupAndRenderDebugLogs("app.name", PLAIN_VALUE, false);
assertThat(logs).contains("app.name").contains(PLAIN_VALUE).contains("map = ");
}
/**
* Loads a one-file group at DEBUG level and returns what the loader logged.
*/
private String loadGroupAndRenderDebugLogs(String key, String value, boolean encrypted) {
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),
MockedConfigKVFile file = new MockedConfigKVFile(contentOf(key, value),
testFileName, testFileGroup, testNamespace);
file.setEncrypted(encrypted);
com.tencent.polaris.configuration.api.core.ConfigFileGroup group =
new com.tencent.polaris.configuration.client.internal.RevisableConfigFileGroup(
testNamespace, testFileGroup, java.util.Collections.singletonList(file), "v1");
@ -298,9 +329,7 @@ public class PolarisConfigSensitiveDataMaskingTest {
.loadGroupPolarisPropertySource(configFileService, testNamespace, testFileGroup);
assertThat(source).isNotNull();
String logs = renderLogs(appender);
assertThat(logs).doesNotContain(SENSITIVE_VALUE);
assertThat(logs).contains("db.password").contains("propertyCount = 1");
return renderLogs(appender);
}
finally {
logger.setLevel(originalLevel);
@ -331,6 +360,116 @@ public class PolarisConfigSensitiveDataMaskingTest {
assertThat(refresher.isEncryptedKey("shared.key")).isTrue();
}
/**
* An ADDED key may be absent from {@code ConfigKVFile#getPropertyNames()} when the listener
* runs; it must still be registered from {@code changedKeys()}.
*/
@Test
public void testAddedEncryptedKeyIsRegisteredFromChangeEvent() {
PolarisConfigPropertyAutoRefresher refresher = buildRefresher();
ListAppender<ILoggingEvent> appender = attachAppender(PolarisConfigPropertyAutoRefresher.class);
MockedConfigKVFile file = new MockedConfigKVFile(contentOf("existing.key", PLAIN_VALUE));
fireChange(refresher, file, encryptedConfigFile(), "db.password",
new ConfigPropertyChangeInfo("db.password", null, SENSITIVE_VALUE, ChangeType.ADDED));
assertThat(refresher.isEncryptedKey("db.password")).isTrue();
assertThat(renderLogs(appender)).doesNotContain(SENSITIVE_VALUE).contains("***(len=");
}
/**
* A log level is not a secret, so the logging.level line keeps the raw value even when the
* file is encrypted. Encryption is a per-file flag, so it also covers harmless keys.
*/
@Test
public void testEncryptedLoggingLevelChangeKeepsRawValue() {
PolarisConfigPropertyAutoRefresher refresher = buildRefresher();
ListAppender<ILoggingEvent> appender = attachAppender(PolarisConfigPropertyAutoRefresher.class);
String levelKey = "logging.level.com.example";
MockedConfigKVFile file = new MockedConfigKVFile(contentOf(levelKey, "DEBUG"));
fireChange(refresher, file, encryptedConfigFile(), levelKey,
new ConfigPropertyChangeInfo(levelKey, "INFO", "DEBUG", ChangeType.MODIFIED));
assertThat(renderLogs(appender)).contains("set logging.level loggerName:com.example, newValue:DEBUG");
}
/**
* A file newly added to a watched group carries its own encrypted flag, so its keys are
* registered at load time and the first @Value refresh log is already masked.
*/
@Test
public void testGroupAddOfEncryptedFileMasksSpringValueRefreshLog() throws Exception {
PolarisRefreshAffectedContextRefresher refresher = buildAffectedRefresher(SENSITIVE_VALUE);
ListAppender<ILoggingEvent> appender = attachAppender(PolarisRefreshAffectedContextRefresher.class);
fireGroupAdd(refresher, "encrypted-add.properties", SENSITIVE_VALUE, true);
assertThat(refresher.isEncryptedKey("db.password")).isTrue();
assertThat(renderLogs(appender)).contains("Auto update polaris changed value successfully")
.doesNotContain(SENSITIVE_VALUE)
.contains("***(len=");
}
/**
* A plain file added to a watched group keeps the original behaviour and logs the raw value:
* judging per file means the unencrypted scenario loses no diagnosability.
*/
@Test
public void testGroupAddOfPlainFileKeepsRawValue() throws Exception {
PolarisRefreshAffectedContextRefresher refresher = buildAffectedRefresher(PLAIN_VALUE);
ListAppender<ILoggingEvent> appender = attachAppender(PolarisRefreshAffectedContextRefresher.class);
fireGroupAdd(refresher, "plain-add.properties", PLAIN_VALUE, false);
assertThat(refresher.isEncryptedKey("db.password")).isFalse();
assertThat(renderLogs(appender)).contains(PLAIN_VALUE).doesNotContain("***(len=");
}
/**
* Registers a one-file group, then adds {@code addedFileName} carrying {@code addedValue}
* under {@code db.password} and waits for the group-add refresh to land.
* <p>
* Each caller must pass a distinct {@code addedFileName}: the registered-property-source set
* behind the group listener is static and grow-only, so a reused name is silently skipped.
*/
private void fireGroupAdd(PolarisRefreshAffectedContextRefresher refresher, String addedFileName,
String addedValue, boolean addedFileEncrypted) throws InterruptedException {
Map<String, Object> existing = new ConcurrentHashMap<>();
existing.put("app.name", PLAIN_VALUE);
MockedConfigKVFile file = new MockedConfigKVFile(existing, testFileName, testFileGroup, testNamespace);
when(configFileService.getConfigPropertiesFile(testNamespace, testFileGroup, testFileName))
.thenReturn(file);
CompositeConfigFile compositeConfigFile = new CompositeConfigFile(Collections.singletonList(file));
PolarisPropertySource polarisPropertySource = new PolarisPropertySource(testNamespace, testFileGroup,
testFileName, compositeConfigFile, new ConcurrentHashMap<>(existing));
PolarisPropertySourceManager.addPropertySource(polarisPropertySource);
RevisableConfigFileGroup group = new RevisableConfigFileGroup(testNamespace, testFileGroup,
Collections.singletonList(file), "v1");
when(configFileService.getConfigFileGroup(testNamespace, testFileGroup)).thenReturn(group);
when(sdkContext.getExtensions()).thenReturn(extensions);
when(extensions.getValueContext()).thenReturn(valueContext);
refresher.onApplicationEvent(null);
Map<String, Object> added = new ConcurrentHashMap<>();
added.put("db.password", addedValue);
MockedConfigKVFile file2 = new MockedConfigKVFile(added, addedFileName, testFileGroup, testNamespace);
file2.setEncrypted(addedFileEncrypted);
when(configFileService.getConfigPropertiesFile(testNamespace, testFileGroup, addedFileName))
.thenReturn(file2);
group.updateConfigFileList(Arrays.asList(file, file2), "v2");
long deadline = System.currentTimeMillis() + 5000;
while (System.currentTimeMillis() < deadline && polarisPropertySource.getProperty("db.password") == null) {
Thread.sleep(50);
}
assertThat(polarisPropertySource.getProperty("db.password")).isEqualTo(addedValue);
}
/**
* The affected-context refresher is used throughout: it is the default implementation and the
* only one whose {@code refreshConfigurationProperties} works against a mocked context.
@ -352,8 +491,8 @@ public class PolarisConfigSensitiveDataMaskingTest {
when(valueContext.getHost()).thenReturn("mockHost");
PolarisRefreshAffectedContextRefresher refresher = new PolarisRefreshAffectedContextRefresher(
polarisConfigProperties, springValueRegistry, placeholderHelper, configFileService, contextRefresher,
sdkContext);
polarisConfigProperties, springValueRegistry, placeholderHelper, configFileService,
contextRefresher, sdkContext);
ConfigurableApplicationContext applicationContext = mock(ConfigurableApplicationContext.class);
ConfigurableListableBeanFactory beanFactory = mock(ConfigurableListableBeanFactory.class);

@ -100,7 +100,8 @@ public class PolarisPropertiesSourceAutoRefresherTest {
@Test
public void testConfigFileChanged() throws Exception {
PolarisRefreshAffectedContextRefresher refresher = new PolarisRefreshAffectedContextRefresher(
polarisConfigProperties, springValueRegistry, placeholderHelper, configFileService, contextRefresher, sdkContext);
polarisConfigProperties, springValueRegistry, placeholderHelper, configFileService,
contextRefresher, sdkContext);
ConfigurableApplicationContext applicationContext = mock(ConfigurableApplicationContext.class);
ConfigurableListableBeanFactory beanFactory = mock(ConfigurableListableBeanFactory.class);
TypeConverter typeConverter = mock(TypeConverter.class);
@ -159,7 +160,8 @@ public class PolarisPropertiesSourceAutoRefresherTest {
@Test
public void testConfigFileGroupChanged() throws Exception {
PolarisRefreshAffectedContextRefresher refresher = new PolarisRefreshAffectedContextRefresher(
polarisConfigProperties, springValueRegistry, placeholderHelper, configFileService, contextRefresher, sdkContext);
polarisConfigProperties, springValueRegistry, placeholderHelper, configFileService,
contextRefresher, sdkContext);
ConfigurableApplicationContext applicationContext = mock(ConfigurableApplicationContext.class);
ConfigurableListableBeanFactory beanFactory = mock(ConfigurableListableBeanFactory.class);
TypeConverter typeConverter = mock(TypeConverter.class);

@ -123,6 +123,8 @@ class SpringConfigEffectiveValueProviderTest {
assertThat(value.getFileValue()).isEqualTo("8080");
assertThat(value.getEffectiveValue()).isEqualTo("9090");
assertThat(value.getPropertySource()).isEqualTo("commandLineArgs");
// not a polaris config file: no coordinate, so the SDK falls back to its conservative path
assertThat(value.getSourceFile()).isNull();
}
@Test
@ -140,6 +142,11 @@ class SpringConfigEffectiveValueProviderTest {
assertThat(value.getEffectiveValue()).isEqualTo("8081");
// still sourced from the polaris file itself: normalized coordinate
assertThat(value.getPropertySource()).isEqualTo("polaris:default/order-service/application.yaml");
// the structured coordinate lets the SDK look up that file's encryption state
assertThat(value.getSourceFile()).isNotNull();
assertThat(value.getSourceFile().getNamespace()).isEqualTo(NAMESPACE);
assertThat(value.getSourceFile().getFileGroup()).isEqualTo(GROUP);
assertThat(value.getSourceFile().getFileName()).isEqualTo(FILE_NAME);
}
@Test
@ -199,6 +206,10 @@ class SpringConfigEffectiveValueProviderTest {
assertThat(value.getEffectiveValue()).isEqualTo("1");
// property source points to the winning sub file, not the opaque group source name
assertThat(value.getPropertySource()).isEqualTo("polaris:default/mygroup/a.yaml");
// coordinate follows the same winning sub file, so encryption is judged per sub file
assertThat(value.getSourceFile()).isNotNull();
assertThat(value.getSourceFile().getFileName()).isEqualTo("a.yaml");
assertThat(value.getSourceFile().getFileGroup()).isEqualTo("mygroup");
// conflicts are sub-file grained: b is excluded, a is reported
List<ConfigKeyConflict> conflicts = provider.resolveConflicts("k", metadata(NAMESPACE, "mygroup", "b.yaml"));

Loading…
Cancel
Save