diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6e51b0a..0967b64 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -29,16 +29,14 @@ jobs: - name: 'npm test' run: 'npm run test' - credentials_file: - name: 'credentials_file' - permissions: - id-token: 'write' - contents: 'read' - runs-on: '${{ matrix.operating-system }}' + + credentials_json: + name: 'credentials_json' + runs-on: '${{ matrix.os }}' strategy: fail-fast: false matrix: - operating-system: + os: - 'ubuntu-latest' - 'windows-latest' - 'macos-latest' @@ -50,66 +48,62 @@ jobs: with: node-version: '12.x' - - name: 'build' - run: |- - npm ci - npm run build - - - uses: 'google-github-actions/setup-gcloud@master' - with: - project_id: 'actions-oidc-test' - - - id: 'auth' - name: 'auth' + - id: 'auth-default' + name: 'auth-default' uses: './' with: - create_credentials_file: true - workload_identity_provider: 'projects/469401941463/locations/global/workloadIdentityPools/github-actions/providers/google-github-actions' - service_account: 'github-secret-accessor@actions-oidc-test.iam.gserviceaccount.com' + credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}' + + - id: 'setup-gcloud' + name: 'setup-gcloud' + uses: 'google-github-actions/setup-gcloud@master' - id: 'gcloud' name: 'gcloud' + shell: 'bash' 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' - - - name: 'build' - run: |- - npm ci - npm run build - - - id: 'auth' - name: 'auth' + - id: 'auth-access-token' + name: 'auth-access-token' uses: './' with: + credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}' token_format: 'access_token' - workload_identity_provider: 'projects/469401941463/locations/global/workloadIdentityPools/github-actions/providers/google-github-actions' - service_account: 'github-secret-accessor@actions-oidc-test.iam.gserviceaccount.com' - id_token: - name: 'id_token' - permissions: - id-token: 'write' - contents: 'read' - runs-on: 'ubuntu-latest' + - id: 'access-token' + name: 'access-token' + shell: 'bash' + run: |- + curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-access-token.outputs.project_id }}/secrets/my-secret/versions/latest:access \ + --silent \ + --show-error \ + --fail \ + --header "Authorization: Bearer ${{ steps.auth-access-token.outputs.access_token }}" + + - id: 'auth-id-token' + name: 'auth-id-token' + uses: './' + with: + credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}' + token_format: 'id_token' + id_token_audience: 'https://secretmanager.googleapis.com/' + id_token_include_email: true + + + workload_identity_federation: + name: 'workload_identity_federation' + runs-on: '${{ matrix.os }}' strategy: fail-fast: false + matrix: + os: + - 'ubuntu-latest' + - 'windows-latest' + - 'macos-latest' + + permissions: + id-token: 'write' steps: - uses: 'actions/checkout@v2' @@ -118,17 +112,47 @@ jobs: with: node-version: '12.x' - - name: 'build' - run: |- - npm ci - npm run build - - - id: 'auth' - name: 'auth' + - id: 'auth-default' + name: 'auth-default' uses: './' with: - token_format: 'id_token' workload_identity_provider: 'projects/469401941463/locations/global/workloadIdentityPools/github-actions/providers/google-github-actions' service_account: 'github-secret-accessor@actions-oidc-test.iam.gserviceaccount.com' - id_token_audience: 'my-aud' + + - id: 'setup-gcloud' + name: 'setup-gcloud' + uses: 'google-github-actions/setup-gcloud@master' + + - id: 'gcloud' + name: 'gcloud' + shell: 'bash' + run: |- + gcloud secrets versions access "latest" --secret "my-secret" + + - id: 'auth-access-token' + name: 'auth-access-token' + uses: './' + with: + workload_identity_provider: 'projects/469401941463/locations/global/workloadIdentityPools/github-actions/providers/google-github-actions' + service_account: 'github-secret-accessor@actions-oidc-test.iam.gserviceaccount.com' + token_format: 'access_token' + + - id: 'access-token' + name: 'access-token' + shell: 'bash' + run: |- + curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-access-token.outputs.project_id }}/secrets/my-secret/versions/latest:access \ + --silent \ + --show-error \ + --fail \ + --header "Authorization: Bearer ${{ steps.auth-access-token.outputs.access_token }}" + + - id: 'auth-id-token' + name: 'auth-id-token' + uses: './' + with: + workload_identity_provider: 'projects/469401941463/locations/global/workloadIdentityPools/github-actions/providers/google-github-actions' + service_account: 'github-secret-accessor@actions-oidc-test.iam.gserviceaccount.com' + token_format: 'id_token' + id_token_audience: 'https://secretmanager.googleapis.com/' id_token_include_email: true diff --git a/.prettierrc.js b/.prettierrc.js index 13a7041..516e613 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -2,7 +2,6 @@ module.exports = { arrowParens: 'always', bracketSpacing: true, endOfLine: 'auto', - jsxBracketSameLine: true, jsxSingleQuote: true, printWidth: 100, quoteProps: 'consistent', diff --git a/README.md b/README.md index 45048c3..4ce74a7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ # auth -This GitHub Action exchanges a GitHub Actions OIDC token into a Google Cloud -access token using [Workload Identity Federation][wif]. This obviates the need +This GitHub Action establishes authentication to Google Cloud. It supports +traditional authentication via a Google Cloud Service Account Key JSON and +authentication via [Workload Identity Federation][wif]. + +Workload Identity Federation is the recommended approach as it obviates the need to export a long-lived Google Cloud service account key and establishes a trust delegation relationship between a particular GitHub Actions workflow invocation and permissions on Google Cloud. -#### Previously +#### With Service Account Key JSON 1. Create a Google Cloud service account and grant IAM permissions 1. Export the long-lived JSON service account key @@ -22,8 +25,12 @@ and permissions on Google Cloud. ## Prerequisites -- This action requires you to create and configure a Google Cloud Workload - Identity Provider. See [#setup](#setup) for instructions. +- For authenticating via Google Cloud Service Account Keys, you must create an + export a Google Cloud Service Account Key in JSON format. + +- For authenticating via Workload Identity Federation, you must create and + configure a Google Cloud Workload Identity Provider. See [setup](#setup) + for instructions. ## Usage @@ -62,17 +69,21 @@ See [Examples](#examples) for more examples. ## Inputs -- `workload_identity_provider`: (Required) The full identifier of the Workload - Identity Provider, including the project number, pool name, and provider - name. This must be the full identifier which includes all parts, for - example: +### Authenticating via Workload Identity Federation + +The following inputs are for _authenticating_ to Google Cloud via Workload +Identity Federation. + +- `workload_identity_provider`: (Required) The full identifier of the Workload Identity + Provider, including the project number, pool name, and provider name. If + provided, this must be the full identifier which includes all parts: ```text projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider ``` -- `service_account`: (Required) Email address or unique identifier of the - Google Cloud service account for which to generate credentials. For example: +- `service_account`: (Required) Email address or unique identifier of the Google Cloud + service account for which to generate credentials. For example: ```text my-service-account@my-project.iam.gserviceaccount.com @@ -81,30 +92,29 @@ See [Examples](#examples) for more examples. - `audience`: (Optional) The value for the audience (`aud`) parameter in the generated GitHub Actions OIDC token. This value defaults to the value of `workload_identity_provider`, which is also the default value Google Cloud - expects for the audience parameter on the token. + expects for the audience parameter on the token. We do not recommend + changing this value. -- `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. +### Authenticating via Service Account Key JSON -- `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. +The following inputs are for _authenticating_ to Google Cloud via a Service +Account Key JSON. **We recommend using Workload Identity Federation instead as +exporting a long-lived Service Account Key JSON credential poses a security +risk.** -- `token_format`: (Optional) Output format for the generated authentication - token. +- `credentials_json`: (Required) The Google Cloud JSON service account key to + use for authentication. To generate access tokens or ID tokens using this + service account, you must grant the underlying service account + `roles/iam.serviceAccountTokenCreator` permissions on itself. - - 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 "". +### Generating OAuth 2.0 access tokens - The default value is "" (skip token creation). +The following inputs are for _generating_ OAuth 2.0 access tokens for +authenticating to Google Cloud as an output for use in future steps in the +workflow. -- `delegates`: (Optional) List of additional service account emails or unique - identities to use for impersonation in the chain. By default there are no - delegates. +- `token_format`: This value must be `"access_token"` to generate OAuth 2.0 + access tokens. To skip token generation, omit or set to the empty string "". - `access_token_lifetime`: (Optional) Desired lifetime duration of the access token, in seconds. This must be specified as the number of seconds with a @@ -118,18 +128,43 @@ See [Examples](#examples) for more examples. https://www.googleapis.com/auth/cloud-platform ``` -- `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. +### Generating ID tokens + +The following inputs are for _generating_ ID tokens for authenticating to Google +Cloud as an output for use in future steps in the workflow. + +- `token_format`: This value must be `"id_token"` to generate ID tokens. To + skip token generation, omit or set to the empty string "". + +- `id_token_audience`: (Required) The audience for the generated ID Token. - `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 "id_token". The default value is false. +### Other inputs + +The following inputs are for controlling the behavior of this GitHub Actions, +regardless of the authentication mechanism. + +- `project_id`: (Optional) Custom project ID to use for authentication and + exporting into other steps. If unspecified, the project ID will be extracted + from the Workload Identity Provider or the Service Account Key JSON. + +- `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 in other steps in the workflow. The default is true. + +- `delegates`: (Optional) List of additional service account emails or unique + identities to use for impersonation in the chain. By default there are no + delegates. + ## Outputs +- `project_id`: Provided or extracted value for the Google Cloud project ID. + - `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. @@ -145,10 +180,60 @@ See [Examples](#examples) for more examples. ## Examples -#### Cloud SDK (gcloud) +### Authenticating via Workload Identity Federation + +This example demonstrates authenticating via Workload Identity Federation. For +more information on how to setup and configure Workload Identity Federation, see +[#setup](#setup). + +```yaml +jobs: + job_id: + # ... + + # Add "id-token" with the intended permissions. + permissions: + contents: 'read' + id-token: 'write' + + steps: + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: `google-github-actions/auth@v0.3.1' + with: + workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider' + service_account: 'my-service-account@my-project.iam.gserviceaccount.com' +``` + +### Authenticating via Service Account Key JSON + +This example demonstrates authenticating via a Google Cloud Service Account Key +JSON. **We recommend using Workload Identity Federation instead as exporting a +long-lived Service Account Key JSON credential poses a security risk.** + +This example assumes you have created a GitHub Secret named 'GOOGLE_CREDENTIALS' +with the contents being an export Google Cloud Service Account Key JSON. See +[Creating and managing Google Cloud Service Account +Keys](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) +for more information. + +```yaml +jobs: + job_id: + # ... + + steps: + - id: 'auth' + name: 'Authenticate to Google Cloud' + uses: `google-github-actions/auth@v0.3.1' + with: + credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}' +``` + +### Configuring 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. +for the `gcloud` CLI tool. Note this does **NOT** work for the `gsutil` tool. ```yaml jobs: @@ -171,7 +256,6 @@ jobs: name: 'Authenticate to Google Cloud' uses: 'google-github-actions/auth@v0.3.1' with: - create_credentials_file: 'true' workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider' service_account: 'my-service-account@my-project.iam.gserviceaccount.com' @@ -187,7 +271,7 @@ jobs: gcloud secrets versions access "latest" --secret "my-secret" ``` -#### Access Token (OAuth 2.0) +### Generating an OAuth 2.0 Access Token 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 @@ -196,6 +280,9 @@ 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). +Note: If you authenticate via `credentials_json`, the service account must have +`roles/iam.serviceAccountTokenCreator` on itself. + ```yaml jobs: job_id: @@ -225,12 +312,15 @@ jobs: --header "Authorization: Bearer ${{ steps.auth.outputs.access_token }}" ``` -#### ID Token (JWT) +### Generating an 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. +Note: If you authenticate via `credentials_json`, the service account must have +`roles/iam.serviceAccountTokenCreator` on itself. + ```yaml jobs: job_id: @@ -262,7 +352,9 @@ jobs: ``` -## Setup + + +## Setting up Workload Identity Federation To exchange a GitHub Actions OIDC token for a Google Cloud access token, you must create and configure a Workload Identity Provider. These instructions use diff --git a/action.yml b/action.yml index 389f02f..67c3f2e 100644 --- a/action.yml +++ b/action.yml @@ -15,42 +15,48 @@ name: 'Authenticate to Google Cloud' author: 'Google LLC' description: |- - Generate credentials to authenticate to Google Cloud from GitHub Actions using - an OIDC token and Workload Identity Federation. + Authenticate to Google Cloud from GitHub Actions via Workload Identity + Federation and GitHub Actions OIDC tokens or via exported Google Cloud service + account keys. inputs: + project_id: + description: |- + ID of the default project to use for future API calls and invocations. If + unspecified, this action will attempt to extract the value from other + inputs such as "service_account" or "credentials_json". + required: false workload_identity_provider: description: |- The full identifier of the Workload Identity Provider, including the - project number, pool name, and provider name. This must be the full - identifier which includes all parts, for example: + project number, pool name, and provider name. If provided, this must be + the full identifier which includes all parts, for example: "projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider". - required: true + This is mutually exclusive with "credentials_json". + required: false service_account: description: |- Email address or unique identifier of the Google Cloud service account for - which to generate credentials. - required: true + which to generate credentials. This is required if + "workload_identity_provider" is specified. + required: false audience: description: |- The value for the audience (aud) parameter in GitHub's generated OIDC - token. This value defaults to the value of workload_identity_provider, + token. This value defaults to the value of "workload_identity_provider", which is also the default value Google Cloud expects for the audience parameter on the token. default: '' required: false + credentials_json: + description: |- + The Google Cloud JSON service account key to use for authentication. This + is mutually exclusive with "workload_identity_provider". + 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: @@ -99,6 +105,9 @@ inputs: required: false outputs: + project_id: + description: |- + Provided or extracted value for the Google Cloud project ID. credentials_file_path: description: |- Path on the local filesystem where the generated credentials file resides. diff --git a/dist/index.js b/dist/index.js index 9734422..9a533ea 100644 --- a/dist/index.js +++ b/dist/index.js @@ -194,7 +194,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }; Object.defineProperty(exports, "__esModule", { value: true }); const core = __importStar(__webpack_require__(470)); -const workload_identity_1 = __webpack_require__(313); +const workload_identity_client_1 = __webpack_require__(911); +const credentials_json_client_1 = __webpack_require__(627); +const base_1 = __webpack_require__(843); const utils_1 = __webpack_require__(163); /** * Executes the main action, documented inline. @@ -203,21 +205,44 @@ function run() { return __awaiter(this, void 0, void 0, function* () { try { // Load configuration. - const workloadIdentityProvider = core.getInput('workload_identity_provider', { - required: true, - }); - const serviceAccount = core.getInput('service_account', { required: true }); - // audience will default to the WIF provider ID when used with WIF - const audience = core.getInput('audience'); + const projectID = core.getInput('project_id'); + const workloadIdentityProvider = core.getInput('workload_identity_provider'); + const serviceAccount = core.getInput('service_account'); + const audience = core.getInput('audience') || `https://iam.googleapis.com/${workloadIdentityProvider}`; + const credentialsJSON = core.getInput('credentials_json'); const createCredentialsFile = core.getBooleanInput('create_credentials_file'); - const activateCredentialsFile = core.getBooleanInput('activate_credentials_file'); const tokenFormat = core.getInput('token_format'); const delegates = (0, utils_1.explodeStrings)(core.getInput('delegates')); - const client = new workload_identity_1.WIFClient({ - providerID: workloadIdentityProvider, - serviceAccount: serviceAccount, - audience: audience, - }); + // Ensure exactly one of workload_identity_provider and credentials_json was + // provided. + if ((!workloadIdentityProvider && !credentialsJSON) || + (workloadIdentityProvider && credentialsJSON)) { + throw new Error('The GitHub Action workflow must specify exactly one of ' + + '"workload_identity_provider" or "credentials_json"!'); + } + // Ensure a service_account was provided if using WIF. + if (workloadIdentityProvider && !serviceAccount) { + throw new Error('The GitHub Action workflow must specify a "service_account" to ' + + 'impersonate when using "workload_identity_provider"!'); + } + // Instantiate the correct client based on the provided input parameters. + let client; + if (workloadIdentityProvider) { + const token = yield core.getIDToken(audience); + client = new workload_identity_client_1.WorkloadIdentityClient({ + projectID: projectID, + providerID: workloadIdentityProvider, + serviceAccount: serviceAccount, + token: token, + audience: audience, + }); + } + else { + client = new credentials_json_client_1.CredentialsJSONClient({ + projectID: projectID, + credentialsJSON: credentialsJSON, + }); + } // 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 @@ -227,16 +252,19 @@ function run() { if (!runnerTempDir) { throw new Error('$RUNNER_TEMP is not set'); } - const { credentialsPath, envVars } = yield client.createCredentialsFile(runnerTempDir); + const credentialsPath = yield client.createCredentialsFile(runnerTempDir); core.setOutput('credentials_file_path', credentialsPath); - // Also set the magic environment variable for gcloud and SDKs if - // requested. - if (activateCredentialsFile && envVars) { - for (const [k, v] of envVars) { - core.exportVariable(k, v); - } - } + core.exportVariable('CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE', credentialsPath); + core.exportVariable('GOOGLE_APPLICATION_CREDENTIALS', credentialsPath); } + // Set the project ID environment variables to the computed values. + const computedProjectID = yield client.getProjectID(); + core.setOutput('project_id', computedProjectID); + core.exportVariable('CLOUDSDK_PROJECT', computedProjectID); + core.exportVariable('CLOUDSDK_CORE_PROJECT', computedProjectID); + core.exportVariable('GCP_PROJECT', computedProjectID); + core.exportVariable('GCLOUD_PROJECT', computedProjectID); + core.exportVariable('GOOGLE_CLOUD_PROJECT', computedProjectID); switch (tokenFormat) { case '': { break; @@ -247,7 +275,9 @@ function run() { case 'access_token': { const accessTokenLifetime = core.getInput('access_token_lifetime'); const accessTokenScopes = (0, utils_1.explodeStrings)(core.getInput('access_token_scopes')); - const { accessToken, expiration } = yield client.getAccessToken({ + const serviceAccount = yield client.getServiceAccount(); + const authToken = yield client.getAuthToken(); + const { accessToken, expiration } = yield base_1.BaseClient.googleAccessToken(authToken, { serviceAccount, delegates, scopes: accessTokenScopes, @@ -261,7 +291,9 @@ function run() { case 'id_token': { const idTokenAudience = core.getInput('id_token_audience', { required: true }); const idTokenIncludeEmail = core.getBooleanInput('id_token_include_email'); - const { token } = yield client.getIDToken({ + const serviceAccount = yield client.getServiceAccount(); + const authToken = yield client.getAuthToken(); + const { token } = yield base_1.BaseClient.googleIDToken(authToken, { serviceAccount, audience: idTokenAudience, delegates, @@ -272,7 +304,7 @@ function run() { break; } default: { - throw new Error(`unknown token format "${tokenFormat}"`); + throw new Error(`Unknown token format "${tokenFormat}"`); } } } @@ -576,19 +608,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.explodeStrings = exports.writeCredFile = void 0; +exports.fromBase64 = exports.toBase64 = exports.explodeStrings = exports.writeSecureFile = void 0; const fs_1 = __webpack_require__(747); const crypto_1 = __importDefault(__webpack_require__(417)); const path_1 = __importDefault(__webpack_require__(622)); /** - * writeCredFile writes a file to disk in a given directory with a + * writeSecureFile writes a file to disk in a given directory with a * random name. * * @param outputDir Directory to create random file in. * @param data Data to write to file. * @returns Path to written file. */ -function writeCredFile(outputDir, data) { +function writeSecureFile(outputDir, data) { return __awaiter(this, void 0, void 0, function* () { // 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 @@ -602,7 +634,7 @@ function writeCredFile(outputDir, data) { return pth; }); } -exports.writeCredFile = writeCredFile; +exports.writeSecureFile = writeSecureFile; /** * Converts a multi-line or comma-separated collection of strings into an array * of trimmed strings. @@ -623,6 +655,28 @@ function explodeStrings(input) { return list; } exports.explodeStrings = explodeStrings; +/** + * toBase64 base64 URL encodes the result. + */ +function toBase64(s) { + return Buffer.from(s) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); +} +exports.toBase64 = toBase64; +/** + * fromBase64 base64 decodes the result, taking into account URL and standard + * encoding with and without padding. + */ +function fromBase64(s) { + const str = s.replace(/-/g, '+').replace(/_/g, '/'); + while (str.length % 4) + s += '='; + return Buffer.from(str, 'base64').toString('utf8'); +} +exports.fromBase64 = fromBase64; /***/ }), @@ -698,174 +752,6 @@ class PersonalAccessTokenCredentialHandler { exports.PersonalAccessTokenCredentialHandler = PersonalAccessTokenCredentialHandler; -/***/ }), - -/***/ 313: -/***/ (function(__unusedmodule, exports, __webpack_require__) { - -"use strict"; - -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.WIFClient = void 0; -const url_1 = __webpack_require__(835); -const core = __importStar(__webpack_require__(470)); -const utils_1 = __webpack_require__(163); -const base_1 = __webpack_require__(843); -class WIFClient { - constructor(opts) { - this.providerID = opts.providerID; - this.serviceAccount = opts.serviceAccount; - this.audience = opts.audience ? opts.audience : `https://iam.googleapis.com/${this.providerID}`; - } - /** - * googleFederatedToken generates a Google Cloud federated token using the - * provided OIDC token and Workload Identity Provider. - */ - static googleFederatedToken({ providerID, token, }) { - return __awaiter(this, void 0, void 0, function* () { - const stsURL = new url_1.URL('https://sts.googleapis.com/v1/token'); - const data = { - audience: '//iam.googleapis.com/' + providerID, - grantType: 'urn:ietf:params:oauth:grant-type:token-exchange', - requestedTokenType: 'urn:ietf:params:oauth:token-type:access_token', - scope: 'https://www.googleapis.com/auth/cloud-platform', - subjectTokenType: 'urn:ietf:params:oauth:token-type:jwt', - subjectToken: token, - }; - const opts = { - hostname: stsURL.hostname, - port: stsURL.port, - path: stsURL.pathname + stsURL.search, - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - }; - try { - const resp = yield base_1.BaseClient.request(opts, JSON.stringify(data)); - const parsed = JSON.parse(resp); - return parsed['access_token']; - } - catch (err) { - throw new Error(`failed to generate Google Cloud federated token for ${providerID}: ${err}`); - } - }); - } - /** - * getFederatedToken generates a Google Cloud federated token using the - * GitHub OIDC token. - */ - getFederatedToken() { - return __awaiter(this, void 0, void 0, function* () { - // Get the GitHub OIDC token. - const githubOIDCToken = yield core.getIDToken(this.audience); - // Exchange the GitHub OIDC token for a Google Federated Token. - const googleFederatedToken = yield WIFClient.googleFederatedToken({ - providerID: this.providerID, - token: githubOIDCToken, - }); - core.setSecret(googleFederatedToken); - return googleFederatedToken; - }); - } - /** - * getAccessToken generates a Google Cloud access token for the provided - * service account email or unique id. - */ - getAccessToken(opts) { - return __awaiter(this, void 0, void 0, function* () { - const googleFederatedToken = yield this.getFederatedToken(); - return yield base_1.BaseClient.googleAccessToken(googleFederatedToken, opts); - }); - } - /** - * getIDToken generates a Google Cloud ID token for the provided - * service account email or unique id. - */ - getIDToken(tokenParams) { - return __awaiter(this, void 0, void 0, function* () { - const googleFederatedToken = yield this.getFederatedToken(); - return yield base_1.BaseClient.googleIDToken(googleFederatedToken, tokenParams); - }); - } - /** - * createCredentialsFile creates a Google Cloud credentials file that can be - * set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries. - */ - createCredentialsFile(outputDir) { - return __awaiter(this, void 0, void 0, function* () { - // 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; - const requestURLRaw = process.env.ACTIONS_ID_TOKEN_REQUEST_URL; - if (!requestToken || !requestURLRaw) { - throw new Error('GitHub Actions did not inject $ACTIONS_ID_TOKEN_REQUEST_TOKEN or ' + - '$ACTIONS_ID_TOKEN_REQUEST_URL into this job. This most likely ' + - 'means the GitHub Actions workflow permissions are incorrect, or ' + - 'this job is being run from a fork. For more information, please ' + - 'see the GitHub documentation at https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token'); - } - const requestURL = new url_1.URL(requestURLRaw); - // Append the audience value to the request. - const params = requestURL.searchParams; - params.set('audience', this.audience); - requestURL.search = params.toString(); - const data = { - type: 'external_account', - audience: `//iam.googleapis.com/${this.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/${this.serviceAccount}:generateAccessToken`, - credential_source: { - url: requestURL, - headers: { - Authorization: `Bearer ${requestToken}`, - }, - format: { - type: 'json', - subject_token_field_name: 'value', - }, - }, - }; - const credentialsPath = yield (0, utils_1.writeCredFile)(outputDir, JSON.stringify(data)); - const envVars = new Map([['GOOGLE_APPLICATION_CREDENTIALS', credentialsPath]]); - return { credentialsPath, envVars }; - }); - } -} -exports.WIFClient = WIFClient; - - /***/ }), /***/ 357: @@ -1872,6 +1758,130 @@ module.exports = require("events"); module.exports = require("path"); +/***/ }), + +/***/ 627: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; +}; +var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +}; +var _CredentialsJSONClient_projectID, _CredentialsJSONClient_credentials; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CredentialsJSONClient = void 0; +const crypto_1 = __webpack_require__(417); +const utils_1 = __webpack_require__(163); +class CredentialsJSONClient { + constructor(opts) { + _CredentialsJSONClient_projectID.set(this, void 0); + _CredentialsJSONClient_credentials.set(this, void 0); + __classPrivateFieldSet(this, _CredentialsJSONClient_credentials, this.parseServiceAccountKeyJSON(opts.credentialsJSON), "f"); + __classPrivateFieldSet(this, _CredentialsJSONClient_projectID, opts.projectID || __classPrivateFieldGet(this, _CredentialsJSONClient_credentials, "f")['project_id'], "f"); + } + /** + * parseServiceAccountKeyJSON attempts to parse the given string as a service + * account key JSON. It handles if the string is base64-encoded. + */ + parseServiceAccountKeyJSON(str) { + if (!str) { + return {}; + } + str = str.trim(); + // If the string doesn't start with a JSON object character, it is probably + // base64-encoded. + if (!str.startsWith('{')) { + str = (0, utils_1.fromBase64)(str); + } + try { + return JSON.parse(str); + } + catch (e) { + throw new SyntaxError(`Failed to parse credentials as JSON: ${e}`); + } + } + /** + * getAuthToken generates a token capable of calling the iamcredentials API. + */ + getAuthToken() { + return __awaiter(this, void 0, void 0, function* () { + const header = { + alg: 'RS256', + typ: 'JWT', + kid: __classPrivateFieldGet(this, _CredentialsJSONClient_credentials, "f")['private_key_id'], + }; + const now = Math.floor(new Date().getTime() / 1000); + const body = { + iss: __classPrivateFieldGet(this, _CredentialsJSONClient_credentials, "f")['client_email'], + sub: __classPrivateFieldGet(this, _CredentialsJSONClient_credentials, "f")['client_email'], + aud: 'https://iamcredentials.googleapis.com/', + iat: now, + exp: now + 3599, + }; + const message = (0, utils_1.toBase64)(JSON.stringify(header)) + '.' + (0, utils_1.toBase64)(JSON.stringify(body)); + try { + const signer = (0, crypto_1.createSign)('RSA-SHA256'); + signer.write(message); + signer.end(); + const signature = signer.sign(__classPrivateFieldGet(this, _CredentialsJSONClient_credentials, "f")['private_key']); + return message + '.' + (0, utils_1.toBase64)(signature); + } + catch (e) { + throw new Error(`Failed to sign auth token: ${e}`); + } + }); + } + /** + * getProjectID returns the project ID. If an override was given, the override + * is returned. Otherwise, this will be the project ID that was extracted from + * the service account key JSON. + */ + getProjectID() { + return __awaiter(this, void 0, void 0, function* () { + return __classPrivateFieldGet(this, _CredentialsJSONClient_projectID, "f"); + }); + } + /** + * getServiceAccount returns the service account email for the authentication, + * extracted from the Service Account Key JSON. + */ + getServiceAccount() { + return __awaiter(this, void 0, void 0, function* () { + return __classPrivateFieldGet(this, _CredentialsJSONClient_credentials, "f")['client_email']; + }); + } + /** + * createCredentialsFile creates a Google Cloud credentials file that can be + * set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries. + */ + createCredentialsFile(outputDir) { + return __awaiter(this, void 0, void 0, function* () { + return yield (0, utils_1.writeSecureFile)(outputDir, JSON.stringify(__classPrivateFieldGet(this, _CredentialsJSONClient_credentials, "f"))); + }); + } +} +exports.CredentialsJSONClient = CredentialsJSONClient; +_CredentialsJSONClient_projectID = new WeakMap(), _CredentialsJSONClient_credentials = new WeakMap(); + + /***/ }), /***/ 631: @@ -2113,8 +2123,8 @@ class BaseClient { expiration: parsed['expireTime'], }; } - catch (err) { - throw new Error(`failed to generate Google Cloud access token for ${serviceAccount}: ${err}`); + catch (e) { + throw new Error(`Failed to generate Google Cloud access token for ${serviceAccount}: ${e}`); } }); } @@ -2122,6 +2132,171 @@ class BaseClient { exports.BaseClient = BaseClient; +/***/ }), + +/***/ 911: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { + if (kind === "m") throw new TypeError("Private method is not writable"); + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; +}; +var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { + if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); + if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); +}; +var _WorkloadIdentityClient_projectID, _WorkloadIdentityClient_providerID, _WorkloadIdentityClient_serviceAccount, _WorkloadIdentityClient_token, _WorkloadIdentityClient_audience; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.WorkloadIdentityClient = void 0; +const url_1 = __webpack_require__(835); +const utils_1 = __webpack_require__(163); +const base_1 = __webpack_require__(843); +class WorkloadIdentityClient { + constructor(opts) { + _WorkloadIdentityClient_projectID.set(this, void 0); + _WorkloadIdentityClient_providerID.set(this, void 0); + _WorkloadIdentityClient_serviceAccount.set(this, void 0); + _WorkloadIdentityClient_token.set(this, void 0); + _WorkloadIdentityClient_audience.set(this, void 0); + __classPrivateFieldSet(this, _WorkloadIdentityClient_providerID, opts.providerID, "f"); + __classPrivateFieldSet(this, _WorkloadIdentityClient_serviceAccount, opts.serviceAccount, "f"); + __classPrivateFieldSet(this, _WorkloadIdentityClient_token, opts.token, "f"); + __classPrivateFieldSet(this, _WorkloadIdentityClient_audience, opts.audience, "f"); + __classPrivateFieldSet(this, _WorkloadIdentityClient_projectID, opts.projectID || this.extractProjectIDFromServiceAccountEmail(__classPrivateFieldGet(this, _WorkloadIdentityClient_serviceAccount, "f")), "f"); + } + /** + * extractProjectIDFromServiceAccountEmail extracts the project ID from the + * service account email address. + */ + extractProjectIDFromServiceAccountEmail(str) { + if (!str) { + return ''; + } + const [, dn] = str.split('@', 2); + if (!str.endsWith('.iam.gserviceaccount.com')) { + throw new Error(`Service account email ${str} is not of the form ` + + `"[name]@[project].iam.gserviceaccount.com. You must manually ` + + `specify the "project_id" parameter in your GitHub Actions workflow.`); + } + const [project] = dn.split('.', 2); + return project; + } + /** + * getAuthToken generates a Google Cloud federated token using the provided OIDC + * token and Workload Identity Provider. + */ + getAuthToken() { + return __awaiter(this, void 0, void 0, function* () { + const stsURL = new url_1.URL('https://sts.googleapis.com/v1/token'); + const data = { + audience: '//iam.googleapis.com/' + __classPrivateFieldGet(this, _WorkloadIdentityClient_providerID, "f"), + grantType: 'urn:ietf:params:oauth:grant-type:token-exchange', + requestedTokenType: 'urn:ietf:params:oauth:token-type:access_token', + scope: 'https://www.googleapis.com/auth/cloud-platform', + subjectTokenType: 'urn:ietf:params:oauth:token-type:jwt', + subjectToken: __classPrivateFieldGet(this, _WorkloadIdentityClient_token, "f"), + }; + const opts = { + hostname: stsURL.hostname, + port: stsURL.port, + path: stsURL.pathname + stsURL.search, + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + }; + try { + const resp = yield base_1.BaseClient.request(opts, JSON.stringify(data)); + const parsed = JSON.parse(resp); + return parsed['access_token']; + } + catch (err) { + throw new Error(`Failed to generate Google Cloud federated token for ${__classPrivateFieldGet(this, _WorkloadIdentityClient_providerID, "f")}: ${err}`); + } + }); + } + /** + * getProjectID returns the project ID. If an override was given, the override + * is returned. Otherwise, this will be the project ID that was extracted from + * the service account key JSON. + */ + getProjectID() { + return __awaiter(this, void 0, void 0, function* () { + return __classPrivateFieldGet(this, _WorkloadIdentityClient_projectID, "f"); + }); + } + /** + * getServiceAccount returns the service account email for the authentication, + * extracted from the input parameter. + */ + getServiceAccount() { + return __awaiter(this, void 0, void 0, function* () { + return __classPrivateFieldGet(this, _WorkloadIdentityClient_serviceAccount, "f"); + }); + } + /** + * createCredentialsFile creates a Google Cloud credentials file that can be + * set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries. + */ + createCredentialsFile(outputDir) { + return __awaiter(this, void 0, void 0, function* () { + // 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; + const requestURLRaw = process.env.ACTIONS_ID_TOKEN_REQUEST_URL; + if (!requestToken || !requestURLRaw) { + throw new Error('GitHub Actions did not inject $ACTIONS_ID_TOKEN_REQUEST_TOKEN or ' + + '$ACTIONS_ID_TOKEN_REQUEST_URL into this job. This most likely ' + + 'means the GitHub Actions workflow permissions are incorrect, or ' + + 'this job is being run from a fork. For more information, please ' + + 'see the GitHub documentation at https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token'); + } + const requestURL = new url_1.URL(requestURLRaw); + // Append the audience value to the request. + const params = requestURL.searchParams; + params.set('audience', __classPrivateFieldGet(this, _WorkloadIdentityClient_audience, "f")); + requestURL.search = params.toString(); + const data = { + type: 'external_account', + audience: `//iam.googleapis.com/${__classPrivateFieldGet(this, _WorkloadIdentityClient_providerID, "f")}`, + 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/${__classPrivateFieldGet(this, _WorkloadIdentityClient_serviceAccount, "f")}:generateAccessToken`, + credential_source: { + url: requestURL, + headers: { + Authorization: `Bearer ${requestToken}`, + }, + format: { + type: 'json', + subject_token_field_name: 'value', + }, + }, + }; + return yield (0, utils_1.writeSecureFile)(outputDir, JSON.stringify(data)); + }); + } +} +exports.WorkloadIdentityClient = WorkloadIdentityClient; +_WorkloadIdentityClient_projectID = new WeakMap(), _WorkloadIdentityClient_providerID = new WeakMap(), _WorkloadIdentityClient_serviceAccount = new WeakMap(), _WorkloadIdentityClient_token = new WeakMap(), _WorkloadIdentityClient_audience = new WeakMap(); + + /***/ }), /***/ 950: diff --git a/package-lock.json b/package-lock.json index 660edba..45b4f05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,12 +64,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", + "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.14.5", + "@babel/helper-validator-identifier": "^7.15.7", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -158,9 +158,9 @@ } }, "node_modules/@cspotcode/source-map-support": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", - "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "dev": true, "dependencies": { "@cspotcode/source-map-consumer": "0.8.0" @@ -213,9 +213,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", - "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, "node_modules/@nodelib/fs.scandir": { @@ -296,19 +296,19 @@ "dev": true }, "node_modules/@types/node": { - "version": "16.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.1.tgz", - "integrity": "sha512-4/Z9DMPKFexZj/Gn3LylFgamNKHm4K3QDi0gz9B26Uk0c8izYf97B5fxfpspMNkWlFupblKM/nV8+NA9Ffvr+w==", + "version": "16.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz", + "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.32.0.tgz", - "integrity": "sha512-+OWTuWRSbWI1KDK8iEyG/6uK2rTm3kpS38wuVifGUTDB6kjEuNrzBI1MUtxnkneuWG/23QehABe2zHHrj+4yuA==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", + "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", "dev": true, "dependencies": { - "@typescript-eslint/experimental-utils": "4.32.0", - "@typescript-eslint/scope-manager": "4.32.0", + "@typescript-eslint/experimental-utils": "4.33.0", + "@typescript-eslint/scope-manager": "4.33.0", "debug": "^4.3.1", "functional-red-black-tree": "^1.0.1", "ignore": "^5.1.8", @@ -334,15 +334,15 @@ } }, "node_modules/@typescript-eslint/experimental-utils": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.32.0.tgz", - "integrity": "sha512-WLoXcc+cQufxRYjTWr4kFt0DyEv6hDgSaFqYhIzQZ05cF+kXfqXdUh+//kgquPJVUBbL3oQGKQxwPbLxHRqm6A==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", + "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.7", - "@typescript-eslint/scope-manager": "4.32.0", - "@typescript-eslint/types": "4.32.0", - "@typescript-eslint/typescript-estree": "4.32.0", + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" }, @@ -357,15 +357,33 @@ "eslint": "*" } }, - "node_modules/@typescript-eslint/parser": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.32.0.tgz", - "integrity": "sha512-lhtYqQ2iEPV5JqV7K+uOVlPePjClj4dOw7K4/Z1F2yvjIUvyr13yJnDzkK6uon4BjHYuHy3EG0c2Z9jEhFk56w==", + "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "4.32.0", - "@typescript-eslint/types": "4.32.0", - "@typescript-eslint/typescript-estree": "4.32.0", + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", + "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", "debug": "^4.3.1" }, "engines": { @@ -385,13 +403,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.32.0.tgz", - "integrity": "sha512-DK+fMSHdM216C0OM/KR1lHXjP1CNtVIhJ54kQxfOE6x8UGFAjha8cXgDMBEIYS2XCYjjCtvTkjQYwL3uvGOo0w==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", + "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "4.32.0", - "@typescript-eslint/visitor-keys": "4.32.0" + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0" }, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" @@ -402,9 +420,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.32.0.tgz", - "integrity": "sha512-LE7Z7BAv0E2UvqzogssGf1x7GPpUalgG07nGCBYb1oK4mFsOiFC/VrSMKbZQzFJdN2JL5XYmsx7C7FX9p9ns0w==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", + "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", "dev": true, "engines": { "node": "^8.10.0 || ^10.13.0 || >=11.10.1" @@ -415,13 +433,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.32.0.tgz", - "integrity": "sha512-tRYCgJ3g1UjMw1cGG8Yn1KzOzNlQ6u1h9AmEtPhb5V5a1TmiHWcRyF/Ic+91M4f43QeChyYlVTcf3DvDTZR9vw==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", + "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "4.32.0", - "@typescript-eslint/visitor-keys": "4.32.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0", "debug": "^4.3.1", "globby": "^11.0.3", "is-glob": "^4.0.1", @@ -442,12 +460,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.32.0.tgz", - "integrity": "sha512-e7NE0qz8W+atzv3Cy9qaQ7BTLwWsm084Z0c4nIO2l3Bp6u9WIgdqCgyPyV5oSPDMIW3b20H59OOCmVk3jw3Ptw==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", + "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "4.32.0", + "@typescript-eslint/types": "4.33.0", "eslint-visitor-keys": "^2.0.0" }, "engines": { @@ -1013,33 +1031,6 @@ } }, "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint/node_modules/eslint-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", @@ -1054,7 +1045,7 @@ "url": "https://github.com/sponsors/mysticatea" } }, - "node_modules/eslint/node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", @@ -1063,6 +1054,15 @@ "node": ">=4" } }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint/node_modules/ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -1121,9 +1121,9 @@ } }, "node_modules/esquery/node_modules/estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "engines": { "node": ">=4.0" @@ -1142,9 +1142,9 @@ } }, "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "engines": { "node": ">=4.0" @@ -1362,9 +1362,9 @@ } }, "node_modules/globals": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", - "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -1424,9 +1424,9 @@ } }, "node_modules/husky": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.2.tgz", - "integrity": "sha512-8yKEWNX4z2YsofXAMT7KvA1g8p+GxtB1ffV8XtpAEGuXNAbCV5wdNKH+qTpw8SM9fh4aMPDR+yQuKfgnreyZlg==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz", + "integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==", "dev": true, "bin": { "husky": "lib/bin.js" @@ -1439,9 +1439,9 @@ } }, "node_modules/ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", "dev": true, "engines": { "node": ">= 4" @@ -1519,9 +1519,9 @@ } }, "node_modules/is-glob": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.2.tgz", - "integrity": "sha512-ZZTOjRcDjuAAAv2cTBQP/lL59ZTArx77+7UzHdWW/XB1mrfp7DEaVpKmZ0XIzx+M7AxfhKcqV+nMetUQmFifwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "dependencies": { "is-extglob": "^2.1.1" @@ -1625,12 +1625,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -1712,9 +1706,9 @@ } }, "node_modules/mocha": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.2.tgz", - "integrity": "sha512-ta3LtJ+63RIBP03VBjMGtSqbe6cWXRejF9SyM9Zyli1CKZJZ+vfCTj3oW24V7wAphMJdpOFLoMI3hjJ1LWbs0w==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", + "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", "dev": true, "dependencies": { "@ungap/promise-all-settled": "1.1.2", @@ -2278,17 +2272,16 @@ } }, "node_modules/table": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", - "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.3.tgz", + "integrity": "sha512-5DkIxeA7XERBqMwJq0aHZOdMadBx4e6eDoFRuyT5VR82J0Ycg2DwM6GfA/EQAhJ+toRTaS1lIdSQCqgrmhPnlw==", "dev": true, "dependencies": { "ajv": "^8.0.1", - "lodash.clonedeep": "^4.5.0", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0" + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" }, "engines": { "node": ">=10.0.0" @@ -2335,12 +2328,12 @@ } }, "node_modules/ts-node": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz", - "integrity": "sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz", + "integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==", "dev": true, "dependencies": { - "@cspotcode/source-map-support": "0.6.1", + "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", @@ -2360,9 +2353,6 @@ "ts-node-transpile-only": "dist/bin-transpile.js", "ts-script": "dist/bin-script-deprecated.js" }, - "engines": { - "node": ">=12.0.0" - }, "peerDependencies": { "@swc/core": ">=1.2.50", "@swc/wasm": ">=1.2.50", @@ -2462,9 +2452,9 @@ } }, "node_modules/typescript": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", - "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", + "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -2654,12 +2644,12 @@ "dev": true }, "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", + "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.14.5", + "@babel/helper-validator-identifier": "^7.15.7", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -2729,9 +2719,9 @@ "dev": true }, "@cspotcode/source-map-support": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz", - "integrity": "sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", + "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "dev": true, "requires": { "@cspotcode/source-map-consumer": "0.8.0" @@ -2774,9 +2764,9 @@ } }, "@humanwhocodes/object-schema": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", - "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, "@nodelib/fs.scandir": { @@ -2848,19 +2838,19 @@ "dev": true }, "@types/node": { - "version": "16.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.1.tgz", - "integrity": "sha512-4/Z9DMPKFexZj/Gn3LylFgamNKHm4K3QDi0gz9B26Uk0c8izYf97B5fxfpspMNkWlFupblKM/nV8+NA9Ffvr+w==", + "version": "16.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz", + "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==", "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.32.0.tgz", - "integrity": "sha512-+OWTuWRSbWI1KDK8iEyG/6uK2rTm3kpS38wuVifGUTDB6kjEuNrzBI1MUtxnkneuWG/23QehABe2zHHrj+4yuA==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", + "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.32.0", - "@typescript-eslint/scope-manager": "4.32.0", + "@typescript-eslint/experimental-utils": "4.33.0", + "@typescript-eslint/scope-manager": "4.33.0", "debug": "^4.3.1", "functional-red-black-tree": "^1.0.1", "ignore": "^5.1.8", @@ -2870,55 +2860,66 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.32.0.tgz", - "integrity": "sha512-WLoXcc+cQufxRYjTWr4kFt0DyEv6hDgSaFqYhIzQZ05cF+kXfqXdUh+//kgquPJVUBbL3oQGKQxwPbLxHRqm6A==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", + "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", "dev": true, "requires": { "@types/json-schema": "^7.0.7", - "@typescript-eslint/scope-manager": "4.32.0", - "@typescript-eslint/types": "4.32.0", - "@typescript-eslint/typescript-estree": "4.32.0", + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + } + } } }, "@typescript-eslint/parser": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.32.0.tgz", - "integrity": "sha512-lhtYqQ2iEPV5JqV7K+uOVlPePjClj4dOw7K4/Z1F2yvjIUvyr13yJnDzkK6uon4BjHYuHy3EG0c2Z9jEhFk56w==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", + "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.32.0", - "@typescript-eslint/types": "4.32.0", - "@typescript-eslint/typescript-estree": "4.32.0", + "@typescript-eslint/scope-manager": "4.33.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/typescript-estree": "4.33.0", "debug": "^4.3.1" } }, "@typescript-eslint/scope-manager": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.32.0.tgz", - "integrity": "sha512-DK+fMSHdM216C0OM/KR1lHXjP1CNtVIhJ54kQxfOE6x8UGFAjha8cXgDMBEIYS2XCYjjCtvTkjQYwL3uvGOo0w==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", + "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", "dev": true, "requires": { - "@typescript-eslint/types": "4.32.0", - "@typescript-eslint/visitor-keys": "4.32.0" + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0" } }, "@typescript-eslint/types": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.32.0.tgz", - "integrity": "sha512-LE7Z7BAv0E2UvqzogssGf1x7GPpUalgG07nGCBYb1oK4mFsOiFC/VrSMKbZQzFJdN2JL5XYmsx7C7FX9p9ns0w==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", + "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.32.0.tgz", - "integrity": "sha512-tRYCgJ3g1UjMw1cGG8Yn1KzOzNlQ6u1h9AmEtPhb5V5a1TmiHWcRyF/Ic+91M4f43QeChyYlVTcf3DvDTZR9vw==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", + "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", "dev": true, "requires": { - "@typescript-eslint/types": "4.32.0", - "@typescript-eslint/visitor-keys": "4.32.0", + "@typescript-eslint/types": "4.33.0", + "@typescript-eslint/visitor-keys": "4.33.0", "debug": "^4.3.1", "globby": "^11.0.3", "is-glob": "^4.0.1", @@ -2927,12 +2928,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.32.0.tgz", - "integrity": "sha512-e7NE0qz8W+atzv3Cy9qaQ7BTLwWsm084Z0c4nIO2l3Bp6u9WIgdqCgyPyV5oSPDMIW3b20H59OOCmVk3jw3Ptw==", + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", + "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.32.0", + "@typescript-eslint/types": "4.33.0", "eslint-visitor-keys": "^2.0.0" } }, @@ -3316,23 +3317,6 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -3368,12 +3352,20 @@ } }, "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, "requires": { - "eslint-visitor-keys": "^2.0.0" + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } } }, "eslint-visitor-keys": { @@ -3417,9 +3409,9 @@ }, "dependencies": { "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true } } @@ -3434,9 +3426,9 @@ }, "dependencies": { "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true } } @@ -3604,9 +3596,9 @@ } }, "globals": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", - "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -3645,15 +3637,15 @@ "dev": true }, "husky": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.2.tgz", - "integrity": "sha512-8yKEWNX4z2YsofXAMT7KvA1g8p+GxtB1ffV8XtpAEGuXNAbCV5wdNKH+qTpw8SM9fh4aMPDR+yQuKfgnreyZlg==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz", + "integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==", "dev": true }, "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", + "integrity": "sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ==", "dev": true }, "import-fresh": { @@ -3710,9 +3702,9 @@ "dev": true }, "is-glob": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.2.tgz", - "integrity": "sha512-ZZTOjRcDjuAAAv2cTBQP/lL59ZTArx77+7UzHdWW/XB1mrfp7DEaVpKmZ0XIzx+M7AxfhKcqV+nMetUQmFifwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "requires": { "is-extglob": "^2.1.1" @@ -3789,12 +3781,6 @@ "p-locate": "^5.0.0" } }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3858,9 +3844,9 @@ } }, "mocha": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.2.tgz", - "integrity": "sha512-ta3LtJ+63RIBP03VBjMGtSqbe6cWXRejF9SyM9Zyli1CKZJZ+vfCTj3oW24V7wAphMJdpOFLoMI3hjJ1LWbs0w==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", + "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", "dev": true, "requires": { "@ungap/promise-all-settled": "1.1.2", @@ -4234,17 +4220,16 @@ } }, "table": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", - "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.3.tgz", + "integrity": "sha512-5DkIxeA7XERBqMwJq0aHZOdMadBx4e6eDoFRuyT5VR82J0Ycg2DwM6GfA/EQAhJ+toRTaS1lIdSQCqgrmhPnlw==", "dev": true, "requires": { "ajv": "^8.0.1", - "lodash.clonedeep": "^4.5.0", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0" + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" }, "dependencies": { "ajv": { @@ -4283,12 +4268,12 @@ } }, "ts-node": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.2.1.tgz", - "integrity": "sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz", + "integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==", "dev": true, "requires": { - "@cspotcode/source-map-support": "0.6.1", + "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", @@ -4358,9 +4343,9 @@ "dev": true }, "typescript": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", - "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", + "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", "dev": true }, "uri-js": { diff --git a/package.json b/package.json index 65fe6e0..99d53aa 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "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'" + "test": "mocha -r ts-node/register -t 120s 'tests/**/*.test.ts'" }, "repository": { "type": "git", diff --git a/src/base.ts b/src/base.ts index 1ed0cc2..f51553d 100644 --- a/src/base.ts +++ b/src/base.ts @@ -5,7 +5,7 @@ import { GoogleAccessTokenResponse, GoogleIDTokenParameters, GoogleIDTokenResponse, -} from './actionauth'; +} from './client/auth_client'; export class BaseClient { /** @@ -132,8 +132,8 @@ export class BaseClient { accessToken: parsed['accessToken'], expiration: parsed['expireTime'], }; - } catch (err) { - throw new Error(`failed to generate Google Cloud access token for ${serviceAccount}: ${err}`); + } catch (e) { + throw new Error(`Failed to generate Google Cloud access token for ${serviceAccount}: ${e}`); } } } diff --git a/src/actionauth.ts b/src/client/auth_client.ts similarity index 74% rename from src/actionauth.ts rename to src/client/auth_client.ts index 128a467..d61095a 100644 --- a/src/actionauth.ts +++ b/src/client/auth_client.ts @@ -1,10 +1,11 @@ /** * Defines the main interface for all clients that generate credentials. */ -export interface ActionAuth { - getAccessToken(opts: GoogleAccessTokenParameters): Promise; - getIDToken(opts: GoogleIDTokenParameters): Promise; - createCredentialsFile(outputDir: string): Promise; +export interface AuthClient { + getAuthToken(): Promise; + getProjectID(): Promise; + getServiceAccount(): Promise; + createCredentialsFile(outputDir: string): Promise; } /** @@ -66,14 +67,3 @@ export interface GoogleIDTokenParameters { export interface GoogleIDTokenResponse { token: string; } - -/** - * CreateCredentialsFileResponse is the response from creating a credential file. - * - * @param credentialsPath Path to the created credentials file. - * @param envVars Optional key value pairs that can be exported as env variables. - */ -export interface CreateCredentialsFileResponse { - credentialsPath: string; - envVars?: Map; -} diff --git a/src/client/credentials_json_client.ts b/src/client/credentials_json_client.ts new file mode 100644 index 0000000..18ee2f9 --- /dev/null +++ b/src/client/credentials_json_client.ts @@ -0,0 +1,128 @@ +'use strict'; + +import { createSign } from 'crypto'; +import { AuthClient } from './auth_client'; +import { toBase64, fromBase64, trimmedString, writeSecureFile } from '../utils'; + +/** + * Available options to create the CredentialsJSONClient. + * + * @param projectID User-supplied value for project ID. If not provided, the + * project ID is extracted from the credentials JSON. + * @param credentialsJSON Raw JSON credentials blob. + */ +interface CredentialsJSONClientOptions { + projectID?: string; + credentialsJSON: string; +} + +/** + * CredentialsJSONClient is a client that accepts a service account key JSON + * credential. + */ +export class CredentialsJSONClient implements AuthClient { + readonly #projectID: string; + readonly #credentials: Record; + + constructor(opts: CredentialsJSONClientOptions) { + this.#credentials = this.parseServiceAccountKeyJSON(opts.credentialsJSON); + this.#projectID = opts.projectID || this.#credentials['project_id']; + } + + /** + * parseServiceAccountKeyJSON attempts to parse the given string as a service + * account key JSON. It handles if the string is base64-encoded. + */ + parseServiceAccountKeyJSON(str: string): Record { + str = trimmedString(str); + if (!str) { + throw new Error(`Missing service account key JSON (got empty value)`); + } + + // If the string doesn't start with a JSON object character, it is probably + // base64-encoded. + if (!str.startsWith('{')) { + str = fromBase64(str); + } + + let creds: Record; + try { + creds = JSON.parse(str); + } catch (e) { + throw new SyntaxError(`Failed to parse credentials as JSON: ${e}`); + } + + const requireValue = (key: string) => { + const val = trimmedString(creds[key]); + if (!val) { + throw new Error(`Service account key JSON is missing required field "${key}"`); + } + }; + + requireValue('project_id'); + requireValue('private_key_id'); + requireValue('private_key'); + requireValue('client_email'); + + return creds; + } + + /** + * getAuthToken generates a token capable of calling the iamcredentials API. + */ + async getAuthToken(): Promise { + const header = { + alg: 'RS256', + typ: 'JWT', + kid: this.#credentials['private_key_id'], + }; + + const now = Math.floor(new Date().getTime() / 1000); + + const body = { + iss: this.#credentials['client_email'], + sub: this.#credentials['client_email'], + aud: 'https://iamcredentials.googleapis.com/', + iat: now, + exp: now + 3599, + }; + + const message = toBase64(JSON.stringify(header)) + '.' + toBase64(JSON.stringify(body)); + + try { + const signer = createSign('RSA-SHA256'); + signer.write(message); + signer.end(); + + const signature = signer.sign(this.#credentials['private_key']); + return message + '.' + toBase64(signature); + } catch (e) { + throw new Error(`Failed to sign auth token: ${e}`); + } + } + + /** + * getProjectID returns the project ID. If an override was given, the override + * is returned. Otherwise, this will be the project ID that was extracted from + * the service account key JSON. + */ + async getProjectID(): Promise { + return this.#projectID; + } + + /** + * getServiceAccount returns the service account email for the authentication, + * extracted from the Service Account Key JSON. + */ + async getServiceAccount(): Promise { + return this.#credentials['client_email']; + } + + /** + * createCredentialsFile creates a Google Cloud credentials file that can be + * set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries. + */ + async createCredentialsFile(outputDir: string): Promise { + return await writeSecureFile(outputDir, JSON.stringify(this.#credentials)); + } +} diff --git a/src/client/workload_identity_client.ts b/src/client/workload_identity_client.ts new file mode 100644 index 0000000..0135055 --- /dev/null +++ b/src/client/workload_identity_client.ts @@ -0,0 +1,175 @@ +'use strict'; + +import { URL } from 'url'; +import { AuthClient } from './auth_client'; +import { writeSecureFile } from '../utils'; +import { BaseClient } from '../base'; + +/** + * Available options to create the WorkloadIdentityClient. + * + * @param projectID User-supplied value for project ID. If not provided, the + * project ID is extracted from the service account email. + * @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 token GitHub OIDC token to use for exchanging with Workload Identity + * Federation. + * @param audience The value for the audience parameter in the generated GitHub + * Actions OIDC token, defaults to the value of workload_identity_provider + */ +interface WorkloadIdentityClientOptions { + projectID?: string; + providerID: string; + serviceAccount: string; + token: string; + audience: string; +} + +/** + * WorkloadIdentityClient is a client that uses the GitHub Actions runtime to + * authentication via Workload Identity. + */ +export class WorkloadIdentityClient implements AuthClient { + readonly #projectID: string; + readonly #providerID: string; + readonly #serviceAccount: string; + readonly #token: string; + readonly #audience: string; + + constructor(opts: WorkloadIdentityClientOptions) { + this.#providerID = opts.providerID; + this.#serviceAccount = opts.serviceAccount; + this.#token = opts.token; + this.#audience = opts.audience; + + this.#projectID = + opts.projectID || this.extractProjectIDFromServiceAccountEmail(this.#serviceAccount); + } + + /** + * extractProjectIDFromServiceAccountEmail extracts the project ID from the + * service account email address. + */ + extractProjectIDFromServiceAccountEmail(str: string): string { + if (!str) { + return ''; + } + + const [, dn] = str.split('@', 2); + if (!str.endsWith('.iam.gserviceaccount.com')) { + throw new Error( + `Service account email ${str} is not of the form ` + + `"[name]@[project].iam.gserviceaccount.com. You must manually ` + + `specify the "project_id" parameter in your GitHub Actions workflow.`, + ); + } + + const [project] = dn.split('.', 2); + return project; + } + + /** + * getAuthToken generates a Google Cloud federated token using the provided OIDC + * token and Workload Identity Provider. + */ + async getAuthToken(): Promise { + const stsURL = new URL('https://sts.googleapis.com/v1/token'); + + const data = { + audience: '//iam.googleapis.com/' + this.#providerID, + grantType: 'urn:ietf:params:oauth:grant-type:token-exchange', + requestedTokenType: 'urn:ietf:params:oauth:token-type:access_token', + scope: 'https://www.googleapis.com/auth/cloud-platform', + subjectTokenType: 'urn:ietf:params:oauth:token-type:jwt', + subjectToken: this.#token, + }; + + const opts = { + hostname: stsURL.hostname, + port: stsURL.port, + path: stsURL.pathname + stsURL.search, + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + }; + + try { + const resp = await BaseClient.request(opts, JSON.stringify(data)); + const parsed = JSON.parse(resp); + return parsed['access_token']; + } catch (err) { + throw new Error( + `Failed to generate Google Cloud federated token for ${this.#providerID}: ${err}`, + ); + } + } + + /** + * getProjectID returns the project ID. If an override was given, the override + * is returned. Otherwise, this will be the project ID that was extracted from + * the service account key JSON. + */ + async getProjectID(): Promise { + return this.#projectID; + } + + /** + * getServiceAccount returns the service account email for the authentication, + * extracted from the input parameter. + */ + async getServiceAccount(): Promise { + return this.#serviceAccount; + } + + /** + * createCredentialsFile creates a Google Cloud credentials file that can be + * set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries. + */ + async createCredentialsFile(outputDir: string): Promise { + // 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; + const requestURLRaw = process.env.ACTIONS_ID_TOKEN_REQUEST_URL; + if (!requestToken || !requestURLRaw) { + throw new Error( + 'GitHub Actions did not inject $ACTIONS_ID_TOKEN_REQUEST_TOKEN or ' + + '$ACTIONS_ID_TOKEN_REQUEST_URL into this job. This most likely ' + + 'means the GitHub Actions workflow permissions are incorrect, or ' + + 'this job is being run from a fork. For more information, please ' + + 'see the GitHub documentation at https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token', + ); + } + const requestURL = new URL(requestURLRaw); + + // Append the audience value to the request. + const params = requestURL.searchParams; + params.set('audience', this.#audience); + requestURL.search = params.toString(); + const data = { + type: 'external_account', + audience: `//iam.googleapis.com/${this.#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/${ + this.#serviceAccount + }:generateAccessToken`, + credential_source: { + url: requestURL, + headers: { + Authorization: `Bearer ${requestToken}`, + }, + format: { + type: 'json', + subject_token_field_name: 'value', + }, + }, + }; + + return await writeSecureFile(outputDir, JSON.stringify(data)); + } +} diff --git a/src/main.ts b/src/main.ts index 6d99219..2cd58a4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,10 @@ 'use strict'; import * as core from '@actions/core'; -import { WIFClient } from './workload_identity'; -import { ActionAuth } from './actionauth'; +import { WorkloadIdentityClient } from './client/workload_identity_client'; +import { CredentialsJSONClient } from './client/credentials_json_client'; +import { AuthClient } from './client/auth_client'; +import { BaseClient } from './base'; import { explodeStrings } from './utils'; /** @@ -11,22 +13,53 @@ import { explodeStrings } from './utils'; async function run(): Promise { try { // Load configuration. - const workloadIdentityProvider = core.getInput('workload_identity_provider', { - required: true, - }); - const serviceAccount = core.getInput('service_account', { required: true }); - // audience will default to the WIF provider ID when used with WIF - const audience = core.getInput('audience'); + const projectID = core.getInput('project_id'); + const workloadIdentityProvider = core.getInput('workload_identity_provider'); + const serviceAccount = core.getInput('service_account'); + const audience = + core.getInput('audience') || `https://iam.googleapis.com/${workloadIdentityProvider}`; + const credentialsJSON = core.getInput('credentials_json'); 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 client: ActionAuth = new WIFClient({ - providerID: workloadIdentityProvider, - serviceAccount: serviceAccount, - audience: audience, - }); + // Ensure exactly one of workload_identity_provider and credentials_json was + // provided. + if ( + (!workloadIdentityProvider && !credentialsJSON) || + (workloadIdentityProvider && credentialsJSON) + ) { + throw new Error( + 'The GitHub Action workflow must specify exactly one of ' + + '"workload_identity_provider" or "credentials_json"!', + ); + } + + // Ensure a service_account was provided if using WIF. + if (workloadIdentityProvider && !serviceAccount) { + throw new Error( + 'The GitHub Action workflow must specify a "service_account" to ' + + 'impersonate when using "workload_identity_provider"!', + ); + } + + // Instantiate the correct client based on the provided input parameters. + let client: AuthClient; + if (workloadIdentityProvider) { + const token = await core.getIDToken(audience); + client = new WorkloadIdentityClient({ + projectID: projectID, + providerID: workloadIdentityProvider, + serviceAccount: serviceAccount, + token: token, + audience: audience, + }); + } else { + client = new CredentialsJSONClient({ + projectID: projectID, + credentialsJSON: credentialsJSON, + }); + } // Always write the credentials file first, before trying to generate // tokens. This will ensure the file is written even if token generation @@ -38,18 +71,21 @@ async function run(): Promise { throw new Error('$RUNNER_TEMP is not set'); } - const { credentialsPath, envVars } = await client.createCredentialsFile(runnerTempDir); + const credentialsPath = await client.createCredentialsFile(runnerTempDir); core.setOutput('credentials_file_path', credentialsPath); - - // Also set the magic environment variable for gcloud and SDKs if - // requested. - if (activateCredentialsFile && envVars) { - for (const [k, v] of envVars) { - core.exportVariable(k, v); - } - } + core.exportVariable('CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE', credentialsPath); + core.exportVariable('GOOGLE_APPLICATION_CREDENTIALS', credentialsPath); } + // Set the project ID environment variables to the computed values. + const computedProjectID = await client.getProjectID(); + core.setOutput('project_id', computedProjectID); + core.exportVariable('CLOUDSDK_PROJECT', computedProjectID); + core.exportVariable('CLOUDSDK_CORE_PROJECT', computedProjectID); + core.exportVariable('GCP_PROJECT', computedProjectID); + core.exportVariable('GCLOUD_PROJECT', computedProjectID); + core.exportVariable('GOOGLE_CLOUD_PROJECT', computedProjectID); + switch (tokenFormat) { case '': { break; @@ -60,8 +96,10 @@ async function run(): Promise { case 'access_token': { const accessTokenLifetime = core.getInput('access_token_lifetime'); const accessTokenScopes = explodeStrings(core.getInput('access_token_scopes')); + const serviceAccount = await client.getServiceAccount(); - const { accessToken, expiration } = await client.getAccessToken({ + const authToken = await client.getAuthToken(); + const { accessToken, expiration } = await BaseClient.googleAccessToken(authToken, { serviceAccount, delegates, scopes: accessTokenScopes, @@ -76,7 +114,10 @@ async function run(): Promise { case 'id_token': { const idTokenAudience = core.getInput('id_token_audience', { required: true }); const idTokenIncludeEmail = core.getBooleanInput('id_token_include_email'); - const { token } = await client.getIDToken({ + const serviceAccount = await client.getServiceAccount(); + + const authToken = await client.getAuthToken(); + const { token } = await BaseClient.googleIDToken(authToken, { serviceAccount, audience: idTokenAudience, delegates, @@ -87,7 +128,7 @@ async function run(): Promise { break; } default: { - throw new Error(`unknown token format "${tokenFormat}"`); + throw new Error(`Unknown token format "${tokenFormat}"`); } } } catch (err) { diff --git a/src/utils.ts b/src/utils.ts index 9bf284c..94ded1c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,14 +3,14 @@ import crypto from 'crypto'; import path from 'path'; /** - * writeCredFile writes a file to disk in a given directory with a + * writeSecureFile writes a file to disk in a given directory with a * random name. * * @param outputDir Directory to create random file in. * @param data Data to write to file. * @returns Path to written file. */ -export async function writeCredFile(outputDir: string, data: string): Promise { +export async function writeSecureFile(outputDir: string, data: string): Promise { // 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 @@ -45,3 +45,32 @@ export function explodeStrings(input: string): Array { } return list; } + +/** + * toBase64 base64 URL encodes the result. + */ +export function toBase64(s: string | Buffer): string { + return Buffer.from(s) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); +} + +/** + * fromBase64 base64 decodes the result, taking into account URL and standard + * encoding with and without padding. + */ +export function fromBase64(s: string): string { + const str = s.replace(/-/g, '+').replace(/_/g, '/'); + while (str.length % 4) s += '='; + return Buffer.from(str, 'base64').toString('utf8'); +} + +/** + * trimmedString returns a string trimmed of whitespace. If the input string is + * null, then it returns the empty string. + */ +export function trimmedString(s: string): string { + return s ? s.trim() : ''; +} diff --git a/src/workload_identity.ts b/src/workload_identity.ts deleted file mode 100644 index 2a6e857..0000000 --- a/src/workload_identity.ts +++ /dev/null @@ -1,178 +0,0 @@ -'use strict'; - -import { URL } from 'url'; -import * as core from '@actions/core'; -import { - ActionAuth, - CreateCredentialsFileResponse, - GoogleAccessTokenParameters, - GoogleAccessTokenResponse, - GoogleIDTokenParameters, - GoogleIDTokenResponse, -} from './actionauth'; -import { writeCredFile } from './utils'; -import { BaseClient } from './base'; - -/** - * GoogleFederatedTokenParameters are the parameters to generate a Federated - * Identity Token as described in: - * - * https://cloud.google.com/iam/docs/access-resources-oidc#exchange-token - * - * @param providerID Full path (including project, location, etc) to the Google - * Cloud Workload Identity Provider. - * @param token OIDC token to exchange for a Google Cloud federated token. - */ -interface GoogleFederatedTokenParameters { - providerID: string; - token: string; -} - -/** - * Available options to create the WIF client. - * - * @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 audience The value for the audience parameter in the generated GitHub Actions OIDC token, - * defaults to the value of workload_identity_provider - */ -interface WIFClientOptions { - providerID: string; - serviceAccount: string; - audience?: string; -} - -export class WIFClient implements ActionAuth { - readonly providerID: string; - readonly serviceAccount: string; - readonly audience: string; - - constructor(opts: WIFClientOptions) { - this.providerID = opts.providerID; - this.serviceAccount = opts.serviceAccount; - this.audience = opts.audience ? opts.audience : `https://iam.googleapis.com/${this.providerID}`; - } - - /** - * googleFederatedToken generates a Google Cloud federated token using the - * provided OIDC token and Workload Identity Provider. - */ - static async googleFederatedToken({ - providerID, - token, - }: GoogleFederatedTokenParameters): Promise { - const stsURL = new URL('https://sts.googleapis.com/v1/token'); - - const data = { - audience: '//iam.googleapis.com/' + providerID, - grantType: 'urn:ietf:params:oauth:grant-type:token-exchange', - requestedTokenType: 'urn:ietf:params:oauth:token-type:access_token', - scope: 'https://www.googleapis.com/auth/cloud-platform', - subjectTokenType: 'urn:ietf:params:oauth:token-type:jwt', - subjectToken: token, - }; - - const opts = { - hostname: stsURL.hostname, - port: stsURL.port, - path: stsURL.pathname + stsURL.search, - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - }; - - try { - const resp = await BaseClient.request(opts, JSON.stringify(data)); - const parsed = JSON.parse(resp); - return parsed['access_token']; - } catch (err) { - throw new Error(`failed to generate Google Cloud federated token for ${providerID}: ${err}`); - } - } - - /** - * getFederatedToken generates a Google Cloud federated token using the - * GitHub OIDC token. - */ - private async getFederatedToken(): Promise { - // Get the GitHub OIDC token. - const githubOIDCToken = await core.getIDToken(this.audience); - // Exchange the GitHub OIDC token for a Google Federated Token. - const googleFederatedToken = await WIFClient.googleFederatedToken({ - providerID: this.providerID, - token: githubOIDCToken, - }); - core.setSecret(googleFederatedToken); - return googleFederatedToken; - } - - /** - * getAccessToken generates a Google Cloud access token for the provided - * service account email or unique id. - */ - async getAccessToken(opts: GoogleAccessTokenParameters): Promise { - const googleFederatedToken = await this.getFederatedToken(); - return await BaseClient.googleAccessToken(googleFederatedToken, opts); - } - - /** - * getIDToken generates a Google Cloud ID token for the provided - * service account email or unique id. - */ - async getIDToken(tokenParams: GoogleIDTokenParameters): Promise { - const googleFederatedToken = await this.getFederatedToken(); - return await BaseClient.googleIDToken(googleFederatedToken, tokenParams); - } - - /** - * createCredentialsFile creates a Google Cloud credentials file that can be - * set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries. - */ - async createCredentialsFile(outputDir: string): Promise { - // 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; - const requestURLRaw = process.env.ACTIONS_ID_TOKEN_REQUEST_URL; - if (!requestToken || !requestURLRaw) { - throw new Error( - 'GitHub Actions did not inject $ACTIONS_ID_TOKEN_REQUEST_TOKEN or ' + - '$ACTIONS_ID_TOKEN_REQUEST_URL into this job. This most likely ' + - 'means the GitHub Actions workflow permissions are incorrect, or ' + - 'this job is being run from a fork. For more information, please ' + - 'see the GitHub documentation at https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token', - ); - } - const requestURL = new URL(requestURLRaw); - - // Append the audience value to the request. - const params = requestURL.searchParams; - params.set('audience', this.audience); - requestURL.search = params.toString(); - const data = { - type: 'external_account', - audience: `//iam.googleapis.com/${this.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/${this.serviceAccount}:generateAccessToken`, - credential_source: { - url: requestURL, - headers: { - Authorization: `Bearer ${requestToken}`, - }, - format: { - type: 'json', - subject_token_field_name: 'value', - }, - }, - }; - - const credentialsPath = await writeCredFile(outputDir, JSON.stringify(data)); - const envVars = new Map([['GOOGLE_APPLICATION_CREDENTIALS', credentialsPath]]); - return { credentialsPath, envVars }; - } -} diff --git a/tests/client.test.ts b/tests/client.test.ts deleted file mode 100644 index 5de6534..0000000 --- a/tests/client.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect } from 'chai'; -import 'mocha'; - -describe('Client', () => { - it('todo'); -}); diff --git a/tests/client/credentials_json_client.test.ts b/tests/client/credentials_json_client.test.ts new file mode 100644 index 0000000..78f71f4 --- /dev/null +++ b/tests/client/credentials_json_client.test.ts @@ -0,0 +1,107 @@ +import 'mocha'; +import { expect } from 'chai'; + +import { tmpdir } from 'os'; +import { readFileSync } from 'fs'; +import { CredentialsJSONClient } from '../../src/client/credentials_json_client'; + +// Yes, this is a real private key. No, it's not valid for authenticating +// Google Cloud. +const credentialsJSON = ` +{ + "type": "service_account", + "project_id": "my-project", + "private_key_id": "1234567890abcdefghijklmnopqrstuvwxyzaabb", + "private_key": "-----BEGIN PRIVATE KEY-----\\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCRVYIJRuxdujaX\\nUfyY9mXT1O0M3PwyT+FnPJVY+6Md7KMiPKpZRYt7okj51Ln1FLcb9mY17LzPEAxS\\nBPn1LWNpSJpmttI/D3U+bG/znf/E89ErVopYWpaynbYrb/Mu478IE9TgvnqJMlkj\\nlQbaxnZ7qhnbI5h6p/HINWfY7xBDGZM1sc2FK9KbNfEzLdW1YiK/lWAwtfM7rbiO\\nZj+LnWm2dgwZxu0h8m68qYYMywzLcV3NTe35qdAznasc1WQvJikY+N82Wu+HjsPa\\nH0fLE3gN5r+BzDYQxEQnWANgxlsHeN9mg5LAg5fyTBwTS7Ato/qQ07da0CSoS1M0\\nriYvuCzhAgMBAAECggEAAai+m9fG5B03kIMLpY5O7Rv9AM+ufb91hx6Nwkp7r4M5\\nt11vY7I96wuYJ92iBu8m4XR6fGw0Xz3gkcQ69ZCu5320hBdPrJsrqXwMhgxgoGcq\\nWuB8aJEWASi+T9hGENA++eDQFMupWV6HafzCdxd4NKAfmZ/xf1OFUu0TVpvxKlAD\\ne6Njz/5+QFdUcNioi7iGy1Qz7xdpClEWdVin8VWe3p6UsCLfHmQfPPuLXOvpBj6k\\niFu9dl93z+8vlDLoAyXSaDeYyRMBGVOBM36cICuVpxfV1s/corEZXhz3aI8mlYiQ\\n6YXTcEnllt+NTJDIL99CnYn+WBVzeIGXtr0EKAyM6QKBgQDCU6FDvU0P8qt45BDm\\nSP2V7uMoI32mjEA3plJzqqSZ9ritxFmylrOttOoTYH2FVjrKPZZsLihSjpmm+wEz\\nGfjd75eSJYAb/m7GNOqbJjqAJIbIMaHfVcH6ODT2b0Tc8v/CK0PZy/jzgt68TdtF\\no462tr8isj7yLpCGdoLq9iq4gwKBgQC/dWTGFnaI08v1uqx6derf+qikSsjlYh4L\\nDdTlI8/eaTR90PFPQ4a8LE8pmhMhkJNg87jAF5VF29sPmlpfKbOC87C2iI8uIHcn\\nu0sTdhn6SukyUSN/eeb1KSDJuxDvIgPRTZj6XMlUulADeLRnlAoWOe0tu/wqpse6\\nB0Qu2oAfywKBgQCMWukESyro1OZit585JQj7jQJG0HOFopETYK722g5vIdM7trDu\\nm4iFc0EJ48xlTOXDgv4tfp0jG9oA0BSKuzyT1+RK64j/LyMFR90XWGIyga9T0v1O\\nmNs1BfnC8JT1XRG7RZKJMZjLEQAdU8KHJt4CPDYLMmDifR1n8RsX59rtTwKBgQCS\\nnAmsKn1gb5cqt2Tmba+LDj3feSj3hjftTQ0u3kqKTNOWWM7AXLwrEl8YQ1TNChHh\\nVyCtcCGtmhrYiuETKDK/X259iHrj3paABUsLPw/Le1uxXTKqpiV2rKTf9XCVPd3g\\ng+RWK4E8cWNeFStIebNzq630rJP/8TDWQkQzALzGGwKBgQC5bnlmipIGhtX2pP92\\niBM8fJC7QXbyYyamriyFjC3o250hHy7mZZG7bd0bH3gw0NdC+OZIBNv7AoNhjsvP\\nuE0Qp/vQXpgHEeYFyfWn6PyHGzqKLFMZ/+iCTuy8Iebs1p5DZY8RMXpx4tv6NfRy\\nbxHUjlOgP7xmXM+OZpNymFlRkg==\\n-----END PRIVATE KEY-----\\n", + "client_email": "my-service-account@my-project.iam.gserviceaccount.com", + "client_id": "123456789098765432101", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/my-service-account%40my-project.iam.gserviceaccount.com" +} +`; + +describe('CredentialsJSONClient', () => { + describe('#parseServiceAccountKeyJSON', () => { + it('throws exception on invalid json', async () => { + const fn = (): CredentialsJSONClient => { + return new CredentialsJSONClient({ + credentialsJSON: 'invalid json', + }); + }; + + expect(fn).to.throw(SyntaxError); + }); + + it('handles base64', async () => { + const fn = (): CredentialsJSONClient => { + return new CredentialsJSONClient({ + credentialsJSON: Buffer.from('{}').toString('base64'), + }); + }; + + expect(fn).to.not.throw(SyntaxError); + }); + }); + + describe('#getAuthToken', () => { + it('signs a jwt', async () => { + const client = new CredentialsJSONClient({ + credentialsJSON: credentialsJSON, + }); + + const token = await client.getAuthToken(); + expect(token).to.not.be.null; + }); + }); + + describe('#getProjectID', () => { + it('extracts project ID from the json', async () => { + const client = new CredentialsJSONClient({ + credentialsJSON: credentialsJSON, + }); + + const result = await client.getProjectID(); + expect(result).to.eq('my-project'); + }); + + it('prefers the override if given', async () => { + const client = new CredentialsJSONClient({ + projectID: 'my-other-project', + credentialsJSON: credentialsJSON, + }); + + const result = await client.getProjectID(); + expect(result).to.eq('my-other-project'); + }); + }); + + describe('#getServiceAccount', () => { + it('extracts service account from the json', async () => { + const client = new CredentialsJSONClient({ + credentialsJSON: credentialsJSON, + }); + + const result = await client.getServiceAccount(); + expect(result).to.eq('my-service-account@my-project.iam.gserviceaccount.com'); + }); + }); + + describe('#createCredentialsFile', () => { + it('writes the file', async () => { + const tmp = tmpdir(); + const client = new CredentialsJSONClient({ + credentialsJSON: credentialsJSON, + }); + + const exp = JSON.parse(credentialsJSON); + + const pth = await client.createCredentialsFile(tmp); + const data = readFileSync(pth); + const got = JSON.parse(data.toString('utf8')); + + expect(got).to.deep.equal(exp); + }); + }); +}); diff --git a/tests/client/workload_identity_client.test.ts b/tests/client/workload_identity_client.test.ts new file mode 100644 index 0000000..8c13910 --- /dev/null +++ b/tests/client/workload_identity_client.test.ts @@ -0,0 +1,102 @@ +import 'mocha'; +import { expect } from 'chai'; + +import { tmpdir } from 'os'; +import { readFileSync } from 'fs'; +import { WorkloadIdentityClient } from '../../src/client/workload_identity_client'; + +describe('WorkloadIdentityClient', () => { + describe('#getProjectID', () => { + it('extracts project ID from the service account email', async () => { + const client = new WorkloadIdentityClient({ + providerID: 'my-provider', + token: 'my-token', + serviceAccount: 'my-service@my-project.iam.gserviceaccount.com', + audience: 'my-aud', + }); + + const result = await client.getProjectID(); + expect(result).to.eq('my-project'); + }); + + it('prefers the override if given', async () => { + const client = new WorkloadIdentityClient({ + projectID: 'my-other-project', + providerID: 'my-provider', + token: 'my-token', + serviceAccount: 'my-service@my-project.iam.gserviceaccount.com', + audience: 'my-aud', + }); + + const result = await client.getProjectID(); + expect(result).to.eq('my-other-project'); + }); + + it('throws an error when extraction fails', async () => { + const fn = () => { + return new WorkloadIdentityClient({ + providerID: 'my-provider', + token: 'my-token', + serviceAccount: 'my-service@developers.google.com', + audience: 'my-aud', + }); + }; + return expect(fn).to.throw(Error); + }); + }); + + describe('#getServiceAccount', () => { + it('returns the provided value', async () => { + const client = new WorkloadIdentityClient({ + projectID: 'my-project', + providerID: 'my-provider', + serviceAccount: 'my-service@my-project.iam.gserviceaccount.com', + token: 'my-token', + audience: 'my-aud', + }); + const result = await client.getServiceAccount(); + expect(result).to.eq('my-service@my-project.iam.gserviceaccount.com'); + }); + }); + + describe('#createCredentialsFile', () => { + it('writes the file', async () => { + process.env.ACTIONS_ID_TOKEN_REQUEST_URL = 'https://actions-token.url'; + process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'github-token'; + + const tmp = tmpdir(); + const client = new WorkloadIdentityClient({ + projectID: 'my-project', + providerID: 'my-provider', + serviceAccount: 'my-service@my-project.iam.gserviceaccount.com', + token: 'my-token', + audience: 'my-aud', + }); + + const exp = { + audience: '//iam.googleapis.com/my-provider', + credential_source: { + format: { + subject_token_field_name: 'value', + type: 'json', + }, + headers: { + Authorization: 'Bearer github-token', + }, + url: 'https://actions-token.url/?audience=my-aud', + }, + service_account_impersonation_url: + 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/my-service@my-project.iam.gserviceaccount.com:generateAccessToken', + subject_token_type: 'urn:ietf:params:oauth:token-type:jwt', + token_url: 'https://sts.googleapis.com/v1/token', + type: 'external_account', + }; + + const pth = await client.createCredentialsFile(tmp); + const data = readFileSync(pth); + const got = JSON.parse(data.toString('utf8')); + + expect(got).to.deep.equal(exp); + }); + }); +});