Clean up exported credentials when the workflow finishes (#67)

* Clean up exported credentials when the workflow finishes

* Fix conditional and log
This commit is contained in:
Seth Vargo 2021-12-01 12:38:47 -05:00 committed by GitHub
parent c6fa692def
commit 1e9245c68a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 2065 additions and 18 deletions

View File

@ -9,7 +9,7 @@ on:
- 'main'
concurrency:
group: '${{ github.head_ref || github.ref }}'
group: '${{github.workflow}}-${{ github.head_ref || github.ref }}'
cancel-in-progress: true
jobs:
@ -41,7 +41,7 @@ jobs:
unit:
name: 'unit'
needs: install_and_compile
needs: 'install_and_compile'
runs-on: 'ubuntu-latest'
steps:
@ -63,7 +63,7 @@ jobs:
credentials_json:
name: 'credentials_json'
needs: install_and_compile
needs: 'install_and_compile'
runs-on: '${{ matrix.os }}'
strategy:
fail-fast: false
@ -125,7 +125,7 @@ jobs:
workload_identity_federation:
name: 'workload_identity_federation'
needs: install_and_compile
needs: 'install_and_compile'
runs-on: '${{ matrix.os }}'
strategy:
fail-fast: false
@ -189,3 +189,37 @@ jobs:
token_format: 'id_token'
id_token_audience: 'https://secretmanager.googleapis.com/'
id_token_include_email: true
# This test ensures that the GOOGLE_APPLICATION_CREDENTIALS environment
# variable is shared with the container and that the path of the file is on
# the shared filesystem with the container and that the USER for the container
# has permissions to read the file.
docker:
name: 'docker'
needs: 'install_and_compile'
runs-on: 'ubuntu-latest'
strategy:
fail-fast: false
steps:
- uses: 'actions/checkout@v2'
- uses: 'actions/setup-node@v2'
with:
node-version: '12.x'
- name: 'npm ci'
run: 'npm ci'
- name: 'npm build'
run: 'npm run build'
- name: 'auth-default'
uses: './'
with:
credentials_json: '${{ secrets.AUTH_SA_KEY_JSON }}'
- name: 'docker'
uses: 'docker://alpine:3'
with:
entrypoint: '/bin/sh'
args: '-euc "test -n "${GOOGLE_APPLICATION_CREDENTIALS}" && test -r "${GOOGLE_APPLICATION_CREDENTIALS}"'

View File

@ -165,6 +165,9 @@ regardless of the authentication mechanism.
identities to use for impersonation in the chain. By default there are no
delegates.
- `cleanup_credentials`: (Optional) If true, the action will remove any
generated credentials from the filesystem upon completion. The default is
true.
## Outputs

View File

@ -71,6 +71,12 @@ inputs:
impersonation in the chain.
default: ''
required: false
cleanup_credentials:
description: |-
If true, the action will remove any generated credentials from the
filesystem upon completion.
default: true
required: false
# access token params
access_token_lifetime:
@ -130,4 +136,5 @@ branding:
runs:
using: 'node12'
main: 'dist/index.js'
main: 'dist/main/index.js'
post: 'dist/post/index.js'

View File

@ -234,11 +234,21 @@ function run() {
// fails, which means continue-on-error actions will still have the file
// available.
if (createCredentialsFile) {
const runnerTempDir = process.env.RUNNER_TEMP;
if (!runnerTempDir) {
throw new Error('$RUNNER_TEMP is not set');
// Note: We explicitly and intentionally export to GITHUB_WORKSPACE
// instead of RUNNER_TEMP, because RUNNER_TEMP is not shared with
// Docker-based actions on the filesystem. Exporting to GITHUB_WORKSPACE
// ensures that the exported credentials are automatically available to
// Docker-based actions without user modification.
//
// This has the unintended side-effect of leaking credentials over time,
// because GITHUB_WORKSPACE is not automatically cleaned up on self-hosted
// runners. To mitigate this issue, this action defines a post step to
// remove any created credentials.
const githubWorkspace = process.env.GITHUB_WORKSPACE;
if (!githubWorkspace) {
throw new Error('$GITHUB_WORKSPACE is not set');
}
const credentialsPath = yield client.createCredentialsFile(runnerTempDir);
const credentialsPath = yield client.createCredentialsFile(githubWorkspace);
(0, core_1.setOutput)('credentials_file_path', credentialsPath);
// CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE is picked up by gcloud to use
// a specific credential file (subject to change and equivalent to auth/credential_file_override)
@ -600,7 +610,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.trimmedString = exports.fromBase64 = exports.toBase64 = exports.explodeStrings = exports.writeSecureFile = void 0;
exports.trimmedString = exports.fromBase64 = exports.toBase64 = exports.explodeStrings = exports.removeExportedCredentials = exports.writeSecureFile = void 0;
const fs_1 = __webpack_require__(747);
const crypto_1 = __importDefault(__webpack_require__(417));
const path_1 = __importDefault(__webpack_require__(622));
@ -627,6 +637,37 @@ function writeSecureFile(outputDir, data) {
});
}
exports.writeSecureFile = writeSecureFile;
/**
* removeExportedCredentials removes any exported credentials file. If the file
* does not exist, it does nothing.
*
* @returns Path of the file that was removed.
*/
function removeExportedCredentials() {
return __awaiter(this, void 0, void 0, function* () {
// 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 '';
}
// Delete the file.
try {
yield fs_1.promises.unlink(credentialsPath);
return credentialsPath;
}
catch (err) {
if (err instanceof Error)
if (err && err.message && err.message.includes('ENOENT')) {
return '';
}
throw new Error(`failed to remove exported credentials: ${err}`);
}
});
}
exports.removeExportedCredentials = removeExportedCredentials;
/**
* Converts a multi-line or comma-separated collection of strings into an array
* of trimmed strings.
@ -1917,7 +1958,7 @@ module.exports = require("util");
/***/ 731:
/***/ (function(module) {
module.exports = {"name":"@google-github-actions/auth","version":"0.4.0","description":"Authenticate to Google Cloud using OIDC tokens or JSON service account keys.","main":"dist/index.js","scripts":{"build":"ncc build src/main.ts","lint":"eslint . --ext .ts,.tsx","format":"prettier --write **/*.ts","test":"mocha -r ts-node/register -t 120s 'tests/**/*.test.ts'"},"repository":{"type":"git","url":"https://github.com/google-github-actions/auth"},"keywords":["actions","google cloud","identity","auth","oidc"],"author":"GoogleCloudPlatform","license":"Apache-2.0","dependencies":{"@actions/core":"^1.6.0"},"devDependencies":{"@types/chai":"^4.2.21","@types/mocha":"^9.0.0","@types/node":"^16.9.1","@typescript-eslint/eslint-plugin":"^4.31.0","@typescript-eslint/parser":"^4.31.0","@zeit/ncc":"^0.22.3","chai":"^4.3.4","eslint":"^7.32.0","eslint-config-prettier":"^8.3.0","eslint-plugin-prettier":"^4.0.0","husky":"^7.0.2","mocha":"^9.1.1","prettier":"^2.4.0","ts-node":"^10.2.1","typescript":"^4.3.5"}};
module.exports = {"name":"@google-github-actions/auth","version":"0.4.0","description":"Authenticate to Google Cloud using OIDC tokens or JSON service account keys.","main":"dist/main/index.js","scripts":{"build":"ncc build src/main.ts -o dist/main && ncc build src/post.ts -o dist/post","lint":"eslint . --ext .ts,.tsx","format":"prettier --write **/*.ts","test":"mocha -r ts-node/register -t 120s 'tests/**/*.test.ts'"},"repository":{"type":"git","url":"https://github.com/google-github-actions/auth"},"keywords":["actions","google cloud","identity","auth","oidc"],"author":"GoogleCloudPlatform","license":"Apache-2.0","dependencies":{"@actions/core":"^1.6.0"},"devDependencies":{"@types/chai":"^4.2.21","@types/mocha":"^9.0.0","@types/node":"^16.9.1","@typescript-eslint/eslint-plugin":"^4.31.0","@typescript-eslint/parser":"^4.31.0","@zeit/ncc":"^0.22.3","chai":"^4.3.4","eslint":"^7.32.0","eslint-config-prettier":"^8.3.0","eslint-plugin-prettier":"^4.0.0","husky":"^7.0.2","mocha":"^9.1.1","prettier":"^2.4.0","ts-node":"^10.2.1","typescript":"^4.3.5"}};
/***/ }),

1860
dist/post/index.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,9 @@
"name": "@google-github-actions/auth",
"version": "0.4.0",
"description": "Authenticate to Google Cloud using OIDC tokens or JSON service account keys.",
"main": "dist/index.js",
"main": "dist/main/index.js",
"scripts": {
"build": "ncc build src/main.ts",
"build": "ncc build src/main.ts -o dist/main && ncc build src/post.ts -o dist/post",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write **/*.ts",
"test": "mocha -r ts-node/register -t 120s 'tests/**/*.test.ts'"

View File

@ -81,12 +81,22 @@ async function run(): Promise<void> {
// fails, which means continue-on-error actions will still have the file
// available.
if (createCredentialsFile) {
const runnerTempDir = process.env.RUNNER_TEMP;
if (!runnerTempDir) {
throw new Error('$RUNNER_TEMP is not set');
// Note: We explicitly and intentionally export to GITHUB_WORKSPACE
// instead of RUNNER_TEMP, because RUNNER_TEMP is not shared with
// Docker-based actions on the filesystem. Exporting to GITHUB_WORKSPACE
// ensures that the exported credentials are automatically available to
// Docker-based actions without user modification.
//
// This has the unintended side-effect of leaking credentials over time,
// because GITHUB_WORKSPACE is not automatically cleaned up on self-hosted
// runners. To mitigate this issue, this action defines a post step to
// remove any created credentials.
const githubWorkspace = process.env.GITHUB_WORKSPACE;
if (!githubWorkspace) {
throw new Error('$GITHUB_WORKSPACE is not set');
}
const credentialsPath = await client.createCredentialsFile(runnerTempDir);
const credentialsPath = await client.createCredentialsFile(githubWorkspace);
setOutput('credentials_file_path', credentialsPath);
// CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE is picked up by gcloud to use
// a specific credential file (subject to change and equivalent to auth/credential_file_override)

27
src/post.ts Normal file
View File

@ -0,0 +1,27 @@
'use strict';
import { getBooleanInput, setFailed, info as logInfo } from '@actions/core';
import { removeExportedCredentials } from './utils';
/**
* Executes the post action, documented inline.
*/
export async function run(): Promise<void> {
try {
const cleanupCredentials: boolean = getBooleanInput('cleanup_credentials');
if (!cleanupCredentials) {
return;
}
const exportedPath = await removeExportedCredentials();
if (exportedPath) {
logInfo(`Removed exported credentials at ${exportedPath}`);
} else {
logInfo('No exported credentials found');
}
} catch (err) {
setFailed(`google-github-actions/auth post failed with: ${err}`);
}
}
run();

View File

@ -27,6 +27,36 @@ export async function writeSecureFile(outputDir: string, data: string): Promise<
return pth;
}
/**
* removeExportedCredentials removes any exported credentials file. If the file
* does not exist, it does nothing.
*
* @returns Path of the file that was removed.
*/
export async function removeExportedCredentials(): Promise<string> {
// 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 '';
}
// Delete the file.
try {
await fs.unlink(credentialsPath);
return credentialsPath;
} catch (err) {
if (err instanceof Error)
if (err && err.message && err.message.includes('ENOENT')) {
return '';
}
throw new Error(`failed to remove exported credentials: ${err}`);
}
}
/**
* Converts a multi-line or comma-separated collection of strings into an array
* of trimmed strings.

35
tests/utils.test.ts Normal file
View File

@ -0,0 +1,35 @@
'use strict';
import 'mocha';
import { expect } from 'chai';
import { tmpdir } from 'os';
import { existsSync } from 'fs';
import { removeExportedCredentials } from '../src/utils';
import { writeSecureFile } from '../src/utils';
describe('post', () => {
describe('#removeExportedCredentials', () => {
it('does nothing when GOOGLE_GHA_CREDS_PATH is unset', async () => {
delete process.env.GOOGLE_GHA_CREDS_PATH;
const pth = await removeExportedCredentials();
expect(pth).to.eq('');
});
it('deletes the file', async () => {
const filePath = await writeSecureFile(tmpdir(), 'my data');
process.env.GOOGLE_GHA_CREDS_PATH = filePath;
const pth = await removeExportedCredentials();
expect(existsSync(filePath)).to.be.false;
expect(pth).to.eq(filePath);
});
it('does not fail if the file does not exist', async () => {
const filePath = '/not/a/file';
process.env.GOOGLE_GHA_CREDS_PATH = filePath;
const pth = await removeExportedCredentials();
expect(pth).to.eq('');
});
});
});