diff --git a/.changes/next-release/feature-AxdbFrontend-d6ce1e6.json b/.changes/next-release/feature-AxdbFrontend-d6ce1e6.json new file mode 100644 index 000000000000..30ef8b7626d7 --- /dev/null +++ b/.changes/next-release/feature-AxdbFrontend-d6ce1e6.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "DSQL", + "contributor": "APandher", + "description": "Add IAM Token Generation Utility for DSQL" +} diff --git a/feature.metadata b/feature.metadata new file mode 100644 index 000000000000..ce11e6bf1e26 --- /dev/null +++ b/feature.metadata @@ -0,0 +1 @@ +{"trebuchetFeatureArn":"arn:aws:trebuchet:::feature:v2:2a236723-7c96-48cd-8055-918b35a8308a","c2jModelsRevision":25,"messageId":1,"serviceId":"DSQL","serviceModule":"dsql"} diff --git a/services/dsql/pom.xml b/services/dsql/pom.xml index fb44de96b622..fb39fa0d3c7f 100644 --- a/services/dsql/pom.xml +++ b/services/dsql/pom.xml @@ -41,6 +41,11 @@ + + nl.jqno.equalsverifier + equalsverifier + test + software.amazon.awssdk protocol-core diff --git a/services/dsql/src/main/java/software/amazon/awssdk/services/dsql/DefaultDsqlUtilities.java b/services/dsql/src/main/java/software/amazon/awssdk/services/dsql/DefaultDsqlUtilities.java new file mode 100644 index 000000000000..bc74fde337ea --- /dev/null +++ b/services/dsql/src/main/java/software/amazon/awssdk/services/dsql/DefaultDsqlUtilities.java @@ -0,0 +1,166 @@ +/* + * 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.dsql; + +import java.time.Clock; +import java.time.Instant; +import software.amazon.awssdk.annotations.Immutable; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.annotations.SdkTestInternalApi; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.CredentialUtils; +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.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dsql.model.GenerateAuthTokenRequest; +import software.amazon.awssdk.utils.CompletableFutureUtils; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.StringUtils; + +@Immutable +@SdkInternalApi +public final class DefaultDsqlUtilities implements DsqlUtilities { + private static final Logger log = Logger.loggerFor(DsqlUtilities.class); + private final Aws4Signer signer = Aws4Signer.create(); + private final Region region; + private final IdentityProvider credentialsProvider; + private final Clock clock; + + public DefaultDsqlUtilities(DefaultBuilder builder) { + this(builder, Clock.systemUTC()); + } + + /** + * For testing purposes only + */ + @SdkTestInternalApi + public DefaultDsqlUtilities(DefaultBuilder builder, Clock clock) { + this.credentialsProvider = builder.credentialsProvider; + this.region = builder.region; + this.clock = clock; + } + + /** + * Used by DSQL low-level client's utilities() method + */ + @SdkInternalApi + static DsqlUtilities create(SdkClientConfiguration clientConfiguration) { + return new DefaultBuilder().clientConfiguration(clientConfiguration).build(); + } + + @Override + public String generateDbConnectAuthToken(GenerateAuthTokenRequest request) { + return generateAuthToken(request, false); + } + + @Override + public String generateDbConnectAdminAuthToken(GenerateAuthTokenRequest request) { + return generateAuthToken(request, true); + } + + private String generateAuthToken(GenerateAuthTokenRequest request, boolean isAdmin) { + String action = isAdmin ? "DbConnectAdmin" : "DbConnect"; + + SdkHttpFullRequest httpRequest = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .protocol("https") + .host(request.hostname()) + .encodedPath("/") + .putRawQueryParameter("Action", action) + .build(); + + Instant expirationTime = Instant.now(clock).plus(request.expiresIn()); + + Aws4PresignerParams presignRequest = Aws4PresignerParams.builder() + .signingClockOverride(clock) + .expirationTime(expirationTime) + .awsCredentials(resolveCredentials(request)) + .signingName("dsql") + .signingRegion(resolveRegion(request)) + .build(); + + SdkHttpFullRequest fullRequest = signer.presign(httpRequest, presignRequest); + String signedUrl = fullRequest.getUri().toString(); + + log.debug(() -> "Generated DSQL authentication token with expiration of " + expirationTime); + return StringUtils.replacePrefixIgnoreCase(signedUrl, "https://", ""); + } + + private Region resolveRegion(GenerateAuthTokenRequest request) { + if (request.region() != null) { + return request.region(); + } + + if (this.region != null) { + return this.region; + } + + throw new IllegalArgumentException("Region must be provided in GenerateAuthTokenRequest or DsqlUtilities"); + } + + // TODO: update this to use AwsCredentialsIdentity when we migrate Signers to accept the new type. + private AwsCredentials resolveCredentials(GenerateAuthTokenRequest request) { + if (request.credentialsIdentityProvider() != null) { + return CredentialUtils.toCredentials( + CompletableFutureUtils.joinLikeSync(request.credentialsIdentityProvider().resolveIdentity())); + } + + if (this.credentialsProvider != null) { + return CredentialUtils.toCredentials(CompletableFutureUtils.joinLikeSync(this.credentialsProvider.resolveIdentity())); + } + + throw new IllegalArgumentException("CredentialsProvider must be provided in GenerateAuthTokenRequest or DsqlUtilities"); + } + + @SdkInternalApi + public static final class DefaultBuilder implements DsqlUtilities.Builder { + private Region region; + private IdentityProvider credentialsProvider; + + Builder clientConfiguration(SdkClientConfiguration clientConfiguration) { + this.credentialsProvider = clientConfiguration.option(AwsClientOption.CREDENTIALS_IDENTITY_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(IdentityProvider credentialsProvider) { + this.credentialsProvider = credentialsProvider; + return this; + } + + /** + * Construct a {@link DsqlUtilities} object. + */ + @Override + public DsqlUtilities build() { + return new DefaultDsqlUtilities(this); + } + } +} \ No newline at end of file diff --git a/services/dsql/src/main/java/software/amazon/awssdk/services/dsql/DsqlUtilities.java b/services/dsql/src/main/java/software/amazon/awssdk/services/dsql/DsqlUtilities.java new file mode 100644 index 000000000000..95d3875dec20 --- /dev/null +++ b/services/dsql/src/main/java/software/amazon/awssdk/services/dsql/DsqlUtilities.java @@ -0,0 +1,143 @@ +/* + * 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.dsql; + +import java.util.function.Consumer; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dsql.model.GenerateAuthTokenRequest; + +/** + * Utilities for working with DSQL. An instance of this class can be created by: + *

