Skip to content

Commit 6776a32

Browse files
authored
Merge pull request #34 from akshayar/master
Support for assuming role before accessing KeySpaces, helps in cross account access
2 parents c2b35c2 + 1ed563a commit 6776a32

File tree

9 files changed

+244
-18
lines changed

9 files changed

+244
-18
lines changed

README.md

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This package implements an authentication plugin for the open-source Datastax Ja
88

99
The plugin depends on the AWS SDK for Java. It uses `AWSCredentialsProvider` to obtain credentials. Because the IAuthenticator interface operates at the level of `InetSocketAddress`, you must specify the service endpoint to use for the connection.
1010
You can provide the Region in the constructor programmatically, via the `AWS_REGION` environment variable, or via the `aws.region` system property.
11+
You can also provide an IAM role to assume for access to KeySpaces, programmatically or via the configuration file.
1112

1213
The full documentation for the plugin is available at
1314
https://docs.aws.amazon.com/keyspaces/latest/devguide/programmatic.credentials.html#programmatic.credentials.SigV4_KEYSPACES.
@@ -35,28 +36,40 @@ You can specify the Region using one of the following four methods:
3536
* Constructor
3637
* Configuration
3738

38-
## Environment Variable
39+
### Environment Variable
3940

4041
You can use the `AWS_REGION` environment variable to match the endpoint that you are communicating with by setting it as part of your application start-up, as follows.
4142

4243
``` shell
4344
$ export AWS_Region=us-east-1
4445
```
45-
## System Property
46+
### System Property
4647

4748
You can use the `aws.region` Java system property by specifying it on the command line, as follows.
4849

4950
``` shell
5051
$ java -Daws.region=us=east-1 ...
5152
```
5253

53-
## Constructor
54+
### Constructor
5455

5556
One of the constructors for `software.aws.mcs.auth.SigV4AuthProvider` takes a `String` representing the Region that will be used for that instance.
5657

57-
## Configuration
58+
### Configuration
5859

59-
Set the Region explicitly in your `advanced.auth-provider.class` configuration (see example below), by specifying the `advanced.auth-provider.aws-region` property.
60+
Set the Region explicitly in your `advanced.auth-provider` configuration (see example below), by specifying the `advanced.auth-provider.aws-region` property.
61+
62+
## Assume IAM Role Configuration
63+
64+
You can specify an IAM role to assume for access to KeySpaces using either the constructor or the driver configuration file
65+
66+
### Constructor
67+
68+
One of the constructors for `software.aws.mcs.auth.SigV4AuthProvider` takes two Strings , the first representing the region and the second representing the ARN of the IAM role to assume.
69+
70+
### Configuration
71+
72+
Set the IAM Role explicitly in your `advanced.auth-provider` configuration (see example below), by specifying the `advanced.auth-provider.aws-role-arn` property.
6073

6174
## Add the Authentication Plugin to the Application
6275

