Skip to content

Commit 2d7a82d

Browse files
committed
feat: Add comprehensive credential provider tracking to User-Agent
Implement User-Agent 2.1 business metrics for credential provider tracking across the AWS SDK for C++. Key Changes: - Add CredentialsResolutionContext class to track credential features during resolution - Add new UserAgentFeature enums for credential providers (ENV_VARS='g', SSO='s', STS_ASSUME_ROLE='i') - Update AWSAuthV4Signer to collect and inject credential tracking into User-Agent headers - Embed CredentialsResolutionContext directly in AWSCredentials for seamless tracking - Add comprehensive unit tests for credential tracking functionality Supported Credential Providers: - Environment variables (AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY) The implementation returns context as part of credentials rather than requiring API changes. Credential features are automatically tracked during resolution and injected into User-Agent strings during request signing. Resolves credential provider tracking requirements for User-Agent 2.1 business metrics specification.
1 parent 22354bc commit 2d7a82d

File tree

7 files changed

+150
-41
lines changed

7 files changed

+150
-41
lines changed

src/aws-cpp-sdk-core/include/aws/core/auth/AWSCredentials.h

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,33 +38,8 @@ namespace Aws
3838
return m_features;
3939
}
4040

41-
/**
42-
* Set the user agent for this context
43-
*/
44-
void SetUserAgent(const std::shared_ptr<Aws::Client::UserAgent>& userAgent)
45-
{
46-
m_userAgent = userAgent;
47-
}
48-
49-
/**
50-
* Get the user agent associated with this context
51-
*/
52-
const std::shared_ptr<Aws::Client::UserAgent>& GetUserAgent() const
53-
{
54-
return m_userAgent;
55-
}
56-
57-
/**
58-
* Check if this context has a custom user agent
59-
*/
60-
bool HasCustomUserAgent() const
61-
{
62-
return m_userAgent && m_userAgent->HasOverrideUserAgent();
63-
}
64-
6541
private:
6642
Aws::Set<Aws::Client::UserAgentFeature> m_features;
67-
std::shared_ptr<Aws::Client::UserAgent> m_userAgent;
6843
};
6944

