From 9440d30113346c9e1eef80f2853cb52a24bb7d35 Mon Sep 17 00:00:00 2001 From: Gaurav Gupta Date: Thu, 15 Feb 2024 09:22:52 +0530 Subject: [PATCH] FISH-7866 AWS SDK Security Token Service (STS) support --- AmazonSQS/AmazonSQSJCAAPI/pom.xml | 4 + .../api/inbound/AmazonSQSActivationSpec.java | 49 ++++--- .../outbound/AmazonSQSManagedConnection.java | 25 +--- .../AmazonSQSManagedConnectionFactory.java | 30 +++- .../api/outbound/STSCredentialsProvider.java | 138 ++++++++++++++++++ AmazonSQS/AmazonSQSRAR/pom.xml | 13 +- 6 files changed, 214 insertions(+), 45 deletions(-) create mode 100644 AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/outbound/STSCredentialsProvider.java diff --git a/AmazonSQS/AmazonSQSJCAAPI/pom.xml b/AmazonSQS/AmazonSQSJCAAPI/pom.xml index d880b095..a02a5b6c 100644 --- a/AmazonSQS/AmazonSQSJCAAPI/pom.xml +++ b/AmazonSQS/AmazonSQSJCAAPI/pom.xml @@ -74,5 +74,9 @@ holder. software.amazon.awssdk sso + + software.amazon.awssdk + sts + diff --git a/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/inbound/AmazonSQSActivationSpec.java b/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/inbound/AmazonSQSActivationSpec.java index 8ed02dcd..c5899cd8 100644 --- a/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/inbound/AmazonSQSActivationSpec.java +++ b/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/inbound/AmazonSQSActivationSpec.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright (c) 2017-2022 Payara Foundation and/or its affiliates. All rights reserved. + * Copyright (c) 2017-2024 Payara Foundation and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development @@ -40,16 +40,19 @@ package fish.payara.cloud.connectors.amazonsqs.api.inbound; import fish.payara.cloud.connectors.amazonsqs.api.AmazonSQSListener; +import fish.payara.cloud.connectors.amazonsqs.api.outbound.STSCredentialsProvider; import jakarta.resource.ResourceException; import jakarta.resource.spi.Activation; import jakarta.resource.spi.ActivationSpec; import jakarta.resource.spi.InvalidPropertyException; import jakarta.resource.spi.ResourceAdapter; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; +import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.utils.StringUtils; /** @@ -73,6 +76,8 @@ public class AmazonSQSActivationSpec implements ActivationSpec, AwsCredentialsPr private String messageAttributeNames = "All"; private String attributeNames = "All"; private String profileName; + private String roleArn; + private String roleSessionName; @Override public void validate() throws InvalidPropertyException { @@ -183,28 +188,32 @@ public void setProfileName(String profileName) { this.profileName = profileName; } + public String getRoleArn() { + return roleArn; + } + + public void setRoleArn(String roleArn) { + this.roleArn = roleArn; + } + + public String getRoleSessionName() { + return roleSessionName; + } + + public void setRoleSessionName(String roleSessionName) { + this.roleSessionName = roleSessionName; + } + @Override public AwsCredentials resolveCredentials() { - // Return Credentials based on what is present, profileName taking priority. - if (StringUtils.isBlank(getProfileName())) { - if (StringUtils.isNotBlank(awsAccessKeyId) && StringUtils.isNotBlank(awsSecretKey)) { - return new AwsCredentials() { - @Override - public String accessKeyId() { - return awsAccessKeyId; - } - - @Override - public String secretAccessKey() { - return awsSecretKey; - } - }; - } else { - return DefaultCredentialsProvider.create().resolveCredentials(); - } - - } else { + if (StringUtils.isNotBlank(getRoleArn())) { + return STSCredentialsProvider.create(getRoleArn(), getRoleSessionName(), Region.of(getRegion())).resolveCredentials(); + } else if (StringUtils.isNotBlank(getProfileName())) { return ProfileCredentialsProvider.builder().profileName(getProfileName()).build().resolveCredentials(); + } else if (StringUtils.isNotBlank(getAwsAccessKeyId()) && StringUtils.isNotBlank(getAwsSecretKey())) { + return AwsBasicCredentials.create(getAwsAccessKeyId(), getAwsSecretKey()); + } else { + return DefaultCredentialsProvider.create().resolveCredentials(); } } } diff --git a/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/outbound/AmazonSQSManagedConnection.java b/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/outbound/AmazonSQSManagedConnection.java index e6a27a00..f39c7a12 100644 --- a/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/outbound/AmazonSQSManagedConnection.java +++ b/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/outbound/AmazonSQSManagedConnection.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright (c) 2017-2022 Payara Foundation and/or its affiliates. All rights reserved. + * Copyright (c) 2017-2024 Payara Foundation and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development @@ -57,7 +57,7 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; @@ -200,25 +200,12 @@ public void close() throws Exception { private AwsCredentialsProvider getCredentials(AmazonSQSManagedConnectionFactory aThis) { AwsCredentialsProvider credentialsProvider; - if (StringUtils.isNotBlank(aThis.getProfileName())) { + if (StringUtils.isNotBlank(aThis.getRoleArn())) { + credentialsProvider = STSCredentialsProvider.create(aThis.getRoleArn(), aThis.getRoleSessionName(), Region.of(aThis.getRegion())); + } else if (StringUtils.isNotBlank(aThis.getProfileName())) { credentialsProvider = ProfileCredentialsProvider.create(aThis.getProfileName()); } else if (StringUtils.isNotBlank(aThis.getAwsAccessKeyId()) && StringUtils.isNotBlank(aThis.getAwsSecretKey())) { - credentialsProvider = new AwsCredentialsProvider(){ - @Override - public AwsCredentials resolveCredentials() { - return new AwsCredentials() { - @Override - public String accessKeyId() { - return aThis.getAwsAccessKeyId(); - } - - @Override - public String secretAccessKey() { - return aThis.getAwsSecretKey(); - } - }; - } - }; + credentialsProvider = () -> AwsBasicCredentials.create(aThis.getAwsAccessKeyId(), aThis.getAwsSecretKey()); } else { credentialsProvider = DefaultCredentialsProvider.create(); } diff --git a/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/outbound/AmazonSQSManagedConnectionFactory.java b/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/outbound/AmazonSQSManagedConnectionFactory.java index 710fc4e2..24825a83 100644 --- a/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/outbound/AmazonSQSManagedConnectionFactory.java +++ b/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/outbound/AmazonSQSManagedConnectionFactory.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright (c) 2017-2022 Payara Foundation and/or its affiliates. All rights reserved. + * Copyright (c) 2017-2024 Payara Foundation and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development @@ -76,6 +76,12 @@ public class AmazonSQSManagedConnectionFactory implements ManagedConnectionFacto @ConfigProperty(description = "AWS Profile Name", type = String.class) private String profileName; + @ConfigProperty(description = "AWS Role ARN", type = String.class) + private String roleArn; + + @ConfigProperty(description = "AWS Session name", type = String.class) + private String roleSessionName; + private PrintWriter logger; public String getAwsSecretKey() { @@ -110,10 +116,21 @@ public void setProfileName(String profileName) { this.profileName = profileName; } - public AmazonSQSManagedConnectionFactory() { + public String getRoleArn() { + return roleArn; + } + + public void setRoleArn(String roleArn) { + this.roleArn = roleArn; } + public String getRoleSessionName() { + return roleSessionName; + } + public void setRoleSessionName(String roleSessionName) { + this.roleSessionName = roleSessionName; + } @Override public Object createConnectionFactory(ConnectionManager cxManager) throws ResourceException { @@ -148,11 +165,13 @@ public PrintWriter getLogWriter() throws ResourceException { @Override public int hashCode() { - int hash = 5; + int hash = 7; hash = 97 * hash + Objects.hashCode(this.awsSecretKey); hash = 97 * hash + Objects.hashCode(this.awsAccessKeyId); hash = 97 * hash + Objects.hashCode(this.region); hash = 97 * hash + Objects.hashCode(this.profileName); + hash = 97 * hash + Objects.hashCode(this.roleArn); + hash = 97 * hash + Objects.hashCode(this.roleSessionName); return hash; } @@ -180,7 +199,10 @@ public boolean equals(Object obj) { if (!Objects.equals(this.profileName, other.profileName)) { return false; } - return true; + if (!Objects.equals(this.roleArn, other.roleArn)) { + return false; + } + return Objects.equals(this.roleSessionName, other.roleSessionName); } diff --git a/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/outbound/STSCredentialsProvider.java b/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/outbound/STSCredentialsProvider.java new file mode 100644 index 00000000..13090e18 --- /dev/null +++ b/AmazonSQS/AmazonSQSJCAAPI/src/main/java/fish/payara/cloud/connectors/amazonsqs/api/outbound/STSCredentialsProvider.java @@ -0,0 +1,138 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2024 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package fish.payara.cloud.connectors.amazonsqs.api.outbound; + +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; +import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; +import software.amazon.awssdk.services.sts.model.Credentials; + +/** + * AWS STS Credentials Provider with caching and thread safety. + * + * This class provides AWS credentials by assuming a role using the AWS Security Token Service (STS). + * It caches the credentials and ensures thread safety using locks. + * + * @author Gaurav Gupta + */ +public class STSCredentialsProvider implements AwsCredentialsProvider { + + private static final Logger LOGGER = Logger.getLogger(STSCredentialsProvider.class.getName()); + private static final Duration EXPIRATION_THRESHOLD = Duration.ofMinutes(5); + private final String roleArn; + private final String roleSessionName; + private final Region region; + private volatile AwsSessionCredentials cachedCredentials; + private volatile Instant expirationTime; + private final Lock lock = new ReentrantLock(); + private static final Map providerInstances = new HashMap<>(); + + /** + * Returns a singleton instance of STSCredentialsProvider for a unique session name. + * + * @param roleArn The ARN of the role to assume. + * @param roleSessionName The name of the role session. + * @param region The AWS region. + * @return The STSCredentialsProvider instance. + */ + public static STSCredentialsProvider create(String roleArn, String roleSessionName, Region region) { + String uniqueSessionKey = roleSessionName + "@" + region.id(); + return providerInstances.computeIfAbsent(uniqueSessionKey, key -> new STSCredentialsProvider(roleArn, roleSessionName, region)); + } + + private STSCredentialsProvider(String roleArn, String roleSessionName, Region region) { + this.roleArn = roleArn; + this.roleSessionName = roleSessionName; + this.region = region; + } + + @Override + public AwsCredentials resolveCredentials() { + if (cachedCredentials != null && !isCredentialsExpired()) { + LOGGER.fine("Reusing cached AWS session credentials"); + return cachedCredentials; + } else { + lock.lock(); + try { + if (cachedCredentials != null && !isCredentialsExpired()) { + LOGGER.fine("Reusing cached AWS session credentials after lock"); + return cachedCredentials; + } + LOGGER.fine("Cached AWS session credentials expired or not present"); + + StsClient stsClient = StsClient.builder().region(region).build(); + AssumeRoleRequest assumeRoleRequest = AssumeRoleRequest.builder() + .roleArn(roleArn) + .roleSessionName(roleSessionName) + .build(); + + AssumeRoleResponse assumeRoleResponse = stsClient.assumeRole(assumeRoleRequest); + Credentials stsCredentials = assumeRoleResponse.credentials(); + cachedCredentials = AwsSessionCredentials.create( + stsCredentials.accessKeyId(), + stsCredentials.secretAccessKey(), + stsCredentials.sessionToken() + ); + expirationTime = stsCredentials.expiration(); + LOGGER.log(Level.FINE, "Obtained new AWS session credentials - Session Token: {0}, Expiration Time: {1}", new Object[]{stsCredentials.sessionToken(), stsCredentials.expiration()}); + return cachedCredentials; + } finally { + lock.unlock(); + } + } + } + + private boolean isCredentialsExpired() { + // Check if the credentials are expired or about to expire + return expirationTime == null || Instant.now().isAfter(expirationTime.minus(EXPIRATION_THRESHOLD)); + } +} diff --git a/AmazonSQS/AmazonSQSRAR/pom.xml b/AmazonSQS/AmazonSQSRAR/pom.xml index 81de0f5d..f654dab0 100644 --- a/AmazonSQS/AmazonSQSRAR/pom.xml +++ b/AmazonSQS/AmazonSQSRAR/pom.xml @@ -53,6 +53,9 @@ holder. Amazon SQS JCA Adapter RAR RAR for the Amazon SQS JCA Adapter http://www.payara.fish + + 2.23.3 + fish.payara.cloud.connectors.amazonsqs @@ -62,13 +65,19 @@ holder. software.amazon.awssdk sqs - 2.23.3 + ${awssdk.version} jar software.amazon.awssdk sso - 2.23.3 + ${awssdk.version} + jar + + + software.amazon.awssdk + sts + ${awssdk.version} jar