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

fix: switch to actions-utils and update deps #91

Merged
merged 1 commit into from
Dec 22, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
sethvargo marked this conversation as resolved.
Show resolved Hide resolved
// 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