@@ -119,7 +132,7 @@ To use the configuration file, set the `advanced.auth-provider.class` to `softwa
119132
1. Set the `advanced.auth-provider.class` to `software.aws.mcs.auth.SigV4AuthProvider`.
120133
1. Set `basic.load-balancing-policy.local-datacenter` to the region name. In this case, use `us-east-2`.
121134

122-
The following is an example of this.
135+
The following is an example of this config without explicit role to be assumed.
123136

124137
``` text
125138
datastax-java-driver {
@@ -138,3 +151,24 @@ The following is an example of this.
138151
}
139152
}
140153
```
154+
155+
The following is an example of this config with an explicit role to be assumed.
156+
157+
``` text
158+
datastax-java-driver {
159+
basic.load-balancing-policy {
160+
class = DefaultLoadBalancingPolicy
161+
local-datacenter = us-east-2
162+
}
163+
advanced {
164+
auth-provider = {
165+
class = software.aws.mcs.auth.SigV4AuthProvider
166+
aws-region = us-east-2
167+
aws-role-arn = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME"
168+
}
169+
ssl-engine-factory {
170+
class = DefaultSslEngineFactory
171+
}
172+
}
173+
}
174+
```

pom.xml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<modelVersion>4.0.0</modelVersion>
44
<groupId>software.aws.mcs</groupId>
55
<artifactId>aws-sigv4-auth-cassandra-java-driver-plugin</artifactId>
6-
<version>4.0.9</version>
6+
<version>4.0.10</version>
77
<name>AWS SigV4 Auth Java Driver 4.x Plugin</name>
88
<description>A Plugin to allow SigV4 authentication for Java Cassandra drivers with Amazon MCS</description>
99
<url>https://github.com/aws/aws-sigv4-auth-cassandra-java-driver-plugin</url>
@@ -44,6 +44,7 @@
4444
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
4545
<jackson.version>2.13.4</jackson.version>
4646
<jackson-databind.version>2.13.4.2</jackson-databind.version>
47+
<aws-sdk-version>2.15.66</aws-sdk-version>
4748
</properties>
4849

4950
<dependencies>
@@ -88,7 +89,13 @@
8889
<dependency>
8990
<groupId>software.amazon.awssdk</groupId>
9091
<artifactId>auth</artifactId>
91-
<version>2.15.66</version>
92+
<version>${aws-sdk-version}</version>
93+
</dependency>
94+
<!-- https://mvnrepository.com/artifact/software.amazon.awssdk/sts -->
95+
<dependency>
96+
<groupId>software.amazon.awssdk</groupId>
97+
<artifactId>sts</artifactId>
98+
<version>${aws-sdk-version}</version>
9299
</dependency>
93100
</dependencies>
94101

src/main/java/software/aws/mcs/auth/SigV4AuthProvider.java

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.Arrays;
3333
import java.util.Collections;
3434
import java.util.List;
35+
import java.util.Optional;
3536
import java.util.concurrent.CompletableFuture;
3637
import java.util.concurrent.CompletionStage;
3738
import javax.crypto.Mac;
@@ -53,6 +54,9 @@
5354
import software.amazon.awssdk.auth.signer.internal.SignerConstant;
5455
import software.amazon.awssdk.regions.Region;
5556
import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain;
57+
import software.amazon.awssdk.services.sts.StsClient;
58+
import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider;
59+
import software.amazon.awssdk.services.sts.model.AssumeRoleRequest;
5660

5761
import static software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider.create;
5862

@@ -100,11 +104,9 @@ public SigV4AuthProvider() {
100104
this(create(), null);
101105
}
102106

103-
private final static DriverOption REGION_OPTION = new DriverOption() {
104-
public String getPath() {
105-
return "advanced.auth-provider.aws-region";
106-
}
107-
};
107+
private final static DriverOption REGION_OPTION = () -> "advanced.auth-provider.aws-region";
108+
109+
private final static DriverOption ROLE_OPTION = () -> "advanced.auth-provider.aws-role-arn";
108110

109111
/**
110112
* This constructor is provided so that the driver can create
@@ -130,7 +132,8 @@ public String getPath() {
130132
* Unused for this plugin.
131133
*/
132134
public SigV4AuthProvider(DriverContext driverContext) {
133-
this(driverContext.getConfig().getDefaultProfile().getString(REGION_OPTION, null));
135+
this(driverContext.getConfig().getDefaultProfile().getString(REGION_OPTION, getDefaultRegion()),
136+
driverContext.getConfig().getDefaultProfile().getString(ROLE_OPTION, null));
134137
}
135138

136139
/**
@@ -143,6 +146,17 @@ public SigV4AuthProvider(final String region) {
143146
this(create(), region);
144147
}
145148

149+
/**
150+
* Create a new Provider, using the specified region and IAM role to assume.
151+
* @param region the region (e.g. us-east-1) to use for signing. A
152+
* null value indicates to use the AWS_REGION environment
153+
* variable, or the "aws.region" system property to configure it.
154+
* @param roleArn The IAM Role ARN which the connecting client should assume before connecting with Amazon Keyspaces.
155+
*/
156+
public SigV4AuthProvider(final String region,final String roleArn) {
157+
this(Optional.ofNullable(roleArn).map(r->(AwsCredentialsProvider)createSTSRoleCredentialProvider(r,region)).orElse(create()), region);
158+
}
159+
146160
/**
147161
* Create a new Provider, using the specified AWSCredentialsProvider and region.
148162
* @param credentialsProvider the credentials provider used to obtain signature material
@@ -154,9 +168,7 @@ public SigV4AuthProvider(@NotNull AwsCredentialsProvider credentialsProvider, fi
154168
this.credentialsProvider = credentialsProvider;
155169

156170
if (region == null) {
157-
DefaultAwsRegionProviderChain chain = new DefaultAwsRegionProviderChain();
158-
Region defaultRegion = chain.getRegion();
159-
this.signingRegion = defaultRegion.toString().toLowerCase();
171+
this.signingRegion = getDefaultRegion();
160172
} else {
161173
this.signingRegion = region.toLowerCase();
162174
}
@@ -373,4 +385,36 @@ static int indexOf(byte[] target, byte[] pattern) {
373385
// Loop exhaustion means we did not find it
374386
return -1;
375387
}
388+
389+
390+
/**
391+
* Creates a STS role credential provider
392+
* @param roleArn The ARN of the role to assume
393+
* @param stsRegion The region of the STS endpoint
394+
* @return The STS role credential provider
395+
*/
396+
private static StsAssumeRoleCredentialsProvider createSTSRoleCredentialProvider(@NotNull String roleArn,
397+
@NotNull String stsRegion) {
398+
final String sessionName="keyspaces-session-"+System.currentTimeMillis();
399+
StsClient stsClient = StsClient.builder()
400+
.region(Region.of(stsRegion))
401+
.build();
402+
AssumeRoleRequest assumeRoleRequest=AssumeRoleRequest.builder()
403+
.roleArn(roleArn)
404+
.roleSessionName(sessionName)
405+
.build();
406+
return StsAssumeRoleCredentialsProvider.builder()
407+
.stsClient(stsClient)
408+
.refreshRequest(assumeRoleRequest)
409+
.build();
410+
}
411+
412+
/**
413+
* Gets the default region for SigV4 if region is not provided.
414+
* @return Default region
415+
*/
416+
private static String getDefaultRegion() {
417+
DefaultAwsRegionProviderChain chain = new DefaultAwsRegionProviderChain();
418+
return chain.getRegion().toString().toLowerCase();
419+
}
376420
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package software.aws.mcs.auth;
2+
3+
/*-
4+
* #%L
5+
* AWS SigV4 Auth Java Driver 4.x Plugin
6+
* %%
7+
* Copyright (C) 2020-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
8+
* %%
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* #L%
21+
*/
22+
23+
import com.datastax.oss.driver.api.core.CqlSession;
24+
import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
25+
import com.datastax.oss.driver.api.core.cql.ResultSet;
26+
import com.datastax.oss.driver.api.core.cql.Row;
27+
import org.junit.platform.commons.util.StringUtils;
28+
29+
import java.io.File;
30+
import java.net.InetSocketAddress;
31+
import java.net.URL;
32+
import java.util.ArrayList;
33+
import java.util.Optional;
34+
35+
public class TestSigV4AssumeRoleConfig {
36+
static String KEYSPACES_DEFAULT_CONF="keyspaces-reference-norole.conf";
37+
/**
38+
* Before executing this test, ensure that KeySpaces tables are created.
39+
* Refer ddl.cql and dml.cql to create and populate tables.
40+
* @param args
41+
* @throws Exception
42+
*/
43+
public static void main(String[] args) throws Exception {
44+
String keySpacesConf=KEYSPACES_DEFAULT_CONF;
45+
if(args.length>1){
46+
keySpacesConf=args[0];
47+
System.out.println("Using key spaces config file: "+keySpacesConf);
48+
keySpacesConf=Optional.of(keySpacesConf).filter(StringUtils::isBlank).orElse(KEYSPACES_DEFAULT_CONF);
49+
}
50+
51+
URL url = TestSigV4AssumeRoleConfig.class.getClassLoader().getResource(keySpacesConf);
52+
53+
File file = new File(url.toURI());
54+
// The CqlSession object is the main entry point of the driver.
55+
// It holds the known state of the actual Cassandra cluster (notably the Metadata).
56+
// This class is thread-safe, you should create a single instance (per target Cassandra cluster), and share
57+
// it throughout your application.
58+
try (CqlSession session = CqlSession.builder()
59+
.withConfigLoader(DriverConfigLoader.fromFile(file))
60+
.build()) {
61+
62+
// We use execute to send a query to Cassandra. This returns a ResultSet, which is essentially a collection
63+
// of Row objects.
64+
ResultSet rs = session.execute("select * from testkeyspace.testconf");
65+
// Extract the first row (which is the only one in this case).
66+
Row row = rs.one();
67+
68+
// Extract the value of the first (and only) column from the row.
69+
String releaseVersion = row.getString("category");
70+
System.out.printf("Cassandra version is: %s%n", releaseVersion);
71+
}
72+
}
73+
}

src/test/java/software/aws/mcs/auth/TestSigV4Config.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ public static void main(String[] args) throws Exception {
6666
.withConfigLoader(DriverConfigLoader.fromFile(file))
6767
.addContactPoints(contactPoints)
6868
.withLocalDatacenter("us-west-2")
69-
.build()) {
69+
.build()) {
7070

7171
// We use execute to send a query to Cassandra. This returns a ResultSet, which is essentially a collection
7272
// of Row objects.

src/test/resources/ddl.cql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
CREATE KEYSPACE "testkeyspace" WITH REPLICATION = {'class': 'SingleRegionStrategy'};
2+
3+
CREATE TABLE "testkeyspace"."testconf"(
4+
"id" ascii,
5+
"category" ascii,
6+
PRIMARY KEY("id")
7+
)
8+
WITH CUSTOM_PROPERTIES = {
9+
'capacity_mode':{
10+
'throughput_mode':'PAY_PER_REQUEST'
11+
},
12+
'point_in_time_recovery':{
13+
'status':'enabled'
14+
},
15+
'encryption_specification':{
16+
'encryption_type':'AWS_OWNED_KMS_KEY'
17+
}
18+
} ;

src/test/resources/dml.cql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INSERT into testkeyspace.testconf(id,category) values('first','first Category');
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
datastax-java-driver {
2+
basic.contact-points = ["cassandra.ap-south-1.amazonaws.com:9142"]
3+
basic.load-balancing-policy {
4+
class = DefaultLoadBalancingPolicy
5+
local-datacenter = ap-south-1
6+
slow-replica-avoidance = false
7+
}
8+
basic.request {
9+
consistency = LOCAL_QUORUM
10+
}
11+
advanced {
12+
auth-provider = {
13+
class = software.aws.mcs.auth.SigV4AuthProvider
14+
aws-region = ap-south-1
15+
}
16+
ssl-engine-factory {
17+
class = DefaultSslEngineFactory
18+
truststore-path = "<path>/cassandra_truststore.jks"
19+
truststore-password = "<password>"
20+
hostname-validation=false
21+
}
22+
}
23+
advanced.connection.pool.local.size = 3
24+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
datastax-java-driver {
2+
basic.contact-points = ["cassandra.ap-south-1.amazonaws.com:9142"]
3+
basic.load-balancing-policy {
4+
class = DefaultLoadBalancingPolicy
5+
local-datacenter = ap-south-1
6+
slow-replica-avoidance = false
7+
}
8+
basic.request {
9+
consistency = LOCAL_QUORUM
10+
}
11+
advanced {
12+
auth-provider = {
13+
class = software.aws.mcs.auth.SigV4AuthProvider
14+
aws-region = ap-south-1
15+
aws-role-arn = "arn:aws:iam::ACCOUNT_ID:role/keyspaces-act2-role"
16+
}
17+
ssl-engine-factory {
18+
class = DefaultSslEngineFactory
19+
truststore-path = "<path>/cassandra_truststore.jks"
20+
truststore-password = "<password>"
21+
hostname-validation=false
22+
}
23+
}
24+
advanced.connection.pool.local.size = 3
25+
}

0 commit comments

Comments
 (0)