Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading