Skip to content

Commit

Permalink
fix: switch to actions-utils and update deps
Browse files Browse the repository at this point in the history
  • Loading branch information
sethvargo committed Dec 22, 2021
1 parent 1b8ec4e commit 55db253
Show file tree
Hide file tree
Showing 11 changed files with 247 additions and 689 deletions.
2 changes: 1 addition & 1 deletion dist/main/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/post/index.js

Large diffs are not rendered by default.

326 changes: 178 additions & 148 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,23 @@
"author": "GoogleCloudPlatform",
"license": "Apache-2.0",
"dependencies": {
"@actions/core": "^1.6.0"
"@actions/core": "^1.6.0",
"@google-github-actions/actions-utils": "^0.1.0"
},
"devDependencies": {
"@types/chai": "^4.2.22",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"@types/node": "^16.11.11",
"@typescript-eslint/eslint-plugin": "^5.5.0",
"@typescript-eslint/parser": "^5.5.0",
"@vercel/ncc": "^0.33.0",
"@types/node": "^17.0.2",
"@typescript-eslint/eslint-plugin": "^5.8.0",
"@typescript-eslint/parser": "^5.8.0",
"@vercel/ncc": "^0.33.1",
"chai": "^4.3.4",
"eslint": "^8.3.0",
"eslint": "^8.5.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"mocha": "^9.1.3",
"prettier": "^2.5.0",
"prettier": "^2.5.1",
"ts-node": "^10.4.0",
"typescript": "^4.5.2"
"typescript": "^4.5.4"
}
}
72 changes: 24 additions & 48 deletions src/client/credentials_json_client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
'use strict';

import { createSign } from 'crypto';
import {
isServiceAccountKey,
parseCredential,
randomFilepath,
ServiceAccountKey,
toBase64,
writeSecureFile,
} from '@google-github-actions/actions-utils';

import { AuthClient } from './auth_client';
import { toBase64, fromBase64, trimmedString, writeSecureFile } from '../utils';

