Adds support for google secret manager in spring cloud config server

Fixes gh-1628
This commit is contained in:
Jose Maria Alvarez
2020-03-13 09:30:43 +01:00
committed by spencergibb
parent fa21f2d02b
commit 64a28c476d
16 changed files with 916 additions and 3 deletions

1
.gitignore vendored
View File

@@ -20,4 +20,5 @@ _site/
.factorypath
.vscode/
.flattened-pom.xml
IntelliJ_Spring_Boot_Java_Conventions.xml
.sdkmanrc

10
pom.xml
View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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());
}
}

View File

@@ -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();
}

View File

@@ -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());
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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:

View File

@@ -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");
}
}