Allow x-vss-e2eid response header to be logged in AzurePipelinesCredential for diagnostics. (#6001)
* Allow x-vss-e2eid response header to be logged in AzurePipelinesCredential for diagnostics. * Dont redact the x-msedge-ref header either. * Add the necessary response headers to the exception message. * Update cspell. * Update CL * Fix size_t comparison * Use std::array to get the size() method. * Add the <array> include directive to be explicit.
This commit is contained in:
parent
bc7feb0784
commit
641dcc84f1
1
.vscode/cspell.json
vendored
1
.vscode/cspell.json
vendored
@ -170,6 +170,7 @@
|
|||||||
"morten",
|
"morten",
|
||||||
"moxygen",
|
"moxygen",
|
||||||
"MSAL",
|
"MSAL",
|
||||||
|
"msedge",
|
||||||
"msft",
|
"msft",
|
||||||
"msiexec",
|
"msiexec",
|
||||||
"MSRC",
|
"MSRC",
|
||||||
|
|||||||
@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
### Other Changes
|
### Other Changes
|
||||||
|
|
||||||
|
- Allow certain response headers to be logged in `AzurePipelinesCredential` for diagnostics and include them in the exception message.
|
||||||
|
|
||||||
## 1.10.0-beta.1 (2024-09-17)
|
## 1.10.0-beta.1 (2024-09-17)
|
||||||
|
|
||||||
### Features Added
|
### Features Added
|
||||||
|
|||||||
@ -58,7 +58,7 @@ namespace Azure { namespace Identity {
|
|||||||
private:
|
private:
|
||||||
std::string m_serviceConnectionId;
|
std::string m_serviceConnectionId;
|
||||||
std::string m_systemAccessToken;
|
std::string m_systemAccessToken;
|
||||||
Azure::Core::Http::_internal::HttpPipeline m_httpPipeline;
|
std::unique_ptr<Azure::Core::Http::_internal::HttpPipeline> m_httpPipeline;
|
||||||
std::string m_oidcRequestUrl;
|
std::string m_oidcRequestUrl;
|
||||||
std::unique_ptr<_detail::ClientAssertionCredentialImpl> m_clientAssertionCredentialImpl;
|
std::unique_ptr<_detail::ClientAssertionCredentialImpl> m_clientAssertionCredentialImpl;
|
||||||
|
|
||||||
|
|||||||
@ -36,9 +36,20 @@ AzurePipelinesCredential::AzurePipelinesCredential(
|
|||||||
std::string systemAccessToken,
|
std::string systemAccessToken,
|
||||||
AzurePipelinesCredentialOptions const& options)
|
AzurePipelinesCredentialOptions const& options)
|
||||||
: TokenCredential("AzurePipelinesCredential"), m_serviceConnectionId(serviceConnectionId),
|
: TokenCredential("AzurePipelinesCredential"), m_serviceConnectionId(serviceConnectionId),
|
||||||
m_systemAccessToken(systemAccessToken),
|
m_systemAccessToken(systemAccessToken)
|
||||||
m_httpPipeline(HttpPipeline(options, "identity", PackageVersion::ToString(), {}, {}))
|
|
||||||
{
|
{
|
||||||
|
// Allow these headers to be logged since they are used for troubleshooting.
|
||||||
|
AzurePipelinesCredentialOptions optionsWithLoggableHeaders = options;
|
||||||
|
optionsWithLoggableHeaders.Log.AllowedHttpHeaders.insert("x-vss-e2eid");
|
||||||
|
optionsWithLoggableHeaders.Log.AllowedHttpHeaders.insert("x-msedge-ref");
|
||||||
|
|
||||||
|
m_httpPipeline = std::make_unique<HttpPipeline>(
|
||||||
|
optionsWithLoggableHeaders,
|
||||||
|
"identity",
|
||||||
|
PackageVersion::ToString(),
|
||||||
|
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>>{},
|
||||||
|
std::vector<std::unique_ptr<Azure::Core::Http::Policies::HttpPolicy>>{});
|
||||||
|
|
||||||
m_oidcRequestUrl = _detail::DefaultOptionValues::GetOidcRequestUrl();
|
m_oidcRequestUrl = _detail::DefaultOptionValues::GetOidcRequestUrl();
|
||||||
|
|
||||||
if (serviceConnectionId.empty())
|
if (serviceConnectionId.empty())
|
||||||
@ -121,8 +132,21 @@ std::string AzurePipelinesCredential::GetOidcTokenResponse(
|
|||||||
+ std::to_string(static_cast<std::underlying_type<decltype(statusCode)>::type>(statusCode))
|
+ std::to_string(static_cast<std::underlying_type<decltype(statusCode)>::type>(statusCode))
|
||||||
+ " (" + response->GetReasonPhrase()
|
+ " (" + response->GetReasonPhrase()
|
||||||
+ ") response from the OIDC endpoint. Check service connection ID and Pipeline "
|
+ ") response from the OIDC endpoint. Check service connection ID and Pipeline "
|
||||||
"configuration.\n\n"
|
"configuration";
|
||||||
+ responseBody;
|
|
||||||
|
auto responseHeaders = response->GetHeaders();
|
||||||
|
auto headerValue = responseHeaders.find("x-vss-e2eid");
|
||||||
|
if (headerValue != responseHeaders.end())
|
||||||
|
{
|
||||||
|
message += "\n" + headerValue->first + ":" + headerValue->second;
|
||||||
|
}
|
||||||
|
headerValue = responseHeaders.find("x-msedge-ref");
|
||||||
|
if (headerValue != responseHeaders.end())
|
||||||
|
{
|
||||||
|
message += "\n" + headerValue->first + ":" + headerValue->second;
|
||||||
|
}
|
||||||
|
message += "\n\n" + responseBody;
|
||||||
|
|
||||||
IdentityLog::Write(IdentityLog::Level::Verbose, message);
|
IdentityLog::Write(IdentityLog::Level::Verbose, message);
|
||||||
|
|
||||||
throw AuthenticationException(message);
|
throw AuthenticationException(message);
|
||||||
@ -158,7 +182,7 @@ AzurePipelinesCredential::~AzurePipelinesCredential() = default;
|
|||||||
std::string AzurePipelinesCredential::GetAssertion(Context const& context) const
|
std::string AzurePipelinesCredential::GetAssertion(Context const& context) const
|
||||||
{
|
{
|
||||||
Azure::Core::Http::Request oidcRequest = CreateOidcRequestMessage();
|
Azure::Core::Http::Request oidcRequest = CreateOidcRequestMessage();
|
||||||
std::unique_ptr<RawResponse> response = m_httpPipeline.Send(oidcRequest, context);
|
std::unique_ptr<RawResponse> response = m_httpPipeline->Send(oidcRequest, context);
|
||||||
|
|
||||||
if (!response)
|
if (!response)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,6 +4,9 @@
|
|||||||
#include "azure/identity/azure_pipelines_credential.hpp"
|
#include "azure/identity/azure_pipelines_credential.hpp"
|
||||||
#include "credential_test_helper.hpp"
|
#include "credential_test_helper.hpp"
|
||||||
|
|
||||||
|
#include <azure/core/diagnostics/logger.hpp>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
@ -13,6 +16,7 @@ using Azure::Core::_internal::Environment;
|
|||||||
using Azure::Core::Credentials::AccessToken;
|
using Azure::Core::Credentials::AccessToken;
|
||||||
using Azure::Core::Credentials::AuthenticationException;
|
using Azure::Core::Credentials::AuthenticationException;
|
||||||
using Azure::Core::Credentials::TokenRequestContext;
|
using Azure::Core::Credentials::TokenRequestContext;
|
||||||
|
using Azure::Core::Diagnostics::Logger;
|
||||||
using Azure::Core::Http::HttpMethod;
|
using Azure::Core::Http::HttpMethod;
|
||||||
using Azure::Identity::AzurePipelinesCredential;
|
using Azure::Identity::AzurePipelinesCredential;
|
||||||
using Azure::Identity::AzurePipelinesCredentialOptions;
|
using Azure::Identity::AzurePipelinesCredentialOptions;
|
||||||
@ -151,6 +155,76 @@ TEST(AzurePipelinesCredential, InvalidArgs)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(AzurePipelinesCredential, RegularExpectedHeadersLogged)
|
||||||
|
{
|
||||||
|
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)); });
|
||||||
|
|
||||||
|
std::map<std::string, std::string> validEnvVars
|
||||||
|
= {{"SYSTEM_OIDCREQUESTURI", "https://localhost/instance"}};
|
||||||
|
CredentialTestHelper::EnvironmentOverride const env(validEnvVars);
|
||||||
|
|
||||||
|
using namespace Azure::Identity::Test::_detail;
|
||||||
|
using Azure::Core::CaseInsensitiveMap;
|
||||||
|
using Azure::Core::Http::HttpStatusCode;
|
||||||
|
|
||||||
|
// The first response is from the OIDC endpoint, the second is from the identity token endpoint.
|
||||||
|
// The x-vss-e2eid header should be logged in the first response, but not in the second.
|
||||||
|
CaseInsensitiveMap responseHeaders;
|
||||||
|
responseHeaders.emplace("x-vss-e2eid", "some id for debugging");
|
||||||
|
responseHeaders.emplace("x-msedge-ref", "some AFD impression log reference");
|
||||||
|
|
||||||
|
CredentialTestHelper::TokenRequestSimulationServerResponse response1
|
||||||
|
= {HttpStatusCode::Ok, "{\"oidcToken\":\"abc/d\"}", responseHeaders};
|
||||||
|
|
||||||
|
CredentialTestHelper::TokenRequestSimulationServerResponse response2
|
||||||
|
= {HttpStatusCode::Ok,
|
||||||
|
"{\"expires_in\":3600, \"access_token\":\"ACCESSTOKEN1\"}",
|
||||||
|
responseHeaders};
|
||||||
|
|
||||||
|
std::vector<CredentialTestHelper::TokenRequestSimulationServerResponse> responses
|
||||||
|
= {response1, response2};
|
||||||
|
|
||||||
|
auto const actual = CredentialTestHelper::SimulateTokenRequest(
|
||||||
|
[](auto transport) {
|
||||||
|
AzurePipelinesCredentialOptions options;
|
||||||
|
options.Transport.Transport = transport;
|
||||||
|
|
||||||
|
std::string tenantId = "01234567-89ab-cdef-fedc-ba8976543210";
|
||||||
|
std::string clientId = "fedcba98-7654-3210-0123-456789abcdef";
|
||||||
|
std::string serviceConnectionId = "a/bc";
|
||||||
|
std::string systemAccessToken = "123";
|
||||||
|
|
||||||
|
return std::make_unique<AzurePipelinesCredential>(
|
||||||
|
tenantId, clientId, serviceConnectionId, systemAccessToken, options);
|
||||||
|
},
|
||||||
|
{{{"https://azure.com/.default"}}},
|
||||||
|
responses);
|
||||||
|
|
||||||
|
auto const& response0 = actual.Responses.at(0);
|
||||||
|
|
||||||
|
EXPECT_EQ(response0.AccessToken.Token, "ACCESSTOKEN1");
|
||||||
|
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
EXPECT_GE(response0.AccessToken.ExpiresOn, response0.EarliestExpiration + 3600s);
|
||||||
|
EXPECT_LE(response0.AccessToken.ExpiresOn, response0.LatestExpiration + 3600s);
|
||||||
|
|
||||||
|
EXPECT_EQ(log.size(), LogMsgVec::size_type(7));
|
||||||
|
// The first response, from the OIDC endpoint, should have the x-vss-e2eid header logged.
|
||||||
|
EXPECT_TRUE(log[2].second.find("some id for debugging") != std::string::npos);
|
||||||
|
EXPECT_TRUE(log[2].second.find("some AFD impression log reference") != std::string::npos);
|
||||||
|
|
||||||
|
// The second response, from the identity token endpoint still has that header redacted, as
|
||||||
|
// expected.
|
||||||
|
EXPECT_TRUE(log[5].second.find("some id for debugging") == std::string::npos);
|
||||||
|
EXPECT_TRUE(log[5].second.find("some AFD impression log reference") == std::string::npos);
|
||||||
|
EXPECT_TRUE(log[5].second.find("REDACTED") != std::string::npos);
|
||||||
|
|
||||||
|
Logger::SetListener(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(AzurePipelinesCredential, Regular)
|
TEST(AzurePipelinesCredential, Regular)
|
||||||
{
|
{
|
||||||
std::map<std::string, std::string> validEnvVars
|
std::map<std::string, std::string> validEnvVars
|
||||||
@ -422,34 +496,64 @@ TEST(AzurePipelinesCredential, InvalidOidcResponse)
|
|||||||
std::string serviceConnectionId = "a/bc";
|
std::string serviceConnectionId = "a/bc";
|
||||||
std::string systemAccessToken = "123";
|
std::string systemAccessToken = "123";
|
||||||
|
|
||||||
|
using Azure::Core::Http::HttpStatusCode;
|
||||||
|
|
||||||
// Non-OK response
|
// Non-OK response
|
||||||
try
|
CredentialTestHelper::TokenRequestSimulationServerResponse testResponse0;
|
||||||
|
testResponse0.StatusCode = HttpStatusCode::BadRequest;
|
||||||
|
testResponse0.Body = "Invalid response body";
|
||||||
|
|
||||||
|
CredentialTestHelper::TokenRequestSimulationServerResponse testResponse1 = testResponse0;
|
||||||
|
testResponse1.Headers.emplace("x-vss-e2eid", "some id for debugging");
|
||||||
|
|
||||||
|
CredentialTestHelper::TokenRequestSimulationServerResponse testResponse2 = testResponse0;
|
||||||
|
testResponse2.Headers.emplace("x-msedge-ref", "some AFD impression log reference");
|
||||||
|
|
||||||
|
CredentialTestHelper::TokenRequestSimulationServerResponse testResponse3 = testResponse0;
|
||||||
|
testResponse3.Headers.emplace("x-vss-e2eid", "some id for debugging");
|
||||||
|
testResponse3.Headers.emplace("x-msedge-ref", "some AFD impression log reference");
|
||||||
|
testResponse3.Headers.emplace("foo", "bar"); // won't show up in the exception message
|
||||||
|
|
||||||
|
CredentialTestHelper::TokenRequestSimulationServerResponse testResponses[4]
|
||||||
|
= {testResponse0, testResponse1, testResponse2, testResponse3};
|
||||||
|
|
||||||
|
std::string baseExpectedMessage
|
||||||
|
= "AzurePipelinesCredential : 400 (Test) response from the OIDC endpoint. Check service "
|
||||||
|
"connection ID and Pipeline configuration";
|
||||||
|
std::array<std::string, 4> expectedMessages
|
||||||
|
= {baseExpectedMessage + "\n\nInvalid response body",
|
||||||
|
baseExpectedMessage + "\nx-vss-e2eid:some id for debugging\n\nInvalid response body",
|
||||||
|
baseExpectedMessage
|
||||||
|
+ "\nx-msedge-ref:some AFD impression log reference\n\nInvalid response body",
|
||||||
|
baseExpectedMessage
|
||||||
|
+ "\nx-vss-e2eid:some id for debugging\nx-msedge-ref:some AFD impression log "
|
||||||
|
"reference\n\nInvalid response body"};
|
||||||
|
|
||||||
|
for (size_t i = 0; i < expectedMessages.size(); i++)
|
||||||
{
|
{
|
||||||
using Azure::Core::Http::HttpStatusCode;
|
try
|
||||||
std::vector<std::string> const testScopes;
|
{
|
||||||
CredentialTestHelper::TokenRequestSimulationServerResponse testResponse;
|
|
||||||
testResponse.StatusCode = HttpStatusCode::BadRequest;
|
|
||||||
testResponse.Body = "Invalid response body";
|
|
||||||
|
|
||||||
static_cast<void>(CredentialTestHelper::SimulateTokenRequest(
|
std::vector<std::string> const testScopes;
|
||||||
[&](auto transport) {
|
CredentialTestHelper::TokenRequestSimulationServerResponse testResponse = testResponses[i];
|
||||||
AzurePipelinesCredentialOptions options;
|
|
||||||
options.Transport.Transport = transport;
|
|
||||||
|
|
||||||
return std::make_unique<AzurePipelinesCredential>(
|
static_cast<void>(CredentialTestHelper::SimulateTokenRequest(
|
||||||
tenantId, clientId, serviceConnectionId, systemAccessToken, options);
|
[&](auto transport) {
|
||||||
},
|
AzurePipelinesCredentialOptions options;
|
||||||
{testScopes},
|
options.Transport.Transport = transport;
|
||||||
{testResponse}));
|
|
||||||
|
|
||||||
EXPECT_TRUE(!"AzurePipelinesCredential should throw given the response above.");
|
return std::make_unique<AzurePipelinesCredential>(
|
||||||
}
|
tenantId, clientId, serviceConnectionId, systemAccessToken, options);
|
||||||
catch (AuthenticationException const& ex)
|
},
|
||||||
{
|
{testScopes},
|
||||||
std::string expectedMessage
|
{testResponse}));
|
||||||
= "AzurePipelinesCredential : 400 (Test) response from the OIDC endpoint. Check service "
|
|
||||||
"connection ID and Pipeline configuration.\n\nInvalid response body";
|
EXPECT_TRUE(!"AzurePipelinesCredential should throw given the response above.");
|
||||||
EXPECT_EQ(ex.what(), expectedMessage) << ex.what();
|
}
|
||||||
|
catch (AuthenticationException const& ex)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(ex.what(), expectedMessages[i]) << ex.what();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalid JSON
|
// Invalid JSON
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user