Restrict wildcard pattern support for configuration files

This commit restricts how wildcards can be used in search
locations for property files. If a search location contains
a pattern, there must be only one '*' and the location should
end with a '*/'. For search locations that specify the file
name, the pattern should end with '*/<filename>'.

The list of files read from wildcard locations are now sorted
alphabetically according to the absolute path of the file.

Closes gh-21217
pull/21283/head
Madhura Bhave 5 years ago
parent 080123ebeb
commit 8ec16bd027

@ -499,7 +499,8 @@ For example, if you have some Redis configuration and some MySQL configuration,
This might result in two separate `application.properties` files mounted at different locations such as `/config/redis/application.properties` and `/config/mysql/application.properties`. This might result in two separate `application.properties` files mounted at different locations such as `/config/redis/application.properties` and `/config/mysql/application.properties`.
In such a case, having a wildcard location of `config/*/`, will result in both files being processed. In such a case, having a wildcard location of `config/*/`, will result in both files being processed.
NOTE: Locations with wildcards are not processed in a deterministic order and files that match the wildcard cannot be used to override keys in the other. NOTE: A wildcard location must contain only one `*` and end with `*/` for search locations that are directories or `*/<filename>` for search locations that are files.
Locations with wildcards are sorted alphabetically based on the absolute path of the file names.
[[boot-features-external-config-application-json]] [[boot-features-external-config-application-json]]

