Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

/**
* OAuth2 credentials representing the built-in service account for a Google Compute Engine VM.
Expand Down Expand Up @@ -117,6 +118,7 @@ public class ComputeEngineCredentials extends GoogleCredentials

private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. ";
private static final String PARSE_ERROR_ACCOUNT = "Error parsing service account response. ";
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@]+@[^@]+\\.[^@]+$");
private static final long serialVersionUID = -4113476462526554235L;

private final String transportFactoryClassName;
Expand Down Expand Up @@ -800,8 +802,20 @@ public String getAccount() {
@InternalApi
@Override
public String getRegionalAccessBoundaryUrl() throws IOException {
String account = getAccount();
// In GKE environments, the default service account might return a non-email placeholder.
// Since RAB lookup requires a valid email-based service account, we skip RAB lookup
// in non-email scenarios by returning null.
if (account == null || !EMAIL_PATTERN.matcher(account).matches()) {
LoggingUtils.log(
LOGGER_PROVIDER,
Level.INFO,
Collections.emptyMap(),
"Regional Access Boundary lookup is skipped for this instance because it is a non-email instance.");
return null;
}
return String.format(
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT, getAccount());
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT, account);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
Expand Down Expand Up @@ -85,6 +86,8 @@ final class RegionalAccessBoundaryManager {
private final AtomicReference<CooldownState> cooldownState =
new AtomicReference<>(new CooldownState(0, INITIAL_COOLDOWN_MILLIS));

private final AtomicBoolean skipRAB = new AtomicBoolean(false);

// Unbounded thread creation is discouraged in library code to avoid resource
// exhaustion. A shared, bounded executor service ensures a hard limit (5)
// on concurrent refresh tasks, while threadCount provides unique names
Expand Down Expand Up @@ -178,7 +181,7 @@ void triggerAsyncRefresh(
final HttpTransportFactory transportFactory,
final RegionalAccessBoundaryProvider provider,
final AccessToken accessToken) {
if (isCooldownActive()) {
if (skipRAB.get() || isCooldownActive()) {
return;
}

Expand All @@ -196,6 +199,11 @@ void triggerAsyncRefresh(
() -> {
try {
String url = provider.getRegionalAccessBoundaryUrl();
if (url == null) {
skipRAB.set(true);
future.set(null);
return;
}
Comment on lines +202 to +206
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

qq, can you remind me again why we need to do future.set(null);?

RegionalAccessBoundary newRAB =
RegionalAccessBoundary.refresh(
transportFactory, url, accessToken, clock, maxRetryElapsedTimeMillis);
Expand Down Expand Up @@ -279,6 +287,11 @@ long getCurrentCooldownMillis() {
return cooldownState.get().durationMillis;
}

@VisibleForTesting
boolean isSkipRAB() {
return skipRAB.get();
}

private static class CooldownState {
/** The time (in milliseconds from epoch) when the current cooldown period expires. */
final long expiryTime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1213,7 +1213,7 @@ void getProjectId_explicitSet_noMDsCall() {
assertEquals(0, transportFactory.transport.getRequestCount());
}

@org.junit.jupiter.api.Test
@Test
void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedException {

String defaultAccountEmail = "default@email.com";
Expand Down Expand Up @@ -1242,6 +1242,76 @@ void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedExce
Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION));
}

@Test
void refresh_regionalAccessBoundaryNonEmail_skipsRABLookup()
throws IOException, InterruptedException {
String nonEmailAccount = "non-email-account-value";
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
RegionalAccessBoundary regionalAccessBoundary =
new RegionalAccessBoundary(
TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION,
TestUtils.REGIONAL_ACCESS_BOUNDARY_LOCATIONS,
null);
transportFactory.transport.setRegionalAccessBoundary(regionalAccessBoundary);
transportFactory.transport.setServiceAccountEmail(nonEmailAccount);

ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();

// Before any call, skipRAB flag should be false
assertFalse(credentials.regionalAccessBoundaryManager.isSkipRAB());

// First call: triggers lookup which determines non-email, returns null, and sets skipRAB to
// true
Map<String, List<String>> headers = credentials.getRequestMetadata();
assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY));

// Since the task is scheduled asynchronously on the shared executor, wait for it to complete
long deadline = System.currentTimeMillis() + 5000;
while (!credentials.regionalAccessBoundaryManager.isSkipRAB()
&& System.currentTimeMillis() < deadline) {
Thread.sleep(50);
}

// Verify skipRAB flag has been set to true
assertTrue(credentials.regionalAccessBoundaryManager.isSkipRAB());

// Verify RAB is still null
assertNull(credentials.getRegionalAccessBoundary());

// Second call: should bypass triggerAsyncRefresh completely and remain null
headers = credentials.getRequestMetadata();
assertNull(headers.get(X_ALLOWED_LOCATIONS_HEADER_KEY));
}

@Test
void getRegionalAccessBoundaryUrl_validEmail_returnsUrl() throws IOException {
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
String defaultAccountEmail = "mail@mail.com";

transportFactory.transport.setServiceAccountEmail(defaultAccountEmail);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();

String expectedUrl =
String.format(
OAuth2Utils.IAM_CREDENTIALS_ALLOWED_LOCATIONS_URL_FORMAT_SERVICE_ACCOUNT,
defaultAccountEmail);
assertEquals(expectedUrl, credentials.getRegionalAccessBoundaryUrl());
}

@Test
void getRegionalAccessBoundaryUrl_invalidEmail_returnsNull() throws IOException {
MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory();
String defaultAccountEmail = "default"; // non-email account format

transportFactory.transport.setServiceAccountEmail(defaultAccountEmail);
ComputeEngineCredentials credentials =
ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build();

assertNull(credentials.getRegionalAccessBoundaryUrl());
}

private void waitForRegionalAccessBoundary(GoogleCredentials credentials)
throws InterruptedException {
long deadline = System.currentTimeMillis() + 5000;
Expand Down
Loading