From 64a28c476dbae58e573e7d1e3be09dcb48733c6a Mon Sep 17 00:00:00 2001 From: Jose Maria Alvarez Date: Fri, 13 Mar 2020 09:30:43 +0100 Subject: [PATCH] Adds support for google secret manager in spring cloud config server Fixes gh-1628 --- .gitignore | 1 + pom.xml | 10 + spring-cloud-config-server/pom.xml | 10 + .../EnvironmentRepositoryConfiguration.java | 35 ++- ...gleSecretManagerEnvironmentProperties.java | 96 ++++++++ ...gleSecretManagerEnvironmentRepository.java | 128 ++++++++++ ...etManagerEnvironmentRepositoryFactory.java | 45 ++++ .../secretmanager/GoogleConfigProvider.java | 23 ++ .../GoogleSecretComparatorByVersion.java | 36 +++ .../GoogleSecretManagerAccessStrategy.java | 34 +++ ...gleSecretManagerAccessStrategyFactory.java | 68 ++++++ .../GoogleSecretManagerV1AccessStrategy.java | 230 ++++++++++++++++++ .../HttpHeaderGoogleConfigProvider.java | 62 +++++ .../src/main/resources/configserver.yml | 2 +- ...cretManagerEnvironmentRepositoryTests.java | 138 +++++++++++ .../org.mockito.plugins.MockMaker | 1 + 16 files changed, 916 insertions(+), 3 deletions(-) create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentProperties.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepository.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryFactory.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleConfigProvider.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretComparatorByVersion.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerAccessStrategy.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerAccessStrategyFactory.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/GoogleSecretManagerV1AccessStrategy.java create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/secretmanager/HttpHeaderGoogleConfigProvider.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/GoogleSecretManagerEnvironmentRepositoryTests.java create mode 100644 spring-cloud-config-server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker 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