diff --git a/.changes/next-release/feature-AWSRDS-e6fec25.json b/.changes/next-release/feature-AWSRDS-e6fec25.json new file mode 100644 index 000000000000..46359b78d941 --- /dev/null +++ b/.changes/next-release/feature-AWSRDS-e6fec25.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS RDS", + "contributor": "abrooksv", + "description": "Add the ability to generate IAM auth tokens for RDS using `RdsUtilities`" +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/UtilitiesMethod.java b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/UtilitiesMethod.java index 331c3481ae8d..dbe82e57961a 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/UtilitiesMethod.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/UtilitiesMethod.java @@ -29,6 +29,9 @@ public class UtilitiesMethod { /** Fqcn of the return type of the operation */ private String returnType; + /** Fqcn of the instance type to be created */ + private String instanceType; + /** * The utilities method will call a protected create() method in the hand-written Utilities class. * These the ordered list of parameters that needs to be passed to the create method. @@ -50,4 +53,12 @@ public List getCreateMethodParams() { public void setCreateMethodParams(List createMethodParams) { this.createMethodParams = createMethodParams; } + + public String getInstanceType() { + return instanceType; + } + + public void setInstanceType(String instanceType) { + this.instanceType = instanceType; + } } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientClass.java index 4d8336d18a03..e27a17bbc74d 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/AsyncClientClass.java @@ -383,13 +383,19 @@ private TypeName eventStreamType(ShapeModel shapeModel) { private MethodSpec utilitiesMethod() { UtilitiesMethod config = model.getCustomizationConfig().getUtilitiesMethod(); ClassName returnType = PoetUtils.classNameFromFqcn(config.getReturnType()); + String instanceClass = config.getInstanceType(); + if (instanceClass == null) { + instanceClass = config.getReturnType(); + } + + ClassName instanceType = PoetUtils.classNameFromFqcn(instanceClass); return MethodSpec.methodBuilder(UtilitiesMethod.METHOD_NAME) .returns(returnType) .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addStatement("return $T.create($L)", - returnType, + instanceType, String.join(",", config.getCreateMethodParams())) .build(); } diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientClass.java index 95724e518af9..7fe594733993 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientClass.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/client/SyncClientClass.java @@ -310,13 +310,19 @@ private MethodSpec closeMethod() { private MethodSpec utilitiesMethod() { UtilitiesMethod config = model.getCustomizationConfig().getUtilitiesMethod(); ClassName returnType = PoetUtils.classNameFromFqcn(config.getReturnType()); + String instanceClass = config.getInstanceType(); + if (instanceClass == null) { + instanceClass = config.getReturnType(); + } + + ClassName instanceType = PoetUtils.classNameFromFqcn(instanceClass); return MethodSpec.methodBuilder(UtilitiesMethod.METHOD_NAME) .returns(returnType) .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addStatement("return $T.create($L)", - returnType, + instanceType, String.join(",", config.getCreateMethodParams())) .build(); } diff --git a/docs/LaunchChangelog.md b/docs/LaunchChangelog.md index 36aa10162009..91bb278af7b8 100644 --- a/docs/LaunchChangelog.md +++ b/docs/LaunchChangelog.md @@ -449,6 +449,10 @@ The S3 client in 2.0 is drastically different from the client in 1.11, because i 1. An SQS client may no longer access SQS queues in regions different than the one with which the client was configured. +## 4.4. RDS Changes + +1. The class`RdsIamAuthTokenGenerator` has been replaced with `RdsUtilities#generateAuthenticationToken`. + # 5. Profile File Changes The parsing of the `~/.aws/config` and `~/.aws/credentials` has changed to more closely emulate that used by the AWS CLI. diff --git a/services/rds/src/main/java/software/amazon/awssdk/services/rds/DefaultRdsUtilities.java b/services/rds/src/main/java/software/amazon/awssdk/services/rds/DefaultRdsUtilities.java new file mode 100644 index 000000000000..f77082792631 --- /dev/null +++ b/services/rds/src/main/java/software/amazon/awssdk/services/rds/DefaultRdsUtilities.java @@ -0,0 +1,156 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.services.rds; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import software.amazon.awssdk.annotations.Immutable; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.signer.Aws4Signer; +import software.amazon.awssdk.auth.signer.params.Aws4PresignerParams; +import software.amazon.awssdk.awscore.client.config.AwsClientOption; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.rds.model.GenerateAuthenticationTokenRequest; +import software.amazon.awssdk.utils.StringUtils; + +@Immutable +@SdkInternalApi +final class DefaultRdsUtilities implements RdsUtilities { + // The time the IAM token is good for. https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html + private static final Duration EXPIRATION_DURATION = Duration.ofMinutes(15); + + private final Aws4Signer signer = Aws4Signer.create(); + private final Region region; + private final AwsCredentialsProvider credentialsProvider; + private final Clock clock; + + DefaultRdsUtilities(DefaultBuilder builder) { + this(builder, Clock.systemUTC()); + } + + /** + * Test Only + */ + DefaultRdsUtilities(DefaultBuilder builder, Clock clock) { + this.credentialsProvider = builder.credentialsProvider; + this.region = builder.region; + this.clock = clock; + } + + /** + * Used by RDS low-level client's utilities() method + */ + @SdkInternalApi + static RdsUtilities create(SdkClientConfiguration clientConfiguration) { + return new DefaultBuilder().clientConfiguration(clientConfiguration).build(); + } + + @Override + public String generateAuthenticationToken(GenerateAuthenticationTokenRequest request) { + SdkHttpFullRequest httpRequest = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .protocol("https") + .host(request.hostname()) + .port(request.port()) + .encodedPath("/") + .putRawQueryParameter("DBUser", request.username()) + .putRawQueryParameter("Action", "connect") + .build(); + + Instant expirationTime = Instant.now(clock).plus(EXPIRATION_DURATION); + Aws4PresignerParams presignRequest = Aws4PresignerParams.builder() + .signingClockOverride(clock) + .expirationTime(expirationTime) + .awsCredentials(resolveCredentials(request).resolveCredentials()) + .signingName("rds-db") + .signingRegion(resolveRegion(request)) + .build(); + + SdkHttpFullRequest fullRequest = signer.presign(httpRequest, presignRequest); + String signedUrl = fullRequest.getUri().toString(); + + // Format should be: >:>/?Action=connect&DBUser=>&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expi... + // Note: This must be the real RDS hostname, not proxy or tunnels + return StringUtils.replacePrefixIgnoreCase(signedUrl, "https://", ""); + } + + private Region resolveRegion(GenerateAuthenticationTokenRequest request) { + if (request.region() != null) { + return request.region(); + } + + if (this.region != null) { + return this.region; + } + + throw new IllegalArgumentException("Region should be provided either in GenerateAuthenticationTokenRequest object " + + "or RdsUtilities object"); + } + + private AwsCredentialsProvider resolveCredentials(GenerateAuthenticationTokenRequest request) { + if (request.credentialsProvider() != null) { + return request.credentialsProvider(); + } + + if (this.credentialsProvider != null) { + return this.credentialsProvider; + } + + throw new IllegalArgumentException("CredentialProvider should be provided either in GenerateAuthenticationTokenRequest " + + "object or RdsUtilities object"); + } + + @SdkInternalApi + static final class DefaultBuilder implements Builder { + private Region region; + private AwsCredentialsProvider credentialsProvider; + + DefaultBuilder() { + } + + Builder clientConfiguration(SdkClientConfiguration clientConfiguration) { + this.credentialsProvider = clientConfiguration.option(AwsClientOption.CREDENTIALS_PROVIDER); + this.region = clientConfiguration.option(AwsClientOption.AWS_REGION); + + return this; + } + + @Override + public Builder region(Region region) { + this.region = region; + return this; + } + + @Override + public Builder credentialsProvider(AwsCredentialsProvider credentialsProvider) { + this.credentialsProvider = credentialsProvider; + return this; + } + + /** + * Construct a {@link RdsUtilities} object. + */ + @Override + public RdsUtilities build() { + return new DefaultRdsUtilities(this); + } + } +} \ No newline at end of file diff --git a/services/rds/src/main/java/software/amazon/awssdk/services/rds/RdsUtilities.java b/services/rds/src/main/java/software/amazon/awssdk/services/rds/RdsUtilities.java new file mode 100644 index 000000000000..d7ce5e19c06b --- /dev/null +++ b/services/rds/src/main/java/software/amazon/awssdk/services/rds/RdsUtilities.java @@ -0,0 +1,106 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.services.rds; + +import java.util.function.Consumer; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.rds.model.GenerateAuthenticationTokenRequest; + +/** + * Utilities for working with RDS. An instance of this class can be created by: + *

