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
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
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.

View File

@ -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

View File

@ -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<string, string> {
const headers: Record<string, string> = {};
if (this._requestReason) {
headers['X-Goog-Request-Reason'] = this._requestReason;
}
return headers;
}
}
export { IAMCredentialsClient, IAMCredentialsClientParameters } from './iamcredentials';

View File

@ -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<string, string | Array<string>> = {};
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<string, string | string[] | boolean> = {
audience: audience,

View File

@ -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)) {

View File

@ -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,

View File

@ -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,
});