Skip to content
Open
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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-bom</artifactId>
<version>1.12.778</version>
<version>1.12.797</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import ch.cyberduck.core.Credentials;
import ch.cyberduck.core.CredentialsConfigurator;
import ch.cyberduck.core.Factory;
import ch.cyberduck.core.Host;
import ch.cyberduck.core.Local;
import ch.cyberduck.core.LocalFactory;
Expand All @@ -33,8 +34,11 @@
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

Expand Down Expand Up @@ -94,6 +98,40 @@ else if(StringUtils.equals(entry.getValue().getAwsAccessIdKey(), credentials.get
return false;
}).map(Map.Entry::getValue).findFirst().orElse(StringUtils.isBlank(host.getCredentials().getUsername()) ? profiles.get("default") : null);
if(null != profile) {
if(profile.isProcessBasedProfile()) {
// Uses external process to retrieve temporary credentials
final String command = profile.getCredentialProcess();
final ObjectMapper mapper = JsonMapper.builder()
Comment on lines +101 to +104
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

New credential_process support isn’t covered by tests, while this class already has dedicated unit tests and fixtures for other profile types. Add a test profile in src/test/resources/.../.aws plus a test that runs a small helper command (or a mocked process runner) returning the expected JSON, and assert the parsed tokens/expiry are applied.

Copilot uses AI. Check for mistakes.
.serializationInclusion(Include.NON_NULL)
.enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.visibility(PropertyAccessor.FIELD, Visibility.ANY).build();
List<String> cmd = new ArrayList<>();
switch(Factory.Platform.getDefault()) {
case windows:
cmd.add("cmd");
cmd.add("/c");
break;
default:
cmd.add("sh");
cmd.add("-c");
break;
}
cmd.add(command);
Comment on lines +110 to +120
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

Using cmd /c or sh -c runs the credential_process through a shell, which changes quoting/escaping semantics and expands shell metacharacters. AWS credential_process is typically executed as a direct process invocation; consider parsing the command into an argv list and calling ProcessBuilder without a shell to better match expected behavior and reduce unintended command interpretation.

Copilot uses AI. Check for mistakes.
final ProcessBuilder builder = new ProcessBuilder(cmd);
try {
final Process process = builder.start();
try(InputStream reader = process.getInputStream()) {
final CachedCredential cached = mapper.readValue(reader, CachedCredential.class);
return credentials.setTokens(new TemporaryAccessTokens(
cached.accessKey, cached.secretKey, cached.sessionToken, cached.getExpiration()));
}
Comment on lines 121 to 128
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

credential_process execution reads only stdout and never checks exit status or drains stderr; a process that writes to stderr (or never exits) can deadlock or block login. Consider redirecting stderr (or consuming it), waiting for the process to finish (with a timeout), and logging stderr/exit code on failure before falling back to existing credentials.

Copilot uses AI. Check for mistakes.
}
catch(IOException e) {
log.warn("Failure \"{}\" parsing cached credentials from {}", e.getMessage(), command);
return credentials;
Comment on lines +130 to +132
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

The warning message says "parsing cached credentials" but this path is parsing credential_process output, not a cache file, and it drops useful failure details like exit code and stderr. Updating the log message (and including stderr/exit status) will make troubleshooting misconfigured profiles much easier.

Copilot uses AI. Check for mistakes.
}
}
if(profile.isRoleBasedProfile()) {
log.debug("Configure credentials from role based profile {}", profile.getProfileName());
if(StringUtils.isBlank(profile.getRoleSourceProfile())) {
Expand All @@ -115,7 +153,7 @@ else if(!profiles.containsKey(profile.getRoleSourceProfile())) {
}
// No further token exchange required
return credentials.setTokens(new TemporaryAccessTokens(
cached.accessKey, cached.secretKey, cached.sessionToken, Instant.parse(cached.expiration).toEpochMilli()));
cached.accessKey, cached.secretKey, cached.sessionToken, cached.getExpiration()));
}
else {
// If a profile defines the role_arn property then the profile is treated as an assume role profile
Expand All @@ -137,7 +175,7 @@ else if(!profiles.containsKey(profile.getRoleSourceProfile())) {
return credentials;
}
return credentials.setTokens(new TemporaryAccessTokens(
cached.accessKey, cached.secretKey, cached.sessionToken, Instant.parse(cached.expiration).toEpochMilli()));
cached.accessKey, cached.secretKey, cached.sessionToken, cached.getExpiration()));
}
log.debug("Set credentials from profile {}", profile.getProfileName());
return credentials
Expand Down Expand Up @@ -241,7 +279,7 @@ private CachedCredential fetchSsoCredentials(final Map<String, String> propertie
log.warn("Failure parsing SSO credentials.");
return null;
}
final Instant expiration = Instant.parse(cached.credentials.expiration);
final Instant expiration = Instant.ofEpochMilli(cached.credentials.getExpiration());
if(expiration.isBefore(Instant.now())) {
log.warn("Expired AWS SSO credentials.");
return null;
Expand Down Expand Up @@ -276,6 +314,15 @@ private static class CachedCredential {
private String sessionToken;
@JsonProperty("Expiration")
private String expiration;

public Long getExpiration() {
try {
return Instant.parse(expiration).toEpochMilli();
}
catch(DateTimeParseException e) {
return -1L;
}
}
}

/**
Expand Down