Skip to content

Commit

Permalink
Add request_reason for plumbing though user-supplied audit information (
Browse files Browse the repository at this point in the history
#413)

Fixes #412
  • Loading branch information
sethvargo committed May 14, 2024
1 parent 34baaec commit e0122d6
Show file tree
Hide file tree
Showing 7 changed files with 58 additions and 45 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 17 additions & 4 deletions src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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, {
Expand All @@ -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';
Expand Down
26 changes: 11 additions & 15 deletions src/client/iamcredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}

Expand All @@ -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;
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 3 additions & 11 deletions src/client/service_account_key_json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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)) {
Expand Down
22 changes: 8 additions & 14 deletions src/client/workload_identity_federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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`,
Expand All @@ -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]'}`);
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -113,6 +114,7 @@ export async function run(logger: Logger) {
client = new WorkloadIdentityFederationClient({
logger: logger,
universe: universe,
requestReason: requestReason,

githubOIDCToken: oidcToken,
githubOIDCTokenRequestURL: oidcTokenRequestURL,
Expand All @@ -126,6 +128,7 @@ export async function run(logger: Logger) {
client = new ServiceAccountKeyClient({
logger: logger,
universe: universe,
requestReason: requestReason,

serviceAccountKey: credentialsJSON,
});
Expand Down

0 comments on commit e0122d6

Please sign in to comment.