-
Notifications
You must be signed in to change notification settings - Fork 11
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
Open
vverman
wants to merge
9
commits into
googleapis:trust_boundary
Choose a base branch
from
vverman:trust-boundary-upto-speed
base: trust_boundary
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,164
−730
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
7d283f5
Changes for trust boundaries async refresh and support for self-signe…
vverman c6c1c7b
Fixed unit tests moved RAB logic out of authclient.
vverman 635bae2
Changed getRegionalAccessBoundaryUrl and fixed a self-signed jwt fail…
vverman 0cfa454
Fixed url for service account RAB lookup.
vverman f40a3e1
Formatting and minor fixes
vverman 3427c0e
Stale RAB retry now has its own flag. RAB refresh now triggered at th…
vverman 1bf531a
A special flow where ID tokens were being used in jwtClient too have …
vverman 2856d2f
vanilla fix
vverman 2b247d4
Changed retry codes to remove the 403 and 404. The 500, 502, 503 and …
vverman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,18 +13,24 @@ | |
| // limitations under the License. | ||
|
|
||
| import {EventEmitter} from 'events'; | ||
| import {Gaxios, GaxiosOptions, GaxiosPromise, GaxiosResponse} from 'gaxios'; | ||
| import { | ||
| Gaxios, | ||
| GaxiosError, | ||
| GaxiosOptions, | ||
| GaxiosPromise, | ||
| GaxiosResponse, | ||
| } from 'gaxios'; | ||
|
|
||
| import {Credentials} from './credentials'; | ||
| import {OriginalAndCamel, originalOrCamelOptions} from '../util'; | ||
| import {log as makeLog} from 'google-logging-utils'; | ||
|
|
||
| import {PRODUCT_NAME, USER_AGENT} from '../shared.cjs'; | ||
| import { | ||
| isTrustBoundaryEnabled, | ||
| NoOpEncodedLocations, | ||
| TrustBoundaryData, | ||
| } from './trustboundary'; | ||
| isRegionalAccessBoundaryEnabled, | ||
| RegionalAccessBoundaryData, | ||
| RegionalAccessBoundaryManager, | ||
| } from './regionalaccessboundary'; | ||
|
|
||
| /** | ||
| * An interface for enforcing `fetch`-type compliance. | ||
|
|
@@ -237,8 +243,8 @@ export abstract class AuthClient | |
| eagerRefreshThresholdMillis = DEFAULT_EAGER_REFRESH_THRESHOLD_MILLIS; | ||
| forceRefreshOnFailure = false; | ||
| universeDomain = DEFAULT_UNIVERSE; | ||
| trustBoundaryEnabled: boolean; | ||
| trustBoundary?: TrustBoundaryData | null; | ||
| regionalAccessBoundaryEnabled: boolean; | ||
| protected regionalAccessBoundaryManager: RegionalAccessBoundaryManager; | ||
|
|
||
| /** | ||
| * Symbols that can be added to GaxiosOptions to specify the method name that is | ||
|
|
@@ -261,12 +267,17 @@ export abstract class AuthClient | |
| this.quotaProjectId = options.get('quota_project_id'); | ||
| this.credentials = options.get('credentials') ?? {}; | ||
| this.universeDomain = options.get('universe_domain') ?? DEFAULT_UNIVERSE; | ||
| this.trustBoundaryEnabled = isTrustBoundaryEnabled(); | ||
| this.trustBoundary = null; | ||
| this.regionalAccessBoundaryEnabled = isRegionalAccessBoundaryEnabled(); | ||
|
|
||
| // Shared client options | ||
| this.transporter = opts.transporter ?? new Gaxios(opts.transporterOptions); | ||
|
|
||
| this.regionalAccessBoundaryManager = new RegionalAccessBoundaryManager({ | ||
| transporter: this.transporter, | ||
| getLookupUrl: async () => this.getRegionalAccessBoundaryUrl(), | ||
| isUniverseDomainDefault: () => this.universeDomain === DEFAULT_UNIVERSE, | ||
| }); | ||
|
|
||
| if (options.get('useAuthRequestParameters') !== false) { | ||
| this.transporter.interceptors.request.add( | ||
| AuthClient.DEFAULT_REQUEST_INTERCEPTOR, | ||
|
|
@@ -371,14 +382,15 @@ export abstract class AuthClient | |
| }>; | ||
|
|
||
| /** | ||
| * Constructs the trust boundary lookup URL for the client. | ||
| * Constructs the regional access boundary lookup URL for the client. | ||
| * | ||
| * @return The trust boundary URL string, or `null` if the client type | ||
| * does not support trust boundaries. | ||
| * @return The regional access boundary URL string, or `null` if the client type | ||
| * does not support regional access boundaries. | ||
| * @throws {Error} If the URL cannot be constructed for a compatible client, | ||
| * for instance, if a required property like a service account email is missing. | ||
| * @internal | ||
| */ | ||
| protected async getTrustBoundaryUrl(): Promise<string | null> { | ||
| public async getRegionalAccessBoundaryUrl(): Promise<string | null> { | ||
| return null; | ||
| } | ||
|
|
||
|
|
@@ -389,6 +401,30 @@ export abstract class AuthClient | |
| this.credentials = credentials; | ||
| } | ||
|
|
||
| /** | ||
| * Manually sets the regional access boundary data. | ||
| * Treating this as a standard cache entry with a 6-hour TTL. | ||
| * @param data The regional access boundary data to set. | ||
| */ | ||
| setRegionalAccessBoundary(data: RegionalAccessBoundaryData) { | ||
| this.regionalAccessBoundaryManager.setRegionalAccessBoundary(data); | ||
| } | ||
|
|
||
| /** | ||
| * Returns the current regional access boundary data. | ||
| */ | ||
| getRegionalAccessBoundary(): RegionalAccessBoundaryData | null { | ||
| return this.regionalAccessBoundaryManager.data; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the current regional access boundary cooldown time in milliseconds. | ||
| * @internal | ||
| */ | ||
| getRegionalAccessBoundaryCooldownTime(): number { | ||
| return this.regionalAccessBoundaryManager.cooldownTime; | ||
| } | ||
|
|
||
| /** | ||
| * Append additional headers, e.g., x-goog-user-project, shared across the | ||
| * classes inheriting AuthClient. This method should be used by any method | ||
|
|
@@ -408,17 +444,28 @@ export abstract class AuthClient | |
| headers.set('x-goog-user-project', this.quotaProjectId); | ||
| } | ||
|
|
||
| if (this.trustBoundaryEnabled && this.trustBoundary) { | ||
| //Empty header sent in case trust-boundary has no-op encoded location. | ||
| headers.set( | ||
| 'x-allowed-locations', | ||
| this.trustBoundary.encodedLocations === NoOpEncodedLocations | ||
| ? '' | ||
| : this.trustBoundary.encodedLocations, | ||
| return headers; | ||
| } | ||
|
|
||
| /** | ||
| * Applies regional access boundary rules to the provided headers. | ||
| * This includes adding the x-allowed-locations header and triggering | ||
| * a background refresh if needed. | ||
| * @param headers The headers to update. | ||
| * @param url Optional destination URL of the request. If missing, assumed global. | ||
| */ | ||
| protected applyRegionalAccessBoundary( | ||
| headers: Headers, | ||
| url?: string | URL, | ||
| ): void { | ||
| const rabHeader = | ||
| this.regionalAccessBoundaryManager.getRegionalAccessBoundaryHeader( | ||
| url, | ||
| headers, | ||
| ); | ||
| if (rabHeader) { | ||
| headers.set('x-allowed-locations', rabHeader); | ||
| } | ||
|
|
||
| return headers; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -446,7 +493,7 @@ export abstract class AuthClient | |
| target.set('authorization', authorizationHeader); | ||
| } | ||
|
|
||
| if (xGoogAllowedLocs || xGoogAllowedLocs === '') { | ||
| if (xGoogAllowedLocs) { | ||
| target.set('x-allowed-locations', xGoogAllowedLocs); | ||
| } | ||
|
|
||
|
|
@@ -588,87 +635,6 @@ export abstract class AuthClient | |
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Refreshes trust boundary data for an authenticated client. | ||
| * Handles caching checks and potential fallbacks. | ||
| * @param tokens The refreshed credentials containing access token to call the trust boundary endpoint. | ||
| * @returns A Promise resolving to TrustBoundaryData or empty-string for no-op trust boundaries. | ||
| * @throws {Error} If the request fails and there is no cache available. | ||
| */ | ||
| protected async refreshTrustBoundary( | ||
| tokens: Credentials, | ||
| ): Promise<TrustBoundaryData | null> { | ||
| if (!this.trustBoundaryEnabled) { | ||
| return null; | ||
| } | ||
|
|
||
| if (this.universeDomain !== DEFAULT_UNIVERSE) { | ||
| // Skipping check for non-default universe domain as this feature is only supported in GDU | ||
| return null; | ||
| } | ||
|
|
||
| const cachedTB = this.trustBoundary; | ||
| if (cachedTB && cachedTB.encodedLocations === NoOpEncodedLocations) { | ||
| return cachedTB; | ||
| } | ||
|
|
||
| const trustBoundaryUrl = await this.getTrustBoundaryUrl(); | ||
| if (!trustBoundaryUrl) { | ||
| return null; | ||
| } | ||
|
|
||
| const accessToken = tokens.access_token; | ||
|
|
||
| if (!accessToken || this.isExpired(tokens)) { | ||
| throw new Error( | ||
| 'TrustBoundary: Error calling lookup endpoint without valid access token', | ||
| ); | ||
| } | ||
|
|
||
| const headers = this.addSharedMetadataHeaders( | ||
| new Headers({ | ||
| //we can directly pass the access_token as the trust boundaries are always fetched after token refresh | ||
| authorization: 'Bearer ' + accessToken, | ||
| }), | ||
| ); | ||
|
|
||
| const opts: GaxiosOptions = { | ||
| ...{ | ||
| retry: true, | ||
| retryConfig: { | ||
| httpMethodsToRetry: ['GET'], | ||
| }, | ||
| }, | ||
| headers, | ||
| url: trustBoundaryUrl, | ||
| }; | ||
|
|
||
| try { | ||
| const {data: trustBoundaryData} = | ||
| // Use the transporter directly here. A standard `client.request` would | ||
| // re-trigger a token refresh, creating an infinite loop. | ||
| await this.transporter.request<TrustBoundaryData>(opts); | ||
|
|
||
| if (!trustBoundaryData.encodedLocations) { | ||
| throw new Error( | ||
| 'TrustBoundary: Malformed response from lookup endpoint.', | ||
| ); | ||
| } | ||
|
|
||
| return trustBoundaryData; | ||
| } catch (error) { | ||
| if (this.trustBoundary) { | ||
| return this.trustBoundary; // return cached tb if call to lookup fails | ||
| } | ||
| throw new Error( | ||
| 'TrustBoundary: Failure while getting trust boundaries:', | ||
| { | ||
| cause: error, | ||
| }, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns whether the provided credentials are expired or will expire within | ||
| * eagerRefreshThresholdMillismilliseconds. | ||
|
|
@@ -682,6 +648,28 @@ export abstract class AuthClient | |
| ? now >= credentials.expiry_date - this.eagerRefreshThresholdMillis | ||
| : false; | ||
| } | ||
|
|
||
| /** | ||
| * Checks if the error is a "stale regional access boundary" error. | ||
| * @param error The error to check. | ||
| */ | ||
| public isStaleRegionalAccessBoundaryError(error: GaxiosError): boolean { | ||
| const res = error.response; | ||
| if (res && res.status === 400) { | ||
| const data = res.data as {error?: {message?: string}; message?: string}; | ||
| const message = | ||
| data?.error?.message || data?.message || error.message || ''; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| return message.toLowerCase().includes('stale regional access boundary'); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Clears the regional access boundary cache. | ||
| */ | ||
| protected clearRegionalAccessBoundaryCache() { | ||
| this.regionalAccessBoundaryManager.clearRegionalAccessBoundaryCache(); | ||
| } | ||
| } | ||
|
|
||
| // TypeScript does not have `HeadersInit` in the standard types yet | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.