├── .gitignore ├── README.md ├── app ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── checketts │ │ │ ├── ConfigServerClientApplication.java │ │ │ └── cloud │ │ │ └── config │ │ │ └── ConfigClientBootstrapConfiguration.java │ └── resources │ │ ├── META-INF │ │ └── spring.factories │ │ ├── application.yml │ │ └── bootstrap.yml │ └── test │ └── java │ └── com │ └── github │ └── checketts │ └── ConfigServerExampleApplicationTests.java ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sample-config-repo ├── application-nightly1.yml ├── application.yml ├── build.gradle ├── configServerExample-nightly1.yml ├── configServerExample-notjson.yml ├── configServerExample.yml └── pre-commit.sh ├── server ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── checketts │ │ │ ├── ConfigServerExampleApplication.java │ │ │ ├── GreeterController.java │ │ │ ├── GreeterProperties.java │ │ │ └── config │ │ │ └── server │ │ │ ├── ConfigServerClient.java │ │ │ ├── DeviceConfig.java │ │ │ ├── EncryptProperties.java │ │ │ ├── FilteringEnvironmentController.java │ │ │ ├── JwtUtils.java │ │ │ ├── KeySanitizationUtil.java │ │ │ ├── LatestRefreshedRepositoryVersionHolder.java │ │ │ ├── RegexGroupExtractor.java │ │ │ └── SanitizeEnforcingEnvironmentEncryptor.java │ └── resources │ │ ├── application.yml │ │ └── bootstrap.yml │ └── test │ └── java │ └── com │ └── github │ └── checketts │ └── ConfigServerExampleApplicationTests.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | out/ 3 | build/ 4 | 5 | *.swp 6 | *.iml 7 | *.ipr 8 | *.iws 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # config-server-example 2 | 3 | Examples for extending and improving [Spring Cloud Config Server](http://cloud.spring.io/spring-cloud-config/spring-cloud-config.html) 4 | presented at [Spring IO Barcelona 2016](http://www.springio.net/). 5 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'spring-boot' 3 | 4 | springBoot { 5 | mainClass = 'com.github.checketts.cloud.config.ConfigServerClientApplication' 6 | } 7 | 8 | jar { 9 | baseName = 'config-server-example' 10 | version = '0.0.1-SNAPSHOT' 11 | } 12 | sourceCompatibility = 1.8 13 | targetCompatibility = 1.8 14 | 15 | repositories { 16 | mavenCentral() 17 | } 18 | 19 | 20 | dependencies { 21 | compile('org.springframework.boot:spring-boot-starter-actuator') 22 | compile('org.springframework.cloud:spring-cloud-starter-config') 23 | compile('org.springframework.boot:spring-boot-starter-security') 24 | compile('org.springframework.boot:spring-boot-starter-web') 25 | testCompile('org.springframework.boot:spring-boot-starter-test') 26 | } 27 | 28 | dependencyManagement { 29 | imports { 30 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:Brixton.RELEASE" 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/checketts/ConfigServerClientApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ConfigServerClientApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ConfigServerClientApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/checketts/cloud/config/ConfigClientBootstrapConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts.cloud.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 6 | import org.springframework.cloud.config.client.ConfigServicePropertySourceLocator; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.http.client.ClientHttpRequestInterceptor; 9 | import org.springframework.web.client.RestTemplate; 10 | 11 | // Needed at Bootstrap configuration time, see spring.factories 12 | @Configuration 13 | @ConditionalOnProperty(value = "spring.cloud.config.enabled") 14 | public class ConfigClientBootstrapConfiguration { 15 | 16 | @Autowired 17 | public void configureCloudConfigRestTemplate(@Value("${config.client.secret}") String configClientSecret, 18 | ConfigServicePropertySourceLocator locator) { 19 | RestTemplate template = new RestTemplate(); 20 | template.getInterceptors().add(authInterceptor(configClientSecret)); 21 | 22 | locator.setRestTemplate(template); 23 | } 24 | 25 | private ClientHttpRequestInterceptor authInterceptor(final String configClientSecret) { 26 | return (request, body, execution) -> { 27 | request.getHeaders().add("Authorization", "Bearer " + configClientSecret); 28 | return execution.execute(request, body); 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.cloud.bootstrap.BootstrapConfiguration=\ 2 | com.github.checketts.cloud.config.ConfigClientBootstrapConfiguration -------------------------------------------------------------------------------- /app/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | security: 2 | basic: 3 | enabled: false 4 | management: 5 | security: 6 | enabled: false 7 | 8 | server: 9 | port: 8081 -------------------------------------------------------------------------------- /app/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring.application.name: dataHub 2 | spring.cloud.config.enabled: true 3 | config.client.secret: fakeButAppeasesSpringInjection 4 | 5 | spring.cloud.config.uri: http://localhost:8080/config -------------------------------------------------------------------------------- /app/src/test/java/com/github/checketts/ConfigServerExampleApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.SpringApplicationConfiguration; 6 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 7 | import org.springframework.test.context.web.WebAppConfiguration; 8 | 9 | @RunWith(SpringJUnit4ClassRunner.class) 10 | @SpringApplicationConfiguration(classes = ConfigServerClientApplication.class) 11 | @WebAppConfiguration 12 | public class ConfigServerExampleApplicationTests { 13 | 14 | @Test 15 | public void contextLoads() { 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '1.3.5.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | def workaroundIdeaBug = { theXml, resourcesParentDir, resourcesType -> 14 | def resourcesElements = theXml.asNode().component.content.sourceFolder.findAll { 15 | it.@url == "file://\$MODULE_DIR\$/src/${resourcesParentDir}/resources" 16 | } 17 | if (resourcesElements) { 18 | resourcesElements.each { resourcesElement -> 19 | def resourcesAttributes = resourcesElement.attributes() 20 | resourcesAttributes.remove('isTestSource') 21 | resourcesAttributes.put('type', resourcesType) 22 | } 23 | } 24 | } 25 | 26 | apply plugin: 'eclipse' 27 | apply plugin: 'spring-boot' 28 | 29 | allprojects { 30 | apply plugin: 'java' 31 | apply plugin: 'idea' 32 | 33 | repositories { 34 | mavenCentral() 35 | } 36 | 37 | dependencyManagement { 38 | imports { 39 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:Brixton.RELEASE" 40 | } 41 | } 42 | 43 | idea { 44 | module.iml.withXml { 45 | workaroundIdeaBug(it, 'main', 'java-resource') 46 | workaroundIdeaBug(it, 'test', 'java-test-resource') 47 | } 48 | } 49 | 50 | } 51 | 52 | def setRunConfig = { xmlFile, name, module, mainClass -> 53 | def runManager = xmlFile.asNode().component.find { it.@name == 'RunManager' } 54 | 55 | def runConfig = runManager.configuration.find { 56 | it.@name == 'config server (Gradle Generated)' && it.@type == 'SpringBootApplicationConfigurationType' 57 | } 58 | if (runConfig != null) { 59 | runConfig.parent().remove(runConfig) 60 | } 61 | def conf = runManager.appendNode 'configuration', [default: 'false', name: name, 62 | type : 'SpringBootApplicationConfigurationType', factoryName: 'Spring Boot'] 63 | conf.appendNode 'extension', [name: "coverage", enabled: "false", merge: "false", sample_coverage: "true", runner: "idea"] 64 | conf.appendNode 'option', [name: "SPRING_BOOT_MAIN_CLASS", value: mainClass] 65 | conf.appendNode 'option', [name: "VM_PARAMETERS", value: ""] 66 | conf.appendNode 'option', [name: "WORKING_DIRECTORY", value: 'file://$MODULE_DIR$'] 67 | conf.appendNode 'option', [name: "ALTERNATIVE_JRE_PATH"] 68 | conf.appendNode 'option', [name: "ACTIVE_PROFILES", value: "local"] 69 | conf.appendNode 'module', [name: module] 70 | conf.appendNode 'envs', [] 71 | conf.appendNode 'method', [] 72 | } 73 | 74 | idea { 75 | project { 76 | jdkName = '1.8' 77 | languageLevel = '1.8' 78 | } 79 | 80 | workspace.iws.withXml { xmlFile -> 81 | setRunConfig(xmlFile, "configServer (Gradle Generated)", "server", "com.github.checketts.ConfigServerExampleApplication") 82 | setRunConfig(xmlFile, "configClient (Gradle Generated)", "app", "com.github.checketts.ConfigServerClientApplication") 83 | } 84 | 85 | } 86 | 87 | 88 | 89 | eclipse { 90 | classpath { 91 | containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER') 92 | containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8' 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/checketts/config-server-example/b7f2b4907921d21c31c5be8916e2f9b0b454aee5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /sample-config-repo/application-nightly1.yml: -------------------------------------------------------------------------------- 1 | deviceDefinitions: 2 | MYSQL: 3 | defaults: 4 | connectionTimeout: 10000 5 | driver: com.mysql.jdbc.Driver 6 | maxConnectionLifetime: 300000 7 | maxPoolSize: 10 8 | subtypes: 9 | USERS_DB: 10 | defaults: 11 | username: bobthefish 12 | devices: 13 | - id: userdb 14 | password: '{cipher}{key:nightly1_v1}2293845729384f9fe938e398e3987' 15 | url: jdbc:mysql://some.server.com:3306 16 | username: frankthesquid 17 | - id: secretUserdb2 18 | password: '{cipher}{key:nightly1_v1}ace87ac9e8cae9ca0ac9e8c798c7' 19 | url: jdbc:mysql://other.server.com:3306 20 | username: brucetheshark 21 | CACHE: 22 | devices: 23 | - id: userCache 24 | password: '{cipher}{key:nightly1_v1}773e94397d5873f9557663fe4c199ee' 25 | url: something:1234 26 | username: myusername -------------------------------------------------------------------------------- /sample-config-repo/application.yml: -------------------------------------------------------------------------------- 1 | endpoints: 2 | env.keys-to-sanitize: "password,\ 3 | secret,\ 4 | key,\ 5 | .*credentials.*,\ 6 | vcap_services,\ 7 | .*\\.keys\\..*,\ 8 | .*\\.secrets.*,\ 9 | pass,\ 10 | salt,\ 11 | token,\ 12 | deviceDefinitions.*user,\ 13 | deviceDefinitions.*username" 14 | # configprops disabled because sanitization does not seem to work on prefixes 15 | configprops.enabled: false 16 | 17 | sample.key: Sample Value -------------------------------------------------------------------------------- /sample-config-repo/build.gradle: -------------------------------------------------------------------------------- 1 | import org.yaml.snakeyaml.Yaml 2 | import org.yaml.snakeyaml.constructor.Constructor 3 | import org.yaml.snakeyaml.nodes.MappingNode 4 | import org.yaml.snakeyaml.parser.ParserException 5 | 6 | //Yaml constructor to help detect duplicate keys. Copied from org.springframework.beans.factory.config.YamlProcessor.StrictMapAppenderConstructor 7 | class StrictMapAppenderConstructor extends Constructor { 8 | @Override 9 | protected Map constructMapping(MappingNode node) { 10 | try { 11 | return super.constructMapping(node); 12 | } 13 | catch (IllegalStateException ex) { 14 | throw new ParserException("while parsing MappingNode", 15 | node.getStartMark(), ex.getMessage(), node.getEndMark()); 16 | } 17 | } 18 | 19 | @Override 20 | protected Map createDefaultMap() { 21 | final Map delegate = super.createDefaultMap(); 22 | return new AbstractMap() { 23 | @Override 24 | public Object put(Object key, Object value) { 25 | if (delegate.containsKey(key)) { 26 | throw new IllegalStateException("Duplicate key: " + key); 27 | } 28 | return delegate.put(key, value); 29 | } 30 | 31 | @Override 32 | public Set> entrySet() { 33 | return delegate.entrySet(); 34 | } 35 | }; 36 | } 37 | } 38 | 39 | 40 | buildscript { 41 | repositories { 42 | mavenCentral() 43 | } 44 | 45 | dependencies { 46 | classpath group: 'org.yaml', name: 'snakeyaml', version: '1.12' 47 | } 48 | } 49 | apply plugin: 'idea' 50 | 51 | task validateYaml << { 52 | def collection = fileTree(dir: '.', includes: ['**/*.yml/**']) 53 | 54 | Yaml yaml = new Yaml(new StrictMapAppenderConstructor()); 55 | 56 | def errors = [] 57 | 58 | collection.each { File file -> 59 | try { 60 | def config = yaml.load(new FileReader(file)) 61 | logger.info "Parsed " + file 62 | } catch (Exception e) { 63 | errors << "Error parsing '" + file + "': " + e.getProblem() + e.getProblemMark() 64 | logger.error("YAML Parsing error. file=" + file, e) 65 | } 66 | } 67 | 68 | if (!errors.isEmpty()) { 69 | throw new RuntimeException("Error:" + errors) 70 | } 71 | } 72 | 73 | //install the commit hook if possible, will execute on every gradle evaluation 74 | def hook = new File('pre-commit.sh') 75 | def hook_folder = new File('./.git/hooks') 76 | def installed_hook = new File('./.git/hooks/pre-commit') 77 | 78 | if (hook.exists() && hook_folder.exists() && !installed_hook.exists()) { 79 | println("Installing pre-commit hook") 80 | exec { //use exec to preserve file permissions 81 | workingDir '.' 82 | commandLine 'cp', hook, installed_hook 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /sample-config-repo/configServerExample-nightly1.yml: -------------------------------------------------------------------------------- 1 | devices: 2 | - userdb 3 | - userCache -------------------------------------------------------------------------------- /sample-config-repo/configServerExample-notjson.yml: -------------------------------------------------------------------------------- 1 | { 2 | "someKey" : '{cipher}5d71d53ed0ed9341303cff5ab71ec4143e9f3e4a35ad2892668a2cea21fcf13e', 3 | "greeter": { 4 | "name": "Clinton", 5 | "from": ["Red", "Yellow", "Blue"] 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /sample-config-repo/configServerExample.yml: -------------------------------------------------------------------------------- 1 | 2 | someKey: '{cipher}5d71d53ed0ed9341303cff5ab71ec4143e9f3e4a35ad2892668a2cea21fcf13e' 3 | 4 | greeter: 5 | name: Clint 6 | from: 7 | - First 8 | - Second 9 | - Third 10 | 11 | 12 | -------------------------------------------------------------------------------- /sample-config-repo/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # this hook is in SCM so that it can be shared 3 | # to install it, create a symbolic link in the projects .git/hooks folder 4 | # 5 | # i.e. - from the .git/hooks directory, run 6 | # $ ln -s ../../git-hooks/pre-commit.sh pre-commit 7 | # 8 | # to skip the tests, run with the --no-verify argument 9 | # i.e. - $ 'git commit --no-verify' 10 | 11 | # stash any unstaged changes 12 | git stash -q --keep-index 13 | 14 | # run the tests with the gradle wrapper 15 | ./gradlew validateYaml 16 | 17 | # store the last exit code in a variable 18 | RESULT=$? 19 | 20 | # unstash the unstashed changes 21 | git stash pop -q 22 | 23 | # return the './gradlew test' exit code 24 | exit $RESULT -------------------------------------------------------------------------------- /server/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'spring-boot' 3 | 4 | springBoot { 5 | mainClass = 'com.github.checketts.ConfigServerExampleApplication' 6 | } 7 | 8 | jar { 9 | baseName = 'config-server-example' 10 | version = '0.0.1-SNAPSHOT' 11 | } 12 | sourceCompatibility = 1.8 13 | targetCompatibility = 1.8 14 | 15 | repositories { 16 | mavenCentral() 17 | } 18 | 19 | 20 | dependencies { 21 | compile("com.nimbusds:nimbus-jose-jwt:4.3.1") 22 | 23 | compile('org.springframework.boot:spring-boot-starter-actuator') 24 | compile('org.springframework.cloud:spring-cloud-starter-config') 25 | compile('org.springframework.cloud:spring-cloud-config-server') 26 | compile('org.springframework.boot:spring-boot-starter-security') 27 | compile('org.springframework.boot:spring-boot-starter-web') 28 | testCompile('org.springframework.boot:spring-boot-starter-test') 29 | } 30 | 31 | dependencyManagement { 32 | imports { 33 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:Brixton.RELEASE" 34 | } 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /server/src/main/java/com/github/checketts/ConfigServerExampleApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 6 | import org.springframework.cloud.config.server.EnableConfigServer; 7 | 8 | import java.io.File; 9 | 10 | @SpringBootApplication 11 | @EnableConfigServer 12 | @EnableConfigurationProperties 13 | public class ConfigServerExampleApplication { 14 | 15 | public static void main(String[] args) { 16 | 17 | //Create a cross platform consistent variable to reference the parent directory 18 | System.setProperty("user.parent.dir", new File(System.getProperty("user.dir")).getParentFile().getAbsolutePath()); 19 | 20 | SpringApplication.run(ConfigServerExampleApplication.class, args); 21 | } 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /server/src/main/java/com/github/checketts/GreeterController.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | import java.util.stream.Collectors; 8 | 9 | @RestController 10 | public class GreeterController { 11 | 12 | private final GreeterProperties greeterProperties; 13 | 14 | @Autowired 15 | public GreeterController(GreeterProperties greeterProperties) { 16 | this.greeterProperties = greeterProperties; 17 | } 18 | 19 | @RequestMapping("/") 20 | public String root() { 21 | return "Hello " + greeterProperties.getName() + " - from " + greeterProperties.getFrom().stream().collect(Collectors.joining(", ")); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/src/main/java/com/github/checketts/GreeterProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | @Component 10 | @ConfigurationProperties("greeter") 11 | public class GreeterProperties { 12 | private String name; 13 | private List from = new ArrayList<>(); 14 | 15 | public GreeterProperties() { 16 | name = "Spencer"; 17 | } 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | 23 | public void setName(String name) { 24 | this.name = name; 25 | } 26 | 27 | public List getFrom() { 28 | return from; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/src/main/java/com/github/checketts/config/server/ConfigServerClient.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts.config.server; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.cloud.bootstrap.config.PropertySourceLocator; 7 | import org.springframework.cloud.config.client.ConfigClientProperties; 8 | import org.springframework.cloud.config.server.encryption.EncryptionController; 9 | import org.springframework.core.env.CompositePropertySource; 10 | import org.springframework.core.env.ConfigurableEnvironment; 11 | import org.springframework.core.env.EnumerablePropertySource; 12 | import org.springframework.core.env.MapPropertySource; 13 | import org.springframework.core.env.StandardEnvironment; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.stereotype.Component; 16 | 17 | import java.util.HashMap; 18 | import java.util.Optional; 19 | 20 | /** 21 | * Encapsulates details of interacting with Spring Cloud Config server endpoints, to avoid dependencies on server classes. 22 | * Currently simply delegates to Spring Cloud Config server classes because it is embedded, 23 | * but when it is no longer embedded then this class should be the only bean to change (probably to use RestTemplate). 24 | */ 25 | @Component 26 | public class ConfigServerClient { 27 | 28 | public static final String DEVICES_PREFIX = "devices_"; 29 | private static final Logger LOG = LoggerFactory.getLogger(ConfigServerClient.class); 30 | 31 | private final EncryptionController encryptionController; 32 | private final EncryptProperties encryptProperties; 33 | 34 | private PropertySourceLocator propertySourceLocator; 35 | 36 | @Autowired 37 | public ConfigServerClient(EncryptionController encryptionController, 38 | // ConfigServicePropertySourceLocator propertySourceLocator, 39 | EncryptProperties encryptProperties) { 40 | this.encryptionController = encryptionController; 41 | this.propertySourceLocator = propertySourceLocator; 42 | this.encryptProperties = encryptProperties; 43 | } 44 | 45 | public String encrypt(String toEncrypt) { 46 | return encryptionController.encrypt(toEncrypt, MediaType.TEXT_PLAIN); 47 | } 48 | 49 | public String decrypt(String toDecrypt) { 50 | return encryptionController.decrypt(toDecrypt, MediaType.TEXT_PLAIN); 51 | } 52 | 53 | /** 54 | * Gets the property source in the same way Spring Cloud Config client code does for the current app, except the 55 | * caller chooses the app, profile, and label. Also the result is unrelated to the current app's PropertySources. 56 | *

