diff --git a/sdk/core/azure-core/inc/azure/core/internal/strings.hpp b/sdk/core/azure-core/inc/azure/core/internal/strings.hpp index 984b74982..4582d97e8 100644 --- a/sdk/core/azure-core/inc/azure/core/internal/strings.hpp +++ b/sdk/core/azure-core/inc/azure/core/internal/strings.hpp @@ -119,6 +119,13 @@ namespace Azure { namespace Core { namespace _internal { return result; } + + static std::string Trim(std::string s) + { + s.erase(s.begin(), std::find_if_not(s.begin(), s.end(), IsSpace)); + s.erase(std::find_if_not(s.rbegin(), s.rend(), IsSpace).base(), s.end()); + return s; + } }; }}} // namespace Azure::Core::_internal diff --git a/sdk/core/azure-core/src/http/user_agent.cpp b/sdk/core/azure-core/src/http/user_agent.cpp index 8967107d9..bfb03537c 100644 --- a/sdk/core/azure-core/src/http/user_agent.cpp +++ b/sdk/core/azure-core/src/http/user_agent.cpp @@ -128,20 +128,6 @@ std::string GetOSVersion() return osVersionInfo.str(); } - -std::string TrimString(std::string s) -{ - s.erase( - s.begin(), - std::find_if_not(s.begin(), s.end(), Azure::Core::_internal::StringExtensions::IsSpace)); - - s.erase( - std::find_if_not(s.rbegin(), s.rend(), Azure::Core::_internal::StringExtensions::IsSpace) - .base(), - s.end()); - - return s; -} } // namespace namespace Azure { namespace Core { namespace Http { namespace _internal { @@ -152,12 +138,14 @@ namespace Azure { namespace Core { namespace Http { namespace _internal { std::string const& applicationId, long cplusplusValue) { + using Azure::Core::_internal::StringExtensions; + // Spec: https://azure.github.io/azure-sdk/general_azurecore.html#telemetry-policy std::ostringstream telemetryId; if (!applicationId.empty()) { - telemetryId << TrimString(applicationId).substr(0, 24) << " "; + telemetryId << StringExtensions::Trim(applicationId).substr(0, 24) << " "; } static std::string const osVer = GetOSVersion(); diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 149bccc54..2ed6e31a6 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added support for the `AZURE_TOKEN_CREDENTIALS` environment variable to `DefaultAzureCredential`, which allows for choosing between 'deployed service' and 'developer tool' credentials. Valid values are 'dev' for developer tools and 'prod' for deployed service. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/identity/azure-identity/src/default_azure_credential.cpp b/sdk/identity/azure-identity/src/default_azure_credential.cpp index 9062fa472..d5c7023ed 100644 --- a/sdk/identity/azure-identity/src/default_azure_credential.cpp +++ b/sdk/identity/azure-identity/src/default_azure_credential.cpp @@ -10,10 +10,15 @@ #include "private/chained_token_credential_impl.hpp" #include "private/identity_log.hpp" +#include +#include + using namespace Azure::Identity; using namespace Azure::Core::Credentials; using Azure::Core::Context; +using Azure::Core::_internal::Environment; +using Azure::Core::_internal::StringExtensions; using Azure::Core::Diagnostics::Logger; using Azure::Identity::_detail::IdentityLog; @@ -38,17 +43,49 @@ DefaultAzureCredential::DefaultAzureCredential( "is the better fit for the application."); // Creating credentials in order to ensure the order of log messages. - auto const envCred = std::make_shared(options); - auto const wiCred = std::make_shared(options); - auto const azCliCred = std::make_shared(options); - auto const managedIdentityCred = std::make_shared(options); + ChainedTokenCredential::Sources miSources; + { + miSources.emplace_back(std::make_shared(options)); + miSources.emplace_back(std::make_shared(options)); + + constexpr auto envVarName = "AZURE_TOKEN_CREDENTIALS"; + const auto envVarValue = Environment::GetVariable(envVarName); + const auto trimmedEnvVarValue = StringExtensions::Trim(envVarValue); + + const auto isProd + = StringExtensions::LocaleInvariantCaseInsensitiveEqual(trimmedEnvVarValue, "prod"); + + const auto logMsg = GetCredentialName() + ": '" + envVarName + "' environment variable is " + + (envVarValue.empty() ? "not set" : ("set to '" + envVarValue + "'")) + + ", therefore AzureCliCredential will " + (isProd ? "NOT " : "") + + "be included in the credential chain."; + + if (isProd) + { + IdentityLog::Write(IdentityLog::Level::Verbose, logMsg); + } + else if ( + trimmedEnvVarValue.empty() + || StringExtensions::LocaleInvariantCaseInsensitiveEqual(trimmedEnvVarValue, "dev")) + { + IdentityLog::Write(IdentityLog::Level::Verbose, logMsg); + miSources.emplace_back(std::make_shared(options)); + } + else + { + throw AuthenticationException( + GetCredentialName() + ": Invalid value '" + envVarValue + "' for the '" + envVarName + + "' environment variable. Allowed values are 'dev' and 'prod' (case insensitive). " + "It is also valid to not have the environment variable defined."); + } + + miSources.emplace_back(std::make_shared(options)); + } // DefaultAzureCredential caches the selected credential, so that it can be reused on subsequent // calls. m_impl = std::make_unique<_detail::ChainedTokenCredentialImpl>( - GetCredentialName(), - ChainedTokenCredential::Sources{envCred, wiCred, azCliCred, managedIdentityCred}, - true); + GetCredentialName(), std::move(miSources), true); } DefaultAzureCredential::~DefaultAzureCredential() = default; diff --git a/sdk/identity/azure-identity/test/ut/default_azure_credential_test.cpp b/sdk/identity/azure-identity/test/ut/default_azure_credential_test.cpp index 5d40e5d9f..6e90c4ce3 100644 --- a/sdk/identity/azure-identity/test/ut/default_azure_credential_test.cpp +++ b/sdk/identity/azure-identity/test/ut/default_azure_credential_test.cpp @@ -138,131 +138,219 @@ TEST(DefaultAzureCredential, CachingCredential) EXPECT_TRUE(c2->WasInvoked); } -TEST(DefaultAzureCredential, LogMessages) +class LogMessages : public ::testing::TestWithParam { +}; + +INSTANTIATE_TEST_SUITE_P( + DefaultAzureCredential, + LogMessages, + ::testing::Values( + "", + " ", + "dev", + "DeV", + "dEv ", + " DEV ", + "prod", + "pRoD", + " PrOd", + "d ev", + "production")); + +TEST_P(LogMessages, ) { - using LogMsgVec = std::vector>; - LogMsgVec log; - Logger::SetLevel(Logger::Level::Verbose); - Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); + const auto azTokenCredsEnvVarValue = GetParam(); + if (azTokenCredsEnvVarValue == "d ev" || azTokenCredsEnvVarValue == "production") + { + CredentialTestHelper::EnvironmentOverride const env( + {{"AZURE_TOKEN_CREDENTIALS", azTokenCredsEnvVarValue}}); - CredentialTestHelper::SimulateTokenRequest( - [&](auto transport) { - TokenCredentialOptions options; - options.Transport.Transport = transport; + EXPECT_THROW( + static_cast(std::make_unique()), AuthenticationException); + } + else + { + const auto isDev = azTokenCredsEnvVarValue == "dev" || azTokenCredsEnvVarValue == "DeV" + || azTokenCredsEnvVarValue == "dEv " || azTokenCredsEnvVarValue == " DEV " + || azTokenCredsEnvVarValue == "" || azTokenCredsEnvVarValue == " "; - CredentialTestHelper::EnvironmentOverride const env({ - {"AZURE_TENANT_ID", "01234567-89ab-cdef-fedc-ba8976543210"}, - {"AZURE_CLIENT_ID", "fedcba98-7654-3210-0123-456789abcdef"}, - {"AZURE_CLIENT_SECRET", "CLIENTSECRET"}, - {"AZURE_AUTHORITY_HOST", "https://microsoft.com/"}, - {"AZURE_FEDERATED_TOKEN_FILE", "azure-identity-test.pem"}, - {"AZURE_USERNAME", ""}, - {"AZURE_PASSWORD", ""}, - {"AZURE_CLIENT_CERTIFICATE_PATH", ""}, - {"MSI_ENDPOINT", ""}, - {"MSI_SECRET", ""}, - {"IDENTITY_ENDPOINT", ""}, - {"IMDS_ENDPOINT", ""}, - {"IDENTITY_HEADER", ""}, - {"IDENTITY_SERVER_THUMBPRINT", ""}, - }); + using LogMsgVec = std::vector>; + LogMsgVec log; + Logger::SetLevel(Logger::Level::Verbose); + Logger::SetListener([&](auto lvl, auto msg) { log.push_back(std::make_pair(lvl, msg)); }); - auto credential = std::make_unique(options); + try + { + CredentialTestHelper::SimulateTokenRequest( + [&](auto transport) { + TokenCredentialOptions options; + options.Transport.Transport = transport; - EXPECT_EQ(log.size(), LogMsgVec::size_type(11)); + CredentialTestHelper::EnvironmentOverride const env({ + {"AZURE_TENANT_ID", "01234567-89ab-cdef-fedc-ba8976543210"}, + {"AZURE_CLIENT_ID", "fedcba98-7654-3210-0123-456789abcdef"}, + {"AZURE_CLIENT_SECRET", "CLIENTSECRET"}, + {"AZURE_AUTHORITY_HOST", "https://microsoft.com/"}, + {"AZURE_FEDERATED_TOKEN_FILE", "azure-identity-test.pem"}, + {"AZURE_USERNAME", ""}, + {"AZURE_PASSWORD", ""}, + {"AZURE_CLIENT_CERTIFICATE_PATH", ""}, + {"MSI_ENDPOINT", ""}, + {"MSI_SECRET", ""}, + {"IDENTITY_ENDPOINT", ""}, + {"IMDS_ENDPOINT", ""}, + {"IDENTITY_HEADER", ""}, + {"IDENTITY_SERVER_THUMBPRINT", ""}, + {"AZURE_TOKEN_CREDENTIALS", azTokenCredsEnvVarValue}, + }); - EXPECT_EQ(log[0].first, Logger::Level::Verbose); - EXPECT_EQ( - log[0].second, - "Identity: Creating DefaultAzureCredential which combines " - "multiple parameterless credentials into a single one." - "\nDefaultAzureCredential is only recommended for the early stages of development, " - "and not for usage in production environment." - "\nOnce the developer focuses on the Credentials and Authentication aspects of their " - "application, DefaultAzureCredential needs to be replaced with the credential that " - "is the better fit for the application."); + auto credential = std::make_unique(options); - EXPECT_EQ(log[1].first, Logger::Level::Informational); - EXPECT_EQ( - log[1].second, - "Identity: EnvironmentCredential gets created with ClientSecretCredential."); + EXPECT_EQ(log.size(), LogMsgVec::size_type(isDev ? 12 : 11)); + LogMsgVec::size_type i = 0; - EXPECT_EQ(log[2].first, Logger::Level::Verbose); - EXPECT_EQ( - log[2].second, - "Identity: EnvironmentCredential: 'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', " - "'AZURE_CLIENT_SECRET', and 'AZURE_AUTHORITY_HOST' environment variables are set, so " - "ClientSecretCredential with corresponding tenantId, clientId, clientSecret, and " - "authorityHost gets created."); + EXPECT_EQ(log.at(i).first, Logger::Level::Verbose); + EXPECT_EQ( + log.at(i).second, + "Identity: Creating DefaultAzureCredential which combines " + "multiple parameterless credentials into a single one." + "\nDefaultAzureCredential is only recommended for the early stages of development, " + "and not for usage in production environment." + "\nOnce the developer focuses on the Credentials " + "and Authentication aspects of their application, " + "DefaultAzureCredential needs to be replaced with the credential that " + "is the better fit for the application."); - EXPECT_EQ(log[3].first, Logger::Level::Informational); - EXPECT_EQ(log[3].second, "Identity: WorkloadIdentityCredential was created successfully."); + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Informational); + EXPECT_EQ( + log.at(i).second, + "Identity: EnvironmentCredential gets created with ClientSecretCredential."); - EXPECT_EQ(log[5].first, Logger::Level::Verbose); - EXPECT_EQ( - log[5].second, - "Identity: ManagedIdentityCredential: Environment is not set up for the credential " - "to be created with App Service 2019 source."); + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Verbose); + EXPECT_EQ( + log.at(i).second, + "Identity: EnvironmentCredential: 'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', " + "'AZURE_CLIENT_SECRET', and 'AZURE_AUTHORITY_HOST' environment variables " + "are set, so ClientSecretCredential with corresponding " + "tenantId, clientId, clientSecret, and authorityHost gets created."); - EXPECT_EQ(log[6].first, Logger::Level::Verbose); - EXPECT_EQ( - log[6].second, - "Identity: ManagedIdentityCredential: Environment is not set up for the credential " - "to be created with App Service 2017 source."); + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Informational); + EXPECT_EQ( + log.at(i).second, "Identity: WorkloadIdentityCredential was created successfully."); - EXPECT_EQ(log[7].first, Logger::Level::Verbose); - EXPECT_EQ( - log[7].second, - "Identity: ManagedIdentityCredential: Environment is not set up for the credential " - "to be created with Cloud Shell source."); + { + const auto variableSetWording = azTokenCredsEnvVarValue.empty() + ? "not set" + : ("set to '" + azTokenCredsEnvVarValue + "'"); - EXPECT_EQ(log[8].first, Logger::Level::Verbose); - EXPECT_EQ( - log[8].second, - "Identity: ManagedIdentityCredential: Environment is not set up for the credential " - "to be created with Azure Arc source."); + const auto beIncludedWording = isDev ? "" : "NOT "; - EXPECT_EQ(log[9].first, Logger::Level::Informational); - EXPECT_EQ( - log[9].second, - "Identity: ManagedIdentityCredential will be created " - "with Azure Instance Metadata Service source." - "\nSuccessful creation does not guarantee further successful token retrieval."); + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Verbose); + EXPECT_EQ( + log.at(i).second, + "Identity: DefaultAzureCredential: " + "'AZURE_TOKEN_CREDENTIALS' environment variable is " + + variableSetWording + ", therefore AzureCliCredential will " + + beIncludedWording + "be included in the credential chain."); + } - EXPECT_EQ(log[4].first, Logger::Level::Informational); - EXPECT_EQ( - log[4].second, - "Identity: AzureCliCredential created." - "\nSuccessful creation does not guarantee further successful token retrieval."); + if (isDev) + { + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Informational); + EXPECT_EQ( + log.at(i).second, + "Identity: AzureCliCredential created." + "\nSuccessful creation does not guarantee further successful token retrieval."); + } - EXPECT_EQ(log[10].first, Logger::Level::Informational); - EXPECT_EQ( - log[10].second, - "Identity: DefaultAzureCredential: Created with the following credentials: " - "EnvironmentCredential, WorkloadIdentityCredential, AzureCliCredential, " - "ManagedIdentityCredential."); + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Verbose); + EXPECT_EQ( + log.at(i).second, + "Identity: ManagedIdentityCredential: Environment is not set up " + "for the credential to be created with App Service 2019 source."); - log.clear(); + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Verbose); + EXPECT_EQ( + log.at(i).second, + "Identity: ManagedIdentityCredential: Environment is not set up " + "for the credential to be created with App Service 2017 source."); - return credential; - }, - {{"https://azure.com/.default"}}, - {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}); + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Verbose); + EXPECT_EQ( + log.at(i).second, + "Identity: ManagedIdentityCredential: Environment is not set up " + "for the credential to be created with Cloud Shell source."); - EXPECT_EQ( - log.size(), - LogMsgVec::size_type(5)); // Request and retry policies will get their messages here as well. + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Verbose); + EXPECT_EQ( + log.at(i).second, + "Identity: ManagedIdentityCredential: Environment is not set up " + "for the credential to be created with Azure Arc source."); - EXPECT_EQ(log[3].first, Logger::Level::Informational); - EXPECT_EQ( - log[3].second, - "Identity: DefaultAzureCredential: Successfully got token from EnvironmentCredential. This " - "credential will be reused for subsequent calls."); + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Informational); + EXPECT_EQ( + log.at(i).second, + "Identity: ManagedIdentityCredential will be created " + "with Azure Instance Metadata Service source." + "\nSuccessful creation does not guarantee further successful token retrieval."); - EXPECT_EQ(log[4].first, Logger::Level::Verbose); - EXPECT_EQ( - log[4].second, - "Identity: DefaultAzureCredential: Saved this credential at index 0 for subsequent calls."); + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Informational); + EXPECT_EQ( + log.at(i).second, + std::string( + "Identity: DefaultAzureCredential: Created with the following credentials: " + "EnvironmentCredential, WorkloadIdentityCredential, ") + + (isDev ? "AzureCliCredential, " : "") + "ManagedIdentityCredential."); - Logger::SetListener(nullptr); + ++i; + EXPECT_EQ(i, log.size()); + + log.clear(); + + return credential; + }, + {{"https://azure.com/.default"}}, + {"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}"}); + + EXPECT_EQ( + log.size(), + LogMsgVec::size_type( + 5)); // Request and retry policies will get their messages here as well. + + LogMsgVec::size_type i = 3; + EXPECT_EQ(log.at(i).first, Logger::Level::Informational); + EXPECT_EQ( + log.at(i).second, + "Identity: DefaultAzureCredential: Successfully got token from EnvironmentCredential. " + "This credential will be reused for subsequent calls."); + + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Verbose); + EXPECT_EQ( + log.at(i).second, + "Identity: DefaultAzureCredential: " + "Saved this credential at index 0 for subsequent calls."); + + ++i; + EXPECT_EQ(i, log.size()); + } + catch (...) + { + Logger::SetListener(nullptr); + throw; + } + + Logger::SetListener(nullptr); + } }