From e0122d6a976dd6794fe6e866adfcb3c11f828b36 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 14 May 2024 12:46:35 -0400 Subject: [PATCH] Add request_reason for plumbing though user-supplied audit information (#413) Fixes https://github.com/google-github-actions/auth/issues/412 --- README.md | 11 ++++++++- action.yml | 6 +++++ src/client/client.ts | 21 +++++++++++++---- src/client/iamcredentials.ts | 26 +++++++++------------- src/client/service_account_key_json.ts | 14 +++--------- src/client/workload_identity_federation.ts | 22 +++++++----------- src/main.ts | 3 +++ 7 files changed, 58 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index b8c2b225..131d1ea3 100644 --- a/README.md +++ b/README.md @@ -269,13 +269,22 @@ regardless of the authentication mechanism. 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_` where endpoint is the API endpoint to override. This only applies to the `auth` action and does not persist to other steps. For example: + You can also override individual API endpoints by setting the environment + variable `GHA_ENDPOINT_OVERRIDE_` 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' ``` +- `request_reason`: (Optional) An optional Reason Request [System + Parameter](https://cloud.google.com/apis/docs/system-parameters) for each + API call made by the GitHub Action. This will inject the + "X-Goog-Request-Reason" HTTP header, which will provide user-supplied + information in Google Cloud audit logs. + - `cleanup_credentials`: (Optional) If true, the action will remove any created credentials from the filesystem upon completion. This only applies if "create_credentials_file" is true. The default is true. diff --git a/action.yml b/action.yml index 7f8ed05b..78ae4ccf 100644 --- a/action.yml +++ b/action.yml @@ -102,6 +102,12 @@ inputs: Hosted Cloud should set this to their universe address. required: false default: 'googleapis.com' + request_reason: + description: |- + An optional Reason Request System Parameter for each API call made by the + GitHub Action. This will inject the "X-Goog-Request-Reason" HTTP header, + which will provide user-supplied information in Google Cloud audit logs. + required: false cleanup_credentials: description: |- If true, the action will remove any created credentials from the diff --git a/src/client/client.ts b/src/client/client.ts index 0876c825..afa43d7a 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -45,12 +45,13 @@ export interface AuthClient { export interface ClientParameters { logger: Logger; universe: string; - child: string; + requestReason?: string; } -export class Client { +export abstract class Client { protected readonly _logger: Logger; protected readonly _httpClient: HttpClient; + private readonly _requestReason: string | undefined; protected readonly _endpoints = { iam: 'https://iam.{universe}/v1', @@ -60,8 +61,8 @@ export class Client { www: 'https://www.{universe}', }; - constructor(opts: ClientParameters) { - this._logger = opts.logger.withNamespace(opts.child); + constructor(child: string, opts: ClientParameters) { + this._logger = opts.logger.withNamespace(child); // Create the http client with our user agent. this._httpClient = new HttpClient(userAgent, undefined, { @@ -73,6 +74,18 @@ export class Client { }); this._endpoints = expandUniverseEndpoints(this._endpoints, opts.universe); + this._requestReason = opts.requestReason; + } + + /** + * _headers returns any added headers to apply to HTTP API calls. + */ + protected _headers(): Record { + const headers: Record = {}; + if (this._requestReason) { + headers['X-Goog-Request-Reason'] = this._requestReason; + } + return headers; } } export { IAMCredentialsClient, IAMCredentialsClientParameters } from './iamcredentials'; diff --git a/src/client/iamcredentials.ts b/src/client/iamcredentials.ts index c86ecc0f..b96755db 100644 --- a/src/client/iamcredentials.ts +++ b/src/client/iamcredentials.ts @@ -16,8 +16,7 @@ import { URLSearchParams } from 'url'; import { errorMessage } from '@google-github-actions/actions-utils'; -import { Client } from './client'; -import { Logger } from '../logger'; +import { Client, ClientParameters } from './client'; /** * GenerateAccessTokenParameters are the inputs to the generateAccessToken call. @@ -42,10 +41,7 @@ export interface GenerateIDTokenParameters { /** * IAMCredentialsClientParameters are the inputs to the IAM client. */ -export interface IAMCredentialsClientParameters { - readonly logger: Logger; - readonly universe: string; - +export interface IAMCredentialsClientParameters extends ClientParameters { readonly authToken: string; } @@ -57,11 +53,7 @@ export class IAMCredentialsClient extends Client { readonly #authToken: string; constructor(opts: IAMCredentialsClientParameters) { - super({ - logger: opts.logger, - universe: opts.universe, - child: `IAMCredentialsClient`, - }); + super('IAMCredentialsClient', opts); this.#authToken = opts.authToken; } @@ -80,7 +72,9 @@ export class IAMCredentialsClient extends Client { const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`; - const headers = { Authorization: `Bearer ${this.#authToken}` }; + const headers = Object.assign(this._headers(), { + Authorization: `Bearer ${this.#authToken}`, + }); const body: Record> = {}; if (delegates && delegates.length > 0) { @@ -126,10 +120,10 @@ export class IAMCredentialsClient extends Client { const pth = `${this._endpoints.oauth2}/token`; - const headers = { + const headers = Object.assign(this._headers(), { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', - }; + }); const body = new URLSearchParams(); body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer'); @@ -173,7 +167,9 @@ export class IAMCredentialsClient extends Client { const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateIdToken`; - const headers = { Authorization: `Bearer ${this.#authToken}` }; + const headers = Object.assign(this._headers(), { + Authorization: `Bearer ${this.#authToken}`, + }); const body: Record = { audience: audience, diff --git a/src/client/service_account_key_json.ts b/src/client/service_account_key_json.ts index fe3dd8e6..b982bc94 100644 --- a/src/client/service_account_key_json.ts +++ b/src/client/service_account_key_json.ts @@ -23,17 +23,13 @@ import { writeSecureFile, } from '@google-github-actions/actions-utils'; -import { AuthClient, Client } from './client'; -import { Logger } from '../logger'; +import { AuthClient, Client, ClientParameters } from './client'; /** * ServiceAccountKeyClientParameters is used as input to the * ServiceAccountKeyClient. */ -export interface ServiceAccountKeyClientParameters { - readonly logger: Logger; - readonly universe: string; - +export interface ServiceAccountKeyClientParameters extends ClientParameters { readonly serviceAccountKey: string; } @@ -46,11 +42,7 @@ export class ServiceAccountKeyClient extends Client implements AuthClient { readonly #audience: string; constructor(opts: ServiceAccountKeyClientParameters) { - super({ - logger: opts.logger, - universe: opts.universe, - child: `ServiceAccountKeyClient`, - }); + super('ServiceAccountKeyClient', opts); const serviceAccountKey = parseCredential(opts.serviceAccountKey); if (!isServiceAccountKey(serviceAccountKey)) { diff --git a/src/client/workload_identity_federation.ts b/src/client/workload_identity_federation.ts index c7106123..8c2235d1 100644 --- a/src/client/workload_identity_federation.ts +++ b/src/client/workload_identity_federation.ts @@ -14,17 +14,13 @@ import { errorMessage, writeSecureFile } from '@google-github-actions/actions-utils'; -import { AuthClient, Client } from './client'; -import { Logger } from '../logger'; +import { AuthClient, Client, ClientParameters } from './client'; /** * WorkloadIdentityFederationClientParameters is used as input to the * WorkloadIdentityFederationClient. */ -export interface WorkloadIdentityFederationClientParameters { - readonly logger: Logger; - readonly universe: string; - +export interface WorkloadIdentityFederationClientParameters extends ClientParameters { readonly githubOIDCToken: string; readonly githubOIDCTokenRequestURL: string; readonly githubOIDCTokenRequestToken: string; @@ -51,11 +47,7 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie #cachedAt?: number; constructor(opts: WorkloadIdentityFederationClientParameters) { - super({ - logger: opts.logger, - universe: opts.universe, - child: `WorkloadIdentityFederationClient`, - }); + super('WorkloadIdentityFederationClient', opts); this.#githubOIDCToken = opts.githubOIDCToken; this.#githubOIDCTokenRequestURL = opts.githubOIDCTokenRequestURL; @@ -90,6 +82,8 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie const pth = `${this._endpoints.sts}/token`; + const headers = Object.assign(this._headers(), {}); + const body = { audience: this.#audience, grantType: `urn:ietf:params:oauth:grant-type:token-exchange`, @@ -106,7 +100,7 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie }); try { - const resp = await this._httpClient.postJson<{ access_token: string }>(pth, body); + const resp = await this._httpClient.postJson<{ access_token: string }>(pth, body, headers); const statusCode = resp.statusCode || 500; if (statusCode < 200 || statusCode > 299) { throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`); @@ -140,9 +134,9 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:signJwt`; - const headers = { + const headers = Object.assign(this._headers(), { Authorization: `Bearer ${await this.getToken()}`, - }; + }); const body = { payload: claims, diff --git a/src/main.ts b/src/main.ts index afa6123f..58aa6a05 100644 --- a/src/main.ts +++ b/src/main.ts @@ -84,6 +84,7 @@ export async function run(logger: Logger) { const tokenFormat = getInput(`token_format`); const delegates = parseMultilineCSV(getInput(`delegates`)); const universe = getInput(`universe`); + const requestReason = getInput(`request_reason`); // Ensure exactly one of workload_identity_provider and credentials_json was // provided. @@ -113,6 +114,7 @@ export async function run(logger: Logger) { client = new WorkloadIdentityFederationClient({ logger: logger, universe: universe, + requestReason: requestReason, githubOIDCToken: oidcToken, githubOIDCTokenRequestURL: oidcTokenRequestURL, @@ -126,6 +128,7 @@ export async function run(logger: Logger) { client = new ServiceAccountKeyClient({ logger: logger, universe: universe, + requestReason: requestReason, serviceAccountKey: credentialsJSON, });