-
Notifications
You must be signed in to change notification settings - Fork 679
feat(auth): Regional Access Boundaries #8356
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: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,7 +31,16 @@ import { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as sts from './stscredentials'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {ClientAuthentication} from './oauth2common'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {SnakeToCamelObject, originalOrCamelOptions} from '../util'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getWorkforcePoolIdFromAudience, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| getWorkloadPoolIdFromAudience, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '../util'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {pkg} from '../shared.cjs'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SERVICE_ACCOUNT_LOOKUP_ENDPOINT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WORKFORCE_LOOKUP_ENDPOINT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WORKLOAD_LOOKUP_ENDPOINT, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from './regionalaccessboundary'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * The required token exchange grant_type: rfc8693#section-2.1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -415,11 +424,12 @@ export abstract class BaseExternalAccountClient extends AuthClient { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * The result has the form: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * { authorization: 'Bearer <access_token_value>' } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async getRequestHeaders(): Promise<Headers> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async getRequestHeaders(url?: string | URL): Promise<Headers> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const accessTokenResponse = await this.getAccessToken(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const headers = new Headers({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authorization: `Bearer ${accessTokenResponse.token}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.applyRegionalAccessBoundary(headers, url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.addSharedMetadataHeaders(headers); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -499,13 +509,14 @@ export abstract class BaseExternalAccountClient extends AuthClient { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reAuthRetried = false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<GaxiosResponse<T>> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let response: GaxiosResponse; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const requestOpts = {...opts}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const requestHeaders = await this.getRequestHeaders(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opts.headers = Gaxios.mergeHeaders(opts.headers); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const requestHeaders = await this.getRequestHeaders(opts.url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requestOpts.headers = Gaxios.mergeHeaders(requestOpts.headers); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.addUserProjectAndAuthHeaders(opts.headers, requestHeaders); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.applyHeadersFromSource(requestOpts.headers, requestHeaders); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response = await this.transporter.request<T>(opts); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response = await this.transporter.request<T>(requestOpts); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const res = (e as GaxiosError).response; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (res) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -684,19 +695,6 @@ export abstract class BaseExternalAccountClient extends AuthClient { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns whether the provided credentials are expired or not. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * If there is no expiry time, assumes the token is not expired or expiring. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param accessToken The credentials to check for expiration. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @return Whether the credentials are expired or not. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private isExpired(accessToken: Credentials): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const now = new Date().getTime(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return accessToken.expiry_date | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? now >= accessToken.expiry_date - this.eagerRefreshThresholdMillis | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @return The list of scopes for the requested GCP access token. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -722,4 +720,54 @@ export abstract class BaseExternalAccountClient extends AuthClient { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| protected getTokenUrl(): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return this.tokenUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Returns the regional access boundary lookup URL for the external account. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * This implementation constructs the URL based on the audience of the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * workforce or workload pool. If the client is configured for service account | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * impersonation, it uses the target service account email to generate | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * the lookup endpoint. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @return The regional access boundary URL string. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @internal | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public async getRegionalAccessBoundaryUrl(): Promise<string> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.serviceAccountImpersonationUrl) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // When impersonating a service account, the regional access boundary is determined | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // by the security policies of the target service account. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const email = this.getServiceAccountEmail(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!email) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `RegionalAccessBoundary: A service account email is required for regional access boundary lookups but could not be determined from the serviceAccountImpersonationUrl ${this.serviceAccountImpersonationUrl}.`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| '{service_account_email}', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| encodeURIComponent(email), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if the audience corresponds to a workload identity pool. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const wfPoolId = getWorkforcePoolIdFromAudience(this.audience); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (wfPoolId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return WORKFORCE_LOOKUP_ENDPOINT.replace( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| '{pool_id}', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| encodeURIComponent(wfPoolId), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if the audience corresponds to a workforce identity pool. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const wlPoolId = getWorkloadPoolIdFromAudience(this.audience); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const projectNumber = this.getProjectNumber(this.audience); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (wlPoolId && projectNumber) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return WORKLOAD_LOOKUP_ENDPOINT.replace( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| '{project_id}', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| projectNumber, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).replace('{pool_id}', wlPoolId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+750
to
+767
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. The comments for workforce and workload identity pools are swapped.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new RangeError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `RegionalAccessBoundary: Invalid audience provided: "${this.audience}" does not correspond to a workforce or workload pool.`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -21,6 +21,7 @@ import { | |||||||||||||||||||||
| OAuth2Client, | ||||||||||||||||||||||
| OAuth2ClientOptions, | ||||||||||||||||||||||
| } from './oauth2client'; | ||||||||||||||||||||||
| import {SERVICE_ACCOUNT_LOOKUP_ENDPOINT} from './regionalaccessboundary'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export interface ComputeOptions extends OAuth2ClientOptions { | ||||||||||||||||||||||
| /** | ||||||||||||||||||||||
|
|
@@ -137,4 +138,45 @@ export class Compute extends OAuth2Client { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Returns the regional access boundary lookup URL for the GCE instance. | ||||||||||||||||||||||
| * This implementation resolves the default service account email of the GCE | ||||||||||||||||||||||
| * instance to construct the lookup endpoint. | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
| * @return The regional access boundary URL string. | ||||||||||||||||||||||
| * @internal | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| public async getRegionalAccessBoundaryUrl(): Promise<string> { | ||||||||||||||||||||||
| const email = await this.resolveServiceAccountEmail(); | ||||||||||||||||||||||
| const regionalAccessBoundaryUrl = SERVICE_ACCOUNT_LOOKUP_ENDPOINT.replace( | ||||||||||||||||||||||
| '{service_account_email}', | ||||||||||||||||||||||
| encodeURIComponent(email), | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| return regionalAccessBoundaryUrl; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Resolves the service account email. If the email is set to 'default', | ||||||||||||||||||||||
| * it fetches the email from the GCE metadata server. | ||||||||||||||||||||||
| * @returns A promise that resolves with the service account email. | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| private async resolveServiceAccountEmail(): Promise<string> { | ||||||||||||||||||||||
| if (this.serviceAccountEmail !== 'default') { | ||||||||||||||||||||||
| // If a specific email is provided, return it directly. | ||||||||||||||||||||||
| return this.serviceAccountEmail; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+164
to
+168
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. If
Suggested change
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Otherwise, fetch the default email from the metadata server. | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| return await gcpMetadata.instance('service-accounts/default/email'); | ||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||
| 'RegionalAccessBoundary: Failed to retrieve default service account email from metadata server.', | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| cause: e, | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
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.
Use optional chaining (
credentials?.expiry_date) to prevent a potential runtimeTypeErrorifcredentialsis null or undefined.