-
Notifications
You must be signed in to change notification settings - Fork 9
feat: Regional Access Boundary Changes #891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trust_boundary
Are you sure you want to change the base?
feat: Regional Access Boundary Changes #891
Conversation
packages/google-auth-library-nodejs/src/auth/regionalaccessboundary.ts
Outdated
Show resolved
Hide resolved
packages/google-auth-library-nodejs/src/auth/baseexternalclient.ts
Outdated
Show resolved
Hide resolved
|
|
||
| const log = makeLog('auth'); | ||
|
|
||
| // googleapis.com |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // googleapis.com |
packages/google-auth-library-nodejs/src/auth/regionalaccessboundary.ts
Outdated
Show resolved
Hide resolved
| export interface RegionalAccessBoundaryData { | ||
| /** | ||
| * The readable text format of the allowed regional access boundary locations. | ||
| * This is optional, as it might not be present if no regional access boundary is enforced. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not accurate. We can't tell if RAB is enforced or not by the list being present.
Also, in the last iteration of design when we removed no-op, this field is not considered optional anymore either. I believe a valid response would have both, but we mostly care about the encodedLocations as that's the field that'd be present in the header.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the catch! I think I missed this while making the changes from the TB -> RAB.
I believe as per the latest confirmation, we now expect both the encoded and the readable locations as part of the response from lookup. So I will make the changes to the comments accordingly.
| private isGlobalEndpoint(url?: string | URL): boolean { | ||
| if (!url) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The isGlobalEndpoint(url?) method currently accepts an optional URL and defaults to true if it's missing. While this works, it makes the method's contract a bit less explicit.
What do you think about refactoring this to make isGlobalEndpoint's signature stricter by always requiring a URL? The logic for handling a missing URL could be moved up to the caller (maybeTriggerRegionalAccessBoundaryRefresh).
This would make it clearer that isGlobalEndpoint's single responsibility is to check a given URL, and the calling method is responsible for the "default to global" logic when no URL is present.
| const maxRetryTime = 60 * 1000; | ||
| let shouldContinue = true; | ||
|
|
||
| while (shouldContinue) { |
There was a problem hiding this comment.
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.
| async getRequestHeaders(url?: string | URL): Promise<Headers> { | ||
| const accessTokenResponse = await this.getAccessToken(); | ||
| const headers = new Headers({ | ||
| authorization: `Bearer ${accessTokenResponse.token}`, | ||
| }); | ||
| this.maybeTriggerRegionalAccessBoundaryRefresh( | ||
| url, | ||
| accessTokenResponse.token!, | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed two problems with this change.
-
The caller is not passing the URL to the getRequestHeaders() method, both in this file and in
externalAccountAuthorizedUserClient.ts, so url is always going to be null. -
Even though this is not a breaking change since we're allowing url parameter to be null, I think we can still make the design better by moving the
maybeTriggerRegionalAccessBoundaryRefresh()call to the caller method (requestAsync()). This way thegetRequestHeaders()doesn't have to know a url, and it can do only 1 task which is getting the headers without potentially invoking a background fetch. Another benefit is that we can enforce that url is not empty, and update the downstream methods such asregionalAccessBoundaryManager.maybeTriggerRegionalAccessBoundaryRefreshandisGlobalEndpointto expect a non-null URL as well.
| !reAuthRetried | ||
| ) { | ||
| this.clearRegionalAccessBoundaryCache(); | ||
| return await this.requestAsync<T>(opts, true); |
There was a problem hiding this comment.
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:
- Initial Call: requestAsync(opts) is called.
- 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. - 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.
| } | ||
|
|
||
| async getRequestHeaders(): Promise<Headers> { | ||
| async getRequestHeaders(url?: string | URL): Promise<Headers> { |
There was a problem hiding this comment.
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.
| response = await this.transporter.request<T>(opts); | ||
| response = await this.transporter.request<T>(requestOpts); | ||
| } catch (e) { | ||
| if ( |
There was a problem hiding this comment.
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.
| * @throws {Error} If the URL cannot be constructed for a compatible client. | ||
| */ | ||
| protected async getTrustBoundaryUrl(): Promise<string | null> { | ||
| public async getRegionalAccessBoundaryUrl(): Promise<string | null> { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| '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'; |
There was a problem hiding this comment.
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.
| this.maybeTriggerRegionalAccessBoundaryRefresh( | ||
| url ?? undefined, | ||
| (tokens.access_token ?? tokens.id_token)!, | ||
| ); |
There was a problem hiding this comment.
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.
238aed8 to
f40a3e1
Compare
…e requestAsync level. RAB urls now have googleapis as universe domain.
Contains changes for the feature Regional Access Boundary (Previously Called Trust Boundaries).
The following are salient changes: