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'
|
- 'main'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: '${{ github.head_ref || github.ref }}'
|
group: '${{github.workflow}}-${{ github.head_ref || github.ref }}'
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@ -41,7 +41,7 @@ jobs:
|
|||||||
|
|
||||||
unit:
|
unit:
|
||||||
name: 'unit'
|
name: 'unit'
|
||||||
needs: install_and_compile
|
needs: 'install_and_compile'
|
||||||
runs-on: 'ubuntu-latest'
|
runs-on: 'ubuntu-latest'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -63,7 +63,7 @@ jobs:
|
|||||||
|
|
||||||
credentials_json:
|
credentials_json:
|
||||||
name: 'credentials_json'
|
name: 'credentials_json'
|
||||||
needs: install_and_compile
|
needs: 'install_and_compile'
|
||||||
runs-on: '${{ matrix.os }}'
|
runs-on: '${{ matrix.os }}'
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@ -125,7 +125,7 @@ jobs:
|
|||||||
|
|
||||||
workload_identity_federation:
|
workload_identity_federation:
|
||||||
name: 'workload_identity_federation'
|
name: 'workload_identity_federation'
|
||||||
needs: install_and_compile
|
needs: 'install_and_compile'
|
||||||
runs-on: '${{ matrix.os }}'
|
runs-on: '${{ matrix.os }}'
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@ -189,3 +189,37 @@ jobs:
|
|||||||
token_format: 'id_token'
|
token_format: 'id_token'
|
||||||
id_token_audience: 'https://secretmanager.googleapis.com/'
|
id_token_audience: 'https://secretmanager.googleapis.com/'
|
||||||
id_token_include_email: true
|
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
|
- For authenticating via Workload Identity Federation, you must create and
|
||||||
configure a Google Cloud Workload Identity Provider. See [setup](#setup)
|
configure a Google Cloud Workload Identity Provider. See [setup](#setup)
|
||||||
for instructions.
|
for instructions.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
- This Action does not support authenticating through service accounts via
|
- 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
|
identities to use for impersonation in the chain. By default there are no
|
||||||
delegates.
|
delegates.
|
||||||
|
|
||||||
|
- `cleanup_credentials`: (Optional) If true, the action will remove any
|
||||||
|
generated credentials from the filesystem upon completion. The default is
|
||||||
|
true.
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
|
|
||||||
|
@ -71,6 +71,12 @@ inputs:
|
|||||||
impersonation in the chain.
|
impersonation in the chain.
|
||||||
default: ''
|
default: ''
|
||||||
required: false
|
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 params
|
||||||
access_token_lifetime:
|
access_token_lifetime:
|
||||||
@ -130,4 +136,5 @@ branding:
|
|||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: 'node12'
|
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
|
// fails, which means continue-on-error actions will still have the file
|
||||||
// available.
|
// available.
|
||||||
if (createCredentialsFile) {
|
if (createCredentialsFile) {
|
||||||
const runnerTempDir = process.env.RUNNER_TEMP;
|
// Note: We explicitly and intentionally export to GITHUB_WORKSPACE
|
||||||
if (!runnerTempDir) {
|
// instead of RUNNER_TEMP, because RUNNER_TEMP is not shared with
|
||||||
throw new Error('$RUNNER_TEMP is not set');
|
// 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);
|
(0, core_1.setOutput)('credentials_file_path', credentialsPath);
|
||||||
// CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE is picked up by gcloud to use
|
// 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)
|
// 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 };
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
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 fs_1 = __webpack_require__(747);
|
||||||
const crypto_1 = __importDefault(__webpack_require__(417));
|
const crypto_1 = __importDefault(__webpack_require__(417));
|
||||||
const path_1 = __importDefault(__webpack_require__(622));
|
const path_1 = __importDefault(__webpack_require__(622));
|
||||||
@ -627,6 +637,37 @@ function writeSecureFile(outputDir, data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
exports.writeSecureFile = writeSecureFile;
|
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
|
* Converts a multi-line or comma-separated collection of strings into an array
|
||||||
* of trimmed strings.
|
* of trimmed strings.
|
||||||
@ -1917,7 +1958,7 @@ module.exports = require("util");
|
|||||||
/***/ 731:
|
/***/ 731:
|
||||||
/***/ (function(module) {
|
/***/ (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",
|
"name": "@google-github-actions/auth",
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"description": "Authenticate to Google Cloud using OIDC tokens or JSON service account keys.",
|
"description": "Authenticate to Google Cloud using OIDC tokens or JSON service account keys.",
|
||||||
"main": "dist/index.js",
|
"main": "dist/main/index.js",
|
||||||
"scripts": {
|
"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",
|
"lint": "eslint . --ext .ts,.tsx",
|
||||||
"format": "prettier --write **/*.ts",
|
"format": "prettier --write **/*.ts",
|
||||||
"test": "mocha -r ts-node/register -t 120s 'tests/**/*.test.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
|
// fails, which means continue-on-error actions will still have the file
|
||||||
// available.
|
// available.
|
||||||
if (createCredentialsFile) {
|
if (createCredentialsFile) {
|
||||||
const runnerTempDir = process.env.RUNNER_TEMP;
|
// Note: We explicitly and intentionally export to GITHUB_WORKSPACE
|
||||||
if (!runnerTempDir) {
|
// instead of RUNNER_TEMP, because RUNNER_TEMP is not shared with
|
||||||
throw new Error('$RUNNER_TEMP is not set');
|
// 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);
|
setOutput('credentials_file_path', credentialsPath);
|
||||||
// CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE is picked up by gcloud to use
|
// 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)
|
// 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;
|
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
|
* Converts a multi-line or comma-separated collection of strings into an array
|
||||||
* of trimmed strings.
|
* 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