Make auth universe-aware (#352)
This adds support for making the action "universe" aware, so it will be usable for TPC and GDCH.
This commit is contained in:
parent
097d292c04
commit
7c4e01fd00
12
README.md
12
README.md
@ -273,6 +273,18 @@ regardless of the authentication mechanism.
|
|||||||
identities to use for impersonation in the chain. By default there are no
|
identities to use for impersonation in the chain. By default there are no
|
||||||
delegates.
|
delegates.
|
||||||
|
|
||||||
|
- `universe`: (Optional) The Google Cloud universe to use for constructing API
|
||||||
|
endpoints. The default universe is "googleapis.com", which corresponds to
|
||||||
|
https://cloud.google.com. Trusted Partner Cloud and Google Distributed
|
||||||
|
Hosted Cloud should set this to their universe address.
|
||||||
|
|
||||||
|
You can also override individual API endpoints by setting the environment variable `GHA_ENDPOINT_OVERRIDE_<endpoint>` where endpoint is the API endpoint to override. This only applies to the `auth` action and does not persist to other steps. For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
GHA_ENDPOINT_OVERRIDE_oauth2: 'https://oauth2.myapi.endpoint/v1'
|
||||||
|
```
|
||||||
|
|
||||||
- `cleanup_credentials`: (Optional) If true, the action will remove any
|
- `cleanup_credentials`: (Optional) If true, the action will remove any
|
||||||
created credentials from the filesystem upon completion. This only applies
|
created credentials from the filesystem upon completion. This only applies
|
||||||
if "create_credentials_file" is true. The default is true.
|
if "create_credentials_file" is true. The default is true.
|
||||||
|
@ -94,6 +94,14 @@ inputs:
|
|||||||
impersonation in the chain.
|
impersonation in the chain.
|
||||||
default: ''
|
default: ''
|
||||||
required: false
|
required: false
|
||||||
|
universe:
|
||||||
|
description: |-
|
||||||
|
The Google Cloud universe to use for constructing API endpoints. The
|
||||||
|
default universe is "googleapis.com", which corresponds to
|
||||||
|
https://cloud.google.com. Trusted Partner Cloud and Google Distributed
|
||||||
|
Hosted Cloud should set this to their universe address.
|
||||||
|
required: false
|
||||||
|
default: 'googleapis.com'
|
||||||
cleanup_credentials:
|
cleanup_credentials:
|
||||||
description: |-
|
description: |-
|
||||||
If true, the action will remove any created credentials from the
|
If true, the action will remove any created credentials from the
|
||||||
|
6
dist/main/index.js
vendored
6
dist/main/index.js
vendored
File diff suppressed because one or more lines are too long
@ -1,36 +0,0 @@
|
|||||||
// Copyright 2023 Google LLC
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client is the default HTTP client for interacting with the IAM credentials
|
|
||||||
* API.
|
|
||||||
*/
|
|
||||||
export interface AuthClient {
|
|
||||||
/**
|
|
||||||
* getToken() gets or generates the best token for the auth client.
|
|
||||||
*/
|
|
||||||
getToken(): Promise<string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* createCredentialsFile creates a credential file (for use with gcloud and
|
|
||||||
* other Google Cloud tools) that instructs the tool how to perform identity
|
|
||||||
* federation.
|
|
||||||
*/
|
|
||||||
createCredentialsFile(outputPath: string): Promise<string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* signJWT signs a JWT using the auth provider.
|
|
||||||
*/
|
|
||||||
signJWT(claims: any): Promise<string>;
|
|
||||||
}
|
|
106
src/client/client.ts
Normal file
106
src/client/client.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// Copyright 2023 Google LLC
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { HttpClient } from '@actions/http-client';
|
||||||
|
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
import { userAgent } from '../utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AuthClient is the default HTTP client for interacting with the IAM credentials
|
||||||
|
* API.
|
||||||
|
*/
|
||||||
|
export interface AuthClient {
|
||||||
|
/**
|
||||||
|
* getToken() gets or generates the best token for the auth client.
|
||||||
|
*/
|
||||||
|
getToken(): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* createCredentialsFile creates a credential file (for use with gcloud and
|
||||||
|
* other Google Cloud tools) that instructs the tool how to perform identity
|
||||||
|
* federation.
|
||||||
|
*/
|
||||||
|
createCredentialsFile(outputPath: string): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* signJWT signs a JWT using the auth provider.
|
||||||
|
*/
|
||||||
|
signJWT(claims: any): Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientParameters {
|
||||||
|
logger: Logger;
|
||||||
|
universe: string;
|
||||||
|
child: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Client {
|
||||||
|
protected readonly _logger: Logger;
|
||||||
|
protected readonly _httpClient: HttpClient;
|
||||||
|
|
||||||
|
protected readonly _endpoints = {
|
||||||
|
iam: 'https://iam.{universe}/v1',
|
||||||
|
iamcredentials: 'https://iamcredentials.{universe}/v1',
|
||||||
|
oauth2: 'https://oauth2.{universe}',
|
||||||
|
sts: 'https://sts.{universe}/v1',
|
||||||
|
www: 'https://www.{universe}',
|
||||||
|
};
|
||||||
|
protected readonly _universe;
|
||||||
|
|
||||||
|
constructor(opts: ClientParameters) {
|
||||||
|
this._logger = opts.logger.withNamespace(opts.child);
|
||||||
|
|
||||||
|
// Create the http client with our user agent.
|
||||||
|
this._httpClient = new HttpClient(userAgent, undefined, {
|
||||||
|
allowRedirects: true,
|
||||||
|
allowRetries: true,
|
||||||
|
keepAlive: true,
|
||||||
|
maxRedirects: 5,
|
||||||
|
maxRetries: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Expand universe to support TPC and custom endpoints.
|
||||||
|
this._universe = opts.universe;
|
||||||
|
for (const key of Object.keys(this._endpoints) as Array<keyof typeof this._endpoints>) {
|
||||||
|
this._endpoints[key] = this.expandEndpoint(key);
|
||||||
|
}
|
||||||
|
this._logger.debug(`Computed endpoints for universe ${this._universe}`, this._endpoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
expandEndpoint(key: keyof typeof this._endpoints): string {
|
||||||
|
const envOverrideKey = `GHA_ENDPOINT_OVERRIDE_${key}`;
|
||||||
|
const envOverrideValue = process.env[envOverrideKey];
|
||||||
|
if (envOverrideValue && envOverrideValue !== '') {
|
||||||
|
this._logger.debug(
|
||||||
|
`Overriding API endpoint for ${key} because ${envOverrideKey} is set`,
|
||||||
|
envOverrideValue,
|
||||||
|
);
|
||||||
|
return envOverrideValue.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (this._endpoints[key] || '').replace(/{universe}/g, this._universe).replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export { IAMCredentialsClient, IAMCredentialsClientParameters } from './iamcredentials';
|
||||||
|
|
||||||
|
export {
|
||||||
|
ServiceAccountKeyClient,
|
||||||
|
ServiceAccountKeyClientParameters,
|
||||||
|
} from './service_account_key_json';
|
||||||
|
|
||||||
|
export {
|
||||||
|
WorkloadIdentityFederationClient,
|
||||||
|
WorkloadIdentityFederationClientParameters,
|
||||||
|
} from './workload_identity_federation';
|
@ -14,10 +14,10 @@
|
|||||||
|
|
||||||
import { URLSearchParams } from 'url';
|
import { URLSearchParams } from 'url';
|
||||||
|
|
||||||
import { HttpClient } from '@actions/http-client';
|
import { errorMessage } from '@google-github-actions/actions-utils';
|
||||||
|
|
||||||
import { Logger } from './logger';
|
import { Client } from './client';
|
||||||
import { expandEndpoint, userAgent } from './utils';
|
import { Logger } from '../logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GenerateAccessTokenParameters are the inputs to the generateAccessToken call.
|
* GenerateAccessTokenParameters are the inputs to the generateAccessToken call.
|
||||||
@ -43,6 +43,9 @@ export interface GenerateIDTokenParameters {
|
|||||||
* IAMCredentialsClientParameters are the inputs to the IAM client.
|
* IAMCredentialsClientParameters are the inputs to the IAM client.
|
||||||
*/
|
*/
|
||||||
export interface IAMCredentialsClientParameters {
|
export interface IAMCredentialsClientParameters {
|
||||||
|
readonly logger: Logger;
|
||||||
|
readonly universe: string;
|
||||||
|
|
||||||
readonly authToken: string;
|
readonly authToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,28 +53,17 @@ export interface IAMCredentialsClientParameters {
|
|||||||
* IAMCredentialsClient is a thin HTTP client around the Google Cloud IAM
|
* IAMCredentialsClient is a thin HTTP client around the Google Cloud IAM
|
||||||
* Credentials API.
|
* Credentials API.
|
||||||
*/
|
*/
|
||||||
export class IAMCredentialsClient {
|
export class IAMCredentialsClient extends Client {
|
||||||
readonly #logger: Logger;
|
|
||||||
readonly #httpClient: HttpClient;
|
|
||||||
readonly #authToken: string;
|
readonly #authToken: string;
|
||||||
|
|
||||||
readonly #universe: string = 'googleapis.com';
|
constructor(opts: IAMCredentialsClientParameters) {
|
||||||
readonly #endpoints = {
|
super({
|
||||||
iamcredentials: 'https://iamcredentials.{universe}/v1',
|
logger: opts.logger,
|
||||||
oauth2: 'https://oauth2.{universe}',
|
universe: opts.universe,
|
||||||
};
|
child: `IAMCredentialsClient`,
|
||||||
|
});
|
||||||
constructor(logger: Logger, opts: IAMCredentialsClientParameters) {
|
|
||||||
this.#logger = logger.withNamespace(this.constructor.name);
|
|
||||||
this.#httpClient = new HttpClient(userAgent);
|
|
||||||
|
|
||||||
this.#authToken = opts.authToken;
|
this.#authToken = opts.authToken;
|
||||||
|
|
||||||
const endpoints = this.#endpoints;
|
|
||||||
for (const key of Object.keys(this.#endpoints) as Array<keyof typeof endpoints>) {
|
|
||||||
this.#endpoints[key] = expandEndpoint(this.#endpoints[key], this.#universe);
|
|
||||||
}
|
|
||||||
this.#logger.debug(`Computed endpoints`, this.#endpoints);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -84,7 +76,9 @@ export class IAMCredentialsClient {
|
|||||||
scopes,
|
scopes,
|
||||||
lifetime,
|
lifetime,
|
||||||
}: GenerateAccessTokenParameters): Promise<string> {
|
}: GenerateAccessTokenParameters): Promise<string> {
|
||||||
const pth = `${this.#endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`;
|
const logger = this._logger.withNamespace('generateAccessToken');
|
||||||
|
|
||||||
|
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`;
|
||||||
|
|
||||||
const headers = { Authorization: `Bearer ${this.#authToken}` };
|
const headers = { Authorization: `Bearer ${this.#authToken}` };
|
||||||
|
|
||||||
@ -100,7 +94,7 @@ export class IAMCredentialsClient {
|
|||||||
body.lifetime = `${lifetime}s`;
|
body.lifetime = `${lifetime}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#logger.withNamespace('generateAccessToken').debug({
|
logger.debug(`Built request`, {
|
||||||
method: `POST`,
|
method: `POST`,
|
||||||
path: pth,
|
path: pth,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
@ -108,7 +102,7 @@ export class IAMCredentialsClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.#httpClient.postJson<{ accessToken: string }>(pth, body, headers);
|
const resp = await this._httpClient.postJson<{ accessToken: string }>(pth, body, headers);
|
||||||
const statusCode = resp.statusCode || 500;
|
const statusCode = resp.statusCode || 500;
|
||||||
if (statusCode < 200 || statusCode > 299) {
|
if (statusCode < 200 || statusCode > 299) {
|
||||||
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
|
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
|
||||||
@ -120,14 +114,17 @@ export class IAMCredentialsClient {
|
|||||||
}
|
}
|
||||||
return result.accessToken;
|
return result.accessToken;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const msg = errorMessage(err);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to generate Google Cloud OAuth 2.0 Access Token for ${serviceAccount}: ${err}`,
|
`Failed to generate Google Cloud OAuth 2.0 Access Token for ${serviceAccount}: ${msg}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateDomainWideDelegationAccessToken(assertion: string): Promise<string> {
|
async generateDomainWideDelegationAccessToken(assertion: string): Promise<string> {
|
||||||
const pth = `${this.#endpoints.oauth2}/token`;
|
const logger = this._logger.withNamespace('generateDomainWideDelegationAccessToken');
|
||||||
|
|
||||||
|
const pth = `${this._endpoints.oauth2}/token`;
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
@ -138,7 +135,7 @@ export class IAMCredentialsClient {
|
|||||||
body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
|
body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
|
||||||
body.append('assertion', assertion);
|
body.append('assertion', assertion);
|
||||||
|
|
||||||
this.#logger.withNamespace('generateDomainWideDelegationAccessToken').debug({
|
logger.debug(`Built request`, {
|
||||||
method: `POST`,
|
method: `POST`,
|
||||||
path: pth,
|
path: pth,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
@ -146,7 +143,7 @@ export class IAMCredentialsClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.#httpClient.post(pth, body.toString(), headers);
|
const resp = await this._httpClient.post(pth, body.toString(), headers);
|
||||||
const respBody = await resp.readBody();
|
const respBody = await resp.readBody();
|
||||||
const statusCode = resp.message.statusCode || 500;
|
const statusCode = resp.message.statusCode || 500;
|
||||||
if (statusCode < 200 || statusCode > 299) {
|
if (statusCode < 200 || statusCode > 299) {
|
||||||
@ -155,8 +152,9 @@ export class IAMCredentialsClient {
|
|||||||
const parsed = JSON.parse(respBody) as { accessToken: string };
|
const parsed = JSON.parse(respBody) as { accessToken: string };
|
||||||
return parsed.accessToken;
|
return parsed.accessToken;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const msg = errorMessage(err);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to generate Google Cloud Domain Wide Delegation OAuth 2.0 Access Token: ${err}`,
|
`Failed to generate Google Cloud Domain Wide Delegation OAuth 2.0 Access Token: ${msg}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -171,7 +169,9 @@ export class IAMCredentialsClient {
|
|||||||
delegates,
|
delegates,
|
||||||
includeEmail,
|
includeEmail,
|
||||||
}: GenerateIDTokenParameters): Promise<string> {
|
}: GenerateIDTokenParameters): Promise<string> {
|
||||||
const pth = `${this.#endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateIdToken`;
|
const logger = this._logger.withNamespace('generateIDToken');
|
||||||
|
|
||||||
|
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateIdToken`;
|
||||||
|
|
||||||
const headers = { Authorization: `Bearer ${this.#authToken}` };
|
const headers = { Authorization: `Bearer ${this.#authToken}` };
|
||||||
|
|
||||||
@ -183,7 +183,7 @@ export class IAMCredentialsClient {
|
|||||||
body.delegates = delegates;
|
body.delegates = delegates;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#logger.withNamespace('generateIDToken').debug({
|
logger.debug(`Built request`, {
|
||||||
method: `POST`,
|
method: `POST`,
|
||||||
path: pth,
|
path: pth,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
@ -191,7 +191,7 @@ export class IAMCredentialsClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.#httpClient.postJson<{ token: string }>(pth, body, headers);
|
const resp = await this._httpClient.postJson<{ token: string }>(pth, body, headers);
|
||||||
const statusCode = resp.statusCode || 500;
|
const statusCode = resp.statusCode || 500;
|
||||||
if (statusCode < 200 || statusCode > 299) {
|
if (statusCode < 200 || statusCode > 299) {
|
||||||
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
|
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
|
||||||
@ -203,19 +203,10 @@ export class IAMCredentialsClient {
|
|||||||
}
|
}
|
||||||
return result.token;
|
return result.token;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const msg = errorMessage(err);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to generate Google Cloud OpenID Connect ID token for ${serviceAccount}: ${err}`,
|
`Failed to generate Google Cloud OpenID Connect ID token for ${serviceAccount}: ${msg}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { AuthClient } from './client/auth_client';
|
|
||||||
export {
|
|
||||||
ServiceAccountKeyClientParameters,
|
|
||||||
ServiceAccountKeyClient,
|
|
||||||
} from './client/credentials_json_client';
|
|
||||||
export {
|
|
||||||
WorkloadIdentityFederationClientParameters,
|
|
||||||
WorkloadIdentityFederationClient,
|
|
||||||
} from './client/workload_identity_client';
|
|
@ -15,6 +15,7 @@
|
|||||||
import { createSign } from 'crypto';
|
import { createSign } from 'crypto';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
errorMessage,
|
||||||
isServiceAccountKey,
|
isServiceAccountKey,
|
||||||
parseCredential,
|
parseCredential,
|
||||||
ServiceAccountKey,
|
ServiceAccountKey,
|
||||||
@ -22,8 +23,7 @@ import {
|
|||||||
writeSecureFile,
|
writeSecureFile,
|
||||||
} from '@google-github-actions/actions-utils';
|
} from '@google-github-actions/actions-utils';
|
||||||
|
|
||||||
import { AuthClient } from './auth_client';
|
import { AuthClient, Client } from './client';
|
||||||
import { expandEndpoint } from '../utils';
|
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,6 +31,9 @@ import { Logger } from '../logger';
|
|||||||
* ServiceAccountKeyClient.
|
* ServiceAccountKeyClient.
|
||||||
*/
|
*/
|
||||||
export interface ServiceAccountKeyClientParameters {
|
export interface ServiceAccountKeyClientParameters {
|
||||||
|
readonly logger: Logger;
|
||||||
|
readonly universe: string;
|
||||||
|
|
||||||
readonly serviceAccountKey: string;
|
readonly serviceAccountKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,33 +41,26 @@ export interface ServiceAccountKeyClientParameters {
|
|||||||
* ServiceAccountKeyClient is an authentication client that expects a Service
|
* ServiceAccountKeyClient is an authentication client that expects a Service
|
||||||
* Account Key JSON file.
|
* Account Key JSON file.
|
||||||
*/
|
*/
|
||||||
export class ServiceAccountKeyClient implements AuthClient {
|
export class ServiceAccountKeyClient extends Client implements AuthClient {
|
||||||
readonly #logger: Logger;
|
|
||||||
readonly #serviceAccountKey: ServiceAccountKey;
|
readonly #serviceAccountKey: ServiceAccountKey;
|
||||||
|
|
||||||
readonly #universe: string = 'googleapis.com';
|
|
||||||
readonly #endpoints = {
|
|
||||||
iamcredentials: 'https://iamcredentials.{universe}/v1',
|
|
||||||
};
|
|
||||||
readonly #audience: string;
|
readonly #audience: string;
|
||||||
|
|
||||||
constructor(logger: Logger, opts: ServiceAccountKeyClientParameters) {
|
constructor(opts: ServiceAccountKeyClientParameters) {
|
||||||
this.#logger = logger.withNamespace(this.constructor.name);
|
super({
|
||||||
|
logger: opts.logger,
|
||||||
|
universe: opts.universe,
|
||||||
|
child: `ServiceAccountKeyClient`,
|
||||||
|
});
|
||||||
|
|
||||||
const serviceAccountKey = parseCredential(opts.serviceAccountKey);
|
const serviceAccountKey = parseCredential(opts.serviceAccountKey);
|
||||||
if (!isServiceAccountKey(serviceAccountKey)) {
|
if (!isServiceAccountKey(serviceAccountKey)) {
|
||||||
throw new Error(`Provided credential is not a valid Google Service Account Key JSON`);
|
throw new Error(`Provided credential is not a valid Google Service Account Key JSON`);
|
||||||
}
|
}
|
||||||
this.#serviceAccountKey = serviceAccountKey;
|
this.#serviceAccountKey = serviceAccountKey;
|
||||||
|
this._logger.debug(`Parsed service account key`, serviceAccountKey.client_email);
|
||||||
|
|
||||||
const endpoints = this.#endpoints;
|
this.#audience = new URL(this._endpoints.iamcredentials).origin + `/`;
|
||||||
for (const key of Object.keys(this.#endpoints) as Array<keyof typeof endpoints>) {
|
this._logger.debug(`Computed audience`, this.#audience);
|
||||||
this.#endpoints[key] = expandEndpoint(this.#endpoints[key], this.#universe);
|
|
||||||
}
|
|
||||||
this.#logger.debug(`Computed endpoints`, this.#endpoints);
|
|
||||||
|
|
||||||
this.#audience = new URL(this.#endpoints.iamcredentials).origin + `/`;
|
|
||||||
this.#logger.debug(`Computed audience`, this.#audience);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,7 +70,8 @@ export class ServiceAccountKeyClient implements AuthClient {
|
|||||||
* use the JWT to call other endpoints without calling iamcredentials.
|
* use the JWT to call other endpoints without calling iamcredentials.
|
||||||
*/
|
*/
|
||||||
async getToken(): Promise<string> {
|
async getToken(): Promise<string> {
|
||||||
try {
|
const logger = this._logger.withNamespace('getToken');
|
||||||
|
|
||||||
const now = Math.floor(new Date().getTime() / 1000);
|
const now = Math.floor(new Date().getTime() / 1000);
|
||||||
|
|
||||||
const claims = {
|
const claims = {
|
||||||
@ -85,14 +82,16 @@ export class ServiceAccountKeyClient implements AuthClient {
|
|||||||
exp: now + 3599,
|
exp: now + 3599,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.#logger.withNamespace('getToken').debug({
|
logger.debug(`Built jwt`, {
|
||||||
claims: claims,
|
claims: claims,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
return await this.signJWT(claims);
|
return await this.signJWT(claims);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const msg = errorMessage(err);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to sign auth token using ${this.#serviceAccountKey.client_email}: ${err}`,
|
`Failed to sign auth token using ${this.#serviceAccountKey.client_email}: ${msg}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,6 +100,8 @@ export class ServiceAccountKeyClient implements AuthClient {
|
|||||||
* signJWT signs a JWT using the Service Account's private key.
|
* signJWT signs a JWT using the Service Account's private key.
|
||||||
*/
|
*/
|
||||||
async signJWT(claims: any): Promise<string> {
|
async signJWT(claims: any): Promise<string> {
|
||||||
|
const logger = this._logger.withNamespace('signJWT');
|
||||||
|
|
||||||
const header = {
|
const header = {
|
||||||
alg: `RS256`,
|
alg: `RS256`,
|
||||||
typ: `JWT`,
|
typ: `JWT`,
|
||||||
@ -109,18 +110,25 @@ export class ServiceAccountKeyClient implements AuthClient {
|
|||||||
|
|
||||||
const message = toBase64(JSON.stringify(header)) + `.` + toBase64(JSON.stringify(claims));
|
const message = toBase64(JSON.stringify(header)) + `.` + toBase64(JSON.stringify(claims));
|
||||||
|
|
||||||
this.#logger.withNamespace('signJWT').debug({
|
logger.debug(`Built jwt`, {
|
||||||
header: header,
|
header: header,
|
||||||
claims: claims,
|
claims: claims,
|
||||||
message: message,
|
message: message,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
const signer = createSign(`RSA-SHA256`);
|
const signer = createSign(`RSA-SHA256`);
|
||||||
signer.write(message);
|
signer.write(message);
|
||||||
signer.end();
|
signer.end();
|
||||||
|
|
||||||
const signature = signer.sign(this.#serviceAccountKey.private_key);
|
const signature = signer.sign(this.#serviceAccountKey.private_key);
|
||||||
return message + '.' + toBase64(signature);
|
return message + '.' + toBase64(signature);
|
||||||
|
} catch (err) {
|
||||||
|
const msg = errorMessage(err);
|
||||||
|
throw new Error(
|
||||||
|
`Failed to sign jwt using private key for ${this.#serviceAccountKey.client_email}: ${msg}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -128,7 +136,12 @@ export class ServiceAccountKeyClient implements AuthClient {
|
|||||||
* the specified outputPath.
|
* the specified outputPath.
|
||||||
*/
|
*/
|
||||||
async createCredentialsFile(outputPath: string): Promise<string> {
|
async createCredentialsFile(outputPath: string): Promise<string> {
|
||||||
this.#logger.withNamespace('createCredentialsFile').debug({ outputPath: outputPath });
|
const logger = this._logger.withNamespace('createCredentialsFile');
|
||||||
|
|
||||||
|
logger.debug(`Creating credentials`, {
|
||||||
|
outputPath: outputPath,
|
||||||
|
});
|
||||||
|
|
||||||
return await writeSecureFile(outputPath, JSON.stringify(this.#serviceAccountKey));
|
return await writeSecureFile(outputPath, JSON.stringify(this.#serviceAccountKey));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -12,12 +12,9 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { HttpClient } from '@actions/http-client';
|
import { errorMessage, writeSecureFile } from '@google-github-actions/actions-utils';
|
||||||
|
|
||||||
import { writeSecureFile } from '@google-github-actions/actions-utils';
|
import { AuthClient, Client } from './client';
|
||||||
|
|
||||||
import { AuthClient } from './auth_client';
|
|
||||||
import { expandEndpoint, userAgent } from '../utils';
|
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,6 +22,9 @@ import { Logger } from '../logger';
|
|||||||
* WorkloadIdentityFederationClient.
|
* WorkloadIdentityFederationClient.
|
||||||
*/
|
*/
|
||||||
export interface WorkloadIdentityFederationClientParameters {
|
export interface WorkloadIdentityFederationClientParameters {
|
||||||
|
readonly logger: Logger;
|
||||||
|
readonly universe: string;
|
||||||
|
|
||||||
readonly githubOIDCToken: string;
|
readonly githubOIDCToken: string;
|
||||||
readonly githubOIDCTokenRequestURL: string;
|
readonly githubOIDCTokenRequestURL: string;
|
||||||
readonly githubOIDCTokenRequestToken: string;
|
readonly githubOIDCTokenRequestToken: string;
|
||||||
@ -38,32 +38,24 @@ export interface WorkloadIdentityFederationClientParameters {
|
|||||||
* WorkloadIdentityFederationClient is an authentication client that configures
|
* WorkloadIdentityFederationClient is an authentication client that configures
|
||||||
* a Workload Identity authentication scheme.
|
* a Workload Identity authentication scheme.
|
||||||
*/
|
*/
|
||||||
export class WorkloadIdentityFederationClient implements AuthClient {
|
export class WorkloadIdentityFederationClient extends Client implements AuthClient {
|
||||||
readonly #logger: Logger;
|
|
||||||
readonly #httpClient: HttpClient;
|
|
||||||
|
|
||||||
readonly #githubOIDCToken: string;
|
readonly #githubOIDCToken: string;
|
||||||
readonly #githubOIDCTokenRequestURL: string;
|
readonly #githubOIDCTokenRequestURL: string;
|
||||||
readonly #githubOIDCTokenRequestToken: string;
|
readonly #githubOIDCTokenRequestToken: string;
|
||||||
readonly #githubOIDCTokenAudience: string;
|
readonly #githubOIDCTokenAudience: string;
|
||||||
readonly #workloadIdentityProviderName: string;
|
readonly #workloadIdentityProviderName: string;
|
||||||
readonly #serviceAccount?: string;
|
readonly #serviceAccount?: string;
|
||||||
|
readonly #audience: string;
|
||||||
|
|
||||||
#cachedToken?: string;
|
#cachedToken?: string;
|
||||||
#cachedAt?: number;
|
#cachedAt?: number;
|
||||||
|
|
||||||
readonly #universe: string = 'googleapis.com';
|
constructor(opts: WorkloadIdentityFederationClientParameters) {
|
||||||
readonly #endpoints = {
|
super({
|
||||||
iam: 'https://iam.{universe}/v1',
|
logger: opts.logger,
|
||||||
iamcredentials: 'https://iamcredentials.{universe}/v1',
|
universe: opts.universe,
|
||||||
sts: 'https://sts.{universe}/v1',
|
child: `WorkloadIdentityFederationClient`,
|
||||||
www: 'https://www.{universe}',
|
});
|
||||||
};
|
|
||||||
readonly #audience: string;
|
|
||||||
|
|
||||||
constructor(logger: Logger, opts: WorkloadIdentityFederationClientParameters) {
|
|
||||||
this.#logger = logger.withNamespace(this.constructor.name);
|
|
||||||
this.#httpClient = new HttpClient(userAgent);
|
|
||||||
|
|
||||||
this.#githubOIDCToken = opts.githubOIDCToken;
|
this.#githubOIDCToken = opts.githubOIDCToken;
|
||||||
this.#githubOIDCTokenRequestURL = opts.githubOIDCTokenRequestURL;
|
this.#githubOIDCTokenRequestURL = opts.githubOIDCTokenRequestURL;
|
||||||
@ -72,15 +64,9 @@ export class WorkloadIdentityFederationClient implements AuthClient {
|
|||||||
this.#workloadIdentityProviderName = opts.workloadIdentityProviderName;
|
this.#workloadIdentityProviderName = opts.workloadIdentityProviderName;
|
||||||
this.#serviceAccount = opts.serviceAccount;
|
this.#serviceAccount = opts.serviceAccount;
|
||||||
|
|
||||||
const endpoints = this.#endpoints;
|
const iamHost = new URL(this._endpoints.iam).host;
|
||||||
for (const key of Object.keys(this.#endpoints) as Array<keyof typeof endpoints>) {
|
|
||||||
this.#endpoints[key] = expandEndpoint(this.#endpoints[key], this.#universe);
|
|
||||||
}
|
|
||||||
this.#logger.debug(`Computed endpoints`, this.#endpoints);
|
|
||||||
|
|
||||||
const iamHost = new URL(this.#endpoints.iam).host;
|
|
||||||
this.#audience = `//${iamHost}/${this.#workloadIdentityProviderName}`;
|
this.#audience = `//${iamHost}/${this.#workloadIdentityProviderName}`;
|
||||||
this.#logger.debug(`Computed audience`, this.#audience);
|
this._logger.debug(`Computed audience`, this.#audience);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,31 +77,36 @@ export class WorkloadIdentityFederationClient implements AuthClient {
|
|||||||
* impersonation.
|
* impersonation.
|
||||||
*/
|
*/
|
||||||
async getToken(): Promise<string> {
|
async getToken(): Promise<string> {
|
||||||
|
const logger = this._logger.withNamespace(`getToken`);
|
||||||
|
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
if (this.#cachedToken && this.#cachedAt && now - this.#cachedAt > 60_000) {
|
if (this.#cachedToken && this.#cachedAt && now - this.#cachedAt > 60_000) {
|
||||||
this.#logger.debug(`Using cached token`);
|
logger.debug(`Using cached token`, {
|
||||||
|
now: now,
|
||||||
|
cachedAt: this.#cachedAt,
|
||||||
|
});
|
||||||
return this.#cachedToken;
|
return this.#cachedToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pth = `${this.#endpoints.sts}/token`;
|
const pth = `${this._endpoints.sts}/token`;
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
audience: this.#audience,
|
audience: this.#audience,
|
||||||
grantType: `urn:ietf:params:oauth:grant-type:token-exchange`,
|
grantType: `urn:ietf:params:oauth:grant-type:token-exchange`,
|
||||||
requestedTokenType: `urn:ietf:params:oauth:token-type:access_token`,
|
requestedTokenType: `urn:ietf:params:oauth:token-type:access_token`,
|
||||||
scope: `${this.#endpoints.www}/auth/cloud-platform`,
|
scope: `${this._endpoints.www}/auth/cloud-platform`,
|
||||||
subjectTokenType: `urn:ietf:params:oauth:token-type:jwt`,
|
subjectTokenType: `urn:ietf:params:oauth:token-type:jwt`,
|
||||||
subjectToken: this.#githubOIDCToken,
|
subjectToken: this.#githubOIDCToken,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.#logger.withNamespace('getToken').debug({
|
logger.debug(`Built request`, {
|
||||||
method: `POST`,
|
method: `POST`,
|
||||||
path: pth,
|
path: pth,
|
||||||
body: body,
|
body: body,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.#httpClient.postJson<{ access_token: string }>(pth, body);
|
const resp = await this._httpClient.postJson<{ access_token: string }>(pth, body);
|
||||||
const statusCode = resp.statusCode || 500;
|
const statusCode = resp.statusCode || 500;
|
||||||
if (statusCode < 200 || statusCode > 299) {
|
if (statusCode < 200 || statusCode > 299) {
|
||||||
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
|
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
|
||||||
@ -130,8 +121,9 @@ export class WorkloadIdentityFederationClient implements AuthClient {
|
|||||||
this.#cachedAt = now;
|
this.#cachedAt = now;
|
||||||
return result.access_token;
|
return result.access_token;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const msg = errorMessage(err);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to generate Google Cloud federated token for ${this.#audience}: ${err}`,
|
`Failed to generate Google Cloud federated token for ${this.#audience}: ${msg}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,11 +132,13 @@ export class WorkloadIdentityFederationClient implements AuthClient {
|
|||||||
* signJWT signs a JWT using the Service Account's private key.
|
* signJWT signs a JWT using the Service Account's private key.
|
||||||
*/
|
*/
|
||||||
async signJWT(claims: any): Promise<string> {
|
async signJWT(claims: any): Promise<string> {
|
||||||
|
const logger = this._logger.withNamespace(`signJWT`);
|
||||||
|
|
||||||
if (!this.#serviceAccount) {
|
if (!this.#serviceAccount) {
|
||||||
throw new Error(`Cannot sign JWTs without specifying a service account`);
|
throw new Error(`Cannot sign JWTs without specifying a service account`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pth = `${this.#endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:signJwt`;
|
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:signJwt`;
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
Authorization: `Bearer ${this.getToken()}`,
|
Authorization: `Bearer ${this.getToken()}`,
|
||||||
@ -154,7 +148,7 @@ export class WorkloadIdentityFederationClient implements AuthClient {
|
|||||||
payload: claims,
|
payload: claims,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.#logger.withNamespace('signJWT').debug({
|
logger.debug(`Built request`, {
|
||||||
method: `POST`,
|
method: `POST`,
|
||||||
path: pth,
|
path: pth,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
@ -162,7 +156,7 @@ export class WorkloadIdentityFederationClient implements AuthClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await this.#httpClient.postJson<{ signedJwt: string }>(pth, body, headers);
|
const resp = await this._httpClient.postJson<{ signedJwt: string }>(pth, body, headers);
|
||||||
const statusCode = resp.statusCode || 500;
|
const statusCode = resp.statusCode || 500;
|
||||||
if (statusCode < 200 || statusCode > 299) {
|
if (statusCode < 200 || statusCode > 299) {
|
||||||
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
|
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
|
||||||
@ -174,7 +168,8 @@ export class WorkloadIdentityFederationClient implements AuthClient {
|
|||||||
}
|
}
|
||||||
return result.signedJwt;
|
return result.signedJwt;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(`Failed to sign JWT using ${this.#serviceAccount}: ${err}`);
|
const msg = errorMessage(err);
|
||||||
|
throw new Error(`Failed to sign JWT using ${this.#serviceAccount}: ${msg}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,6 +178,8 @@ export class WorkloadIdentityFederationClient implements AuthClient {
|
|||||||
* to disk at the specific outputPath.
|
* to disk at the specific outputPath.
|
||||||
*/
|
*/
|
||||||
async createCredentialsFile(outputPath: string): Promise<string> {
|
async createCredentialsFile(outputPath: string): Promise<string> {
|
||||||
|
const logger = this._logger.withNamespace(`createCredentialsFile`);
|
||||||
|
|
||||||
const requestURL = new URL(this.#githubOIDCTokenRequestURL);
|
const requestURL = new URL(this.#githubOIDCTokenRequestURL);
|
||||||
|
|
||||||
// Append the audience value to the request.
|
// Append the audience value to the request.
|
||||||
@ -194,7 +191,7 @@ export class WorkloadIdentityFederationClient implements AuthClient {
|
|||||||
type: `external_account`,
|
type: `external_account`,
|
||||||
audience: this.#audience,
|
audience: this.#audience,
|
||||||
subject_token_type: `urn:ietf:params:oauth:token-type:jwt`,
|
subject_token_type: `urn:ietf:params:oauth:token-type:jwt`,
|
||||||
token_url: `${this.#endpoints.sts}/token`,
|
token_url: `${this._endpoints.sts}/token`,
|
||||||
credential_source: {
|
credential_source: {
|
||||||
url: requestURL,
|
url: requestURL,
|
||||||
headers: {
|
headers: {
|
||||||
@ -210,10 +207,15 @@ export class WorkloadIdentityFederationClient implements AuthClient {
|
|||||||
// Only request impersonation if a service account was given, otherwise use
|
// Only request impersonation if a service account was given, otherwise use
|
||||||
// the WIF identity directly.
|
// the WIF identity directly.
|
||||||
if (this.#serviceAccount) {
|
if (this.#serviceAccount) {
|
||||||
data.service_account_impersonation_url = `${this.#endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:generateAccessToken`;
|
const impersonationURL = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:generateAccessToken`;
|
||||||
|
logger.debug(`Enabling service account impersonation via ${impersonationURL}`);
|
||||||
|
data.service_account_impersonation_url = impersonationURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#logger.withNamespace('createCredentialsFile').debug({ outputPath: outputPath });
|
logger.debug(`Creating credentials`, {
|
||||||
|
outputPath: outputPath,
|
||||||
|
});
|
||||||
|
|
||||||
return await writeSecureFile(outputPath, JSON.stringify(data));
|
return await writeSecureFile(outputPath, JSON.stringify(data));
|
||||||
}
|
}
|
||||||
}
|
}
|
18
src/main.ts
18
src/main.ts
@ -39,7 +39,7 @@ import {
|
|||||||
IAMCredentialsClient,
|
IAMCredentialsClient,
|
||||||
ServiceAccountKeyClient,
|
ServiceAccountKeyClient,
|
||||||
WorkloadIdentityFederationClient,
|
WorkloadIdentityFederationClient,
|
||||||
} from './base';
|
} from './client/client';
|
||||||
import { Logger } from './logger';
|
import { Logger } from './logger';
|
||||||
import {
|
import {
|
||||||
buildDomainWideDelegationJWT,
|
buildDomainWideDelegationJWT,
|
||||||
@ -112,6 +112,7 @@ async function main(logger: Logger) {
|
|||||||
const exportEnvironmentVariables = getBooleanInput(`export_environment_variables`);
|
const exportEnvironmentVariables = getBooleanInput(`export_environment_variables`);
|
||||||
const tokenFormat = getInput(`token_format`);
|
const tokenFormat = getInput(`token_format`);
|
||||||
const delegates = parseCSV(getInput(`delegates`));
|
const delegates = parseCSV(getInput(`delegates`));
|
||||||
|
const universe = getInput(`universe`);
|
||||||
|
|
||||||
// Ensure exactly one of workload_identity_provider and credentials_json was
|
// Ensure exactly one of workload_identity_provider and credentials_json was
|
||||||
// provided.
|
// provided.
|
||||||
@ -138,7 +139,10 @@ async function main(logger: Logger) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const oidcToken = await getIDToken(oidcTokenAudience);
|
const oidcToken = await getIDToken(oidcTokenAudience);
|
||||||
client = new WorkloadIdentityFederationClient(logger, {
|
client = new WorkloadIdentityFederationClient({
|
||||||
|
logger: logger,
|
||||||
|
universe: universe,
|
||||||
|
|
||||||
githubOIDCToken: oidcToken,
|
githubOIDCToken: oidcToken,
|
||||||
githubOIDCTokenRequestURL: oidcTokenRequestURL,
|
githubOIDCTokenRequestURL: oidcTokenRequestURL,
|
||||||
githubOIDCTokenRequestToken: oidcTokenRequestToken,
|
githubOIDCTokenRequestToken: oidcTokenRequestToken,
|
||||||
@ -148,7 +152,10 @@ async function main(logger: Logger) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Using credentials JSON`);
|
logger.debug(`Using credentials JSON`);
|
||||||
client = new ServiceAccountKeyClient(logger, {
|
client = new ServiceAccountKeyClient({
|
||||||
|
logger: logger,
|
||||||
|
universe: universe,
|
||||||
|
|
||||||
serviceAccountKey: credentialsJSON,
|
serviceAccountKey: credentialsJSON,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -245,7 +252,10 @@ async function main(logger: Logger) {
|
|||||||
setOutput('auth_token', authToken);
|
setOutput('auth_token', authToken);
|
||||||
|
|
||||||
// Create the credential client, we might not use it, but it's basically free.
|
// Create the credential client, we might not use it, but it's basically free.
|
||||||
const iamCredentialsClient = new IAMCredentialsClient(logger, {
|
const iamCredentialsClient = new IAMCredentialsClient({
|
||||||
|
logger: logger,
|
||||||
|
universe: universe,
|
||||||
|
|
||||||
authToken: authToken,
|
authToken: authToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -134,13 +134,6 @@ export function projectIDFromServiceAccountEmail(serviceAccount?: string): strin
|
|||||||
return addressParts[0];
|
return addressParts[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* expandEndpoint expands the input url relative to the universe.
|
|
||||||
*/
|
|
||||||
export function expandEndpoint(input: string, universe: string): string {
|
|
||||||
return (input || '').replace(/{universe}/g, universe).replace(/\/+$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* generateCredentialsFilename creates a predictable filename under which
|
* generateCredentialsFilename creates a predictable filename under which
|
||||||
* credentials are written. This string is the filename, not the filepath. It must match the format:
|
* credentials are written. This string is the filename, not the filepath. It must match the format:
|
||||||
|
@ -22,7 +22,7 @@ import { tmpdir } from 'os';
|
|||||||
import { randomFilename } from '@google-github-actions/actions-utils';
|
import { randomFilename } from '@google-github-actions/actions-utils';
|
||||||
|
|
||||||
import { NullLogger } from '../../src/logger';
|
import { NullLogger } from '../../src/logger';
|
||||||
import { ServiceAccountKeyClient } from '../../src/client/credentials_json_client';
|
import { ServiceAccountKeyClient } from '../../src/client/service_account_key_json';
|
||||||
|
|
||||||
// Yes, this is a real private key. No, it's not valid for authenticating
|
// Yes, this is a real private key. No, it's not valid for authenticating
|
||||||
// Google Cloud.
|
// Google Cloud.
|
||||||
@ -45,7 +45,9 @@ describe('ServiceAccountKeyClient', () => {
|
|||||||
describe('#parseServiceAccountKeyJSON', () => {
|
describe('#parseServiceAccountKeyJSON', () => {
|
||||||
it('throws exception on invalid json', async () => {
|
it('throws exception on invalid json', async () => {
|
||||||
assert.rejects(async () => {
|
assert.rejects(async () => {
|
||||||
new ServiceAccountKeyClient(new NullLogger(), {
|
new ServiceAccountKeyClient({
|
||||||
|
logger: new NullLogger(),
|
||||||
|
universe: 'googleapis.com',
|
||||||
serviceAccountKey: 'invalid json',
|
serviceAccountKey: 'invalid json',
|
||||||
});
|
});
|
||||||
}, SyntaxError);
|
}, SyntaxError);
|
||||||
@ -53,7 +55,9 @@ describe('ServiceAccountKeyClient', () => {
|
|||||||
|
|
||||||
it('handles base64', async () => {
|
it('handles base64', async () => {
|
||||||
assert.rejects(async () => {
|
assert.rejects(async () => {
|
||||||
new ServiceAccountKeyClient(new NullLogger(), {
|
new ServiceAccountKeyClient({
|
||||||
|
logger: new NullLogger(),
|
||||||
|
universe: 'googleapis.com',
|
||||||
serviceAccountKey: 'base64',
|
serviceAccountKey: 'base64',
|
||||||
});
|
});
|
||||||
}, SyntaxError);
|
}, SyntaxError);
|
||||||
@ -62,7 +66,9 @@ describe('ServiceAccountKeyClient', () => {
|
|||||||
|
|
||||||
describe('#getToken', () => {
|
describe('#getToken', () => {
|
||||||
it('gets a token', async () => {
|
it('gets a token', async () => {
|
||||||
const client = new ServiceAccountKeyClient(new NullLogger(), {
|
const client = new ServiceAccountKeyClient({
|
||||||
|
logger: new NullLogger(),
|
||||||
|
universe: 'googleapis.com',
|
||||||
serviceAccountKey: credentialsJSON,
|
serviceAccountKey: credentialsJSON,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -73,7 +79,9 @@ describe('ServiceAccountKeyClient', () => {
|
|||||||
|
|
||||||
describe('#signJWT', () => {
|
describe('#signJWT', () => {
|
||||||
it('signs a jwt', async () => {
|
it('signs a jwt', async () => {
|
||||||
const client = new ServiceAccountKeyClient(new NullLogger(), {
|
const client = new ServiceAccountKeyClient({
|
||||||
|
logger: new NullLogger(),
|
||||||
|
universe: 'googleapis.com',
|
||||||
serviceAccountKey: credentialsJSON,
|
serviceAccountKey: credentialsJSON,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -85,7 +93,9 @@ describe('ServiceAccountKeyClient', () => {
|
|||||||
describe('#createCredentialsFile', () => {
|
describe('#createCredentialsFile', () => {
|
||||||
it('writes the file', async () => {
|
it('writes the file', async () => {
|
||||||
const outputFile = pathjoin(tmpdir(), randomFilename());
|
const outputFile = pathjoin(tmpdir(), randomFilename());
|
||||||
const client = new ServiceAccountKeyClient(new NullLogger(), {
|
const client = new ServiceAccountKeyClient({
|
||||||
|
logger: new NullLogger(),
|
||||||
|
universe: 'googleapis.com',
|
||||||
serviceAccountKey: credentialsJSON,
|
serviceAccountKey: credentialsJSON,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -22,13 +22,16 @@ import { readFileSync } from 'fs';
|
|||||||
import { randomFilename } from '@google-github-actions/actions-utils';
|
import { randomFilename } from '@google-github-actions/actions-utils';
|
||||||
|
|
||||||
import { NullLogger } from '../../src/logger';
|
import { NullLogger } from '../../src/logger';
|
||||||
import { WorkloadIdentityFederationClient } from '../../src/client/workload_identity_client';
|
import { WorkloadIdentityFederationClient } from '../../src/client/workload_identity_federation';
|
||||||
|
|
||||||
describe('WorkloadIdentityFederationClient', () => {
|
describe('WorkloadIdentityFederationClient', () => {
|
||||||
describe('#createCredentialsFile', () => {
|
describe('#createCredentialsFile', () => {
|
||||||
it('writes the file', async () => {
|
it('writes the file', async () => {
|
||||||
const outputFile = pathjoin(tmpdir(), randomFilename());
|
const outputFile = pathjoin(tmpdir(), randomFilename());
|
||||||
const client = new WorkloadIdentityFederationClient(new NullLogger(), {
|
const client = new WorkloadIdentityFederationClient({
|
||||||
|
logger: new NullLogger(),
|
||||||
|
universe: 'googleapis.com',
|
||||||
|
|
||||||
githubOIDCToken: 'my-token',
|
githubOIDCToken: 'my-token',
|
||||||
githubOIDCTokenRequestURL: 'https://example.com/',
|
githubOIDCTokenRequestURL: 'https://example.com/',
|
||||||
githubOIDCTokenRequestToken: 'token',
|
githubOIDCTokenRequestToken: 'token',
|
||||||
@ -62,7 +65,10 @@ describe('WorkloadIdentityFederationClient', () => {
|
|||||||
|
|
||||||
it('writes the file with impersonation', async () => {
|
it('writes the file with impersonation', async () => {
|
||||||
const outputFile = pathjoin(tmpdir(), randomFilename());
|
const outputFile = pathjoin(tmpdir(), randomFilename());
|
||||||
const client = new WorkloadIdentityFederationClient(new NullLogger(), {
|
const client = new WorkloadIdentityFederationClient({
|
||||||
|
logger: new NullLogger(),
|
||||||
|
universe: 'googleapis.com',
|
||||||
|
|
||||||
githubOIDCToken: 'my-token',
|
githubOIDCToken: 'my-token',
|
||||||
githubOIDCTokenRequestURL: 'https://example.com/',
|
githubOIDCTokenRequestURL: 'https://example.com/',
|
||||||
githubOIDCTokenRequestToken: 'token',
|
githubOIDCTokenRequestToken: 'token',
|
||||||
|
@ -19,7 +19,6 @@ import {
|
|||||||
buildDomainWideDelegationJWT,
|
buildDomainWideDelegationJWT,
|
||||||
computeProjectID,
|
computeProjectID,
|
||||||
computeServiceAccountEmail,
|
computeServiceAccountEmail,
|
||||||
expandEndpoint,
|
|
||||||
generateCredentialsFilename,
|
generateCredentialsFilename,
|
||||||
projectIDFromServiceAccountEmail,
|
projectIDFromServiceAccountEmail,
|
||||||
} from '../src/utils';
|
} from '../src/utils';
|
||||||
@ -163,52 +162,6 @@ describe('Utils', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#expandEndpoint', async () => {
|
|
||||||
const cases = [
|
|
||||||
{
|
|
||||||
name: 'empty',
|
|
||||||
endpoint: '',
|
|
||||||
universe: '',
|
|
||||||
exp: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'no match',
|
|
||||||
endpoint: 'https://www.googleapis.com',
|
|
||||||
universe: 'foobar',
|
|
||||||
exp: 'https://www.googleapis.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'removes trailing slash',
|
|
||||||
endpoint: 'https://www.googleapis.com/',
|
|
||||||
exp: 'https://www.googleapis.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'removes trailing slashes',
|
|
||||||
endpoint: 'https://www.googleapis.com/////',
|
|
||||||
exp: 'https://www.googleapis.com',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'replaces {universe}',
|
|
||||||
endpoint: 'https://www.{universe}',
|
|
||||||
universe: 'foo.bar',
|
|
||||||
exp: 'https://www.foo.bar',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'replaces multiple {universe}',
|
|
||||||
endpoint: 'https://www.{universe}.{universe}',
|
|
||||||
universe: 'foo.bar',
|
|
||||||
exp: 'https://www.foo.bar.foo.bar',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
cases.forEach(async (tc) => {
|
|
||||||
it(tc.name, async () => {
|
|
||||||
const result = expandEndpoint(tc.endpoint, tc.universe || '');
|
|
||||||
assert.deepStrictEqual(result, tc.exp);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#generateCredentialsFilename', async () => {
|
describe('#generateCredentialsFilename', async () => {
|
||||||
it('returns a string matching the regex', () => {
|
it('returns a string matching the regex', () => {
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
|
Loading…
Reference in New Issue
Block a user