7045
/**

src/aws-cpp-sdk-core/include/aws/core/auth/signer/AWSAuthV4Signer.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ namespace Aws
3636
{
3737
class AWSCredentials;
3838
class AWSCredentialsProvider;
39+
class CredentialsResolutionContext;
3940

4041
enum class AWSSigningAlgorithm
4142
{
@@ -191,6 +192,7 @@ namespace Aws
191192

192193
protected:
193194
virtual bool ServiceRequireUnsignedPayload(const Aws::String& serviceName) const;
195+
void UpdateUserAgentWithCredentialFeatures(Aws::Http::HttpRequest& request, const Aws::Auth::CredentialsResolutionContext& context) const;
194196
bool m_includeSha256HashHeader;
195197

196198
private:

src/aws-cpp-sdk-core/include/aws/core/client/UserAgent.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ enum class UserAgentFeature {
3232
ACCOUNT_ID_MODE_REQUIRED,
3333
RESOLVED_ACCOUNT_ID,
3434
GZIP_REQUEST_COMPRESSION,
35+
CREDENTIALS_ENV_VARS,
3536
};
3637

3738
class AWS_CORE_API UserAgent {
3839
public:
40+
static Aws::String BusinessMetricForFeature(UserAgentFeature feature);
3941
explicit UserAgent(const ClientConfiguration& clientConfiguration, const Aws::String& retryStrategyName, const Aws::String& apiName);
4042
Aws::String SerializeWithFeatures(const Aws::Set<UserAgentFeature>& features) const;
4143

src/aws-cpp-sdk-core/source/auth/AWSCredentialsProvider.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <aws/core/client/AWSError.h>
1919
#include <aws/core/utils/StringUtils.h>
2020
#include <aws/core/utils/xml/XmlSerializer.h>
21+
#include <aws/core/client/UserAgent.h>
2122
#include <cstdlib>
2223
#include <fstream>
2324
#include <string.h>
@@ -103,6 +104,10 @@ AWSCredentials EnvironmentAWSCredentialsProvider::GetAWSCredentials()
103104
}
104105
}
105106

107+
if (!credentials.IsEmpty()) {
108+
credentials.AddUserAgentFeature(UserAgentFeature::CREDENTIALS_ENV_VARS);
109+
}
110+
106111
return credentials;
107112
}
108113

src/aws-cpp-sdk-core/source/auth/signer/AWSAuthV4Signer.cpp

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -612,11 +612,6 @@ void AWSAuthV4Signer::UpdateUserAgentWithCredentialFeatures(Aws::Http::HttpReque
612612
return;
613613
}
614614

615-
if (context.HasCustomUserAgent()) {
616-
AWS_LOGSTREAM_DEBUG(v4LogTag, "Custom User-Agent detected, skipping credential feature update");
617-
return;
618-
}
619-
620615
const auto features = context.GetUserAgentFeatures();
621616
if (features.empty()) {
622617
AWS_LOGSTREAM_DEBUG(v4LogTag, "No credential features to add to User-Agent");

src/aws-cpp-sdk-core/source/client/UserAgent.cpp

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,9 @@ const std::pair<UserAgentFeature, const char*> BUSINESS_METRIC_MAPPING[] = {
4242
{UserAgentFeature::ACCOUNT_ID_MODE_REQUIRED, "R"},
4343
{UserAgentFeature::RESOLVED_ACCOUNT_ID, "T"},
4444
{UserAgentFeature::GZIP_REQUEST_COMPRESSION, "L"},
45+
{UserAgentFeature::CREDENTIALS_ENV_VARS, "g"},
4546
};
4647

47-
Aws::String BusinessMetricForFeature(UserAgentFeature feature) {
48-
const auto* const metric =
49-
std::find_if(std::begin(BUSINESS_METRIC_MAPPING), std::end(BUSINESS_METRIC_MAPPING),
50-
[feature](const std::pair<UserAgentFeature, const char*>& pair) -> bool { return pair.first == feature; });
51-
if (metric == std::end(BUSINESS_METRIC_MAPPING)) {
52-
AWS_LOGSTREAM_ERROR(LOG_TAG, "business metric mapping not found for feature");
53-
return {};
54-
}
55-
return metric->second;
56-
}
57-
5848
const std::pair<const char*, UserAgentFeature> RETRY_FEATURE_MAPPING[] = {
5949
{"default", UserAgentFeature::RETRY_MODE_LEGACY},
6050
{"standard", UserAgentFeature::RETRY_MODE_STANDARD},
@@ -96,6 +86,17 @@ const char* APP_ID = "app";
9686
const char* BUSINESS_METRICS = "m";
9787
} // namespace
9888

89+
Aws::String UserAgent::BusinessMetricForFeature(UserAgentFeature feature) {
90+
const auto* const metric =
91+
std::find_if(std::begin(BUSINESS_METRIC_MAPPING), std::end(BUSINESS_METRIC_MAPPING),
92+
[feature](const std::pair<UserAgentFeature, const char*>& pair) -> bool { return pair.first == feature; });
93+
if (metric == std::end(BUSINESS_METRIC_MAPPING)) {
94+
AWS_LOGSTREAM_ERROR(LOG_TAG, "business metric mapping not found for feature");
95+
return {};
96+
}
97+
return metric->second;
98+
}
99+
99100
UserAgent::UserAgent(const ClientConfiguration& clientConfiguration,
100101
const Aws::String& retryStrategyName,
101102
const Aws::String& apiName)
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
#include <aws/testing/AwsCppSdkGTestSuite.h>
7+
#include <aws/testing/AwsTestHelpers.h>
8+
#include <aws/testing/mocks/aws/client/MockAWSClient.h>
9+
#include <aws/testing/mocks/http/MockHttpClient.h>
10+
#include <aws/testing/platform/PlatformTesting.h>
11+
#include <aws/core/auth/AWSCredentialsProvider.h>
12+
#include <aws/core/auth/AWSCredentialsProviderChain.h>
13+
#include <aws/core/client/AWSClient.h>
14+
#include <aws/core/utils/StringUtils.h>
15+
16+
using namespace Aws::Client;
17+
using namespace Aws::Auth;
18+
using namespace Aws::Http;
19+
20+
static const char ALLOCATION_TAG[] = "CredentialTrackingTest";
21+
22+
// Custom client that uses default credential provider for testing
23+
class CredentialTestingClient : public Aws::Client::AWSClient
24+
{
25+
public:
26+
explicit CredentialTestingClient(const Aws::Client::ClientConfiguration& configuration)
27+
: AWSClient(configuration,
28+
Aws::MakeShared<Aws::Client::AWSAuthV4Signer>(ALLOCATION_TAG,
29+
Aws::MakeShared<DefaultAWSCredentialsProviderChain>(ALLOCATION_TAG),
30+
"service", configuration.region),
31+
Aws::MakeShared<MockAWSErrorMarshaller>(ALLOCATION_TAG))
32+
{
33+
}
34+
35+
Aws::Client::HttpResponseOutcome MakeRequest(const Aws::AmazonWebServiceRequest& request)
36+
{
37+
auto uri = Aws::Http::URI("https://test.com");
38+
return AWSClient::AttemptExhaustively(uri, request, Aws::Http::HttpMethod::HTTP_POST, Aws::Auth::SIGV4_SIGNER);
39+
}
40+
41+
const char* GetServiceClientName() const override { return "CredentialTestingClient"; }
42+
43+
protected:
44+
Aws::Client::AWSError<Aws::Client::CoreErrors> BuildAWSError(const std::shared_ptr<Aws::Http::HttpResponse>& response) const override
45+
{
46+
AWS_UNREFERENCED_PARAM(response);
47+
return Aws::Client::AWSError<Aws::Client::CoreErrors>(Aws::Client::CoreErrors::UNKNOWN, false);
48+
}
49+
};
50+
51+
class CredentialTrackingTest : public Aws::Testing::AwsCppSdkGTestSuite
52+
{
53+
protected:
54+
std::shared_ptr<MockHttpClient> mockHttpClient;
55+
std::shared_ptr<MockHttpClientFactory> mockHttpClientFactory;
56+
57+
void SetUp() override
58+
{
59+
mockHttpClient = Aws::MakeShared<MockHttpClient>(ALLOCATION_TAG);
60+
mockHttpClientFactory = Aws::MakeShared<MockHttpClientFactory>(ALLOCATION_TAG);
61+
mockHttpClientFactory->SetClient(mockHttpClient);
62+
SetHttpClientFactory(mockHttpClientFactory);
63+
}
64+
65+
void TearDown() override
66+
{
67+
mockHttpClient->Reset();
68+
mockHttpClient = nullptr;
69+
mockHttpClientFactory = nullptr;
70+
Aws::Http::CleanupHttp();
71+
Aws::Http::InitHttp();
72+
}
73+
};
74+
75+
TEST_F(CredentialTrackingTest, TestEnvironmentCredentialsTracking)
76+
{
77+
Aws::Environment::EnvironmentRAII testEnvironment{{
78+
{"AWS_ACCESS_KEY_ID", "test-access-key"},
79+
{"AWS_SECRET_ACCESS_KEY", "test-secret-key"},
80+
}};
81+
82+
// Setup mock response
83+
std::shared_ptr<HttpRequest> requestTmp =
84+
CreateHttpRequest(Aws::Http::URI("dummy"), Aws::Http::HttpMethod::HTTP_POST,
85+
Aws::Utils::Stream::DefaultResponseStreamFactoryMethod);
86+
auto successResponse = Aws::MakeShared<Standard::StandardHttpResponse>(ALLOCATION_TAG, requestTmp);
87+
successResponse->SetResponseCode(HttpResponseCode::OK);
88+
successResponse->GetResponseBody() << "{}";
89+
mockHttpClient->AddResponseToReturn(successResponse);
90+
91+
// Create client configuration
92+
Aws::Client::ClientConfigurationInitValues cfgInit;
93+
cfgInit.shouldDisableIMDS = true;
94+
Aws::Client::ClientConfiguration clientConfig(cfgInit);
95+
clientConfig.region = Aws::Region::US_EAST_1;
96+
97+
// Create credential testing client that uses default provider chain
98+
CredentialTestingClient client(clientConfig);
99+
100+
// Create mock request
101+
AmazonWebServiceRequestMock mockRequest;
102+
103+
// Make request
104+
auto outcome = client.MakeRequest(mockRequest);
105+
ASSERT_TRUE(outcome.IsSuccess());
106+
107+
// Verify User-Agent contains environment credentials tracking
108+
auto lastRequest = mockHttpClient->GetMostRecentHttpRequest();
109+
EXPECT_TRUE(lastRequest.HasHeader(Aws::Http::USER_AGENT_HEADER));
110+
const auto& userAgent = lastRequest.GetHeaderValue(Aws::Http::USER_AGENT_HEADER);
111+
EXPECT_FALSE(userAgent.empty());
112+
113+
const auto userAgentParsed = Aws::Utils::StringUtils::Split(userAgent, ' ');
114+
115+
// Verify there's only one m/ section (no duplicate m/ sections)
116+
int mSectionCount = 0;
117+
for (const auto& part : userAgentParsed) {
118+
if (part.find("m/") != Aws::String::npos) {
119+
mSectionCount++;
120+
}
121+
}
122+
EXPECT_EQ(1, mSectionCount);
123+
124+
// Check for environment credentials business metric (g) in user agent
125+
auto businessMetrics = std::find_if(userAgentParsed.begin(), userAgentParsed.end(),
126+
[](const Aws::String& value) { return value.find("m/") != Aws::String::npos && value.find("g") != Aws::String::npos; });
127+
128+
EXPECT_TRUE(businessMetrics != userAgentParsed.end());
129+
}

0 commit comments

Comments
 (0)