Add the ability to generate and export a credentials file (#7)

This credentials file can be passed to gcloud or other Google Cloud SDKs to automatically do the exchange.
This commit is contained in:
Seth Vargo 2021-09-21 18:10:27 -04:00 committed by GitHub
parent c7bb6ad28f
commit febe21311b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 571 additions and 153 deletions

View File

@ -16,7 +16,7 @@ jobs:
steps:
- uses: 'actions/checkout@v2'
- uses: 'actions/setup-node@master'
- uses: 'actions/setup-node@v2'
with:
node-version: '12.x'
@ -29,11 +29,11 @@ jobs:
- name: 'npm test'
run: 'npm run test'
access_token:
name: 'access_token'
credentials_file:
name: 'credentials_file'
permissions:
id-token: write
contents: read
id-token: 'write'
contents: 'read'
runs-on: '${{ matrix.operating-system }}'
strategy:
fail-fast: false
@ -42,15 +42,50 @@ jobs:
- 'ubuntu-latest'
- 'windows-latest'
- 'macos-latest'
steps:
- uses: 'actions/checkout@v2'
- uses: 'actions/setup-node@master'
- uses: 'actions/setup-node@v2'
with:
node-version: '12.x'
- id: 'access-token'
name: 'integration'
- uses: 'google-github-actions/setup-gcloud@master'
with:
project_id: 'actions-oidc-test'
- id: 'auth'
name: 'auth'
uses: './'
with:
create_credentials_file: true
workload_identity_provider: 'projects/469401941463/locations/global/workloadIdentityPools/github-actions/providers/github-oidc-auth-google-cloud'
service_account: 'github-secret-accessor@actions-oidc-test.iam.gserviceaccount.com'
- id: 'gcloud'
name: 'gcloud'
run: |-
gcloud auth login --brief --cred-file="${{ steps.auth.outputs.credentials_file_path }}"
gcloud secrets versions access "latest" --secret "my-secret"
access_token:
name: 'access_token'
permissions:
id-token: 'write'
contents: 'read'
runs-on: 'ubuntu-latest'
strategy:
fail-fast: false
steps:
- uses: 'actions/checkout@v2'
- uses: 'actions/setup-node@v2'
with:
node-version: '12.x'
- id: 'auth'
name: 'auth'
uses: './'
with:
token_format: 'access_token'
@ -60,25 +95,21 @@ jobs:
id_token:
name: 'id_token'
permissions:
id-token: write
contents: read
runs-on: '${{ matrix.operating-system }}'
id-token: 'write'
contents: 'read'
runs-on: 'ubuntu-latest'
strategy:
fail-fast: false
matrix:
operating-system:
- 'ubuntu-latest'
- 'windows-latest'
- 'macos-latest'
steps:
- uses: 'actions/checkout@v2'
- uses: 'actions/setup-node@master'
- uses: 'actions/setup-node@v2'
with:
node-version: '12.x'
- id: 'id-token'
name: 'integration'
- id: 'auth'
name: 'auth'
uses: './'
with:
token_format: 'id_token'

172
README.md
View File

@ -19,11 +19,13 @@ and permissions on Google Cloud.
1. Exchange the GitHub Actions OIDC token for a short-lived Google Cloud access
token
## Prerequisites
- This action requires you to create and configure a Google Cloud Workload
Identity Provider. See [#setup](#setup) for instructions.
## Usage
```yaml
@ -33,11 +35,10 @@ jobs:
# Add "id-token" with the intended permissions.
permissions:
id-token: write
contents: read
id-token: 'write'
steps:
- id: 'google-cloud-auth'
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'sethvargo/oidc-auth-google-cloud@v0.2.0'
with:
@ -45,13 +46,16 @@ jobs:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
# Example of using the output:
- id: 'access-secret'
# Example of using the token:
- name: 'Access secret'
run: |-
curl https://secretmanager.googleapis.com/v1/projects/my-project/secrets/my-secret/versions/1:access \
--header "Authorization: Bearer ${{ steps.google-cloud-auth.outputs.access_token }}"
--header "Authorization: Bearer ${{ steps.auth.outputs.access_token }}"
```
See [Examples](#examples) for more examples.
## Inputs
- `workload_identity_provider`: (Required) The full identifier of the Workload
@ -75,9 +79,24 @@ jobs:
`"sigstore"`, but this variable exists in case custom values are permitted
in the future. The default value is `"sigstore"`.
- `token_format`: (Optional) Format of the generated token. For OAuth 2.0
access tokens, specify "access_token". For OIDC tokens, specify "id_token".
The default value is "access_token".
- `create_credentials_file`: (Optional) If true, the action will securely
generate a credentials file which can be used for authentication via gcloud
and Google Cloud SDKs. The default is false.
- `activate_credentials_file`: (Optional) If true and
"create_credentials_file" is also true, this will set the
`GOOGLE_APPLICATION_CREDENTIALS` environment variable to the path to the
credentials file, which gcloud and Google Cloud SDKs automatically consume.
The default value is true.
- `token_format`: (Optional) Output format for the generated authentication
token.
- For OAuth 2.0 access tokens, specify "access_token".
- For OIDC tokens, specify "id_token".
- To skip token generation, omit or set to the empty string "".
The default value is "" (skip token creation).
- `delegates`: (Optional) List of additional service account emails or unique
identities to use for impersonation in the chain. By default there are no
@ -95,23 +114,144 @@ jobs:
https://www.googleapis.com/auth/cloud-platform
```
- `id_token_audience`: (Optional) The audience for the generated ID Token.
- `id_token_audience`: (Required\*) The audience for the generated ID Token.
This option is required when "token_format" is "id_token", but otherwise can
be omitted.
- `id_token_include_email`: (Optional) Optional parameter of whether to
include the service account email in the generated token. If true, the token
will contain "email" and "email_verified" claims. This is only valid when
"token_format" is "access_token". The default value is false.
## Outputs
- `access_token`: The authenticated Google Cloud access token for calling
other Google Cloud APIs.
- `credentials_file_path`: Path on the local filesystem where the generated
credentials file resides. This is only available if
"create_credentials_file" was set to true.
- `access_token_expiration`: The RFC3339 UTC "Zulu" format timestamp when the
token expires.
- `access_token`: The Google Cloud access token for calling other Google Cloud
APIs. This is only available when "token_format" is "access_token".
- `access_token_expiration`: The RFC3339 UTC "Zulu" format timestamp for the
access token. This is only available when "token_format" is "access_token".
- `id_token`: The Google Cloud ID token. This is only available when
"token_format" is "id_token".
## Examples
#### Cloud SDK (gcloud)
This example demonstrates using this GitHub Action to configure authentication
for the `gcloud` CLI tool. Note this does **not** work for the `gsutil` tool.
```yaml
jobs:
run:
# ...
# Add "id-token" with the intended permissions.
permissions:
id-token: 'write'
steps:
# Install gcloud, do not specify authentication.
- uses: 'google-github-actions/setup-gcloud@master'
with:
project_id: 'my-project'
# Configure Workload Identity Federation via a credentials file.
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'sethvargo/oidc-auth-google-cloud@v0.2.0'
with:
create_credentials_file: 'access_token'
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
# Authenticate using the created credentials file.
- id: 'gcloud'
name: 'gcloud'
run: |-
gcloud auth login --brief --cred-file="${{ steps.auth.outputs.credentials_file_path }}"
# Now you can run gcloud commands authenticated as the impersonated service account.
gcloud secrets versions access "latest" --secret "my-secret"
```
#### Access Token (OAuth 2.0)
This example demonstrates using this GitHub Action to generate an OAuth 2.0
Access Token for authenticating to Google Cloud. Most Google Cloud APIs accept
this access token as authentication.
The default lifetime is 1 hour, but you can request up to 12 hours if you set
the [`constraints/iam.allowServiceAccountCredentialLifetimeExtension` organization policy](https://cloud.google.com/resource-manager/docs/organization-policy/org-policy-constraints).
```yaml
jobs:
run:
# ...
# Add "id-token" with the intended permissions.
permissions:
id-token: 'write'
steps:
# Configure Workload Identity Federation and generate an access token.
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'sethvargo/oidc-auth-google-cloud@v0.2.0'
with:
token_format: 'access_token'
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
access_token_lifetime: '300s' # optional, default: '3600s' (1 hour)
# Example of using the output. The token is usually provided as a Bearer
# token.
- id: 'access-secret'
run: |-
curl https://secretmanager.googleapis.com/v1/projects/my-project/secrets/my-secret/versions/1:access \
--header "Authorization: Bearer ${{ steps.auth.outputs.access_token }}"
```
#### ID Token (JWT)
This example demonstrates using this GitHub Action to generate a Google Cloud ID
Token for authenticating to Google Cloud. This is most commonly used when
invoking a Cloud Run service.
```yaml
jobs:
run:
# ...
# Add "id-token" with the intended permissions.
permissions:
id-token: 'write'
steps:
# Configure Workload Identity Federation and generate an access token.
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'sethvargo/oidc-auth-google-cloud@v0.2.0'
with:
token_format: 'access_token'
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
id_token_audience: 'https://myapp-uvehjacqzq.a.run.app' # required, value depends on target
id_token_include_email: true # optional
# Example of using the output. The token is usually provided as a Bearer
# token.
- id: 'invoke-service'
run: |-
curl https://myapp-uvehjacqzq.a.run.app \
--header "Authorization: Bearer ${{ steps.auth.outputs.id_token }}"
```
- `id_token`: The authenticated Google Cloud ID token. This token is only
generated when `id_token_audience` input parameter is provided.
## Setup

View File

@ -38,12 +38,27 @@ inputs:
exists in case custom values are permitted in the future.
default: 'sigstore'
required: false
create_credentials_file:
description: |-
If true, the action will securely generate a credentials file which can be
used for authentication via gcloud and Google Cloud SDKs.
default: false
required: false
activate_credentials_file:
description: |-
If true and create_credentials_file is also true, this will set the
GOOGLE_APPLICATION_CREDENTIALS environment variable to the path to the
credentials file, which gcloud and Google Cloud SDKs automatically
consume.
default: true
required: false
token_format:
description: |-
Format for the generated token. For OAuth 2.0 access tokens, specify
"access_token". For OIDC tokens, specify "id_token".
default: 'access_token'
required: true
Output format for the generated authentication token. For OAuth 2.0 access
tokens, specify "access_token". For OIDC tokens, specify "id_token". To
skip token generation, leave this value empty.
default: ''
required: false
delegates:
description: |-
List of additional service account emails or unique identities to use for
@ -78,19 +93,23 @@ inputs:
Optional parameter of whether to include the service account email in the
generated token. If true, the token will contain "email" and
"email_verified" claims. This is only valid when "token_format" is
"access_token".
"id_token".
default: false
required: false
outputs:
credentials_file_path:
description: |-
Path on the local filesystem where the generated credentials file resides.
This is only available if "create_credentials_file" was set to true.
access_token:
description: |-
The Google Cloud access token for calling other Google Cloud APIs. This
is only available when "token_format" is "access_token".
The Google Cloud access token for calling other Google Cloud APIs. This is
only available when "token_format" is "access_token".
access_token_expiration:
description: |-
The expiration timestamp for the access token. This is only available
when "token_format" is "access_token".
The RFC3339 UTC "Zulu" format timestamp for the access token. This is only
available when "token_format" is "access_token".
id_token:
description: |-
The Google Cloud ID token. This is only available when "token_format" is

115
dist/index.js vendored
View File

@ -194,6 +194,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(__webpack_require__(470));
const client_1 = __webpack_require__(976);
const url_1 = __webpack_require__(835);
/**
* Converts a multi-line or comma-separated collection of strings into an array
* of trimmed strings.
@ -225,12 +226,49 @@ function run() {
});
const serviceAccount = core.getInput('service_account', { required: true });
const audience = core.getInput('audience');
const tokenFormat = core.getInput('token_format', { required: true });
const createCredentialsFile = core.getBooleanInput('create_credentials_file');
const activateCredentialsFile = core.getBooleanInput('activate_credentials_file');
const tokenFormat = core.getInput('token_format');
const delegates = explodeStrings(core.getInput('delegates'));
const accessTokenLifetime = core.getInput('access_token_lifetime');
const accessTokenScopes = explodeStrings(core.getInput('access_token_scopes'));
const idTokenAudience = core.getInput('id_token_audience');
const idTokenIncludeEmail = core.getBooleanInput('id_token_include_email');
// Always write the credentials file first, before trying to generate
// tokens. This will ensure the file is written even if token generation
// fails, which means continue-on-error actions will still have the file
// available.
if (createCredentialsFile) {
const runnerTempDir = process.env.RUNNER_TEMP;
// Extract the request token and request URL from the environment. These
// are only set when an id-token is requested and the submitter has
// collaborator permissions.
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
if (!requestToken) {
throw new Error('$ACTIONS_ID_TOKEN_REQUEST_TOKEN is not set');
}
const requestURLRaw = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
if (!requestURLRaw) {
throw new Error('$ACTIONS_ID_TOKEN_REQUEST_URL is not set');
}
const requestURL = new url_1.URL(requestURLRaw);
// Append the audience value to the request.
const params = requestURL.searchParams;
params.set('audience', audience);
requestURL.search = params.toString();
// Create the credentials file.
const outputPath = yield client_1.Client.createCredentialsFile({
providerID: workloadIdentityProvider,
serviceAccount: serviceAccount,
requestToken: requestToken,
requestURL: requestURL.toString(),
outputDir: runnerTempDir,
});
core.setOutput('credentials_file_path', outputPath);
// Also set the magic environment variable for gcloud and SDKs if
// requested.
if (activateCredentialsFile) {
core.exportVariable('GOOGLE_APPLICATION_CREDENTIALS', outputPath);
}
}
// getFederatedToken is a closure that gets the federated token.
const getFederatedToken = () => __awaiter(this, void 0, void 0, function* () {
// Get the GitHub OIDC token.
const githubOIDCToken = yield core.getIDToken(audience);
// Exchange the GitHub OIDC token for a Google Federated Token.
@ -239,9 +277,19 @@ function run() {
token: githubOIDCToken,
});
core.setSecret(googleFederatedToken);
return googleFederatedToken;
});
switch (tokenFormat) {
case '': {
break;
}
case null: {
break;
}
case 'access_token': {
// Exchange the Google Federated Token for an access token.
const accessTokenLifetime = core.getInput('access_token_lifetime');
const accessTokenScopes = explodeStrings(core.getInput('access_token_scopes'));
const googleFederatedToken = yield getFederatedToken();
const { accessToken, expiration } = yield client_1.Client.googleAccessToken({
token: googleFederatedToken,
serviceAccount: serviceAccount,
@ -255,7 +303,9 @@ function run() {
break;
}
case 'id_token': {
// Exchange the Google Federated Token for an id token.
const idTokenAudience = core.getInput('id_token_audience', { required: true });
const idTokenIncludeEmail = core.getBooleanInput('id_token_include_email');
const googleFederatedToken = yield getFederatedToken();
const { token } = yield client_1.Client.googleIDToken({
token: googleFederatedToken,
serviceAccount: serviceAccount,
@ -640,6 +690,13 @@ module.exports = require("assert");
module.exports = __webpack_require__(141);
/***/ }),
/***/ 417:
/***/ (function(module) {
module.exports = require("crypto");
/***/ }),
/***/ 431:
@ -1823,6 +1880,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
Object.defineProperty(exports, "__esModule", { value: true });
exports.Client = void 0;
const https_1 = __importDefault(__webpack_require__(211));
const fs_1 = __webpack_require__(747);
const crypto_1 = __importDefault(__webpack_require__(417));
const path_1 = __importDefault(__webpack_require__(622));
const url_1 = __webpack_require__(835);
class Client {
/**
@ -1830,6 +1890,12 @@ class Client {
* request.
*/
static request(opts, data) {
if (!opts.headers) {
opts.headers = {};
}
if (!opts.headers['User-Agent']) {
opts.headers['User-Agent'] = 'sethvargo:oidc-auth-google-cloud/0.2.1';
}
return new Promise((resolve, reject) => {
const req = https_1.default.request(opts, (res) => {
res.setEncoding('utf8');
@ -1963,6 +2029,41 @@ class Client {
}
});
}
/**
* createCredentialsFile creates a Google Cloud credentials file that can be
* set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries.
*/
static createCredentialsFile({ providerID, serviceAccount, requestToken, requestURL, outputDir, }) {
return __awaiter(this, void 0, void 0, function* () {
const data = {
type: 'external_account',
audience: `//iam.googleapis.com/${providerID}`,
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
token_url: 'https://sts.googleapis.com/v1/token',
service_account_impersonation_url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`,
credential_source: {
url: requestURL,
headers: {
Authorization: `Bearer ${requestToken}`,
},
format: {
type: 'json',
subject_token_field_name: 'value',
},
},
};
// Generate a random filename to store the credential. 12 bytes is 24
// characters in hex. It's not the ideal entropy, but we have to be under
// the 255 character limit for Windows filenames (which includes their
// entire leading path).
const uniqueName = crypto_1.default.randomBytes(12).toString('hex');
const pth = path_1.default.join(outputDir, uniqueName);
// Write the file as 0640 so the owner has RW, group as R, and the file is
// otherwise unreadable. Also write with EXCL to prevent a symlink attack.
yield fs_1.promises.writeFile(pth, JSON.stringify(data), { mode: 0o640, flag: 'wx' });
return pth;
});
}
}
exports.Client = Client;

160
package-lock.json generated
View File

@ -269,9 +269,9 @@
"dev": true
},
"node_modules/@types/chai": {
"version": "4.2.21",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.21.tgz",
"integrity": "sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg==",
"version": "4.2.22",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz",
"integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==",
"dev": true
},
"node_modules/@types/json-schema": {
@ -287,19 +287,19 @@
"dev": true
},
"node_modules/@types/node": {
"version": "16.9.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.2.tgz",
"integrity": "sha512-ZHty/hKoOLZvSz6BtP1g7tc7nUeJhoCf3flLjh8ZEv1vFKBWHXcnMbJMyN/pftSljNyy0kNW/UqI3DccnBnZ8w==",
"version": "16.9.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.6.tgz",
"integrity": "sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "4.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.31.1.tgz",
"integrity": "sha512-UDqhWmd5i0TvPLmbK5xY3UZB0zEGseF+DHPghZ37Sb83Qd3p8ujhvAtkU4OF46Ka5Pm5kWvFIx0cCTBFKo0alA==",
"version": "4.31.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.31.2.tgz",
"integrity": "sha512-w63SCQ4bIwWN/+3FxzpnWrDjQRXVEGiTt9tJTRptRXeFvdZc/wLiz3FQUwNQ2CVoRGI6KUWMNUj/pk63noUfcA==",
"dev": true,
"dependencies": {
"@typescript-eslint/experimental-utils": "4.31.1",
"@typescript-eslint/scope-manager": "4.31.1",
"@typescript-eslint/experimental-utils": "4.31.2",
"@typescript-eslint/scope-manager": "4.31.2",
"debug": "^4.3.1",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.1.0",
@ -324,15 +324,15 @@
}
},
"node_modules/@typescript-eslint/experimental-utils": {
"version": "4.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.31.1.tgz",
"integrity": "sha512-NtoPsqmcSsWty0mcL5nTZXMf7Ei0Xr2MT8jWjXMVgRK0/1qeQ2jZzLFUh4QtyJ4+/lPUyMw5cSfeeME+Zrtp9Q==",
"version": "4.31.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.31.2.tgz",
"integrity": "sha512-3tm2T4nyA970yQ6R3JZV9l0yilE2FedYg8dcXrTar34zC9r6JB7WyBQbpIVongKPlhEMjhQ01qkwrzWy38Bk1Q==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.7",
"@typescript-eslint/scope-manager": "4.31.1",
"@typescript-eslint/types": "4.31.1",
"@typescript-eslint/typescript-estree": "4.31.1",
"@typescript-eslint/scope-manager": "4.31.2",
"@typescript-eslint/types": "4.31.2",
"@typescript-eslint/typescript-estree": "4.31.2",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0"
},
@ -348,14 +348,14 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "4.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.31.1.tgz",
"integrity": "sha512-dnVZDB6FhpIby6yVbHkwTKkn2ypjVIfAR9nh+kYsA/ZL0JlTsd22BiDjouotisY3Irmd3OW1qlk9EI5R8GrvRQ==",
"version": "4.31.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.31.2.tgz",
"integrity": "sha512-EcdO0E7M/sv23S/rLvenHkb58l3XhuSZzKf6DBvLgHqOYdL6YFMYVtreGFWirxaU2mS1GYDby3Lyxco7X5+Vjw==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "4.31.1",
"@typescript-eslint/types": "4.31.1",
"@typescript-eslint/typescript-estree": "4.31.1",
"@typescript-eslint/scope-manager": "4.31.2",
"@typescript-eslint/types": "4.31.2",
"@typescript-eslint/typescript-estree": "4.31.2",
"debug": "^4.3.1"
},
"engines": {
@ -375,13 +375,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "4.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.31.1.tgz",
"integrity": "sha512-N1Uhn6SqNtU2XpFSkD4oA+F0PfKdWHyr4bTX0xTj8NRx1314gBDRL1LUuZd5+L3oP+wo6hCbZpaa1in6SwMcVQ==",
"version": "4.31.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.31.2.tgz",
"integrity": "sha512-2JGwudpFoR/3Czq6mPpE8zBPYdHWFGL6lUNIGolbKQeSNv4EAiHaR5GVDQaLA0FwgcdcMtRk+SBJbFGL7+La5w==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "4.31.1",
"@typescript-eslint/visitor-keys": "4.31.1"
"@typescript-eslint/types": "4.31.2",
"@typescript-eslint/visitor-keys": "4.31.2"
},
"engines": {
"node": "^8.10.0 || ^10.13.0 || >=11.10.1"
@ -392,9 +392,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "4.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.31.1.tgz",
"integrity": "sha512-kixltt51ZJGKENNW88IY5MYqTBA8FR0Md8QdGbJD2pKZ+D5IvxjTYDNtJPDxFBiXmka2aJsITdB1BtO1fsgmsQ==",
"version": "4.31.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.31.2.tgz",
"integrity": "sha512-kWiTTBCTKEdBGrZKwFvOlGNcAsKGJSBc8xLvSjSppFO88AqGxGNYtF36EuEYG6XZ9vT0xX8RNiHbQUKglbSi1w==",
"dev": true,
"engines": {
"node": "^8.10.0 || ^10.13.0 || >=11.10.1"
@ -405,13 +405,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "4.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.31.1.tgz",
"integrity": "sha512-EGHkbsUvjFrvRnusk6yFGqrqMBTue5E5ROnS5puj3laGQPasVUgwhrxfcgkdHNFECHAewpvELE1Gjv0XO3mdWg==",
"version": "4.31.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.31.2.tgz",
"integrity": "sha512-ieBq8U9at6PvaC7/Z6oe8D3czeW5d//Fo1xkF/s9394VR0bg/UaMYPdARiWyKX+lLEjY3w/FNZJxitMsiWv+wA==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "4.31.1",
"@typescript-eslint/visitor-keys": "4.31.1",
"@typescript-eslint/types": "4.31.2",
"@typescript-eslint/visitor-keys": "4.31.2",
"debug": "^4.3.1",
"globby": "^11.0.3",
"is-glob": "^4.0.1",
@ -432,12 +432,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "4.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.31.1.tgz",
"integrity": "sha512-PCncP8hEqKw6SOJY+3St4LVtoZpPPn+Zlpm7KW5xnviMhdqcsBty4Lsg4J/VECpJjw1CkROaZhH4B8M1OfnXTQ==",
"version": "4.31.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.31.2.tgz",
"integrity": "sha512-PrBId7EQq2Nibns7dd/ch6S6/M4/iwLM9McbgeEbCXfxdwRUNxJ4UNreJ6Gh3fI2GNKNrWnQxKL7oCPmngKBug==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "4.31.1",
"@typescript-eslint/types": "4.31.2",
"eslint-visitor-keys": "^2.0.0"
},
"engines": {
@ -2888,9 +2888,9 @@
"dev": true
},
"@types/chai": {
"version": "4.2.21",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.21.tgz",
"integrity": "sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg==",
"version": "4.2.22",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz",
"integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==",
"dev": true
},
"@types/json-schema": {
@ -2906,19 +2906,19 @@
"dev": true
},
"@types/node": {
"version": "16.9.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.2.tgz",
"integrity": "sha512-ZHty/hKoOLZvSz6BtP1g7tc7nUeJhoCf3flLjh8ZEv1vFKBWHXcnMbJMyN/pftSljNyy0kNW/UqI3DccnBnZ8w==",
"version": "16.9.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.6.tgz",
"integrity": "sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==",
"dev": true
},
"@typescript-eslint/eslint-plugin": {
"version": "4.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.31.1.tgz",
"integrity": "sha512-UDqhWmd5i0TvPLmbK5xY3UZB0zEGseF+DHPghZ37Sb83Qd3p8ujhvAtkU4OF46Ka5Pm5kWvFIx0cCTBFKo0alA==",
"version": "4.31.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.31.2.tgz",
"integrity": "sha512-w63SCQ4bIwWN/+3FxzpnWrDjQRXVEGiTt9tJTRptRXeFvdZc/wLiz3FQUwNQ2CVoRGI6KUWMNUj/pk63noUfcA==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "4.31.1",
"@typescript-eslint/scope-manager": "4.31.1",
"@typescript-eslint/experimental-utils": "4.31.2",
"@typescript-eslint/scope-manager": "4.31.2",
"debug": "^4.3.1",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.1.0",
@ -2927,55 +2927,55 @@
}
},
"@typescript-eslint/experimental-utils": {
"version": "4.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.31.1.tgz",
"integrity": "sha512-NtoPsqmcSsWty0mcL5nTZXMf7Ei0Xr2MT8jWjXMVgRK0/1qeQ2jZzLFUh4QtyJ4+/lPUyMw5cSfeeME+Zrtp9Q==",
"version": "4.31.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.31.2.tgz",
"integrity": "sha512-3tm2T4nyA970yQ6R3JZV9l0yilE2FedYg8dcXrTar34zC9r6JB7WyBQbpIVongKPlhEMjhQ01qkwrzWy38Bk1Q==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.7",
"@typescript-eslint/scope-manager": "4.31.1",
"@typescript-eslint/types": "4.31.1",
"@typescript-eslint/typescript-estree": "4.31.1",
"@typescript-eslint/scope-manager": "4.31.2",
"@typescript-eslint/types": "4.31.2",
"@typescript-eslint/typescript-estree": "4.31.2",
"eslint-scope": "^5.1.1",
"eslint-utils": "^3.0.0"
}
},
"@typescript-eslint/parser": {
"version": "4.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.31.1.tgz",
"integrity": "sha512-dnVZDB6FhpIby6yVbHkwTKkn2ypjVIfAR9nh+kYsA/ZL0JlTsd22BiDjouotisY3Irmd3OW1qlk9EI5R8GrvRQ==",
"version": "4.31.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.31.2.tgz",
"integrity": "sha512-EcdO0E7M/sv23S/rLvenHkb58l3XhuSZzKf6DBvLgHqOYdL6YFMYVtreGFWirxaU2mS1GYDby3Lyxco7X5+Vjw==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "4.31.1",
"@typescript-eslint/types": "4.31.1",
"@typescript-eslint/typescript-estree": "4.31.1",
"@typescript-eslint/scope-manager": "4.31.2",
"@typescript-eslint/types": "4.31.2",
"@typescript-eslint/typescript-estree": "4.31.2",
"debug": "^4.3.1"
}
},
"@typescript-eslint/scope-manager": {
"version": "4.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.31.1.tgz",
"integrity": "sha512-N1Uhn6SqNtU2XpFSkD4oA+F0PfKdWHyr4bTX0xTj8NRx1314gBDRL1LUuZd5+L3oP+wo6hCbZpaa1in6SwMcVQ==",
"version": "4.31.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.31.2.tgz",
"integrity": "sha512-2JGwudpFoR/3Czq6mPpE8zBPYdHWFGL6lUNIGolbKQeSNv4EAiHaR5GVDQaLA0FwgcdcMtRk+SBJbFGL7+La5w==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.31.1",
"@typescript-eslint/visitor-keys": "4.31.1"
"@typescript-eslint/types": "4.31.2",
"@typescript-eslint/visitor-keys": "4.31.2"
}
},
"@typescript-eslint/types": {
"version": "4.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.31.1.tgz",
"integrity": "sha512-kixltt51ZJGKENNW88IY5MYqTBA8FR0Md8QdGbJD2pKZ+D5IvxjTYDNtJPDxFBiXmka2aJsITdB1BtO1fsgmsQ==",
"version": "4.31.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.31.2.tgz",
"integrity": "sha512-kWiTTBCTKEdBGrZKwFvOlGNcAsKGJSBc8xLvSjSppFO88AqGxGNYtF36EuEYG6XZ9vT0xX8RNiHbQUKglbSi1w==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "4.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.31.1.tgz",
"integrity": "sha512-EGHkbsUvjFrvRnusk6yFGqrqMBTue5E5ROnS5puj3laGQPasVUgwhrxfcgkdHNFECHAewpvELE1Gjv0XO3mdWg==",
"version": "4.31.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.31.2.tgz",
"integrity": "sha512-ieBq8U9at6PvaC7/Z6oe8D3czeW5d//Fo1xkF/s9394VR0bg/UaMYPdARiWyKX+lLEjY3w/FNZJxitMsiWv+wA==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.31.1",
"@typescript-eslint/visitor-keys": "4.31.1",
"@typescript-eslint/types": "4.31.2",
"@typescript-eslint/visitor-keys": "4.31.2",
"debug": "^4.3.1",
"globby": "^11.0.3",
"is-glob": "^4.0.1",
@ -2984,12 +2984,12 @@
}
},
"@typescript-eslint/visitor-keys": {
"version": "4.31.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.31.1.tgz",
"integrity": "sha512-PCncP8hEqKw6SOJY+3St4LVtoZpPPn+Zlpm7KW5xnviMhdqcsBty4Lsg4J/VECpJjw1CkROaZhH4B8M1OfnXTQ==",
"version": "4.31.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.31.2.tgz",
"integrity": "sha512-PrBId7EQq2Nibns7dd/ch6S6/M4/iwLM9McbgeEbCXfxdwRUNxJ4UNreJ6Gh3fI2GNKNrWnQxKL7oCPmngKBug==",
"dev": true,
"requires": {
"@typescript-eslint/types": "4.31.1",
"@typescript-eslint/types": "4.31.2",
"eslint-visitor-keys": "^2.0.0"
}
},

View File

@ -1,4 +1,9 @@
'use strict';
import https, { RequestOptions } from 'https';
import { promises as fs } from 'fs';
import crypto from 'crypto';
import path from 'path';
import { URL } from 'url';
/**
@ -82,6 +87,28 @@ interface GoogleIDTokenResponse {
token: string;
}
/**
* CreateCredentialsFileParameters are the parameters to generate a Google Cloud
* credentials file for use with gcloud and other SDKs.
*
* @param providerID Full path (including project, location, etc) to the Google
* Cloud Workload Identity Provider.
* @param serviceAccount Email address or unique identifier of the service
* account to impersonate
* @param requestToken Local environment token to use as authentication to
* acquire the real OIDC token.
* @param requestURL URL endpoint to use to request the token.
* @param outputDir Path to a directory on disk where the file should be
* written. The function will determine the file name and write to it securely.
*/
interface CreateCredentialsFileParameters {
providerID: string;
serviceAccount: string;
requestToken: string;
requestURL: string;
outputDir: string;
}
export class Client {
/**
* request is a high-level helper that returns a promise from the executed
@ -255,4 +282,47 @@ export class Client {
throw new Error(`failed to generate Google Cloud ID token for ${serviceAccount}: ${err}`);
}
}
/**
* createCredentialsFile creates a Google Cloud credentials file that can be
* set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries.
*/
static async createCredentialsFile({
providerID,
serviceAccount,
requestToken,
requestURL,
outputDir,
}: CreateCredentialsFileParameters): Promise<string> {
const data = {
type: 'external_account',
audience: `//iam.googleapis.com/${providerID}`,
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
token_url: 'https://sts.googleapis.com/v1/token',
service_account_impersonation_url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`,
credential_source: {
url: requestURL,
headers: {
Authorization: `Bearer ${requestToken}`,
},
format: {
type: 'json',
subject_token_field_name: 'value',
},
},
};
// Generate a random filename to store the credential. 12 bytes is 24
// characters in hex. It's not the ideal entropy, but we have to be under
// the 255 character limit for Windows filenames (which includes their
// entire leading path).
const uniqueName = crypto.randomBytes(12).toString('hex');
const pth = path.join(outputDir, uniqueName);
// Write the file as 0640 so the owner has RW, group as R, and the file is
// otherwise unreadable. Also write with EXCL to prevent a symlink attack.
await fs.writeFile(pth, JSON.stringify(data), { mode: 0o640, flag: 'wx' });
return pth;
}
}

View File

@ -2,6 +2,7 @@
import * as core from '@actions/core';
import { Client } from './client';
import { URL } from 'url';
/**
* Converts a multi-line or comma-separated collection of strings into an array
@ -35,13 +36,55 @@ async function run(): Promise<void> {
});
const serviceAccount = core.getInput('service_account', { required: true });
const audience = core.getInput('audience');
const tokenFormat = core.getInput('token_format', { required: true });
const createCredentialsFile = core.getBooleanInput('create_credentials_file');
const activateCredentialsFile = core.getBooleanInput('activate_credentials_file');
const tokenFormat = core.getInput('token_format');
const delegates = explodeStrings(core.getInput('delegates'));
const accessTokenLifetime = core.getInput('access_token_lifetime');
const accessTokenScopes = explodeStrings(core.getInput('access_token_scopes'));
const idTokenAudience = core.getInput('id_token_audience');
const idTokenIncludeEmail = core.getBooleanInput('id_token_include_email');
// Always write the credentials file first, before trying to generate
// tokens. This will ensure the file is written even if token generation
// fails, which means continue-on-error actions will still have the file
// available.
if (createCredentialsFile) {
const runnerTempDir = process.env.RUNNER_TEMP!;
// Extract the request token and request URL from the environment. These
// are only set when an id-token is requested and the submitter has
// collaborator permissions.
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
if (!requestToken) {
throw new Error('$ACTIONS_ID_TOKEN_REQUEST_TOKEN is not set');
}
const requestURLRaw = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
if (!requestURLRaw) {
throw new Error('$ACTIONS_ID_TOKEN_REQUEST_URL is not set');
}
const requestURL = new URL(requestURLRaw);
// Append the audience value to the request.
const params = requestURL.searchParams;
params.set('audience', audience);
requestURL.search = params.toString();
// Create the credentials file.
const outputPath = await Client.createCredentialsFile({
providerID: workloadIdentityProvider,
serviceAccount: serviceAccount,
requestToken: requestToken,
requestURL: requestURL.toString(),
outputDir: runnerTempDir,
});
core.setOutput('credentials_file_path', outputPath);
// Also set the magic environment variable for gcloud and SDKs if
// requested.
if (activateCredentialsFile) {
core.exportVariable('GOOGLE_APPLICATION_CREDENTIALS', outputPath);
}
}
// getFederatedToken is a closure that gets the federated token.
const getFederatedToken = async (): Promise<string> => {
// Get the GitHub OIDC token.
const githubOIDCToken = await core.getIDToken(audience);
@ -51,10 +94,21 @@ async function run(): Promise<void> {
token: githubOIDCToken,
});
core.setSecret(googleFederatedToken);
return googleFederatedToken;
};
switch (tokenFormat) {
case '': {
break;
}
case null: {
break;
}
case 'access_token': {
// Exchange the Google Federated Token for an access token.
const accessTokenLifetime = core.getInput('access_token_lifetime');
const accessTokenScopes = explodeStrings(core.getInput('access_token_scopes'));
const googleFederatedToken = await getFederatedToken();
const { accessToken, expiration } = await Client.googleAccessToken({
token: googleFederatedToken,
serviceAccount: serviceAccount,
@ -68,7 +122,10 @@ async function run(): Promise<void> {
break;
}
case 'id_token': {
// Exchange the Google Federated Token for an id token.
const idTokenAudience = core.getInput('id_token_audience', { required: true });
const idTokenIncludeEmail = core.getBooleanInput('id_token_include_email');
const googleFederatedToken = await getFederatedToken();
const { token } = await Client.googleIDToken({
token: googleFederatedToken,
serviceAccount: serviceAccount,