diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index e319e4d33..e2ef3506f 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added support for more `AZURE_TOKEN_CREDENTIALS` environment variable values to specify a single credential type to use in `DefaultAzureCredential`. In addition to `dev` and `prod`, possible values now include `EnvironmentCredential`, `WorkloadIdentityCredential`, `ManagedIdentityCredential`, and `AzureCliCredential` - each for the corresponding credential type. + ### 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 1b859893d..b167e9a76 100644 --- a/sdk/identity/azure-identity/src/default_azure_credential.cpp +++ b/sdk/identity/azure-identity/src/default_azure_credential.cpp @@ -13,6 +13,10 @@ #include #include +#include +#include +#include + using namespace Azure::Identity; using namespace Azure::Core::Credentials; @@ -45,39 +49,139 @@ DefaultAzureCredential::DefaultAzureCredential( // Creating credentials in order to ensure the order of log messages. ChainedTokenCredential::Sources credentialChain; { - credentialChain.emplace_back(std::make_shared(options)); - credentialChain.emplace_back(std::make_shared(options)); - credentialChain.emplace_back(std::make_shared(options)); + struct CredentialInfo + { + bool IsProd; + std::string CredentialName; + std::function( + const Core::Credentials::TokenCredentialOptions&)> + Create; + }; + + static const std::array credentials = { + CredentialInfo{ + true, + "EnvironmentCredential", + [](auto options) { return std::make_shared(options); }}, + CredentialInfo{ + true, + "WorkloadIdentityCredential", + [](auto options) { return std::make_shared(options); }}, + CredentialInfo{ + true, + "ManagedIdentityCredential", + [](auto options) { return std::make_shared(options); }}, + CredentialInfo{ + false, + "AzureCliCredential", + [](auto options) { return 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) + bool specificCred = false; + if (!trimmedEnvVarValue.empty()) { - IdentityLog::Write(IdentityLog::Level::Verbose, logMsg); + for (const auto& cred : credentials) + { + if (StringExtensions::LocaleInvariantCaseInsensitiveEqual( + trimmedEnvVarValue, cred.CredentialName)) + { + specificCred = true; + IdentityLog::Write( + IdentityLog::Level::Verbose, + GetCredentialName() + ": '" + envVarName + "' environment variable is set to '" + + envVarValue + + "', therefore credential chain will only contain single credential: " + + cred.CredentialName + '.'); + credentialChain.emplace_back(cred.Create(options)); + break; + } + } } - else if ( - trimmedEnvVarValue.empty() - || StringExtensions::LocaleInvariantCaseInsensitiveEqual(trimmedEnvVarValue, "dev")) + + if (!specificCred) { - IdentityLog::Write(IdentityLog::Level::Verbose, logMsg); - credentialChain.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."); + for (const auto& cred : credentials) + { + if (cred.IsProd) + { + credentialChain.emplace_back(cred.Create(options)); + } + } + + const auto isProd + = StringExtensions::LocaleInvariantCaseInsensitiveEqual(trimmedEnvVarValue, "prod"); + + static const auto devCredCount = std::count_if( + credentials.begin(), credentials.end(), [](auto& cred) { return !cred.IsProd; }); + + std::string devCredNames; + { + std::remove_const::type devCredNum = 0; + for (const auto& cred : credentials) + { + if (!cred.IsProd) + { + if (!devCredNames.empty()) + { + if (devCredCount == 2) + { + devCredNames += " and "; + } + else + { + ++devCredNum; + devCredNames += (devCredNum < devCredCount) ? ", " : ", and "; + } + } + + devCredNames += cred.CredentialName; + } + } + } + + const auto logMsg = GetCredentialName() + ": '" + envVarName + "' environment variable is " + + (envVarValue.empty() ? "not set" : ("set to '" + envVarValue + "'")) + + ((devCredCount > 0) + ? (", therefore " + devCredNames + " 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); + for (const auto& cred : credentials) + { + if (!cred.IsProd) + { + credentialChain.emplace_back(cred.Create(options)); + } + } + } + else + { + std::string allowedCredNames; + for (std::size_t i = 0; i < credentials.size(); ++i) + { + allowedCredNames += ((i < credentials.size() - 1) ? ", '" : ", and '") + + credentials[i].CredentialName + '\''; + } + + throw AuthenticationException( + GetCredentialName() + ": Invalid value '" + envVarValue + "' for the '" + envVarName + + "' environment variable. Allowed values are 'dev', 'prod'" + allowedCredNames + + " (case insensitive). " + "It is also valid to not have the environment variable defined."); + } } } 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 0f6cad58c..8e1cbd067 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 @@ -354,3 +354,171 @@ TEST_P(LogMessages, ) Logger::SetListener(nullptr); } } + +struct SpecificCredentialInfo +{ + std::string CredentialName; + std::string EnvVarValue; + size_t ExpectedLogMsgCount; +}; + +class LogMessagesForSpecificCredential : public ::testing::TestWithParam { +}; + +INSTANTIATE_TEST_SUITE_P( + DefaultAzureCredential, + LogMessagesForSpecificCredential, + ::testing::Values( + SpecificCredentialInfo{"EnvironmentCredential", "eNvIrOnMeNtCrEdEnTiAl", 5}, + SpecificCredentialInfo{"WorkloadIdentityCredential", "workloadidentitycredential", 4}, + SpecificCredentialInfo{"ManagedIdentityCredential", "MANAGEDIDENTITYCREDENTIAL", 8}, + SpecificCredentialInfo{"AzureCliCredential", " AzureCLICredential ", 4})); + +TEST_P(LogMessagesForSpecificCredential, ) +{ + const auto specificCredentialInfo = GetParam(); + + 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)); }); + + try + { + 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", specificCredentialInfo.EnvVarValue}, + }); + + static_cast(std::make_unique()); + + EXPECT_EQ(log.size(), specificCredentialInfo.ExpectedLogMsgCount); + LogMsgVec::size_type i = 0; + + 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."); + + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Verbose); + EXPECT_EQ( + log.at(i).second, + "Identity: DefaultAzureCredential: " + "'AZURE_TOKEN_CREDENTIALS' environment variable is set to '" + + specificCredentialInfo.EnvVarValue + + "', therefore credential chain will only contain single credential: " + + specificCredentialInfo.CredentialName + '.'); + + if (specificCredentialInfo.CredentialName == "EnvironmentCredential") + { + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Informational); + EXPECT_EQ( + log.at(i).second, + "Identity: EnvironmentCredential gets created with ClientSecretCredential."); + + ++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."); + } + else if (specificCredentialInfo.CredentialName == "WorkloadIdentityCredential") + { + ++i; + EXPECT_EQ(log.at(i).first, Logger::Level::Informational); + EXPECT_EQ(log.at(i).second, "Identity: WorkloadIdentityCredential was created successfully."); + } + else if (specificCredentialInfo.CredentialName == "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."); + + ++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."); + + ++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."); + + ++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."); + + ++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."); + } + else if (specificCredentialInfo.CredentialName == "AzureCliCredential") + { + ++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."); + } + + ++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: " + + specificCredentialInfo.CredentialName + '.')); + + ++i; + EXPECT_EQ(i, log.size()); + + log.clear(); + } + catch (...) + { + Logger::SetListener(nullptr); + throw; + } + + Logger::SetListener(nullptr); +}