/**
* Available options to create the CredentialsJSONClient.
Expand All @@ -22,49 +30,16 @@ interface CredentialsJSONClientOptions {
*/
export class CredentialsJSONClient implements AuthClient {
readonly #projectID: string;
readonly #credentials: Record<string, string>;
readonly #credentials: ServiceAccountKey;

constructor(opts: CredentialsJSONClientOptions) {
this.#credentials = this.parseServiceAccountKeyJSON(opts.credentialsJSON);
this.#projectID = opts.projectID || this.#credentials['project_id'];
}

/**
* parseServiceAccountKeyJSON attempts to parse the given string as a service
* account key JSON. It handles if the string is base64-encoded.
*/
parseServiceAccountKeyJSON(str: string): Record<string, string> {
str = trimmedString(str);
if (!str) {
throw new Error(`Missing service account key JSON (got empty value)`);
}

// If the string doesn't start with a JSON object character, it is probably
// base64-encoded.
if (!str.startsWith('{')) {
str = fromBase64(str);
const credentials = parseCredential(opts.credentialsJSON);
if (!isServiceAccountKey(credentials)) {
throw new Error(`Provided credential is not a valid service account key JSON`);
}
this.#credentials = credentials;

let creds: Record<string, string>;
try {
creds = JSON.parse(str);
} catch (e) {
throw new SyntaxError(`Failed to parse credentials as JSON: ${e}`);
}

const requireValue = (key: string) => {
const val = trimmedString(creds[key]);
if (!val) {
throw new Error(`Service account key JSON is missing required field "${key}"`);
}
};

requireValue('project_id');
requireValue('private_key_id');
requireValue('private_key');
requireValue('client_email');

return creds;
this.#projectID = opts.projectID || this.#credentials.project_id;
}

/**
Expand All @@ -74,14 +49,14 @@ export class CredentialsJSONClient implements AuthClient {
const header = {
alg: 'RS256',
typ: 'JWT',
kid: this.#credentials['private_key_id'],
kid: this.#credentials.private_key_id,
};

const now = Math.floor(new Date().getTime() / 1000);

const body = {
iss: this.#credentials['client_email'],
sub: this.#credentials['client_email'],
iss: this.#credentials.client_email,
sub: this.#credentials.client_email,
aud: 'https://iamcredentials.googleapis.com/',
iat: now,
exp: now + 3599,
Expand All @@ -94,7 +69,7 @@ export class CredentialsJSONClient implements AuthClient {
signer.write(message);
signer.end();

const signature = signer.sign(this.#credentials['private_key']);
const signature = signer.sign(this.#credentials.private_key);
return message + '.' + toBase64(signature);
} catch (err) {
throw new Error(`Failed to sign auth token using ${await this.getServiceAccount()}: ${err}`);
Expand All @@ -110,7 +85,7 @@ export class CredentialsJSONClient implements AuthClient {
const header = {
alg: 'RS256',
typ: 'JWT',
kid: this.#credentials['private_key_id'],
kid: this.#credentials.private_key_id,
};

const message = toBase64(JSON.stringify(header)) + '.' + toBase64(unsignedJWT);
Expand All @@ -120,7 +95,7 @@ export class CredentialsJSONClient implements AuthClient {
signer.write(message);
signer.end();

const signature = signer.sign(this.#credentials['private_key']);
const signature = signer.sign(this.#credentials.private_key);
const jwt = message + '.' + toBase64(signature);
return jwt;
} catch (err) {
Expand All @@ -142,14 +117,15 @@ export class CredentialsJSONClient implements AuthClient {
* extracted from the Service Account Key JSON.
*/
async getServiceAccount(): Promise<string> {
return this.#credentials['client_email'];
return this.#credentials.client_email;
}

/**
* createCredentialsFile creates a Google Cloud credentials file that can be
* set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries.
*/
async createCredentialsFile(outputDir: string): Promise<string> {
return await writeSecureFile(outputDir, JSON.stringify(this.#credentials));
const outputFile = randomFilepath(outputDir);
return await writeSecureFile(outputFile, JSON.stringify(this.#credentials));
}
}
6 changes: 4 additions & 2 deletions src/client/workload_identity_client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'use strict';

import { URL } from 'url';
import { randomFilepath, writeSecureFile } from '@google-github-actions/actions-utils';

import { AuthClient } from './auth_client';
import { writeSecureFile } from '../utils';
import { BaseClient } from '../base';

/**
Expand Down Expand Up @@ -208,6 +209,7 @@ export class WorkloadIdentityClient implements AuthClient {
},
};

return await writeSecureFile(outputDir, JSON.stringify(data));
const outputFile = randomFilepath(outputDir);
return await writeSecureFile(outputFile, JSON.stringify(data));
}
}
18 changes: 11 additions & 7 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@ import {
setOutput,
setSecret,
} from '@actions/core';
import {
errorMessage,
exactlyOneOf,
parseCSV,
parseDuration,
} from '@google-github-actions/actions-utils';

import { WorkloadIdentityClient } from './client/workload_identity_client';
import { CredentialsJSONClient } from './client/credentials_json_client';
import { AuthClient } from './client/auth_client';
import { BaseClient } from './base';
import { buildDomainWideDelegationJWT, errorMessage, explodeStrings, parseDuration } from './utils';
import { buildDomainWideDelegationJWT } from './utils';

const secretsWarning =
`If you are specifying input values via GitHub secrets, ensure the secret ` +
Expand All @@ -42,14 +49,11 @@ async function run(): Promise<void> {
const credentialsJSON = getInput('credentials_json');
const createCredentialsFile = getBooleanInput('create_credentials_file');
const tokenFormat = getInput('token_format');
const delegates = explodeStrings(getInput('delegates'));
const delegates = parseCSV(getInput('delegates'));

// Ensure exactly one of workload_identity_provider and credentials_json was
// provided.
if (
(!workloadIdentityProvider && !credentialsJSON) ||
(workloadIdentityProvider && credentialsJSON)
) {
if (!exactlyOneOf(workloadIdentityProvider, credentialsJSON)) {
throw new Error(
'The GitHub Action workflow must specify exactly one of ' +
'"workload_identity_provider" or "credentials_json"! ' +
Expand Down Expand Up @@ -159,7 +163,7 @@ async function run(): Promise<void> {
logDebug(`Creating access token`);

const accessTokenLifetime = parseDuration(getInput('access_token_lifetime'));
const accessTokenScopes = explodeStrings(getInput('access_token_scopes'));
const accessTokenScopes = parseCSV(getInput('access_token_scopes'));
const accessTokenSubject = getInput('access_token_subject');
const serviceAccount = await client.getServiceAccount();

Expand Down
21 changes: 16 additions & 5 deletions src/post.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

import { getBooleanInput, setFailed, info as logInfo } from '@actions/core';
import { removeExportedCredentials } from './utils';
import { errorMessage, removeFile } from '@google-github-actions/actions-utils';

/**
* Executes the post action, documented inline.
Expand All @@ -13,14 +13,25 @@ export async function run(): Promise<void> {
return;
}

const exportedPath = await removeExportedCredentials();
if (exportedPath) {
logInfo(`Removed exported credentials at ${exportedPath}`);
// Look up the credentials path, if one exists. Note that we only check the
// environment variable set by our action, since we don't want to
// accidentially clean up if someone set GOOGLE_APPLICATION_CREDENTIALS or
// another environment variable manually.
const credentialsPath = process.env['GOOGLE_GHA_CREDS_PATH'];
if (!credentialsPath) {
return;
}

// Remove the file.
const removed = await removeFile(credentialsPath);
if (removed) {
logInfo(`Removed exported credentials at ${credentialsPath}`);
} else {
logInfo('No exported credentials found');
}
} catch (err) {
setFailed(`google-github-actions/auth post failed with: ${err}`);
const msg = errorMessage(err);
setFailed(`google-github-actions/auth post failed with: ${msg}`);
}
}

Expand Down

0 comments on commit 55db253

Please sign in to comment.