Skip to content

Commit

Permalink
feat: add workforce config support. (#1251)
Browse files Browse the repository at this point in the history
See go/workforce-pools-client-support.

Add support of work_force_user_config field for workforce pool and related logic in ExternalClient(calling constructor for IdentityPoolClient) BaseExternalClient(the parent class of IdentityPoolClient). 
The logic change is only related to refreshAccessToken() flow, since this API is non-public, we use getAccessToken() to test the flow instead. 

Add 16 tests:
ExternalClient:
1. fromJson() for IdentityPoolClient, throw an error is work_force_user_project provided by audience is not workforce audience.
2. fromJson() For IdentityPoolClient, return expected response is work_force_user_project provided and audience is workforce audience.

BaseExternalClient:
1. getAccessToken() Should apply basic auth on workforce configs with client auth provided(no impersonation).
2. getAccessToken() Should apply work_force_user_project on workforce configs without client auth(no impersonation).
3. getAccessToken() Should not throw if workforce audience and client auth but work_force_user_project not provided(no impersonation).
4. getAccessToken() Should not throw if workforce audience and no client auth but work_force_user_project provided( impersonation).
5. Constructor(), throw an error is work_force_user_project provided by audience is not workforce audience.
6. Constructor(), return expected response is work_force_user_project provided and audience is workforce audience.
7.  getProjectId(), should resolve with workforce projectID if no client auth not and workforce user project are defined.
8. getProjectId(), should not pass workforce user project if client auth is defined.

IdentityPoolClient:
1. getAccessToken() Should apply basic auth on workforce configs with client auth provided(no impersonation).
2. getAccessToken() Should apply work_force_user_project on workforce configs without client auth(no impersonation).
3. getAccessToken() Should not throw if workforce audience and client auth but work_force_user_project not provided(no impersonation).
4. getAccessToken() Should not throw if workforce audience and no client auth but work_force_user_project provided( impersonation).
5. Constructor(), throw an error is work_force_user_project provided by audience is not workforce audience.
6. Constructor(), return expected response is work_force_user_project provided and audience is workforce audience.
  • Loading branch information
xil222 committed Sep 28, 2021
1 parent 2ac7df9 commit fe29e38
Show file tree
Hide file tree
Showing 5 changed files with 679 additions and 9 deletions.
44 changes: 37 additions & 7 deletions src/auth/baseexternalclient.ts
Expand Up @@ -57,6 +57,9 @@ export const EXTERNAL_ACCOUNT_TYPE = 'external_account';
/** Cloud resource manager URL used to retrieve project information. */
export const CLOUD_RESOURCE_MANAGER =
'https://cloudresourcemanager.googleapis.com/v1/projects/';
/** The workforce audience pattern. */
const WORKFORCE_AUDIENCE_PATTERN =
'//iam.googleapis.com/locations/[^/]+/workforcePools/[^/]+/providers/.+';

/**
* Base external account credentials json interface.
Expand All @@ -71,6 +74,7 @@ export interface BaseExternalAccountClientOptions {
client_id?: string;
client_secret?: string;
quota_project_id?: string;
workforce_pool_user_project?: string;
}

/**
Expand Down Expand Up @@ -127,6 +131,8 @@ export abstract class BaseExternalAccountClient extends AuthClient {
private readonly subjectTokenType: string;
private readonly serviceAccountImpersonationUrl?: string;
private readonly stsCredential: sts.StsCredentials;
private readonly clientAuth?: ClientAuthentication;
private readonly workforcePoolUserProject?: string;
public projectId: string | null;
public projectNumber: string | null;
public readonly eagerRefreshThresholdMillis: number;
Expand All @@ -152,7 +158,7 @@ export abstract class BaseExternalAccountClient extends AuthClient {
`received "${options.type}"`
);
}
const clientAuth = options.client_id
this.clientAuth = options.client_id
? ({
confidentialClientType: 'basic',
clientId: options.client_id,
Expand All @@ -162,13 +168,27 @@ export abstract class BaseExternalAccountClient extends AuthClient {
if (!this.validateGoogleAPIsUrl('sts', options.token_url)) {
throw new Error(`"${options.token_url}" is not a valid token url.`);
}
this.stsCredential = new sts.StsCredentials(options.token_url, clientAuth);
this.stsCredential = new sts.StsCredentials(
options.token_url,
this.clientAuth
);
// Default OAuth scope. This could be overridden via public property.
this.scopes = [DEFAULT_OAUTH_SCOPE];
this.cachedAccessToken = null;
this.audience = options.audience;
this.subjectTokenType = options.subject_token_type;
this.quotaProjectId = options.quota_project_id;
this.workforcePoolUserProject = options.workforce_pool_user_project;
const workforceAudiencePattern = new RegExp(WORKFORCE_AUDIENCE_PATTERN);
if (
this.workforcePoolUserProject &&
!this.audience.match(workforceAudiencePattern)
) {
throw new Error(
'workforcePoolUserProject should not be set for non-workforce pool ' +
'credentials.'
);
}
if (
typeof options.service_account_impersonation_url !== 'undefined' &&
!this.validateGoogleAPIsUrl(
Expand Down Expand Up @@ -290,8 +310,9 @@ export abstract class BaseExternalAccountClient extends AuthClient {

/**
* @return A promise that resolves with the project ID corresponding to the
* current workload identity pool. When not determinable, this resolves with
* null.
* current workload identity pool or current workforce pool if
* determinable. For workforce pool credential, it returns the project ID
* corresponding to the workforcePoolUserProject.
* This is introduced to match the current pattern of using the Auth
* library:
* const projectId = await auth.getProjectId();
Expand All @@ -303,15 +324,16 @@ export abstract class BaseExternalAccountClient extends AuthClient {
* https://cloud.google.com/resource-manager/reference/rest/v1/projects/get#authorization-scopes
*/
async getProjectId(): Promise<string | null> {
const projectNumber = this.projectNumber || this.workforcePoolUserProject;
if (this.projectId) {
// Return previously determined project ID.
return this.projectId;
} else if (this.projectNumber) {
} else if (projectNumber) {
// Preferable not to use request() to avoid retrial policies.
const headers = await this.getRequestHeaders();
const response = await this.transporter.request<ProjectInfo>({
headers,
url: `${CLOUD_RESOURCE_MANAGER}${this.projectNumber}`,
url: `${CLOUD_RESOURCE_MANAGER}${projectNumber}`,
responseType: 'json',
});
this.projectId = response.data.projectId;
Expand Down Expand Up @@ -401,8 +423,16 @@ export abstract class BaseExternalAccountClient extends AuthClient {
};

// Exchange the external credentials for a GCP access token.
// Client auth is prioritized over passing the workforcePoolUserProject
// parameter for STS token exchange.
const additionalOptions =
!this.clientAuth && this.workforcePoolUserProject
? {userProject: this.workforcePoolUserProject}
: undefined;
const stsResponse = await this.stsCredential.exchangeToken(
stsCredentialsOptions
stsCredentialsOptions,
undefined,
additionalOptions
);

if (this.serviceAccountImpersonationUrl) {
Expand Down
3 changes: 2 additions & 1 deletion src/auth/identitypoolclient.ts
Expand Up @@ -70,7 +70,8 @@ export class IdentityPoolClient extends BaseExternalAccountClient {
* Instantiate an IdentityPoolClient instance using the provided JSON
* object loaded from an external account credentials file.
* An error is thrown if the credential is not a valid file-sourced or
* url-sourced credential.
* url-sourced credential or a workforce pool user project is provided
* with a non workforce audience.
* @param options The external account options object typically loaded
* from the external account JSON credential file.
* @param additionalOptions Optional additional behavior customization
Expand Down

0 comments on commit fe29e38

Please sign in to comment.