Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
@@ -1,6 +1,5 @@
package com.salesforce.multicloudj.blob.gcp;

import com.google.api.gax.paging.Page;
import com.google.cloud.storage.Blob;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.BlobInfo;
Expand All @@ -19,11 +18,9 @@
import com.salesforce.multicloudj.blob.driver.MultipartUpload;
import com.salesforce.multicloudj.blob.driver.MultipartUploadRequest;
import com.salesforce.multicloudj.blob.driver.PresignedUrlRequest;
import com.salesforce.multicloudj.blob.driver.UploadPartResponse;
import com.salesforce.multicloudj.blob.driver.UploadRequest;
import com.salesforce.multicloudj.blob.driver.UploadResponse;
import com.salesforce.multicloudj.common.exceptions.InvalidArgumentException;
import com.salesforce.multicloudj.common.exceptions.UnSupportedOperationException;
import com.salesforce.multicloudj.common.util.HexUtil;
import lombok.Getter;
import org.apache.commons.lang3.tuple.ImmutablePair;
Expand Down
26 changes: 23 additions & 3 deletions examples/src/main/java/com/salesforce/multicloudj/sts/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.salesforce.multicloudj.sts.model.AssumeRoleWebIdentityRequest;
import com.salesforce.multicloudj.sts.model.AssumedRoleRequest;
import com.salesforce.multicloudj.sts.model.CallerIdentity;
import com.salesforce.multicloudj.sts.model.CredentialScope;
import com.salesforce.multicloudj.sts.model.CredentialsOverrider;
import com.salesforce.multicloudj.sts.model.CredentialsType;
import com.salesforce.multicloudj.sts.model.GetCallerIdentityRequest;
Expand All @@ -20,10 +21,9 @@

import static com.salesforce.multicloudj.sts.curl.requestToCurl;


