Skip to content

Commit

Permalink
Alternative option to RDS Library for IAM (aws#867)
Browse files Browse the repository at this point in the history
  • Loading branch information
sergiyvamz authored and jasonli-improving committed Feb 2, 2024
1 parent cc5745c commit 2336483
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 19 deletions.
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,39 @@ public class IamAuthConnectionPlugin extends AbstractConnectionPlugin {
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) {
// 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();

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

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

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);

@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

0 comments on commit 2336483

Please sign in to comment.