diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index 029349bb83..25723b3a77 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -80,7 +80,7 @@ |spring.cloud.gcp.pubsub.subscriber.retry.retry-delay-multiplier | | RetryDelayMultiplier controls the change in retry delay. The retry delay of the previous call is multiplied by the RetryDelayMultiplier to calculate the retry delay for the next call. |spring.cloud.gcp.pubsub.subscriber.retry.rpc-timeout-multiplier | | RpcTimeoutMultiplier controls the change in RPC timeout. The timeout of the previous call is multiplied by the RpcTimeoutMultiplier to calculate the timeout for the next call. |spring.cloud.gcp.pubsub.subscriber.retry.total-timeout-seconds | | TotalTimeout has ultimate control over how long the logic should keep trying the remote call until it gives up completely. The higher the total timeout, the more retries can be attempted. -|spring.cloud.gcp.secretmanager.bootstrap.enabled | true | Auto-configure GCP Secret Manager support components. +|spring.cloud.gcp.secretmanager.enabled | true | Auto-configure GCP Secret Manager support components. |spring.cloud.gcp.secretmanager.credentials.encoded-key | | |spring.cloud.gcp.secretmanager.credentials.location | | |spring.cloud.gcp.secretmanager.credentials.scopes | | diff --git a/docs/src/main/asciidoc/secretmanager.adoc b/docs/src/main/asciidoc/secretmanager.adoc index 25be4d8692..18f88fd6a6 100644 --- a/docs/src/main/asciidoc/secretmanager.adoc +++ b/docs/src/main/asciidoc/secretmanager.adoc @@ -5,7 +5,7 @@ A detailed summary of its features can be found in the https://cloud.google.com/ Spring Cloud GCP provides: -* A Spring Boot starter which automatically loads the secrets of your GCP project into your application context as a https://cloud.spring.io/spring-cloud-commons/multi/multi__spring_cloud_context_application_context_services.html#_the_bootstrap_application_context[Bootstrap Property Source]. +* A property source which allows you to specify and load the secrets of your GCP project into your application context as a https://cloud.spring.io/spring-cloud-commons/multi/multi__spring_cloud_context_application_context_services.html#_the_bootstrap_application_context[Bootstrap Property Source]. * A `SecretManagerTemplate` which allows you to read, write, and update secrets in Secret Manager. === Dependency Setup @@ -31,34 +31,68 @@ dependencies { } ---- +==== Configuration + +By default, Spring Cloud GCP Secret Manager will authenticate using Application Default Credentials. +This can be overridden using the authentication properties. + +NOTE: All of the below settings must be specified in a https://cloud.spring.io/spring-cloud-commons/multi/multi__spring_cloud_context_application_context_services.html#_the_bootstrap_application_context[`bootstrap.properties`] (or `bootstrap.yaml`) file which is the properties file used to configure settings for bootstrap-phase Spring configuration. + +|=== +| Name | Description | Required | Default value +| `spring.cloud.gcp.secretmanager.enabled` | Enables the Secret Manager bootstrap property and template configuration. | No | `true` +| `spring.cloud.gcp.secretmanager.credentials.location` | OAuth2 credentials for authenticating to the Google Cloud Secret Manager API. | No | By default, infers credentials from https://cloud.google.com/docs/authentication/production[Application Default Credentials]. +| `spring.cloud.gcp.secretmanager.credentials.encoded-key` | Base64-encoded contents of OAuth2 account private key for authenticating to the Google Cloud Secret Manager API. | No | By default, infers credentials from https://cloud.google.com/docs/authentication/production[Application Default Credentials]. +| `spring.cloud.gcp.secretmanager.project-id` | The default GCP Project used to access Secret Manager API for the template and property source. | No | By default, infers the project from https://cloud.google.com/docs/authentication/production[Application Default Credentials]. +|=== + === Secret Manager Property Source The Spring Cloud GCP integration for Google Cloud Secret Manager enables you to use Secret Manager as a bootstrap property source. -This feature allows you to automatically load your GCP project's secrets as properties into the application context during the https://cloud.spring.io/spring-cloud-commons/reference/html/#the-bootstrap-application-context[Bootstrap Phase], which refers to the initial phase when a Spring application is being loaded. +This allows you to specify and load secrets from Google Cloud Secret Manager as properties into the application context during the https://cloud.spring.io/spring-cloud-commons/reference/html/#the-bootstrap-application-context[Bootstrap Phase], which refers to the initial phase when a Spring application is being loaded. -NOTE: This feature disabled by default; to use it, you must set `spring.cloud.gcp.secretmanager.bootstrap.enabled` to **true** in your `bootstrap.properties` file. +The Secret Manager property source uses the following syntax to specify secrets: -Spring Cloud GCP will load the **latest** version of each secret into the application context. +[source] +---- +# 1. Long form - specify the project ID, secret ID, and version +sm://projects//secrets//versions/} -All secrets will be loaded into the application environment using their `secretId` as the property name. -For example, if your secret's id is `my-secret` then it will be accessible as a property using the name `my-secret`. -If you would like to append a prefix string to all property names imported from Secret Manager, you may use the `spring.cloud.gcp.secretmanager.secret-name-prefix` setting described below. +# 2. Long form - specify project ID, secret ID, and use latest version +sm://projects//secrets/ -==== Configuration (Bootstrap) +# 3. Short form - specify project ID, secret ID, and version +sm://// -Spring Cloud GCP Secret Manager offers several bootstrap configuration properties to customize the behavior. +# 4. Short form - default project; specify secret + version +# +# The project is inferred from the spring.cloud.gcp.secretmanager.project-id setting +# in your bootstrap.properties (see Configuration) or from application-default credentials if +# this is not set. +sm:/// -NOTE: All of the below settings must be specified in a https://cloud.spring.io/spring-cloud-commons/multi/multi__spring_cloud_context_application_context_services.html#_the_bootstrap_application_context[`bootstrap.properties`] (or `bootstrap.yaml`) file which is the properties file used to configure settings for bootstrap-phase Spring configuration. +# 5. Shortest form - specify secret ID, use default project and latest version. +sm:// +---- -|=== -| Name | Description | Required | Default value -| `spring.cloud.gcp.secretmanager.bootstrap.enabled` | Enables loading secrets from Secret Manager as a bootstrap property source. Set this to **true** to enable the feature. | No | `false` -| `spring.cloud.gcp.secretmanager.secret-name-prefix` | A prefix string to prepend to the property names of secrets read from Secret Manager | No | "" (empty string) -| `spring.cloud.gcp.secretmanager.versions.` | Defines a version for a specific secret-id to read from Secret Manager instead of using the latest version. | No | "" (empty string) -|=== +You can use this syntax in the following places: + +1. In your `application.properties` or `bootstrap.properties` files: ++ +[source] +---- +# Example of the project-secret long-form syntax. +spring.datasource.password=${sm://projects/my-gcp-project/secrets/my-secret} +---- -See the Authentication Settings section below for information on how to set properties to authenticate to Secret Manager. +2. Access the value using the `@Value` annotation. ++ +[source] +---- +// Example of using shortest form syntax. +@Value("${sm://my-secret}") +---- === Secret Manager Template @@ -74,20 +108,6 @@ private SecretManagerTemplate secretManagerTemplate; Please consult https://github.com/spring-cloud/spring-cloud-gcp/blob/master/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerOperations.java[`SecretManagerOperations`] for information on what operations are available for the Secret Manager template. -==== Configuration - -The auto-configured `SecretManagerTemplate` bean can be customized using configuration properties. -By default, Spring Cloud GCP Secret Manager will authenticate using Application Default Credentials. -This can be overridden using the authentication properties. - -|=== -| Name | Description | Required | Default value -| `spring.cloud.gcp.secretmanager.enabled` | Enables the autowiring of the `SecretManagerTemplate` in the application context. | No | `true` -| `spring.cloud.gcp.secretmanager.credentials.location` | OAuth2 credentials for authenticating to the Google Cloud Secret Manager API. | No | By default, infers credentials from https://cloud.google.com/docs/authentication/production[Application Default Credentials]. -| `spring.cloud.gcp.secretmanager.credentials.encoded-key` | Base64-encoded contents of OAuth2 account private key for authenticating to the Google Cloud Secret Manager API. | No | By default, infers credentials from https://cloud.google.com/docs/authentication/production[Application Default Credentials]. -| `spring.cloud.gcp.secretmanager.project-id` | The GCP Project used to access Secret Manager API. | No | By default, infers the project from https://cloud.google.com/docs/authentication/production[Application Default Credentials]. -|=== - === Sample A https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample[Secret Manager Sample Application] is provided which demonstrates basic property source loading and usage of the template class. diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/GcpSecretManagerAutoConfiguration.java b/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/GcpSecretManagerAutoConfiguration.java deleted file mode 100644 index b33d6309b7..0000000000 --- a/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/GcpSecretManagerAutoConfiguration.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 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.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.cloud.gcp.secretmanager.SecretManagerTemplate; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -/** - * Autoconfiguration for GCP Secret Manager which provides an instance of the - * {@link SecretManagerTemplate}. - * - * @author Daniel Zou - * @since 1.2.2 - */ -@Configuration -@EnableConfigurationProperties(GcpSecretManagerProperties.class) -@ConditionalOnClass(SecretManagerServiceClient.class) -@ConditionalOnProperty(value = "spring.cloud.gcp.secretmanager.enabled", matchIfMissing = true) -public class GcpSecretManagerAutoConfiguration { - - private final GcpSecretManagerProperties properties; - - private final CredentialsProvider credentialsProvider; - - private final GcpProjectIdProvider gcpProjectIdProvider; - - public GcpSecretManagerAutoConfiguration( - GcpSecretManagerProperties properties) throws IOException { - this.properties = properties; - this.credentialsProvider = new DefaultCredentialsProvider(properties); - this.gcpProjectIdProvider = properties.getProjectId() != null - ? properties::getProjectId - : new DefaultGcpProjectIdProvider(); - } - - @Bean - @ConditionalOnMissingBean - public SecretManagerServiceClient secretManagerClient() throws IOException { - SecretManagerServiceSettings settings = SecretManagerServiceSettings.newBuilder() - .setCredentialsProvider(this.credentialsProvider) - .setHeaderProvider( - new UserAgentHeaderProvider(GcpSecretManagerAutoConfiguration.class)) - .build(); - - return SecretManagerServiceClient.create(settings); - } - - @Bean - @ConditionalOnMissingBean - public SecretManagerTemplate secretManagerTemplate(SecretManagerServiceClient client) { - return new SecretManagerTemplate(client, this.gcpProjectIdProvider); - } -} diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/GcpSecretManagerBootstrapConfiguration.java b/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/GcpSecretManagerBootstrapConfiguration.java index 423536b53e..ad00879251 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/GcpSecretManagerBootstrapConfiguration.java +++ b/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/GcpSecretManagerBootstrapConfiguration.java @@ -32,6 +32,8 @@ import org.springframework.cloud.gcp.core.DefaultGcpProjectIdProvider; import org.springframework.cloud.gcp.core.GcpProjectIdProvider; import org.springframework.cloud.gcp.core.UserAgentHeaderProvider; +import org.springframework.cloud.gcp.secretmanager.SecretManagerPropertySourceLocator; +import org.springframework.cloud.gcp.secretmanager.SecretManagerTemplate; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; @@ -48,11 +50,9 @@ @Configuration @EnableConfigurationProperties(GcpSecretManagerProperties.class) @ConditionalOnClass(SecretManagerServiceClient.class) -@ConditionalOnProperty("spring.cloud.gcp.secretmanager.bootstrap.enabled") +@ConditionalOnProperty(value = "spring.cloud.gcp.secretmanager.enabled", matchIfMissing = true) public class GcpSecretManagerBootstrapConfiguration { - private final GcpSecretManagerProperties properties; - private final CredentialsProvider credentialsProvider; private final GcpProjectIdProvider gcpProjectIdProvider; @@ -61,7 +61,6 @@ public GcpSecretManagerBootstrapConfiguration( GcpSecretManagerProperties properties, ConfigurableEnvironment configurableEnvironment) throws IOException { - this.properties = properties; this.credentialsProvider = new DefaultCredentialsProvider(properties); this.gcpProjectIdProvider = properties.getProjectId() != null ? properties::getProjectId @@ -97,10 +96,17 @@ public SecretManagerServiceClient secretManagerClient() throws IOException { } @Bean - public PropertySourceLocator secretManagerPropertySourceLocator(SecretManagerServiceClient client) { - SecretManagerPropertySourceLocator propertySourceLocator = new SecretManagerPropertySourceLocator( - client, this.gcpProjectIdProvider, this.properties.getSecretNamePrefix()); - propertySourceLocator.setVersions(this.properties.getVersions()); + @ConditionalOnMissingBean + public SecretManagerTemplate secretManagerTemplate(SecretManagerServiceClient client) { + return new SecretManagerTemplate(client, this.gcpProjectIdProvider); + } + + @Bean + @ConditionalOnMissingBean + public PropertySourceLocator secretManagerPropertySourceLocator( + SecretManagerTemplate secretManagerTemplate) { + SecretManagerPropertySourceLocator propertySourceLocator = + new SecretManagerPropertySourceLocator(secretManagerTemplate, this.gcpProjectIdProvider); return propertySourceLocator; } } diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/GcpSecretManagerProperties.java b/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/GcpSecretManagerProperties.java index 295904cdc8..beb1f02f85 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/GcpSecretManagerProperties.java +++ b/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/GcpSecretManagerProperties.java @@ -16,9 +16,6 @@ package org.springframework.cloud.gcp.autoconfigure.secretmanager; -import java.util.HashMap; -import java.util.Map; - import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.cloud.gcp.core.Credentials; @@ -39,17 +36,6 @@ public class GcpSecretManagerProperties implements CredentialsSupplier { */ private String projectId; - /** - * Defines a prefix String that will be prepended to the environment property names - * of secrets in Secret Manager. - */ - private String secretNamePrefix = ""; - - /** - * Defines versions for specific secret-ids. - */ - private Map versions = new HashMap<>(); - public Credentials getCredentials() { return credentials; } @@ -61,20 +47,4 @@ public String getProjectId() { public void setProjectId(String projectId) { this.projectId = projectId; } - - public String getSecretNamePrefix() { - return secretNamePrefix; - } - - public void setSecretNamePrefix(String secretNamePrefix) { - this.secretNamePrefix = secretNamePrefix; - } - - public Map getVersions() { - return versions; - } - - public void setVersions(Map versions) { - this.versions = versions; - } } diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/SecretManagerPropertySource.java b/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/SecretManagerPropertySource.java deleted file mode 100644 index 4c20ea22c0..0000000000 --- a/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/SecretManagerPropertySource.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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.Collections; -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.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -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 - * @author Eddú Meléndez - * @since 1.2.2 - */ -public class SecretManagerPropertySource extends EnumerablePropertySource { - - private static final Log LOGGER = LogFactory.getLog(SecretManagerPropertySource.class); - - private static final String LATEST_VERSION_STRING = "latest"; - - private final Map properties; - - private final String[] propertyNames; - - public SecretManagerPropertySource( - String propertySourceName, - SecretManagerServiceClient client, - GcpProjectIdProvider projectIdProvider, - String secretsPrefix) { - this(propertySourceName, client, projectIdProvider, secretsPrefix, Collections.EMPTY_MAP); - } - - public SecretManagerPropertySource( - String propertySourceName, - SecretManagerServiceClient client, - GcpProjectIdProvider projectIdProvider, - String secretsPrefix, - Map versions) { - super(propertySourceName, client); - - Map propertiesMap = createSecretsPropertiesMap( - client, projectIdProvider.getProjectId(), secretsPrefix, versions); - - 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 createSecretsPropertiesMap( - SecretManagerServiceClient client, String projectId, String secretsPrefix, Map versions) { - - ListSecretsPagedResponse response = client.listSecrets(ProjectName.of(projectId)); - Map secretsMap = new HashMap<>(); - for (Secret secret : response.iterateAll()) { - String secretId = extractSecretId(secret); - ByteString secretPayload = getSecretPayload(client, projectId, secretId, versions); - if (secretPayload != null) { - secretsMap.put(secretsPrefix + secretId, secretPayload); - } - } - - return secretsMap; - } - - private static ByteString getSecretPayload( - SecretManagerServiceClient client, - String projectId, - String secretId, - Map versions) { - - String version = versions.containsKey(secretId) ? versions.get(secretId) : LATEST_VERSION_STRING; - - SecretVersionName secretVersionName = SecretVersionName.newBuilder() - .setProject(projectId) - .setSecret(secretId) - .setSecretVersion(version) - .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. - * - *

