Skip to content

Commit

Permalink
FISH-7866 AWS SDK Security Token Service (STS) support
Browse files Browse the repository at this point in the history
  • Loading branch information
jGauravGupta committed Feb 15, 2024
1 parent 1e98eb5 commit 9440d30
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 45 deletions.
4 changes: 4 additions & 0 deletions AmazonSQS/AmazonSQSJCAAPI/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,9 @@ holder.
<groupId>software.amazon.awssdk</groupId>
<artifactId>sso</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sts</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;

/**
Expand All @@ -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 {
Expand Down Expand Up @@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
}


Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, STSCredentialsProvider> 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));
}
}
13 changes: 11 additions & 2 deletions AmazonSQS/AmazonSQSRAR/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ holder.
<name>Amazon SQS JCA Adapter RAR</name>
<description>RAR for the Amazon SQS JCA Adapter</description>
<url>http://www.payara.fish</url>
<properties>
<awssdk.version>2.23.3</awssdk.version>
</properties>
<dependencies>
<dependency>
<groupId>fish.payara.cloud.connectors.amazonsqs</groupId>
Expand All @@ -62,13 +65,19 @@ holder.
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sqs</artifactId>
<version>2.23.3</version>
<version>${awssdk.version}</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sso</artifactId>
<version>2.23.3</version>
<version>${awssdk.version}</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sts</artifactId>
<version>${awssdk.version}</version>
<type>jar</type>
</dependency>
</dependencies>
Expand Down

0 comments on commit 9440d30

Please sign in to comment.