Add request_reason for plumbing though user-supplied audit information (#413)

Fixes https://github.com/google-github-actions/auth/issues/412
This commit is contained in:
Seth Vargo 2024-05-14 12:46:35 -04:00 committed by GitHub
parent 34baaec3f3
commit e0122d6a97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 58 additions and 45 deletions

View File

@ -269,13 +269,22 @@ regardless of the authentication mechanism.
https://cloud.google.com. Trusted Partner Cloud and Google Distributed https://cloud.google.com. Trusted Partner Cloud and Google Distributed
Hosted Cloud should set this to their universe address. 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: 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 ```yaml
env: env:
GHA_ENDPOINT_OVERRIDE_oauth2: 'https://oauth2.myapi.endpoint/v1' 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 - `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.

View File

@ -102,6 +102,12 @@ inputs:
Hosted Cloud should set this to their universe address. Hosted Cloud should set this to their universe address.
required: false required: false
default: 'googleapis.com' 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: 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

View File

@ -45,12 +45,13 @@ export interface AuthClient {
export interface ClientParameters { export interface ClientParameters {
logger: Logger; logger: Logger;
universe: string; universe: string;
child: string; requestReason?: string;
} }
export class Client { export abstract class Client {
protected readonly _logger: Logger; protected readonly _logger: Logger;
protected readonly _httpClient: HttpClient; protected readonly _httpClient: HttpClient;
private readonly _requestReason: string | undefined;
protected readonly _endpoints = { protected readonly _endpoints = {
iam: 'https://iam.{universe}/v1', iam: 'https://iam.{universe}/v1',
@ -60,8 +61,8 @@ export class Client {
www: 'https://www.{universe}', www: 'https://www.{universe}',
}; };
constructor(opts: ClientParameters) { constructor(child: string, opts: ClientParameters) {
this._logger = opts.logger.withNamespace(opts.child); this._logger = opts.logger.withNamespace(child);
// Create the http client with our user agent. // Create the http client with our user agent.
this._httpClient = new HttpClient(userAgent, undefined, { this._httpClient = new HttpClient(userAgent, undefined, {
@ -73,6 +74,18 @@ export class Client {
}); });
this._endpoints = expandUniverseEndpoints(this._endpoints, opts.universe); 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<string, string> {
const headers: Record<string, string> = {};
if (this._requestReason) {
headers['X-Goog-Request-Reason'] = this._requestReason;
}
return headers;
} }
} }
export { IAMCredentialsClient, IAMCredentialsClientParameters } from './iamcredentials'; export { IAMCredentialsClient, IAMCredentialsClientParameters } from './iamcredentials';

View File

@ -16,8 +16,7 @@ import { URLSearchParams } from 'url';
import { errorMessage } from '@google-github-actions/actions-utils'; import { errorMessage } from '@google-github-actions/actions-utils';
import { Client } from './client'; import { Client, ClientParameters } from './client';
import { Logger } from '../logger';
/** /**
* GenerateAccessTokenParameters are the inputs to the generateAccessToken call. * GenerateAccessTokenParameters are the inputs to the generateAccessToken call.
@ -42,10 +41,7 @@ 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 extends ClientParameters {
readonly logger: Logger;
readonly universe: string;
readonly authToken: string; readonly authToken: string;
} }
@ -57,11 +53,7 @@ export class IAMCredentialsClient extends Client {
readonly #authToken: string; readonly #authToken: string;
constructor(opts: IAMCredentialsClientParameters) { constructor(opts: IAMCredentialsClientParameters) {
super({ super('IAMCredentialsClient', opts);
logger: opts.logger,
universe: opts.universe,
child: `IAMCredentialsClient`,
});
this.#authToken = opts.authToken; this.#authToken = opts.authToken;
} }
@ -80,7 +72,9 @@ export class IAMCredentialsClient extends Client {
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`; 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<string, string | Array<string>> = {}; const body: Record<string, string | Array<string>> = {};
if (delegates && delegates.length > 0) { if (delegates && delegates.length > 0) {
@ -126,10 +120,10 @@ export class IAMCredentialsClient extends Client {
const pth = `${this._endpoints.oauth2}/token`; const pth = `${this._endpoints.oauth2}/token`;
const headers = { const headers = Object.assign(this._headers(), {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}; });
const body = new URLSearchParams(); const body = new URLSearchParams();
body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer'); 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 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<string, string | string[] | boolean> = { const body: Record<string, string | string[] | boolean> = {
audience: audience, audience: audience,

View File

@ -23,17 +23,13 @@ import {
writeSecureFile, writeSecureFile,
} from '@google-github-actions/actions-utils'; } from '@google-github-actions/actions-utils';
import { AuthClient, Client } from './client'; import { AuthClient, Client, ClientParameters } from './client';
import { Logger } from '../logger';
/** /**
* ServiceAccountKeyClientParameters is used as input to the * ServiceAccountKeyClientParameters is used as input to the
* ServiceAccountKeyClient. * ServiceAccountKeyClient.
*/ */
export interface ServiceAccountKeyClientParameters { export interface ServiceAccountKeyClientParameters extends ClientParameters {
readonly logger: Logger;
readonly universe: string;
readonly serviceAccountKey: string; readonly serviceAccountKey: string;
} }
@ -46,11 +42,7 @@ export class ServiceAccountKeyClient extends Client implements AuthClient {
readonly #audience: string; readonly #audience: string;
constructor(opts: ServiceAccountKeyClientParameters) { constructor(opts: ServiceAccountKeyClientParameters) {
super({ super('ServiceAccountKeyClient', opts);
logger: opts.logger,
universe: opts.universe,
child: `ServiceAccountKeyClient`,
});
const serviceAccountKey = parseCredential(opts.serviceAccountKey); const serviceAccountKey = parseCredential(opts.serviceAccountKey);
if (!isServiceAccountKey(serviceAccountKey)) { if (!isServiceAccountKey(serviceAccountKey)) {

View File

@ -14,17 +14,13 @@
import { errorMessage, writeSecureFile } from '@google-github-actions/actions-utils'; import { errorMessage, writeSecureFile } from '@google-github-actions/actions-utils';
import { AuthClient, Client } from './client'; import { AuthClient, Client, ClientParameters } from './client';
import { Logger } from '../logger';
/** /**
* WorkloadIdentityFederationClientParameters is used as input to the * WorkloadIdentityFederationClientParameters is used as input to the
* WorkloadIdentityFederationClient. * WorkloadIdentityFederationClient.
*/ */
export interface WorkloadIdentityFederationClientParameters { export interface WorkloadIdentityFederationClientParameters extends ClientParameters {
readonly logger: Logger;
readonly universe: string;
readonly githubOIDCToken: string; readonly githubOIDCToken: string;
readonly githubOIDCTokenRequestURL: string; readonly githubOIDCTokenRequestURL: string;
readonly githubOIDCTokenRequestToken: string; readonly githubOIDCTokenRequestToken: string;
@ -51,11 +47,7 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
#cachedAt?: number; #cachedAt?: number;
constructor(opts: WorkloadIdentityFederationClientParameters) { constructor(opts: WorkloadIdentityFederationClientParameters) {
super({ super('WorkloadIdentityFederationClient', opts);
logger: opts.logger,
universe: opts.universe,
child: `WorkloadIdentityFederationClient`,
});
this.#githubOIDCToken = opts.githubOIDCToken; this.#githubOIDCToken = opts.githubOIDCToken;
this.#githubOIDCTokenRequestURL = opts.githubOIDCTokenRequestURL; this.#githubOIDCTokenRequestURL = opts.githubOIDCTokenRequestURL;
@ -90,6 +82,8 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
const pth = `${this._endpoints.sts}/token`; const pth = `${this._endpoints.sts}/token`;
const headers = Object.assign(this._headers(), {});
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`,
@ -106,7 +100,7 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
}); });
try { 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; 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]'}`);
@ -140,9 +134,9 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:signJwt`; const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:signJwt`;
const headers = { const headers = Object.assign(this._headers(), {
Authorization: `Bearer ${await this.getToken()}`, Authorization: `Bearer ${await this.getToken()}`,
}; });
const body = { const body = {
payload: claims, payload: claims,

View File

@ -84,6 +84,7 @@ export async function run(logger: Logger) {
const tokenFormat = getInput(`token_format`); const tokenFormat = getInput(`token_format`);
const delegates = parseMultilineCSV(getInput(`delegates`)); const delegates = parseMultilineCSV(getInput(`delegates`));
const universe = getInput(`universe`); const universe = getInput(`universe`);
const requestReason = getInput(`request_reason`);
// Ensure exactly one of workload_identity_provider and credentials_json was // Ensure exactly one of workload_identity_provider and credentials_json was
// provided. // provided.
@ -113,6 +114,7 @@ export async function run(logger: Logger) {
client = new WorkloadIdentityFederationClient({ client = new WorkloadIdentityFederationClient({
logger: logger, logger: logger,
universe: universe, universe: universe,
requestReason: requestReason,
githubOIDCToken: oidcToken, githubOIDCToken: oidcToken,
githubOIDCTokenRequestURL: oidcTokenRequestURL, githubOIDCTokenRequestURL: oidcTokenRequestURL,
@ -126,6 +128,7 @@ export async function run(logger: Logger) {
client = new ServiceAccountKeyClient({ client = new ServiceAccountKeyClient({
logger: logger, logger: logger,
universe: universe, universe: universe,
requestReason: requestReason,
serviceAccountKey: credentialsJSON, serviceAccountKey: credentialsJSON,
}); });