- * 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) { - String[] secretNameTokens = secret.getName().split("/"); - return secretNameTokens[secretNameTokens.length - 1]; - } -} diff --git a/spring-cloud-gcp-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-gcp-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 1bb5c7e46c..a620c9de5a 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-gcp-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -55,7 +55,7 @@ "defaultValue": true }, { - "name": "spring.cloud.gcp.secretmanager.bootstrap.enabled", + "name": "spring.cloud.gcp.secretmanager.enabled", "type": "java.lang.Boolean", "description": "Auto-configure GCP Secret Manager support components.", "defaultValue": true diff --git a/spring-cloud-gcp-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-cloud-gcp-autoconfigure/src/main/resources/META-INF/spring.factories index 67ab215a72..66425f4def 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-gcp-autoconfigure/src/main/resources/META-INF/spring.factories @@ -20,8 +20,7 @@ org.springframework.cloud.gcp.autoconfigure.datastore.GcpDatastoreEmulatorAutoCo org.springframework.cloud.gcp.autoconfigure.bigquery.GcpBigQueryAutoConfiguration,\ org.springframework.cloud.gcp.autoconfigure.datastore.DatastoreTransactionManagerAutoConfiguration,\ org.springframework.cloud.gcp.autoconfigure.firestore.FirestoreRepositoriesAutoConfiguration,\ -org.springframework.cloud.gcp.autoconfigure.pubsub.health.PubSubHealthIndicatorAutoConfiguration,\ -org.springframework.cloud.gcp.autoconfigure.secretmanager.GcpSecretManagerAutoConfiguration +org.springframework.cloud.gcp.autoconfigure.pubsub.health.PubSubHealthIndicatorAutoConfiguration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.gcp.autoconfigure.config.GcpConfigBootstrapConfiguration,\ diff --git a/spring-cloud-gcp-autoconfigure/src/test/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/it/SecretManagerIntegrationTests.java b/spring-cloud-gcp-autoconfigure/src/test/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/it/SecretManagerIntegrationTests.java index 29c5b92fb2..2842d4be8a 100644 --- a/spring-cloud-gcp-autoconfigure/src/test/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/it/SecretManagerIntegrationTests.java +++ b/spring-cloud-gcp-autoconfigure/src/test/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/it/SecretManagerIntegrationTests.java @@ -17,7 +17,6 @@ package org.springframework.cloud.gcp.autoconfigure.secretmanager.it; import java.util.concurrent.TimeUnit; -import java.util.stream.StreamSupport; import com.google.api.gax.rpc.NotFoundException; import com.google.cloud.secretmanager.v1beta1.AddSecretVersionRequest; @@ -26,9 +25,9 @@ import com.google.cloud.secretmanager.v1beta1.Replication; 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.SecretName; import com.google.cloud.secretmanager.v1beta1.SecretPayload; +import com.google.cloud.secretmanager.v1beta1.SecretVersionName; import com.google.protobuf.ByteString; import io.grpc.StatusRuntimeException; import org.apache.commons.logging.Log; @@ -41,7 +40,6 @@ import org.springframework.boot.WebApplicationType; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.gcp.autoconfigure.core.GcpContextAutoConfiguration; -import org.springframework.cloud.gcp.autoconfigure.secretmanager.GcpSecretManagerAutoConfiguration; import org.springframework.cloud.gcp.autoconfigure.secretmanager.GcpSecretManagerBootstrapConfiguration; import org.springframework.cloud.gcp.core.GcpProjectIdProvider; import org.springframework.context.ConfigurableApplicationContext; @@ -74,7 +72,7 @@ public static void prepare() { @Before public void setupSecretManager() { ConfigurableApplicationContext context = new SpringApplicationBuilder() - .sources(GcpContextAutoConfiguration.class, GcpSecretManagerAutoConfiguration.class) + .sources(GcpContextAutoConfiguration.class, GcpSecretManagerBootstrapConfiguration.class) .web(WebApplicationType.NONE) .run(); @@ -93,13 +91,13 @@ public void testConfiguration() { ConfigurableApplicationContext context = new SpringApplicationBuilder() .sources(GcpContextAutoConfiguration.class, GcpSecretManagerBootstrapConfiguration.class) .web(WebApplicationType.NONE) - .properties("spring.cloud.gcp.secretmanager.bootstrap.enabled=true") .run(); - assertThat(context.getEnvironment().getProperty(TEST_SECRET_ID)) + assertThat(context.getEnvironment().getProperty("sm://" + TEST_SECRET_ID)) .isEqualTo("the secret data."); - byte[] byteArraySecret = context.getEnvironment().getProperty(TEST_SECRET_ID, byte[].class); + byte[] byteArraySecret = context.getEnvironment().getProperty( + "sm://" + TEST_SECRET_ID + "/latest", byte[].class); assertThat(byteArraySecret).isEqualTo("the secret data.".getBytes()); } @@ -113,7 +111,8 @@ public void testConfigurationDisabled() { .properties("spring.cloud.gcp.secretmanager.enabled=false") .run(); - assertThat(context.getEnvironment().getProperty(TEST_SECRET_ID, String.class)).isNull(); + assertThat(context.getEnvironment().getProperty( + "sm://" + TEST_SECRET_ID, String.class)).isNull(); } @Test @@ -125,26 +124,26 @@ public void testSecretsWithSpecificVersion() { ConfigurableApplicationContext context = new SpringApplicationBuilder() .sources(GcpContextAutoConfiguration.class, GcpSecretManagerBootstrapConfiguration.class) .web(WebApplicationType.NONE) - .properties("spring.cloud.gcp.secretmanager.bootstrap.enabled=true") - .properties("spring.cloud.gcp.secretmanager.versions." + VERSIONED_SECRET_ID + "=2") .run(); - String versionedSecret = context.getEnvironment().getProperty(VERSIONED_SECRET_ID, String.class); + String versionedSecret = context.getEnvironment().getProperty( + "sm://" + VERSIONED_SECRET_ID + "/2", String.class); assertThat(versionedSecret).isEqualTo("the secret data v2"); } @Test - public void testSecretsWithMissingVersion() { + public void testMissingSecret() { createSecret(VERSIONED_SECRET_ID, "the secret data"); - createSecret(VERSIONED_SECRET_ID, "the secret data v2"); - assertThatThrownBy(() -> new SpringApplicationBuilder() + ConfigurableApplicationContext context = new SpringApplicationBuilder() .sources(GcpContextAutoConfiguration.class, GcpSecretManagerBootstrapConfiguration.class) .web(WebApplicationType.NONE) - .properties("spring.cloud.gcp.secretmanager.bootstrap.enabled=true") - .properties("spring.cloud.gcp.secretmanager.versions." + VERSIONED_SECRET_ID + "=7") - .run()) - .hasCauseInstanceOf(StatusRuntimeException.class); + .run(); + + assertThatThrownBy(() -> + context.getEnvironment().getProperty("sm://" + VERSIONED_SECRET_ID + "/2", String.class)) + .hasCauseInstanceOf(StatusRuntimeException.class) + .hasMessageContaining("NOT_FOUND"); } /** * Creates the secret with the specified payload if the secret does not already exist. @@ -186,10 +185,20 @@ private void createSecretPayload(String secretId, String data) { } private boolean secretExists(String secretId) { - ProjectName projectName = ProjectName.of(projectIdProvider.getProjectId()); - ListSecretsPagedResponse listSecretsResponse = this.client.listSecrets(projectName); - return StreamSupport.stream(listSecretsResponse.iterateAll().spliterator(), false) - .anyMatch(secret -> secret.getName().contains(secretId)); + try { + SecretVersionName secretVersionName = + SecretVersionName.newBuilder() + .setProject(projectIdProvider.getProjectId()) + .setSecret(secretId) + .setSecretVersion("latest") + .build(); + this.client.accessSecretVersion(secretVersionName); + } + catch (NotFoundException e) { + return false; + } + + return true; } private void deleteSecret(String secretId) { diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/java/com/example/SecretManagerWebController.java b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/java/com/example/SecretManagerWebController.java index 5620d44332..b69ac0fdb4 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/java/com/example/SecretManagerWebController.java +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/java/com/example/SecretManagerWebController.java @@ -45,17 +45,18 @@ public class SecretManagerWebController { // Application secrets can be accessed using @Value and passing in the secret name. // Note that the secret name is prefixed with "secrets" because of the prefix setting in // bootstrap.properties. - @Value("${secrets.application-secret}") - private String applicationSecretValue; + @Value("${sm://application-secret}") + private String appSecret; - // Application secret is set into the properties file and get here using @Value - @Value("${my-application-secret}") - private String myApplicationSecretValue; + // Multiple ways of loading the application-secret are demonstrated in bootstrap.properties. + // Try it with my-app-secret-1 or my-app-secret-2 + @Value("${my-app-secret-1}") + private String myAppSecret; @GetMapping("/") public ModelAndView renderIndex(ModelMap map) { - map.put("applicationSecret", this.applicationSecretValue); - map.put("myApplicationSecret", this.myApplicationSecretValue); + map.put("applicationSecret", this.appSecret); + map.put("myApplicationSecret", this.myAppSecret); return new ModelAndView("index.html", map); } @@ -73,11 +74,12 @@ public String getSecret( String secretPayload; if (StringUtils.isEmpty(projectId)) { - secretPayload = this.secretManagerTemplate.getSecretString(secretId, version); + secretPayload = this.secretManagerTemplate.getSecretString( + "sm://" + secretId + "/" + version); } else { - secretPayload = - this.secretManagerTemplate.getSecretByteString(secretId, version, projectId).toStringUtf8(); + secretPayload = this.secretManagerTemplate.getSecretString( + "sm://" + projectId + "/" + secretId + "/" + version); } return "Secret ID: " + secretId + " | Value: " + secretPayload @@ -98,8 +100,8 @@ public ModelAndView createSecret( this.secretManagerTemplate.createSecret(secretId, secretPayload.getBytes(), projectId); } - map.put("applicationSecret", this.applicationSecretValue); - map.put("myApplicationSecret", this.myApplicationSecretValue); + map.put("applicationSecret", this.appSecret); + map.put("myApplicationSecret", this.myAppSecret); map.put("message", "Secret created!"); return new ModelAndView("index.html", map); } diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/resources/bootstrap.properties b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/resources/bootstrap.properties index 34bc2eb902..fb404e9a67 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/resources/bootstrap.properties +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/main/resources/bootstrap.properties @@ -1,14 +1,15 @@ -# Set this setting to true if you want to load your GCP secrets as a -# property source for your application. -spring.cloud.gcp.secretmanager.bootstrap.enabled=true - -# This optional setting adds a prefix to your secret property names that get -# injected into the application Environment. -spring.cloud.gcp.secretmanager.secret-name-prefix=secrets. +# You can directly load the secret into a variable like this example +# This demonstrates multiple ways of loading the same application secret using template syntax. +# +# Please refer to the documentation for the protocol syntax. +# +# Examples: +# my-app-secret-3=${sm://projects//secrets/application-secret} +# my-app-secret-4=${sm://projects//secrets/application-secret/version/latest} +# my-app-secret-5=${sm:///application-secret/latest} -# You can specify the version of your secret like this example -# Pattern: spring.cloud.gcp.secretmanager.versions.= -#spring.cloud.gcp.secretmanager.versions.application-secret=1 +# Defaults to latest version if omitted. +my-app-secret-1=${sm://application-secret} -# You can directly load the secret into a variable like this example -my-application-secret=${secrets.application-secret} +# Use to specify explicit version (like version 1) under the default project. +my-app-secret-2=${sm://application-secret/1} diff --git a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/test/java/com/example/SecretManagerSampleTests.java b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/test/java/com/example/SecretManagerSampleTests.java index 7dc0f66160..9e56874e36 100644 --- a/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/test/java/com/example/SecretManagerSampleTests.java +++ b/spring-cloud-gcp-samples/spring-cloud-gcp-secretmanager-sample/src/test/java/com/example/SecretManagerSampleTests.java @@ -38,7 +38,7 @@ @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = SecretManagerApplication.class, - properties = {"spring.cloud.gcp.secretmanager.bootstrap.enabled=true"}) + properties = {"spring.cloud.gcp.secretmanager.enabled=true"}) public class SecretManagerSampleTests { @Autowired diff --git a/spring-cloud-gcp-secretmanager/pom.xml b/spring-cloud-gcp-secretmanager/pom.xml index 922731b3e0..a05289d2e7 100644 --- a/spring-cloud-gcp-secretmanager/pom.xml +++ b/spring-cloud-gcp-secretmanager/pom.xml @@ -16,6 +16,11 @@ + + org.springframework.cloud + spring-cloud-context + + org.springframework.cloud spring-cloud-gcp-core diff --git a/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerOperations.java b/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerOperations.java index 4bbcecda19..d82ece42de 100644 --- a/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerOperations.java +++ b/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerOperations.java @@ -16,11 +16,27 @@ package org.springframework.cloud.gcp.secretmanager; -import com.google.protobuf.ByteString; - /** * Describes supported operations that one can perform on the Secret Manager API. * + *

For some methods you may specify the secret from GCP Secret Manager by URI string. + * The following secret URI syntax is supported: + * + * 1. Long form - specify the project ID, secret ID, and version + * sm://projects/{project-id}/secrets/{secret-id}/versions/{version-id} + * + * 2. Long form - specify project ID, secret ID, and use latest version + * sm://projects/{project-id}/secrets/{secret-id} + * + * 3. Short form - specify project ID, secret ID, and version + * sm://{project-id}/{secret-id}/{version-id} + * + * 4. Short form - specify secret and version, use default GCP project configured + * sm://{secret-id}/{version} + * + * 5. Shortest form - specify secret ID, use default project and latest version. + * sm://{secret-id} + * * @author Daniel Zou * @since 1.2.2 */ @@ -70,65 +86,36 @@ public interface SecretManagerOperations { void createSecret(String secretId, byte[] payload, String projectId); /** - * Gets the secret payload of the specified {@code secretId} at the latest version. + * Gets the secret payload of the specified {@code secretIdentifier} secret. * - * @param secretId unique identifier of your secret in Secret Manager. - * @return The secret payload as String - */ - String getSecretString(String secretId); - - /** - * Gets the secret payload of the specified {@code secretId} at version - * {@code versionName}. + *

The {@code secretIdentifier} must either be a secret ID or a fully qualified + * `sm://` protocol string which specifies the secret (see javadocs of + * {@link SecretManagerOperations} for the protocol format). * - * @param secretId unique identifier of your secret in Secret Manager. - * @param versionName which version of the secret to load. The version can be a version - * number as a string (e.g. "5") or an alias (e.g. "latest"). - * @return The secret payload as String - */ - String getSecretString(String secretId, String versionName); - - /** - * Gets the secret payload of the specified {@code secretId} at the latest version. + * If the secret ID string is passed in, then this will return the payload of the secret for + * the default project at the latest version. * - * @param secretId unique identifier of your secret in Secret Manager. - * @return The secret payload as byte[] + * @param secretIdentifier the GCP secret ID of the secret or an sm:// formatted + * string specifying the secret. + * @return The secret payload as String */ - byte[] getSecretBytes(String secretId); + String getSecretString(String secretIdentifier); /** - * Gets the secret payload of the specified {@code secretId} at version - * {@code versionName}. + * Gets the secret payload of the specified {@code secretIdentifier} secret. * - * @param secretId unique identifier of your secret in Secret Manager. - * @param versionName which version of the secret to load. The version can be a version - * number as a string (e.g. "5") or an alias (e.g. "latest"). - * @return The secret payload as byte[] - */ - byte[] getSecretBytes(String secretId, String versionName); - - /** - * Gets the secret payload of the specified {@code secretId} at version - * {@code versionName}. + *

The {@code secretIdentifier} must either be a secret ID or a fully qualified + * `sm://` protocol string which specifies the secret (see javadocs of + * {@link SecretManagerOperations} for the protocol format). * - * @param secretId unique identifier of your secret in Secret Manager. - * @param versionName which version of the secret to load. The version can be a version - * number as a string (e.g. "5") or an alias (e.g. "latest"). - * @return The secret payload as {@link ByteString} - */ - ByteString getSecretByteString(String secretId, String versionName); - - /** - * Gets the secret payload of the specified {@code secretId} at version - * {@code versionName} for a specific {@code projectId}. + * If the secret ID string is passed in, then this will return the payload of the secret for + * the default project at the latest version. * - * @param secretId unique identifier of your secret in Secret Manager. - * @param versionName which version of the secret to load. The version can be a version - * number as a string (e.g. "5") or an alias (e.g. "latest"). - * @param projectId unique identifier of your project. - * @return The secret payload as {@link ByteString} + * @param secretIdentifier the GCP secret ID of the secret or an sm:// formatted + * string specifying the secret. + * @return The secret payload as byte array */ - ByteString getSecretByteString(String secretId, String versionName, String projectId); + byte[] getSecretBytes(String secretIdentifier); /** * Returns true if there already exists a secret under the GCP project with the diff --git a/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerPropertySource.java b/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerPropertySource.java new file mode 100644 index 0000000000..f5a32c70ba --- /dev/null +++ b/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerPropertySource.java @@ -0,0 +1,65 @@ +/* + * 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.secretmanager; + +import com.google.cloud.secretmanager.v1beta1.SecretVersionName; + +import org.springframework.cloud.gcp.core.GcpProjectIdProvider; +import org.springframework.core.env.EnumerablePropertySource; + +/** + * A property source for Secret Manager which accesses the Secret Manager APIs when {@link #getProperty} is called. + * + * @author Daniel Zou + * @author Eddú Meléndez + * @since 1.2.2 + */ +public class SecretManagerPropertySource extends EnumerablePropertySource { + + private final GcpProjectIdProvider projectIdProvider; + + public SecretManagerPropertySource( + String propertySourceName, + SecretManagerTemplate secretManagerTemplate, + GcpProjectIdProvider projectIdProvider) { + super(propertySourceName, secretManagerTemplate); + + this.projectIdProvider = projectIdProvider; + } + + @Override + public Object getProperty(String name) { + SecretVersionName secretIdentifier = + SecretManagerPropertyUtils.getSecretVersionName(name, this.projectIdProvider); + + if (secretIdentifier != null) { + return getSource().getSecretByteString(secretIdentifier); + } + else { + return null; + } + } + + /** + * The {@link SecretManagerPropertySource} is not enumerable, so this always returns an empty array. + * @return the empty array. + */ + @Override + public String[] getPropertyNames() { + return new String[0]; + } +} diff --git a/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/SecretManagerPropertySourceLocator.java b/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerPropertySourceLocator.java similarity index 68% rename from spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/SecretManagerPropertySourceLocator.java rename to spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerPropertySourceLocator.java index d15226c4e3..25c7ccad64 100644 --- a/spring-cloud-gcp-autoconfigure/src/main/java/org/springframework/cloud/gcp/autoconfigure/secretmanager/SecretManagerPropertySourceLocator.java +++ b/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerPropertySourceLocator.java @@ -14,11 +14,7 @@ * limitations under the License. */ -package org.springframework.cloud.gcp.autoconfigure.secretmanager; - -import java.util.Map; - -import com.google.cloud.secretmanager.v1beta1.SecretManagerServiceClient; +package org.springframework.cloud.gcp.secretmanager; import org.springframework.cloud.bootstrap.config.PropertySourceLocator; import org.springframework.cloud.gcp.core.GcpProjectIdProvider; @@ -37,34 +33,22 @@ public class SecretManagerPropertySourceLocator implements PropertySourceLocator private static final String SECRET_MANAGER_NAME = "spring-cloud-gcp-secret-manager"; - private final SecretManagerServiceClient client; + private final SecretManagerTemplate template; private final GcpProjectIdProvider projectIdProvider; - private final String secretsPrefix; - - private Map versions; - - SecretManagerPropertySourceLocator( - SecretManagerServiceClient client, - GcpProjectIdProvider projectIdProvider, - String secretsPrefix) { - this.client = client; + public SecretManagerPropertySourceLocator( + SecretManagerTemplate template, + GcpProjectIdProvider projectIdProvider) { + this.template = template; this.projectIdProvider = projectIdProvider; - this.secretsPrefix = secretsPrefix; - } - - public void setVersions(Map versions) { - this.versions = versions; } @Override public PropertySource locate(Environment environment) { return new SecretManagerPropertySource( SECRET_MANAGER_NAME, - this.client, - this.projectIdProvider, - this.secretsPrefix, - this.versions); + this.template, + this.projectIdProvider); } } diff --git a/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerPropertyUtils.java b/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerPropertyUtils.java new file mode 100644 index 0000000000..dec0ac6564 --- /dev/null +++ b/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerPropertyUtils.java @@ -0,0 +1,102 @@ +/* + * 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.secretmanager; + +import com.google.cloud.secretmanager.v1beta1.SecretVersionName; + +import org.springframework.cloud.gcp.core.GcpProjectIdProvider; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Utilities for parsing Secret Manager properties. + * + * @author Daniel Zou + */ +final class SecretManagerPropertyUtils { + + private static final String GCP_SECRET_PREFIX = "sm://"; + + private SecretManagerPropertyUtils() { } + + static SecretVersionName getSecretVersionName(String input, GcpProjectIdProvider projectIdProvider) { + if (!input.startsWith(GCP_SECRET_PREFIX)) { + return null; + } + + String resourcePath = input.substring(GCP_SECRET_PREFIX.length()); + String[] tokens = resourcePath.split("/"); + + String projectId = projectIdProvider.getProjectId(); + String secretId = null; + String version = "latest"; + + if (tokens.length == 1) { + // property is form "sm://" + secretId = tokens[0]; + } + else if (tokens.length == 2) { + // property is form "sm:///" + secretId = tokens[0]; + version = tokens[1]; + } + else if (tokens.length == 3) { + // property is form "sm:////" + projectId = tokens[0]; + secretId = tokens[1]; + version = tokens[2]; + } + else if (tokens.length == 4 + && tokens[0].equals("projects") + && tokens[2].equals("secrets")) { + // property is form "sm://projects//secrets/" + projectId = tokens[1]; + secretId = tokens[3]; + } + else if (tokens.length == 6 + && tokens[0].equals("projects") + && tokens[2].equals("secrets") + && tokens[4].equals("versions")) { + // property is form "sm://projects//secrets//versions/" + projectId = tokens[1]; + secretId = tokens[3]; + version = tokens[5]; + } + else { + throw new IllegalArgumentException( + "Unrecognized format for specifying a GCP Secret Manager secret: " + input); + } + + Assert.isTrue( + !StringUtils.isEmpty(secretId), + "The GCP Secret Manager secret id must not be empty: " + input); + + Assert.isTrue( + !StringUtils.isEmpty(projectId), + "The GCP Secret Manager project id must not be empty: " + input); + + Assert.isTrue( + !StringUtils.isEmpty(version), + "The GCP Secret Manager secret version must not be empty: " + input); + + return SecretVersionName.newBuilder() + .setProject(projectId) + .setSecret(secretId) + .setSecretVersion(version) + .build(); + } +} diff --git a/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerTemplate.java b/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerTemplate.java index 7cc63e3379..817b8008b3 100644 --- a/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerTemplate.java +++ b/spring-cloud-gcp-secretmanager/src/main/java/org/springframework/cloud/gcp/secretmanager/SecretManagerTemplate.java @@ -67,33 +67,54 @@ public void createSecret(String secretId, byte[] payload) { } @Override - public String getSecretString(String secretId) { - return getSecretString(secretId, LATEST_VERSION); + public void createSecret(String secretId, byte[] payload, String projectId) { + createNewSecretVersion(secretId, ByteString.copyFrom(payload), projectId); } + @Override - public String getSecretString(String secretId, String versionName) { - return getSecretByteString(secretId, versionName).toStringUtf8(); + public String getSecretString(String secretIdentifier) { + return getSecretByteString(secretIdentifier).toStringUtf8(); } @Override - public byte[] getSecretBytes(String secretId) { - return getSecretBytes(secretId, LATEST_VERSION); + public byte[] getSecretBytes(String secretIdentifier) { + return getSecretByteString(secretIdentifier).toByteArray(); } @Override - public byte[] getSecretBytes(String secretId, String versionName) { - return getSecretByteString(secretId, versionName).toByteArray(); + public boolean secretExists(String secretId) { + return secretExists(secretId, this.projectIdProvider.getProjectId()); } @Override - public ByteString getSecretByteString(String secretId, String versionName) { - return getSecretByteString(secretId, versionName, this.projectIdProvider.getProjectId()); + public boolean secretExists(String secretId, String projectId) { + SecretName secretName = SecretName.of(projectId, secretId); + try { + this.secretManagerServiceClient.getSecret(secretName); + } + catch (NotFoundException ex) { + return false; + } + + return true; } - @Override - public boolean secretExists(String secretId) { - return secretExists(secretId, this.projectIdProvider.getProjectId()); + ByteString getSecretByteString(String secretIdentifier) { + SecretVersionName secretVersionName = + SecretManagerPropertyUtils.getSecretVersionName(secretIdentifier, projectIdProvider); + + if (secretVersionName == null) { + secretVersionName = getDefaultSecretVersionName(secretIdentifier); + } + + return getSecretByteString(secretVersionName); + } + + ByteString getSecretByteString(SecretVersionName secretVersionName) { + AccessSecretVersionResponse response = + secretManagerServiceClient.accessSecretVersion(secretVersionName); + return response.getPayload().getData(); } /** @@ -136,33 +157,11 @@ private void createSecretInternal(String secretId, String projectId) { this.secretManagerServiceClient.createSecret(request); } - @Override - public void createSecret(String secretId, byte[] payload, String projectId) { - createNewSecretVersion(secretId, ByteString.copyFrom(payload), projectId); - } - - @Override - public ByteString getSecretByteString(String secretId, String versionName, String projectId) { - SecretVersionName secretVersionName = SecretVersionName.of( - projectId, - secretId, - versionName); - - AccessSecretVersionResponse response = secretManagerServiceClient.accessSecretVersion(secretVersionName); - - return response.getPayload().getData(); - } - - @Override - public boolean secretExists(String secretId, String projectId) { - SecretName secretName = SecretName.of(projectId, secretId); - try { - this.secretManagerServiceClient.getSecret(secretName); - } - catch (NotFoundException ex) { - return false; - } - - return true; + private SecretVersionName getDefaultSecretVersionName(String secretId) { + return SecretVersionName.newBuilder() + .setProject(this.projectIdProvider.getProjectId()) + .setSecret(secretId) + .setSecretVersion(LATEST_VERSION) + .build(); } } diff --git a/spring-cloud-gcp-secretmanager/src/test/java/org/springframework/cloud/gcp/secretmanager/SecretManagerPropertyUtilsTests.java b/spring-cloud-gcp-secretmanager/src/test/java/org/springframework/cloud/gcp/secretmanager/SecretManagerPropertyUtilsTests.java new file mode 100644 index 0000000000..41a67c42be --- /dev/null +++ b/spring-cloud-gcp-secretmanager/src/test/java/org/springframework/cloud/gcp/secretmanager/SecretManagerPropertyUtilsTests.java @@ -0,0 +1,104 @@ +/* + * 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.secretmanager; + +import com.google.cloud.secretmanager.v1beta1.SecretVersionName; +import org.junit.Test; + +import org.springframework.cloud.gcp.core.GcpProjectIdProvider; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class SecretManagerPropertyUtilsTests { + + private static final GcpProjectIdProvider DEFAULT_PROJECT_ID_PROVIDER = () -> "defaultProject"; + + @Test + public void testNonSecret() { + String property = "spring.cloud.datasource"; + SecretVersionName secretIdentifier = + SecretManagerPropertyUtils.getSecretVersionName(property, DEFAULT_PROJECT_ID_PROVIDER); + + assertThat(secretIdentifier).isNull(); + } + + @Test + public void testInvalidSecretFormat_missingSecretId() { + String property = "sm://"; + + assertThatThrownBy(() -> + SecretManagerPropertyUtils.getSecretVersionName(property, DEFAULT_PROJECT_ID_PROVIDER)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("The GCP Secret Manager secret id must not be empty"); + } + + @Test + public void testShortProperty_secretId() { + String property = "sm://the-secret"; + SecretVersionName secretIdentifier = + SecretManagerPropertyUtils.getSecretVersionName(property, DEFAULT_PROJECT_ID_PROVIDER); + + assertThat(secretIdentifier.getProject()).isEqualTo("defaultProject"); + assertThat(secretIdentifier.getSecret()).isEqualTo("the-secret"); + assertThat(secretIdentifier.getSecretVersion()).isEqualTo("latest"); + } + + @Test + public void testShortProperty_projectSecretId() { + String property = "sm://the-secret/the-version"; + SecretVersionName secretIdentifier = + SecretManagerPropertyUtils.getSecretVersionName(property, DEFAULT_PROJECT_ID_PROVIDER); + + assertThat(secretIdentifier.getProject()).isEqualTo("defaultProject"); + assertThat(secretIdentifier.getSecret()).isEqualTo("the-secret"); + assertThat(secretIdentifier.getSecretVersion()).isEqualTo("the-version"); + } + + @Test + public void testShortProperty_projectSecretIdVersion() { + String property = "sm://my-project/the-secret/2"; + SecretVersionName secretIdentifier = + SecretManagerPropertyUtils.getSecretVersionName(property, DEFAULT_PROJECT_ID_PROVIDER); + + assertThat(secretIdentifier.getProject()).isEqualTo("my-project"); + assertThat(secretIdentifier.getSecret()).isEqualTo("the-secret"); + assertThat(secretIdentifier.getSecretVersion()).isEqualTo("2"); + } + + @Test + public void testLongProperty_projectSecret() { + String property = "sm://projects/my-project/secrets/the-secret"; + SecretVersionName secretIdentifier = + SecretManagerPropertyUtils.getSecretVersionName(property, DEFAULT_PROJECT_ID_PROVIDER); + + assertThat(secretIdentifier.getProject()).isEqualTo("my-project"); + assertThat(secretIdentifier.getSecret()).isEqualTo("the-secret"); + assertThat(secretIdentifier.getSecretVersion()).isEqualTo("latest"); + } + + @Test + public void testLongProperty_projectSecretVersion() { + String property = "sm://projects/my-project/secrets/the-secret/versions/3"; + SecretVersionName secretIdentifier = + SecretManagerPropertyUtils.getSecretVersionName(property, DEFAULT_PROJECT_ID_PROVIDER); + + assertThat(secretIdentifier.getProject()).isEqualTo("my-project"); + assertThat(secretIdentifier.getSecret()).isEqualTo("the-secret"); + assertThat(secretIdentifier.getSecretVersion()).isEqualTo("3"); + } +} diff --git a/spring-cloud-gcp-secretmanager/src/test/java/org/springframework/cloud/gcp/secretmanager/SecretManagerTemplateTests.java b/spring-cloud-gcp-secretmanager/src/test/java/org/springframework/cloud/gcp/secretmanager/SecretManagerTemplateTests.java index a463ac4419..1f9f27ebac 100644 --- a/spring-cloud-gcp-secretmanager/src/test/java/org/springframework/cloud/gcp/secretmanager/SecretManagerTemplateTests.java +++ b/spring-cloud-gcp-secretmanager/src/test/java/org/springframework/cloud/gcp/secretmanager/SecretManagerTemplateTests.java @@ -161,7 +161,7 @@ public void testAccessSecretBytes() { SecretVersionName.of("my-project", "my-secret", "latest")); assertThat(result).isEqualTo("get after it.".getBytes()); - result = this.secretManagerTemplate.getSecretBytes("my-secret", "1"); + result = this.secretManagerTemplate.getSecretBytes("sm://my-secret/1"); verify(this.client).accessSecretVersion( SecretVersionName.of("my-project", "my-secret", "1")); assertThat(result).isEqualTo("get after it.".getBytes()); @@ -174,22 +174,12 @@ public void testAccessSecretString() { SecretVersionName.of("my-project", "my-secret", "latest")); assertThat(result).isEqualTo("get after it."); - result = this.secretManagerTemplate.getSecretString("my-secret", "1"); + result = this.secretManagerTemplate.getSecretString("sm://my-secret/1"); verify(this.client).accessSecretVersion( SecretVersionName.of("my-project", "my-secret", "1")); assertThat(result).isEqualTo("get after it."); } - @Test - public void testAccessSecretByteString_withProject() { - ByteString result = - this.secretManagerTemplate.getSecretByteString("my-secret", "1", "custom-project"); - - verify(this.client).accessSecretVersion( - SecretVersionName.of("custom-project", "my-secret", "1")); - assertThat(result.toStringUtf8()).isEqualTo("get after it."); - } - private void verifyCreateSecretRequest(String secretId, String projectId) { Secret secretToAdd = Secret.newBuilder() .setReplication(