From 184e3f1ab036059acbcbc93674be32ba756072cf Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 8 Oct 2024 19:51:24 +0200 Subject: [PATCH] feat(NODE-6069): OIDC k8s machine workflow --- .evergreen/config.in.yml | 52 ++++++++++++++++ .evergreen/config.yml | 59 +++++++++++++++++++ .evergreen/generate_evergreen_tasks.js | 3 +- .evergreen/run-oidc-prose-tests.sh | 8 ++- .evergreen/run-oidc-tests-k8s.sh | 12 ++++ package.json | 1 + src/cmap/auth/mongo_credentials.ts | 5 +- src/cmap/auth/mongodb_oidc.ts | 4 +- .../auth/mongodb_oidc/k8s_machine_workflow.ts | 38 ++++++++++++ .../auth/mongodb_oidc_k8s.prose.07.test.ts | 37 ++++++++++++ test/spec/auth/legacy/connection-string.json | 20 +++++++ test/spec/auth/legacy/connection-string.yml | 15 +++++ 12 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 .evergreen/run-oidc-tests-k8s.sh create mode 100644 src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts create mode 100644 test/integration/auth/mongodb_oidc_k8s.prose.07.test.ts diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index ccbc4e18c7..6f7bd5a9ff 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -1265,6 +1265,36 @@ tasks: args: - src/.evergreen/run-azure-kms-tests.sh + - name: "oidc-auth-test-k8s-latest" + commands: + - func: "install dependencies" + - command: subprocess.exec + type: test + params: + working_dir: src + binary: bash + env: + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + ENVIRONMENT: k8s + K8S_VARIANT: eks + SCRIPT: run-oidc-prose-tests.sh + include_expansions_in_env: ["DRIVERS_TOOLS", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"] + args: + - .evergreen/run-oidc-tests-k8s.sh + - command: subprocess.exec + type: test + params: + working_dir: src + binary: bash + env: + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + ENVIRONMENT: k8s + K8S_VARIANT: gke + SCRIPT: run-oidc-prose-tests.sh + include_expansions_in_env: ["DRIVERS_TOOLS", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"] + args: + - .evergreen/run-oidc-tests-k8s.sh + - name: "oidc-auth-test-azure-latest" commands: - func: "install dependencies" @@ -1487,6 +1517,28 @@ task_groups: tasks: - test-azurekms-task + - name: testk8soidc_task_group + setup_group: + - func: fetch source + - command: ec2.assume_role + params: + role_arn: ${OIDC_AWS_ROLE_ARN} + - command: subprocess.exec + params: + binary: bash + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/k8s/setup.sh + teardown_task: + - command: subprocess.exec + params: + binary: bash + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/k8s/teardown.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-k8s-latest + - name: testtestoidc_task_group setup_group: - func: fetch source diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 8c3ba94ed4..af8bbe4357 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -1217,6 +1217,43 @@ tasks: EXPECTED_AZUREKMS_OUTCOME: failure args: - src/.evergreen/run-azure-kms-tests.sh + - name: oidc-auth-test-k8s-latest + commands: + - func: install dependencies + - command: subprocess.exec + type: test + params: + working_dir: src + binary: bash + env: + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + ENVIRONMENT: k8s + K8S_VARIANT: eks + SCRIPT: run-oidc-prose-tests.sh + include_expansions_in_env: + - DRIVERS_TOOLS + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - AWS_SESSION_TOKEN + args: + - .evergreen/run-oidc-tests-k8s.sh + - command: subprocess.exec + type: test + params: + working_dir: src + binary: bash + env: + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + ENVIRONMENT: k8s + K8S_VARIANT: gke + SCRIPT: run-oidc-prose-tests.sh + include_expansions_in_env: + - DRIVERS_TOOLS + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - AWS_SESSION_TOKEN + args: + - .evergreen/run-oidc-tests-k8s.sh - name: oidc-auth-test-azure-latest commands: - func: install dependencies @@ -4422,6 +4459,27 @@ task_groups: - ${DRIVERS_TOOLS}/.evergreen/csfle/azurekms/teardown.sh tasks: - test-azurekms-task + - name: testk8soidc_task_group + setup_group: + - func: fetch source + - command: ec2.assume_role + params: + role_arn: ${OIDC_AWS_ROLE_ARN} + - command: subprocess.exec + params: + binary: bash + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/k8s/setup.sh + teardown_task: + - command: subprocess.exec + params: + binary: bash + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/k8s/teardown.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-k8s-latest - name: testtestoidc_task_group setup_group: - func: fetch source @@ -5114,6 +5172,7 @@ buildvariants: - testtestoidc_task_group - testazureoidc_task_group - testgcpoidc_task_group + - testk8soidc_task_group - name: rhel8-test-atlas display_name: Atlas Cluster Tests run_on: rhel80-large diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index 13b5deec49..3b9f328e8f 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -733,7 +733,8 @@ BUILD_VARIANTS.push({ tasks: [ 'testtestoidc_task_group', 'testazureoidc_task_group', - 'testgcpoidc_task_group' + 'testgcpoidc_task_group', + 'testk8soidc_task_group' ] }); diff --git a/.evergreen/run-oidc-prose-tests.sh b/.evergreen/run-oidc-prose-tests.sh index 1f19612979..0be8b1e0dc 100755 --- a/.evergreen/run-oidc-prose-tests.sh +++ b/.evergreen/run-oidc-prose-tests.sh @@ -19,10 +19,16 @@ if [ "$ENVIRONMENT" = "azure" ]; then npm run check:oidc-azure elif [ "$ENVIRONMENT" = "gcp" ]; then npm run check:oidc-gcp -else +elif [ "$ENVIRONMENT" = "test" ]; then if [ -z "${OIDC_TOKEN_FILE}" ]; then echo "Must specify OIDC_TOKEN_FILE" exit 1 fi npm run check:oidc-test +else + if [ -z "${K8S_VARIANT}" ]; then + echo "Must specify K8S_VARIANT" + exit 1 + fi + npm run check:oidc-k8s fi diff --git a/.evergreen/run-oidc-tests-k8s.sh b/.evergreen/run-oidc-tests-k8s.sh new file mode 100644 index 0000000000..18f4a6014c --- /dev/null +++ b/.evergreen/run-oidc-tests-k8s.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -o xtrace # Write all commands first to stderr +set -o errexit # Exit the script with error if any of the commands fail + +export K8S_DRIVERS_TAR_FILE=/tmp/node-mongodb-native.tgz +tar czf $K8S_DRIVERS_TAR_FILE . +bash $DRIVERS_TOOLS/.evergreen/auth_oidc/k8s/setup-pod.sh +bash $DRIVERS_TOOLS/.evergreen/auth_oidc/k8s/run-self-test.sh +export K8S_TEST_CMD="ENVIRONMENT=k8s ./.evergreen/${SCRIPT}" +source $DRIVERS_TOOLS/.evergreen/auth_oidc/k8s/secrets-export.sh +bash $DRIVERS_TOOLS/.evergreen/auth_oidc/k8s/run-driver-test.sh +bash $DRIVERS_TOOLS/.evergreen/auth_oidc/k8s/teardown-pod.sh \ No newline at end of file diff --git a/package.json b/package.json index d8b9f5945b..ba205a06fd 100644 --- a/package.json +++ b/package.json @@ -151,6 +151,7 @@ "check:oidc-test": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc.prose.test.ts", "check:oidc-azure": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc_azure.prose.05.test.ts", "check:oidc-gcp": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc_gcp.prose.06.test.ts", + "check:oidc-k8s": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc_k8s.prose.07.test.ts", "check:ocsp": "mocha --config test/manual/mocharc.json test/manual/ocsp_support.test.js", "check:kerberos": "nyc mocha --config test/manual/mocharc.json test/manual/kerberos.test.ts", "check:tls": "mocha --config test/manual/mocharc.json test/manual/tls_support.test.ts", diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index 00cbfc6b00..97c457945d 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -31,7 +31,8 @@ function getDefaultAuthMechanism(hello: Document | null): AuthMechanism { const ALLOWED_ENVIRONMENT_NAMES: AuthMechanismProperties['ENVIRONMENT'][] = [ 'test', 'azure', - 'gcp' + 'gcp', + 'k8s' ]; const ALLOWED_HOSTS_ERROR = 'Auth mechanism property ALLOWED_HOSTS must be an array of strings.'; @@ -62,7 +63,7 @@ export interface AuthMechanismProperties extends Document { /** A user provided OIDC human interacted callback function. */ OIDC_HUMAN_CALLBACK?: OIDCCallbackFunction; /** The OIDC environment. Note that 'test' is for internal use only. */ - ENVIRONMENT?: 'test' | 'azure' | 'gcp'; + ENVIRONMENT?: 'test' | 'azure' | 'gcp' | 'k8s'; /** Allowed hosts that OIDC auth can connect to. */ ALLOWED_HOSTS?: string[]; /** The resource token for OIDC auth in Azure and GCP. */ diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index d4b2a843ae..1cd249aac2 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -6,6 +6,7 @@ import { type AuthContext, AuthProvider } from './auth_provider'; import type { MongoCredentials } from './mongo_credentials'; import { AzureMachineWorkflow } from './mongodb_oidc/azure_machine_workflow'; import { GCPMachineWorkflow } from './mongodb_oidc/gcp_machine_workflow'; +import { K8SMachineWorkflow } from './mongodb_oidc/k8s_machine_workflow'; import { TokenCache } from './mongodb_oidc/token_cache'; import { TokenMachineWorkflow } from './mongodb_oidc/token_machine_workflow'; @@ -88,7 +89,7 @@ export type OIDCCallbackFunction = (params: OIDCCallbackParams) => Promise Workflow> = new Map(); OIDC_WORKFLOWS.set('test', () => new TokenMachineWorkflow(new TokenCache())); OIDC_WORKFLOWS.set('azure', () => new AzureMachineWorkflow(new TokenCache())); OIDC_WORKFLOWS.set('gcp', () => new GCPMachineWorkflow(new TokenCache())); +OIDC_WORKFLOWS.set('k8s', () => new K8SMachineWorkflow(new TokenCache())); /** * OIDC auth provider. diff --git a/src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts new file mode 100644 index 0000000000..22dc9cb9f6 --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts @@ -0,0 +1,38 @@ +import { readFile } from 'fs/promises'; + +import { type AccessToken, MachineWorkflow } from './machine_workflow'; +import { type TokenCache } from './token_cache'; + +/** The fallback file name */ +const FALLBACK_FILENAME = '/var/run/secrets/kubernetes.io/serviceaccount/token'; + +/** The azure environment variable for the file name. */ +const AZURE_FILENAME = 'AZURE_FEDERATED_TOKEN_FILE'; + +/** The AWS environment variable for the file name. */ +const AWS_FILENAME = 'AWS_WEB_IDENTITY_TOKEN_FILE'; + +export class K8SMachineWorkflow extends MachineWorkflow { + /** + * Instantiate the machine workflow. + */ + constructor(cache: TokenCache) { + super(cache); + } + + /** + * Get the token from the environment. + */ + async getToken(): Promise { + let filename: string; + if (process.env[AZURE_FILENAME]) { + filename = process.env[AZURE_FILENAME]; + } else if (process.env[AWS_FILENAME]) { + filename = process.env[AWS_FILENAME]; + } else { + filename = FALLBACK_FILENAME; + } + const token = await readFile(filename, 'utf8'); + return { access_token: token }; + } +} diff --git a/test/integration/auth/mongodb_oidc_k8s.prose.07.test.ts b/test/integration/auth/mongodb_oidc_k8s.prose.07.test.ts new file mode 100644 index 0000000000..57de3d30ce --- /dev/null +++ b/test/integration/auth/mongodb_oidc_k8s.prose.07.test.ts @@ -0,0 +1,37 @@ +import { expect } from 'chai'; + +import { type Collection, MongoClient } from '../../mongodb'; + +const DEFAULT_URI = 'mongodb://127.0.0.1:27017'; + +describe('OIDC Auth Spec K8s Tests', function () { + // Note there is no spec or tests for GCP yet, these are 2 scenarios based on the + // drivers tools scripts available. + describe('6. GCP Tests', function () { + let client: MongoClient; + let collection: Collection; + + beforeEach(function () { + if (!this.configuration.isOIDC(process.env.MONGODB_URI_SINGLE, 'k8s')) { + this.skipReason = 'K8s OIDC prose tests require a K8s OIDC environment.'; + this.skip(); + } + }); + + afterEach(async function () { + await client?.close(); + }); + + describe('7.1 K8s With Environment Set', function () { + beforeEach(function () { + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI); + collection = client.db('test').collection('test'); + }); + + it('successfully authenticates', async function () { + const result = await collection.findOne(); + expect(result).to.not.be.null; + }); + }); + }); +}); diff --git a/test/spec/auth/legacy/connection-string.json b/test/spec/auth/legacy/connection-string.json index 67aafbff6e..3a099c8137 100644 --- a/test/spec/auth/legacy/connection-string.json +++ b/test/spec/auth/legacy/connection-string.json @@ -626,6 +626,26 @@ "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp", "valid": false, "credential": null + }, + { + "description": "should recognise the mechanism with k8s provider (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:k8s", + "valid": true, + "credential": { + "username": null, + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "k8s" + } + } + }, + { + "description": "should throw an error for a username and password with k8s provider (MONGODB-OIDC)", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:k8s", + "valid": false, + "credential": null } ] } diff --git a/test/spec/auth/legacy/connection-string.yml b/test/spec/auth/legacy/connection-string.yml index ded258f29d..6b82ef4225 100644 --- a/test/spec/auth/legacy/connection-string.yml +++ b/test/spec/auth/legacy/connection-string.yml @@ -454,3 +454,18 @@ tests: uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp valid: false credential: null +- description: should recognise the mechanism with k8s provider (MONGODB-OIDC) + uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:k8s + valid: true + credential: + username: null + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: k8s +- description: should throw an error for a username and password with k8s provider + (MONGODB-OIDC) + uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:k8s + valid: false + credential: null \ No newline at end of file