Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-45bc696.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "AWS SDK for Java v2",
"contributor": "",
"description": "Implemented the CREDENTIALS_IMDS business metric feature that tracks when Instance Metadata Service (IMDS) credentials are being used in AWS SDK requests."
}
Comment thread
L-Applin marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.auth.credentials;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.any;
import static com.github.tomakehurst.wiremock.client.WireMock.matching;
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;

import com.github.tomakehurst.wiremock.junit.WireMockRule;
import java.util.Optional;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId;

/**
* Integration test that verifies CREDENTIALS_IMDS business metric appears in User-Agent headers.
*/
public class ImdsUserAgentIntegrationTest {

@Rule
public WireMockRule mockServer = new WireMockRule(0);

@Before
public void setup() {
stubFor(any(urlEqualTo("/"))
.willReturn(aResponse()
.withStatus(200)
.withBody("{\"message\": \"success\"}")));
}

@Test
public void imdsCredentials_shouldAppearInUserAgentHeader() {
AwsCredentials imdsCredentials = createImdsCredentials();
makeHttpRequestWithCredentials(imdsCredentials);

// Verify that the User-Agent header contains "m/0"
verify(postRequestedFor(urlEqualTo("/"))
.withHeader("User-Agent", matching(".*m/0.*")));
}

@Test
public void staticCredentials_shouldNotAppearInUserAgentHeader() {
AwsCredentials staticCredentials = AwsBasicCredentials.create("test-key", "test-secret");
makeHttpRequestWithCredentials(staticCredentials);

// Verify that the User-Agent header does not contain "m/0"
verify(postRequestedFor(urlEqualTo("/"))
.withHeader("User-Agent",
matching("^(?!.*m/0).*$")));
}

private void makeHttpRequestWithCredentials(AwsCredentials credentials) {
String userAgent = buildUserAgentWithCredentials(credentials);

System.out.println("Making HTTP request with User-Agent: " + userAgent);

// Make HTTP request with the User-Agent using simple HTTP connection
try {
java.net.URL url = new java.net.URL("http://localhost:" + mockServer.port() + "/");
Comment thread
L-Applin marked this conversation as resolved.
Outdated
java.net.HttpURLConnection connection = (java.net.HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("User-Agent", userAgent);
connection.setRequestProperty("Content-Type", "application/json");
connection.setDoOutput(true);

// Write request body
try (java.io.OutputStream os = connection.getOutputStream()) {
byte[] input = "{}".getBytes("utf-8");
os.write(input, 0, input.length);
}

int responseCode = connection.getResponseCode();

connection.disconnect();
} catch (Exception e) {
e.printStackTrace();
}
}

private String buildUserAgentWithCredentials(AwsCredentials credentials) {
String baseUserAgent = "aws-sdk-java/2.32.32 Linux/5.4.0 Java_HotSpot(TM)_64-Bit_Server_VM/17.0.1";

// Check if credentials are IMDS
boolean isImds = credentials.providerName()
.map(name -> name.contains("InstanceProfile"))
.orElse(false);

if (isImds) {
// Add business metrics in the format - "m/0"
return baseUserAgent + " m/" + BusinessMetricFeatureId.CREDENTIALS_IMDS.value();
} else {
return baseUserAgent;
}
}

private AwsCredentials createImdsCredentials() {
AwsCredentials baseCredentials = AwsBasicCredentials.create("test-access-key", "test-secret-key");
return new AwsCredentials() {
@Override
public String accessKeyId() {
return baseCredentials.accessKeyId();
}

@Override
public String secretAccessKey() {
return baseCredentials.secretAccessKey();
}

@Override
public Optional<String> providerName() {
return Optional.of("InstanceProfileCredentialsProvider");
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.auth.credentials.internal;

import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute;
import software.amazon.awssdk.core.SdkRequest;
import software.amazon.awssdk.core.interceptor.Context;
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute;
import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId;

/**
* Interceptor that adds the CREDENTIALS_IMDS business metric when IMDS credentials are being used.
*/
@SdkInternalApi
public final class ImdsCredentialsBusinessMetricInterceptor implements ExecutionInterceptor {

@Override
public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttributes executionAttributes) {
AwsCredentials credentials = executionAttributes.getAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS);

if (credentials != null && isImdsCredentials(credentials)) {
executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any chances getAttribute could return null?

.addMetric(BusinessMetricFeatureId.CREDENTIALS_IMDS.value());
}

return context.request();
}

private boolean isImdsCredentials(AwsCredentials credentials) {
return credentials.providerName()
.map(name -> name.contains("InstanceProfile"))
Comment thread
L-Applin marked this conversation as resolved.
Outdated
.orElse(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
software.amazon.awssdk.auth.credentials.internal.ImdsCredentialsBusinessMetricInterceptor
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.awssdk.auth.credentials;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.auth.credentials.internal.ImdsCredentialsBusinessMetricInterceptor;
import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute;
import software.amazon.awssdk.core.SdkRequest;
import software.amazon.awssdk.core.interceptor.Context;
import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute;
import software.amazon.awssdk.core.useragent.BusinessMetricCollection;
import software.amazon.awssdk.core.useragent.BusinessMetricFeatureId;

public class ImdsCredentialsInUserAgentTest {

private ImdsCredentialsBusinessMetricInterceptor interceptor;
private ExecutionAttributes executionAttributes;
private BusinessMetricCollection businessMetrics;
private Context.ModifyRequest context;

@BeforeEach
void setUp() {
interceptor = new ImdsCredentialsBusinessMetricInterceptor();
executionAttributes = new ExecutionAttributes();
businessMetrics = new BusinessMetricCollection();
executionAttributes.putAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS, businessMetrics);

context = mock(Context.ModifyRequest.class);
SdkRequest request = mock(SdkRequest.class);
when(context.request()).thenReturn(request);
}

@Test
public void imdsCredentials_shouldHaveImdsCredentialsBusinessMetric() {
// Create credentials with IMDS provider name
AwsCredentials imdsCredentials = createCredentialsWithProviderName("InstanceProfileCredentialsProvider");
executionAttributes.putAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS, imdsCredentials);

interceptor.modifyRequest(context, executionAttributes);

// Verify that the CREDENTIALS_IMDS metric is added to business metrics
assertThat(businessMetrics.recordedMetrics())
.contains(BusinessMetricFeatureId.CREDENTIALS_IMDS.value());

// Verify that the metric appears in the user agent string
String userAgentString = businessMetrics.asBoundedString();
assertThat(userAgentString).contains(BusinessMetricFeatureId.CREDENTIALS_IMDS.value());
}

@Test
public void containerCredentials_shouldNotHaveImdsCredentialsBusinessMetric() {
// Test with "ContainerCredentialsProvider" provider name - should not be considered IMDS
AwsCredentials containerCredentials = createCredentialsWithProviderName("ContainerCredentialsProvider");
executionAttributes.putAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS, containerCredentials);

interceptor.modifyRequest(context, executionAttributes);

assertThat(businessMetrics.recordedMetrics())
.doesNotContain(BusinessMetricFeatureId.CREDENTIALS_IMDS.value());
}

@Test
public void credentialsWithoutProviderName_shouldNotHaveImdsCredentialsBusinessMetric() {
// Test with credentials that don't have a provider name
AwsCredentials credentialsWithoutProviderName = AwsBasicCredentials.create("accessKey", "secretKey");
executionAttributes.putAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS, credentialsWithoutProviderName);

interceptor.modifyRequest(context, executionAttributes);

assertThat(businessMetrics.recordedMetrics())
.doesNotContain(BusinessMetricFeatureId.CREDENTIALS_IMDS.value());
}

private AwsCredentials createCredentialsWithProviderName(String providerName) {
AwsCredentials baseCredentials = AwsBasicCredentials.create("test-access-key", "test-secret-key");
return new AwsCredentials() {
@Override
public String accessKeyId() {
return baseCredentials.accessKeyId();
}

@Override
public String secretAccessKey() {
return baseCredentials.secretAccessKey();
}

@Override
public Optional<String> providerName() {
return Optional.of(providerName);
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public enum BusinessMetricFeatureId {
RESOLVED_ACCOUNT_ID("T"),
DDB_MAPPER("d"),
BEARER_SERVICE_ENV_VARS("3"),
CREDENTIALS_IMDS("0"),
UNKNOWN("Unknown");

private static final Map<String, BusinessMetricFeatureId> VALUE_MAP =
Expand Down
Loading