Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 7 additions & 3 deletions gcp-auth-extension/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ dependencies {
tasks {
test {
useJUnitPlatform()
// Unset relevant environment variables to provide a clean state for the tests
environment("GOOGLE_CLOUD_PROJECT", "")
environment("GOOGLE_CLOUD_QUOTA_PROJECT", "")
// exclude integration test
exclude("io/opentelemetry/contrib/gcp/auth/GcpAuthExtensionEndToEndTest.class")
}
Expand Down Expand Up @@ -103,15 +106,15 @@ tasks.register<Copy>("copyAgent") {
})
}

tasks.register<Test>("IntegrationTest") {
tasks.register<Test>("IntegrationTestUserCreds") {
dependsOn(tasks.shadowJar)
dependsOn(tasks.named("copyAgent"))

useJUnitPlatform()
// include only the integration test file
include("io/opentelemetry/contrib/gcp/auth/GcpAuthExtensionEndToEndTest.class")

val fakeCredsFilePath = project.file("src/test/resources/fakecreds.json").absolutePath
val fakeCredsFilePath = project.file("src/test/resources/fake_user_creds.json").absolutePath

environment("GOOGLE_CLOUD_QUOTA_PROJECT", "quota-project-id")
environment("GOOGLE_APPLICATION_CREDENTIALS", fakeCredsFilePath)
Expand All @@ -127,6 +130,7 @@ tasks.register<Test>("IntegrationTest") {
"-Dotel.metrics.exporter=none",
"-Dotel.logs.exporter=none",
"-Dotel.exporter.otlp.protocol=http/protobuf",
"-Dmockserver.logLevel=off"
"-Dotel.javaagent.debug=false",
"-Dmockserver.logLevel=trace"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import java.util.Locale;
import java.util.Optional;
import java.util.function.Supplier;

/**
Expand Down Expand Up @@ -101,4 +102,20 @@ String getConfiguredValueWithFallback(Supplier<String> fallback) {
return fallback.get();
}
}

/**
* Retrieves the value for this option, prioritizing environment variables before system
* properties. If neither an environment variable nor a system property is set for this option,
* then an empty {@link Optional} is returned.
*
* @return The configured value for the option, if set, obtained from the environment variable,
* system property, or empty {@link Optional}, in that order of precedence.
*/
Optional<String> getConfiguredValueAsOptional() {
try {
return Optional.of(this.getConfiguredValue());
} catch (ConfigurationException e) {
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
* An AutoConfigurationCustomizerProvider for Google Cloud Platform (GCP) OpenTelemetry (OTLP)
Expand All @@ -40,7 +43,7 @@
public class GcpAuthAutoConfigurationCustomizerProvider
implements AutoConfigurationCustomizerProvider {

static final String QUOTA_USER_PROJECT_HEADER = "X-Goog-User-Project";
static final String QUOTA_USER_PROJECT_HEADER = "x-goog-user-project";
static final String GCP_USER_PROJECT_ID_KEY = "gcp.project_id";

/**
Expand Down Expand Up @@ -95,20 +98,34 @@ private static SpanExporter addAuthorizationHeaders(
}

private static Map<String, String> getRequiredHeaderMap(GoogleCredentials credentials) {
Map<String, String> gcpHeaders = new HashMap<>();
Map<String, List<String>> gcpHeaders;
try {
credentials.refreshIfExpired();
// this also refreshes the credentials, if required
gcpHeaders = credentials.getRequestMetadata();
} catch (IOException e) {
throw new GoogleAuthException(Reason.FAILED_ADC_REFRESH, e);
}
gcpHeaders.put("Authorization", "Bearer " + credentials.getAccessToken().getTokenValue());
String configuredQuotaProjectId =
ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getConfiguredValueWithFallback(
credentials::getQuotaProjectId);
if (configuredQuotaProjectId != null && !configuredQuotaProjectId.isEmpty()) {
gcpHeaders.put(QUOTA_USER_PROJECT_HEADER, configuredQuotaProjectId);
// flatten list
Map<String, String> flattenedHeaders =
gcpHeaders.entrySet().stream()
.collect(
Collectors.toMap(
Map.Entry::getKey,
entry ->
entry.getValue().stream()
.filter(Objects::nonNull) // Filter nulls
.filter(s -> !s.isEmpty()) // Filter empty strings
.collect(Collectors.joining(","))));
// Add quota user project header if not detected by the auth library and user provided it via
// system properties.
if (!flattenedHeaders.containsKey(QUOTA_USER_PROJECT_HEADER)) {
Optional<String> maybeConfiguredQuotaProjectId =
ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getConfiguredValueAsOptional();
maybeConfiguredQuotaProjectId.ifPresent(
configuredQuotaProjectId ->
flattenedHeaders.put(QUOTA_USER_PROJECT_HEADER, configuredQuotaProjectId));
}
return gcpHeaders;
return flattenedHeaders;
}

// Updates the current resource with the attributes required for ingesting OTLP data on GCP.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.AbstractMap.SimpleEntry;
Expand Down Expand Up @@ -214,26 +215,33 @@ public void testCustomizerFailWithMissingResourceProject() {
@ParameterizedTest
@MethodSource("provideQuotaBehaviorTestCases")
@SuppressWarnings("CannotMockMethod")
public void testQuotaProjectBehavior(QuotaProjectIdTestBehavior testCase) {
public void testQuotaProjectBehavior(QuotaProjectIdTestBehavior testCase) throws IOException {
// Set resource project system property
System.setProperty(
ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID);
// Configure mock credentials to return fake access token
Mockito.when(mockedGoogleCredentials.getAccessToken())
.thenReturn(new AccessToken("fake", Date.from(Instant.now())));

// To prevent unncecessary stubbings, mock getQuotaProjectId only when necessary
if (testCase.getUserSpecifiedQuotaProjectId() == null
|| testCase.getUserSpecifiedQuotaProjectId().isEmpty()) {
String quotaProjectFromCredential =
testCase.getIsQuotaProjectPresentInCredentials() ? DUMMY_GCP_QUOTA_PROJECT_ID : null;
Mockito.when(mockedGoogleCredentials.getQuotaProjectId())
.thenReturn(quotaProjectFromCredential);

// Prepare request metadata
AccessToken fakeAccessToken = new AccessToken("fake", Date.from(Instant.now()));
ImmutableMap<String, List<String>> mockedRequestMetadata;
if (testCase.getIsQuotaProjectPresentInMetadata()) {
mockedRequestMetadata =
ImmutableMap.of(
"Authorization",
Collections.singletonList("Bearer " + fakeAccessToken.getTokenValue()),
QUOTA_USER_PROJECT_HEADER,
Collections.singletonList(DUMMY_GCP_QUOTA_PROJECT_ID));
} else {
mockedRequestMetadata =
ImmutableMap.of(
"Authorization",
Collections.singletonList("Bearer " + fakeAccessToken.getTokenValue()));
}
// mock credentials to return the prepared request metadata
Mockito.when(mockedGoogleCredentials.getRequestMetadata()).thenReturn(mockedRequestMetadata);

// configure environment according to test case
String quotaProjectId = testCase.getUserSpecifiedQuotaProjectId(); // maybe empty string
if (testCase.getUserSpecifiedQuotaProjectId() != null) {
if (quotaProjectId != null) {
// user specified a quota project id
System.setProperty(
ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getSystemProperty(), quotaProjectId);
Expand Down Expand Up @@ -288,58 +296,62 @@ public void testQuotaProjectBehavior(QuotaProjectIdTestBehavior testCase) {
* indicates the expectation that the QUOTA_USER_PROJECT_HEADER should not be present in the
* export headers.
*
* <p>{@code true} for {@link QuotaProjectIdTestBehavior#getIsQuotaProjectPresentInCredentials()}
* <p>{@code true} for {@link QuotaProjectIdTestBehavior#getIsQuotaProjectPresentInMetadata()}
* indicates that the mocked credentials are configured to provide DUMMY_GCP_QUOTA_PROJECT_ID as
* the quota project ID.
*/
private static Stream<Arguments> provideQuotaBehaviorTestCases() {
return Stream.of(
// If quota project present in metadata, it will be used
Arguments.of(
QuotaProjectIdTestBehavior.builder()
.setUserSpecifiedQuotaProjectId(DUMMY_GCP_QUOTA_PROJECT_ID)
.setIsQuotaProjectPresentInCredentials(true)
.setIsQuotaProjectPresentInMetadata(true)
.setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
.build()),
Arguments.of(
QuotaProjectIdTestBehavior.builder()
.setUserSpecifiedQuotaProjectId(DUMMY_GCP_QUOTA_PROJECT_ID)
.setIsQuotaProjectPresentInCredentials(false)
.setUserSpecifiedQuotaProjectId("my-custom-quota-project-id")
.setIsQuotaProjectPresentInMetadata(true)
.setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
.build()),
// If quota project not present in request metadata, then user specified project is used
Arguments.of(
QuotaProjectIdTestBehavior.builder()
.setUserSpecifiedQuotaProjectId("my-custom-quota-project-id")
.setIsQuotaProjectPresentInCredentials(true)
.setExpectedQuotaProjectInHeader("my-custom-quota-project-id")
.setUserSpecifiedQuotaProjectId(DUMMY_GCP_QUOTA_PROJECT_ID)
.setIsQuotaProjectPresentInMetadata(false)
.setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
.build()),
Arguments.of(
QuotaProjectIdTestBehavior.builder()
.setUserSpecifiedQuotaProjectId("my-custom-quota-project-id")
.setIsQuotaProjectPresentInCredentials(false)
.setIsQuotaProjectPresentInMetadata(false)
.setExpectedQuotaProjectInHeader("my-custom-quota-project-id")
.build()),
// Testing for special edge case inputs
// user-specified quota project is empty
Arguments.of(
QuotaProjectIdTestBehavior.builder()
.setUserSpecifiedQuotaProjectId("") // user explicitly specifies empty
.setIsQuotaProjectPresentInCredentials(true)
.setIsQuotaProjectPresentInMetadata(true)
.setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
.build()),
Arguments.of(
QuotaProjectIdTestBehavior.builder()
.setUserSpecifiedQuotaProjectId(null) // user omits specifying quota project
.setIsQuotaProjectPresentInCredentials(true)
.setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
.setUserSpecifiedQuotaProjectId("")
.setIsQuotaProjectPresentInMetadata(false)
.setExpectedQuotaProjectInHeader(null)
.build()),
Arguments.of(
QuotaProjectIdTestBehavior.builder()
.setUserSpecifiedQuotaProjectId("")
.setIsQuotaProjectPresentInCredentials(false)
.setExpectedQuotaProjectInHeader(null)
.setUserSpecifiedQuotaProjectId(null) // user omits specifying quota project
.setIsQuotaProjectPresentInMetadata(true)
.setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
.build()),
Arguments.of(
QuotaProjectIdTestBehavior.builder()
.setUserSpecifiedQuotaProjectId(null)
.setIsQuotaProjectPresentInCredentials(false)
.setIsQuotaProjectPresentInMetadata(false)
.setExpectedQuotaProjectInHeader(null)
.build()));
}
Expand Down Expand Up @@ -367,7 +379,7 @@ abstract static class QuotaProjectIdTestBehavior {
@Nullable
abstract String getUserSpecifiedQuotaProjectId();

abstract boolean getIsQuotaProjectPresentInCredentials();
abstract boolean getIsQuotaProjectPresentInMetadata();

// If expected quota project in header is null, the header entry should not be present in export
@Nullable
Expand All @@ -382,9 +394,15 @@ static Builder builder() {
abstract static class Builder {
abstract Builder setUserSpecifiedQuotaProjectId(String quotaProjectId);

abstract Builder setIsQuotaProjectPresentInCredentials(
boolean quotaProjectPresentInCredentials);
abstract Builder setIsQuotaProjectPresentInMetadata(boolean quotaProjectPresentInMetadata);

/**
* Sets the expected quota project header value for the test case. A null value is allowed,
* and it indicates that the header should not be present in the export request.
*
* @param expectedQuotaProjectInHeader the expected header value to match in the export
* headers.
*/
abstract Builder setExpectedQuotaProjectInHeader(String expectedQuotaProjectInHeader);

abstract QuotaProjectIdTestBehavior build();
Expand All @@ -393,10 +411,18 @@ abstract Builder setIsQuotaProjectPresentInCredentials(

@SuppressWarnings("CannotMockMethod")
private void prepareMockBehaviorForGoogleCredentials() {
Mockito.when(mockedGoogleCredentials.getQuotaProjectId())
.thenReturn(DUMMY_GCP_QUOTA_PROJECT_ID);
Mockito.when(mockedGoogleCredentials.getAccessToken())
.thenReturn(new AccessToken("fake", Date.from(Instant.now())));
AccessToken fakeAccessToken = new AccessToken("fake", Date.from(Instant.now()));
try {
Mockito.when(mockedGoogleCredentials.getRequestMetadata())
.thenReturn(
ImmutableMap.of(
"Authorization",
Collections.singletonList("Bearer " + fakeAccessToken.getTokenValue()),
QUOTA_USER_PROJECT_HEADER,
Collections.singletonList(DUMMY_GCP_QUOTA_PROJECT_ID)));
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(SpanExporter spanExporter) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,9 @@ public static void setup() throws NoSuchAlgorithmException, KeyManagementExcepti
// Set up mock OTLP backend server to which traces will be exported
backendServer = ClientAndServer.startClientAndServer(EXPORTER_ENDPOINT_PORT);
backendServer.when(request()).respond(response().withStatusCode(200));

// Set up the mock gcp metadata server to provide fake credentials
String accessTokenResponse =
"{\"access_token\": \"fake.access_token\",\"expires_in\": 3600, \"token_type\": \"Bearer\"}";

mockGcpOAuth2Server = ClientAndServer.startClientAndServer(MOCK_GCP_OAUTH2_PORT);

MockServerClient mockServerClient =
Expand Down
7 changes: 7 additions & 0 deletions gcp-auth-extension/src/test/resources/fake_user_creds.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"client_id": "....apps.googleusercontent.com",
"client_secret": "...",
"refresh_token": "1//...",
"quota_project_id": "your-configured-quota-project",
"type": "authorized_user"
}
13 changes: 0 additions & 13 deletions gcp-auth-extension/src/test/resources/fakecreds.json

This file was deleted.

Loading