Skip to content

Commit 56dcdc1

Browse files
authored
Implement credential tracking for profile, process, STS, and IMDS providers (#3527)
Add user agent feature tracking to identify credential provider usage: - Profile credentials: CREDENTIALS_PROFILE (n) - Process credentials: CREDENTIALS_PROFILE_PROCESS (o) - STS credentials: CREDENTIALS_STS (p) - IMDS credentials: CREDENTIALS_IMDS (q) Changes: - Add UserAgentFeature enum values for credential tracking - Update credential providers to add tracking features - Add comprehensive tests for credential tracking validation - Fix type mismatches and improve error handling
1 parent e81d6c3 commit 56dcdc1

File tree

5 files changed

+164
-53
lines changed

5 files changed

+164
-53
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ enum class UserAgentFeature {
3333
RESOLVED_ACCOUNT_ID,
3434
GZIP_REQUEST_COMPRESSION,
3535
CREDENTIALS_ENV_VARS,
36+
CREDENTIALS_PROFILE,
37+
CREDENTIALS_PROFILE_PROCESS,
38+
CREDENTIALS_IMDS,
39+
CREDENTIALS_STS_ASSUME_ROLE,
3640
};
3741

3842
class AWS_CORE_API UserAgent {

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,11 @@ AWSCredentials ProfileConfigFileAWSCredentialsProvider::GetAWSCredentials()
201201

202202
if(credsFileProfileIter != profiles.end())
203203
{
204-
return credsFileProfileIter->second.GetCredentials();
204+
AWSCredentials credentials = credsFileProfileIter->second.GetCredentials();
205+
if (!credentials.IsEmpty()) {
206+
credentials.AddUserAgentFeature(UserAgentFeature::CREDENTIALS_PROFILE);
207+
}
208+
return credentials;
205209
}
206210

207211
return AWSCredentials();
@@ -265,7 +269,11 @@ AWSCredentials InstanceProfileCredentialsProvider::GetAWSCredentials()
265269
auto profileIter = profiles.find(Aws::Config::INSTANCE_PROFILE_KEY);
266270

267271
if (profileIter != profiles.end()) {
268-
return profileIter->second.GetCredentials();
272+
AWSCredentials credentials = profileIter->second.GetCredentials();
273+
if (!credentials.IsEmpty()) {
274+
credentials.AddUserAgentFeature(UserAgentFeature::CREDENTIALS_IMDS);
275+
}
276+
return credentials;
269277
}
270278
}
271279
else
@@ -357,6 +365,9 @@ void ProcessCredentialsProvider::Reload()
357365
return;
358366
}
359367
m_credentials = GetCredentialsFromProcess(command);
368+
if (!m_credentials.IsEmpty()) {
369+
m_credentials.AddUserAgentFeature(UserAgentFeature::CREDENTIALS_PROFILE_PROCESS);
370+
}
360371
}
361372

362373
void ProcessCredentialsProvider::RefreshIfExpired()

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ AWSCredentials STSAssumeRoleWebIdentityCredentialsProvider::GetAWSCredentials()
104104

105105
std::unique_lock<std::mutex> lock{m_refreshMutex};
106106
m_refreshSignal.wait_for(lock, m_providerFuturesTimeoutMs, [&refreshDone]() -> bool { return refreshDone; });
107+
108+
if (!credentials.IsEmpty()) {
109+
credentials.AddUserAgentFeature(Aws::Client::UserAgentFeature::CREDENTIALS_STS_ASSUME_ROLE);
110+
}
111+
107112
return credentials;
108113
}
109114

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ const std::pair<UserAgentFeature, const char*> BUSINESS_METRIC_MAPPING[] = {
4343
{UserAgentFeature::RESOLVED_ACCOUNT_ID, "T"},
4444
{UserAgentFeature::GZIP_REQUEST_COMPRESSION, "L"},
4545
{UserAgentFeature::CREDENTIALS_ENV_VARS, "g"},
46+
{UserAgentFeature::CREDENTIALS_PROFILE, "n"},
47+
{UserAgentFeature::CREDENTIALS_PROFILE_PROCESS, "v"},
48+
{UserAgentFeature::CREDENTIALS_IMDS, "0"},
49+
{UserAgentFeature::CREDENTIALS_STS_ASSUME_ROLE, "i"},
4650
};
4751