+ * 1) Using the low-level client {@link RdsClient#utilities()} (or {@link RdsAsyncClient#utilities()}} method. This is + * recommended as SDK will use the same configuration from the {@link RdsClient} object to create the {@link RdsUtilities} object. + * + *

+ * RdsClient rdsClient = RdsClient.create();
+ * RdsUtilities utilities = rdsClient.utilities();
+ * 
+ *

+ * + *

+ * 2) Directly using the {@link #builder()} method. + * + *

+ * RdsUtilities utilities = RdsUtilities.builder()
+ *  .credentialsProvider(DefaultCredentialsProvider.create())
+ *  .region(Region.US_WEST_2)
+ *  .build()
+ * 
+ *

+ * + * Note: This class does not make network calls. + */ +@SdkPublicApi +public interface RdsUtilities { + /** + * Create a builder that can be used to configure and create a {@link RdsUtilities}. + */ + static Builder builder() { + return new DefaultRdsUtilities.DefaultBuilder(); + } + + /** + * Generates an authorization tokens for IAM authentication to an RDS database. + * + * @param request The request used to generate the auth token + * @return String to use as the RDS auth token + * @throws IllegalArgumentException if the required parameters are not valid + */ + default String generateAuthenticationToken(Consumer request) { + return generateAuthenticationToken(GenerateAuthenticationTokenRequest.builder().applyMutation(request).build()); + } + + /** + * Generates an authorization tokens for IAM authentication to an RDS database. + * + * @param request The request used to generate the auth token + * @return String to use as the RDS auth token + * @throws IllegalArgumentException if the required parameters are not valid + */ + default String generateAuthenticationToken(GenerateAuthenticationTokenRequest request) { + RdsUtilities.builder().region(Region.US_WEST_2).build(); + throw new UnsupportedOperationException(); + } + + /** + * Builder for creating an instance of {@link RdsUtilities}. It can be configured using {@link RdsUtilities#builder()}. + * Once configured, the {@link RdsUtilities} can created using {@link #build()}. + */ + @SdkPublicApi + interface Builder { + /** + * The default region to use when working with the methods in {@link RdsUtilities} class. + * + * @return This object for method chaining + */ + Builder region(Region region); + + /** + * The default credentials provider to use when working with the methods in {@link RdsUtilities} class. + * + * @return This object for method chaining + */ + Builder credentialsProvider(AwsCredentialsProvider credentialsProvider); + + /** + * Create a {@link RdsUtilities} + */ + RdsUtilities build(); + } +} diff --git a/services/rds/src/main/java/software/amazon/awssdk/services/rds/model/GenerateAuthenticationTokenRequest.java b/services/rds/src/main/java/software/amazon/awssdk/services/rds/model/GenerateAuthenticationTokenRequest.java new file mode 100644 index 000000000000..2c23705ae4ce --- /dev/null +++ b/services/rds/src/main/java/software/amazon/awssdk/services/rds/model/GenerateAuthenticationTokenRequest.java @@ -0,0 +1,190 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.services.rds.model; + +import software.amazon.awssdk.annotations.NotThreadSafe; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.rds.RdsUtilities; +import software.amazon.awssdk.utils.Validate; +import software.amazon.awssdk.utils.builder.CopyableBuilder; +import software.amazon.awssdk.utils.builder.ToCopyableBuilder; + +/** + * Input parameters for generating an auth token for IAM database authentication. + */ +@SdkPublicApi +public final class GenerateAuthenticationTokenRequest implements + ToCopyableBuilder { + private final String hostname; + private final int port; + private final String username; + private final Region region; + private final AwsCredentialsProvider credentialsProvider; + + private GenerateAuthenticationTokenRequest(BuilderImpl builder) { + this.hostname = Validate.notEmpty(builder.hostname, "hostname"); + this.port = Validate.isPositive(builder.port, "port"); + this.username = Validate.notEmpty(builder.username, "username"); + this.region = builder.region; + this.credentialsProvider = builder.credentialsProvider; + } + + /** + * @return The hostname of the database to connect to + */ + public String hostname() { + return hostname; + } + + /** + * @return The port of the database to connect to + */ + public int port() { + return port; + } + + /** + * @return The username to log in as. + */ + public String username() { + return username; + } + + /** + * @return The region the database is hosted in. If specified, takes precedence over the value specified in + * {@link RdsUtilities.Builder#region(Region)} + */ + public Region region() { + return region; + } + + /** + * @return The credentials provider to sign the IAM auth request with. If specified, takes precedence over the value + * specified in {@link RdsUtilities.Builder#credentialsProvider(AwsCredentialsProvider)}} + */ + public AwsCredentialsProvider credentialsProvider() { + return credentialsProvider; + } + + @Override + public Builder toBuilder() { + return new BuilderImpl(this); + } + + /** + * Creates a builder for {@link RdsUtilities}. + */ + public static Builder builder() { + return new BuilderImpl(); + } + + /** + * A builder for a {@link GenerateAuthenticationTokenRequest}, created with {@link #builder()}. + */ + @SdkPublicApi + @NotThreadSafe + public interface Builder extends CopyableBuilder { + /** + * The hostname of the database to connect to + * + * @return This object for method chaining + */ + Builder hostname(String endpoint); + + /** + * The port number the database is listening on. + * + * @return This object for method chaining + */ + Builder port(int port); + + /** + * The username to log in as. + * + * @return This object for method chaining + */ + Builder username(String userName); + + /** + * The region the database is hosted in. If specified, takes precedence over the value specified in + * {@link RdsUtilities.Builder#region(Region)} + * + * @return This object for method chaining + */ + Builder region(Region region); + + /** + * The credentials provider to sign the IAM auth request with. If specified, takes precedence over the value + * specified in {@link RdsUtilities.Builder#credentialsProvider(AwsCredentialsProvider)}} + * + * @return This object for method chaining + */ + Builder credentialsProvider(AwsCredentialsProvider credentialsProvider); + + @Override + GenerateAuthenticationTokenRequest build(); + } + + private static final class BuilderImpl implements Builder { + private String hostname; + private int port; + private String username; + private Region region; + private AwsCredentialsProvider credentialsProvider; + + private BuilderImpl() { + } + + private BuilderImpl(GenerateAuthenticationTokenRequest request) { + this.hostname = request.hostname; + this.port = request.port; + this.username = request.username; + this.region = request.region; + this.credentialsProvider = request.credentialsProvider; + } + + public Builder hostname(String endpoint) { + this.hostname = endpoint; + return this; + } + + public Builder port(int port) { + this.port = port; + return this; + } + + public Builder username(String userName) { + this.username = userName; + return this; + } + + public Builder region(Region region) { + this.region = region; + return this; + } + + public Builder credentialsProvider(AwsCredentialsProvider credentialsProvider) { + this.credentialsProvider = credentialsProvider; + return this; + } + + public GenerateAuthenticationTokenRequest build() { + return new GenerateAuthenticationTokenRequest(this); + } + } +} \ No newline at end of file diff --git a/services/rds/src/main/resources/codegen-resources/customization.config b/services/rds/src/main/resources/codegen-resources/customization.config index 0eb1b018e172..9c878f2672a1 100644 --- a/services/rds/src/main/resources/codegen-resources/customization.config +++ b/services/rds/src/main/resources/codegen-resources/customization.config @@ -83,5 +83,12 @@ "describeReservedDBInstances", "describeReservedDBInstancesOfferings", "describeSourceRegions" - ] + ], + "utilitiesMethod": { + "returnType": "software.amazon.awssdk.services.rds.RdsUtilities", + "instanceType": "software.amazon.awssdk.services.rds.DefaultRdsUtilities", + "createMethodParams": [ + "clientConfiguration" + ] + } } diff --git a/services/rds/src/test/java/software/amazon/awssdk/services/rds/DefaultRdsUtilitiesTest.java b/services/rds/src/test/java/software/amazon/awssdk/services/rds/DefaultRdsUtilitiesTest.java new file mode 100644 index 000000000000..125b899a6d4a --- /dev/null +++ b/services/rds/src/test/java/software/amazon/awssdk/services/rds/DefaultRdsUtilitiesTest.java @@ -0,0 +1,128 @@ +package software.amazon.awssdk.services.rds; + +import org.junit.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; + +import java.time.Clock; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import software.amazon.awssdk.services.rds.DefaultRdsUtilities.DefaultBuilder; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +public class DefaultRdsUtilitiesTest { + private final ZoneId utcZone = ZoneId.of("UTC").normalized(); + private final Clock fixedClock = Clock.fixed(ZonedDateTime.of(2016, 11, 7, 17, 39, 33, 0, utcZone).toInstant(), utcZone); + + @Test + public void testTokenGenerationWithBuilderDefaults() { + AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create("access_key", "secret_key") + ); + DefaultBuilder utilitiesBuilder = (DefaultBuilder) RdsUtilities.builder() + .credentialsProvider(credentialsProvider) + .region(Region.US_EAST_1); + + DefaultRdsUtilities rdsUtilities = new DefaultRdsUtilities(utilitiesBuilder, fixedClock); + + String authenticationToken = rdsUtilities.generateAuthenticationToken(builder -> { + builder.username("mySQLUser") + .hostname("host.us-east-1.amazonaws.com") + .port(3306); + }); + + String expectedToken = "host.us-east-1.amazonaws.com:3306/?DBUser=mySQLUser&Action=connect&" + + "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20161107T173933Z&X-Amz-SignedHeaders=host&" + + "X-Amz-Expires=900&X-Amz-Credential=access_key%2F20161107%2Fus-east-1%2Frds-db%2Faws4_request&" + + "X-Amz-Signature=87ab58107ef49f1c311a412f98b7f976b0b5152ffb559f0d36c6c9a0c5e0e362"; + assertThat(authenticationToken).isEqualTo(expectedToken); + } + + @Test + public void testTokenGenerationWithOverriddenCredentials() { + AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create("foo", "bar") + ); + DefaultBuilder utilitiesBuilder = (DefaultBuilder) RdsUtilities.builder() + .credentialsProvider(credentialsProvider) + .region(Region.US_EAST_1); + DefaultRdsUtilities rdsUtilities = new DefaultRdsUtilities(utilitiesBuilder, fixedClock); + + String authenticationToken = rdsUtilities.generateAuthenticationToken(builder -> { + builder.username("mySQLUser") + .hostname("host.us-east-1.amazonaws.com") + .port(3306) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("access_key", "secret_key") + )); + }); + + String expectedToken = "host.us-east-1.amazonaws.com:3306/?DBUser=mySQLUser&Action=connect&" + + "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20161107T173933Z&X-Amz-SignedHeaders=host&" + + "X-Amz-Expires=900&X-Amz-Credential=access_key%2F20161107%2Fus-east-1%2Frds-db%2Faws4_request&" + + "X-Amz-Signature=87ab58107ef49f1c311a412f98b7f976b0b5152ffb559f0d36c6c9a0c5e0e362"; + assertThat(authenticationToken).isEqualTo(expectedToken); + } + + @Test + public void testTokenGenerationWithOverriddenRegion() { + AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create("access_key", "secret_key") + ); + DefaultBuilder utilitiesBuilder = (DefaultBuilder) RdsUtilities.builder() + .credentialsProvider(credentialsProvider) + .region(Region.US_WEST_2); + + DefaultRdsUtilities rdsUtilities = new DefaultRdsUtilities(utilitiesBuilder, fixedClock); + + String authenticationToken = rdsUtilities.generateAuthenticationToken(builder -> { + builder.username("mySQLUser") + .hostname("host.us-east-1.amazonaws.com") + .port(3306) + .region(Region.US_EAST_1); + }); + + String expectedToken = "host.us-east-1.amazonaws.com:3306/?DBUser=mySQLUser&Action=connect&" + + "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20161107T173933Z&X-Amz-SignedHeaders=host&" + + "X-Amz-Expires=900&X-Amz-Credential=access_key%2F20161107%2Fus-east-1%2Frds-db%2Faws4_request&" + + "X-Amz-Signature=87ab58107ef49f1c311a412f98b7f976b0b5152ffb559f0d36c6c9a0c5e0e362"; + assertThat(authenticationToken).isEqualTo(expectedToken); + } + + @Test + public void testMissingRegionThrowsException() { + AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create("access_key", "secret_key") + ); + DefaultBuilder utilitiesBuilder = (DefaultBuilder) RdsUtilities.builder() + .credentialsProvider(credentialsProvider); + + DefaultRdsUtilities rdsUtilities = new DefaultRdsUtilities(utilitiesBuilder, fixedClock); + + assertThatThrownBy(() -> rdsUtilities.generateAuthenticationToken(builder -> { + builder.username("mySQLUser") + .hostname("host.us-east-1.amazonaws.com") + .port(3306); + })).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Region should be provided"); + } + + @Test + public void testMissingCredentialsThrowsException() { + DefaultBuilder utilitiesBuilder = (DefaultBuilder) RdsUtilities.builder() + .region(Region.US_WEST_2); + + DefaultRdsUtilities rdsUtilities = new DefaultRdsUtilities(utilitiesBuilder, fixedClock); + + assertThatThrownBy(() -> rdsUtilities.generateAuthenticationToken(builder -> { + builder.username("mySQLUser") + .hostname("host.us-east-1.amazonaws.com") + .port(3306); + })).isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("CredentialProvider should be provided"); + } +} \ No newline at end of file