Skip to content

feat: Regional Access Boundary Changes#891

Open
vverman wants to merge 9 commits intogoogleapis:trust_boundaryfrom
vverman:trust-boundary-upto-speed
Open

feat: Regional Access Boundary Changes#891
vverman wants to merge 9 commits intogoogleapis:trust_boundaryfrom
vverman:trust-boundary-upto-speed

Conversation

@vverman
Copy link
Contributor

@vverman vverman commented Jan 17, 2026

Contains changes for the feature Regional Access Boundary (Previously Called Trust Boundaries).

The following are salient changes:

  1. Calls to refresh RAB are now all async and in a separate thread.
  2. Logic for refreshing RAB now exists in its own class for cleaner maintenance.
  3. Self-signed jwts are within scope.
  4. Changes to how we trigger RAB refresh and deal with refresh errors.

@vverman vverman requested a review from a team as a code owner January 17, 2026 01:31
const maxRetryTime = 60 * 1000;
let shouldContinue = true;

while (shouldContinue) {
Copy link

Choose a reason for hiding this comment

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

This library already uses gaxios, which provides a powerful and configurable built-in retry mechanism (including exponential backoff) via its retryConfig. This library already uses gaxios, which provides a powerful and configurable built-in retry mechanism (including exponential backoff) via its retryConfig.

The fetchRegionalAccessBoundary method already enables gaxios retries, and then we're wrapping it here in the custom retry logic. I'd recommend refactoring to remove this while loop. Instead, you can add retry conditions to the retryConfig in fetchRegionalAccessBoundary method to achieve the desired behavior. This would leverage existing gaxios functionality, simplify the code, and align with the library's established patterns for request retries.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implemented! Thank you for the suggestion!

Comment on lines 525 to 528
!reAuthRetried
) {
this.clearRegionalAccessBoundaryCache();
return await this.requestAsync<T>(opts, true);
Copy link

Choose a reason for hiding this comment

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

I think we should have two separate flags for the auth retry, and this one (retryWithoutRAB maybe?). Consider this scenario which the single flag approach would fail to handle:

  1. Initial Call: requestAsync(opts) is called.
  2. First Failure: The request fails with a stale RAB error. The catch block clears the RAB cache and retries
    the request, with reAuthRetried set to True.
  3. Second Failure: The retried request (now sent without the x-allowed-locations header) fails, but this time with a 401 Unauthorized error because the access token has just expired.

With a single reAuthRetried flag, this second failure would not be handled because it thinks the token has already been refreshed and the request has been retried with the updated credential, and an error would be thrown.

If we use two flags, we can ensure that we only try once without the allowed location header, and then we attempt a true reAuth.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great point, I was of the opinion that we shouldn't hold the user back with too many retries. Thanks for the clarification, I have made the changes.

}

