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:
parent
c7bb6ad28f
commit
febe21311b
69
.github/workflows/test.yaml
vendored
69
.github/workflows/test.yaml
vendored
@ -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
172
README.md
@ -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
|
||||
|
||||
|
37
action.yml
37
action.yml
@ -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
|
||||
|
129
dist/index.js
vendored
129
dist/index.js
vendored
@ -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,23 +226,70 @@ 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');
|
||||
// Get the GitHub OIDC token.
|
||||
const githubOIDCToken = yield core.getIDToken(audience);
|
||||
// Exchange the GitHub OIDC token for a Google Federated Token.
|
||||
const googleFederatedToken = yield client_1.Client.googleFederatedToken({
|
||||
providerID: workloadIdentityProvider,
|
||||
token: githubOIDCToken,
|
||||
// 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.
|
||||
const googleFederatedToken = yield client_1.Client.googleFederatedToken({
|
||||
providerID: workloadIdentityProvider,
|
||||
token: githubOIDCToken,
|
||||
});
|
||||
core.setSecret(googleFederatedToken);
|
||||
return googleFederatedToken;
|
||||
});
|
||||
core.setSecret(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
160
package-lock.json
generated
@ -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"
|
||||
}
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
87
src/main.ts
87
src/main.ts
@ -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,26 +36,79 @@ 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');
|
||||
|
||||
// Get the GitHub OIDC token.
|
||||
const githubOIDCToken = await core.getIDToken(audience);
|
||||
// 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!;
|
||||
|
||||
// Exchange the GitHub OIDC token for a Google Federated Token.
|
||||
const googleFederatedToken = await Client.googleFederatedToken({
|
||||
providerID: workloadIdentityProvider,
|
||||
token: githubOIDCToken,
|
||||
});
|
||||
core.setSecret(googleFederatedToken);
|
||||
// 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);
|
||||
|
||||
// Exchange the GitHub OIDC token for a Google Federated Token.
|
||||
const googleFederatedToken = await Client.googleFederatedToken({
|
||||
providerID: workloadIdentityProvider,
|
||||
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,
|
||||
|
Loading…
Reference in New Issue
Block a user