@ -16,10 +16,12 @@
package org.springframework.boot.context.config; package org.springframework.boot.context.config;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.Deque; import java.util.Deque;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -28,9 +30,12 @@ import java.util.LinkedHashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@ -60,9 +65,9 @@ import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.Profiles; import org.springframework.core.env.Profiles;
import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySource;
import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -163,6 +168,8 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
private static final Resource[] EMPTY_RESOURCES = {}; private static final Resource[] EMPTY_RESOURCES = {};
private static final Comparator<File> FILE_COMPARATOR = Comparator.comparing(File::getAbsolutePath);
private String searchLocations; private String searchLocations;
private String names; private String names;
@ -304,8 +311,6 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
private final ResourceLoader resourceLoader; private final ResourceLoader resourceLoader;
private final PathMatchingResourcePatternResolver patternResolver;
private final List<PropertySourceLoader> propertySourceLoaders; private final List<PropertySourceLoader> propertySourceLoaders;
private Deque<Profile> profiles; private Deque<Profile> profiles;
@ -325,7 +330,6 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
: new DefaultResourceLoader(getClass().getClassLoader()); : new DefaultResourceLoader(getClass().getClassLoader());
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader()); getClass().getClassLoader());
this.patternResolver = new PathMatchingResourcePatternResolver(this.resourceLoader);
} }
void load() { void load() {
@ -555,9 +559,22 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
private Resource[] getResources(String location) { private Resource[] getResources(String location) {
try { try {
return this.patternResolver.getResources(location); if (location.contains("*")) {
String directoryPath = location.substring(0, location.indexOf("*/"));
String fileName = location.substring(location.lastIndexOf("/") + 1);
Resource resource = this.resourceLoader.getResource(directoryPath);
File[] files = resource.getFile().listFiles(File::isDirectory);
if (files != null) {
Arrays.sort(files, FILE_COMPARATOR);
return Arrays.stream(files).map((file) -> file.listFiles((dir, name) -> name.equals(fileName)))
.filter(Objects::nonNull).flatMap((Function<File[], Stream<File>>) Arrays::stream)
.map(FileSystemResource::new).toArray(Resource[]::new);
} }
catch (IOException ex) { return EMPTY_RESOURCES;
}
return new Resource[] { this.resourceLoader.getResource(location) };
}
catch (Exception ex) {
return EMPTY_RESOURCES; return EMPTY_RESOURCES;
} }
} }
@ -658,7 +675,8 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
if (!path.contains("$")) { if (!path.contains("$")) {
path = StringUtils.cleanPath(path); path = StringUtils.cleanPath(path);
Assert.state(!path.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX), Assert.state(!path.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX),
"Classpath wildard patterns cannot be used as a search location"); "Classpath wildcard patterns cannot be used as a search location");
validateWildcardLocation(path);
if (!ResourceUtils.isUrl(path)) { if (!ResourceUtils.isUrl(path)) {
path = ResourceUtils.FILE_URL_PREFIX + path; path = ResourceUtils.FILE_URL_PREFIX + path;
} }
@ -669,6 +687,15 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
return locations; return locations;
} }
private void validateWildcardLocation(String path) {
if (path.contains("*")) {
Assert.state(StringUtils.countOccurrencesOf(path, "*") == 1,
"Wildard pattern with multiple '*'s cannot be used as search location");
String directoryPath = path.substring(0, path.lastIndexOf("/") + 1);
Assert.state(directoryPath.endsWith("*/"), "Wildcard patterns must end with '*/'");
}
}
private Set<String> getSearchNames() { private Set<String> getSearchNames() {
if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) { if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
String property = this.environment.getProperty(CONFIG_NAME_PROPERTY); String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);

@ -716,7 +716,7 @@ class ConfigFileApplicationListenerTests {
"spring.config.location=classpath*:override.properties"); "spring.config.location=classpath*:override.properties");
assertThatIllegalStateException() assertThatIllegalStateException()
.isThrownBy(() -> this.initializer.postProcessEnvironment(this.environment, this.application)) .isThrownBy(() -> this.initializer.postProcessEnvironment(this.environment, this.application))
.withMessage("Classpath wildard patterns cannot be used as a search location"); .withMessage("Classpath wildcard patterns cannot be used as a search location");
} }
@Test @Test
@ -1032,6 +1032,36 @@ class ConfigFileApplicationListenerTests {
this.initializer.postProcessEnvironment(this.environment, this.application); this.initializer.postProcessEnvironment(this.environment, this.application);
} }
@Test
void directoryLocationsWithWildcardShouldHaveWildcardAsLastCharacterBeforeSlash() {
String location = "file:src/test/resources/*/config/";
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
"spring.config.location=" + location);
assertThatIllegalStateException()
.isThrownBy(() -> this.initializer.postProcessEnvironment(this.environment, this.application))
.withMessage("Wildcard patterns must end with '*/'");
}
@Test
void directoryLocationsWithMultipleWildcardsShouldThrowException() {
String location = "file:src/test/resources/config/**/";
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
"spring.config.location=" + location);
assertThatIllegalStateException()
.isThrownBy(() -> this.initializer.postProcessEnvironment(this.environment, this.application))
.withMessage("Wildard pattern with multiple '*'s cannot be used as search location");
}
@Test
void locationsWithWildcardDirectoriesShouldRestrictToOneLevelDeep() {
String location = "file:src/test/resources/config/*/";
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
"spring.config.location=" + location);
this.initializer.setSearchNames("testproperties");
this.initializer.postProcessEnvironment(this.environment, this.application);
assertThat(this.environment.getProperty("third.property")).isNull();
}
@Test @Test
void locationsWithWildcardDirectoriesShouldLoadAllFilesThatMatch() { void locationsWithWildcardDirectoriesShouldLoadAllFilesThatMatch() {
String location = "file:src/test/resources/config/*/"; String location = "file:src/test/resources/config/*/";
@ -1045,6 +1075,22 @@ class ConfigFileApplicationListenerTests {
assertThat(second).isEqualTo("ball"); assertThat(second).isEqualTo("ball");
} }
@Test
void locationsWithWildcardDirectoriesShouldSortAlphabeticallyBasedOnAbsolutePath() {
String location = "file:src/test/resources/config/*/";
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
"spring.config.location=" + location);
this.initializer.setSearchNames("testproperties");
this.initializer.postProcessEnvironment(this.environment, this.application);
List<String> sources = this.environment.getPropertySources().stream()
.filter((source) -> source.getName().contains("applicationConfig")).map((source) -> {
String name = source.getName();
return name.substring(name.indexOf("src/test/resources"));
}).collect(Collectors.toList());
assertThat(sources).containsExactly("src/test/resources/config/1-first/testproperties.properties]]",
"src/test/resources/config/2-second/testproperties.properties]]");
}
@Test @Test
void locationsWithWildcardFilesShouldLoadAllFilesThatMatch() { void locationsWithWildcardFilesShouldLoadAllFilesThatMatch() {
String location = "file:src/test/resources/config/*/testproperties.properties"; String location = "file:src/test/resources/config/*/testproperties.properties";

Loading…
Cancel
Save