Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -373,9 +373,7 @@ final RegionalAccessBoundary getRegionalAccessBoundary() {
*/
void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessToken token)
throws IOException {
if (!(this instanceof RegionalAccessBoundaryProvider)
|| !RegionalAccessBoundary.isEnabled()
|| !isDefaultUniverseDomain()) {
if (!(this instanceof RegionalAccessBoundaryProvider) || !isDefaultUniverseDomain()) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,18 @@
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpUnsuccessfulResponseHandler;
import com.google.api.client.json.GenericJson;
import com.google.api.client.json.JsonParser;
import com.google.api.client.json.JsonObjectParser;
import com.google.api.client.util.Clock;
import com.google.api.client.util.ExponentialBackOff;
import com.google.api.client.util.Key;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;

/**
* Represents the regional access boundary configuration for a credential. This class holds the
Expand All @@ -67,10 +65,6 @@ final class RegionalAccessBoundary implements Serializable {
static final String X_ALLOWED_LOCATIONS_HEADER_KEY = "x-allowed-locations";
private static final long serialVersionUID = -2428522338274020302L;

// Note: this is for internal testing use use only.
// TODO: Fix unit test mocks so this can be removed
// Refer -> https://github.com/googleapis/google-auth-library-java/issues/1898
Comment thread
vverman marked this conversation as resolved.
static final String ENABLE_EXPERIMENT_ENV_VAR = "GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT";
static final long TTL_MILLIS = 6 * 60 * 60 * 1000L; // 6 hours
static final long REFRESH_THRESHOLD_MILLIS = 1 * 60 * 60 * 1000L; // 1 hour

Expand All @@ -79,8 +73,6 @@ final class RegionalAccessBoundary implements Serializable {
private final long refreshTime;
private transient Clock clock;

private static EnvironmentProvider environmentProvider = SystemEnvironmentProvider.getInstance();

/**
* Creates a new RegionalAccessBoundary instance.
*
Expand Down Expand Up @@ -172,30 +164,6 @@ public String toString() {
}
}

@VisibleForTesting
static void setEnvironmentProviderForTest(@Nullable EnvironmentProvider provider) {
Comment thread
vverman marked this conversation as resolved.
environmentProvider = provider == null ? SystemEnvironmentProvider.getInstance() : provider;
}

/**
* Checks if the regional access boundary feature is enabled. The feature is enabled if the
* environment variable or system property "GOOGLE_AUTH_TRUST_BOUNDARY_ENABLE_EXPERIMENT" is set
* to "true" or "1" (case-insensitive).
*
* @return True if the regional access boundary feature is enabled, false otherwise.
*/
static boolean isEnabled() {
String enabled = environmentProvider.getEnv(ENABLE_EXPERIMENT_ENV_VAR);
if (enabled == null) {
enabled = System.getProperty(ENABLE_EXPERIMENT_ENV_VAR);
}
if (enabled == null) {
return false;
}
String lowercased = enabled.toLowerCase();
return "true".equals(lowercased) || "1".equals(enabled);
}

/**
* Refreshes the regional access boundary by making a network call to the lookup endpoint.
*
Expand Down Expand Up @@ -223,6 +191,8 @@ static RegionalAccessBoundary refresh(

HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory();
HttpRequest request = requestFactory.buildGetRequest(new GenericUrl(url));
// Disable automatic logging by google-http-java-client to prevent leakage of sensitive tokens.
request.setLoggingEnabled(false);
request.getHeaders().setAuthorization("Bearer " + accessToken.getTokenValue());

// Add retry logic
Expand All @@ -249,15 +219,20 @@ static RegionalAccessBoundary refresh(
HttpIOExceptionHandler ioExceptionHandler = new HttpBackOffIOExceptionHandler(backoff);
request.setIOExceptionHandler(ioExceptionHandler);

request.setParser(new JsonObjectParser(OAuth2Utils.JSON_FACTORY));

RegionalAccessBoundaryResponse json;
HttpResponse response = null;
try {
HttpResponse response = request.execute();
String responseString = response.parseAsString();
JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(responseString);
json = parser.parseAndClose(RegionalAccessBoundaryResponse.class);
response = request.execute();
json = response.parseAs(RegionalAccessBoundaryResponse.class);
} catch (IOException e) {
throw new IOException(
"RegionalAccessBoundary: Failure while getting regional access boundaries:", e);
} finally {
if (response != null) {
response.disconnect();
}
}
String encodedLocations = json.getEncodedLocations();
// The encodedLocations is the value attached to the x-allowed-locations header, and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,18 @@

package com.google.auth.oauth2;

import static com.google.auth.oauth2.LoggingUtils.log;

import com.google.api.client.util.Clock;
import com.google.api.core.InternalApi;
import com.google.auth.http.HttpTransportFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.SettableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import javax.annotation.Nullable;
Expand All @@ -59,7 +66,7 @@ final class RegionalAccessBoundaryManager {
* The default maximum elapsed time in milliseconds for retrying Regional Access Boundary lookup
* requests.
*/
private static final int DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS = 60000;
static final int DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS = 60000;

/**
* cachedRAB uses AtomicReference to provide thread-safe, lock-free access to the cached data for
Expand All @@ -78,22 +85,62 @@ final class RegionalAccessBoundaryManager {
private final AtomicReference<CooldownState> cooldownState =
new AtomicReference<>(new CooldownState(0, INITIAL_COOLDOWN_MILLIS));

// 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
// for easier debugging.
private static final AtomicInteger threadCount = new AtomicInteger(0);

// Bounded executor service ensures hard limits on concurrent refresh tasks and queued tasks
// to avoid resource exhaustion.
private static final int EXECUTOR_POOL_SIZE = 5;
private static final int EXECUTOR_QUEUE_CAPACITY = 100;

private static final ExecutorService DEFAULT_SHARED_EXECUTOR;

static {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(
EXECUTOR_POOL_SIZE, // corePoolSize: threads to keep alive
EXECUTOR_POOL_SIZE, // maximumPoolSize: max threads allowed
1, // keepAliveTime: time to wait before terminating idle threads
TimeUnit.HOURS, // unit for keepAliveTime
new LinkedBlockingQueue<>(EXECUTOR_QUEUE_CAPACITY), // work queue with bound
r -> {
Thread t = new Thread(r, "RAB-refresh-" + threadCount.getAndIncrement());
t.setDaemon(true);
return t;
});
// Allow core threads to time out so the executor can shrink to 0 when idle.
// Ensures threads are released when idle to avoid unnecessary resource usage.
executor.allowCoreThreadTimeOut(true);
Comment thread
vverman marked this conversation as resolved.
DEFAULT_SHARED_EXECUTOR = executor;
}

private final transient Clock clock;
private final int maxRetryElapsedTimeMillis;
private final ExecutorService executor;

/**
* Creates a new RegionalAccessBoundaryManager with the default retry timeout of 60 seconds.
*
* @param clock The clock to use for cooldown and expiration checks.
*/
RegionalAccessBoundaryManager(Clock clock) {
this(clock, DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS);
this(clock, DEFAULT_MAX_RETRY_ELAPSED_TIME_MILLIS, DEFAULT_SHARED_EXECUTOR);
}

@VisibleForTesting
RegionalAccessBoundaryManager(Clock clock, int maxRetryElapsedTimeMillis) {
this(clock, maxRetryElapsedTimeMillis, DEFAULT_SHARED_EXECUTOR);
}

@VisibleForTesting
RegionalAccessBoundaryManager(
Clock clock, int maxRetryElapsedTimeMillis, ExecutorService executor) {
this.clock = clock != null ? clock : Clock.SYSTEM;
this.maxRetryElapsedTimeMillis = maxRetryElapsedTimeMillis;
this.executor = executor;
}

/**
Expand All @@ -111,6 +158,11 @@ RegionalAccessBoundary getCachedRAB() {
return null;
}

@VisibleForTesting
void setCachedRAB(RegionalAccessBoundary rab) {
this.cachedRAB.set(rab);
}

/**
* Triggers an asynchronous refresh of the RegionalAccessBoundary if it is not already being
* refreshed and if the cooldown period is not active.
Expand Down Expand Up @@ -161,19 +213,17 @@ void triggerAsyncRefresh(
};

try {
// We use new Thread() here instead of
// CompletableFuture.runAsync() (which uses ForkJoinPool.commonPool()).
// This avoids consuming CPU resources since
// The common pool has a small, fixed number of threads designed for
// CPU-bound tasks.
Thread refreshThread = new Thread(refreshTask, "RAB-refresh-thread");
refreshThread.setDaemon(true);
refreshThread.start();
this.executor.submit(refreshTask);
} catch (Exception | Error e) {
// If scheduling fails (e.g., RejectedExecutionException, OutOfMemoryError for threads),
// the task's finally block will never execute. We must release the lock here.
handleRefreshFailure(
new Exception("Regional Access Boundary background refresh failed to schedule", e));
log(
LOGGER_PROVIDER,
Level.FINE,
null,
"Could not submit background refresh task for Regional Access Boundary. "
+ "This is non-blocking and the library will attempt to refresh on the next access. Error: "
+ e.getMessage());
future.setException(e);
refreshFuture.set(null);
}
Expand Down Expand Up @@ -201,13 +251,13 @@ private void handleRefreshFailure(Exception e) {
// concurrent failures from logging redundant messages or incorrectly calculating
// the exponential backoff.
if (cooldownState.compareAndSet(currentCooldownState, next)) {
LoggingUtils.log(
log(
LOGGER_PROVIDER,
Level.FINE,
null,
"Regional Access Boundary lookup failed; entering cooldown for "
"Regional Access Boundary lookup was not successful; will retry after a cooldown of "
+ (next.durationMillis / 60000)
+ "m. Error: "
+ "m. This is handled automatically. Details: "
+ e.getMessage());
}
}
Expand Down
Loading
Loading