57 | * If {applicationName} is the default ("application"), an extra property source is requested for a 58 | * profile of "devices_{environmentId}". 59 | * 60 | * @see org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration 61 | * @see org.springframework.cloud.config.client.ConfigServicePropertySourceLocator 62 | */ 63 | public EnumerablePropertySource getPropertySource(String applicationName, String environmentId) { 64 | return getPropertySource(applicationName, environmentId, "master"); 65 | } 66 | 67 | public EnumerablePropertySource getPropertySource(String applicationName, String environmentId, String branch) { 68 | EnumerablePropertySource result = newPropertySource(applicationName, environmentId, branch); 69 | if ("application".equals(applicationName)) { 70 | String profile = DEVICES_PREFIX + environmentId; 71 | EnumerablePropertySource deviceSource = newPropertySource(applicationName, profile, branch); 72 | CompositePropertySource deviceSourceWithUniqueName = new CompositePropertySource("devicesSource"); 73 | deviceSourceWithUniqueName.addPropertySource(deviceSource); 74 | 75 | CompositePropertySource composite = new CompositePropertySource("envPlusDevices"); 76 | composite.addPropertySource(result); 77 | composite.addPropertySource(deviceSourceWithUniqueName); 78 | result = composite; 79 | } 80 | return result; 81 | } 82 | 83 | private EnumerablePropertySource newPropertySource(String applicationName, String environmentId, String branch) { 84 | throw new UnsupportedOperationException("Property source Locator is not wired up properly, check constructor"); 85 | // ConfigurableEnvironment environment = newEnvironment(applicationName, environmentId, branch); 86 | // 87 | // PropertySource source = propertySourceLocator.locate(environment); 88 | // LOG.info("getPropertySource(). env={}, source={}", environment, source); 89 | // if (null == source) { 90 | // source = new MapPropertySource("emptyPropertySource", Collections.emptyMap()); 91 | // } 92 | // return (EnumerablePropertySource)source; 93 | } 94 | 95 | private ConfigurableEnvironment newEnvironment(String applicationName, String environmentId, String branch) { 96 | HashMap map = new HashMap<>(); 97 | map.put(ConfigClientProperties.PREFIX + ".name", applicationName); 98 | map.put(ConfigClientProperties.PREFIX + ".profile", environmentId); 99 | map.put(ConfigClientProperties.PREFIX + ".label", branch); 100 | 101 | ConfigurableEnvironment environment = new StandardEnvironment(); 102 | environment.getPropertySources().addFirst(new MapPropertySource("profiles", map)); 103 | return environment; 104 | 105 | } 106 | 107 | public String toEncryptedValue(String value, Optional environmentId, Optional serviceOrDeviceId) { 108 | String keyAlias = toKeyAlias(environmentId, serviceOrDeviceId); 109 | if (!encryptProperties.getKeys().containsKey(keyAlias)) { 110 | keyAlias = toKeyAlias(environmentId, Optional.empty()); 111 | if (!encryptProperties.getKeys().containsKey(keyAlias)) { 112 | throw new IllegalStateException("Encryption key not found for environment=" + environmentId + ", svc/dvc=" + serviceOrDeviceId); 113 | } 114 | } 115 | return "'" + toEncryptedValue(Optional.of(keyAlias), value) + "'"; 116 | } 117 | 118 | public String toEncryptedValue(Optional keyAlias, String value) { 119 | String aliasPrefixedValue = keyAlias.isPresent() ? String.format("{key:%s}%s", keyAlias.get(), value) : value; 120 | return "{cipher}" + encrypt(aliasPrefixedValue) + ""; 121 | } 122 | 123 | 124 | private String toKeyAlias(Optional environmentId, Optional serviceOrDeviceId) { 125 | if (environmentId.isPresent()) { 126 | if (serviceOrDeviceId.isPresent()) { 127 | return environmentId.get() + "_" + serviceOrDeviceId.get() + "_v1"; 128 | } 129 | return environmentId.get() + "_v1"; 130 | } 131 | throw new IllegalStateException("environmentId expected for secure properties"); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /server/src/main/java/com/github/checketts/config/server/DeviceConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts.config.server; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public class DeviceConfig { 9 | private Map deviceDefinitions = new HashMap<>(); 10 | 11 | public Map getDeviceDefinitions() { 12 | return deviceDefinitions; 13 | } 14 | 15 | public void setDeviceDefinitions(Map deviceDefinitions) { 16 | this.deviceDefinitions = deviceDefinitions; 17 | } 18 | 19 | 20 | public static class DevicesWithDefaults { 21 | 22 | private Map defaults = new HashMap<>(); 23 | private Map subtypes = new HashMap<>(); 24 | 25 | private List> devices = new ArrayList<>(); 26 | 27 | public Map getDefaults() { 28 | return defaults; 29 | } 30 | 31 | public void setDefaults(Map defaults) { 32 | this.defaults = defaults; 33 | } 34 | 35 | public List> getDevices() { 36 | return devices; 37 | } 38 | 39 | public void setDevices(List> devices) { 40 | this.devices = devices; 41 | } 42 | 43 | public Map getSubtypes() { 44 | return subtypes; 45 | } 46 | 47 | public void setSubtypes(Map subtypes) { 48 | this.subtypes = subtypes; 49 | } 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /server/src/main/java/com/github/checketts/config/server/EncryptProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts.config.server; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.cloud.context.config.annotation.RefreshScope; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.Map; 8 | 9 | @ConfigurationProperties("config.server.encrypt") 10 | @Component 11 | @RefreshScope 12 | public class EncryptProperties { 13 | /** 14 | * Symmetric keys by alias. As a stronger alternative consider using a keystore. 15 | */ 16 | private Map keys; 17 | 18 | public Map getKeys() { 19 | return keys; 20 | } 21 | 22 | public void setKeys(Map keys) { 23 | this.keys = keys; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /server/src/main/java/com/github/checketts/config/server/FilteringEnvironmentController.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts.config.server; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.cloud.config.environment.Environment; 7 | import org.springframework.cloud.config.environment.PropertySource; 8 | import org.springframework.cloud.config.server.config.ConfigServerProperties; 9 | import org.springframework.cloud.config.server.encryption.EnvironmentEncryptor; 10 | import org.springframework.cloud.config.server.environment.EnvironmentController; 11 | import org.springframework.cloud.config.server.environment.EnvironmentEncryptorEnvironmentRepository; 12 | import org.springframework.cloud.config.server.environment.EnvironmentRepository; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | import org.springframework.web.bind.annotation.RequestMethod; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | import java.util.ArrayList; 19 | import java.util.Collections; 20 | import java.util.Iterator; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.regex.Matcher; 24 | import java.util.regex.Pattern; 25 | 26 | /** 27 | * Created by clintchecketts on 10/20/15. 28 | */ 29 | @RestController 30 | @RequestMapping(method = RequestMethod.GET, value = "/config") 31 | public class FilteringEnvironmentController extends EnvironmentController { 32 | 33 | private static final Logger LOG = LoggerFactory.getLogger(FilteringEnvironmentController.class); 34 | private static final Pattern devicePattern = Pattern.compile("(deviceDefinitions.[\\w\\.]+).devices\\[\\d*\\]"); 35 | private static final Pattern superPattern = Pattern.compile("(deviceDefinitions.[\\w\\.]+).subtypes\\.[\\w_]+"); 36 | 37 | @Autowired 38 | public FilteringEnvironmentController(EnvironmentRepository repository, 39 | EnvironmentEncryptor environmentEncryptor, 40 | ConfigServerProperties configServerProperties) { 41 | super(encrypted(repository, environmentEncryptor, configServerProperties)); 42 | } 43 | 44 | private static EnvironmentEncryptorEnvironmentRepository encrypted(EnvironmentRepository repository, 45 | EnvironmentEncryptor environmentEncryptor, 46 | ConfigServerProperties configServerProperties) { 47 | EnvironmentEncryptorEnvironmentRepository encrypted = 48 | new EnvironmentEncryptorEnvironmentRepository(repository, environmentEncryptor); 49 | encrypted.setOverrides(configServerProperties.getOverrides()); 50 | return encrypted; 51 | } 52 | 53 | //@VisibleForTesting 54 | static boolean isRequiredDefault(String key, List deviceTypes) { 55 | for (String deviceType : deviceTypes) { 56 | if (key.startsWith(deviceType + ".defaults")) { 57 | return true; 58 | } 59 | } 60 | return false; 61 | } 62 | 63 | private static String getTypeFromDevice(String deviceString) { 64 | Matcher typeMatcher = devicePattern.matcher(deviceString); 65 | if (typeMatcher.matches()) { 66 | return typeMatcher.group(1); 67 | } else { 68 | return null; 69 | } 70 | } 71 | 72 | //@VisibleForTesting 73 | static List getDeviceTypeAndSuperTypes(String prefix) { 74 | List deviceTypes = new ArrayList<>(); 75 | String device = getTypeFromDevice(prefix); 76 | if (device == null) { 77 | LOG.warn("Invalid device property. Device Properties should end with devices[\\d] and start with deviceDefinitions. was [{}]", prefix); 78 | return deviceTypes; 79 | } 80 | //add the device itself 81 | deviceTypes.add(device); 82 | 83 | //start looking for super types 84 | String superType = device; 85 | while (superType.length() > 0) { 86 | Matcher superMatcher = superPattern.matcher(superType); 87 | if (superMatcher.matches()) { 88 | superType = superMatcher.group(1); 89 | deviceTypes.add(superType); 90 | } else { 91 | //done finding devices 92 | break; 93 | } 94 | } 95 | return deviceTypes; 96 | } 97 | 98 | @Override 99 | public Environment labelled(@PathVariable String name, @PathVariable String profiles, 100 | @PathVariable String label) { 101 | Environment env = super.labelled(name, profiles, label); 102 | 103 | addEnvironmentRepoMetadata(env); 104 | 105 | List serviceDevices = findAssignedDevices(env); 106 | if (!"application".equals(name)) { 107 | removeUnassignedDeviceDefinitions(env, serviceDevices); 108 | } 109 | return env; 110 | } 111 | 112 | private void addEnvironmentRepoMetadata(Environment env) { 113 | String version = null == env.getVersion() ? "unknown" : env.getVersion(); 114 | env.addFirst(new PropertySource("environment-repository-metadata", Collections.singletonMap( 115 | "cloud.config.environment.repository.version", version))); 116 | } 117 | 118 | private void removeUnassignedDeviceDefinitions(Environment env, List serviceDevices) { 119 | List deviceDefinitons = new ArrayList<>(); 120 | List assignedServiceDevicePrefixes = new ArrayList<>(); 121 | List unassignedDevices = new ArrayList<>(); 122 | 123 | env.getPropertySources().stream().forEach(propSource -> { 124 | Iterator> it = propSource.getSource().entrySet().iterator(); 125 | while (it.hasNext()) { 126 | Map.Entry entry = it.next(); 127 | String key = (String) entry.getKey(); 128 | if (key.startsWith("deviceDefinitions")) { 129 | it.remove(); 130 | deviceDefinitons.add(new DeviceDefinitionPart(key, entry.getValue(), propSource)); 131 | if (key.endsWith(".id")) { 132 | if (serviceDevices.contains(entry.getValue())) { 133 | assignedServiceDevicePrefixes.add(key.replace(".id", "")); 134 | } else { 135 | unassignedDevices.add(entry.getValue().toString()); 136 | } 137 | } 138 | } 139 | } 140 | }); 141 | 142 | LOG.debug("Removing properties for devices not assigned to service. allowed={}, removed={}", serviceDevices, 143 | unassignedDevices); 144 | 145 | assignedServiceDevicePrefixes.stream().forEach(prefix -> { 146 | List deviceTypes = getDeviceTypeAndSuperTypes(prefix); 147 | deviceDefinitons.stream() 148 | .filter(def -> def.key.startsWith(prefix) || isRequiredDefault(def.key, deviceTypes)) 149 | .forEach(def -> { 150 | Map sourceMap = (Map) def.source.getSource(); 151 | sourceMap.put(def.key, def.value); 152 | }); 153 | }); 154 | } 155 | 156 | private List findAssignedDevices(Environment env) { 157 | List devices = new ArrayList(); 158 | 159 | env.getPropertySources().stream().forEach(propSource -> { 160 | propSource.getSource().entrySet().stream() 161 | .filter(entry -> ((String) entry.getKey()).startsWith("devices[")) 162 | .forEach(entry -> devices.add((String) entry.getValue())); 163 | }); 164 | 165 | return devices; 166 | } 167 | 168 | private static class DeviceDefinitionPart { 169 | String key; 170 | Object value; 171 | PropertySource source; 172 | 173 | public DeviceDefinitionPart(String key, Object value, PropertySource source) { 174 | this.key = key; 175 | this.value = value; 176 | this.source = source; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /server/src/main/java/com/github/checketts/config/server/JwtUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts.config.server; 2 | 3 | import com.nimbusds.jose.JOSEException; 4 | import com.nimbusds.jose.JWSAlgorithm; 5 | import com.nimbusds.jose.JWSHeader; 6 | import com.nimbusds.jose.crypto.MACSigner; 7 | import com.nimbusds.jose.crypto.MACVerifier; 8 | import com.nimbusds.jwt.JWTClaimsSet; 9 | import com.nimbusds.jwt.SignedJWT; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.stereotype.Component; 15 | 16 | import java.security.SecureRandom; 17 | import java.sql.Date; 18 | import java.text.ParseException; 19 | import java.time.Instant; 20 | import java.time.temporal.ChronoUnit; 21 | import java.util.List; 22 | import java.util.stream.Collectors; 23 | 24 | @Component 25 | public class JwtUtils { 26 | 27 | public static final String APP_ENVIRONMENT = "appEnvironment"; 28 | public static final String APP_SERVICE = "appService"; 29 | public static final String APP_AUTHORITIES = "appAuthorities"; 30 | private final static Logger LOG = LoggerFactory.getLogger(JwtUtils.class); 31 | private final List serviceAuthVerifiers; 32 | private SecureRandom secureRandom; 33 | 34 | @Autowired 35 | public JwtUtils(@Value("#{'${config.server.authentication.jwt.secrets}'.split(',')}") 36 | List appServiceAuthSecrets) { 37 | this.serviceAuthVerifiers = appServiceAuthSecrets.stream() 38 | .map(JwtUtils::newMacVerifier) 39 | .collect(Collectors.toList()); 40 | this.secureRandom = new SecureRandom(); 41 | } 42 | 43 | private static MACVerifier newMacVerifier(String v) { 44 | try { 45 | return new MACVerifier(v); 46 | } catch (JOSEException e) { 47 | throw new IllegalStateException(e); 48 | } 49 | } 50 | 51 | public SignedJWT appServiceJwt(String serviceId, String environmentId) { 52 | JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() 53 | .jwtID(String.valueOf(secureRandom.nextInt())) 54 | .claim(APP_SERVICE, serviceId) 55 | .claim(APP_ENVIRONMENT, environmentId) 56 | .build(); 57 | SignedJWT jwt = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet); 58 | sign(jwt); 59 | return jwt; 60 | } 61 | 62 | public SignedJWT adminJwt(String username) { 63 | return adminJwt(username, Instant.now().plus(1, ChronoUnit.HOURS)); 64 | } 65 | 66 | public SignedJWT adminJwt(String username, Instant instant) { 67 | JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() 68 | .jwtID(String.valueOf(secureRandom.nextInt())) 69 | .subject(username) 70 | .claim(APP_AUTHORITIES, "Admin") 71 | .expirationTime(Date.from(instant)) 72 | .build(); 73 | SignedJWT jwt = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet); 74 | sign(jwt); 75 | return jwt; 76 | } 77 | 78 | private void sign(SignedJWT jwt) { 79 | try { 80 | MACSigner signer = new MACSigner(serviceAuthVerifiers.get(0).getSecret()); 81 | jwt.sign(signer); 82 | } catch (JOSEException e) { 83 | String claimSet = ""; 84 | try { 85 | claimSet = jwt.getJWTClaimsSet().toString(); 86 | } catch (ParseException e1) { 87 | LOG.error("Unable to parse claimset", e1); 88 | } 89 | 90 | throw new IllegalStateException("Problem signing JWT token. claimSet=" + claimSet, e); 91 | } 92 | } 93 | 94 | public boolean isValidAppServiceJwt(SignedJWT jwt) throws ParseException { 95 | int secretCount = serviceAuthVerifiers.size(); 96 | for (int i = 0; i < secretCount; i++) { 97 | JWTClaimsSet claimSet = jwt.getJWTClaimsSet(); 98 | String serviceId = (String) claimSet.getClaim(APP_SERVICE); 99 | String environmentId = (String) claimSet.getClaim(APP_ENVIRONMENT); 100 | 101 | MACVerifier verifier = serviceAuthVerifiers.get(i); 102 | String secret = verifier.getSecretString(); 103 | String secretPrefix = secret.substring(0, Math.min(3, secret.length())); 104 | String msgSuffix = String.format("secret=[%s... (%s of %s)], service=%s, env=%s, jwtId=%s", 105 | secretPrefix, i + 1, secretCount, serviceId, environmentId, claimSet.getJWTID()); 106 | try { 107 | boolean verified = jwt.verify(verifier); 108 | LOG.info("App service Jwt verified={}. {}", verified, msgSuffix); 109 | if (verified) { 110 | return true; 111 | } 112 | } catch (JOSEException e) { 113 | throw new IllegalStateException(String.format("Problem verifying JWT token. %s", msgSuffix), e); 114 | } 115 | } 116 | return false; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /server/src/main/java/com/github/checketts/config/server/KeySanitizationUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts.config.server; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.List; 8 | import java.util.regex.Pattern; 9 | 10 | import static java.util.stream.Collectors.toList; 11 | 12 | @Component 13 | public class KeySanitizationUtil { 14 | 15 | //Copied from org.springframework.boot.actuate.endpoint.Sanitizer to maintain parity 16 | private static final String[] REGEX_PARTS = {"*", "$", "^", "+"}; 17 | private final List sanitizedKeyPatterns; 18 | 19 | @Autowired 20 | public KeySanitizationUtil( 21 | @Value("#{'${endpoints.env.keys-to-sanitize}'.split(',')}") List keysToSanitize) { 22 | this.sanitizedKeyPatterns = keysToSanitize.stream().map(KeySanitizationUtil::getPattern).collect(toList()); 23 | } 24 | 25 | @SuppressWarnings("squid:UnusedPrivateMethod") //False positive from Sonar as it is used in constructor 26 | //Copied from org.springframework.boot.actuate.endpoint.Sanitizer to maintain parity 27 | private static Pattern getPattern(String value) { 28 | if (isRegex(value)) { 29 | return Pattern.compile(value, Pattern.CASE_INSENSITIVE); 30 | } 31 | return Pattern.compile(".*" + value + "$", Pattern.CASE_INSENSITIVE); 32 | } 33 | 34 | private static boolean isRegex(String value) { 35 | for (String part : REGEX_PARTS) { 36 | if (value.contains(part)) { 37 | return true; 38 | } 39 | } 40 | return false; 41 | } 42 | 43 | public boolean shouldSanitize(String key) { 44 | return sanitizedKeyPatterns.stream().anyMatch(pattern -> pattern.matcher(key).matches()); 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server/src/main/java/com/github/checketts/config/server/LatestRefreshedRepositoryVersionHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts.config.server; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.util.concurrent.atomic.AtomicReference; 6 | 7 | @Component 8 | public class LatestRefreshedRepositoryVersionHolder { 9 | 10 | public static final String UNKNOWN_REPOSITORY_VERSION = "unknown"; 11 | 12 | private AtomicReference latestRefreshedRepositoryVersion = new AtomicReference<>(UNKNOWN_REPOSITORY_VERSION); 13 | 14 | public String getLatestRefreshedRepositoryVersion() { 15 | return latestRefreshedRepositoryVersion.get(); 16 | } 17 | 18 | public void setLatestRefreshedRepositoryVersion(String latestRefreshedRepositoryVersion) { 19 | this.latestRefreshedRepositoryVersion.set(latestRefreshedRepositoryVersion); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server/src/main/java/com/github/checketts/config/server/RegexGroupExtractor.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts.config.server; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.util.concurrent.ConcurrentHashMap; 6 | import java.util.concurrent.ConcurrentMap; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | /** 11 | * Utility bean useful in areas like SpEL WebSecurityExpressions 12 | */ 13 | @Component 14 | public class RegexGroupExtractor { 15 | 16 | private final ConcurrentMap precompiledPatterns = new ConcurrentHashMap<>(); 17 | 18 | /** 19 | * Extracts the string obtained by matching the supplied regex and capture group and prepending a prefix. 20 | * 21 | * @param toMatch The string to extract from 22 | * @param regex The regex to match against that includes the capture group to extract 23 | * @param groupIndex The capture group index 24 | * @param returnPrefix A prefix to prepend to the extracted string. 25 | * @return the string obtained by matching the supplied regex and capture group and prepending a prefix. 26 | */ 27 | public String extract(String toMatch, String regex, int groupIndex, String returnPrefix) { 28 | Pattern groupingPattern = pattern(regex); 29 | Matcher groupingMatcher = groupingPattern.matcher(toMatch); 30 | if (groupingMatcher.matches()) { 31 | if (groupIndex <= groupingMatcher.groupCount()) { 32 | return returnPrefix + groupingMatcher.group(groupIndex); 33 | } 34 | } 35 | throw new IllegalArgumentException(String.format("String to extract not found. toMatch=%s, regex=%s, groupIndex=%s,", 36 | toMatch, regex, groupIndex)); 37 | } 38 | 39 | /** 40 | * Returns a precompiled {@link Pattern} for the supplied regex, if available, otherwise caches and returns a new one 41 | */ 42 | private Pattern pattern(String regex) { 43 | if (precompiledPatterns.containsKey(regex)) { 44 | return precompiledPatterns.get(regex); 45 | } 46 | Pattern result = Pattern.compile(regex); 47 | precompiledPatterns.put(regex, result); 48 | return result; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /server/src/main/java/com/github/checketts/config/server/SanitizeEnforcingEnvironmentEncryptor.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts.config.server; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.cloud.config.environment.Environment; 7 | import org.springframework.cloud.config.environment.PropertySource; 8 | import org.springframework.cloud.config.server.encryption.CipherEnvironmentEncryptor; 9 | import org.springframework.cloud.config.server.encryption.EnvironmentEncryptor; 10 | import org.springframework.cloud.config.server.encryption.TextEncryptorLocator; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.util.LinkedHashMap; 14 | import java.util.LinkedHashSet; 15 | import java.util.Map; 16 | 17 | @Component 18 | public class SanitizeEnforcingEnvironmentEncryptor implements EnvironmentEncryptor { 19 | 20 | public static final String DECRYPT_WITHHELD_MSG = 21 | "Decrypted value withheld because key is not registered for sanitization"; 22 | private final static Logger LOG = LoggerFactory.getLogger(SanitizeEnforcingEnvironmentEncryptor.class); 23 | 24 | private final CipherEnvironmentEncryptor delegate; 25 | private final KeySanitizationUtil keySanitizationUtil; 26 | 27 | @Autowired 28 | public SanitizeEnforcingEnvironmentEncryptor(TextEncryptorLocator textEncryptorLocator, 29 | KeySanitizationUtil keySanitizationUtil) { 30 | this.delegate = new CipherEnvironmentEncryptor(textEncryptorLocator); 31 | this.keySanitizationUtil = keySanitizationUtil; 32 | } 33 | 34 | @Override 35 | public Environment decrypt(Environment environment) { 36 | Environment sanitizedEnv = new Environment(environment.getName(), 37 | environment.getProfiles(), environment.getLabel(), environment.getVersion()); 38 | for (PropertySource source : environment.getPropertySources()) { 39 | Map map = new LinkedHashMap<>(source.getSource()); 40 | for (Map.Entry entry : new LinkedHashSet<>(map.entrySet())) { 41 | Object key = entry.getKey(); 42 | String name = key.toString(); 43 | String value = entry.getValue().toString(); 44 | if (value.startsWith("{cipher}") && !keySanitizationUtil.shouldSanitize(name)) { 45 | LOG.warn("Not decrypting value because key is not registered for sanitization. key={}", key); 46 | map.remove(key); 47 | map.put(name, DECRYPT_WITHHELD_MSG); 48 | } 49 | } 50 | sanitizedEnv.add(new PropertySource(source.getName(), map)); 51 | } 52 | return delegate.decrypt(sanitizedEnv); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | security: 2 | basic: 3 | enabled: false 4 | management: 5 | security: 6 | enabled: false 7 | 8 | greeter: 9 | name: World 10 | 11 | 12 | config.server: 13 | # Ideally would be YAML list, but using comma-separated string because Spring Test does not yet support YAML properties, only .properties 14 | authentication.jwt.secrets: "tNYH6mjVwXevTnTrqVne3XrmKUF4a4YP,\ 15 | h5zf4MTg6RP7VB44gupy7fDsTHUGMmEx" 16 | encrypt.keys: 17 | envA_v1: "symmetricKeyValue_envA" 18 | envB_v1: "symmetricKeyValue_envB" 19 | envB_serviceB_v1: "symmetricKeyValue_envB_serviceB" 20 | globalEnvA: "symmetricKeyValue_globalEnvA" 21 | nightly1_v1: XmsvMqkDvvdy8ZEtVaTNmcQSNFkJKcft 22 | 23 | 24 | -------------------------------------------------------------------------------- /server/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring.cloud.config: 2 | server: 3 | bootstrap: true 4 | # git: 5 | # uri: cloud-configuration@localhost:cloud-config.git 6 | # searchPaths: '*' 7 | # prefix: raw-config 8 | 9 | encrypt.key: "symmetricKeyValue_default" 10 | 11 | spring.application.name: configServerExample 12 | 13 | 14 | # Needed to select a native EnvironmentRepository rather than a Git EnvironmentRepository (the default) 15 | spring.profiles.include: native 16 | 17 | # ${user.parent.dir} is set in main() to get the parent of ${user.dir} 18 | spring.cloud.config.server.native.searchLocations: file://${user.parent.dir}/sample-config-repo 19 | -------------------------------------------------------------------------------- /server/src/test/java/com/github/checketts/ConfigServerExampleApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.github.checketts; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.SpringApplicationConfiguration; 6 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 7 | import org.springframework.test.context.web.WebAppConfiguration; 8 | 9 | @RunWith(SpringJUnit4ClassRunner.class) 10 | @SpringApplicationConfiguration(classes = ConfigServerExampleApplication.class) 11 | @WebAppConfiguration 12 | public class ConfigServerExampleApplicationTests { 13 | 14 | @Test 15 | public void contextLoads() { 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'configServerExample' 2 | include 'app' 3 | include 'server' 4 | --------------------------------------------------------------------------------