async getRequestHeaders(): Promise<Headers> {
async getRequestHeaders(url?: string | URL): Promise<Headers> {
Copy link

Choose a reason for hiding this comment

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

Same concern with the getRequestHeaders method in the baseexternalclient.ts file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We are now calling the refresh RAB from the requestAsync with the URL so this concern has been addressed!

response = await this.transporter.request<T>(opts);
response = await this.transporter.request<T>(requestOpts);
} catch (e) {
if (
Copy link

Choose a reason for hiding this comment

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

Same concern with reusing the reAuthRetried flag for the two different scenario.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed!

* @throws {Error} If the URL cannot be constructed for a compatible client.
*/
protected async getTrustBoundaryUrl(): Promise<string | null> {
public async getRegionalAccessBoundaryUrl(): Promise<string | null> {
Copy link

Choose a reason for hiding this comment

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

Why this method became public?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I made it so for ease of testing the getRegionalAccessBoundaryUrl logic. For ex: in the base external client we have scenarios where we want to test if the right url is returned or error is thrown in case of workload, workforce or a null audience.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd go ahead and put an @private on it, as that will cause it to be omitted from docs, but it will still show up in the transpiled class. Anyone digging in the source can see the @private to know they shouldn't use it.

Actually, it looks like @internal omits it from any exported .d.ts, so that may be enough. I'd double-check the output though, to see what it makes.

'https://staging-iamcredentials.sandbox.{universe_domain}/v1/projects/-/serviceAccounts/{service_account_email}/allowedLocations';

export const WORKLOAD_LOOKUP_ENDPOINT =
'https://iamcredentials.{universe_domain}/v1/projects/{project_id}/locations/global/workloadIdentityPools/{pool_id}/allowedLocations';
Copy link

Choose a reason for hiding this comment

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

We heard back from the backend team that the TPC won't be supported in the future either, so we don't have to consider universe domains other than GDU in here. Let's hardcode it in the URLs and simplify our logic too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gotcha, implemented the change!

Comment on lines 144 to 147
this.maybeTriggerRegionalAccessBoundaryRefresh(
url ?? undefined,
(tokens.access_token ?? tokens.id_token)!,
);
Copy link

Choose a reason for hiding this comment

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

IIUC the flow correctly, this block is for requesting an id token, and id tokens are not supported by RAB, so there is no need to trigger a refresh here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for this catch, I have implemented an isIDtoken flag which indicates to requestAsync inside oauth2client as to whether the token is an idToken and if so we skip RAB lookup.

@vverman vverman force-pushed the trust-boundary-upto-speed branch from 238aed8 to f40a3e1 Compare January 29, 2026 02:12
…e requestAsync level. RAB urls now have googleapis as universe domain.
@vverman vverman force-pushed the trust-boundary-upto-speed branch from e3e00da to 3427c0e Compare January 29, 2026 19:14
@vverman vverman requested a review from nbayati January 30, 2026 21:00
Copy link
Contributor

@feywind feywind left a comment

Choose a reason for hiding this comment

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

I don't see anything too weird in this, I guess let me know when you've got one that merges against main?

if (res && res.status === 400) {
const data = res.data as {error?: {message?: string}; message?: string};
const message =
data?.error?.message || data?.message || error.message || '';
Copy link
Contributor

Choose a reason for hiding this comment

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

?? might be more idiomatic now, unless e.g. data?.message could be empty but not null/undefined.

* @throws {Error} If the URL cannot be constructed for a compatible client.
*/
protected async getTrustBoundaryUrl(): Promise<string | null> {
public async getRegionalAccessBoundaryUrl(): Promise<string | null> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd go ahead and put an @private on it, as that will cause it to be omitted from docs, but it will still show up in the transpiled class. Anyone digging in the source can see the @private to know they shouldn't use it.

Actually, it looks like @internal omits it from any exported .d.ts, so that may be enough. I'd double-check the output though, to see what it makes.

}

protected async getTrustBoundaryUrl(): Promise<string> {
public async getRegionalAccessBoundaryUrl(): Promise<string> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment re protected -> public.

}

protected async getTrustBoundaryUrl(): Promise<string> {
public async getRegionalAccessBoundaryUrl(): Promise<string> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto protected -> public.

* Triggers an asynchronous regional access boundary refresh if needed.
* @param accessToken The access token to use for the lookup.
*/
private maybeTriggerRegionalAccessBoundaryRefresh(accessToken: string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Absolutely nitpicky, but I like to try to put an explicit return type on void methods just to make sure that you notice if something changes internal to it.

accessToken: MOCK_ACCESS_TOKEN,
expireTime: new Date(now + ONE_HOUR_IN_SECS * 1000).toISOString(),
accessToken: 'SA_ACCESS_TOKEN',
expireTime: new Date(Date.now() + 3600000).toISOString(),
Copy link
Contributor

Choose a reason for hiding this comment

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

You could still do the multiplication thing here... like 60 * 60 * 1000.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants