From 35255976074fdb6c986e4bc224a83464d0030f0d Mon Sep 17 00:00:00 2001 From: Bailey Pearson Date: Tue, 25 Jul 2023 11:07:17 -0600 Subject: [PATCH] refactor(NODE-5422): convert FLE kms providers and errors to Typescript (#3779) --- .evergreen/run-azure-kms-tests.sh | 2 +- .evergreen/run-gcp-kms-tests.sh | 2 +- .evergreen/run-kerberos-tests.sh | 5 - .evergreen/run-serverless-tests.sh | 2 +- .evergreen/run-socks5-tests.sh | 2 +- .evergreen/run-tests.sh | 2 +- .evergreen/run-unit-tests.sh | 2 +- package-lock.json | 185 ++++++++++++++---- package.json | 5 +- .../{errors.js => errors.ts} | 52 +++-- src/client-side-encryption/providers/aws.js | 22 --- src/client-side-encryption/providers/aws.ts | 20 ++ src/client-side-encryption/providers/azure.js | 174 ---------------- src/client-side-encryption/providers/azure.ts | 164 ++++++++++++++++ src/client-side-encryption/providers/gcp.js | 20 -- src/client-side-encryption/providers/gcp.ts | 16 ++ src/client-side-encryption/providers/index.js | 52 ----- src/client-side-encryption/providers/index.ts | 165 ++++++++++++++++ .../providers/{utils.js => utils.ts} | 20 +- src/cmap/auth/mongodb_aws.ts | 8 - src/deps.ts | 33 +++- ...ion.prose.18.azure_kms_mock_server.test.ts | 7 +- ...er.test.js => credentialsProvider.test.ts} | 112 ++++++----- .../requirements.helper.js | 48 ----- .../requirements.helper.ts | 33 ++++ tsconfig.json | 3 +- 26 files changed, 697 insertions(+), 459 deletions(-) rename src/client-side-encryption/{errors.js => errors.ts} (57%) delete mode 100644 src/client-side-encryption/providers/aws.js create mode 100644 src/client-side-encryption/providers/aws.ts delete mode 100644 src/client-side-encryption/providers/azure.js create mode 100644 src/client-side-encryption/providers/azure.ts delete mode 100644 src/client-side-encryption/providers/gcp.js create mode 100644 src/client-side-encryption/providers/gcp.ts delete mode 100644 src/client-side-encryption/providers/index.js create mode 100644 src/client-side-encryption/providers/index.ts rename src/client-side-encryption/providers/{utils.js => utils.ts} (73%) rename test/unit/client-side-encryption/providers/{credentialsProvider.test.js => credentialsProvider.test.ts} (83%) delete mode 100644 test/unit/client-side-encryption/requirements.helper.js create mode 100644 test/unit/client-side-encryption/requirements.helper.ts diff --git a/.evergreen/run-azure-kms-tests.sh b/.evergreen/run-azure-kms-tests.sh index 5f1d53de3c..741b7135b8 100644 --- a/.evergreen/run-azure-kms-tests.sh +++ b/.evergreen/run-azure-kms-tests.sh @@ -9,7 +9,7 @@ source ".evergreen/init-node-and-npm-env.sh" set -o xtrace -npm install mongodb-client-encryption@alpha --force +npm install mongodb-client-encryption@alpha export MONGODB_URI="mongodb://localhost:27017" diff --git a/.evergreen/run-gcp-kms-tests.sh b/.evergreen/run-gcp-kms-tests.sh index b3c4aeaa2b..35cb203b07 100644 --- a/.evergreen/run-gcp-kms-tests.sh +++ b/.evergreen/run-gcp-kms-tests.sh @@ -9,7 +9,7 @@ source ".evergreen/init-node-and-npm-env.sh" set -o xtrace -npm install mongodb-client-encryption@alpha --force +npm install mongodb-client-encryption@alpha npm install gcp-metadata export MONGODB_URI="mongodb://localhost:27017" diff --git a/.evergreen/run-kerberos-tests.sh b/.evergreen/run-kerberos-tests.sh index eb6b4b4422..bc3a8d3752 100644 --- a/.evergreen/run-kerberos-tests.sh +++ b/.evergreen/run-kerberos-tests.sh @@ -24,11 +24,6 @@ set -o xtrace npm install kerberos@">=2.0.0-beta.0" npm run check:kerberos -if [ "$NODE_LTS_VERSION" != "latest" ] && [ $NODE_LTS_VERSION -lt 20 ]; then - npm install kerberos@"^1.1.7" - npm run check:kerberos -fi - set +o xtrace # destroy ticket diff --git a/.evergreen/run-serverless-tests.sh b/.evergreen/run-serverless-tests.sh index 7ffae74752..df95d81844 100755 --- a/.evergreen/run-serverless-tests.sh +++ b/.evergreen/run-serverless-tests.sh @@ -10,7 +10,7 @@ if [ -z ${MONGODB_URI+omitted} ]; then echo "MONGODB_URI is unset" && exit 1; fi if [ -z ${SERVERLESS_ATLAS_USER+omitted} ]; then echo "SERVERLESS_ATLAS_USER is unset" && exit 1; fi if [ -z ${SERVERLESS_ATLAS_PASSWORD+omitted} ]; then echo "SERVERLESS_ATLAS_PASSWORD is unset" && exit 1; fi -npm install mongodb-client-encryption@alpha --force +npm install mongodb-client-encryption@alpha npx mocha \ --config test/mocha_mongodb.json \ diff --git a/.evergreen/run-socks5-tests.sh b/.evergreen/run-socks5-tests.sh index de77ac15ec..9093fe5d0a 100644 --- a/.evergreen/run-socks5-tests.sh +++ b/.evergreen/run-socks5-tests.sh @@ -20,7 +20,7 @@ function setup_fle() { # CSFLE_AWS_TEMP_ACCESS_KEY_ID, CSFLE_AWS_TEMP_SECRET_ACCESS_KEY, CSFLE_AWS_TEMP_SESSION_TOKEN . "$DRIVERS_TOOLS"/.evergreen/csfle/set-temp-creds.sh - npm i --force mongodb-client-encryption@alpha + npm i mongodb-client-encryption@alpha export KMIP_TLS_CA_FILE="${DRIVERS_TOOLS}/.evergreen/x509gen/ca.pem" export KMIP_TLS_CERT_FILE="${DRIVERS_TOOLS}/.evergreen/x509gen/client.pem" export TEST_CSFLE=true diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index c2988ae6ed..5daf11d356 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -52,7 +52,7 @@ else source "$DRIVERS_TOOLS"/.evergreen/csfle/set-temp-creds.sh fi -npm install mongodb-client-encryption@alpha --force +npm install mongodb-client-encryption@alpha npm install @mongodb-js/zstd npm install snappy diff --git a/.evergreen/run-unit-tests.sh b/.evergreen/run-unit-tests.sh index 3f95c724d4..79a447a401 100644 --- a/.evergreen/run-unit-tests.sh +++ b/.evergreen/run-unit-tests.sh @@ -4,6 +4,6 @@ set -o errexit # Exit the script with error if any of the commands fail source "${PROJECT_DIRECTORY}/.evergreen/init-node-and-npm-env.sh" set -o xtrace -npm i --force mongodb-client-encryption@alpha +npm i mongodb-client-encryption@alpha npx nyc npm run check:unit diff --git a/package-lock.json b/package-lock.json index fd9f212379..e601bf5af9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,10 +43,11 @@ "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-tsdoc": "^0.2.17", "express": "^4.18.2", + "gcp-metadata": "^5.2.0", "js-yaml": "^4.1.0", "mocha": "^10.2.0", "mocha-sinon": "^2.1.2", - "mongodb-client-encryption": "^2.8.0", + "mongodb-client-encryption": "^6.0.0-alpha.0", "mongodb-legacy": "^5.0.0", "nyc": "^15.1.0", "prettier": "^2.8.8", @@ -73,7 +74,7 @@ "@mongodb-js/zstd": "^1.1.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=2.3.0 <3", + "mongodb-client-encryption": ">=6.0.0-alpha.0 <7", "snappy": "^7.2.2" }, "peerDependenciesMeta": { @@ -3111,6 +3112,18 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -3391,6 +3404,15 @@ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", "dev": true }, + "node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -4706,6 +4728,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5041,6 +5069,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "dev": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "dev": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -5401,6 +5457,19 @@ "integrity": "sha512-o0PWwVCSp3O0wS6FvNr6xfBCHgt0m1tvPLFOCc2iFDKTRAXhB7m8klDf7ErowFH8POa6dVdGatKU5I1YYwzUyg==", "dev": true }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6049,6 +6118,15 @@ "node": ">=4" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -6514,13 +6592,58 @@ "node": ">=10" } }, - "node_modules/mongodb": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.4.0.tgz", - "integrity": "sha512-6GDKgO7WiYUw+ILap143VXfAou06hjxDGgYUZWGnI4hgoZfP3el0G3l69JqJF8SEQbZmC+SN/2a0aWI/aWJoxA==", + "node_modules/mongodb-client-encryption": { + "version": "6.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-6.0.0-alpha.0.tgz", + "integrity": "sha512-lwkwJcjgXnxtd3A5otzTchxtqS+aVmsGpVaYnpnrL2m2s59uWXJpVStPQBt54SYDPt0Eu7pcT8nrWcVvZGZFfg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": ">=12.9.0" + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongodb-connection-string-url/node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "node_modules/mongodb-legacy": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mongodb-legacy/-/mongodb-legacy-5.0.0.tgz", + "integrity": "sha512-q2G+MRwde6114bCAF/EZLmMXSsebIKMHmzsfOJq6M/Tj4gr3wLT50+rJsJNkiR0e0kjFx3dllWjqwRR1n11Zsw==", "dev": true, "dependencies": { - "bson": "^5.2.0", + "mongodb": "^5.0.0" + }, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-legacy/node_modules/mongodb": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.7.0.tgz", + "integrity": "sha512-zm82Bq33QbqtxDf58fLWBwTjARK3NSvKYjyz997KSy6hpat0prjeX/kxjbPVyZY60XYPDNETaHkHJI2UCzSLuw==", + "dev": true, + "dependencies": { + "bson": "^5.4.0", "mongodb-connection-string-url": "^2.6.0", "socks": "^2.7.1" }, @@ -6532,6 +6655,8 @@ }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.201.0", + "@mongodb-js/zstd": "^1.1.0", + "kerberos": "^2.0.1", "mongodb-client-encryption": ">=2.3.0 <3", "snappy": "^7.2.2" }, @@ -6539,6 +6664,12 @@ "@aws-sdk/credential-providers": { "optional": true }, + "@mongodb-js/zstd": { + "optional": true + }, + "kerberos": { + "optional": true + }, "mongodb-client-encryption": { "optional": true }, @@ -6547,12 +6678,14 @@ } } }, - "node_modules/mongodb-client-encryption": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-2.8.0.tgz", - "integrity": "sha512-wIcaETX0Acis9hJkUf2SvtPMq/F1G2gxZXgp8QAe2yJzL+cIUpii8Yv4i3LIeZVwYuYSue8F6/e4pHaE21On7A==", + "node_modules/mongodb-legacy/node_modules/mongodb-client-encryption": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/mongodb-client-encryption/-/mongodb-client-encryption-2.9.0.tgz", + "integrity": "sha512-OGMfTnS+JJ49ksWdExQ5048ynaQJLhPjbOi3i44PbU2sdufKH0Z4YZqn1pvd/eQ4WgLfbmSws3u9kAiFNFxpOg==", "dev": true, "hasInstallScript": true, + "optional": true, + "peer": true, "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^4.3.0", @@ -6576,36 +6709,6 @@ } } }, - "node_modules/mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", - "dependencies": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" - } - }, - "node_modules/mongodb-connection-string-url/node_modules/@types/whatwg-url": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", - "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", - "dependencies": { - "@types/node": "*", - "@types/webidl-conversions": "*" - } - }, - "node_modules/mongodb-legacy": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mongodb-legacy/-/mongodb-legacy-5.0.0.tgz", - "integrity": "sha512-q2G+MRwde6114bCAF/EZLmMXSsebIKMHmzsfOJq6M/Tj4gr3wLT50+rJsJNkiR0e0kjFx3dllWjqwRR1n11Zsw==", - "dev": true, - "dependencies": { - "mongodb": "^5.0.0" - }, - "engines": { - "node": ">=14.20.1" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index 1328c34f91..12003ba28a 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@mongodb-js/zstd": "^1.1.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=2.3.0 <3", + "mongodb-client-encryption": ">=6.0.0-alpha.0 <7", "snappy": "^7.2.2" }, "peerDependenciesMeta": { @@ -90,10 +90,11 @@ "eslint-plugin-simple-import-sort": "^10.0.0", "eslint-plugin-tsdoc": "^0.2.17", "express": "^4.18.2", + "gcp-metadata": "^5.2.0", "js-yaml": "^4.1.0", "mocha": "^10.2.0", "mocha-sinon": "^2.1.2", - "mongodb-client-encryption": "^2.8.0", + "mongodb-client-encryption": "^6.0.0-alpha.0", "mongodb-legacy": "^5.0.0", "nyc": "^15.1.0", "prettier": "^2.8.8", diff --git a/src/client-side-encryption/errors.js b/src/client-side-encryption/errors.ts similarity index 57% rename from src/client-side-encryption/errors.js rename to src/client-side-encryption/errors.ts index bb0a72ff62..615446e4c7 100644 --- a/src/client-side-encryption/errors.js +++ b/src/client-side-encryption/errors.ts @@ -1,63 +1,75 @@ +import { type Document } from '../bson'; + /** - * @class + * @public * An error indicating that something went wrong specifically with MongoDB Client Encryption */ export class MongoCryptError extends Error { - constructor(message, options = {}) { - super(message); - if (options.cause != null) { - this.cause = options.cause; - } + /** @internal */ + constructor(message: string, options: { cause?: Error } = {}) { + super(message, options); } - get name() { + override get name() { return 'MongoCryptError'; } } /** - * @class + * @public * An error indicating that `ClientEncryption.createEncryptedCollection()` failed to create data keys */ export class MongoCryptCreateDataKeyError extends MongoCryptError { - constructor({ encryptedFields, cause }) { + encryptedFields: Document; + /** @internal */ + constructor({ encryptedFields, cause }: { encryptedFields: Document; cause: Error }) { super(`Unable to complete creating data keys: ${cause.message}`, { cause }); this.encryptedFields = encryptedFields; } - get name() { + override get name() { return 'MongoCryptCreateDataKeyError'; } } /** - * @class + * @public * An error indicating that `ClientEncryption.createEncryptedCollection()` failed to create a collection */ export class MongoCryptCreateEncryptedCollectionError extends MongoCryptError { - constructor({ encryptedFields, cause }) { + encryptedFields: Document; + /** @internal */ + constructor({ encryptedFields, cause }: { encryptedFields: Document; cause: Error }) { super(`Unable to create collection: ${cause.message}`, { cause }); this.encryptedFields = encryptedFields; } - get name() { + override get name() { return 'MongoCryptCreateEncryptedCollectionError'; } } /** - * @class + * @public * An error indicating that mongodb-client-encryption failed to auto-refresh Azure KMS credentials. */ export class MongoCryptAzureKMSRequestError extends MongoCryptError { - /** - * @param {string} message - * @param {object | undefined} body - */ - constructor(message, body) { + /** The body of the http response that failed, if present. */ + body?: Document; + /** @internal */ + constructor(message: string, body?: Document) { super(message); this.body = body; } + + override get name(): string { + return 'MongoCryptAzureKMSRequestError'; + } } -export class MongoCryptKMSRequestNetworkTimeoutError extends MongoCryptError {} +/** @public */ +export class MongoCryptKMSRequestNetworkTimeoutError extends MongoCryptError { + override get name(): string { + return 'MongoCryptKMSRequestNetworkTimeoutError'; + } +} diff --git a/src/client-side-encryption/providers/aws.js b/src/client-side-encryption/providers/aws.js deleted file mode 100644 index 05b0fddb37..0000000000 --- a/src/client-side-encryption/providers/aws.js +++ /dev/null @@ -1,22 +0,0 @@ -let awsCredentialProviders = null; -/** @ignore */ -export async function loadAWSCredentials(kmsProviders) { - if (awsCredentialProviders == null) { - try { - // Ensure you always wrap an optional require in the try block NODE-3199 - awsCredentialProviders = require('@aws-sdk/credential-providers'); - // eslint-disable-next-line no-empty - } catch {} - } - - if (awsCredentialProviders != null) { - const { fromNodeProviderChain } = awsCredentialProviders; - const provider = fromNodeProviderChain(); - // The state machine is the only place calling this so it will - // catch if there is a rejection here. - const aws = await provider(); - return { ...kmsProviders, aws }; - } - - return kmsProviders; -} diff --git a/src/client-side-encryption/providers/aws.ts b/src/client-side-encryption/providers/aws.ts new file mode 100644 index 0000000000..64aa9f0adc --- /dev/null +++ b/src/client-side-encryption/providers/aws.ts @@ -0,0 +1,20 @@ +import { getAwsCredentialProvider } from '../../deps'; +import { type KMSProviders } from '.'; + +/** + * @internal + */ +export async function loadAWSCredentials(kmsProviders: KMSProviders): Promise { + const credentialProvider = getAwsCredentialProvider(); + + if ('kModuleError' in credentialProvider) { + return kmsProviders; + } + + const { fromNodeProviderChain } = credentialProvider; + const provider = fromNodeProviderChain(); + // The state machine is the only place calling this so it will + // catch if there is a rejection here. + const aws = await provider(); + return { ...kmsProviders, aws }; +} diff --git a/src/client-side-encryption/providers/azure.js b/src/client-side-encryption/providers/azure.js deleted file mode 100644 index d6525607dd..0000000000 --- a/src/client-side-encryption/providers/azure.js +++ /dev/null @@ -1,174 +0,0 @@ -import { - MongoCryptAzureKMSRequestError, - MongoCryptKMSRequestNetworkTimeoutError -} from '../errors'; -import * as utils from './utils'; - -const MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS = 6000; - -/** - * @class - * @ignore - */ -export class AzureCredentialCache { - constructor() { - /** - * @type { { accessToken: string, expiresOnTimestamp: number } | null} - */ - this.cachedToken = null; - } - - async getToken() { - if (this.needsRefresh(this.cachedToken)) { - this.cachedToken = await this._getToken(); - } - - return { accessToken: this.cachedToken.accessToken }; - } - - needsRefresh(token) { - if (token == null) { - return true; - } - const timeUntilExpirationMS = token.expiresOnTimestamp - Date.now(); - return timeUntilExpirationMS <= MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS; - } - - /** - * exposed for testing - * @ignore - */ - resetCache() { - this.cachedToken = null; - } - - /** - * exposed for testing - * @ignore - */ - _getToken() { - return fetchAzureKMSToken(); - } -} -/** - * @type{ AzureCredentialCache } - * @ignore - */ -export let tokenCache = new AzureCredentialCache(); - -/** - * @typedef {object} KmsRequestResponsePayload - * @property {string | undefined} access_token - * @property {string | undefined} expires_in - * - * @ignore - */ - -/** - * @param { {body: string, status: number }} response - * @returns { Promise<{ accessToken: string, expiresOnTimestamp: number } >} - * @ignore - */ -export async function parseResponse(response) { - const { status, body: rawBody } = response; - - /** - * @type { KmsRequestResponsePayload } - */ - const body = (() => { - try { - return JSON.parse(rawBody); - } catch { - throw new MongoCryptAzureKMSRequestError('Malformed JSON body in GET request.'); - } - })(); - - if (status !== 200) { - throw new MongoCryptAzureKMSRequestError('Unable to complete request.', body); - } - - if (!body.access_token) { - throw new MongoCryptAzureKMSRequestError( - 'Malformed response body - missing field `access_token`.' - ); - } - - if (!body.expires_in) { - throw new MongoCryptAzureKMSRequestError( - 'Malformed response body - missing field `expires_in`.' - ); - } - - const expiresInMS = Number(body.expires_in) * 1000; - if (Number.isNaN(expiresInMS)) { - throw new MongoCryptAzureKMSRequestError( - 'Malformed response body - unable to parse int from `expires_in` field.' - ); - } - - return { - accessToken: body.access_token, - expiresOnTimestamp: Date.now() + expiresInMS - }; -} - -/** - * @param {object} options - * @param {object | undefined} [options.headers] - * @param {URL | undefined} [options.url] - * - * @ignore - */ -export function prepareRequest(options) { - const url = - options.url == null - ? new URL('http://169.254.169.254/metadata/identity/oauth2/token') - : new URL(options.url); - - url.searchParams.append('api-version', '2018-02-01'); - url.searchParams.append('resource', 'https://vault.azure.net'); - - const headers = { ...options.headers, 'Content-Type': 'application/json', Metadata: true }; - return { headers, url }; -} - -/** - * @typedef {object} AzureKMSRequestOptions - * @property {object | undefined} headers - * @property {URL | undefined} url - * @ignore - */ - -/** - * @typedef {object} AzureKMSRequestResponse - * @property {string} accessToken - * @property {number} expiresOnTimestamp - * @ignore - */ - -/** - * exported only for testing purposes in the driver - * - * @param {AzureKMSRequestOptions} options - * @returns {Promise} - * - * @ignore - */ -export async function fetchAzureKMSToken(options = {}) { - const { headers, url } = prepareRequest(options); - const response = await utils.get(url, { headers }).catch(error => { - if (error instanceof MongoCryptKMSRequestNetworkTimeoutError) { - throw new MongoCryptAzureKMSRequestError(`[Azure KMS] ${error.message}`); - } - throw error; - }); - return parseResponse(response); -} - -/** - * @ignore - */ -export async function loadAzureCredentials(kmsProviders) { - const azure = await tokenCache.getToken(); - return { ...kmsProviders, azure }; -} diff --git a/src/client-side-encryption/providers/azure.ts b/src/client-side-encryption/providers/azure.ts new file mode 100644 index 0000000000..eaac6422a7 --- /dev/null +++ b/src/client-side-encryption/providers/azure.ts @@ -0,0 +1,164 @@ +import { type Document } from '../../bson'; + +import { MongoCryptAzureKMSRequestError, MongoCryptKMSRequestNetworkTimeoutError } from '../errors'; +import { type KMSProviders } from './index'; +import { get } from './utils'; + +const MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS = 6000; + +/** + * The access token that libmongocrypt expects for Azure kms. + */ +interface AccessToken { + accessToken: string; +} + +/** + * The response from the azure idms endpoint, including the `expiresOnTimestamp`. + * `expiresOnTimestamp` is needed for caching. + */ +interface AzureTokenCacheEntry extends AccessToken { + accessToken: string; + expiresOnTimestamp: number; +} + +/** + * @internal + */ +export class AzureCredentialCache { + cachedToken: AzureTokenCacheEntry | null = null; + + async getToken(): Promise { + if (this.cachedToken == null || this.needsRefresh(this.cachedToken)) { + this.cachedToken = await this._getToken(); + } + + return { accessToken: this.cachedToken.accessToken }; + } + + needsRefresh(token: AzureTokenCacheEntry): boolean { + const timeUntilExpirationMS = token.expiresOnTimestamp - Date.now(); + return timeUntilExpirationMS <= MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS; + } + + /** + * exposed for testing + */ + resetCache() { + this.cachedToken = null; + } + + /** + * exposed for testing + */ + _getToken(): Promise { + return fetchAzureKMSToken(); + } +} + +/** @internal */ +export const tokenCache = new AzureCredentialCache(); + +/** @internal */ +async function parseResponse(response: { body: string; status?: number }): Promise { + const { status, body: rawBody } = response; + + const body: { expires_in?: number; access_token?: string } = (() => { + try { + return JSON.parse(rawBody); + } catch { + throw new MongoCryptAzureKMSRequestError('Malformed JSON body in GET request.'); + } + })(); + + if (status !== 200) { + throw new MongoCryptAzureKMSRequestError('Unable to complete request.', body); + } + + if (!body.access_token) { + throw new MongoCryptAzureKMSRequestError( + 'Malformed response body - missing field `access_token`.' + ); + } + + if (!body.expires_in) { + throw new MongoCryptAzureKMSRequestError( + 'Malformed response body - missing field `expires_in`.' + ); + } + + const expiresInMS = Number(body.expires_in) * 1000; + if (Number.isNaN(expiresInMS)) { + throw new MongoCryptAzureKMSRequestError( + 'Malformed response body - unable to parse int from `expires_in` field.' + ); + } + + return { + accessToken: body.access_token, + expiresOnTimestamp: Date.now() + expiresInMS + }; +} + +/** + * @internal + * + * exposed for CSFLE + * [prose test 18](https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/tests#azure-imds-credentials) + */ +export interface AzureKMSRequestOptions { + headers?: Document; + url?: URL | string; +} + +/** + * @internal + * + * parses any options provided by prose tests to `fetchAzureKMSToken` and merges them with + * the default values for headers and the request url. + */ +export function prepareRequest(options: AzureKMSRequestOptions): { + headers: Document; + url: URL; +} { + const url = new URL(options.url?.toString() ?? 'http://169.254.169.254/metadata/identity/oauth2/token'); + + url.searchParams.append('api-version', '2018-02-01'); + url.searchParams.append('resource', 'https://vault.azure.net'); + + const headers = { ...options.headers, 'Content-Type': 'application/json', Metadata: true }; + return { headers, url }; +} + +/** + * @internal + * + * `AzureKMSRequestOptions` allows prose tests to modify the http request sent to the idms + * servers. This is required to simulate different server conditions. No options are expected to + * be set outside of tests. + * + * exposed for CSFLE + * [prose test 18](https://github.com/mongodb/specifications/tree/master/source/client-side-encryption/tests#azure-imds-credentials) + */ +export async function fetchAzureKMSToken( + options: AzureKMSRequestOptions = {} +): Promise { + const { headers, url } = prepareRequest(options); + const response = await get(url, { headers }).catch(error => { + if (error instanceof MongoCryptKMSRequestNetworkTimeoutError) { + throw new MongoCryptAzureKMSRequestError(`[Azure KMS] ${error.message}`); + } + throw error; + }); + return parseResponse(response); +} + +/** + * @internal + * + * @throws Will reject with a `MongoCryptError` if the http request fails or the http response is malformed. + */ +export async function loadAzureCredentials(kmsProviders: KMSProviders): Promise { + const azure = await tokenCache.getToken(); + return { ...kmsProviders, azure }; +} diff --git a/src/client-side-encryption/providers/gcp.js b/src/client-side-encryption/providers/gcp.js deleted file mode 100644 index d529e62a33..0000000000 --- a/src/client-side-encryption/providers/gcp.js +++ /dev/null @@ -1,20 +0,0 @@ -let gcpMetadata = null; -/** @ignore */ -export async function loadGCPCredentials(kmsProviders) { - if (gcpMetadata == null) { - try { - // Ensure you always wrap an optional require in the try block NODE-3199 - gcpMetadata = require('gcp-metadata'); - // eslint-disable-next-line no-empty - } catch {} - } - - if (gcpMetadata != null) { - const { access_token: accessToken } = await gcpMetadata.instance({ - property: 'service-accounts/default/token' - }); - return { ...kmsProviders, gcp: { accessToken } }; - } - - return kmsProviders; -} diff --git a/src/client-side-encryption/providers/gcp.ts b/src/client-side-encryption/providers/gcp.ts new file mode 100644 index 0000000000..1415d216aa --- /dev/null +++ b/src/client-side-encryption/providers/gcp.ts @@ -0,0 +1,16 @@ +import { getGcpMetadata } from '../../deps'; +import { type KMSProviders } from '.'; + +/** @internal */ +export async function loadGCPCredentials(kmsProviders: KMSProviders): Promise { + const gcpMetadata = getGcpMetadata(); + + if ('kModuleError' in gcpMetadata) { + return kmsProviders; + } + + const { access_token: accessToken } = await gcpMetadata.instance<{ access_token: string }>({ + property: 'service-accounts/default/token' + }); + return { ...kmsProviders, gcp: { accessToken } }; +} diff --git a/src/client-side-encryption/providers/index.js b/src/client-side-encryption/providers/index.js deleted file mode 100644 index 506f676ac6..0000000000 --- a/src/client-side-encryption/providers/index.js +++ /dev/null @@ -1,52 +0,0 @@ -import { loadAWSCredentials } from './aws' -import { loadAzureCredentials, fetchAzureKMSToken }from './azure'; -import { loadGCPCredentials } from './gcp'; - -/** - * Auto credential fetching should only occur when the provider is defined on the kmsProviders map - * and the settings are an empty object. - * - * This is distinct from a nullish provider key. - * - * @param {'aws' | 'gcp' | 'azure'} provider - * @param {object} kmsProviders - * - * @ignore - */ -function isEmptyCredentials(provider, kmsProviders) { - return ( - provider in kmsProviders && - kmsProviders[provider] != null && - typeof kmsProviders[provider] === 'object' && - Object.keys(kmsProviders[provider]).length === 0 - ); -} - -/** - * Load cloud provider credentials for the user provided KMS providers. - * Credentials will only attempt to get loaded if they do not exist - * and no existing credentials will get overwritten. - * - * @param {object} kmsProviders - The user provided KMS providers. - * @returns {object} The new kms providers. - * - * @ignore - */ -async function loadCredentials(kmsProviders) { - let finalKMSProviders = kmsProviders; - - if (isEmptyCredentials('aws', kmsProviders)) { - finalKMSProviders = await loadAWSCredentials(finalKMSProviders); - } - - if (isEmptyCredentials('gcp', kmsProviders)) { - finalKMSProviders = await loadGCPCredentials(finalKMSProviders); - } - - if (isEmptyCredentials('azure', kmsProviders)) { - finalKMSProviders = await loadAzureCredentials(finalKMSProviders); - } - return finalKMSProviders; -} - -module.exports = { loadCredentials, isEmptyCredentials, fetchAzureKMSToken }; diff --git a/src/client-side-encryption/providers/index.ts b/src/client-side-encryption/providers/index.ts new file mode 100644 index 0000000000..5e4024d51e --- /dev/null +++ b/src/client-side-encryption/providers/index.ts @@ -0,0 +1,165 @@ +import { loadAWSCredentials } from './aws'; +import { loadAzureCredentials } from './azure'; +import { loadGCPCredentials } from './gcp'; + +/** + * @public + */ +export type KMSProvider = 'aws' | 'azure' | 'gcp' | 'local'; + +/** + * @public + * Configuration options that are used by specific KMS providers during key generation, encryption, and decryption. + */ +export interface KMSProviders { + /** + * Configuration options for using 'aws' as your KMS provider + */ + aws?: + | { + /** + * The access key used for the AWS KMS provider + */ + accessKeyId: string; + + /** + * The secret access key used for the AWS KMS provider + */ + secretAccessKey: string; + + /** + * An optional AWS session token that will be used as the + * X-Amz-Security-Token header for AWS requests. + */ + sessionToken?: string; + } + | Record; + + /** + * Configuration options for using 'local' as your KMS provider + */ + local?: { + /** + * The master key used to encrypt/decrypt data keys. + * A 96-byte long Buffer or base64 encoded string. + */ + key: Buffer | string; + }; + + /** + * Configuration options for using 'kmip' as your KMS provider + */ + kmip?: { + /** + * The output endpoint string. + * The endpoint consists of a hostname and port separated by a colon. + * E.g. "example.com:123". A port is always present. + */ + endpoint?: string; + }; + + /** + * Configuration options for using 'azure' as your KMS provider + */ + azure?: + | { + /** + * The tenant ID identifies the organization for the account + */ + tenantId: string; + + /** + * The client ID to authenticate a registered application + */ + clientId: string; + + /** + * The client secret to authenticate a registered application + */ + clientSecret: string; + + /** + * If present, a host with optional port. E.g. "example.com" or "example.com:443". + * This is optional, and only needed if customer is using a non-commercial Azure instance + * (e.g. a government or China account, which use different URLs). + * Defaults to "login.microsoftonline.com" + */ + identityPlatformEndpoint?: string | undefined; + } + | { + /** + * If present, an access token to authenticate with Azure. + */ + accessToken: string; + } + | Record; + + /** + * Configuration options for using 'gcp' as your KMS provider + */ + gcp?: + | { + /** + * The service account email to authenticate + */ + email: string; + + /** + * A PKCS#8 encrypted key. This can either be a base64 string or a binary representation + */ + privateKey: string | Buffer; + + /** + * If present, a host with optional port. E.g. "example.com" or "example.com:443". + * Defaults to "oauth2.googleapis.com" + */ + endpoint?: string | undefined; + } + | { + /** + * If present, an access token to authenticate with GCP. + */ + accessToken: string; + } + | Record; +} + +/** + * Auto credential fetching should only occur when the provider is defined on the kmsProviders map + * and the settings are an empty object. + * + * This is distinct from a nullish provider key. + * + * @internal - exposed for testing purposes only + */ +export function isEmptyCredentials(providerName: KMSProvider, kmsProviders: KMSProviders): boolean { + const provider = kmsProviders[providerName]; + if (provider == null) { + return false; + } + return typeof provider === 'object' && Object.keys(provider).length === 0; +} + +/** + * Load cloud provider credentials for the user provided KMS providers. + * Credentials will only attempt to get loaded if they do not exist + * and no existing credentials will get overwritten. + * + * @internal + */ +export async function loadCredentials(kmsProviders: KMSProviders): Promise { + let finalKMSProviders = kmsProviders; + + if (isEmptyCredentials('aws', kmsProviders)) { + finalKMSProviders = await loadAWSCredentials(finalKMSProviders); + } + + if (isEmptyCredentials('gcp', kmsProviders)) { + finalKMSProviders = await loadGCPCredentials(finalKMSProviders); + } + + if (isEmptyCredentials('azure', kmsProviders)) { + finalKMSProviders = await loadAzureCredentials(finalKMSProviders); + } + return finalKMSProviders; +} diff --git a/src/client-side-encryption/providers/utils.js b/src/client-side-encryption/providers/utils.ts similarity index 73% rename from src/client-side-encryption/providers/utils.js rename to src/client-side-encryption/providers/utils.ts index 942851bf54..8d5362c699 100644 --- a/src/client-side-encryption/providers/utils.js +++ b/src/client-side-encryption/providers/utils.ts @@ -1,16 +1,18 @@ -import { MongoCryptKMSRequestNetworkTimeoutError } from '../errors'; import * as http from 'http'; +import { clearTimeout, setTimeout } from 'timers'; + +import { MongoCryptKMSRequestNetworkTimeoutError } from '../errors'; /** - * @param {URL | string} url - * @param {http.RequestOptions} options - * - * @returns { Promise<{ body: string, status: number }> } - * @ignore + * @internal */ -function get(url, options = {}) { +export function get( + url: URL | string, + options: http.RequestOptions = {} +): Promise<{ body: string; status: number | undefined }> { return new Promise((resolve, reject) => { - let timeoutId; + /* eslint-disable prefer-const */ + let timeoutId: NodeJS.Timeout; const request = http .get(url, options, response => { response.setEncoding('utf8'); @@ -33,5 +35,3 @@ function get(url, options = {}) { }, 10000); }); } - -module.exports = { get }; diff --git a/src/cmap/auth/mongodb_aws.ts b/src/cmap/auth/mongodb_aws.ts index 57e3a028ff..0001c08047 100644 --- a/src/cmap/auth/mongodb_aws.ts +++ b/src/cmap/auth/mongodb_aws.ts @@ -157,14 +157,6 @@ interface AWSTempCredentials { Expiration?: Date; } -/* @internal */ -export interface AWSCredentials { - accessKeyId?: string; - secretAccessKey?: string; - sessionToken?: string; - expiration?: Date; -} - async function makeTempCredentials(credentials: MongoCredentials): Promise { function makeMongoCredentialsFromAWSTemp(creds: AWSTempCredentials) { if (!creds.AccessKeyId || !creds.SecretAccessKey || !creds.Token) { diff --git a/src/deps.ts b/src/deps.ts index e79e3f7fb6..f3c8965685 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import type { Document } from './bson'; -import type { AWSCredentials } from './cmap/auth/mongodb_aws'; import type { ProxyOptions } from './cmap/connection'; import { MongoMissingDependencyError } from './error'; import type { MongoClient } from './mongo_client'; @@ -76,6 +75,18 @@ export function getZstdLibrary(): typeof ZStandard | { kModuleError: MongoMissin } } +/** + * @internal + * Copy of the AwsCredentialIdentityProvider interface from [`smithy/types`](https://socket.dev/npm/package/\@smithy/types/files/1.1.1/dist-types/identity/awsCredentialIdentity.d.ts), + * the return type of the aws-sdk's `fromNodeProviderChain().provider()`. + */ +export interface AWSCredentials { + accessKeyId: string; + secretAccessKey: string; + sessionToken: string; + expiration?: Date; +} + type CredentialProvider = { fromNodeProviderChain(this: void): () => Promise; }; @@ -97,6 +108,26 @@ export function getAwsCredentialProvider(): } } +/** @internal */ +export type GcpMetadata = + | typeof import('gcp-metadata') + | { kModuleError: MongoMissingDependencyError }; + +export function getGcpMetadata(): GcpMetadata { + try { + // Ensure you always wrap an optional require in the try block NODE-3199 + const credentialProvider = require('gcp-metadata'); + return credentialProvider; + } catch { + return makeErrorModule( + new MongoMissingDependencyError( + 'Optional module `gcp-metadata` not found.' + + ' Please install it to enable getting gcp credentials via the official sdk.' + ) + ); + } +} + /** @internal */ export type SnappyLib = { /** diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts index 86f2ac216a..c99820b6f8 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts @@ -3,11 +3,14 @@ import { expect } from 'chai'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { MongoCryptAzureKMSRequestError } from '../../../src/client-side-encryption/errors'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { fetchAzureKMSToken } from '../../../src/client-side-encryption/providers/azure'; +import { + type AzureKMSRequestOptions, + fetchAzureKMSToken +} from '../../../src/client-side-encryption/providers/azure'; import { type Document } from '../../mongodb'; const BASE_URL = new URL(`http://127.0.0.1:8080/metadata/identity/oauth2/token`); -class KMSRequestOptions { +class KMSRequestOptions implements AzureKMSRequestOptions { url: URL = BASE_URL; headers: Document; constructor(testCase?: 'empty-json' | 'bad-json' | '404' | '500' | 'slow') { diff --git a/test/unit/client-side-encryption/providers/credentialsProvider.test.js b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts similarity index 83% rename from test/unit/client-side-encryption/providers/credentialsProvider.test.js rename to test/unit/client-side-encryption/providers/credentialsProvider.test.ts index e23a7b06ca..f8f735033b 100644 --- a/test/unit/client-side-encryption/providers/credentialsProvider.test.js +++ b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts @@ -1,16 +1,26 @@ -'use strict'; - -const { expect } = require('chai'); -const http = require('http'); -const requirements = require('../requirements.helper'); -const { loadCredentials, isEmptyCredentials } = require('../../../../src/client-side-encryption/providers'); -const { tokenCache, fetchAzureKMSToken } = require('../../../../src/client-side-encryption/providers/azure'); -const sinon = require('sinon'); -const utils = require('../../../../src/client-side-encryption/providers/utils'); -const { - MongoCryptKMSRequestNetworkTimeoutError, - MongoCryptAzureKMSRequestError -} = require('../../../../src/client-side-encryption/errors'); +import { expect } from 'chai'; +import * as http from 'http'; +import * as sinon from 'sinon'; + +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { + MongoCryptAzureKMSRequestError, + MongoCryptKMSRequestNetworkTimeoutError +} from '../../../../src/client-side-encryption/errors'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { + isEmptyCredentials, + type KMSProviders, + loadCredentials +} from '../../../../src/client-side-encryption/providers'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { + fetchAzureKMSToken, + tokenCache +} from '../../../../src/client-side-encryption/providers/azure'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import * as utils from '../../../../src/client-side-encryption/providers/utils'; +import * as requirements from '../requirements.helper'; const originalAccessKeyId = process.env.AWS_ACCESS_KEY_ID; const originalSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; @@ -19,23 +29,28 @@ const originalSessionToken = process.env.AWS_SESSION_TOKEN; describe('#loadCredentials', function () { context('isEmptyCredentials()', () => { it('returns true for an empty object', () => { - expect(isEmptyCredentials('rainyCloud', { rainyCloud: {} })).to.be.true; + expect(isEmptyCredentials('aws', { aws: {} })).to.be.true; }); it('returns false for an object with keys', () => { - expect(isEmptyCredentials('rainyCloud', { rainyCloud: { password: 'secret' } })).to.be.false; + // @ts-expect-error Testing error conditions here + expect(isEmptyCredentials('aws', { aws: { password: 'secret' } })).to.be.false; }); it('returns false for an nullish credentials', () => { - expect(isEmptyCredentials('rainyCloud', { rainyCloud: null })).to.be.false; - expect(isEmptyCredentials('rainyCloud', { rainyCloud: undefined })).to.be.false; - expect(isEmptyCredentials('rainyCloud', {})).to.be.false; + // @ts-expect-error Testing error conditions here + expect(isEmptyCredentials('aws', { aws: null })).to.be.false; + expect(isEmptyCredentials('aws', { aws: undefined })).to.be.false; + expect(isEmptyCredentials('aws', {})).to.be.false; }); it('returns false for non object credentials', () => { - expect(isEmptyCredentials('rainyCloud', { rainyCloud: 0 })).to.be.false; - expect(isEmptyCredentials('rainyCloud', { rainyCloud: false })).to.be.false; - expect(isEmptyCredentials('rainyCloud', { rainyCloud: Symbol('secret') })).to.be.false; + // @ts-expect-error Testing error conditions here + expect(isEmptyCredentials('aws', { aws: 0 })).to.be.false; + // @ts-expect-error Testing error conditions here + expect(isEmptyCredentials('aws', { aws: false })).to.be.false; + // @ts-expect-error Testing error conditions here + expect(isEmptyCredentials('aws', { aws: Symbol('secret') })).to.be.false; }); }); @@ -63,8 +78,8 @@ describe('#loadCredentials', function () { before(function () { if (!requirements.credentialProvidersInstalled.aws) { - this.currentTest.skipReason = 'Cannot refresh credentials without sdk provider'; - this.currentTest.skip(); + this.currentTest!.skipReason = 'Cannot refresh credentials without sdk provider'; + this.currentTest!.skip(); return; } }); @@ -92,8 +107,8 @@ describe('#loadCredentials', function () { before(function () { if (!requirements.credentialProvidersInstalled.aws) { - this.currentTest.skipReason = 'Cannot refresh credentials without sdk provider'; - this.currentTest.skip(); + this.currentTest!.skipReason = 'Cannot refresh credentials without sdk provider'; + this.currentTest!.skip(); return; } }); @@ -114,19 +129,19 @@ describe('#loadCredentials', function () { }); context('when aws is not empty', function () { - const kmsProviders = { + const kmsProviders: KMSProviders = { local: { key: Buffer.alloc(96) }, aws: { accessKeyId: 'example' - } + } as any }; before(function () { if (!requirements.credentialProvidersInstalled.aws) { - this.currentTest.skipReason = 'Cannot refresh credentials without sdk provider'; - this.currentTest.skip(); + this.currentTest!.skipReason = 'Cannot refresh credentials without sdk provider'; + this.currentTest!.skip(); return; } }); @@ -149,8 +164,8 @@ describe('#loadCredentials', function () { before(function () { if (requirements.credentialProvidersInstalled.aws) { - this.currentTest.skipReason = 'Credentials will be loaded when sdk present'; - this.currentTest.skip(); + this.currentTest!.skipReason = 'Credentials will be loaded when sdk present'; + this.currentTest!.skip(); return; } }); @@ -195,8 +210,8 @@ describe('#loadCredentials', function () { context('and gcp-metadata is installed', () => { beforeEach(function () { if (!requirements.credentialProvidersInstalled.gcp) { - this.currentTest.skipReason = 'Tests require gcp-metadata to be installed'; - this.currentTest.skip(); + this.currentTest!.skipReason = 'Tests require gcp-metadata to be installed'; + this.currentTest!.skip(); return; } }); @@ -233,8 +248,8 @@ describe('#loadCredentials', function () { context('and gcp-metadata is not installed', () => { beforeEach(function () { if (requirements.credentialProvidersInstalled.gcp) { - this.currentTest.skipReason = 'Tests require gcp-metadata to be installed'; - this.currentTest.skip(); + this.currentTest!.skipReason = 'Tests require gcp-metadata to be installed'; + this.currentTest!.skip(); return; } }); @@ -261,7 +276,7 @@ describe('#loadCredentials', function () { }); context('when there is no cached token', () => { - let mockToken = { + const mockToken = { accessToken: 'mock token', expiresOnTimestamp: Date.now() }; @@ -269,7 +284,7 @@ describe('#loadCredentials', function () { let token; beforeEach(async () => { - sinon.stub(cache, '_getToken').returns(mockToken); + sinon.stub(cache, '_getToken').resolves(mockToken); token = await cache.getToken(); }); it('fetches a token', async () => { @@ -282,7 +297,7 @@ describe('#loadCredentials', function () { context('when there is a cached token', () => { context('when the cached token expires <= 1 minute from the current time', () => { - let mockToken = { + const mockToken = { accessToken: 'mock token', expiresOnTimestamp: Date.now() }; @@ -294,7 +309,7 @@ describe('#loadCredentials', function () { accessToken: 'a new key', expiresOnTimestamp: Date.now() + 3000 }; - sinon.stub(cache, '_getToken').returns(mockToken); + sinon.stub(cache, '_getToken').resolves(mockToken); token = await cache.getToken(); }); @@ -307,12 +322,12 @@ describe('#loadCredentials', function () { }); context('when the cached token expires > 1 minute from the current time', () => { - let expiredToken = { + const expiredToken = { token: 'mock token', expiresOnTimestamp: Date.now() }; - let expectedMockToken = { + const expectedMockToken = { accessToken: 'a new key', expiresOnTimestamp: Date.now() + 10000 }; @@ -320,8 +335,8 @@ describe('#loadCredentials', function () { let token; beforeEach(async () => { - cache.cachedToken = expiredToken; - sinon.stub(cache, '_getToken').returns(expectedMockToken); + cache.cachedToken = expiredToken as any; + sinon.stub(cache, '_getToken').resolves(expectedMockToken); token = await cache.getToken(); }); it('returns the cached token', () => { @@ -420,7 +435,9 @@ describe('#loadCredentials', function () { afterEach(() => sinon.restore()); context('when the request times out', () => { before(() => { - sinon.stub(utils, 'get').rejects(new MongoCryptKMSRequestNetworkTimeoutError()); + sinon + .stub(utils, 'get') + .rejects(new MongoCryptKMSRequestNetworkTimeoutError('request timed out')); }); it('throws a MongoCryptKMSRequestError', async () => { @@ -432,7 +449,7 @@ describe('#loadCredentials', function () { context('when the request returns a non-200 error', () => { context('when the request has no body', () => { before(() => { - sinon.stub(utils, 'get').resolves({ status: 400 }); + sinon.stub(utils, 'get').resolves({ status: 400 } as any); }); it('throws a MongoCryptKMSRequestError', async () => { @@ -476,7 +493,7 @@ describe('#loadCredentials', function () { context('when the request returns a 200 response', () => { context('when the request has no body', () => { before(() => { - sinon.stub(utils, 'get').resolves({ status: 200 }); + sinon.stub(utils, 'get').resolves({ status: 200 } as any); }); it('throws a MongoCryptKMSRequestError', async () => { @@ -547,7 +564,8 @@ describe('#loadCredentials', function () { it('returns the token in the `azure` field of the kms providers', async () => { const kmsProviders = await loadCredentials({ azure: {} }); - expect(kmsProviders).to.have.property('azure').to.deep.equal({ accessToken: 'token' }); + const azure = kmsProviders.azure; + expect(azure).to.have.property('accessToken', 'token'); }); }); }); diff --git a/test/unit/client-side-encryption/requirements.helper.js b/test/unit/client-side-encryption/requirements.helper.js deleted file mode 100644 index a456c49197..0000000000 --- a/test/unit/client-side-encryption/requirements.helper.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -// Data Key Stuff -const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; -const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; -const AWS_REGION = process.env.AWS_REGION; -const AWS_CMK_ID = process.env.AWS_CMK_ID; - -const awsKmsProviders = { - aws: { accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY } -}; -const awsDataKeyOptions = { masterKey: { key: AWS_CMK_ID, region: AWS_REGION } }; - -const SKIP_AWS_TESTS = [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_CMK_ID].some(secret => !secret); - -function isAWSCredentialProviderInstalled() { - try { - require.resolve('@aws-sdk/credential-providers'); - return true; - } catch { - return false; - } -} - -function isGCPCredentialProviderInstalled() { - try { - require.resolve('gcp-metadata'); - return true; - } catch { - return false; - } -} - -module.exports = { - SKIP_AWS_TESTS, - KEYS: { - AWS_ACCESS_KEY_ID, - AWS_SECRET_ACCESS_KEY, - AWS_REGION, - AWS_CMK_ID - }, - awsKmsProviders, - awsDataKeyOptions, - credentialProvidersInstalled: { - aws: isAWSCredentialProviderInstalled(), - gcp: isGCPCredentialProviderInstalled() - } -}; diff --git a/test/unit/client-side-encryption/requirements.helper.ts b/test/unit/client-side-encryption/requirements.helper.ts new file mode 100644 index 0000000000..68e6ecfce7 --- /dev/null +++ b/test/unit/client-side-encryption/requirements.helper.ts @@ -0,0 +1,33 @@ +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { getAwsCredentialProvider, getGcpMetadata } from '../../../src/deps'; + +// Data Key Stuff +export const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; +export const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; +export const AWS_REGION = process.env.AWS_REGION; +export const AWS_CMK_ID = process.env.AWS_CMK_ID; + +export const awsKmsProviders = { + aws: { accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY } +}; +export const awsDataKeyOptions = { masterKey: { key: AWS_CMK_ID, region: AWS_REGION } }; + +export const SKIP_AWS_TESTS = [ + AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY, + AWS_REGION, + AWS_CMK_ID +].some(secret => !secret); + +export function isAWSCredentialProviderInstalled() { + return !('kModuleError' in getAwsCredentialProvider()); +} + +export function isGCPCredentialProviderInstalled() { + return !('kModuleError' in getGcpMetadata()); +} + +export const credentialProvidersInstalled = { + aws: isAWSCredentialProviderInstalled(), + gcp: isGCPCredentialProviderInstalled() +}; diff --git a/tsconfig.json b/tsconfig.json index c8b6b54e07..0d08d12998 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,8 @@ "moduleResolution": "node", "skipLibCheck": true, "lib": [ - "es2021" + "es2021", + "ES2022.Error" ], // We don't make use of tslib helpers, all syntax used is supported by target engine "importHelpers": false,