Skip to content

Commit b6ee429

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 4d13fe0 commit b6ee429

File tree

7 files changed

+250
-11
lines changed

7 files changed

+250
-11
lines changed

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,42 @@
66
#pragma once
77

88
#include <aws/core/Core_EXPORTS.h>
9+
#include <aws/core/client/UserAgent.h>
910
#include <aws/core/utils/memory/stl/AWSString.h>
1011
#include <aws/core/utils/DateTime.h>
1112
namespace Aws
1213
{
1314
namespace Auth
1415
{
16+
/**
17+
* Context class for credential resolution that tracks features used during credential retrieval.
18+
*/
19+
class AWS_CORE_API CredentialsResolutionContext
20+
{
21+
public:
22+
// Default constructor - no features tracked
23+
CredentialsResolutionContext() = default;
24+
25+
/**
26+
* Add a user agent feature to track credential usage.
27+
*/
28+
void AddUserAgentFeature(Aws::Client::UserAgentFeature feature)
29+
{
30+
m_features.insert(feature);
31+
}
32+
33+
/**
34+
* Get all tracked credential features.
35+
*/
36+
const Aws::Set<Aws::Client::UserAgentFeature> GetUserAgentFeatures() const
37+
{
38+
return m_features;
39+
}
40+
41+
private:
42+
Aws::Set<Aws::Client::UserAgentFeature> m_features;
43+
};
44+
1545
/**
1646
* Simple data object around aws credentials
1747
*/
@@ -214,12 +244,24 @@ namespace Aws
214244
m_expiration = expiration;
215245
}
216246

247+
/**
248+
* Gets credential resolution context. this is information about the call
249+
* such as what credentials provider was used to to resolve the credentials
250+
*/
251+
inline CredentialsResolutionContext GetContext() { return m_context; }
252+
253+
/**
254+
* Adds a user agent feature used during credentials resolution to the credentials
255+
* context. This is useful to track which credentials provider was used.
256+
*/
257+
inline void AddUserAgentFeatire(Aws::Client::UserAgentFeature feature) { m_context.AddUserAgentFeature(feature); }
217258
private:
218259
Aws::String m_accessKeyId;
219260
Aws::String m_secretKey;
220261
Aws::String m_sessionToken;
221262
Aws::Utils::DateTime m_expiration;
222263
Aws::String m_accountId;
264+
CredentialsResolutionContext m_context;
223265
};
224266
}
225267
}

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
void SetApiName(const Aws::String& apiName) { m_api = apiName; }

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.AddUserAgentFeatire(UserAgentFeature::CREDENTIALS_ENV_VARS);
109+
}
110+
106111
return credentials;
107112
}
108113

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <aws/core/auth/signer/AWSAuthSignerHelper.h>
99

1010
#include <aws/core/auth/AWSCredentialsProvider.h>
11+
#include <aws/core/client/UserAgent.h>
1112
#include <aws/core/http/HttpRequest.h>
1213
#include <aws/core/http/URI.h>
1314
#include <aws/core/utils/DateTime.h>
@@ -25,6 +26,7 @@
2526

2627
#include <iomanip>
2728
#include <cstring>
29+
#include <numeric>
2830

2931
using namespace Aws;
3032
using namespace Aws::Client;
@@ -81,6 +83,8 @@ bool AWSAuthV4Signer::SignRequestWithSigV4a(Aws::Http::HttpRequest& request, con
8183
bool signBody, long long expirationTimeInSeconds, Aws::Crt::Auth::SignatureType signatureType) const
8284
{
8385
AWSCredentials credentials = GetCredentials(request.GetServiceSpecificParameters());
86+
87+
UpdateUserAgentWithCredentialFeatures(request, credentials.GetContext());
8488
auto crtCredentials = Aws::MakeShared<Aws::Crt::Auth::Credentials>(v4AsymmetricLogTag,
8589
Aws::Crt::ByteCursorFromCString(credentials.GetAWSAccessKeyId().c_str()),
8690
Aws::Crt::ByteCursorFromCString(credentials.GetAWSSecretKey().c_str()),
@@ -336,6 +340,9 @@ bool AWSAuthV4Signer::SignRequestWithCreds(Aws::Http::HttpRequest& request, cons
336340
bool AWSAuthV4Signer::SignRequest(Aws::Http::HttpRequest& request, const char* region, const char* serviceName, bool signBody) const
337341
{
338342
AWSCredentials credentials = GetCredentials(request.GetServiceSpecificParameters());
343+
344+
UpdateUserAgentWithCredentialFeatures(request, credentials.GetContext());
345+
339346
return SignRequestWithCreds(request, credentials, region, serviceName, signBody);
340347
}
341348

@@ -464,6 +471,9 @@ bool AWSAuthV4Signer::PresignRequest(Aws::Http::HttpRequest& request, const Aws:
464471
bool AWSAuthV4Signer::PresignRequest(Aws::Http::HttpRequest& request, const char* region, const char* serviceName, long long expirationTimeInSeconds) const
465472
{
466473
AWSCredentials credentials = GetCredentials(request.GetServiceSpecificParameters());
474+
475+
UpdateUserAgentWithCredentialFeatures(request, credentials.GetContext());
476+
467477
return PresignRequest(request, credentials, region,serviceName, expirationTimeInSeconds );
468478
}
469479

@@ -595,3 +605,51 @@ Aws::Auth::AWSCredentials AWSAuthV4Signer::GetCredentials(const std::shared_ptr<
595605
AWS_UNREFERENCED_PARAM(serviceSpecificParameters);
596606
return m_credentialsProvider->GetAWSCredentials();
597607
}
608+
609+
void AWSAuthV4Signer::UpdateUserAgentWithCredentialFeatures(Aws::Http::HttpRequest& request, const Aws::Auth::CredentialsResolutionContext& context) const {
610+
if (!request.HasHeader(USER_AGENT)) {
611+
AWS_LOGSTREAM_DEBUG(v4LogTag, "Request does not have User-Agent header, skipping credential feature update");
612+
return;
613+
}
614+
615+
const auto features = context.GetUserAgentFeatures();
616+
if (features.empty()) {
617+
AWS_LOGSTREAM_DEBUG(v4LogTag, "No credential features to add to User-Agent");
618+
return;
619+
}
620+
621+
std::vector<Aws::String> businessMetrics(features.size());
622+
std::transform(features.begin(),
623+
features.end(),
624+
businessMetrics.begin(),
625+
[](UserAgentFeature feature) -> Aws::String { return UserAgent::BusinessMetricForFeature(feature); });
626+
627+
const auto credentialFeatures = std::accumulate(std::next(businessMetrics.begin()),
628+
businessMetrics.end(),
629+
businessMetrics.front(),
630+
[](const Aws::String& a, const Aws::String& b) {
631+
return a + "," + b;
632+
});
633+
634+
const auto userAgent = request.GetHeaderValue(USER_AGENT);
635+
auto userAgentParsed = Aws::Utils::StringUtils::Split(userAgent, ' ');
636+
auto metricsSegment = std::find_if(userAgentParsed.begin(), userAgentParsed.end(),
637+
[](const Aws::String& value) { return value.find("m/") != Aws::String::npos; });
638+
639+
if (metricsSegment != userAgentParsed.end()) {
640+
// Add new metrics to existing metrics section
641+
*metricsSegment = Aws::String{*metricsSegment + "," + credentialFeatures};
642+
} else {
643+
// No metrics section exists, add new one
644+
userAgentParsed.push_back("m/" + credentialFeatures);
645+
}
646+
647+
// Reassemble all parts with spaces
648+
const auto newUserAgent = std::accumulate(std::next(userAgentParsed.begin()),
649+
userAgentParsed.end(),
650+
userAgentParsed.front(),
651+
[](const Aws::String& a, const Aws::String& b) {
652+
return a + " " + b;
653+
});
654+
request.SetUserAgent(newUserAgent);
655+
}

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)