public class Main {

static String provider = "aws";
static String provider = "gcp";

public static void main(String[] args) {
assumeRole();
Expand All @@ -35,9 +35,29 @@ public static void main(String[] args) {

public static void assumeRole() {
StsClient client = StsClient.builder(provider).withRegion("us-west-2").build();

// Create a cloud-agnostic credential scope with condition
CredentialScope.AvailabilityCondition condition = CredentialScope.AvailabilityCondition.builder()
.resourcePrefix("storage://my-bucket/documents/")
.title("Limit to documents folder")
.description("Only allow access to objects in the documents folder")
.build();

CredentialScope.ScopeRule rule = CredentialScope.ScopeRule.builder()
.availableResource("storage://my-bucket/*")
.availablePermission("storage:GetObject")
.availablePermission("storage:PutObject")
.availabilityCondition(condition)
.build();

CredentialScope credentialScope = CredentialScope.builder()
.rule(rule)
.build();

AssumedRoleRequest request = AssumedRoleRequest.newBuilder()
.withRole("arn:aws:iam::<account>:role/<role-name>")
.withRole("chameleon@substrate-sdk-gcp-poc1.iam.gserviceaccount.com")
.withSessionName("my-session")
.withCredentialScope(credentialScope)
.build();
StsCredentials stsCredentials = client.getAssumeRoleCredentials(request);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id" : "90d18175-4f86-4190-9f40-5583fb96ea8c",
"name" : "",
"request" : {
"urlPattern": "/\\?Action=AssumeRole&Timestamp=.*&RoleArn=acs%3Aram%3A%3A1936276257662232%3Arole%2Fchameleon-test-role&SecurityToken=.*&SignatureVersion=1.0&Format=JSON&RoleSessionName=any-session&SignatureNonce=.*&Version=2015-04-01&AccessKeyId=.*&Signature=.*%3D&SignatureMethod=HMAC-SHA1&RegionId=cn-shanghai",
"urlPattern": "/\\?Action=AssumeRole&DurationSeconds=3600&Timestamp=.*&RoleArn=acs%3Aram%3A%3A1936276257662232%3Arole%2Fchameleon-test-role&SecurityToken=.*&SignatureVersion=1.0&Format=JSON&RoleSessionName=any-session&SignatureNonce=.*&Version=2015-04-01&AccessKeyId=.*&Signature=.*%3D&SignatureMethod=HMAC-SHA1&RegionId=cn-shanghai",
"method" : "POST"
},
"response" : {
Expand Down
5 changes: 5 additions & 0 deletions sts/sts-aws/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
<version>3.12.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
Expand Down
132 changes: 118 additions & 14 deletions sts/sts-aws/src/main/java/com/salesforce/multicloudj/sts/aws/AwsSts.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.salesforce.multicloudj.sts.aws;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auto.service.AutoService;
import com.salesforce.multicloudj.common.aws.CommonErrorCodeMapping;
import com.salesforce.multicloudj.common.exceptions.InvalidArgumentException;
import com.salesforce.multicloudj.common.exceptions.SubstrateSdkException;
import com.salesforce.multicloudj.common.exceptions.UnAuthorizedException;
import com.salesforce.multicloudj.common.exceptions.UnknownException;
import com.salesforce.multicloudj.sts.driver.AbstractSts;
import com.salesforce.multicloudj.sts.model.AssumeRoleWebIdentityRequest;
import com.salesforce.multicloudj.sts.model.AssumedRoleRequest;
import com.salesforce.multicloudj.sts.model.CallerIdentity;
import com.salesforce.multicloudj.sts.model.CredentialScope;
import com.salesforce.multicloudj.sts.model.GetAccessTokenRequest;
import com.salesforce.multicloudj.sts.model.StsCredentials;
import software.amazon.awssdk.awscore.exception.AwsServiceException;
Expand All @@ -25,15 +29,16 @@
import software.amazon.awssdk.services.sts.model.GetSessionTokenRequest;
import software.amazon.awssdk.services.sts.model.GetSessionTokenResponse;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

@SuppressWarnings("rawtypes")
@AutoService(AbstractSts.class)
public class AwsSts extends AbstractSts {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private StsClient stsClient;

public AwsSts(Builder builder) {
Expand Down Expand Up @@ -62,12 +67,18 @@ public Builder builder() {

@Override
protected StsCredentials getSTSCredentialsWithAssumeRole(AssumedRoleRequest request) {
AssumeRoleRequest roleRequest = AssumeRoleRequest.builder()
AssumeRoleRequest.Builder roleRequestBuilder = AssumeRoleRequest.builder()
.roleArn(request.getRole())
.roleSessionName(request.getSessionName() != null ? request.getSessionName() : "multicloudj-" + System.currentTimeMillis())
.durationSeconds(request.getExpiration() != 0 ? request.getExpiration() : null)
.build();
AssumeRoleResponse response = stsClient.assumeRole(roleRequest);
.durationSeconds(request.getExpiration() != 0 ? request.getExpiration() : null);

// If credential scope is provided, convert to AWS IAM policy JSON
if (request.getCredentialScope() != null) {
String policyJson = convertToAwsPolicy(request.getCredentialScope());
roleRequestBuilder.policy(policyJson);
}

AssumeRoleResponse response = stsClient.assumeRole(roleRequestBuilder.build());
Credentials credentials = response.credentials();

return new StsCredentials(
Expand All @@ -76,6 +87,106 @@ protected StsCredentials getSTSCredentialsWithAssumeRole(AssumedRoleRequest requ
credentials.sessionToken());
}

/**
* Converts cloud-agnostic CredentialScope to AWS IAM Policy JSON.
*/
private String convertToAwsPolicy(CredentialScope credentialScope) {
List<Map<String, Object>> statements = new ArrayList<>();

for (CredentialScope.ScopeRule rule : credentialScope.getRules()) {
Map<String, Object> statement = new HashMap<>();
statement.put("Effect", "Allow");

// Convert permissions (format: "storage:GetObject" -> "s3:GetObject")
List<String> actions = rule.getAvailablePermissions().stream()
.map(this::convertPermissionToAction)
.collect(Collectors.toList());
statement.put("Action", actions);

// Convert resource (format: "storage://my-bucket" -> "arn:aws:s3:::my-bucket/*")
String resource = convertResourceToArn(rule.getAvailableResource());
statement.put("Resource", resource);

// Add condition if present
if (rule.getAvailabilityCondition() != null) {
Map<String, Object> condition = convertConditionToAwsCondition(
rule.getAvailabilityCondition());
if (!condition.isEmpty()) {
statement.put("Condition", condition);
}
}

statements.add(statement);
}

Map<String, Object> policy = new HashMap<>();
policy.put("Version", "2012-10-17");
policy.put("Statement", statements);

return toJsonString(policy);
}

/**
* Converts cloud-agnostic permission to AWS Action.
* Maps MultiCloudJ storage actions to AWS S3 actions.
* Example: "storage:GetObject" -> "s3:GetObject"
*/
private String convertPermissionToAction(String permission) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QQ:

  1. Can we expect non-storage related actions in permission string going forward ?
  2. Should we add a unsupported check for permission if it doesn't start with storage: ?

String action = permission.substring("storage:".length());
return "s3:" + action;
}

/**
* Converts cloud-agnostic resource to AWS ARN.
* Maps MultiCloudJ storage URIs to AWS S3 ARNs.
* Example: "storage://my-bucket" -> "arn:aws:s3:::my-bucket/*"
*/
private String convertResourceToArn(String resource) {
String bucketName = resource.substring("storage://".length());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: Should we strict prefix check "storage://" for resource string till we support other use cases ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very likely it won't be extended based on known use-cases. but I think it's good idea to add precondition until we add support, will add it.

// AWS requires /* suffix for bucket-level access
return "arn:aws:s3:::" + bucketName + "/*";
}

/**
* Converts cloud-agnostic availability condition to AWS IAM condition.
* Converts resourcePrefix to AWS IAM Condition with StringLike and s3:prefix.
* Example: "storage://my-bucket/documents/" -> {"StringLike": {"s3:prefix": "documents/"}}
*/
private Map<String, Object> convertConditionToAwsCondition(
CredentialScope.AvailabilityCondition condition) {
Map<String, Object> awsCondition = new HashMap<>();

// Convert cloud-agnostic resourcePrefix to AWS IAM condition
if (condition.getResourcePrefix() != null && !condition.getResourcePrefix().isEmpty()) {
String resourcePrefix = condition.getResourcePrefix();
if (resourcePrefix.startsWith("storage://")) {
String path = resourcePrefix.substring("storage://".length());
// Extract path prefix after bucket name
if (path.contains("/")) {
String pathPrefix = path.substring(path.indexOf("/") + 1);
if (!pathPrefix.isEmpty()) {
Map<String, String> stringLike = new HashMap<>();
stringLike.put("s3:prefix", pathPrefix);
awsCondition.put("StringLike", stringLike);
}
}
}
}

return awsCondition;
}

/**
* Converts Map to JSON string.
*/
private String toJsonString(Map<String, Object> map) {
try {
return OBJECT_MAPPER.writeValueAsString(map);
} catch (JsonProcessingException e) {
throw new InvalidArgumentException("scoped credentials is not in right format", e);
}
}

@Override
protected CallerIdentity getCallerIdentityFromProvider(com.salesforce.multicloudj.sts.model.GetCallerIdentityRequest request) {
GetCallerIdentityRequest callerIdentityRequest = GetCallerIdentityRequest.builder().build();
Expand Down Expand Up @@ -152,13 +263,6 @@ public Builder self() {
return this;
}

public Builder setParam(Map<String, String> params) {
if (!Objects.equals(params.get("customPro"), "")) {
param = params.get("customPro");
}
return this;
}

@Override
public AwsSts build() {
this.param = region;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.salesforce.multicloudj.sts.aws;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.salesforce.multicloudj.sts.model.AssumeRoleWebIdentityRequest;
import com.salesforce.multicloudj.sts.model.AssumedRoleRequest;
import com.salesforce.multicloudj.sts.model.CallerIdentity;
import com.salesforce.multicloudj.sts.model.CredentialScope;
import com.salesforce.multicloudj.sts.model.GetAccessTokenRequest;
import com.salesforce.multicloudj.sts.model.StsCredentials;
import org.junit.jupiter.api.Assertions;
Expand Down Expand Up @@ -120,4 +123,52 @@ public void TestAssumeRoleWithWebIdentityWithoutSessionName() {
Assertions.assertEquals("testSecret", credentials.getAccessKeySecret());
Assertions.assertEquals("testToken", credentials.getSecurityToken());
}

private void assertJsonEquals(String expected, String actual) throws Exception {
ObjectMapper mapper = new ObjectMapper();
JsonNode expectedNode = mapper.readTree(expected);
JsonNode actualNode = mapper.readTree(actual);
Assertions.assertEquals(expectedNode, actualNode);
}

@Test
public void TestAssumeRoleWithCredentialScope() throws Exception {
AwsSts sts = new AwsSts().builder().build(mockStsClient);

// Create cloud-agnostic CredentialScope with availability condition
CredentialScope.AvailabilityCondition condition = CredentialScope.AvailabilityCondition.builder()
.resourcePrefix("storage://my-bucket/documents/")
.title("Limit to documents folder")
.description("Only allow access to objects in the documents folder")
.build();

CredentialScope.ScopeRule rule = CredentialScope.ScopeRule.builder()
.availableResource("storage://my-bucket")
.availablePermission("storage:GetObject")
.availablePermission("storage:PutObject")
.availabilityCondition(condition)
.build();

CredentialScope credentialScope = CredentialScope.builder()
.rule(rule)
.build();

AssumedRoleRequest request = AssumedRoleRequest.newBuilder()
.withRole("arn:aws:iam::123456789012:role/test-role")
.withSessionName("testSession")
.withCredentialScope(credentialScope)
.build();

StsCredentials credentials = sts.assumeRole(request);
Assertions.assertNotNull(credentials);

// Verify that policy was set with correct conversion including condition
org.mockito.ArgumentCaptor<AssumeRoleRequest> captor = org.mockito.ArgumentCaptor.forClass(AssumeRoleRequest.class);
Mockito.verify(mockStsClient, Mockito.atLeastOnce()).assumeRole(captor.capture());
AssumeRoleRequest capturedRequest = captor.getValue();

// Verify the complete policy structure with condition (parse JSON to ignore key ordering)
String expectedPolicy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":[\"s3:GetObject\",\"s3:PutObject\"],\"Resource\":\"arn:aws:s3:::my-bucket/*\",\"Condition\":{\"StringLike\":{\"s3:prefix\":\"documents/\"}}}]}";
assertJsonEquals(expectedPolicy, capturedRequest.policy());
}
}
4 changes: 2 additions & 2 deletions sts/sts-aws/src/test/resources/mappings/post-oi7cu8zgp5.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
"url" : "/",
"method" : "POST",
"bodyPatterns" : [ {
"equalTo" : "Action=AssumeRole&Version=2011-06-15&RoleArn=arn%3Aaws%3Aiam%3A%3A654654370895%3Arole%2Fchameleon-jcloud-test&RoleSessionName=any-session",
"equalTo" : "Action=AssumeRole&Version=2011-06-15&RoleArn=arn%3Aaws%3Aiam%3A%3A654654370895%3Arole%2Fchameleon-jcloud-test&RoleSessionName=any-session&DurationSeconds=3600",
"caseInsensitive" : false
} ]
},
"response" : {
"status" : 200,
"body" : "<AssumeRoleResponse xmlns=\"https://sts.amazonaws.com/doc/2011-06-15/\">\n <AssumeRoleResult>\n <AssumedRoleUser>\n <AssumedRoleId>AROAZQ3DQ6RHVFNX2KMJM:any-session</AssumedRoleId>\n <Arn>arn:aws:sts::654654370895:assumed-role/chameleon-jcloud-test/any-session</Arn>\n </AssumedRoleUser>\n <Credentials>\n <AccessKeyId>ASIAZQ3DQ6RHUHXN5WQ4</AccessKeyId>\n <SecretAccessKey>gY8iT2TtBR/fuNS19U9L982ENLqbnIb+yp+XYLJm</SecretAccessKey>\n <SessionToken>IQoJb3JpZ2luX2VjENv//////////wEaCXVzLXdlc3QtMiJIMEYCIQDxjYcXDRVfgKhH3ZYt026cz24g2w04d3FC7ORAyhPreAIhAIvfMmMoznPyul2Yg3kBtV31M3tvDKT97R2+3WOjTpC+KqECCPT//////////wEQABoMNjU0NjU0MzcwODk1IgzPfj05k2RjXQiU6+Mq9QFu9RdGQO/R4MRlRXmYhViCAv7Vg16jRnYAfHSVuKt+SctywLqqyqrkRs0kyTlCp0BZ4o0d9FR3ODzD9dOiMWZ+haB8iwJ0hrbHQPz9mmipTici8RFyc4uwfPWEkhc3Ra4U/TzfDs8N98Fry7xZjZmZ+1a378YpNLoWeSnFjXPMh2Ivkp01zi+k721nHf4nfpFFjgFNwsnEopE1Bc8G8W3ak0gqMZJQ6Oeo18HloFhbNpJBfIc7gjacpfPEvIWEKMOTtBGBKO0Fi+W/T2dPIfdrZhbbYdnOxEPEAEM9O5KFJKqwYAtDpoFjAb9xpOCFk47bkusY6zCs++e2BjqcAc1JPov5+mjvw+8PQ2grLuMXoxda+ZaP3xIJQ3xg1nr0KO8pJkHe46Tj92b1iy3lWpy2bZVAI9qFGBS2ZY53Oy5EXZb56o/k9quLAQjy/PcGvyB9m1KJln6LDhhKf+KCajpJWWG9iZWQIosJPXU64A6XxMju0JXC7S1nXNH46AuF2JwPAor0CxK7tHimuHjYN7Cxl7Gv1s7K/2oqWw==</SessionToken>\n <Expiration>2024-09-05T19:51:24Z</Expiration>\n </Credentials>\n </AssumeRoleResult>\n <ResponseMetadata>\n <RequestId>5ad47c30-460b-4fe5-a15b-f3a4cef730f3</RequestId>\n </ResponseMetadata>\n</AssumeRoleResponse>\n",
"body" : "<AssumeRoleResponse xmlns=\"https://sts.amazonaws.com/doc/2011-06-15/\">\n <AssumeRoleResult>\n <AssumedRoleUser>\n <AssumedRoleId>AROAZQ3DQ6RHVFNX2KMJM:any-session</AssumedRoleId>\n <Arn>arn:aws:sts::654654370895:assumed-role/chameleon-jcloud-test/any-session</Arn>\n </AssumedRoleUser>\n <Credentials>\n <AccessKeyId>keyid</AccessKeyId>\n <SecretAccessKey>secretkey</SecretAccessKey>\n <SessionToken>secrettoken</SessionToken>\n <Expiration>2024-09-05T19:51:24Z</Expiration>\n </Credentials>\n </AssumeRoleResult>\n <ResponseMetadata>\n <RequestId>5ad47c30-460b-4fe5-a15b-f3a4cef730f3</RequestId>\n </ResponseMetadata>\n</AssumeRoleResponse>\n",
"headers" : {
"x-amzn-RequestId" : "5ad47c30-460b-4fe5-a15b-f3a4cef730f3",
"Date" : "Thu, 05 Sep 2024 18:51:24 GMT",
Expand Down
Loading
Loading