1717
1818import ch .cyberduck .core .Credentials ;
1919import ch .cyberduck .core .CredentialsConfigurator ;
20+ import ch .cyberduck .core .Factory ;
2021import ch .cyberduck .core .Host ;
2122import ch .cyberduck .core .Local ;
2223import ch .cyberduck .core .LocalFactory ;
3334import java .io .InputStream ;
3435import java .nio .charset .StandardCharsets ;
3536import java .time .Instant ;
37+ import java .time .format .DateTimeParseException ;
38+ import java .util .ArrayList ;
3639import java .util .HashMap ;
3740import java .util .LinkedHashMap ;
41+ import java .util .List ;
3842import java .util .Map ;
3943import 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 /**
0 commit comments