Adds support for google secret manager in spring cloud config server
Fixes gh-1628
This commit is contained in:
committed by
spencergibb
parent
fa21f2d02b
commit
64a28c476d
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,4 +20,5 @@ _site/
|
||||
.factorypath
|
||||
.vscode/
|
||||
.flattened-pom.xml
|
||||
IntelliJ_Spring_Boot_Java_Conventions.xml
|
||||
.sdkmanrc
|
||||
|
||||
10
pom.xml
10
pom.xml
@@ -93,6 +93,16 @@
|
||||
<artifactId>google-auth-library-oauth2-http</artifactId>
|
||||
<version>0.22.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>google-cloud-secretmanager</artifactId>
|
||||
<version>1.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.apis</groupId>
|
||||
<artifactId>google-api-services-cloudresourcemanager</artifactId>
|
||||
<version>v1-rev20200210-1.30.9</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<profiles>
|
||||
|
||||
@@ -114,6 +114,16 @@
|
||||
<artifactId>google-api-services-iam</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.cloud</groupId>
|
||||
<artifactId>google-cloud-secretmanager</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.apis</groupId>
|
||||
<artifactId>google-api-services-cloudresourcemanager</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auth</groupId>
|
||||
<artifactId>google-auth-library-oauth2-http</artifactId>
|
||||
|
||||
@@ -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<HttpServletRequest> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<HttpServletRequest> 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<String, String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<GoogleSecretManagerEnvironmentRepository, GoogleSecretManagerEnvironmentProperties> {
|
||||
|
||||
private final ObjectProvider<HttpServletRequest> request;
|
||||
|
||||
public GoogleSecretManagerEnvironmentRepositoryFactory(
|
||||
ObjectProvider<HttpServletRequest> request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GoogleSecretManagerEnvironmentRepository build(
|
||||
GoogleSecretManagerEnvironmentProperties environmentProperties)
|
||||
throws Exception {
|
||||
return new GoogleSecretManagerEnvironmentRepository(request, new RestTemplate(),
|
||||
environmentProperties);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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<SecretVersion> {
|
||||
|
||||
@Override
|
||||
public int compare(SecretVersion leftVersion, SecretVersion rightVersion) {
|
||||
if (rightVersion == null) {
|
||||
return 1;
|
||||
}
|
||||
if (leftVersion == null) {
|
||||
return -1;
|
||||
}
|
||||
return leftVersion.getName().compareTo(rightVersion.getName());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Secret> getSecrets();
|
||||
|
||||
String getSecretValue(Secret secret, Comparator<SecretVersion> comparator);
|
||||
|
||||
String getSecretName(Secret secret);
|
||||
|
||||
Boolean checkRemotePermissions();
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Secret> 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<Secret> result = new ArrayList<Secret>();
|
||||
pagedListSecretResponse.iterateAll().forEach(result::add);
|
||||
|
||||
// List all secrets.
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<SecretVersion> 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<SecretVersion> result = new ArrayList<SecretVersion>();
|
||||
pagedListVersionResponse.iterateAll().forEach(result::add);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSecretValue(Secret secret, Comparator<SecretVersion> comparator) {
|
||||
String result = null;
|
||||
List<SecretVersion> 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<String> 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<String> entity = new HttpEntity<String>("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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<HttpServletRequest> httpRequest;
|
||||
|
||||
public HttpHeaderGoogleConfigProvider(ObjectProvider<HttpServletRequest> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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<Secret> secrets = new ArrayList<Secret>();
|
||||
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<SecretVersion> secrets = new ArrayList<SecretVersion>();
|
||||
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<AccessSecretVersionRequest> matcher = new ArgumentMatcher<AccessSecretVersionRequest>() {
|
||||
@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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
mock-maker-inline
|
||||
Reference in New Issue
Block a user