Skip to content
This repository has been archived by the owner on Jan 19, 2022. It is now read-only.

Add initial secret manager property source implementation #2168

Merged
merged 9 commits into from
Feb 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions spring-cloud-gcp-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,13 @@
<optional>true</optional>
</dependency>

<!-- Secret Manager -->
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-secretmanager</artifactId>
<optional>true</optional>
</dependency>

<!-- Config -->
<dependency>
<groupId>org.springframework.cloud</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2017-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.gcp.autoconfigure.secretmanager;

import java.io.IOException;

import com.google.api.gax.core.CredentialsProvider;
import com.google.cloud.secretmanager.v1beta1.SecretManagerServiceClient;
import com.google.cloud.secretmanager.v1beta1.SecretManagerServiceSettings;
import com.google.protobuf.ByteString;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.cloud.gcp.core.DefaultCredentialsProvider;
import org.springframework.cloud.gcp.core.DefaultGcpProjectIdProvider;
import org.springframework.cloud.gcp.core.GcpProjectIdProvider;
import org.springframework.cloud.gcp.core.UserAgentHeaderProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.env.ConfigurableEnvironment;

/**
* Bootstrap Autoconfiguration for GCP Secret Manager which enables loading secrets as
* properties into the application {@link org.springframework.core.env.Environment}.
*
* @author Daniel Zou
* @since 1.3
*/
@Configuration
@EnableConfigurationProperties(GcpSecretManagerProperties.class)
@ConditionalOnClass(SecretManagerServiceClient.class)
@ConditionalOnProperty(value = "spring.cloud.gcp.secretmanager.enabled", matchIfMissing = true)
public class GcpSecretManagerBootstrapConfiguration {
dzou marked this conversation as resolved.
Show resolved Hide resolved

private final GcpSecretManagerProperties properties;

private final CredentialsProvider credentialsProvider;

private final GcpProjectIdProvider gcpProjectIdProvider;

public GcpSecretManagerBootstrapConfiguration(
GcpSecretManagerProperties properties,
ConfigurableEnvironment configurableEnvironment) throws IOException {

this.properties = properties;
this.credentialsProvider = new DefaultCredentialsProvider(properties);
this.gcpProjectIdProvider = properties.getProjectId() != null
? properties::getProjectId
: new DefaultGcpProjectIdProvider();

// Registers {@link ByteString} type converters to convert to String and byte[].
configurableEnvironment.getConversionService().addConverter(
new Converter<ByteString, String>() {
@Override
public String convert(ByteString source) {
return source.toStringUtf8();
}
});

configurableEnvironment.getConversionService().addConverter(
new Converter<ByteString, byte[]>() {
@Override
public byte[] convert(ByteString source) {
return source.toByteArray();
}
});
}

@Bean
@ConditionalOnMissingBean
public SecretManagerServiceClient secretManagerClient() throws IOException {
SecretManagerServiceSettings settings = SecretManagerServiceSettings.newBuilder()
.setCredentialsProvider(this.credentialsProvider)
.setHeaderProvider(new UserAgentHeaderProvider(GcpSecretManagerBootstrapConfiguration.class))
.build();

return SecretManagerServiceClient.create(settings);
}

@Bean
public PropertySourceLocator secretManagerPropertySourceLocator(SecretManagerServiceClient client) {
return new SecretManagerPropertySourceLocator(
client, this.gcpProjectIdProvider, this.properties.getSecretPropertyNamespace());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2017-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.gcp.autoconfigure.secretmanager;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.cloud.gcp.core.Credentials;
import org.springframework.cloud.gcp.core.CredentialsSupplier;
import org.springframework.cloud.gcp.core.GcpScope;

@ConfigurationProperties("spring.cloud.gcp.secretmanager")
public class GcpSecretManagerProperties implements CredentialsSupplier {

/**
* Overrides the GCP OAuth2 credentials specified in the Core module.
*/
@NestedConfigurationProperty
private final Credentials credentials = new Credentials(GcpScope.CLOUD_PLATFORM.getUrl());

/**
* Overrides the GCP Project ID specified in the Core module.
*/
private String projectId;

/**
* Defines a prefix String that will be prepended to the environment property names
* of secrets in Secret Manager.
*/
private String secretPropertyNamespace = "";

public Credentials getCredentials() {
return credentials;
}

public String getProjectId() {
return projectId;
}

public void setProjectId(String projectId) {
this.projectId = projectId;
}

public String getSecretPropertyNamespace() {
return secretPropertyNamespace;
}

public void setSecretPropertyNamespace(String secretPropertyNamespace) {
this.secretPropertyNamespace = secretPropertyNamespace;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2017-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.gcp.autoconfigure.secretmanager;

import java.util.HashMap;
import java.util.Map;

import com.google.cloud.secretmanager.v1beta1.AccessSecretVersionResponse;
import com.google.cloud.secretmanager.v1beta1.ProjectName;
import com.google.cloud.secretmanager.v1beta1.Secret;
import com.google.cloud.secretmanager.v1beta1.SecretManagerServiceClient;
import com.google.cloud.secretmanager.v1beta1.SecretManagerServiceClient.ListSecretsPagedResponse;
import com.google.cloud.secretmanager.v1beta1.SecretVersionName;
import com.google.protobuf.ByteString;

import org.springframework.cloud.gcp.core.GcpProjectIdProvider;
import org.springframework.core.env.EnumerablePropertySource;

/**
* Retrieves secrets from GCP Secret Manager under the current GCP project.
*
* @author Daniel Zou
* @since 1.3
*/
public class SecretManagerPropertySource extends EnumerablePropertySource<SecretManagerServiceClient> {
dzou marked this conversation as resolved.
Show resolved Hide resolved

private static final String LATEST_VERSION_STRING = "latest";

private final Map<String, Object> properties;

private final String[] propertyNames;

public SecretManagerPropertySource(
String propertySourceName,
SecretManagerServiceClient client,
GcpProjectIdProvider projectIdProvider,
String secretsNamespace) {

super(propertySourceName, client);

Map<String, Object> propertiesMap = createSecretsPropertiesMap(
client, projectIdProvider.getProjectId(), secretsNamespace);

this.properties = propertiesMap;
this.propertyNames = propertiesMap.keySet().toArray(new String[propertiesMap.size()]);
}

@Override
public String[] getPropertyNames() {
return propertyNames;
}

@Override
public Object getProperty(String name) {
return properties.get(name);
}

private static Map<String, Object> createSecretsPropertiesMap(
SecretManagerServiceClient client, String projectId, String secretsNamespace) {

ListSecretsPagedResponse response = client.listSecrets(ProjectName.of(projectId));

HashMap<String, Object> secretsMap = new HashMap<>();
for (Secret secret : response.iterateAll()) {
String secretId = extractSecretId(secret);
ByteString secretPayload = getSecretPayload(client, projectId, secretId);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, but a potential future optimization would be to parallelize this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted, added to #2176 to track.

secretsMap.put(secretsNamespace + secretId, secretPayload);
}

return secretsMap;
}

private static ByteString getSecretPayload(
SecretManagerServiceClient client, String projectId, String secretId) {

SecretVersionName secretVersionName = SecretVersionName.newBuilder()
.setProject(projectId)
.setSecret(secretId)
.setSecretVersion(LATEST_VERSION_STRING)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think people will want to access specific secret versions. Defaulting to "latest" is not a production best practice.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good point, we'll definitely need an answer for this. I think we'll have to discuss a bit more in-depth with our team what is the best way to let users specify which secret versions they want to load into the environment; I guess we wouldn't want to load all versions of all secrets by default.

I added this point to #2176 to track; we will definitely implement this before releasing to users. I just want this PR to setup the initial scaffolding.

.build();

AccessSecretVersionResponse response = client.accessSecretVersion(secretVersionName);
return response.getPayload().getData();
}

/**
* Extracts the Secret ID from the {@link Secret}. The secret ID refers to the unique ID
* given to the secret when it is saved under a GCP project.
*
* <p>
* The secret ID is extracted from the full secret name of the form:
* projects/${PROJECT_ID}/secrets/${SECRET_ID}
*/
private static String extractSecretId(Secret secret) {
dzou marked this conversation as resolved.
Show resolved Hide resolved
String[] secretNameTokens = secret.getName().split("/");
return secretNameTokens[secretNameTokens.length - 1];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2017-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.gcp.autoconfigure.secretmanager;

import com.google.cloud.secretmanager.v1beta1.SecretManagerServiceClient;

import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.cloud.gcp.core.GcpProjectIdProvider;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;

/**
* Implementation of {@link PropertySourceLocator} which provides GCP Secret Manager as a
* property source.
*
* @author Daniel Zou
* @since 1.3
*/
public class SecretManagerPropertySourceLocator implements PropertySourceLocator {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it makes sense to put this class into a new spring-cloud-gcp-secretmanager module. Then we could give people a single-dependency starter instead of having them bring in the autoconfiguration module combined with explicitly setting a property.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it just be conditional on class of the secret manager api rather than setting a property?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor Author

@dzou dzou Feb 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, we can discuss moving this in the next PR with the addition of secret template which will necessitate creating a the new secretmanager module. For now I am keeping the door open if we decide not to add a template class then this will mirror how our spring config support is organized (in which all the relevant classes are kept in autoconfigure/config).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a tracking issue to add a template?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is tracked in #2176


private static final String SECRET_MANAGER_NAME = "spring-cloud-gcp-secret-manager";

private final SecretManagerServiceClient client;

private final GcpProjectIdProvider projectIdProvider;

private final String secretsNamespace;

SecretManagerPropertySourceLocator(
SecretManagerServiceClient client,
GcpProjectIdProvider projectIdProvider,
String secretsNamespace) {
this.client = client;
this.projectIdProvider = projectIdProvider;
this.secretsNamespace = secretsNamespace;
}

@Override
public PropertySource<?> locate(Environment environment) {
return new SecretManagerPropertySource(
SECRET_MANAGER_NAME,
this.client,
this.projectIdProvider,
this.secretsNamespace);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* Auto-configuration for Spring Cloud GCP Secret Manager module.
*/
package org.springframework.cloud.gcp.autoconfigure.secretmanager;
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ org.springframework.cloud.gcp.autoconfigure.datastore.DatastoreTransactionManage
org.springframework.cloud.gcp.autoconfigure.firestore.FirestoreRepositoriesAutoConfiguration

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.gcp.autoconfigure.config.GcpConfigBootstrapConfiguration
org.springframework.cloud.gcp.autoconfigure.config.GcpConfigBootstrapConfiguration,\
org.springframework.cloud.gcp.autoconfigure.secretmanager.GcpSecretManagerBootstrapConfiguration
Loading