diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java index bcfe916c3168..eff14ea47b40 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java @@ -800,8 +800,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 || !account.contains("@")) { + 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); } /** diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java index e35efe86f7a0..683015614782 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/RegionalAccessBoundaryManager.java @@ -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; @@ -85,6 +86,8 @@ final class RegionalAccessBoundaryManager { private final AtomicReference 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 @@ -178,7 +181,7 @@ void triggerAsyncRefresh( final HttpTransportFactory transportFactory, final RegionalAccessBoundaryProvider provider, final AccessToken accessToken) { - if (isCooldownActive()) { + if (skipRAB.get() || isCooldownActive()) { return; } @@ -196,6 +199,11 @@ void triggerAsyncRefresh( () -> { try { String url = provider.getRegionalAccessBoundaryUrl(); + if (url == null) { + skipRAB.set(true); + future.set(null); + return; + } RegionalAccessBoundary newRAB = RegionalAccessBoundary.refresh( transportFactory, url, accessToken, clock, maxRetryElapsedTimeMillis); @@ -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; diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java index 78bfd5ddaaa4..31eba072c2c5 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java @@ -1242,6 +1242,48 @@ void refresh_regionalAccessBoundarySuccess() throws IOException, InterruptedExce Arrays.asList(TestUtils.REGIONAL_ACCESS_BOUNDARY_ENCODED_LOCATION)); } + @org.junit.jupiter.api.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> 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)); + } + private void waitForRegionalAccessBoundary(GoogleCredentials credentials) throws InterruptedException { long deadline = System.currentTimeMillis() + 5000;