Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Alternative option to RDS Library for IAM #867

Merged
merged 7 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@
AWS Identity and Access Management (IAM) grants users access control across all Amazon Web Services. IAM supports granular permissions, giving you the ability to grant different permissions to different users. For more information on IAM and it's use cases, please refer to the [IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html).

## Prerequisites
> :warning: **Note:** To preserve compatibility with customers using the community driver, IAM Authentication requires the [AWS Java SDK RDS v2.x](https://central.sonatype.com/artifact/software.amazon.awssdk/rds) to be included separately in the classpath. The AWS Java SDK RDS is a runtime dependency and must be resolved.
> [!WARNING]\
> To preserve compatibility with customers using the community driver, IAM Authentication requires the [AWS Java SDK RDS v2.x](https://central.sonatype.com/artifact/software.amazon.awssdk/rds) to be included separately in the classpath. The AWS Java SDK RDS is a runtime dependency and must be resolved.
> <br><br>
> Since [AWS Java SDK RDS v2.x](https://central.sonatype.com/artifact/software.amazon.awssdk/rds) size is around 5.4Mb (22Mb including all RDS SDK dependencies), some users may experience difficulties using the plugin due to limited available disk size. In such case, [AWS Java SDK RDS v2.x](https://central.sonatype.com/artifact/software.amazon.awssdk/rds) required dependency may be replaced with just two required dependencies which have a smaller footprint (around 300Kb in total):
> [`software.amazon.awssdk:http-client-spi`](https://central.sonatype.com/artifact/software.amazon.awssdk/http-client-spi)
> [`software.amazon.awssdk:auth`](https://central.sonatype.com/artifact/software.amazon.awssdk/auth)
> <br><br>
> It's recommended to use [AWS Java SDK RDS v2.x](https://central.sonatype.com/artifact/software.amazon.awssdk/rds) when it's possible.


To enable the IAM Authentication Connection Plugin, add the plugin code `iam` to the [`wrapperPlugins`](../UsingTheJdbcDriver.md#connection-plugin-manager-parameters) value, or to the current [driver profile](../UsingTheJdbcDriver.md#connection-plugin-manager-parameters).

Expand Down
4 changes: 4 additions & 0 deletions wrapper/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ dependencies {
implementation("org.checkerframework:checker-qual:3.42.0")
compileOnly("org.apache.httpcomponents:httpclient:4.5.14")
compileOnly("software.amazon.awssdk:rds:2.22.13")
compileOnly("software.amazon.awssdk:auth:2.22.13") // Required for IAM (light implementation)
compileOnly("software.amazon.awssdk:http-client-spi:2.22.13") // Required for IAM (light implementation)
compileOnly("software.amazon.awssdk:sts:2.23.13")
compileOnly("com.zaxxer:HikariCP:4.0.3") // Version 4.+ is compatible with Java 8
compileOnly("software.amazon.awssdk:secretsmanager:2.23.3")
Expand Down Expand Up @@ -60,6 +62,8 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-jdbc:2.7.13") // 2.7.13 is the last version compatible with Java 8
testImplementation("org.mockito:mockito-inline:4.11.0") // 4.11.0 is the last version compatible with Java 8
testImplementation("software.amazon.awssdk:rds:2.22.13")
testImplementation("software.amazon.awssdk:auth:2.22.13") // Required for IAM (light implementation)
testImplementation("software.amazon.awssdk:http-client-spi:2.22.13") // Required for IAM (light implementation)
testImplementation("software.amazon.awssdk:ec2:2.23.8")
testImplementation("software.amazon.awssdk:secretsmanager:2.23.3")
testImplementation("software.amazon.awssdk:sts:2.23.13")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@
import software.amazon.jdbc.plugin.DefaultConnectionPlugin;
import software.amazon.jdbc.plugin.DriverMetaDataConnectionPluginFactory;
import software.amazon.jdbc.plugin.ExecutionTimeConnectionPluginFactory;
import software.amazon.jdbc.plugin.IamAuthConnectionPluginFactory;
import software.amazon.jdbc.plugin.LogQueryConnectionPluginFactory;
import software.amazon.jdbc.plugin.dev.DeveloperConnectionPluginFactory;
import software.amazon.jdbc.plugin.efm.HostMonitoringConnectionPluginFactory;
import software.amazon.jdbc.plugin.failover.FailoverConnectionPluginFactory;
import software.amazon.jdbc.plugin.federatedauth.FederatedAuthPluginFactory;
import software.amazon.jdbc.plugin.iam.IamAuthConnectionPluginFactory;
import software.amazon.jdbc.plugin.readwritesplitting.ReadWriteSplittingPluginFactory;
import software.amazon.jdbc.plugin.staledns.AuroraStaleDnsPluginFactory;
import software.amazon.jdbc.plugin.strategy.fastestresponse.FastestResponseStrategyPluginFactory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@
import software.amazon.jdbc.plugin.DataCacheConnectionPlugin;
import software.amazon.jdbc.plugin.DefaultConnectionPlugin;
import software.amazon.jdbc.plugin.ExecutionTimeConnectionPlugin;
import software.amazon.jdbc.plugin.IamAuthConnectionPlugin;
import software.amazon.jdbc.plugin.LogQueryConnectionPlugin;
import software.amazon.jdbc.plugin.efm.HostMonitoringConnectionPlugin;
import software.amazon.jdbc.plugin.failover.FailoverConnectionPlugin;
import software.amazon.jdbc.plugin.iam.IamAuthConnectionPlugin;
import software.amazon.jdbc.plugin.readwritesplitting.ReadWriteSplittingPlugin;
import software.amazon.jdbc.plugin.staledns.AuroraStaleDnsPlugin;
import software.amazon.jdbc.plugin.strategy.fastestresponse.FastestResponseStrategyPlugin;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package software.amazon.jdbc.plugin;
package software.amazon.jdbc.plugin.iam;

import java.sql.Connection;
import java.sql.SQLException;
Expand All @@ -28,14 +28,16 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.rds.RdsUtilities;
import software.amazon.jdbc.AwsWrapperProperty;
import software.amazon.jdbc.HostSpec;
import software.amazon.jdbc.JdbcCallable;
import software.amazon.jdbc.PluginService;
import software.amazon.jdbc.PropertyDefinition;
import software.amazon.jdbc.authentication.AwsCredentialsManager;
import software.amazon.jdbc.plugin.AbstractConnectionPlugin;
import software.amazon.jdbc.plugin.TokenInfo;
import software.amazon.jdbc.util.IamAuthUtils;
import software.amazon.jdbc.util.Messages;
import software.amazon.jdbc.util.RdsUtils;
Expand Down Expand Up @@ -87,13 +89,45 @@
private final TelemetryGauge cacheSizeGauge;
private final TelemetryCounter fetchTokenCounter;

private IamTokenUtility iamTokenUtility = new RegularRdsUtility();

public IamAuthConnectionPlugin(final @NonNull PluginService pluginService) {

this.checkRequiredDependencies();

this.pluginService = pluginService;
this.telemetryFactory = pluginService.getTelemetryFactory();
this.cacheSizeGauge = telemetryFactory.createGauge("iam.tokenCache.size", () -> (long) tokenCache.size());
this.fetchTokenCounter = telemetryFactory.createCounter("iam.fetchToken.count");
}

private void checkRequiredDependencies() {
try {
// RegularRdsUtility requires AWS Java SDK RDS v2.x to be presented in classpath.
Class.forName("software.amazon.awssdk.services.rds.RdsUtilities");
} catch (final ClassNotFoundException e) {
ClassNotFoundException lastException = e;

Check warning on line 109 in wrapper/src/main/java/software/amazon/jdbc/plugin/iam/IamAuthConnectionPlugin.java

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused assignment

Variable `lastException` initializer `e` is redundant
// If SDK RDS isn't presented, try to check required dependency for LightRdsUtility.
try {
// LightRdsUtility requires "software.amazon.awssdk.auth"
// and "software.amazon.awssdk.http-client-spi" libraries.
Class.forName("software.amazon.awssdk.http.SdkHttpFullRequest");
Class.forName("software.amazon.awssdk.auth.signer.params.Aws4PresignerParams");

// Required libraries are presented. Use lighter version of RDS utility.
iamTokenUtility = new LightRdsUtility();
lastException = null;

} catch (final ClassNotFoundException ex) {
throw new RuntimeException(Messages.get("IamAuthConnectionPlugin.javaSdkNotInClasspath"), ex);
}

if (lastException != null) {

Check warning on line 125 in wrapper/src/main/java/software/amazon/jdbc/plugin/iam/IamAuthConnectionPlugin.java

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Constant values

Condition `lastException != null` is always `false`
sergiyvamz marked this conversation as resolved.
Show resolved Hide resolved
throw new RuntimeException(Messages.get("IamAuthConnectionPlugin.javaSdkNotInClasspath"), lastException);
}
}
}

@Override
public Set<String> getSubscribedMethods() {
return subscribedMethods;
Expand Down Expand Up @@ -230,16 +264,16 @@

try {
final String user = PropertyDefinition.USER.getString(props);
final RdsUtilities utilities = RdsUtilities.builder()
.credentialsProvider(AwsCredentialsManager.getProvider(originalHostSpec, props))
.region(region)
.build();
return utilities.generateAuthenticationToken((builder) ->
builder
.hostname(hostname)
.port(port)
.username(user)
);
if (StringUtils.isNullOrEmpty(user)) {
throw new RuntimeException(
Messages.get(
"IamAuthConnectionPlugin.missingRequiredConfigParameter",
new Object[] {PropertyDefinition.USER.name}));
}

final AwsCredentialsProvider credentialsProvider = AwsCredentialsManager.getProvider(originalHostSpec, props);
return this.iamTokenUtility.generateAuthenticationToken(credentialsProvider, region, hostname, port, user);

} catch (Exception ex) {
telemetryContext.setSuccess(false);
telemetryContext.setException(ex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package software.amazon.jdbc.plugin;
package software.amazon.jdbc.plugin.iam;

import java.util.Properties;
import software.amazon.jdbc.ConnectionPlugin;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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.
* 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 software.amazon.jdbc.plugin.iam;

import org.checkerframework.checker.nullness.qual.NonNull;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.regions.Region;

public interface IamTokenUtility {

String generateAuthenticationToken(
final @NonNull AwsCredentialsProvider credentialsProvider,
final @NonNull Region region,
final @NonNull String hostname,
final int port,
final @NonNull String username);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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.
* 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 software.amazon.jdbc.plugin.iam;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.logging.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
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.http.SdkHttpFullRequest;
import software.amazon.awssdk.http.SdkHttpMethod;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.utils.CompletableFutureUtils;
import software.amazon.awssdk.utils.StringUtils;
import software.amazon.jdbc.util.Messages;

public class LightRdsUtility implements IamTokenUtility {

private static final Logger LOGGER = Logger.getLogger(LightRdsUtility.class.getName());

// 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);
sergiyvamz marked this conversation as resolved.
Show resolved Hide resolved

@Override
public String generateAuthenticationToken(
final @NonNull AwsCredentialsProvider credentialsProvider,
final @NonNull Region region,
final @NonNull String hostname,
final int port,
final @NonNull String username) {

// The following code is inspired by software.amazon.awssdk.services.rds.DefaultRdsUtilities,
// method generateAuthenticationToken(GenerateAuthenticationTokenRequest request).
// Update this code when original method changes.

final Clock clock = Clock.systemUTC();
final Aws4Signer signer = Aws4Signer.create();

SdkHttpFullRequest httpRequest = SdkHttpFullRequest.builder()
.method(SdkHttpMethod.GET)
.protocol("https")
.host(hostname)
.port(port)
.encodedPath("/")
.putRawQueryParameter("DBUser", username)
.putRawQueryParameter("Action", "connect")
.build();

Instant expirationTime = Instant.now(clock).plus(EXPIRATION_DURATION);

final AwsCredentials credentials = CredentialUtils.toCredentials(
CompletableFutureUtils.joinLikeSync(credentialsProvider.resolveIdentity()));

Aws4PresignerParams presignRequest = Aws4PresignerParams.builder()
.signingClockOverride(clock)
.expirationTime(expirationTime)
.awsCredentials(credentials)
.signingName("rds-db")
.signingRegion(region)
.build();

SdkHttpFullRequest fullRequest = signer.presign(httpRequest, presignRequest);
String signedUrl = fullRequest.getUri().toString();

// Format should be:
// <hostname>>:<port>>/?Action=connect&DBUser=<username>>&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expi...
// Note: This must be the real RDS hostname, not proxy or tunnels
String result = StringUtils.replacePrefixIgnoreCase(signedUrl, "https://", "");
LOGGER.finest(() -> "Generated RDS authentication token with expiration of " + expirationTime);
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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.
* 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 software.amazon.jdbc.plugin.iam;

import org.checkerframework.checker.nullness.qual.NonNull;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.rds.RdsUtilities;

public class RegularRdsUtility implements IamTokenUtility {

@Override
public String generateAuthenticationToken(
final @NonNull AwsCredentialsProvider credentialsProvider,
final @NonNull Region region,
final @NonNull String hostname,
final int port,
final @NonNull String username) {

final RdsUtilities utilities = RdsUtilities.builder()
.credentialsProvider(credentialsProvider)
.region(region)
.build();
return utilities.generateAuthenticationToken((builder) ->
builder
.hostname(hostname)
.port(port)
.username(username)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ IamAuthConnectionPlugin.useCachedIamToken=Use cached IAM token = ''{0}''
IamAuthConnectionPlugin.generatedNewIamToken=Generated new IAM token = ''{0}''
IamAuthConnectionPlugin.unhandledException=Unhandled exception: ''{0}''
IamAuthConnectionPlugin.connectException=Error occurred while opening a connection: ''{0}''
IamAuthConnectionPlugin.javaSdkNotInClasspath=Required dependency 'AWS IAM Authentication Plugin' is not on the classpath.
IamAuthConnectionPlugin.missingRequiredConfigParameter=Configuration parameter ''{0}'' is required.

# Log Query Connection Plugin
LogQueryConnectionPlugin.executingQuery=[{0}] Executing query: {1}
Expand Down
Loading
Loading