/** * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ #include <aws/core/config/AWSProfileConfigLoader.h> #include <aws/core/utils/memory/stl/AWSSet.h> #include <aws/core/utils/memory/stl/AWSStreamFwd.h> #include <aws/core/utils/StringUtils.h> #include <aws/core/utils/logging/LogMacros.h> #include <fstream> namespace Aws { namespace Config { using namespace Aws::Utils; using namespace Aws::Auth; static const char REGION_KEY[] = "region"; static const char ACCESS_KEY_ID_KEY[] = "aws_access_key_id"; static const char SECRET_KEY_KEY[] = "aws_secret_access_key"; static const char SESSION_TOKEN_KEY[] = "aws_session_token"; static const char SSO_START_URL_KEY[] = "sso_start_url"; static const char SSO_REGION_KEY[] = "sso_region"; static const char SSO_ACCOUNT_ID_KEY[] = "sso_account_id"; static const char SSO_ROLE_NAME_KEY[] = "sso_role_name"; static const char SSO_SESSION_KEY[] = "sso_session"; static const char ROLE_ARN_KEY[] = "role_arn"; static const char EXTERNAL_ID_KEY[] = "external_id"; static const char CREDENTIAL_PROCESS_COMMAND[] = "credential_process"; static const char SOURCE_PROFILE_KEY[] = "source_profile"; static const char PROFILE_SECTION[] = "profile"; static const char DEFAULT[] = "default"; static const char SSO_SESSION_SECTION[] = "sso-session"; static const char DEFAULTS_MODE_KEY[] = "defaults_mode"; static const char EQ = '='; static const char LEFT_BRACKET = '['; static const char RIGHT_BRACKET = ']'; static const char PARSER_TAG[] = "Aws::Config::ConfigFileProfileFSM"; // generated by python from identifier regex pattern from the spec: R"([A-Za-z0-9_\-/.%@:\+]+)": // #py: ''.join(chr(i) for i in range(128) if re.match("[A-Za-z0-9_\-\/.%@:\+]", chr(i))) const char IDENTIFIER_ALLOWED_CHARACTERS[] = R"(%+-./0123456789:@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz)"; static const size_t IDENTIFIER_ALLOWED_CHARACTERS_SZ = sizeof(IDENTIFIER_ALLOWED_CHARACTERS) - 1; const char WHITESPACE_CHARACTERS[] = "\t "; static const size_t WHITESPACE_CHARACTERS_SZ = sizeof(WHITESPACE_CHARACTERS) - 1; const char COMMENT_START[] = "#;"; static const size_t COMMENT_START_SZ = sizeof(COMMENT_START) - 1; struct ProfilePropertyAccessFunctions { const char* PropertyKey; std::function<void(Profile&, const Aws::String&)> Setter; std::function<const Aws::String&(const Profile&)> Getter; }; static const ProfilePropertyAccessFunctions PROFILE_PROPERTY_FUNCS[] = {{REGION_KEY, &Profile::SetRegion, &Profile::GetRegion}, //ACCESS_KEY_ID_KEY, - AwsCredentials require special handling //SECRET_KEY_KEY, //SESSION_TOKEN_KEY, {SSO_START_URL_KEY, &Profile::SetSsoStartUrl, &Profile::GetSsoStartUrl}, {SSO_REGION_KEY, &Profile::SetSsoRegion, &Profile::GetSsoRegion}, {SSO_ACCOUNT_ID_KEY, &Profile::SetSsoAccountId, &Profile::GetSsoAccountId}, {SSO_ROLE_NAME_KEY, &Profile::SetSsoRoleName, &Profile::GetSsoRoleName}, //SSO_SESSION_KEY - SsoSession requires special handling {ROLE_ARN_KEY, &Profile::SetRoleArn, &Profile::GetRoleArn}, {EXTERNAL_ID_KEY, &Profile::SetExternalId, &Profile::GetExternalId}, {CREDENTIAL_PROCESS_COMMAND, &Profile::SetCredentialProcess, &Profile::GetCredentialProcess}, {SOURCE_PROFILE_KEY, &Profile::SetSourceProfile, &Profile::GetSourceProfile}, {DEFAULTS_MODE_KEY, &Profile::SetDefaultsMode, &Profile::GetDefaultsMode}}; template<typename EntryT, size_t N> const EntryT* FindInStaticArray(const EntryT (&array)[N], const Aws::String& searchKey) { const EntryT* found = std::find_if(array, array + N, [&searchKey](const EntryT& entry) { return searchKey == entry.PropertyKey; }); if(!!found && found != array + N) return found; return nullptr; } static const char* PROFILE_KEY_SPECIAL_HANDLING[] = {ACCESS_KEY_ID_KEY, SECRET_KEY_KEY, SESSION_TOKEN_KEY, SSO_SESSION_KEY}; static const size_t PROFILE_KEY_SPECIAL_HANDLING_SZ = sizeof(PROFILE_KEY_SPECIAL_HANDLING) / sizeof(PROFILE_KEY_SPECIAL_HANDLING[0]); struct SsoSessionPropertyAccessFunctions { const char* PropertyKey; std::function<void(Profile::SsoSession&, const Aws::String&)> Setter; std::function<const Aws::String&(const Profile::SsoSession&)> Getter; }; static const SsoSessionPropertyAccessFunctions SSO_SESSION_PROPERTY_FUNCS[] = {{SSO_REGION_KEY, &Profile::SsoSession::SetSsoRegion, &Profile::SsoSession::GetSsoRegion}, {SSO_START_URL_KEY, &Profile::SsoSession::SetSsoStartUrl, &Profile::SsoSession::GetSsoStartUrl}}; class ConfigFileProfileFSM { public: ConfigFileProfileFSM(bool useProfilePrefix) : m_useProfilePrefix(useProfilePrefix) {} const Aws::Map<String, Profile>& GetProfiles() const { return m_foundProfiles; } void ParseStream(Aws::IStream& stream) { static const size_t ASSUME_EMPTY_LEN = 3; State currentState = START; Aws::String currentSectionName; Aws::Map<Aws::String, Aws::String> currentKeyValues; Aws::String rawLine; while(std::getline(stream, rawLine) && currentState != FAILURE) { Aws::String line = rawLine.substr(0, rawLine.find_first_of(COMMENT_START)); // ignore comments if (line.empty() || line.length() < ASSUME_EMPTY_LEN || line.find_first_not_of(WHITESPACE_CHARACTERS) == Aws::String::npos) { continue; } auto openPos = line.find(LEFT_BRACKET); auto closePos = line.find(RIGHT_BRACKET); if(openPos != std::string::npos && closePos != std::string::npos) { FlushSection(currentState, currentSectionName, currentKeyValues); currentKeyValues.clear(); ParseSectionDeclaration(line, currentSectionName, currentState); continue; } if(PROFILE_FOUND == currentState || SSO_SESSION_FOUND == currentState) { auto equalsPos = line.find(EQ); if (equalsPos != std::string::npos) { auto key = StringUtils::Trim(line.substr(0, equalsPos).c_str()); auto value = StringUtils::Trim(line.substr(equalsPos + 1).c_str()); currentKeyValues[key] = value; continue; } } if(UNKNOWN_SECTION_FOUND == currentState) { // skip any unknown sections continue; } AWS_LOGSTREAM_ERROR(PARSER_TAG, "Unexpected line in the aws shared profile: " << rawLine); currentState = FAILURE; break; } FlushSection(currentState, currentSectionName, currentKeyValues); // Put sso-sessions into profiles for(auto& profile : m_foundProfiles) { const Aws::String& profileSsoSessionName = profile.second.GetValue(SSO_SESSION_KEY); if(!profileSsoSessionName.empty()) { auto ssoSessionIt = m_foundSsoSessions.find(profileSsoSessionName); if(ssoSessionIt == m_foundSsoSessions.end()) { AWS_LOGSTREAM_ERROR(PARSER_TAG, "AWS profile has reference to a missing sso_session: " << profileSsoSessionName); currentState = FAILURE; continue; } auto ssoSession = ssoSessionIt->second; auto prof = profile.second; // If sso session and profile have conflicting start url or region, fail to parse // the session/sso specific profile properties auto hasConflictingStartUrls = !ssoSession.GetSsoStartUrl().empty() && !prof.GetSsoStartUrl().empty() && ssoSession.GetSsoStartUrl() != prof.GetSsoStartUrl(); auto hasConflictingRegions = !ssoSession.GetSsoRegion().empty() && !prof.GetSsoRegion().empty() && ssoSession.GetSsoRegion() != prof.GetSsoRegion(); if (hasConflictingStartUrls || hasConflictingRegions) { AWS_LOGSTREAM_ERROR(PARSER_TAG, "SSO profile has a start url or region conflict with sso session"); prof.SetSsoStartUrl(""); prof.SetSsoRegion(""); prof.SetSsoAccountId(""); prof.SetSsoRoleName(""); continue; } profile.second.SetSsoSession(ssoSessionIt->second); } } if(FAILURE == currentState) { AWS_LOGSTREAM_ERROR(PARSER_TAG, "AWS shared profile config parsing failed"); } } private: // true means Shared Config parsing, false means Shared Credentials parsing bool m_useProfilePrefix = false; enum State { START = 0, PROFILE_FOUND, SSO_SESSION_FOUND, UNKNOWN_SECTION_FOUND, FAILURE }; /** * Helper function to parse a single word (aka section identifier) containing allowed characters from a line and a pos * i.e. line="[ profile default ]";identifierBegin=10 will return "default" * @param line, a section definition line being parsed * @param identifierBegin, an Aws::String position to start parsing * @param oErrorMsg, a reference to Aws::String to store error message in case of a parsing error. * @return Aws::String, e.g. "default" */ Aws::String ParseIdentifier(const Aws::String& line, Aws::String::size_type identifierBegin, Aws::String& oErrorMsg) { // pos at the beginning of section Identifier (or sso_session section keyword) Aws::String::size_type identifierLength = 0; Aws::String::size_type pos = identifierBegin; while(pos < line.length()) { if(std::find(IDENTIFIER_ALLOWED_CHARACTERS, IDENTIFIER_ALLOWED_CHARACTERS + IDENTIFIER_ALLOWED_CHARACTERS_SZ, line[pos]) != IDENTIFIER_ALLOWED_CHARACTERS + IDENTIFIER_ALLOWED_CHARACTERS_SZ) { identifierLength++; pos++; } else { break; } } const Aws::String SECTION_END_CHARS_TO_SKIP = Aws::String(WHITESPACE_CHARACTERS) + RIGHT_BRACKET; if(identifierLength == 0) { oErrorMsg = "identifier is missing"; return ""; } if(pos >= line.size() || SECTION_END_CHARS_TO_SKIP.find(line[pos]) == Aws::String::npos) { oErrorMsg = "a blank space character or closing bracket is expected after Identifier"; return ""; } Aws::String sectionIdentifier = line.substr(identifierBegin, identifierLength); return sectionIdentifier; } /** * A helper function to parse config section declaration line * @param line, an input line, e.g. "[profile default]" * @param ioSectionName, a return argument representing parsed section Identifier, e.g. "default" * @param ioState, a return argument representing parser state, e.g. PROFILE_FOUND */ void ParseSectionDeclaration(const Aws::String& line, Aws::String& ioSectionName, State& ioState) { do { // goto in a form of "do { break; } while(0);" Aws::String::size_type pos = 0; pos = line.find_first_not_of(WHITESPACE_CHARACTERS, pos); if(pos != Aws::String::npos && LEFT_BRACKET != line[pos]) { AWS_LOGSTREAM_ERROR(PARSER_TAG, "First non-blank space character of a section definition must be [, line:" << line); break; } pos++; pos = line.find_first_not_of(WHITESPACE_CHARACTERS, pos); if(pos == Aws::String::npos || pos >= line.size()) { AWS_LOGSTREAM_ERROR(PARSER_TAG, "Unknown section found in the aws config file: " << line); break; } bool defaultProfileOrSsoSectionRequired = false; if (m_useProfilePrefix) { // in configuration files, the profile name must start with profile. (eg. [profile profile-name]), // except where the profile name is default. When the profile name is default it may start with profile static const size_t PROFILE_KEYWORD_LENGTH = 7; if(line.rfind(PROFILE_SECTION, pos + PROFILE_KEYWORD_LENGTH) != Aws::String::npos) { // skipping required (optional for default) profile keyword pos += PROFILE_KEYWORD_LENGTH; if(pos >= line.size() || std::find(WHITESPACE_CHARACTERS, WHITESPACE_CHARACTERS + WHITESPACE_CHARACTERS_SZ, line[pos]) == WHITESPACE_CHARACTERS + WHITESPACE_CHARACTERS_SZ) { AWS_LOGSTREAM_ERROR(PARSER_TAG, "Expected a blank space after \"profile\" keyword: " << line); break; } pos = line.find_first_not_of(WHITESPACE_CHARACTERS, pos); } else { defaultProfileOrSsoSectionRequired = true; } } Aws::String errorMsg; Aws::String sectionIdentifier = ParseIdentifier(line, pos, errorMsg); if (!errorMsg.empty()) { AWS_LOGSTREAM_ERROR(PARSER_TAG, "Failed to parse section identifier: " << errorMsg << " " << line); break; } pos += sectionIdentifier.length(); if(defaultProfileOrSsoSectionRequired) { if (sectionIdentifier != DEFAULT && sectionIdentifier != SSO_SESSION_SECTION) { AWS_LOGSTREAM_ERROR(PARSER_TAG, "In configuration files, the profile name must start with " "profile keyword (except default profile): " << line); break; } if (sectionIdentifier != SSO_SESSION_SECTION) { // profile found, still pending check for closing bracket ioState = PROFILE_FOUND; ioSectionName = sectionIdentifier; } } if(!m_useProfilePrefix || sectionIdentifier != SSO_SESSION_SECTION) { // profile found, still pending check for closing bracket ioState = PROFILE_FOUND; ioSectionName = sectionIdentifier; } if(m_useProfilePrefix && sectionIdentifier == SSO_SESSION_SECTION) { // "[sso_session..." found, continue parsing for sso_session identifier pos = line.find_first_not_of(WHITESPACE_CHARACTERS, pos); if(pos == Aws::String::npos) { AWS_LOGSTREAM_ERROR(PARSER_TAG, "Expected a blank space after \"sso_session\" keyword: " << line); break; } sectionIdentifier = ParseIdentifier(line, pos, errorMsg); if (!errorMsg.empty()) { AWS_LOGSTREAM_ERROR(PARSER_TAG, "Failed to parse section identifier: " << errorMsg << " " << line); break; } pos += sectionIdentifier.length(); // sso_session found, still pending check for closing bracket ioState = SSO_SESSION_FOUND; ioSectionName = sectionIdentifier; } pos = line.find_first_not_of(WHITESPACE_CHARACTERS, pos); if(pos == Aws::String::npos) { AWS_LOGSTREAM_ERROR(PARSER_TAG, "Expected a non-blank space after section identifier (i.e. missing \"]\"): " << line); break; } if(line[pos] != RIGHT_BRACKET) { AWS_LOGSTREAM_ERROR(PARSER_TAG, "Missing closing bracket after Section Identifier " "(i.e. missing \"]\" or extra non-blank characters before \"]\"): " << line); break; } pos++; pos = line.find_first_not_of(WHITESPACE_CHARACTERS, pos); if(pos != Aws::String::npos && std::find(COMMENT_START, COMMENT_START + COMMENT_START_SZ, line[pos]) == COMMENT_START + COMMENT_START_SZ) { AWS_LOGSTREAM_ERROR(PARSER_TAG, "Found unexpected characters after closing bracket of Section Identifier " << line); break; } // the rest is a comment, and we don't care about it. if ((ioState != SSO_SESSION_FOUND && ioState != PROFILE_FOUND) || ioSectionName.empty()) { AWS_LOGSTREAM_FATAL(PARSER_TAG, "Unexpected parser state after attempting to parse section " << line); break; } return; } while(0); // end of goto in a form of "do { break; } while(0);" ioSectionName.erase(); ioState = UNKNOWN_SECTION_FOUND; return; } /** * A helper function to store currently being parsed section along with its properties * (i.e. [profile default] and its key1=val1 under). * @param currentState, a current parser State, e.g. PROFILE_FOUND * @param currentSectionName, a current section identifier, e.g. "default" * @param currentKeyValues, a map of parsed key-value properties of a section definition being recorded */ void FlushSection(const State currentState, const Aws::String& currentSectionName, Aws::Map<Aws::String, Aws::String>& currentKeyValues) { if(START == currentState || currentSectionName.empty()) { return; //nothing to flush } if(PROFILE_FOUND == currentState) { Profile& profile = m_foundProfiles[currentSectionName]; for(const auto& keyVal : currentKeyValues) { auto setterFuncPtr = FindInStaticArray(PROFILE_PROPERTY_FUNCS, keyVal.first); if(setterFuncPtr) { AWS_LOGSTREAM_DEBUG(PARSER_TAG, "Found " << setterFuncPtr->PropertyKey << " " << keyVal.second); setterFuncPtr->Setter(profile, keyVal.second); } else { auto specialPropertyKey = std::find_if(PROFILE_KEY_SPECIAL_HANDLING, PROFILE_KEY_SPECIAL_HANDLING + PROFILE_KEY_SPECIAL_HANDLING_SZ, [&keyVal](const char* entry) { return !!entry && keyVal.first == entry; }); if (specialPropertyKey && specialPropertyKey != PROFILE_KEY_SPECIAL_HANDLING + PROFILE_KEY_SPECIAL_HANDLING_SZ) { AWS_LOGSTREAM_INFO(PARSER_TAG, "Unknown property: " << keyVal.first << " in the profile: " << currentSectionName); } } } auto accessKeyIdIter = currentKeyValues.find(ACCESS_KEY_ID_KEY); Aws::String accessKey, secretKey, sessionToken; if (accessKeyIdIter != currentKeyValues.end()) { accessKey = accessKeyIdIter->second; AWS_LOGSTREAM_DEBUG(PARSER_TAG, "found access key " << accessKey); auto secretAccessKeyIter = currentKeyValues.find(SECRET_KEY_KEY); auto sessionTokenIter = currentKeyValues.find(SESSION_TOKEN_KEY); if (secretAccessKeyIter != currentKeyValues.end()) { secretKey = secretAccessKeyIter->second; } else { AWS_LOGSTREAM_ERROR(PARSER_TAG, "No secret access key found even though an access key was specified. This will cause all signed AWS calls to fail."); } if (sessionTokenIter != currentKeyValues.end()) { sessionToken = sessionTokenIter->second; } profile.SetCredentials(Aws::Auth::AWSCredentials(accessKey, secretKey, sessionToken)); } if (!profile.GetSsoStartUrl().empty() || !profile.GetSsoRegion().empty() || !profile.GetSsoAccountId().empty() || !profile.GetSsoRoleName().empty()) { // If there is no sso session, all fields are required. If an SSO session is present, // then only account id and sso role name are required. auto hasSession = currentKeyValues.find(SSO_SESSION_KEY) != currentKeyValues.end(); auto hasInvalidProfileWithoutSession = !hasSession && (profile.GetSsoStartUrl().empty() || profile.GetSsoRegion().empty() || profile.GetSsoAccountId().empty() || profile.GetSsoRoleName().empty()); auto hasInvalidProfileWithSession = hasSession && (profile.GetSsoAccountId().empty() || profile.GetSsoRoleName().empty()); if (hasInvalidProfileWithoutSession || hasInvalidProfileWithSession) { profile.SetSsoStartUrl(""); profile.SetSsoRegion(""); profile.SetSsoAccountId(""); profile.SetSsoRoleName(""); AWS_LOGSTREAM_ERROR(PARSER_TAG, "invalid SSO configuration for aws profile " << currentSectionName); } } profile.SetName(currentSectionName); profile.SetAllKeyValPairs(std::move(currentKeyValues)); } else if (SSO_SESSION_FOUND == currentState) { Profile::SsoSession& ssoSession = m_foundSsoSessions[currentSectionName]; for(const auto& keyVal : currentKeyValues) { auto setterFuncPtr = FindInStaticArray(SSO_SESSION_PROPERTY_FUNCS, keyVal.first); if(setterFuncPtr) { AWS_LOGSTREAM_DEBUG(PARSER_TAG, "Found sso-session property " << setterFuncPtr->PropertyKey << " " << keyVal.second); setterFuncPtr->Setter(ssoSession, keyVal.second); } else { AWS_LOGSTREAM_INFO(PARSER_TAG, "Unknown property: " << keyVal.first << " in the sso-session: " << currentSectionName); } } ssoSession.SetName(currentSectionName); ssoSession.SetAllKeyValPairs(std::move(currentKeyValues)); } else { AWS_LOGSTREAM_FATAL(PARSER_TAG, "Unknown parser error: unexpected state " << currentState); } } Aws::Map<String, Profile> m_foundProfiles; Aws::Map<String, Profile::SsoSession> m_foundSsoSessions; }; static const char* const CONFIG_FILE_LOADER = "Aws::Config::AWSConfigFileProfileConfigLoader"; AWSConfigFileProfileConfigLoader::AWSConfigFileProfileConfigLoader(const Aws::String& fileName, bool useProfilePrefix) : m_fileName(fileName), m_useProfilePrefix(useProfilePrefix) { AWS_LOGSTREAM_INFO(CONFIG_FILE_LOADER, "Initializing config loader against fileName " << fileName << " and using profilePrefix = " << useProfilePrefix); } bool AWSConfigFileProfileConfigLoader::LoadInternal() { m_profiles.clear(); Aws::IFStream inputFile(m_fileName.c_str()); if(inputFile) { ConfigFileProfileFSM parser(m_useProfilePrefix); parser.ParseStream(inputFile); m_profiles = parser.GetProfiles(); return m_profiles.size() > 0; } AWS_LOGSTREAM_INFO(CONFIG_FILE_LOADER, "Unable to open config file " << m_fileName << " for reading."); return false; } bool AWSConfigFileProfileConfigLoader::PersistInternal(const Aws::Map<Aws::String, Profile>& profiles) { Aws::OFStream outputFile(m_fileName.c_str(), std::ios_base::out | std::ios_base::trunc); if(outputFile) { Aws::UnorderedMap<Aws::String, std::reference_wrapper<const Profile::SsoSession>> ssoSessionsToDump; for(const auto& profile : profiles) { Aws::String prefix = m_useProfilePrefix ? PROFILE_SECTION : ""; AWS_LOGSTREAM_DEBUG(CONFIG_FILE_LOADER, "Writing profile " << profile.first << " to disk."); outputFile << LEFT_BRACKET << prefix << " " << profile.second.GetName() << RIGHT_BRACKET << std::endl; const Aws::Auth::AWSCredentials& credentials = profile.second.GetCredentials(); if (!credentials.GetAWSAccessKeyId().empty()) { outputFile << ACCESS_KEY_ID_KEY << EQ << credentials.GetAWSAccessKeyId() << std::endl; } if (!credentials.GetAWSSecretKey().empty()) { outputFile << SECRET_KEY_KEY << EQ << credentials.GetAWSSecretKey() << std::endl; } if(!credentials.GetSessionToken().empty()) { outputFile << SESSION_TOKEN_KEY << EQ << credentials.GetSessionToken() << std::endl; } // credentials.GetExpiration().Millis() <- is not present in a config. for(const auto& profilePropertyPair : PROFILE_PROPERTY_FUNCS) { const auto& profilePropertyValue = profilePropertyPair.Getter(profile.second); if(!profilePropertyValue.empty()) { outputFile << profilePropertyPair.PropertyKey << EQ << profilePropertyValue << std::endl; } } if(profile.second.IsSsoSessionSet()) { const auto& ssoSession = profile.second.GetSsoSession(); const auto alreadyScheduledForDumpIt = ssoSessionsToDump.find(ssoSession.GetName()); if (alreadyScheduledForDumpIt != ssoSessionsToDump.end() && alreadyScheduledForDumpIt->second.get() != ssoSession) { AWS_LOGSTREAM_WARN(CONFIG_FILE_LOADER, "2 or more profiles reference 'sso-session' section " "with the same name but different properties: " << ssoSession.GetName()); } else { ssoSessionsToDump.insert({ssoSession.GetName(), std::cref(ssoSession)}); } outputFile << SSO_SESSION_KEY << EQ << ssoSession.GetName() << std::endl; } outputFile << std::endl; } for(const auto& ssoSessionPair : ssoSessionsToDump) { AWS_LOGSTREAM_DEBUG(CONFIG_FILE_LOADER, "Writing sso-session " << ssoSessionPair.first << " to disk."); const Profile::SsoSession& ssoSession = ssoSessionPair.second.get(); outputFile << LEFT_BRACKET << SSO_SESSION_SECTION << " " << ssoSession.GetName() << RIGHT_BRACKET << std::endl; for(const auto& ssoSessionPropertyPair : SSO_SESSION_PROPERTY_FUNCS) { const auto& profilePropertyValue = ssoSessionPropertyPair.Getter(ssoSession); if(!profilePropertyValue.empty()) { outputFile << ssoSessionPropertyPair.PropertyKey << EQ << profilePropertyValue << std::endl; } } outputFile << std::endl; } AWS_LOGSTREAM_INFO(CONFIG_FILE_LOADER, "Profiles written to config file " << m_fileName); return true; } AWS_LOGSTREAM_WARN(CONFIG_FILE_LOADER, "Unable to open config file " << m_fileName << " for writing."); return false; } } // Config namespace } // Aws namespace