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:
parent
34baaec3f3
commit
e0122d6a97
11
README.md
11
README.md
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
|
@ -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)) {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user