From f64b39784caed501153f8836c1cfbeaadcb286c1 Mon Sep 17 00:00:00 2001 From: Alexander Haslam Date: Fri, 27 Dec 2019 17:30:12 -0800 Subject: [PATCH 1/2] Add AWS secrets manager store --- pom.xml | 1 + vertx-config-aws-secrets-manager/pom.xml | 32 +++++++ .../main/java/examples/ConfigAWSExamples.java | 30 +++++++ .../src/main/java/examples/package-info.java | 21 +++++ .../config/aws/AWSMissingSecretException.java | 10 +++ .../config/aws/AWSSecretTypeException.java | 8 ++ .../config/aws/AWSSecretsManagerStore.java | 83 +++++++++++++++++++ .../aws/AWSSecretsManagerStoreFactory.java | 41 +++++++++ .../io.vertx.config.spi.ConfigStoreFactory | 18 ++++ .../aws/AWSSecretsManagerStoreTest.java | 81 ++++++++++++++++++ 10 files changed, 325 insertions(+) create mode 100644 vertx-config-aws-secrets-manager/pom.xml create mode 100644 vertx-config-aws-secrets-manager/src/main/java/examples/ConfigAWSExamples.java create mode 100644 vertx-config-aws-secrets-manager/src/main/java/examples/package-info.java create mode 100644 vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSMissingSecretException.java create mode 100644 vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSSecretTypeException.java create mode 100644 vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSSecretsManagerStore.java create mode 100644 vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSSecretsManagerStoreFactory.java create mode 100644 vertx-config-aws-secrets-manager/src/main/resources/META-INF/services/io.vertx.config.spi.ConfigStoreFactory create mode 100644 vertx-config-aws-secrets-manager/src/test/java/io/vertx/config/aws/AWSSecretsManagerStoreTest.java diff --git a/pom.xml b/pom.xml index bebfd46a..c70bd030 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,7 @@ vertx-config-zookeeper vertx-config-consul vertx-config-vault + vertx-config-aws-secrets-manager diff --git a/vertx-config-aws-secrets-manager/pom.xml b/vertx-config-aws-secrets-manager/pom.xml new file mode 100644 index 00000000..256fc581 --- /dev/null +++ b/vertx-config-aws-secrets-manager/pom.xml @@ -0,0 +1,32 @@ + + + + vertx-config-parent + io.vertx + 4.0.0-SNAPSHOT + + 4.0.0 + + vertx-config-aws-secrets-manager + + + false + + + + + io.vertx + vertx-config + ${project.version} + + + + com.amazonaws + aws-java-sdk-secretsmanager + 1.11.697 + + + + diff --git a/vertx-config-aws-secrets-manager/src/main/java/examples/ConfigAWSExamples.java b/vertx-config-aws-secrets-manager/src/main/java/examples/ConfigAWSExamples.java new file mode 100644 index 00000000..b5a48167 --- /dev/null +++ b/vertx-config-aws-secrets-manager/src/main/java/examples/ConfigAWSExamples.java @@ -0,0 +1,30 @@ +package examples; + +import io.vertx.config.ConfigRetriever; +import io.vertx.config.ConfigRetrieverOptions; +import io.vertx.config.ConfigStoreOptions; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; + +public class ConfigAWSExamples { + + /** + * Environment variables that are required to be set to use AWS + * AWS_ACCESS_KEY_ID + * AWS_SECRET_ACCESS_KEY + * @param vertx + */ + public void example1(Vertx vertx) { + ConfigStoreOptions store = new ConfigStoreOptions() + .setType("aws-secrets-manager") + .setConfig(new JsonObject() + .put("region", "us-west-2") + .put("secretName", "test/example") + ); + + ConfigRetriever retriever = ConfigRetriever.create(vertx, + new ConfigRetrieverOptions().addStore(store)); + } + + +} diff --git a/vertx-config-aws-secrets-manager/src/main/java/examples/package-info.java b/vertx-config-aws-secrets-manager/src/main/java/examples/package-info.java new file mode 100644 index 00000000..852e37b3 --- /dev/null +++ b/vertx-config-aws-secrets-manager/src/main/java/examples/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. and others + * + * Red Hat licenses this file to you 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: + * + * http://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. + * + */ + +@Source +package examples; + +import io.vertx.docgen.Source; diff --git a/vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSMissingSecretException.java b/vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSMissingSecretException.java new file mode 100644 index 00000000..1ed853e8 --- /dev/null +++ b/vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSMissingSecretException.java @@ -0,0 +1,10 @@ +package io.vertx.config.aws; + +/** + * An exception that is thrown when an AWS Secret is not found + */ +public class AWSMissingSecretException extends Exception { + public AWSMissingSecretException(String message) { + super(message); + } +} diff --git a/vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSSecretTypeException.java b/vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSSecretTypeException.java new file mode 100644 index 00000000..2ef7b4d9 --- /dev/null +++ b/vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSSecretTypeException.java @@ -0,0 +1,8 @@ +package io.vertx.config.aws; + +/** + * An exception that is thrown when an AWS Secret is not of a usable type + */ +public class AWSSecretTypeException extends Exception { + public AWSSecretTypeException(String message) { super(message); } +} diff --git a/vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSSecretsManagerStore.java b/vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSSecretsManagerStore.java new file mode 100644 index 00000000..f5d70cc8 --- /dev/null +++ b/vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSSecretsManagerStore.java @@ -0,0 +1,83 @@ +package io.vertx.config.aws; + + +import com.amazonaws.services.secretsmanager.AWSSecretsManager; +import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder; +import com.amazonaws.services.secretsmanager.model.*; +import io.vertx.config.spi.ConfigStore; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.JsonObject; + +import java.util.Objects; + +class AWSSecretsManagerStore implements ConfigStore { + private final Vertx vertx; + private final String region; + private final String secretName; + + public AWSSecretsManagerStore(Vertx vertx, JsonObject configuration) { + this.vertx = Objects.requireNonNull(vertx); + this.region = configuration.getString("region"); + this.secretName = configuration.getString("secretName"); + } + + @Override + public void get(Handler> completionHandler) { + + try { + // Check Environment variables exist, but we don't need to use them outselves, AWS SDK will use automatically. + System.getenv("AWS_ACCESS_KEY_ID"); + System.getenv("AWS_SECRET_ACCESS_KEY"); + + vertx.executeBlocking( + future -> { + AWSSecretsManager client = AWSSecretsManagerClientBuilder.standard() + .withRegion(region) + .build(); + + GetSecretValueRequest getSecretValueRequest = new GetSecretValueRequest() + .withSecretId(secretName); + + try { + GetSecretValueResult getSecretValueResult = client.getSecretValue(getSecretValueRequest); + if (getSecretValueResult != null) { + String secret = getSecretValueResult.getSecretString(); + + if (secret != null) { + future.complete(new JsonObject(secret)); + } else { + throw new AWSSecretTypeException("Secret is not a string"); + } + } else { + throw new AWSMissingSecretException(secretName + " not found"); + } + } catch (DecodeException | AWSMissingSecretException | DecryptionFailureException | InternalServiceErrorException | InvalidParameterException | InvalidRequestException | ResourceNotFoundException | AWSSecretTypeException e) { + future.fail(e); + } + }, + v -> { + if (v.failed()) { + completionHandler.handle(Future.failedFuture(v.cause())); + } else { + JsonObject configFromAWSSecretsManager = (JsonObject) v.result(); + completionHandler.handle(Future.succeededFuture(configFromAWSSecretsManager.toBuffer())); + } + } + ); + + } catch (NullPointerException e) { + completionHandler.handle(Future.failedFuture("No AWS environment variable credentials present")); + } + + } + + @Override + public void close(Handler completionHandler) { + completionHandler.handle(null); + } +} diff --git a/vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSSecretsManagerStoreFactory.java b/vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSSecretsManagerStoreFactory.java new file mode 100644 index 00000000..b8275bd7 --- /dev/null +++ b/vertx-config-aws-secrets-manager/src/main/java/io/vertx/config/aws/AWSSecretsManagerStoreFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2014 Red Hat, Inc. and others + * + * Red Hat licenses this file to you 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: + * + * http://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 io.vertx.config.aws; + + +import io.vertx.config.spi.ConfigStore; +import io.vertx.config.spi.ConfigStoreFactory; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; + +/** + * Factory to create {@link AWSSecretsManagerStore} instances. + * + * @author Alexander Haslam + */ +public class AWSSecretsManagerStoreFactory implements ConfigStoreFactory { + @Override + public String name() { + return "aws-secrets-manager"; + } + + @Override + public ConfigStore create(Vertx vertx, JsonObject configuration) { + return new AWSSecretsManagerStore(vertx, configuration); + } +} diff --git a/vertx-config-aws-secrets-manager/src/main/resources/META-INF/services/io.vertx.config.spi.ConfigStoreFactory b/vertx-config-aws-secrets-manager/src/main/resources/META-INF/services/io.vertx.config.spi.ConfigStoreFactory new file mode 100644 index 00000000..4c019859 --- /dev/null +++ b/vertx-config-aws-secrets-manager/src/main/resources/META-INF/services/io.vertx.config.spi.ConfigStoreFactory @@ -0,0 +1,18 @@ +# +# Copyright (c) 2014 Red Hat, Inc. and others +# +# Red Hat licenses this file to you 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: +# +# http://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. +# +# + +io.vertx.config.aws.AWSSecretsManagerStoreFactory diff --git a/vertx-config-aws-secrets-manager/src/test/java/io/vertx/config/aws/AWSSecretsManagerStoreTest.java b/vertx-config-aws-secrets-manager/src/test/java/io/vertx/config/aws/AWSSecretsManagerStoreTest.java new file mode 100644 index 00000000..716ccfba --- /dev/null +++ b/vertx-config-aws-secrets-manager/src/test/java/io/vertx/config/aws/AWSSecretsManagerStoreTest.java @@ -0,0 +1,81 @@ +package io.vertx.config.aws; + + +import io.vertx.config.ConfigRetriever; +import io.vertx.config.ConfigRetrieverOptions; +import io.vertx.config.ConfigStoreOptions; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.unit.Async; +import io.vertx.ext.unit.TestContext; +import io.vertx.ext.unit.junit.VertxUnitRunner; +import org.assertj.core.api.Assertions; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +/** + * Factory to create {@link AWSSecretsManagerStore} instances. + * + * @author Alexander Haslam + */ +@RunWith(VertxUnitRunner.class) +public class AWSSecretsManagerStoreTest { + + private ConfigRetriever retriever; + private Vertx vertx; + + + @Before + public void setUp(TestContext tc) throws IOException { + vertx = Vertx.vertx(); + vertx.exceptionHandler(tc.exceptionHandler()); + } + + @After + public void tearDown(TestContext tc) { + retriever.close(); + vertx.close(tc.asyncAssertSuccess()); + } + + @Test + public void getExampleFromAWSSecretsManager(TestContext tc) throws Exception { + Async async = tc.async(); + retriever = ConfigRetriever.create(vertx, + new ConfigRetrieverOptions().addStore( + new ConfigStoreOptions() + .setType("aws-secrets-manager") + .setConfig(new JsonObject() + .put("region", "us-west-2") + .put("secretName", "test/example") + ))); + retriever.getConfig(ar -> { + assertThat(ar.succeeded()).isTrue(); + JsonObject json = ar.result(); + Assertions.assertThat(json).isNotNull(); + Assertions.assertThat(json.getString("key")).isEqualTo("value"); + + Assertions.assertThat(json.getBoolean("true")).isTrue(); + Assertions.assertThat(json.getBoolean("false")).isFalse(); + + Assertions.assertThat(json.getString("missing")).isNull(); + + Assertions.assertThat(json.getInteger("int")).isEqualTo(5); + Assertions.assertThat(json.getDouble("float")).isEqualTo(25.3); + + Assertions.assertThat(json.getJsonArray("array").size()).isEqualTo(3); + Assertions.assertThat(json.getJsonArray("array").contains(1)).isTrue(); + Assertions.assertThat(json.getJsonArray("array").contains(2)).isTrue(); + Assertions.assertThat(json.getJsonArray("array").contains(3)).isTrue(); + + Assertions.assertThat(json.getJsonObject("sub").getString("foo")).isEqualTo("bar"); + async.complete(); + }); + } + +} From 0618b57ebb6d35b2a82573d59e9c2d6b9a6e04be Mon Sep 17 00:00:00 2001 From: Alexander Haslam Date: Fri, 27 Dec 2019 17:52:23 -0800 Subject: [PATCH 2/2] Add documentation --- pom.xml | 2 +- .../src/main/asciidoc/aws-store.adoc | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 vertx-config-aws-secrets-manager/src/main/asciidoc/aws-store.adoc diff --git a/pom.xml b/pom.xml index c70bd030..813501ab 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ vertx-config-zookeeper vertx-config-consul vertx-config-vault - vertx-config-aws-secrets-manager + vertx-config-aws-secrets-manager diff --git a/vertx-config-aws-secrets-manager/src/main/asciidoc/aws-store.adoc b/vertx-config-aws-secrets-manager/src/main/asciidoc/aws-store.adoc new file mode 100644 index 00000000..b3c3394b --- /dev/null +++ b/vertx-config-aws-secrets-manager/src/main/asciidoc/aws-store.adoc @@ -0,0 +1,46 @@ +=== AWS Secrets Manager Configuration Store + +The AWS Secrets Manager Configuration Store extends the Vert.x Configuration Retriever and provides the way to retrieve configuration from AWS Secrets Manager. +It uses the AWS SDK as a client. + +==== Using the AWS Secrets Manager Configuration Store + +To use the AWS Secrets Manager Configuration Store, add the following dependency to the _dependencies_ section of your build descriptor: + +* Maven (in your `pom.xml`): + +[source,xml,subs="+attributes"] +---- + + io.vertx + vertx-config-aws-secrets-manager + ${maven.version} + + + io.vertx + vertx-config + ${maven.version} + +---- + +* Gradle (in your `build.gradle` file): + +[source,groovy,subs="+attributes"] +---- +compile 'io.vertx:vertx-config:${maven.version}' +compile 'io.vertx:vertx-config-aws-secrets-manager:${maven.version}' +---- + +==== Configuring the store + +Once added to your classpath or dependencies, you need to configure the {@link io.vertx.config.ConfigRetriever} to use this store: + +[source,$lang] +---- +{@link examples.ConfigAWSExamples#example1(io.vertx.core.Vertx)} +---- + +The store configuration is used to configure the AWS SDK client and the _secretName_ containing the configuration. +Notice that the format of the configuration can be PlainText JSON only. + +You must also have the _AWS_ACCESS_KEY_ID_ & _AWS_SECRET_ACCESS_KEY_ environment variables set so AWS SDK can authenticate.