Skip to content

Commit e0122d6

Browse files
authored
Add request_reason for plumbing though user-supplied audit information (#413)
Fixes #412
1 parent 34baaec commit e0122d6

File tree

7 files changed

+58
-45
lines changed

7 files changed

+58
-45
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,13 +269,22 @@ regardless of the authentication mechanism.
269269
https://cloud.google.com. Trusted Partner Cloud and Google Distributed
270270
Hosted Cloud should set this to their universe address.
271271
272-
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:
272+
You can also override individual API endpoints by setting the environment
273+
variable `GHA_ENDPOINT_OVERRIDE_<endpoint>` where endpoint is the API
274+
endpoint to override. This only applies to the `auth` action and does not
275+
persist to other steps. For example:
273276
274277
```yaml
275278
env:
276279
GHA_ENDPOINT_OVERRIDE_oauth2: 'https://oauth2.myapi.endpoint/v1'
277280
```
278281
282+
- `request_reason`: (Optional) An optional Reason Request [System
283+
Parameter](https://cloud.google.com/apis/docs/system-parameters) for each
284+
API call made by the GitHub Action. This will inject the
285+
"X-Goog-Request-Reason" HTTP header, which will provide user-supplied
286+
information in Google Cloud audit logs.
287+
279288
- `cleanup_credentials`: (Optional) If true, the action will remove any
280289
created credentials from the filesystem upon completion. This only applies
281290
if "create_credentials_file" is true. The default is true.

action.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ inputs:
102102
Hosted Cloud should set this to their universe address.
103103
required: false
104104
default: 'googleapis.com'
105+
request_reason:
106+
description: |-
107+
An optional Reason Request System Parameter for each API call made by the
108+
GitHub Action. This will inject the "X-Goog-Request-Reason" HTTP header,
109+
which will provide user-supplied information in Google Cloud audit logs.
110+
required: false
105111
cleanup_credentials:
106112
description: |-
107113
If true, the action will remove any created credentials from the

src/client/client.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,13 @@ export interface AuthClient {
4545
export interface ClientParameters {
4646
logger: Logger;
4747
universe: string;
48-
child: string;
48+
requestReason?: string;
4949
}
5050

51-
export class Client {
51+
export abstract class Client {
5252
protected readonly _logger: Logger;
5353
protected readonly _httpClient: HttpClient;
54+
private readonly _requestReason: string | undefined;
5455

5556
protected readonly _endpoints = {
5657
iam: 'https://iam.{universe}/v1',
@@ -60,8 +61,8 @@ export class Client {
6061
www: 'https://www.{universe}',
6162
};
6263

63-
constructor(opts: ClientParameters) {
64-
this._logger = opts.logger.withNamespace(opts.child);
64+
constructor(child: string, opts: ClientParameters) {
65+
this._logger = opts.logger.withNamespace(child);
6566

6667
// Create the http client with our user agent.
6768
this._httpClient = new HttpClient(userAgent, undefined, {
@@ -73,6 +74,18 @@ export class Client {
7374
});
7475

7576
this._endpoints = expandUniverseEndpoints(this._endpoints, opts.universe);
77+
this._requestReason = opts.requestReason;
78+
}
79+
80+
/**
81+
* _headers returns any added headers to apply to HTTP API calls.
82+
*/
83+
protected _headers(): Record<string, string> {
84+
const headers: Record<string, string> = {};
85+
if (this._requestReason) {
86+
headers['X-Goog-Request-Reason'] = this._requestReason;
87+
}
88+
return headers;
7689
}
7790
}
7891
export { IAMCredentialsClient, IAMCredentialsClientParameters } from './iamcredentials';

src/client/iamcredentials.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import { URLSearchParams } from 'url';
1616

1717
import { errorMessage } from '@google-github-actions/actions-utils';
1818

19-
import { Client } from './client';
20-
import { Logger } from '../logger';
19+
import { Client, ClientParameters } from './client';
2120

2221
/**
2322
* GenerateAccessTokenParameters are the inputs to the generateAccessToken call.
@@ -42,10 +41,7 @@ export interface GenerateIDTokenParameters {
4241
/**
4342
* IAMCredentialsClientParameters are the inputs to the IAM client.
4443
*/
45-
export interface IAMCredentialsClientParameters {
46-
readonly logger: Logger;
47-
readonly universe: string;
48-
44+
export interface IAMCredentialsClientParameters extends ClientParameters {
4945
readonly authToken: string;
5046
}
5147

@@ -57,11 +53,7 @@ export class IAMCredentialsClient extends Client {
5753
readonly #authToken: string;
5854

5955
constructor(opts: IAMCredentialsClientParameters) {
60-
super({
61-
logger: opts.logger,
62-
universe: opts.universe,
63-
child: `IAMCredentialsClient`,
64-
});
56+
super('IAMCredentialsClient', opts);
6557

6658
this.#authToken = opts.authToken;
6759
}
@@ -80,7 +72,9 @@ export class IAMCredentialsClient extends Client {
8072

8173
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`;
8274

83-
const headers = { Authorization: `Bearer ${this.#authToken}` };
75+
const headers = Object.assign(this._headers(), {
76+
Authorization: `Bearer ${this.#authToken}`,
77+
});
8478

8579
const body: Record<string, string | Array<string>> = {};
8680
if (delegates && delegates.length > 0) {
@@ -126,10 +120,10 @@ export class IAMCredentialsClient extends Client {
126120

127121
const pth = `${this._endpoints.oauth2}/token`;
128122

129-
const headers = {
123+
const headers = Object.assign(this._headers(), {
130124
'Accept': 'application/json',
131125
'Content-Type': 'application/x-www-form-urlencoded',
132-
};
126+
});
133127

134128
const body = new URLSearchParams();
135129
body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
@@ -173,7 +167,9 @@ export class IAMCredentialsClient extends Client {
173167

174168
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateIdToken`;
175169

176-
const headers = { Authorization: `Bearer ${this.#authToken}` };
170+
const headers = Object.assign(this._headers(), {
171+
Authorization: `Bearer ${this.#authToken}`,
172+
});
177173

178174
const body: Record<string, string | string[] | boolean> = {
179175
audience: audience,

src/client/service_account_key_json.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,13 @@ import {
2323
writeSecureFile,
2424
} from '@google-github-actions/actions-utils';
2525

26-
import { AuthClient, Client } from './client';
27-
import { Logger } from '../logger';
26+
import { AuthClient, Client, ClientParameters } from './client';
2827

2928
/**
3029
* ServiceAccountKeyClientParameters is used as input to the
3130
* ServiceAccountKeyClient.
3231
*/
33-
export interface ServiceAccountKeyClientParameters {
34-
readonly logger: Logger;
35-
readonly universe: string;
36-
32+
export interface ServiceAccountKeyClientParameters extends ClientParameters {
3733
readonly serviceAccountKey: string;
3834
}
3935

@@ -46,11 +42,7 @@ export class ServiceAccountKeyClient extends Client implements AuthClient {
4642
readonly #audience: string;
4743

4844
constructor(opts: ServiceAccountKeyClientParameters) {
49-
super({
50-
logger: opts.logger,
51-
universe: opts.universe,
52-
child: `ServiceAccountKeyClient`,
53-
});
45+
super('ServiceAccountKeyClient', opts);
5446

5547
const serviceAccountKey = parseCredential(opts.serviceAccountKey);
5648
if (!isServiceAccountKey(serviceAccountKey)) {

src/client/workload_identity_federation.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,13 @@
1414

1515
import { errorMessage, writeSecureFile } from '@google-github-actions/actions-utils';
1616

17-
import { AuthClient, Client } from './client';
18-
import { Logger } from '../logger';
17+
import { AuthClient, Client, ClientParameters } from './client';
1918

2019
/**
2120
* WorkloadIdentityFederationClientParameters is used as input to the
2221
* WorkloadIdentityFederationClient.
2322
*/
24-
export interface WorkloadIdentityFederationClientParameters {
25-
readonly logger: Logger;
26-
readonly universe: string;
27-
23+
export interface WorkloadIdentityFederationClientParameters extends ClientParameters {
2824
readonly githubOIDCToken: string;
2925
readonly githubOIDCTokenRequestURL: string;
3026
readonly githubOIDCTokenRequestToken: string;
@@ -51,11 +47,7 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
5147
#cachedAt?: number;
5248

5349
constructor(opts: WorkloadIdentityFederationClientParameters) {
54-
super({
55-
logger: opts.logger,
56-
universe: opts.universe,
57-
child: `WorkloadIdentityFederationClient`,
58-
});
50+
super('WorkloadIdentityFederationClient', opts);
5951

6052
this.#githubOIDCToken = opts.githubOIDCToken;
6153
this.#githubOIDCTokenRequestURL = opts.githubOIDCTokenRequestURL;
@@ -90,6 +82,8 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
9082

9183
const pth = `${this._endpoints.sts}/token`;
9284

85+
const headers = Object.assign(this._headers(), {});
86+
9387
const body = {
9488
audience: this.#audience,
9589
grantType: `urn:ietf:params:oauth:grant-type:token-exchange`,
@@ -106,7 +100,7 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
106100
});
107101

108102
try {
109-
const resp = await this._httpClient.postJson<{ access_token: string }>(pth, body);
103+
const resp = await this._httpClient.postJson<{ access_token: string }>(pth, body, headers);
110104
const statusCode = resp.statusCode || 500;
111105
if (statusCode < 200 || statusCode > 299) {
112106
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
@@ -140,9 +134,9 @@ export class WorkloadIdentityFederationClient extends Client implements AuthClie
140134

141135
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:signJwt`;
142136

143-
const headers = {
137+
const headers = Object.assign(this._headers(), {
144138
Authorization: `Bearer ${await this.getToken()}`,
145-
};
139+
});
146140

147141
const body = {
148142
payload: claims,

src/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export async function run(logger: Logger) {
8484
const tokenFormat = getInput(`token_format`);
8585
const delegates = parseMultilineCSV(getInput(`delegates`));
8686
const universe = getInput(`universe`);
87+
const requestReason = getInput(`request_reason`);
8788

8889
// Ensure exactly one of workload_identity_provider and credentials_json was
8990
// provided.
@@ -113,6 +114,7 @@ export async function run(logger: Logger) {
113114
client = new WorkloadIdentityFederationClient({
114115
logger: logger,
115116
universe: universe,
117+
requestReason: requestReason,
116118

117119
githubOIDCToken: oidcToken,
118120
githubOIDCTokenRequestURL: oidcTokenRequestURL,
@@ -126,6 +128,7 @@ export async function run(logger: Logger) {
126128
client = new ServiceAccountKeyClient({
127129
logger: logger,
128130
universe: universe,
131+
requestReason: requestReason,
129132

130133
serviceAccountKey: credentialsJSON,
131134
});

0 commit comments

Comments
 (0)