4852
const std::pair<const char*, UserAgentFeature> RETRY_FEATURE_MAPPING[] = {

tests/aws-cpp-sdk-core-tests/aws/auth/CredentialTrackingTest.cpp

Lines changed: 138 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,56 @@
77
#include <aws/testing/AwsTestHelpers.h>
88
#include <aws/testing/mocks/aws/client/MockAWSClient.h>
99
#include <aws/testing/mocks/http/MockHttpClient.h>
10+
#include <aws/testing/mocks/aws/auth/MockAWSHttpResourceClient.h>
1011
#include <aws/testing/platform/PlatformTesting.h>
1112
#include <aws/core/auth/AWSCredentialsProvider.h>
1213
#include <aws/core/auth/AWSCredentialsProviderChain.h>
1314
#include <aws/core/client/AWSClient.h>
1415
#include <aws/core/utils/StringUtils.h>
16+
#include <aws/core/utils/HashingUtils.h>
17+
#include <aws/core/platform/FileSystem.h>
18+
#include <aws/core/utils/FileSystemUtils.h>
19+
#include <fstream>
20+
#include <sys/stat.h>
1521

1622
using namespace Aws::Client;
1723
using namespace Aws::Auth;
1824
using namespace Aws::Http;
1925

20-
static const char ALLOCATION_TAG[] = "CredentialTrackingTest";
26+
namespace {
27+
const char* TEST_LOG_TAG = "CredentialTrackingTest";
28+
}
29+
30+
static Aws::String WrapEchoStringWithSingleQuoteForUnixShell(Aws::String str)
31+
{
32+
#ifndef _WIN32
33+
str.insert(0, 1, '\'');
34+
str.append(1, '\'');
35+
#endif
36+
return str;
37+
}
2138

2239
// Custom client that uses default credential provider for testing
2340
class CredentialTestingClient : public Aws::Client::AWSClient
2441
{
2542
public:
2643
explicit CredentialTestingClient(const Aws::Client::ClientConfiguration& configuration)
2744
: AWSClient(configuration,
28-
Aws::MakeShared<Aws::Client::AWSAuthV4Signer>(ALLOCATION_TAG,
29-
Aws::MakeShared<DefaultAWSCredentialsProviderChain>(ALLOCATION_TAG),
45+
Aws::MakeShared<Aws::Client::AWSAuthV4Signer>(TEST_LOG_TAG,
46+
Aws::MakeShared<DefaultAWSCredentialsProviderChain>(TEST_LOG_TAG),
3047
"service", configuration.region),
31-
Aws::MakeShared<MockAWSErrorMarshaller>(ALLOCATION_TAG))
48+
Aws::MakeShared<MockAWSErrorMarshaller>(TEST_LOG_TAG))
49+
{
50+
}
51+
52+
// Constructor with custom credential provider for IMDS test
53+
explicit CredentialTestingClient(const Aws::Client::ClientConfiguration& configuration,
54+
std::shared_ptr<AWSCredentialsProvider> credentialsProvider)
55+
: AWSClient(configuration,
56+
Aws::MakeShared<Aws::Client::AWSAuthV4Signer>(TEST_LOG_TAG,
57+
credentialsProvider,
58+
"service", configuration.region),
59+
Aws::MakeShared<MockAWSErrorMarshaller>(TEST_LOG_TAG))
3260
{
3361
}
3462

@@ -56,8 +84,8 @@ class CredentialTrackingTest : public Aws::Testing::AwsCppSdkGTestSuite
5684

5785
void SetUp() override
5886
{
59-
mockHttpClient = Aws::MakeShared<MockHttpClient>(ALLOCATION_TAG);
60-
mockHttpClientFactory = Aws::MakeShared<MockHttpClientFactory>(ALLOCATION_TAG);
87+
mockHttpClient = Aws::MakeShared<MockHttpClient>(TEST_LOG_TAG);
88+
mockHttpClientFactory = Aws::MakeShared<MockHttpClientFactory>(TEST_LOG_TAG);
6189
mockHttpClientFactory->SetClient(mockHttpClient);
6290
SetHttpClientFactory(mockHttpClientFactory);
6391
}
@@ -70,6 +98,56 @@ class CredentialTrackingTest : public Aws::Testing::AwsCppSdkGTestSuite
7098
Aws::Http::CleanupHttp();
7199
Aws::Http::InitHttp();
72100
}
101+
102+
void RunTestWithCredentialsProvider(const std::shared_ptr<AWSCredentialsProvider>& credentialsProvider, const Aws::String& id) {
103+
// Setup mock response
104+
std::shared_ptr<HttpRequest> requestTmp =
105+
CreateHttpRequest(Aws::Http::URI("dummy"), Aws::Http::HttpMethod::HTTP_POST,
106+
Aws::Utils::Stream::DefaultResponseStreamFactoryMethod);
107+
auto successResponse = Aws::MakeShared<Standard::StandardHttpResponse>(TEST_LOG_TAG, requestTmp);
108+
successResponse->SetResponseCode(HttpResponseCode::OK);
109+
successResponse->GetResponseBody() << "{}";
110+
mockHttpClient->AddResponseToReturn(successResponse);
111+
112+
// Create client configuration
113+
Aws::Client::ClientConfigurationInitValues cfgInit;
114+
cfgInit.shouldDisableIMDS = true;
115+
Aws::Client::ClientConfiguration clientConfig(cfgInit);
116+
clientConfig.region = Aws::Region::US_EAST_1;
117+
118+
// Create credential testing client that uses default provider chain
119+
CredentialTestingClient client(clientConfig, credentialsProvider);
120+
121+
// Create mock request
122+
AmazonWebServiceRequestMock mockRequest;
123+
124+
// Make request
125+
auto outcome = client.MakeRequest(mockRequest);
126+
ASSERT_TRUE(outcome.IsSuccess());
127+
128+
// Verify User-Agent contains environment credentials tracking
129+
auto lastRequest = mockHttpClient->GetMostRecentHttpRequest();
130+
EXPECT_TRUE(lastRequest.HasHeader(Aws::Http::USER_AGENT_HEADER));
131+
const auto& userAgent = lastRequest.GetHeaderValue(Aws::Http::USER_AGENT_HEADER);
132+
EXPECT_FALSE(userAgent.empty());
133+
134+
const auto userAgentParsed = Aws::Utils::StringUtils::Split(userAgent, ' ');
135+
136+
// Verify there's only one m/ section (no duplicate m/ sections)
137+
int mSectionCount = 0;
138+
for (const auto& part : userAgentParsed) {
139+
if (part.find("m/") != Aws::String::npos) {
140+
mSectionCount++;
141+
}
142+
}
143+
EXPECT_EQ(1, mSectionCount);
144+
145+
// Check for environment credentials business metric (g) in user agent
146+
auto businessMetrics = std::find_if(userAgentParsed.begin(), userAgentParsed.end(),
147+
[&id](const Aws::String& value) { return value.find("m/") != Aws::String::npos && value.find(id) != Aws::String::npos; });
148+
149+
EXPECT_TRUE(businessMetrics != userAgentParsed.end());
150+
}
73151
};
74152

75153
TEST_F(CredentialTrackingTest, TestEnvironmentCredentialsTracking)
@@ -78,52 +156,61 @@ TEST_F(CredentialTrackingTest, TestEnvironmentCredentialsTracking)
78156
{"AWS_ACCESS_KEY_ID", "test-access-key"},
79157
{"AWS_SECRET_ACCESS_KEY", "test-secret-key"},
80158
}};
159+
auto credsProvider = Aws::MakeShared<Aws::Auth::EnvironmentAWSCredentialsProvider>(TEST_LOG_TAG);
160+
RunTestWithCredentialsProvider(std::move(credsProvider), "g");
161+
}
81162

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);
163+
TEST_F(CredentialTrackingTest, TestProfileCredentialsTracking)
164+
{
165+
// Create temporary credentials file
166+
Aws::Utils::TempFile credentialsFile(std::ios_base::out | std::ios_base::trunc);
167+
ASSERT_TRUE(credentialsFile.good());
168+
credentialsFile << "[default]" << std::endl;
169+
credentialsFile << "aws_access_key_id = test-profile-access-key" << std::endl;
170+
credentialsFile << "aws_secret_access_key = test-profile-secret-key" << std::endl;
171+
credentialsFile.close();
172+
173+
// Set environment to use our test credentials file
174+
Aws::Environment::EnvironmentRAII testEnvironment{{
175+
{"AWS_SHARED_CREDENTIALS_FILE", credentialsFile.GetFileName().c_str()},
176+
}};
177+
Aws::Config::ReloadCachedCredentialsFile();
123178

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; });
179+
auto credsProvider = Aws::MakeShared<Aws::Auth::ProfileConfigFileAWSCredentialsProvider>(TEST_LOG_TAG);
180+
RunTestWithCredentialsProvider(std::move(credsProvider), "n");
181+
}
127182