+ * 1) Using the low-level client {@link DsqlClient#utilities()} (or {@link DsqlAsyncClient#utilities()}} method. + * This is recommended as SDK will use the same configuration from the {@link DsqlClient} object to create the + * {@link DsqlUtilities} object. + * + * @snippet : + * {@code + * DsqlClient dsqlClient = DsqlClient.create(); + * DsqlUtilities utilities = DsqlClient.utilities(); + * } + * + *

+ * 2) Directly using the {@link #builder()} method. + * + * @snippet : + * {@code + * DsqlUtilities utilities = DsqlUtilities.builder() + * .credentialsProvider(DefaultCredentialsProvider.create()) + * .region(Region.US_WEST_2) + * .build() + * } + * + * Note: This class does not make network calls. + */ +@SdkPublicApi +public interface DsqlUtilities { + /** + * Create a builder that can be used to configure and create a {@link DsqlUtilities}. + */ + static Builder builder() { + return new DefaultDsqlUtilities.DefaultBuilder(); + } + + /** + * Generates an authentication token for IAM authentication to an DSQL database. + * + * @param request The request used to generate the authentication token + * @return String to use as the DSQL authentication token + * @throws IllegalArgumentException if the required parameters are not valid + */ + default String generateDbConnectAuthToken(Consumer request) { + return generateDbConnectAuthToken(GenerateAuthTokenRequest.builder().applyMutation(request).build()); + } + + /** + * Generates an authentication token for IAM authentication to an DSQL database. + * + * @param request The request used to generate the authentication token + * @return String to use as the DSQL authentication token + * @throws IllegalArgumentException if the required parameters are not valid + */ + default String generateDbConnectAuthToken(GenerateAuthTokenRequest request) { + throw new UnsupportedOperationException(); + } + + /** + * Generates an admin authentication token for IAM authentication to an DSQL database. + * + * @param request The request used to generate the admin authentication token + * @return String to use as the DSQL authentication token + * @throws IllegalArgumentException if the required parameters are not valid + */ + default String generateDbConnectAdminAuthToken(Consumer request) { + return generateDbConnectAdminAuthToken(GenerateAuthTokenRequest.builder().applyMutation(request).build()); + } + + /** + * Generates an admin authentication token for IAM authentication to an DSQL database. + * + * @param request The request used to generate the admin authentication token + * @return String to use as the DSQL authentication token + * @throws IllegalArgumentException if the required parameters are not valid + */ + default String generateDbConnectAdminAuthToken(GenerateAuthTokenRequest request) { + throw new UnsupportedOperationException(); + } + + + /** + * Builder for creating an instance of {@link DsqlUtilities}. It can be configured using + * {@link DsqlUtilities#builder()}. + * Once configured, the {@link DsqlUtilities} can created using {@link #build()}. + */ + @SdkPublicApi + interface Builder { + /** + * The default region to use when working with the methods in {@link DsqlUtilities} class. + * + * @return This object for method chaining + */ + Builder region(Region region); + + /** + * The default credentials provider to use when working with the methods in {@link DsqlUtilities} class. + * + * @return This object for method chaining + */ + default Builder credentialsProvider(AwsCredentialsProvider credentialsProvider) { + return credentialsProvider((IdentityProvider) credentialsProvider); + } + + /** + * The default credentials provider to use when working with the methods in {@link DsqlUtilities} class. + * + * @return This object for method chaining + */ + default Builder credentialsProvider(IdentityProvider credentialsProvider) { + throw new UnsupportedOperationException(); + } + + /** + * Create a {@link DsqlUtilities} + */ + DsqlUtilities build(); + } +} \ No newline at end of file diff --git a/services/dsql/src/main/java/software/amazon/awssdk/services/dsql/model/GenerateAuthTokenRequest.java b/services/dsql/src/main/java/software/amazon/awssdk/services/dsql/model/GenerateAuthTokenRequest.java new file mode 100644 index 000000000000..921f2be84ed1 --- /dev/null +++ b/services/dsql/src/main/java/software/amazon/awssdk/services/dsql/model/GenerateAuthTokenRequest.java @@ -0,0 +1,239 @@ +/* + * 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.dsql.model; + +import java.time.Duration; +import java.util.Objects; +import software.amazon.awssdk.annotations.NotThreadSafe; +import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.CredentialUtils; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dsql.DsqlUtilities; +import software.amazon.awssdk.utils.ToString; +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 authentication token for IAM database authentication for DSQL. + */ +@SdkPublicApi +public final class GenerateAuthTokenRequest implements + ToCopyableBuilder { + // The time the IAM token is good for based on RDS. https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html + private static final Duration EXPIRATION_DURATION = Duration.ofSeconds(900L); + + private final String hostname; + private final Region region; + private final Duration expiresIn; + private final IdentityProvider credentialsProvider; + + private GenerateAuthTokenRequest(BuilderImpl builder) { + this.hostname = Validate.notEmpty(builder.hostname, "hostname"); + this.region = builder.region; + this.credentialsProvider = builder.credentialsProvider; + this.expiresIn = (builder.expiresIn != null) ? builder.expiresIn : + EXPIRATION_DURATION; + } + + @Override + public String toString() { + return ToString.builder("GenerateAuthTokenRequest") + .add("hostname", hostname) + .add("region", region) + .add("expiresIn", expiresIn) + .add("credentialsProvider", credentialsProvider) + .build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GenerateAuthTokenRequest that = (GenerateAuthTokenRequest) o; + return Objects.equals(hostname, that.hostname) && + Objects.equals(region, that.region) && + Objects.equals(expiresIn, that.expiresIn) && + Objects.equals(credentialsProvider, that.credentialsProvider); + } + + @Override + public int hashCode() { + int hashCode = 1; + hashCode = 31 * hashCode + Objects.hashCode(hostname); + hashCode = 31 * hashCode + Objects.hashCode(region); + hashCode = 31 * hashCode + Objects.hashCode(expiresIn); + hashCode = 31 * hashCode + Objects.hashCode(credentialsProvider); + return hashCode; + } + + /** + * @return The hostname of the database to connect to + */ + public String hostname() { + return hostname; + } + + /** + * @return The token expiry duration + */ + public Duration expiresIn() { + return expiresIn; + } + + /** + * @return The region the database is hosted in. If specified, takes precedence over the value specified in + * {@link DsqlUtilities.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 DsqlUtilities.Builder#credentialsProvider}} + */ + public AwsCredentialsProvider credentialsProvider() { + return CredentialUtils.toCredentialsProvider(credentialsProvider); + } + + /** + * @return The credentials provider to sign the IAM auth request with. If specified, takes precedence over the value + * specified in {@link DsqlUtilities.Builder#credentialsProvider(AwsCredentialsProvider)}} + */ + public IdentityProvider credentialsIdentityProvider() { + return credentialsProvider; + } + + @Override + public Builder toBuilder() { + return new BuilderImpl(this); + } + + /** + * Creates a builder for {@link DsqlUtilities}. + */ + public static Builder builder() { + return new BuilderImpl(); + } + + /** + * A builder for a {@link GenerateAuthTokenRequest}, 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 region the database is hosted in. If specified, takes precedence over the value specified in + * {@link DsqlUtilities.Builder#region(Region)} + * + * @return This object for method chaining + */ + Builder region(Region region); + + /** + * The duration a token is valid for. + * + * @return This object for method chaining + */ + Builder expiresIn(Duration expiresIn); + + /** + * The credentials provider to sign the IAM auth request with. If specified, takes precedence over the value + * specified in {@link DsqlUtilities.Builder#credentialsProvider)}} + * + * @return This object for method chaining + */ + default Builder credentialsProvider(AwsCredentialsProvider credentialsProvider) { + return credentialsProvider((IdentityProvider) credentialsProvider); + } + + /** + * The credentials provider to sign the IAM auth request with. If specified, takes precedence over the value + * specified in {@link DsqlUtilities.Builder#credentialsProvider}} + * + * @return This object for method chaining + */ + default Builder credentialsProvider(IdentityProvider credentialsProvider) { + throw new UnsupportedOperationException(); + } + + @Override + GenerateAuthTokenRequest build(); + } + + private static final class BuilderImpl implements Builder { + private String hostname; + private Region region; + private Duration expiresIn; + private IdentityProvider credentialsProvider; + + private BuilderImpl() { + } + + private BuilderImpl(GenerateAuthTokenRequest request) { + this.hostname = request.hostname; + this.region = request.region; + this.expiresIn = request.expiresIn; + this.credentialsProvider = request.credentialsProvider; + } + + @Override + public Builder hostname(String hostname) { + this.hostname = hostname; + return this; + } + + @Override + public Builder expiresIn(Duration expiresIn) { + this.expiresIn = expiresIn; + return this; + } + + @Override + public Builder region(Region region) { + this.region = region; + return this; + } + + @Override + public Builder credentialsProvider(IdentityProvider credentialsProvider) { + this.credentialsProvider = credentialsProvider; + return this; + } + + @Override + public GenerateAuthTokenRequest build() { + return new GenerateAuthTokenRequest(this); + } + } +} \ No newline at end of file diff --git a/services/dsql/src/main/resources/codegen-resources/customization.config b/services/dsql/src/main/resources/codegen-resources/customization.config new file mode 100644 index 000000000000..a63c69ef3dd5 --- /dev/null +++ b/services/dsql/src/main/resources/codegen-resources/customization.config @@ -0,0 +1,9 @@ +{ + "utilitiesMethod": { + "returnType": "software.amazon.awssdk.services.dsql.DsqlUtilities", + "instanceType": "software.amazon.awssdk.services.dsql.DefaultDsqlUtilities", + "createMethodParams": [ + "clientConfiguration" + ] + } +} diff --git a/services/dsql/src/test/java/software/amazon/awssdk/services/dsql/DefaultDsqlUtilitiesTest.java b/services/dsql/src/test/java/software/amazon/awssdk/services/dsql/DefaultDsqlUtilitiesTest.java new file mode 100644 index 000000000000..db953c6061c1 --- /dev/null +++ b/services/dsql/src/test/java/software/amazon/awssdk/services/dsql/DefaultDsqlUtilitiesTest.java @@ -0,0 +1,213 @@ +/* + * 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.dsql; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import java.time.Clock; +import java.time.Duration; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.function.Consumer; +import org.junit.jupiter.api.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.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.IdentityProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dsql.DefaultDsqlUtilities.DefaultBuilder; +import software.amazon.awssdk.services.dsql.model.GenerateAuthTokenRequest; + +public class DefaultDsqlUtilitiesTest { + private final ZoneId utcZone = ZoneId.of("UTC").normalized(); + private final Clock fixedClock = Clock.fixed(ZonedDateTime.of(2024, 11, 7, 17, 39, 33, 0, utcZone).toInstant(), utcZone); + private static final String HOSTNAME = "test.dsql.us-east-1.on.aws"; + private static final String EXPECTED_TOKEN = "test.dsql.us-east-1.on.aws/?Action=DbConnect&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date" + + "=20241107T173933Z&X-Amz-SignedHeaders=host&X-Amz-Expires=900&X-Amz-Credential=access_key" + + "%2F20241107%2Fus-east-1%2Fdsql%2Faws4_request&X-Amz-Signature=e319d85380261f643d78a558f76257f05aacea758a6ccd42a2510e2ae0854a47"; + private static final String EXPECTED_ADMIN_TOKEN = "test.dsql.us-east-1.on.aws/?Action=DbConnectAdmin&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date" + + "=20241107T173933Z&X-Amz-SignedHeaders=host&X-Amz-Expires=900&X-Amz-Credential=access_key" + + "%2F20241107%2Fus-east-1%2Fdsql%2Faws4_request&X-Amz-Signature=a08adc4c84a490014ce374b90c98ba9ed015b77b451c0d9f9fb3f8ca8c6f9c36"; + + @Test + public void tokenGenerationWithBuilderDefaultsUsingAwsCredentialsProvider_isSuccessful() { + AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create("access_key", "secret_key")); + + DsqlUtilities.Builder builder = DsqlUtilities.builder() + .credentialsProvider(credentialsProvider) + .region(Region.US_EAST_1); + + tokenGenerationWithBuilderDefaults(builder); + } + + @Test + public void tokenGenerationWithBuilderDefaultsUsingIdentityProvider_isSuccessful() { + IdentityProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create("access_key", "secret_key")); + + DsqlUtilities.Builder utilitiesBuilder = DsqlUtilities.builder() + .credentialsProvider(credentialsProvider) + .region(Region.US_EAST_1); + + tokenGenerationWithBuilderDefaults(utilitiesBuilder); + } + + private void tokenGenerationWithBuilderDefaults(DsqlUtilities.Builder builder) { + DsqlUtilities dsqlUtilities = new DefaultDsqlUtilities((DefaultBuilder) builder, fixedClock); + + String authToken = dsqlUtilities.generateDbConnectAuthToken(b -> b.hostname(HOSTNAME)); + assertThat(authToken).isEqualTo(EXPECTED_TOKEN); + + String adminAuthToken = dsqlUtilities.generateDbConnectAdminAuthToken(b -> b.hostname(HOSTNAME)); + assertThat(adminAuthToken).isEqualTo(EXPECTED_ADMIN_TOKEN); + } + + @Test + public void tokenGenerationWithOverriddenCredentialsUsingAwsCredentialsProvider_isSuccessful() { + AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create("foo", "bar")); + + DsqlUtilities.Builder builder = DsqlUtilities.builder() + .credentialsProvider(credentialsProvider) + .region(Region.US_EAST_1); + + tokenGenerationWithOverriddenCredentials(builder, b -> b.credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("access_key", "secret_key")))); + } + + @Test + public void tokenGenerationWithOverriddenCredentialsUsingIdentityProvider_isSuccessful() { + IdentityProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create("foo", "bar")); + + DsqlUtilities.Builder builder = DsqlUtilities.builder() + .credentialsProvider(credentialsProvider) + .region(Region.US_EAST_1); + + tokenGenerationWithOverriddenCredentials(builder, b -> b.credentialsProvider( + (IdentityProvider) StaticCredentialsProvider.create(AwsBasicCredentials.create("access_key", "secret_key")))); + } + + private void tokenGenerationWithOverriddenCredentials(DsqlUtilities.Builder builder, + Consumer credsBuilder) { + DsqlUtilities dsqlUtilities = new DefaultDsqlUtilities((DefaultBuilder) builder, fixedClock); + + String authToken = dsqlUtilities.generateDbConnectAuthToken(b -> b.hostname(HOSTNAME).applyMutation(credsBuilder)); + assertThat(authToken).isEqualTo(EXPECTED_TOKEN); + + String adminAuthToken = dsqlUtilities.generateDbConnectAdminAuthToken(b -> b.hostname(HOSTNAME).applyMutation(credsBuilder)); + assertThat(adminAuthToken).isEqualTo(EXPECTED_ADMIN_TOKEN); + } + + @Test + public void tokenGenerationWithOverriddenRegion_isSuccessful() { + AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create("access_key", "secret_key")); + + DsqlUtilities.Builder builder = DsqlUtilities.builder() + .credentialsProvider(credentialsProvider) + .region(Region.US_WEST_2); + + DsqlUtilities utilities = new DefaultDsqlUtilities((DefaultBuilder) builder, fixedClock); + + String authToken = utilities.generateDbConnectAuthToken(b -> b.hostname(HOSTNAME).region(Region.US_EAST_1)); + assertThat(authToken).isEqualTo(EXPECTED_TOKEN); + + String adminAuthToken = utilities.generateDbConnectAdminAuthToken(b -> b.hostname(HOSTNAME).region(Region.US_EAST_1)); + assertThat(adminAuthToken).isEqualTo(EXPECTED_ADMIN_TOKEN); + } + + @Test + public void missingRegion_throwsException() { + AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create("access_key", "secret_key")); + + DsqlUtilities.Builder builder = DsqlUtilities.builder() + .credentialsProvider(credentialsProvider); + + DsqlUtilities utilities = new DefaultDsqlUtilities((DefaultBuilder) builder, fixedClock); + + assertThatThrownBy(() -> utilities.generateDbConnectAuthToken(b -> b.hostname(HOSTNAME))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Region must be provided in GenerateAuthTokenRequest or DsqlUtilities"); + + assertThatThrownBy(() -> utilities.generateDbConnectAdminAuthToken(b -> b.hostname(HOSTNAME))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Region must be provided in GenerateAuthTokenRequest or DsqlUtilities"); + } + + @Test + public void missingCredentials_throwsException() { + DsqlUtilities.Builder builder = DsqlUtilities.builder() + .region(Region.US_WEST_2); + + DsqlUtilities utilities = new DefaultDsqlUtilities((DefaultBuilder) builder, fixedClock); + + assertThatThrownBy(() -> utilities.generateDbConnectAuthToken(b -> b.hostname(HOSTNAME))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("CredentialsProvider must be provided in GenerateAuthTokenRequest or DsqlUtilities"); + + assertThatThrownBy(() -> utilities.generateDbConnectAdminAuthToken(b -> b.hostname(HOSTNAME))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("CredentialsProvider must be provided in GenerateAuthTokenRequest or DsqlUtilities"); + } + + @Test + public void missingHostname_throwsException() { + AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create("access_key", "secret_key")); + + DsqlUtilities.Builder builder = DsqlUtilities.builder() + .credentialsProvider(credentialsProvider); + + DsqlUtilities utilities = new DefaultDsqlUtilities((DefaultBuilder) builder, fixedClock); + + assertThatThrownBy(() -> utilities.generateDbConnectAuthToken(GenerateAuthTokenRequest.builder().build())) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("hostname"); + + assertThatThrownBy(() -> utilities.generateDbConnectAdminAuthToken(GenerateAuthTokenRequest.builder().build())) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("hostname"); + } + + @Test + public void tokenGenerationWithCustomExpiry_isSuccessful() { + AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create("access_key", "secret_key")); + + DsqlUtilities.Builder builder = DsqlUtilities.builder() + .credentialsProvider(credentialsProvider) + .region(Region.US_EAST_1); + + DsqlUtilities utilities = new DefaultDsqlUtilities((DefaultBuilder) builder, fixedClock); + Duration expiry = Duration.ofSeconds(3600L); + + String authToken = utilities.generateDbConnectAuthToken(b -> b.hostname(HOSTNAME).expiresIn(expiry)); + String expectedToken = "test.dsql.us-east-1.on.aws/?Action=DbConnect&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=" + + "20241107T173933Z&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Credential=access_key" + + "%2F20241107%2Fus-east-1%2Fdsql%2Faws4_request&X-Amz-Signature=63987ab6908fe81bfcaa5a5120444a8d012751992bcdfec351522db555232c51"; + assertThat(authToken).isEqualTo(expectedToken); + + String adminAuthToken = utilities.generateDbConnectAdminAuthToken(b -> b.hostname(HOSTNAME).expiresIn(expiry)); + String expectedAdminToken = "test.dsql.us-east-1.on.aws/?Action=DbConnectAdmin&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=" + + "20241107T173933Z&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Credential=access_key" + + "%2F20241107%2Fus-east-1%2Fdsql%2Faws4_request&X-Amz-Signature=46e02dfc8d6d07289b9e5910ccd9a50f1c3b798e48ccd92153255df508bd0b82"; + assertThat(adminAuthToken).isEqualTo(expectedAdminToken); + } +} \ No newline at end of file diff --git a/services/dsql/src/test/java/software/amazon/awssdk/services/dsql/GenerateAuthTokenRequestTest.java b/services/dsql/src/test/java/software/amazon/awssdk/services/dsql/GenerateAuthTokenRequestTest.java new file mode 100644 index 000000000000..9dc1c2825cb6 --- /dev/null +++ b/services/dsql/src/test/java/software/amazon/awssdk/services/dsql/GenerateAuthTokenRequestTest.java @@ -0,0 +1,29 @@ +/* + * 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.dsql; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.dsql.model.GenerateAuthTokenRequest; + +public class GenerateAuthTokenRequestTest { + + @Test + void equalsHashcode() { + EqualsVerifier.forClass(GenerateAuthTokenRequest.class) + .withNonnullFields("hostname", "region") + .verify(); + } +} \ No newline at end of file