Implement AZURE_TOKEN_CREDENTIALS (#6593)
* Implement AZURE_TOKEN_CREDENTIALS * Clang-format * Update sdk/identity/azure-identity/src/default_azure_credential.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update sdk/identity/azure-identity/src/default_azure_credential.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update sdk/identity/azure-identity/CHANGELOG.md Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * TrimString() * Test against contains() or startsWith() being used * Update sdk/identity/azure-identity/src/default_azure_credential.cpp Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com> * TrimString() => StringExtensions::Trim() --------- Co-authored-by: Anton Kolesnyk <antkmsft@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Scott Addie <10702007+scottaddie@users.noreply.github.com>
This commit is contained in:
parent
2d5514fa8a
commit
47c738bba2
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -10,10 +10,15 @@
|
||||
#include "private/chained_token_credential_impl.hpp"
|
||||
#include "private/identity_log.hpp"
|
||||
|
||||
#include <azure/core/internal/environment.hpp>
|
||||
#include <azure/core/internal/strings.hpp>
|
||||
|
||||
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<EnvironmentCredential>(options);
|
||||
auto const wiCred = std::make_shared<WorkloadIdentityCredential>(options);
|
||||
auto const azCliCred = std::make_shared<AzureCliCredential>(options);
|
||||
auto const managedIdentityCred = std::make_shared<ManagedIdentityCredential>(options);
|
||||
ChainedTokenCredential::Sources miSources;
|
||||
{
|
||||
miSources.emplace_back(std::make_shared<EnvironmentCredential>(options));
|
||||
miSources.emplace_back(std::make_shared<WorkloadIdentityCredential>(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<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.");
|
||||
}
|
||||
|
||||
miSources.emplace_back(std::make_shared<ManagedIdentityCredential>(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;
|
||||
|
||||
@ -138,131 +138,219 @@ TEST(DefaultAzureCredential, CachingCredential)
|
||||
EXPECT_TRUE(c2->WasInvoked);
|
||||
}
|
||||
|
||||
TEST(DefaultAzureCredential, LogMessages)
|
||||
class LogMessages : public ::testing::TestWithParam<std::string> {
|
||||
};
|
||||
|
||||
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<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)); });
|
||||
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<void>(std::make_unique<DefaultAzureCredential>()), 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<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)); });
|
||||
|
||||
auto credential = std::make_unique<DefaultAzureCredential>(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<DefaultAzureCredential>(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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user