128-
EXPECT_TRUE(businessMetrics != userAgentParsed.end());
183+
TEST_F(CredentialTrackingTest, TestProfileProcessCredentialsTracking)
184+
{
185+
// Create temporary config file with credential_process
186+
Aws::Utils::TempFile configFile(std::ios_base::out | std::ios_base::trunc);
187+
ASSERT_TRUE(configFile.good());
188+
configFile << "[default]" << std::endl;
189+
configFile << "credential_process = echo " << WrapEchoStringWithSingleQuoteForUnixShell("{\"Version\": 1, \"AccessKeyId\": \"test-process-key\", \"SecretAccessKey\": \"test-process-secret\"}") << std::endl;
190+
configFile.close();
191+
192+
// Set environment to use our test config file
193+
Aws::Environment::EnvironmentRAII testEnvironment{{
194+
{"AWS_CONFIG_FILE", configFile.GetFileName().c_str()},
195+
}};
196+
197+
// Force reload config file after setting environment variable
198+
Aws::Config::ReloadCachedConfigFile();
199+
200+
auto credsProvider = Aws::MakeShared<Aws::Auth::ProcessCredentialsProvider>(TEST_LOG_TAG);
201+
RunTestWithCredentialsProvider(std::move(credsProvider), "v");
129202
}
203+
204+
TEST_F(CredentialTrackingTest, TestInstanceProfileCredentialsTracking)
205+
{
206+
// Create mock EC2 metadata client with valid credentials
207+
auto mockClient = Aws::MakeShared<MockEC2MetadataClient>(TEST_LOG_TAG);
208+
const char* validCredentials = R"({ "AccessKeyId": "test-imds-access-key", "SecretAccessKey": "test-imds-secret-key", "Token": "test-imds-token", "Code": "Success", "Expiration": "2037-04-19T00:00:00Z" })";
209+
mockClient->SetMockedCredentialsValue(validCredentials);
210+
211+
// Create IMDS credential provider with mock client
212+
auto imdsProvider = Aws::MakeShared<InstanceProfileCredentialsProvider>(TEST_LOG_TAG,
213+
Aws::MakeShared<Aws::Config::EC2InstanceProfileConfigLoader>(TEST_LOG_TAG, mockClient), 1000);
214+
215+
RunTestWithCredentialsProvider(std::move(imdsProvider), "0");
216+
}

0 commit comments

Comments
 (0)