Selaa lähdekoodia

Issue 2249 – Allow assuming IAM role in S3 sessions + Only profiles that contain both aws_access_key_id and aws_secret_access_key are listed on the Login dialog

https://winscp.net/tracker/2249

Source commit: 00151a6bc71d411588f2c2909076561731bf8ed9
Martin Prikryl 1 vuosi sitten
vanhempi
sitoutus
fb147cdc2a

+ 4 - 0
libs/libs3/inc/libs3.h

@@ -141,6 +141,8 @@ extern "C" {
  **/
 #define S3_DEFAULT_HOSTNAME                "s3.amazonaws.com"
 
+#define S3_SERVICE                         "s3" // WINSCP
+
 
 /**
  * S3_MAX_BUCKET_NAME_SIZE is the maximum size of a bucket name.
@@ -748,6 +750,8 @@ typedef struct S3BucketContext
      * If NULL, the default region ("us-east-1") will be used.
      */
     const char *authRegion;
+
+    const char *service; // WINSCP
 } S3BucketContext;
 
 

+ 4 - 2
libs/libs3/inc/util.h

@@ -73,10 +73,12 @@
 
 #define MAX_ACCESS_KEY_ID_LENGTH S3_MAX_ACCESS_KEY_ID_LENGTH
 
+#define S3_MAX_SCOPE_LENGTH 3 // WINSCP "s3"/"sts"
+
 // Maximum length of a credential string
-// <access key>/<yyyymmdd>/<region>/s3/aws4_request
+// <access key>/<yyyymmdd>/<region>/<scope>/aws4_request // WINSCP
 #define MAX_CREDENTIAL_SIZE \
-   (MAX_ACCESS_KEY_ID_LENGTH + 1) + 8 + 1 + S3_MAX_REGION_LENGTH + sizeof("/s3/aws4_request")
+   (MAX_ACCESS_KEY_ID_LENGTH + 1) + 8 + 1 + S3_MAX_REGION_LENGTH + sizeof("//aws4_request") + S3_MAX_SCOPE_LENGTH // WINSCP
 
 // Utilities -----------------------------------------------------------------
 

+ 3 - 2
libs/libs3/src/error_parser.c

