diff --git a/.gitignore b/.gitignore
index 72530d2b..5c1d57a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,4 +20,5 @@ _site/
.factorypath
.vscode/
.flattened-pom.xml
+IntelliJ_Spring_Boot_Java_Conventions.xml
.sdkmanrc
diff --git a/pom.xml b/pom.xml
index f3bbe6ec..af56ee71 100644
--- a/pom.xml
+++ b/pom.xml
@@ -93,6 +93,16 @@
google-auth-library-oauth2-http
0.22.0
+
+ com.google.cloud
+ google-cloud-secretmanager
+ 1.0.1
+
+
+ com.google.apis
+ google-api-services-cloudresourcemanager
+ v1-rev20200210-1.30.9
+
diff --git a/spring-cloud-config-server/pom.xml b/spring-cloud-config-server/pom.xml
index 4cebc248..dcd9d737 100644
--- a/spring-cloud-config-server/pom.xml
+++ b/spring-cloud-config-server/pom.xml
@@ -114,6 +114,16 @@
google-api-services-iam
true
+
+ com.google.cloud
+ google-cloud-secretmanager
+ true
+
+
+ com.google.apis
+ google-api-services-cloudresourcemanager
+ true
+
com.google.auth
google-auth-library-oauth2-http
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java
index cf49e30f..271ba57c 100644
--- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java
@@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletRequest;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.secretsmanager.AWSSecretsManager;
+import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
import org.apache.http.client.HttpClient;
import org.eclipse.jgit.api.TransportConfigCallback;
import org.tmatesoft.svn.core.SVNException;
@@ -54,6 +55,9 @@ import org.springframework.cloud.config.server.environment.CredhubEnvironmentRep
import org.springframework.cloud.config.server.environment.CredhubEnvironmentRepositoryFactory;
import org.springframework.cloud.config.server.environment.EnvironmentRepository;
import org.springframework.cloud.config.server.environment.EnvironmentWatch;
+import org.springframework.cloud.config.server.environment.GoogleSecretManagerEnvironmentProperties;
+import org.springframework.cloud.config.server.environment.GoogleSecretManagerEnvironmentRepository;
+import org.springframework.cloud.config.server.environment.GoogleSecretManagerEnvironmentRepositoryFactory;
import org.springframework.cloud.config.server.environment.HttpClientConfigurableHttpConnectionFactory;
import org.springframework.cloud.config.server.environment.HttpClientVaultRestTemplateFactory;
import org.springframework.cloud.config.server.environment.HttpRequestConfigTokenProvider;
@@ -107,13 +111,13 @@ import org.springframework.vault.core.VaultTemplate;
@EnableConfigurationProperties({ SvnKitEnvironmentProperties.class, CredhubEnvironmentProperties.class,
JdbcEnvironmentProperties.class, NativeEnvironmentProperties.class, VaultEnvironmentProperties.class,
RedisEnvironmentProperties.class, AwsS3EnvironmentProperties.class,
- AwsSecretsManagerEnvironmentProperties.class })
+ AwsSecretsManagerEnvironmentProperties.class, GoogleSecretManagerEnvironmentProperties.class })
@Import({ CompositeRepositoryConfiguration.class, JdbcRepositoryConfiguration.class, VaultConfiguration.class,
VaultRepositoryConfiguration.class, SpringVaultRepositoryConfiguration.class, CredhubConfiguration.class,
CredhubRepositoryConfiguration.class, SvnRepositoryConfiguration.class, NativeRepositoryConfiguration.class,
GitRepositoryConfiguration.class, RedisRepositoryConfiguration.class, GoogleCloudSourceConfiguration.class,
AwsS3RepositoryConfiguration.class, AwsSecretsManagerRepositoryConfiguration.class,
- DefaultRepositoryConfiguration.class })
+ GoogleSecretManagerRepositoryConfiguration.class, DefaultRepositoryConfiguration.class })
public class EnvironmentRepositoryConfiguration {
@Bean
@@ -248,6 +252,18 @@ public class EnvironmentRepositoryConfiguration {
}
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(SecretManagerServiceClient.class)
+ static class GoogleSecretManagerFactoryConfig {
+
+ @Bean
+ public GoogleSecretManagerEnvironmentRepositoryFactory googleSecretManagerEnvironmentRepositoryFactory(
+ ObjectProvider request) {
+ return new GoogleSecretManagerEnvironmentRepositoryFactory(request);
+ }
+
+ }
+
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HttpClient.class)
@ConditionalOnMissingClass("org.springframework.vault.core.VaultTemplate")
@@ -491,3 +507,18 @@ class CompositeRepositoryConfiguration {
}
}
+
+@Configuration(proxyBeanMethods = false)
+@Profile("secret-manager")
+@ConditionalOnClass(SecretManagerServiceClient.class)
+class GoogleSecretManagerRepositoryConfiguration {
+
+ @Bean
+ public GoogleSecretManagerEnvironmentRepository googleSecretManagerEnvironmentRepository(
+ GoogleSecretManagerEnvironmentRepositoryFactory factory,
+ GoogleSecretManagerEnvironmentProperties environmentProperties)
+ throws Exception {
+ return factory.build(environmentProperties);
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentProperties.java
new file mode 100644
index 00000000..2c1bbd7e
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentProperties.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2013-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.config.server.environment;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.config.server.support.EnvironmentRepositoryProperties;
+import org.springframework.core.Ordered;
+
+/**
+ * @author Jose Maria Alvarez
+ */
+@ConfigurationProperties("spring.cloud.config.server.gcp-secret-manager")
+public class GoogleSecretManagerEnvironmentProperties
+ implements EnvironmentRepositoryProperties {
+
+ private int order = Ordered.LOWEST_PRECEDENCE;
+
+ private String applicationLabel = "application";
+
+ private String profileLabel = "profile";
+
+ private String serviceAccount = null;
+
+ private boolean tokenMandatory = true;
+
+ private Integer version = 1;
+
+ /**
+ * The metadata URL to get the project ID from.
+ */
+ public static final String GOOGLE_METADATA_PROJECT_URL = "http://metadata.google.internal/computeMetadata/v1/project/project-id";
+
+ @Override
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ public int getOrder() {
+ return order;
+ }
+
+ public Boolean getTokenMandatory() {
+ return tokenMandatory;
+ }
+
+ public void setTokenMandatory(Boolean tokenMandatory) {
+ this.tokenMandatory = tokenMandatory;
+ }
+
+ public String getApplicationLabel() {
+ return applicationLabel;
+ }
+
+ public void setApplicationLabel(String applicationLabel) {
+ this.applicationLabel = applicationLabel;
+ }
+
+ public String getProfileLabel() {
+ return profileLabel;
+ }
+
+ public void setProfileLabel(String profileLabel) {
+ this.profileLabel = profileLabel;
+ }
+
+ public Integer getVersion() {
+ return version;
+ }
+
+ public void setVersion(Integer version) {
+ this.version = version;
+ }
+
+ public String getServiceAccount() {
+ return serviceAccount;
+ }
+
+ public void setServiceAccount(String serviceAccount) {
+ this.serviceAccount = serviceAccount;
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepository.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepository.java
new file mode 100644
index 00000000..f76da9fc
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepository.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2013-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.config.server.environment;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import com.google.cloud.secretmanager.v1.Secret;
+import org.apache.commons.lang3.StringUtils;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.cloud.config.environment.Environment;
+import org.springframework.cloud.config.environment.PropertySource;
+import org.springframework.cloud.config.server.environment.secretmanager.GoogleConfigProvider;
+import org.springframework.cloud.config.server.environment.secretmanager.GoogleSecretComparatorByVersion;
+import org.springframework.cloud.config.server.environment.secretmanager.GoogleSecretManagerAccessStrategy;
+import org.springframework.cloud.config.server.environment.secretmanager.GoogleSecretManagerAccessStrategyFactory;
+import org.springframework.cloud.config.server.environment.secretmanager.HttpHeaderGoogleConfigProvider;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * @author Jose Maria Alvarez
+ */
+public class GoogleSecretManagerEnvironmentRepository implements EnvironmentRepository {
+
+ private String applicationLabel;
+
+ private String profileLabel;
+
+ private GoogleSecretManagerAccessStrategy accessStrategy;
+
+ private boolean tokenMandatory;
+
+ private GoogleConfigProvider configProvider;
+
+ public GoogleSecretManagerEnvironmentRepository(
+ ObjectProvider request, RestTemplate rest,
+ GoogleSecretManagerEnvironmentProperties properties) {
+ this.applicationLabel = properties.getApplicationLabel();
+ this.profileLabel = properties.getProfileLabel();
+ this.configProvider = new HttpHeaderGoogleConfigProvider(request);
+ this.accessStrategy = GoogleSecretManagerAccessStrategyFactory.forVersion(rest,
+ configProvider, properties);
+ this.tokenMandatory = properties.getTokenMandatory();
+ }
+
+ @Override
+ public Environment findOne(String application, String profile, String label) {
+ if (StringUtils.isEmpty(label)) {
+ label = "master";
+ }
+ if (StringUtils.isEmpty(profile)) {
+ profile = "default";
+ }
+ if (!profile.startsWith("default")) {
+ profile = "default," + profile;
+ }
+ String[] profiles = org.springframework.util.StringUtils
+ .trimArrayElements(org.springframework.util.StringUtils.commaDelimitedListToStringArray(profile));
+ Environment result = new Environment(application, profile, label, null, null);
+ if (tokenMandatory) {
+ if (accessStrategy.checkRemotePermissions()) {
+ addPropertySource(application, profiles, result);
+ }
+ }
+ else {
+ addPropertySource(application, profiles, result);
+ }
+ return result;
+ }
+
+ private void addPropertySource(String application, String[] profiles,
+ Environment result) {
+ for (String profileUnit : profiles) {
+ Map, ?> secrets = getSecrets(application, profileUnit);
+ if (!secrets.isEmpty()) {
+ result.add(new PropertySource("gsm:" + application + "-" + profileUnit,
+ secrets));
+ }
+ }
+ }
+
+ /**
+ * @param application the application name
+ * @param profile the profile name
+ * @return the properties to add into the environment
+ */
+ private Map, ?> getSecrets(String application, String profile) {
+ Map result = new HashMap<>();
+ String prefix = configProvider
+ .getValue(HttpHeaderGoogleConfigProvider.PREFIX_HEADER, false);
+ for (Secret secret : accessStrategy.getSecrets()) {
+ if (secret.getLabelsOrDefault(applicationLabel, "application")
+ .equalsIgnoreCase(application)
+ && secret.getLabelsOrDefault(profileLabel, "profile")
+ .equalsIgnoreCase(profile)) {
+ result.put(accessStrategy.getSecretName(secret), accessStrategy
+ .getSecretValue(secret, new GoogleSecretComparatorByVersion()));
+ }
+ else if (StringUtils.isNotBlank(prefix)
+ && accessStrategy.getSecretName(secret).startsWith(prefix)) {
+ result.put(
+ StringUtils.removeStart(accessStrategy.getSecretName(secret),
+ prefix),
+ accessStrategy.getSecretValue(secret,
+ new GoogleSecretComparatorByVersion()));
+ }
+ }
+ return result;
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryFactory.java
new file mode 100644
index 00000000..063967de
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2013-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.config.server.environment;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * @author Jose Maria Alvarez
+ */
+public class GoogleSecretManagerEnvironmentRepositoryFactory implements
+ EnvironmentRepositoryFactory {
+
+ private final ObjectProvider request;
+
+ public GoogleSecretManagerEnvironmentRepositoryFactory(
+ ObjectProvider request) {
+ this.request = request;
+ }
+
+ @Override
+ public GoogleSecretManagerEnvironmentRepository build(
+ GoogleSecretManagerEnvironmentProperties environmentProperties)
+ throws Exception {
+ return new GoogleSecretManagerEnvironmentRepository(request, new RestTemplate(),
+ environmentProperties);
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleConfigProvider.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleConfigProvider.java
new file mode 100644
index 00000000..fbb963a6
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleConfigProvider.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.config.server.environment.secretmanager;
+
+public interface GoogleConfigProvider {
+
+ String getValue(String key, Boolean mandatory);
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretComparatorByVersion.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretComparatorByVersion.java
new file mode 100644
index 00000000..391026a4
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretComparatorByVersion.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2013-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.config.server.environment.secretmanager;
+
+import java.util.Comparator;
+
+import com.google.cloud.secretmanager.v1.SecretVersion;
+
+public class GoogleSecretComparatorByVersion implements Comparator {
+
+ @Override
+ public int compare(SecretVersion leftVersion, SecretVersion rightVersion) {
+ if (rightVersion == null) {
+ return 1;
+ }
+ if (leftVersion == null) {
+ return -1;
+ }
+ return leftVersion.getName().compareTo(rightVersion.getName());
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerAccessStrategy.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerAccessStrategy.java
new file mode 100644
index 00000000..65566cb4
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerAccessStrategy.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2013-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.config.server.environment.secretmanager;
+
+import java.util.Comparator;
+
+import com.google.cloud.secretmanager.v1.Secret;
+import com.google.cloud.secretmanager.v1.SecretVersion;
+
+public interface GoogleSecretManagerAccessStrategy {
+
+ Iterable getSecrets();
+
+ String getSecretValue(Secret secret, Comparator comparator);
+
+ String getSecretName(Secret secret);
+
+ Boolean checkRemotePermissions();
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerAccessStrategyFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerAccessStrategyFactory.java
new file mode 100644
index 00000000..9ef225ba
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerAccessStrategyFactory.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2013-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.config.server.environment.secretmanager;
+
+import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
+
+import org.springframework.cloud.config.server.environment.GoogleSecretManagerEnvironmentProperties;
+import org.springframework.cloud.config.server.environment.RepositoryException;
+import org.springframework.web.client.RestTemplate;
+
+public final class GoogleSecretManagerAccessStrategyFactory {
+
+ private GoogleSecretManagerAccessStrategyFactory() {
+ throw new IllegalStateException("Can't instantiate an utility class");
+ }
+
+ public static GoogleSecretManagerAccessStrategy forVersion(RestTemplate rest,
+ GoogleConfigProvider configProvider,
+ GoogleSecretManagerEnvironmentProperties properties) {
+
+ switch (properties.getVersion()) {
+ case 1:
+ try {
+ return new GoogleSecretManagerV1AccessStrategy(rest, configProvider,
+ properties.getServiceAccount());
+ }
+ catch (Exception e) {
+ throw new RepositoryException("Cannot create service client", e);
+ }
+ default:
+ throw new IllegalArgumentException(
+ "No support for given Google Secret manager backend version "
+ + properties.getVersion());
+ }
+
+ }
+
+ public static GoogleSecretManagerAccessStrategy forVersion(RestTemplate rest,
+ GoogleConfigProvider configProvider,
+ GoogleSecretManagerEnvironmentProperties properties,
+ SecretManagerServiceClient client) {
+
+ switch (properties.getVersion()) {
+ case 1:
+ return new GoogleSecretManagerV1AccessStrategy(rest, configProvider,
+ client);
+ default:
+ throw new IllegalArgumentException(
+ "No support for given Google Secret manager backend version "
+ + properties.getVersion());
+ }
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerV1AccessStrategy.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerV1AccessStrategy.java
new file mode 100644
index 00000000..6b4b5943
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerV1AccessStrategy.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2013-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.config.server.environment.secretmanager;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
+import com.google.api.client.http.HttpRequestInitializer;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.api.gax.core.FixedCredentialsProvider;
+import com.google.api.services.cloudresourcemanager.CloudResourceManager;
+import com.google.api.services.cloudresourcemanager.model.TestIamPermissionsRequest;
+import com.google.api.services.cloudresourcemanager.model.TestIamPermissionsResponse;
+import com.google.auth.http.HttpCredentialsAdapter;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.secretmanager.v1.AccessSecretVersionRequest;
+import com.google.cloud.secretmanager.v1.AccessSecretVersionResponse;
+import com.google.cloud.secretmanager.v1.ListSecretVersionsRequest;
+import com.google.cloud.secretmanager.v1.ListSecretsRequest;
+import com.google.cloud.secretmanager.v1.ProjectName;
+import com.google.cloud.secretmanager.v1.Secret;
+import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
+import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings;
+import com.google.cloud.secretmanager.v1.SecretName;
+import com.google.cloud.secretmanager.v1.SecretVersion;
+import com.google.cloud.secretmanager.v1.SecretVersionName;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.cloud.config.server.environment.GoogleSecretManagerEnvironmentProperties;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.client.RestTemplate;
+
+public class GoogleSecretManagerV1AccessStrategy
+ implements GoogleSecretManagerAccessStrategy {
+
+ private final SecretManagerServiceClient client;
+
+ private final RestTemplate rest;
+
+ private final GoogleConfigProvider configProvider;
+
+ private static final String APPLICATION_NAME = "spring-cloud-config-server";
+
+ private static final String ACCESS_SECRET_PERMISSION = "secretmanager.versions.access";
+
+ private static Log logger = LogFactory
+ .getLog(GoogleSecretManagerV1AccessStrategy.class);
+
+ public GoogleSecretManagerV1AccessStrategy(RestTemplate rest,
+ GoogleConfigProvider configProvider, String serviceAccountFile)
+ throws IOException {
+ if (StringUtils.isNotEmpty(serviceAccountFile)) {
+ GoogleCredentials creds = GoogleCredentials
+ .fromStream(new FileInputStream(new File(serviceAccountFile)));
+ this.client = SecretManagerServiceClient.create(SecretManagerServiceSettings
+ .newBuilder()
+ .setCredentialsProvider(FixedCredentialsProvider.create(creds))
+ .build());
+ }
+ else {
+ this.client = SecretManagerServiceClient.create();
+ }
+ this.rest = rest;
+ this.configProvider = configProvider;
+ }
+
+ public GoogleSecretManagerV1AccessStrategy(RestTemplate rest,
+ GoogleConfigProvider configProvider, SecretManagerServiceClient client) {
+ this.client = client;
+ this.rest = rest;
+ this.configProvider = configProvider;
+ }
+
+ @Override
+ public List getSecrets() {
+ // Build the parent name.
+ ProjectName project = ProjectName.of(getProjectId());
+
+ // Create the request.
+ ListSecretsRequest listSecretRequest = ListSecretsRequest.newBuilder()
+ .setParent(project.toString()).build();
+
+ // Get all secrets.
+ SecretManagerServiceClient.ListSecretsPagedResponse pagedListSecretResponse = client
+ .listSecrets(listSecretRequest);
+
+ List result = new ArrayList();
+ pagedListSecretResponse.iterateAll().forEach(result::add);
+
+ // List all secrets.
+ return result;
+ }
+
+ private List getSecretVersions(Secret secret) {
+ SecretName parent = SecretName.parse(secret.getName());
+
+ // Create the request.
+ ListSecretVersionsRequest listVersionRequest = ListSecretVersionsRequest
+ .newBuilder().setParent(parent.toString()).build();
+
+ // Get all versions.
+ SecretManagerServiceClient.ListSecretVersionsPagedResponse pagedListVersionResponse = client
+ .listSecretVersions(listVersionRequest);
+ List result = new ArrayList();
+ pagedListVersionResponse.iterateAll().forEach(result::add);
+ return result;
+ }
+
+ @Override
+ public String getSecretValue(Secret secret, Comparator comparator) {
+ String result = null;
+ List versions = getSecretVersions(secret);
+ SecretVersion winner = null;
+ for (SecretVersion secretVersion : versions) {
+ if ((secretVersion.getState()
+ .getNumber() == SecretVersion.State.ENABLED_VALUE)
+ && comparator.compare(secretVersion, winner) > 0) {
+ winner = secretVersion;
+ }
+ }
+ if (winner != null) {
+ SecretVersionName name = SecretVersionName.parse(winner.getName());
+ // Access the secret version.
+ AccessSecretVersionRequest request = AccessSecretVersionRequest.newBuilder()
+ .setName(name.toString()).build();
+ AccessSecretVersionResponse response = client.accessSecretVersion(request);
+ result = response.getPayload().getData().toStringUtf8();
+ }
+ return result;
+ }
+
+ @Override
+ public String getSecretName(Secret secret) {
+ SecretName parent = SecretName.parse(secret.getName());
+ return parent.getSecret();
+ }
+
+ @Override
+ public Boolean checkRemotePermissions() {
+ CloudResourceManager service = null;
+ try {
+ AccessToken accessToken = new AccessToken(getAccessToken(), null);
+ GoogleCredentials credential = new GoogleCredentials(accessToken);
+ HttpRequestInitializer requestInitializer = new HttpCredentialsAdapter(
+ credential);
+ service = new CloudResourceManager.Builder(
+ GoogleNetHttpTransport.newTrustedTransport(),
+ JacksonFactory.getDefaultInstance(), requestInitializer)
+ .setApplicationName(APPLICATION_NAME).build();
+ List permissionsList = Arrays.asList(ACCESS_SECRET_PERMISSION);
+
+ TestIamPermissionsRequest requestBody = new TestIamPermissionsRequest()
+ .setPermissions(permissionsList);
+
+ TestIamPermissionsResponse testIamPermissionsResponse = service.projects()
+ .testIamPermissions(getProjectId(), requestBody).execute();
+
+ if (testIamPermissionsResponse.getPermissions() != null
+ && testIamPermissionsResponse.size() >= 1) {
+ return Boolean.TRUE;
+ }
+ else {
+ logger.warn(
+ "Access token has no permissions to access secrets in project");
+ return Boolean.FALSE;
+ }
+ }
+ catch (Exception e) {
+ logger.info("Unable to check token permissions", e);
+ return Boolean.FALSE;
+ }
+ }
+
+ private String getAccessToken() {
+ return configProvider.getValue(HttpHeaderGoogleConfigProvider.ACCESS_TOKEN_HEADER,
+ true);
+ }
+
+ /**
+ * @return
+ */
+ private String getProjectId() {
+ String result = null;
+ try {
+ result = configProvider
+ .getValue(HttpHeaderGoogleConfigProvider.PROJECT_ID_HEADER, true);
+ }
+ catch (Exception e) {
+ // not in GCP
+ HttpEntity entity = new HttpEntity("parameters",
+ getMetadataHttpHeaders());
+ result = rest.exchange(
+ GoogleSecretManagerEnvironmentProperties.GOOGLE_METADATA_PROJECT_URL,
+ HttpMethod.GET, entity, String.class).getBody();
+ }
+ return result;
+ }
+
+ private static HttpHeaders getMetadataHttpHeaders() {
+ HttpHeaders headers = new HttpHeaders();
+ headers.set("Metadata-Flavor", "Google");
+ return headers;
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/HttpHeaderGoogleConfigProvider.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/HttpHeaderGoogleConfigProvider.java
new file mode 100644
index 00000000..cee762e2
--- /dev/null
+++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/HttpHeaderGoogleConfigProvider.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2013-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.config.server.environment.secretmanager;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.util.StringUtils;
+
+public class HttpHeaderGoogleConfigProvider implements GoogleConfigProvider {
+
+ /**
+ * The Project ID Header admited to get the project name for google cloud secret
+ * manager.
+ */
+ public static final String PROJECT_ID_HEADER = "X-Project-ID";
+
+ /**
+ * The Config Token ID Header admited to get the access token from the client.
+ */
+ public static final String ACCESS_TOKEN_HEADER = "X-Config-Token";
+
+ /**
+ * The prefix we should search for in secrets to take them into account.
+ */
+ public static final String PREFIX_HEADER = "X-Secret-Prefix";
+
+ private ObjectProvider httpRequest;
+
+ public HttpHeaderGoogleConfigProvider(ObjectProvider request) {
+ this.httpRequest = request;
+ }
+
+ @Override
+ public String getValue(String key, Boolean mandatory) {
+ HttpServletRequest request = httpRequest.getIfAvailable();
+ if (request == null) {
+ throw new IllegalStateException("No HttpServletRequest available");
+ }
+ String value = request.getHeader(key);
+ if (!StringUtils.hasLength(value) && mandatory) {
+ throw new IllegalArgumentException(
+ "Missing required header in HttpServletRequest: " + key);
+ }
+ return value;
+ }
+
+}
diff --git a/spring-cloud-config-server/src/main/resources/configserver.yml b/spring-cloud-config-server/src/main/resources/configserver.yml
index 3c08dd50..1310e4cf 100644
--- a/spring-cloud-config-server/src/main/resources/configserver.yml
+++ b/spring-cloud-config-server/src/main/resources/configserver.yml
@@ -1,5 +1,6 @@
info:
component: Config Server
+
spring:
application:
name: configserver
@@ -14,7 +15,6 @@ spring:
repos:
- patterns: multi-repo-demo-*
uri: https://github.com/spring-cloud-samples/config-repo
-
server:
port: 8888
management:
diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryTests.java
new file mode 100644
index 00000000..e6377b13
--- /dev/null
+++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryTests.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2016-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.cloud.config.server.environment;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.cloud.secretmanager.v1.AccessSecretVersionRequest;
+import com.google.cloud.secretmanager.v1.AccessSecretVersionResponse;
+import com.google.cloud.secretmanager.v1.ListSecretVersionsRequest;
+import com.google.cloud.secretmanager.v1.ListSecretsRequest;
+import com.google.cloud.secretmanager.v1.Secret;
+import com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
+import com.google.cloud.secretmanager.v1.SecretPayload;
+import com.google.cloud.secretmanager.v1.SecretVersion;
+import com.google.protobuf.ByteString;
+import org.junit.Test;
+import org.mockito.ArgumentMatcher;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+import org.springframework.cloud.config.server.environment.secretmanager.GoogleConfigProvider;
+import org.springframework.cloud.config.server.environment.secretmanager.GoogleSecretComparatorByVersion;
+import org.springframework.cloud.config.server.environment.secretmanager.GoogleSecretManagerAccessStrategyFactory;
+import org.springframework.cloud.config.server.environment.secretmanager.GoogleSecretManagerV1AccessStrategy;
+import org.springframework.cloud.config.server.environment.secretmanager.HttpHeaderGoogleConfigProvider;
+import org.springframework.web.client.RestTemplate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class GoogleSecretManagerEnvironmentRepositoryTests {
+
+ @Test
+ public void testSupportedStrategy() {
+ GoogleSecretManagerEnvironmentProperties properties = new GoogleSecretManagerEnvironmentProperties();
+ SecretManagerServiceClient mock = mock(SecretManagerServiceClient.class);
+ properties.setVersion(1);
+ assertThat(GoogleSecretManagerAccessStrategyFactory.forVersion(null, null,
+ properties, mock) instanceof GoogleSecretManagerV1AccessStrategy).isTrue();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetUnsupportedStrategy() {
+ GoogleSecretManagerEnvironmentProperties properties = new GoogleSecretManagerEnvironmentProperties();
+ SecretManagerServiceClient mock = mock(SecretManagerServiceClient.class);
+ properties.setVersion(2);
+ GoogleSecretManagerAccessStrategyFactory.forVersion(null, null, properties, mock);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testGetSecrets() throws IOException {
+ RestTemplate rest = mock(RestTemplate.class);
+ GoogleConfigProvider provider = mock(HttpHeaderGoogleConfigProvider.class);
+ when(provider.getValue(HttpHeaderGoogleConfigProvider.PROJECT_ID_HEADER, true))
+ .thenReturn("test-project");
+ SecretManagerServiceClient mock = mock(SecretManagerServiceClient.class);
+ SecretManagerServiceClient.ListSecretsPagedResponse response = mock(
+ SecretManagerServiceClient.ListSecretsPagedResponse.class);
+ Secret secret = Secret.newBuilder().setName("projects/test-project/secrets/test")
+ .build();
+ List secrets = new ArrayList();
+ secrets.add(secret);
+ when(response.iterateAll()).thenReturn(secrets);
+ Mockito.doReturn(response).when(mock).listSecrets(any(ListSecretsRequest.class));
+ GoogleSecretManagerV1AccessStrategy strategy = new GoogleSecretManagerV1AccessStrategy(
+ rest, provider, mock);
+ assertThat(strategy.getSecrets().size()).isEqualTo(1);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testGetSecretValues() throws IOException {
+ RestTemplate rest = mock(RestTemplate.class);
+ GoogleConfigProvider provider = mock(HttpHeaderGoogleConfigProvider.class);
+ when(provider.getValue(HttpHeaderGoogleConfigProvider.PROJECT_ID_HEADER, true))
+ .thenReturn("test-project");
+ SecretManagerServiceClient mock = mock(SecretManagerServiceClient.class);
+ SecretManagerServiceClient.ListSecretVersionsPagedResponse response = mock(
+ SecretManagerServiceClient.ListSecretVersionsPagedResponse.class);
+ SecretVersion secret1 = SecretVersion.newBuilder()
+ .setName("projects/test-project/secrets/test/versions/1")
+ .setState(SecretVersion.State.ENABLED).build();
+ SecretVersion secret2 = SecretVersion.newBuilder()
+ .setName("projects/test-project/secrets/test/versions/2")
+ .setState(SecretVersion.State.DISABLED).build();
+ List secrets = new ArrayList();
+ secrets.add(secret1);
+ secrets.add(secret2);
+ when(response.iterateAll()).thenReturn(secrets);
+ Mockito.doReturn(response).when(mock)
+ .listSecretVersions(any(ListSecretVersionsRequest.class));
+ GoogleSecretManagerV1AccessStrategy strategy = new GoogleSecretManagerV1AccessStrategy(
+ rest, provider, mock);
+ AccessSecretVersionResponse accessSecretVersionResponse = mock(
+ AccessSecretVersionResponse.class);
+ SecretPayload payload = mock(SecretPayload.class);
+ ByteString data = mock(ByteString.class);
+ when(accessSecretVersionResponse.getPayload()).thenReturn(payload);
+ when(payload.getData()).thenReturn(data);
+ when(data.toStringUtf8()).thenReturn("test-value");
+ ArgumentMatcher matcher = new ArgumentMatcher() {
+ @Override
+ public boolean matches(
+ AccessSecretVersionRequest accessSecretVersionRequest) {
+ if (accessSecretVersionRequest.getName()
+ .equals("projects/test-project/secrets/test/versions/1")) {
+ return true;
+ }
+ return false;
+ }
+ };
+ Mockito.doReturn(accessSecretVersionResponse).when(mock)
+ .accessSecretVersion(ArgumentMatchers.argThat(matcher));
+ assertThat(strategy.getSecretValue(
+ Secret.newBuilder().setName("projects/test-project/secrets/test").build(),
+ new GoogleSecretComparatorByVersion())).isEqualTo("test-value");
+ }
+
+}
diff --git a/spring-cloud-config-server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/spring-cloud-config-server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 00000000..1f0955d4
--- /dev/null
+++ b/spring-cloud-config-server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline