Skip to content

Commit 772d053

Browse files
authored
Merge pull request #17864 from iterate-ch/feature/GH-11664
Add support for AWS credential_process configuration, allowing external processes to provide temporary AWS credentials.
2 parents 7ba6a4f + bec3162 commit 772d053

File tree

4 files changed

+100
-10
lines changed

4 files changed

+100
-10
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@
396396
<dependency>
397397
<groupId>com.amazonaws</groupId>
398398
<artifactId>aws-java-sdk-bom</artifactId>
399-
<version>1.12.778</version>
399+
<version>1.12.797</version>
400400
<type>pom</type>
401401
<scope>import</scope>
402402
</dependency>

s3/src/main/java/ch/cyberduck/core/s3/S3CredentialsConfigurator.java

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import ch.cyberduck.core.Credentials;
1919
import ch.cyberduck.core.CredentialsConfigurator;
20+
import ch.cyberduck.core.Factory;
2021
import ch.cyberduck.core.Host;
2122
import ch.cyberduck.core.Local;
2223
import ch.cyberduck.core.LocalFactory;
@@ -33,8 +34,11 @@
3334
import java.io.InputStream;
3435
import java.nio.charset.StandardCharsets;
3536
import java.time.Instant;
37+
import java.time.format.DateTimeParseException;
38+
import java.util.ArrayList;
3639
import java.util.HashMap;
3740
import java.util.LinkedHashMap;
41+
import java.util.List;
3842
import java.util.Map;
3943
import java.util.Scanner;
4044

@@ -94,6 +98,48 @@ else if(StringUtils.equals(entry.getValue().getAwsAccessIdKey(), credentials.get
9498
return false;
9599
}).map(Map.Entry::getValue).findFirst().orElse(StringUtils.isBlank(host.getCredentials().getUsername()) ? profiles.get("default") : null);
96100
if(null != profile) {
101+
if(profile.isProcessBasedProfile()) {
102+
// Uses external process to retrieve temporary credentials
103+
final String command = profile.getCredentialProcess();
104+
final ObjectMapper mapper = JsonMapper.builder()
105+
.serializationInclusion(Include.NON_NULL)
106+
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
107+
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
108+
.visibility(PropertyAccessor.FIELD, Visibility.ANY).build();
109+
List<String> cmd = new ArrayList<>();
110+
switch(Factory.Platform.getDefault()) {
111+
case windows:
112+
cmd.add("cmd");
113+
cmd.add("/c");
114+
break;
115+
default:
116+
cmd.add("sh");
117+
cmd.add("-c");
118+
break;
119+
}
120+
cmd.add(command);
121+
final ProcessBuilder builder = new ProcessBuilder(cmd);
122+
try {
123+
final Process process = builder.start();
124+
try(InputStream reader = process.getInputStream()) {
125+
final CachedCredential cached = mapper.readValue(reader, CachedCredential.class);
126+
credentials.setTokens(new TemporaryAccessTokens(
127+
cached.accessKey, cached.secretKey, cached.sessionToken, cached.getExpiration()));
128+
process.waitFor();
129+
if(process.exitValue() != 0) {
130+
throw new IOException(String.format("Unexpected exit code %d for process %s", process.exitValue(), command));
131+
}
132+
return credentials;
133+
}
134+
finally {
135+
process.destroy();
136+
}
137+
}
138+
catch(IOException | InterruptedException e) {
139+
log.warn("Failure \"{}\" parsing credentials from output of command {}", e.getMessage(), command);
140+
return credentials;
141+
}
142+
}
97143
if(profile.isRoleBasedProfile()) {
98144
log.debug("Configure credentials from role based profile {}", profile.getProfileName());
99145
if(StringUtils.isBlank(profile.getRoleSourceProfile())) {
@@ -115,7 +161,7 @@ else if(!profiles.containsKey(profile.getRoleSourceProfile())) {
115161
}
116162
// No further token exchange required
117163
return credentials.setTokens(new TemporaryAccessTokens(
118-
cached.accessKey, cached.secretKey, cached.sessionToken, Instant.parse(cached.expiration).toEpochMilli()));
164+
cached.accessKey, cached.secretKey, cached.sessionToken, cached.getExpiration()));
119165
}
120166
else {
121167
// If a profile defines the role_arn property then the profile is treated as an assume role profile
@@ -137,7 +183,7 @@ else if(!profiles.containsKey(profile.getRoleSourceProfile())) {
137183
return credentials;
138184
}
139185
return credentials.setTokens(new TemporaryAccessTokens(
140-
cached.accessKey, cached.secretKey, cached.sessionToken, Instant.parse(cached.expiration).toEpochMilli()));
186+
cached.accessKey, cached.secretKey, cached.sessionToken, cached.getExpiration()));
141187
}
142188
log.debug("Set credentials from profile {}", profile.getProfileName());
143189
return credentials
@@ -241,7 +287,7 @@ private CachedCredential fetchSsoCredentials(final Map<String, String> propertie
241287
log.warn("Failure parsing SSO credentials.");
242288
return null;
243289
}
244-
final Instant expiration = Instant.parse(cached.credentials.expiration);
290+
final Instant expiration = Instant.ofEpochMilli(cached.credentials.getExpiration());
245291
if(expiration.isBefore(Instant.now())) {
246292
log.warn("Expired AWS SSO credentials.");
247293
return null;
@@ -276,6 +322,15 @@ private static class CachedCredential {
276322
private String sessionToken;
277323
@JsonProperty("Expiration")
278324
private String expiration;
325+
326+
public Long getExpiration() {
327+
try {
328+
return Instant.parse(expiration).toEpochMilli();
329+
}
330+
catch(DateTimeParseException e) {
331+
return -1L;
332+
}
333+
}
279334
}
280335

281336
/**

s3/src/test/java/ch/cyberduck/core/s3/S3CredentialsConfiguratorTest.java

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
import ch.cyberduck.core.Credentials;
19+
import ch.cyberduck.core.Factory;
1920
import ch.cyberduck.core.Host;
2021
import ch.cyberduck.core.LocalFactory;
2122
import ch.cyberduck.core.TestProtocol;
@@ -26,6 +27,7 @@
2627
import java.io.File;
2728

2829
import static org.junit.Assert.assertEquals;
30+
import static org.junit.Assume.assumeFalse;
2931

3032
public class S3CredentialsConfiguratorTest {
3133

@@ -38,16 +40,14 @@ public void testConfigure() throws Exception {
3840
@Test
3941
public void readFailureForInvalidAWSCredentialsProfileEntry() throws Exception {
4042
final Credentials credentials = new Credentials("test_s3_profile");
41-
final Credentials verify = new S3CredentialsConfigurator(LocalFactory.get(new File("src/test/resources/invalid/.aws").getAbsolutePath())
42-
)
43+
final Credentials verify = new S3CredentialsConfigurator(LocalFactory.get(new File("src/test/resources/invalid/.aws").getAbsolutePath()))
4344
.reload().configure(new Host(new TestProtocol(), StringUtils.EMPTY, credentials));
4445
assertEquals(credentials, verify);
4546
}
4647

4748
@Test
4849
public void readSuccessForValidAWSCredentialsProfileEntry() throws Exception {
49-
final Credentials verify = new S3CredentialsConfigurator(LocalFactory.get(new File("src/test/resources/valid/.aws").getAbsolutePath())
50-
)
50+
final Credentials verify = new S3CredentialsConfigurator(LocalFactory.get(new File("src/test/resources/valid/.aws").getAbsolutePath()))
5151
.reload().configure(new Host(new TestProtocol(), StringUtils.EMPTY, new Credentials("test_s3_profile")));
5252
assertEquals("test_s3_profile", verify.getUsername());
5353
assertEquals("", verify.getPassword());
@@ -58,12 +58,38 @@ public void readSuccessForValidAWSCredentialsProfileEntry() throws Exception {
5858

5959
@Test
6060
public void readSSOCachedTemporaryTokens() throws Exception {
61-
final Credentials verify = new S3CredentialsConfigurator(LocalFactory.get(new File("src/test/resources/valid/.aws").getAbsolutePath())
62-
)
61+
final Credentials verify = new S3CredentialsConfigurator(LocalFactory.get(new File("src/test/resources/valid/.aws").getAbsolutePath()))
6362
.reload().configure(new Host(new TestProtocol(), StringUtils.EMPTY, new Credentials("ReadOnlyAccess-189584543480")));
6463
assertEquals("TESTACCESSKEY", verify.getTokens().getAccessKeyId());
6564
assertEquals("TESTSECRETKEY", verify.getTokens().getSecretAccessKey());
6665
assertEquals("TESTSESSIONTOKEN", verify.getTokens().getSessionToken());
6766
assertEquals(3497005724000L, verify.getTokens().getExpiryInMilliseconds(), 0L);
6867
}
68+
69+
@Test
70+
public void readCredentialProcessTokens() throws Exception {
71+
assumeFalse(Factory.Platform.getDefault().equals(Factory.Platform.Name.windows));
72+
final Credentials verify = new S3CredentialsConfigurator(LocalFactory.get(new File("src/test/resources/valid/.aws").getAbsolutePath()))
73+
.reload().configure(new Host(new TestProtocol(), StringUtils.EMPTY, new Credentials("credential_process_profile")));
74+
assertEquals("PROCESSACCESSKEY", verify.getTokens().getAccessKeyId());
75+
assertEquals("PROCESSSECRETKEY", verify.getTokens().getSecretAccessKey());
76+
assertEquals("PROCESSSESSIONTOKEN", verify.getTokens().getSessionToken());
77+
assertEquals(3497005724000L, verify.getTokens().getExpiryInMilliseconds(), 0L);
78+
}
79+
80+
@Test
81+
public void readCredentialProcessTokensExitCode() throws Exception {
82+
assumeFalse(Factory.Platform.getDefault().equals(Factory.Platform.Name.windows));
83+
final Credentials credentials = new Credentials("credential_process_profile_error_exit");
84+
assertEquals(credentials, new S3CredentialsConfigurator(LocalFactory.get(new File("src/test/resources/valid/.aws").getAbsolutePath()))
85+
.reload().configure(new Host(new TestProtocol(), StringUtils.EMPTY, credentials)));
86+
}
87+
88+
@Test
89+
public void readCredentialProcessTokensParseError() throws Exception {
90+
assumeFalse(Factory.Platform.getDefault().equals(Factory.Platform.Name.windows));
91+
final Credentials credentials = new Credentials("credential_process_profile_parser_error");
92+
assertEquals(credentials, new S3CredentialsConfigurator(LocalFactory.get(new File("src/test/resources/valid/.aws").getAbsolutePath()))
93+
.reload().configure(new Host(new TestProtocol(), StringUtils.EMPTY, credentials)));
94+
}
6995
}

s3/src/test/resources/valid/.aws/config

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,12 @@ sso_region = us-east-1
44
sso_account_id = 189584543480
55
sso_role_name = ReadOnlyAccess
66
region = eu-west-1
7+
8+
[profile credential_process_profile]
9+
credential_process = printf '{"Version": 1,"AccessKeyId":"PROCESSACCESSKEY","SecretAccessKey":"PROCESSSECRETKEY","SessionToken":"PROCESSSESSIONTOKEN","Expiration":"2080-10-24T14:28:44Z"}'
10+
11+
[profile credential_process_profile_error_exit]
12+
credential_process = exit 1
13+
14+
[profile credential_process_profile_parser_error]
15+
credential_process = printf '{"Version": 1'

0 commit comments

Comments
 (0)