@@ -49,10 +49,11 @@ static S3Status errorXmlCallback(const char *elementPath, const char *data,
     if (!strcmp(elementPath, "Error")) {
         // Ignore, this is the Error element itself, we only care about subs
     }
-    else if (!strcmp(elementPath, "Error/Code")) {
+    // WINSCP: ErrorResponse is for STS
+    else if (!strcmp(elementPath, "Error/Code") || !strcmp(elementPath, "ErrorResponse/Error/Code")) {
         string_buffer_append(errorParser->code, data, dataLen, fit);
     }
-    else if (!strcmp(elementPath, "Error/Message")) {
+    else if (!strcmp(elementPath, "Error/Message") || !strcmp(elementPath, "ErrorResponse/Error/Message")) {
         string_buffer_append(errorParser->message, data, dataLen, fit);
         errorParser->s3ErrorDetails.message = errorParser->message;
     }

+ 11 - 9
libs/libs3/src/request.c

@@ -1006,13 +1006,14 @@ static S3Status compose_auth_header(const RequestParams *params,
     if (params->bucketContext.authRegion) {
         awsRegion = params->bucketContext.authRegion;
     }
-    char scope[sizeof(values->requestDateISO8601) + sizeof(awsRegion) +
-               sizeof("//s3/aws4_request") + 1];
-    snprintf(scope, sizeof(scope), "%.8s/%s/s3/aws4_request",
-             values->requestDateISO8601, awsRegion);
+    const char * service = (params->bucketContext.service != NULL) ? params->bucketContext.service : S3_SERVICE; // WINSCP
+    int scopeSize = 8 + strlen(awsRegion) + strlen(service) + sizeof("///aws4_request"); // WINSCP
+    char * scope = new char[scopeSize]; // WINSCP
+    snprintf(scope, scopeSize, "%.8s/%s/%s/aws4_request",
+             values->requestDateISO8601, awsRegion, service); // WINSCP
 
     const int stringToSignLen = 17 + 17 + sizeof(values->requestDateISO8601) +
-        sizeof(scope) + sizeof(canonicalRequestHashHex) + 1; // WINSCP (heap allocation)
+        scopeSize - 1 + sizeof(canonicalRequestHashHex) + 1; // WINSCP (heap allocation)
     char * stringToSign = new char[stringToSignLen];
     snprintf(stringToSign, stringToSignLen, "AWS4-HMAC-SHA256\n%s\n%s\n%s",
              values->requestDateISO8601, scope, canonicalRequestHashHex);
@@ -1034,7 +1035,7 @@ static S3Status compose_auth_header(const RequestParams *params,
     CCHmac(kCCHmacAlgSHA256, dateKey, S3_SHA256_DIGEST_LENGTH, awsRegion,
            strlen(awsRegion), dateRegionKey);
     unsigned char dateRegionServiceKey[S3_SHA256_DIGEST_LENGTH];
-    CCHmac(kCCHmacAlgSHA256, dateRegionKey, S3_SHA256_DIGEST_LENGTH, "s3", 2,
+    CCHmac(kCCHmacAlgSHA256, dateRegionKey, S3_SHA256_DIGEST_LENGTH, "s3", 2, // not updated to WINSCP
            dateRegionServiceKey);
     unsigned char signingKey[S3_SHA256_DIGEST_LENGTH];
     CCHmac(kCCHmacAlgSHA256, dateRegionServiceKey, S3_SHA256_DIGEST_LENGTH,
@@ -1055,7 +1056,7 @@ static S3Status compose_auth_header(const RequestParams *params,
          NULL);
     unsigned char dateRegionServiceKey[S3_SHA256_DIGEST_LENGTH];
     HMAC(sha256evp, dateRegionKey, S3_SHA256_DIGEST_LENGTH,
-         (const unsigned char*) "s3", 2, dateRegionServiceKey, NULL);
+         (const unsigned char*) service, strlen(service), dateRegionServiceKey, NULL); // WINSCP
     unsigned char signingKey[S3_SHA256_DIGEST_LENGTH];
     HMAC(sha256evp, dateRegionServiceKey, S3_SHA256_DIGEST_LENGTH,
          (const unsigned char*) "aws4_request", strlen("aws4_request"),
@@ -1078,8 +1079,9 @@ static S3Status compose_auth_header(const RequestParams *params,
     }
 
     snprintf(values->authCredential, sizeof(values->authCredential),
-             "%s/%.8s/%s/s3/aws4_request", params->bucketContext.accessKeyId,
-             values->requestDateISO8601, awsRegion);
+             "%s/%.8s/%s/%s/aws4_request", params->bucketContext.accessKeyId,
+             values->requestDateISO8601, awsRegion, service); // WINSCP
+    delete[] scope; // WINSCP
 
     snprintf(values->authorizationHeader,
              sizeof(values->authorizationHeader),

+ 243 - 28
source/core/S3FileSystem.cpp

@@ -23,6 +23,8 @@
 #include "Http.h"
 #include <System.JSON.hpp>
 #include <System.DateUtils.hpp>
+#include "request.h"
+#include <XMLDoc.hpp>
 //---------------------------------------------------------------------------
 #pragma package(smart_init)
 //---------------------------------------------------------------------------
@@ -39,6 +41,12 @@
 #define AWS_PROFILE L"AWS_PROFILE"
 #define AWS_PROFILE_DEFAULT L"default"
 #define AWS_CONFIG_PROFILE_PREFIX L"profile "
+#define AWS_SOURCE_PROFILE_KEY L"source_profile"
+#define AWS_CREDENTIAL_SOURCE_KEY L"credential_source"
+#define AWS_CREDENTIAL_SOURCE_ENVIRONMENT L"Environment"
+#define AWS_CREDENTIAL_SOURCE_METADATA L"Ec2InstanceMetadata"
+#define AWS_ROLE_ARN L"AWS_ROLE_ARN"
+#define AWS_ROLE_ARN_KEY L"role_arn"
 //---------------------------------------------------------------------------
 static std::unique_ptr<TCriticalSection> LibS3Section(TraceInitPtr(new TCriticalSection()));
 //---------------------------------------------------------------------------
@@ -59,6 +67,11 @@ UnicodeString __fastcall S3LibDefaultRegion()
   return StrFromS3(S3_DEFAULT_REGION);
 }
 //---------------------------------------------------------------------------
+bool IsAmazonS3SessionData(TSessionData * Data)
+{
+  return IsDomainOrSubdomain(Data->HostNameExpanded, S3HostName);
+}
+//---------------------------------------------------------------------------
 static void NeedS3Config(
   UnicodeString & FileName, TDateTime & TimeStamp, std::unique_ptr<TCustomIniFile> & File,
   const UnicodeString & DefaultName, const UnicodeString & EnvName)
@@ -128,12 +141,28 @@ void GetS3Profiles(TStrings * Profiles, TCustomIniFile * File, const UnicodeStri
       UnicodeString Section = Sections->Strings[Index];
       if (Prefix.IsEmpty() || StartsText(Prefix, Section))
       {
-        // This is not consistent with AWS CLI.
-        // AWS CLI fails if one of AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY is set and other is missing:
-        // "Partial credentials found in env, missing: AWS_SECRET_ACCESS_KEY"
-        if (!File->ReadString(Section, AWS_ACCESS_KEY_ID, EmptyStr).IsEmpty() ||
-            !File->ReadString(Section, AWS_SECRET_ACCESS_KEY, EmptyStr).IsEmpty() ||
-            !File->ReadString(Section, AWS_SESSION_TOKEN, EmptyStr).IsEmpty())
+        bool Supported = false;
+        if (!File->ReadString(Section, AWS_ACCESS_KEY_ID, EmptyStr).IsEmpty() &&
+            !File->ReadString(Section, AWS_SECRET_ACCESS_KEY, EmptyStr).IsEmpty())
+        {
+          Supported = true;
+        }
+        else if (!File->ReadString(Section, AWS_ROLE_ARN_KEY, EmptyStr).IsEmpty())
+        {
+          if (!File->ReadString(Section, AWS_SOURCE_PROFILE_KEY, EmptyStr).IsEmpty())
+          {
+            Supported = true;
+          }
+          else
+          {
+            UnicodeString CredentialSource = File->ReadString(Section, AWS_CREDENTIAL_SOURCE_KEY, EmptyStr);
+            Supported =
+              SameText(CredentialSource, AWS_CREDENTIAL_SOURCE_ENVIRONMENT) ||
+              SameText(CredentialSource, AWS_CREDENTIAL_SOURCE_METADATA);
+          }
+        }
+
+        if (Supported)
         {
           Profiles->Add(MidStr(Section, Prefix.Length() + 1));
         }
@@ -153,7 +182,7 @@ TStrings * GetS3Profiles()
   return Result.release();
 }
 //---------------------------------------------------------------------------
-UnicodeString ReadUrl(const UnicodeString & Url)
+static UnicodeString ReadUrl(const UnicodeString & Url)
 {
   std::unique_ptr<THttp> Http(new THttp());
   Http->URL = Url;
@@ -162,6 +191,11 @@ UnicodeString ReadUrl(const UnicodeString & Url)
   return Http->Response.Trim();
 }
 //---------------------------------------------------------------------------
+static TDateTime ParseExpiration(const UnicodeString & S)
+{
+  return ISO8601ToDate(S, false);
+}
+//---------------------------------------------------------------------------
 static UnicodeString GetS3ConfigValue(
   TCustomIniFile * File, const UnicodeString & Profile, const UnicodeString & Name, const UnicodeString & Prefix, UnicodeString & Source)
 {
@@ -185,32 +219,73 @@ static UnicodeString GetS3ConfigValue(
   return Result;
 }
 //---------------------------------------------------------------------------
-UnicodeString GetS3ConfigValue(
-  const UnicodeString & Profile, const UnicodeString & Name, const UnicodeString & CredentialsName, UnicodeString * Source)
+static UnicodeString GetS3ConfigValue(
+  const UnicodeString & Profile, const UnicodeString & Name, UnicodeString & Source)
+{
+  UnicodeString Result = GetS3ConfigValue(S3CredentialsFile.get(), Profile, Name, EmptyStr, Source);
+  if (Result.IsEmpty())
+  {
+    Result = GetS3ConfigValue(S3ConfigFile.get(), Profile, Name, AWS_CONFIG_PROFILE_PREFIX, Source);
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
+static UnicodeString GetS3ConfigValue(
+  const UnicodeString & Profile, const UnicodeString & EnvName, const UnicodeString & AConfigName,
+  const UnicodeString & CredentialsName, UnicodeString * Source)
 {
   UnicodeString Result;
   UnicodeString ASource;
   TGuard Guard(LibS3Section.get());
+  bool TryCredentials = true;
 
   try
   {
     if (Profile.IsEmpty())
     {
-      Result = GetEnvironmentVariable(Name);
+      Result = GetEnvironmentVariable(EnvName);
     }
     if (!Result.IsEmpty())
     {
-      ASource = FORMAT(L"%%%s%%", (Name));
+      ASource = FORMAT(L"%%%s%%", (EnvName));
     }
-    else
+    else if (!AConfigName.IsEmpty())
     {
       NeedS3Config();
 
       UnicodeString AProfile = DefaultStr(Profile, S3Profile);
-      Result = GetS3ConfigValue(S3CredentialsFile.get(), AProfile, Name, EmptyStr, ASource);
+      UnicodeString ConfigName = DefaultStr(AConfigName, EnvName);
+      Result = GetS3ConfigValue(AProfile, ConfigName, ASource);
       if (Result.IsEmpty())
       {
-        Result = GetS3ConfigValue(S3ConfigFile.get(), AProfile, Name, AWS_CONFIG_PROFILE_PREFIX, ASource);
+        UnicodeString SourceSource;
+        UnicodeString SourceProfile = GetS3ConfigValue(AProfile, AWS_SOURCE_PROFILE_KEY, SourceSource);
+        if (!SourceProfile.IsEmpty())
+        {
+          TryCredentials = false;
+          Result = GetS3ConfigValue(SourceProfile, ConfigName, ASource);
+        }
+        else
+        {
+          UnicodeString CredentialSource = GetS3ConfigValue(AProfile, AWS_CREDENTIAL_SOURCE_KEY, SourceSource);
+          if (!CredentialSource.IsEmpty())
+          {
+            TryCredentials = false;
+            if (SameText(CredentialSource, AWS_CREDENTIAL_SOURCE_ENVIRONMENT))
+            {
+              Result = GetS3ConfigValue(EmptyStr, EnvName, EmptyStr, EmptyStr, &ASource);
+            }
+            else if (SameText(CredentialSource, AWS_CREDENTIAL_SOURCE_METADATA))
+            {
+              Result = GetS3ConfigValue(EmptyStr, EmptyStr, EmptyStr, CredentialsName, &ASource);
+            }
+          }
+        }
+
+        if (!Result.IsEmpty())
+        {
+          ASource = FORMAT(L"%s=>%s", (SourceSource, ASource));
+        }
       }
     }
   }
@@ -219,7 +294,7 @@ UnicodeString GetS3ConfigValue(
     throw ExtException(&E, MainInstructions(LoadStr(S3_CONFIG_ERROR)));
   }
 
-  if (Result.IsEmpty())
+  if (Result.IsEmpty() && TryCredentials && !CredentialsName.IsEmpty())
   {
     if (S3SecurityProfileChecked && (S3CredentialsExpiration != TDateTime()) && (IncHour(S3CredentialsExpiration, -1) < Now()))
     {
@@ -273,7 +348,7 @@ UnicodeString GetS3ConfigValue(
             throw new Exception(L"Missing \"Expiration\" value");
           }
           UnicodeString ExpirationStr = ExpirationValue->Value();
-          S3CredentialsExpiration = ISO8601ToDate(ExpirationStr, false);
+          S3CredentialsExpiration = ParseExpiration(ExpirationStr);
           AppLogFmt(L"Credentials expiration: %s", (StandardTimestamp(S3CredentialsExpiration)));
 
           std::unique_ptr<TJSONPairEnumerator> Enumerator(ProfileData->GetEnumerator());
@@ -313,17 +388,22 @@ UnicodeString GetS3ConfigValue(
 //---------------------------------------------------------------------------
 UnicodeString S3EnvUserName(const UnicodeString & Profile, UnicodeString * Source)
 {
-  return GetS3ConfigValue(Profile, AWS_ACCESS_KEY_ID, L"AccessKeyId", Source);
+  return GetS3ConfigValue(Profile, AWS_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID, L"AccessKeyId", Source);
 }
 //---------------------------------------------------------------------------
 UnicodeString S3EnvPassword(const UnicodeString & Profile, UnicodeString * Source)
 {
-  return GetS3ConfigValue(Profile, AWS_SECRET_ACCESS_KEY, L"SecretAccessKey", Source);
+  return GetS3ConfigValue(Profile, AWS_SECRET_ACCESS_KEY, AWS_SECRET_ACCESS_KEY, L"SecretAccessKey", Source);
 }
 //---------------------------------------------------------------------------
 UnicodeString S3EnvSessionToken(const UnicodeString & Profile, UnicodeString * Source)
 {
-  return GetS3ConfigValue(Profile, AWS_SESSION_TOKEN, L"Token", Source);
+  return GetS3ConfigValue(Profile, AWS_SESSION_TOKEN, AWS_SESSION_TOKEN, L"Token", Source);
+}
+//---------------------------------------------------------------------------
+UnicodeString S3EnvRoleArn(const UnicodeString & Profile, UnicodeString * Source)
+{
+  return GetS3ConfigValue(Profile, AWS_ROLE_ARN, AWS_ROLE_ARN_KEY, EmptyStr, Source);
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
@@ -392,11 +472,6 @@ void __fastcall TS3FileSystem::Open()
       FTerminal->FatalError(NULL, LoadStr(CREDENTIALS_NOT_SPECIFIED));
     }
   }
-  FAccessKeyId = UTF8String(AccessKeyId);
-  if (FAccessKeyId.Length() > S3_MAX_ACCESS_KEY_ID_LENGTH)
-  {
-    FAccessKeyId.SetLength(S3_MAX_ACCESS_KEY_ID_LENGTH);
-  }
 
   UnicodeString Password = Data->Password;
   if (Password.IsEmpty() && Data->S3CredentialsEnv)
@@ -417,7 +492,6 @@ void __fastcall TS3FileSystem::Open()
       FTerminal->FatalError(NULL, LoadStr(CREDENTIALS_NOT_SPECIFIED));
     }
   }
-  FSecretAccessKey = UTF8String(SecretAccessKey);
 
   UnicodeString SessionToken = Data->S3SessionToken;
   if (SessionToken.IsEmpty() && Data->S3CredentialsEnv)
@@ -429,8 +503,8 @@ void __fastcall TS3FileSystem::Open()
       FTerminal->LogEvent(FORMAT(L"Session token read from %s", (SessionTokenSource)));
     }
   }
-  FSecurityTokenBuf = UTF8String(SessionToken);
-  FSecurityToken = static_cast<const char *>(FSecurityTokenBuf.data());
+
+  SetCredentials(AccessKeyId, SecretAccessKey, SessionToken);
 
   FHostName = UTF8String(Data->HostNameExpanded);
   FPortSuffix = UTF8String();
@@ -457,6 +531,25 @@ void __fastcall TS3FileSystem::Open()
 
   S3_set_request_context_requester_pays(FRequestContext, Data->S3RequesterPays);
 
+  if (IsAmazonS3SessionData(Data))
+  {
+    UnicodeString RoleArn = Data->S3RoleArn;
+    if (RoleArn.IsEmpty())
+    {
+      UnicodeString RoleArnSource;
+      RoleArn = S3EnvRoleArn(S3Profile, &RoleArnSource);
+      if (!RoleArn.IsEmpty())
+      {
+        FTerminal->LogEvent(FORMAT(L"Role ARN read from %s", (RoleArnSource)));
+      }
+    }
+
+    if (!RoleArn.IsEmpty())
+    {
+      AssumeRole(RoleArn);
+    }
+  }
+
   FActive = false;
   try
   {
@@ -476,6 +569,20 @@ void __fastcall TS3FileSystem::Open()
   FActive = true;
 }
 //---------------------------------------------------------------------------
+void TS3FileSystem::SetCredentials(
+  const UnicodeString & AccessKeyId, const UnicodeString & SecretAccessKey, const UnicodeString & SessionToken)
+{
+  FAccessKeyId = UTF8String(AccessKeyId);
+  if (FAccessKeyId.Length() > S3_MAX_ACCESS_KEY_ID_LENGTH)
+  {
+    FAccessKeyId.SetLength(S3_MAX_ACCESS_KEY_ID_LENGTH);
+  }
+  FSecretAccessKey = UTF8String(SecretAccessKey);
+
+  FSecurityTokenBuf = UTF8String(SessionToken);
+  FSecurityToken = static_cast<const char *>(FSecurityTokenBuf.data());
+}
+//---------------------------------------------------------------------------
 struct TLibS3CallbackData
 {
   TLibS3CallbackData()
@@ -711,6 +818,114 @@ void TS3FileSystem::LibS3Deinitialize()
   S3_deinitialize();
 }
 //---------------------------------------------------------------------------
+struct TLibS3AssumeRoleCallbackData : TLibS3CallbackData
+{
+  RawByteString Contents;
+};
+//---------------------------------------------------------------------------
+const UnicodeString AssumeRoleVersion(TraceInitStr(L"2011-06-15"));
+const UnicodeString AssumeRoleNamespace(TraceInitStr(FORMAT(L"https://sts.amazonaws.com/doc/%s/", (AssumeRoleVersion))));
+//---------------------------------------------------------------------------
+static _di_IXMLNode AssumeRoleNeedNode(const _di_IXMLNodeList & NodeList, const UnicodeString & Name)
+{
+  _di_IXMLNode Result = NodeList->FindNode(Name, AssumeRoleNamespace);
+  if (Result == NULL)
+  {
+    throw Exception(FMTLOAD(S3_ASSUME_ROLE_RESPONSE_ERROR, (Name)));
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
+void TS3FileSystem::AssumeRole(const UnicodeString & RoleArn)
+{
+  // According to AWS cli does, AWS_ROLE_SESSION_NAME does not apply here
+  UnicodeString RoleSessionName = DefaultStr(FTerminal->SessionData->S3RoleSessionName, AppNameString());
+
+  try
+  {
+    TLibS3AssumeRoleCallbackData Data;
+    RequestInit(Data);
+
+    UnicodeString QueryParams =
+      FORMAT(L"Version=%s&Action=AssumeRole&RoleSessionName=%s&RoleArn=%s", (
+        AssumeRoleVersion, EncodeUrlString(RoleSessionName), EncodeUrlString(RoleArn)));
+
+    UTF8String AuthRegionBuf = UTF8String(FAuthRegion);
+    UTF8String QueryParamsBuf = UTF8String(QueryParams);
+    UTF8String StsService = L"sts";
+    AnsiString StsHostName =
+      AnsiString(ReplaceStr(S3LibDefaultHostName(), FORMAT(L"%s.", (UnicodeString(S3_SERVICE))), FORMAT(L"%s.", (UnicodeString(StsService)))));
+    DebugAssert(StsHostName != S3LibDefaultHostName());
+
+    RequestParams AssumeRoleRequestParams =
+    {
+      HttpRequestTypeGET,
+      {
+        StsHostName.c_str(),
+        NULL,
+        S3ProtocolHTTPS,
+        S3UriStylePath, // Otherwise lib3s prefixes "(null)." (because of NULL bucketName)
+        FAccessKeyId.c_str(),
+        FSecretAccessKey.c_str(),
+        FSecurityToken,
+        AuthRegionBuf.c_str(),
+        StsService.c_str(),
+      },
+      NULL,
+      QueryParamsBuf.c_str(),
+      NULL, NULL, NULL, NULL, 0, 0, NULL, NULL, NULL, 0,
+      LibS3AssumeRoleDataCallback,
+      LibS3AssumeRoleCompleteCallback,
+      &Data,
+      FTimeout
+    };
+
+    request_perform(&AssumeRoleRequestParams, FRequestContext);
+
+    CheckLibS3Error(Data);
+
+    const _di_IXMLDocument Document = interface_cast<Xmlintf::IXMLDocument>(new TXMLDocument(NULL));
+    Document->LoadFromXML(UnicodeString(UTF8String(Data.Contents)));
+    _di_IXMLNode ResponseNode = AssumeRoleNeedNode(Document->ChildNodes, L"AssumeRoleResponse");
+    _di_IXMLNode ResultNode = AssumeRoleNeedNode(ResponseNode->ChildNodes, L"AssumeRoleResult");
+    _di_IXMLNode CredentialsNode = AssumeRoleNeedNode(ResultNode->ChildNodes, L"Credentials");
+    UnicodeString AccessKeyId = AssumeRoleNeedNode(CredentialsNode->ChildNodes, L"AccessKeyId")->Text;
+    UnicodeString SecretAccessKey = AssumeRoleNeedNode(CredentialsNode->ChildNodes, L"SecretAccessKey")->Text;
+    UnicodeString SessionToken = AssumeRoleNeedNode(CredentialsNode->ChildNodes, L"SessionToken")->Text;
+    UnicodeString ExpirationStr = AssumeRoleNeedNode(CredentialsNode->ChildNodes, L"Expiration")->Text;
+
+    FTerminal->LogEvent(FORMAT(L"Assumed role \"%s\".", (RoleArn)));
+    FTerminal->LogEvent(FORMAT(L"New acess key is: %s", (AccessKeyId)));
+    if (Configuration->LogSensitive)
+    {
+      FTerminal->LogEvent(FORMAT(L"Secret access key: %s", (SecretAccessKey)));
+      FTerminal->LogEvent(FORMAT(L"Session token: %s", (SessionToken)));
+    }
+
+    // Only logged for now
+    TDateTime Expiration = ParseExpiration(ExpirationStr);
+    FTerminal->LogEvent(FORMAT(L"Credentials expiration: %s", (StandardTimestamp(Expiration))));
+
+    SetCredentials(AccessKeyId, SecretAccessKey, SessionToken);
+  }
+  catch (Exception & E)
+  {
+    throw ExtException(MainInstructions(FMTLOAD(S3_ASSUME_ROLE_ERROR, (RoleArn))), &E);
+  }
+}
+//---------------------------------------------------------------------------
+void TS3FileSystem::LibS3AssumeRoleCompleteCallback(S3Status Status, const S3ErrorDetails * Error, void * CallbackData)
+{
+  LibS3ResponseCompleteCallback(Status, Error, CallbackData);
+}
+//---------------------------------------------------------------------------
+S3Status TS3FileSystem::LibS3AssumeRoleDataCallback(int BufferSize, const char * Buffer, void * CallbackData)
+{
+  TLibS3AssumeRoleCallbackData & Data = *static_cast<TLibS3AssumeRoleCallbackData *>(CallbackData);
+  Data.Contents += RawByteString(Buffer, BufferSize);
+  return S3StatusOK;
+}
+//---------------------------------------------------------------------------
 UnicodeString TS3FileSystem::GetFolderKey(const UnicodeString & Key)
 {
   return Key + L"/";
@@ -877,7 +1092,7 @@ bool __fastcall TS3FileSystem::GetActive()
 //---------------------------------------------------------------------------
 void __fastcall TS3FileSystem::CollectUsage()
 {
-  if (IsDomainOrSubdomain(FTerminal->SessionData->HostNameExpanded, S3HostName))
+  if (IsAmazonS3SessionData(FTerminal->SessionData))
   {
     FTerminal->Configuration->Usage->Inc(L"OpenedSessionsS3Amazon");
   }

+ 6 - 0
source/core/S3FileSystem.h

@@ -166,6 +166,8 @@ protected:
   unsigned short AclGrantToPermissions(S3AclGrant & AclGrant, const TS3FileProperties & Properties);
   bool ParsePathForPropertiesRequests(
     const UnicodeString & Path, const TRemoteFile * File, UnicodeString & BucketName, UnicodeString & Key);
+  void AssumeRole(const UnicodeString & RoleArn);
+  void SetCredentials(const UnicodeString & AccessKeyId, const UnicodeString & SecretAccessKey, const UnicodeString & SessionToken);
 
   static TS3FileSystem * GetFileSystem(void * CallbackData);
   static void LibS3SessionCallback(ne_session_s * Session, void * CallbackData);
@@ -184,6 +186,8 @@ protected:
   static int LibS3MultipartCommitPutObjectDataCallback(int BufferSize, char * Buffer, void * CallbackData);
   static S3Status LibS3MultipartResponsePropertiesCallback(const S3ResponseProperties * Properties, void * CallbackData);
   static S3Status LibS3GetObjectDataCallback(int BufferSize, const char * Buffer, void * CallbackData);
+  static void LibS3AssumeRoleCompleteCallback(S3Status Status, const S3ErrorDetails * Error, void * CallbackData);
+  static S3Status LibS3AssumeRoleDataCallback(int BufferSize, const char * Buffer, void * CallbackData);
 
   static const int S3MinMultiPartChunkSize;
   static const int S3MaxMultiPartChunks;
@@ -192,9 +196,11 @@ protected:
 UnicodeString __fastcall S3LibVersion();
 UnicodeString __fastcall S3LibDefaultHostName();
 UnicodeString __fastcall S3LibDefaultRegion();
+bool IsAmazonS3SessionData(TSessionData * Data);
 TStrings * GetS3Profiles();
 UnicodeString S3EnvUserName(const UnicodeString & Profile, UnicodeString * Source = NULL);
 UnicodeString S3EnvPassword(const UnicodeString & Profile, UnicodeString * Source = NULL);
 UnicodeString S3EnvSessionToken(const UnicodeString & Profile, UnicodeString * Source = NULL);
+UnicodeString S3EnvRoleArn(const UnicodeString & Profile, UnicodeString * Source = NULL);
 //------------------------------------------------------------------------------
 #endif

+ 18 - 0
source/core/SessionData.cpp

@@ -294,6 +294,8 @@ void __fastcall TSessionData::DefaultSettings()
   // S3
   S3DefaultRegion = EmptyStr;
   S3SessionToken = EmptyStr;
+  S3RoleArn = EmptyStr;
+  S3RoleSessionName = EmptyStr;
   S3Profile = EmptyStr;
   S3UrlStyle = s3usVirtualHost;
   S3MaxKeys = asAuto;
@@ -463,6 +465,8 @@ void __fastcall TSessionData::NonPersistant()
   \
   PROPERTY(S3DefaultRegion); \
   PROPERTY(S3SessionToken); \
+  PROPERTY(S3RoleArn); \
+  PROPERTY(S3RoleSessionName); \
   PROPERTY(S3Profile); \
   PROPERTY(S3UrlStyle); \
   PROPERTY(S3MaxKeys); \
@@ -813,6 +817,8 @@ void __fastcall TSessionData::DoLoad(THierarchicalStorage * Storage, bool PuttyI
 
   S3DefaultRegion = Storage->ReadString(L"S3DefaultRegion", S3DefaultRegion);
   S3SessionToken = Storage->ReadString(L"S3SessionToken", S3SessionToken);
+  S3RoleArn = Storage->ReadString(L"S3RoleArn", S3RoleArn);
+  S3RoleSessionName = Storage->ReadString(L"S3RoleSessionName", S3RoleSessionName);
   S3Profile = Storage->ReadString(L"S3Profile", S3Profile);
   S3UrlStyle = (TS3UrlStyle)Storage->ReadInteger(L"S3UrlStyle", S3UrlStyle);
   S3MaxKeys = Storage->ReadEnum(L"S3MaxKeys", S3MaxKeys, AutoSwitchMapping);
@@ -1143,6 +1149,8 @@ void __fastcall TSessionData::DoSave(THierarchicalStorage * Storage,
     WRITE_DATA(Integer, InternalEditorEncoding);
     WRITE_DATA(String, S3DefaultRegion);
     WRITE_DATA(String, S3SessionToken);
+    WRITE_DATA(String, S3RoleArn);
+    WRITE_DATA(String, S3RoleSessionName);
     WRITE_DATA(String, S3Profile);
     WRITE_DATA(Integer, S3UrlStyle);
     WRITE_DATA(Integer, S3MaxKeys);
@@ -4569,6 +4577,16 @@ void __fastcall TSessionData::SetS3SessionToken(UnicodeString value)
   SET_SESSION_PROPERTY(S3SessionToken);
 }
 //---------------------------------------------------------------------
+void __fastcall TSessionData::SetS3RoleArn(UnicodeString value)
+{
+  SET_SESSION_PROPERTY(S3RoleArn);
+}
+//---------------------------------------------------------------------
+void __fastcall TSessionData::SetS3RoleSessionName(UnicodeString value)
+{
+  SET_SESSION_PROPERTY(S3RoleSessionName);
+}
+//---------------------------------------------------------------------
 void __fastcall TSessionData::SetS3Profile(UnicodeString value)
 {
   SET_SESSION_PROPERTY(S3Profile);

+ 6 - 0
source/core/SessionData.h

@@ -234,6 +234,8 @@ private:
   int FInternalEditorEncoding;
   UnicodeString FS3DefaultRegion;
   UnicodeString FS3SessionToken;
+  UnicodeString FS3RoleArn;
+  UnicodeString FS3RoleSessionName;
   UnicodeString FS3Profile;
   TS3UrlStyle FS3UrlStyle;
   TAutoSwitch FS3MaxKeys;
@@ -429,6 +431,8 @@ private:
   void __fastcall SetInternalEditorEncoding(int value);
   void __fastcall SetS3DefaultRegion(UnicodeString value);
   void __fastcall SetS3SessionToken(UnicodeString value);
+  void __fastcall SetS3RoleArn(UnicodeString value);
+  void __fastcall SetS3RoleSessionName(UnicodeString value);
   void __fastcall SetS3Profile(UnicodeString value);
   void __fastcall SetS3UrlStyle(TS3UrlStyle value);
   void __fastcall SetS3MaxKeys(TAutoSwitch value);
@@ -716,6 +720,8 @@ public:
   __property int InternalEditorEncoding = { read = FInternalEditorEncoding, write = SetInternalEditorEncoding };
   __property UnicodeString S3DefaultRegion = { read = FS3DefaultRegion, write = SetS3DefaultRegion };
   __property UnicodeString S3SessionToken = { read = FS3SessionToken, write = SetS3SessionToken };
+  __property UnicodeString S3RoleArn = { read = FS3RoleArn, write = SetS3RoleArn };
+  __property UnicodeString S3RoleSessionName = { read = FS3RoleSessionName, write = SetS3RoleSessionName };
   __property UnicodeString S3Profile = { read = FS3Profile, write = SetS3Profile };
   __property TS3UrlStyle S3UrlStyle = { read = FS3UrlStyle, write = SetS3UrlStyle };
   __property TAutoSwitch S3MaxKeys = { read = FS3MaxKeys, write = SetS3MaxKeys };

+ 4 - 0
source/core/SessionInfo.cpp

@@ -1430,6 +1430,10 @@ void __fastcall TSessionLog::DoAddStartupInfo(TSessionData * Data)
       {
         ADF(L"S3: Session token: %s", (Data->S3SessionToken));
       }
+      if (!Data->S3RoleArn.IsEmpty())
+      {
+        ADF(L"S3: Role ARN: %s (session name: %s)", (Data->S3RoleArn, DefaultStr(Data->S3RoleSessionName, L"default")));
+      }
       if (Data->S3CredentialsEnv)
       {
         ADF(L"S3: Credentials from AWS environment: %s", (DefaultStr(Data->S3Profile, L"General")));

+ 10 - 2
source/forms/Login.cpp

@@ -2219,6 +2219,7 @@ void TLoginDialog::UpdateS3Credentials()
     if (FSessionData != NULL)
     {
       FSessionData->S3SessionToken = S3EnvSessionToken(S3Profile);
+      FSessionData->S3RoleArn = S3EnvRoleArn(S3Profile);
     }
   }
 }
@@ -2255,9 +2256,16 @@ void __fastcall TLoginDialog::TransferProtocolComboChange(TObject * Sender)
           {
             PasswordEdit->Clear();
           }
-          if ((FSessionData != NULL) && (FSessionData->S3SessionToken == S3EnvSessionToken(S3Profile)))
+          if (FSessionData != NULL)
           {
-            FSessionData->S3SessionToken = UnicodeString();
+            if (FSessionData->S3SessionToken == S3EnvSessionToken(S3Profile))
+            {
+              FSessionData->S3SessionToken = EmptyStr;
+            }
+            if (FSessionData->S3RoleArn == S3EnvRoleArn(S3Profile))
+            {
+              FSessionData->S3RoleArn = EmptyStr;
+            }
           }
         }
       }

+ 6 - 0
source/forms/SiteAdvanced.cpp

@@ -229,11 +229,13 @@ void __fastcall TSiteAdvancedDialog::LoadSession()
     S3RequesterPaysCheck->Checked = FSessionData->S3RequesterPays;
 
     UnicodeString S3SessionToken = FSessionData->S3SessionToken;
+    UnicodeString S3RoleArn = FSessionData->S3RoleArn;
     if (FSessionData->HasAutoCredentials())
     {
       try
       {
         S3SessionToken = S3EnvSessionToken(FSessionData->S3Profile);
+        S3RoleArn = S3EnvRoleArn(FSessionData->S3Profile);
       }
       catch (...)
       {
@@ -241,6 +243,7 @@ void __fastcall TSiteAdvancedDialog::LoadSession()
       }
     }
     S3SessionTokenMemo->Lines->Text = S3SessionToken;
+    S3RoleArnEdit->Text = S3RoleArn;
 
     // Authentication page
     SshNoUserAuthCheck->Checked = FSessionData->SshNoUserAuth;
@@ -664,6 +667,7 @@ void __fastcall TSiteAdvancedDialog::SaveSession(TSessionData * SessionData)
     // Trim not to try to authenticate with a stray new-line
     SessionData->S3SessionToken = S3SessionTokenMemo->Lines->Text.Trim();
   }
+  FSessionData->S3RoleArn = S3RoleArnEdit->Text;
 
   // Proxy page
   SessionData->ProxyMethod = GetProxyMethod();
@@ -1071,6 +1075,8 @@ void __fastcall TSiteAdvancedDialog::UpdateControls()
     S3Sheet->Enabled = S3Protocol;
     EnableControl(S3SessionTokenMemo, S3Sheet->Enabled && !FSessionData->HasAutoCredentials());
     EnableControl(S3SessionTokenLabel, S3SessionTokenMemo->Enabled);
+    EnableControl(S3RoleArnEdit, S3SessionTokenMemo->Enabled && IsAmazonS3SessionData(FSessionData));
+    EnableControl(S3RoleArnLabel, S3RoleArnEdit->Enabled);
 
     // tunnel sheet
     TunnelSheet->Enabled = SshProtocol;

+ 21 - 3
source/forms/SiteAdvanced.dfm

@@ -1123,13 +1123,13 @@ object SiteAdvancedDialog: TSiteAdvancedDialog
           Left = 1
           Top = 109
           Width = 393
-          Height = 143
+          Height = 189
           Anchors = [akLeft, akTop, akRight]
           Caption = 'Authentication'
           TabOrder = 1
           DesignSize = (
             393
-            143)
+            189)
           object S3SessionTokenLabel: TLabel
             Left = 12
             Top = 20
@@ -1138,17 +1138,35 @@ object SiteAdvancedDialog: TSiteAdvancedDialog
             Caption = '&Session token:'
             FocusControl = S3SessionTokenMemo
           end
+          object S3RoleArnLabel: TLabel
+            Left = 11
+            Top = 140
+            Width = 49
+            Height = 13
+            Caption = '&Role ARN:'
+            FocusControl = S3RoleArnEdit
+          end
           object S3SessionTokenMemo: TMemo
             Left = 11
             Top = 36
             Width = 371
             Height = 93
-            Anchors = [akLeft, akTop, akRight, akBottom]
+            Anchors = [akLeft, akTop, akRight]
             MaxLength = 10000
             TabOrder = 0
             OnChange = DataChange
             OnKeyDown = NoteMemoKeyDown
           end
+          object S3RoleArnEdit: TEdit
+            Left = 12
+            Top = 156
+            Width = 368
+            Height = 21
+            Anchors = [akLeft, akTop, akRight]
+            TabOrder = 1
+            Text = 'S3RoleArnEdit'
+            OnChange = DataChange
+          end
         end
       end
       object WebDavSheet: TTabSheet

+ 2 - 0
source/forms/SiteAdvanced.h

@@ -285,6 +285,8 @@ __published:
   TCheckBox *S3RequesterPaysCheck;
   TRadioButton *FtpPingDirectoryListingButton;
   TCheckBox *UsePosixRenameCheck;
+  TLabel *S3RoleArnLabel;
+  TEdit *S3RoleArnEdit;
   void __fastcall DataChange(TObject *Sender);
   void __fastcall FormShow(TObject *Sender);
   void __fastcall PageControlChange(TObject *Sender);

+ 2 - 0
source/resource/TextsCore.h

@@ -288,6 +288,8 @@
 #define SSH_HOST_CA_NO_KEY_TYPE 768
 #define SSH_HOST_CA_CERTIFICATE 769
 #define SSH_HOST_CA_INVALID     770
+#define S3_ASSUME_ROLE_ERROR    780
+#define S3_ASSUME_ROLE_RESPONSE_ERROR 781
 
 #define CORE_CONFIRMATION_STRINGS 300
 #define CONFIRM_PROLONG_TIMEOUT3 301

+ 2 - 0
source/resource/TextsCore1.rc

@@ -264,6 +264,8 @@ BEGIN
   SSH_HOST_CA_NO_KEY_TYPE, "Invalid key (no key type)."
   SSH_HOST_CA_CERTIFICATE, "CA key may not be a certificate (type is '%s')."
   SSH_HOST_CA_INVALID, "Invalid '%s' key data."
+  S3_ASSUME_ROLE_ERROR, "Error assuming role '%s'."
+  S3_ASSUME_ROLE_RESPONSE_ERROR, "Unexpected response to assume role request (%s)."
 
   CORE_CONFIRMATION_STRINGS, "CORE_CONFIRMATION"
   CONFIRM_PROLONG_TIMEOUT3, "Host is not communicating for %d seconds.\n\nWait for another %0:d seconds?"