Identity: 'AZURE_TOKEN_CREDENTIALS' env var support for specific credential names (#6634)
* Identity: 'AZURE_TOKEN_CREDENTIALS' env var support for specific credential names * No need to use index-based loop at that place * Clang-format * Require 'Credential' suffix for env var values * Update sdk/identity/azure-identity/CHANGELOG.md Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> --------- Co-authored-by: Anton Kolesnyk <antkmsft@users.noreply.github.com> Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com>
This commit is contained in:
parent
794de09e07
commit
d0fee3a090
@ -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
|
||||
|
||||
@ -13,6 +13,10 @@
|
||||
#include <azure/core/internal/environment.hpp>
|
||||
#include <azure/core/internal/strings.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <type_traits>
|
||||
|
||||
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<EnvironmentCredential>(options));
|
||||
credentialChain.emplace_back(std::make_shared<WorkloadIdentityCredential>(options));
|
||||
credentialChain.emplace_back(std::make_shared<ManagedIdentityCredential>(options));
|
||||
struct CredentialInfo
|
||||
{
|
||||
bool IsProd;
|
||||
std::string CredentialName;
|
||||
std::function<std::shared_ptr<Core::Credentials::TokenCredential>(
|
||||
const Core::Credentials::TokenCredentialOptions&)>
|
||||
Create;
|
||||
};
|
||||
|
||||
static const std::array<CredentialInfo, 4> credentials = {
|
||||
CredentialInfo{
|
||||
true,
|
||||
"EnvironmentCredential",
|
||||
[](auto options) { return std::make_shared<EnvironmentCredential>(options); }},
|
||||
CredentialInfo{
|
||||
true,
|
||||
"WorkloadIdentityCredential",
|
||||
[](auto options) { return std::make_shared<WorkloadIdentityCredential>(options); }},
|
||||
CredentialInfo{
|
||||
true,
|
||||
"ManagedIdentityCredential",
|
||||
[](auto options) { return std::make_shared<ManagedIdentityCredential>(options); }},
|
||||
CredentialInfo{
|
||||
false,
|
||||
"AzureCliCredential",
|
||||
[](auto options) { return std::make_shared<AzureCliCredential>(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<AzureCliCredential>(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<decltype(devCredCount)>::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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<SpecificCredentialInfo> {
|
||||
};
|
||||
|
||||
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<std::pair<Logger::Level, std::string>>;
|
||||
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<void>(std::make_unique<DefaultAzureCredential>());
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user