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:
parent
c6fa692def
commit
1e9245c68a
42
.github/workflows/test.yaml
vendored
42
.github/workflows/test.yaml
vendored
@ -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}"'
|
||||
|
@ -31,7 +31,7 @@ and permissions on Google Cloud.
|
||||
- For authenticating via Workload Identity Federation, you must create and
|
||||
configure a Google Cloud Workload Identity Provider. See [setup](#setup)
|
||||
for instructions.
|
||||
|
||||
|
||||
## Limitations
|
||||
|
||||
- This Action does not support authenticating through service accounts via
|
||||
@ -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
|
||||
|
||||
|
@ -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'
|
||||
|
53
dist/index.js → dist/main/index.js
vendored
53
dist/index.js → dist/main/index.js
vendored
@ -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
1860
dist/post/index.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -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'"
|
||||
|
18
src/main.ts
18
src/main.ts
@ -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
27
src/post.ts
Normal 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();
|
30
src/utils.ts
30
src/utils.ts
@ -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
35
tests/utils.test.ts
Normal 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('');
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user