diff --git a/.evergreen/config.yml b/.evergreen/config.yml index a1587a281d..d2bfd4c920 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -520,6 +520,18 @@ functions: args: - .evergreen/run-mongodb-oidc-test.sh + "run oidc k8s auth test": + - command: subprocess.exec + type: test + params: + binary: bash + working_dir: src + env: + OIDC_ENV: k8s + include_expansions_in_env: ["DRIVERS_TOOLS", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "K8S_VARIANT"] + args: + - ${PROJECT_DIRECTORY}/.evergreen/run-mongodb-oidc-remote-test.sh + "run aws auth test with aws credentials as environment variables": - command: shell.exec type: test @@ -873,6 +885,32 @@ task_groups: tasks: - oidc-auth-test-gcp + - name: testk8soidc_task_group + setup_group: + - func: fetch source + - func: prepare resources + - func: fix absolute paths + - func: make files executable + - command: ec2.assume_role + params: + role_arn: ${aws_test_secrets_role} + duration_seconds: 1800 + - 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 + - name: testoidc_task_group setup_group: - func: fetch source @@ -1557,40 +1595,41 @@ tasks: - name: "oidc-auth-test-azure" commands: - - command: shell.exec + - command: subprocess.exec type: test params: - shell: bash - script: |- - set -o errexit - . src/.evergreen/scripts/env.sh - cd src - git add . - git commit -m "add files" - export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-python-driver.tgz - git archive -o $AZUREOIDC_DRIVERS_TAR_FILE HEAD - export AZUREOIDC_TEST_CMD="OIDC_ENV=azure ./.evergreen/run-mongodb-oidc-test.sh" - bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh + binary: bash + working_dir: src + env: + OIDC_ENV: azure + include_expansions_in_env: ["DRIVERS_TOOLS"] + args: + - ${PROJECT_DIRECTORY}/.evergreen/run-mongodb-oidc-remote-test.sh - name: "oidc-auth-test-gcp" commands: - - command: shell.exec + - command: subprocess.exec type: test params: - shell: bash - script: |- - set -o errexit - . src/.evergreen/scripts/env.sh - cd src - git add . - git commit -m "add files" - export GCPOIDC_DRIVERS_TAR_FILE=/tmp/mongo-python-driver.tgz - git archive -o $GCPOIDC_DRIVERS_TAR_FILE HEAD - # Define the command to run on the VM. - # Ensure that we source the environment file created for us, set up any other variables we need, - # and then run our test suite on the vm. - export GCPOIDC_TEST_CMD="OIDC_ENV=gcp ./.evergreen/run-mongodb-oidc-test.sh" - bash $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/run-driver-test.sh + binary: bash + working_dir: src + env: + OIDC_ENV: gcp + include_expansions_in_env: ["DRIVERS_TOOLS"] + args: + - ${PROJECT_DIRECTORY}/.evergreen/run-mongodb-oidc-remote-test.sh + + - name: "oidc-auth-test-k8s" + commands: + - func: "run oidc k8s auth test" + vars: + K8S_VARIANT: eks + - func: "run oidc k8s auth test" + vars: + K8S_VARIANT: gke + - func: "run oidc k8s auth test" + vars: + K8S_VARIANT: aks # }}} - name: "coverage-report" tags: ["coverage"] @@ -1749,20 +1788,6 @@ buildvariants: tasks: - name: "coverage-report" -- name: testazureoidc-variant - display_name: "OIDC Auth Azure" - run_on: ubuntu2204-small - tasks: - - name: testazureoidc_task_group - batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README - -- name: testgcpoidc-variant - display_name: "OIDC Auth GCP" - run_on: ubuntu2204-small - tasks: - - name: testgcpoidc_task_group - batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README - - name: testgcpkms-variant display_name: "GCP KMS" run_on: diff --git a/.evergreen/generated_configs/variants.yml b/.evergreen/generated_configs/variants.yml index 327becc249..944bfdae6d 100644 --- a/.evergreen/generated_configs/variants.yml +++ b/.evergreen/generated_configs/variants.yml @@ -1096,12 +1096,15 @@ buildvariants: VERSION: "8.0" # Oidc auth tests - - name: oidc-auth-rhel8 + - name: oidc-auth-ubuntu-22 tasks: - name: testoidc_task_group - display_name: OIDC Auth RHEL8 + - name: testazureoidc_task_group + - name: testgcpoidc_task_group + - name: testk8soidc_task_group + display_name: OIDC Auth Ubuntu-22 run_on: - - rhel87-small + - ubuntu2204-small batchtime: 20160 - name: oidc-auth-macos tasks: diff --git a/.evergreen/run-mongodb-oidc-remote-test.sh b/.evergreen/run-mongodb-oidc-remote-test.sh new file mode 100755 index 0000000000..bb90bddf07 --- /dev/null +++ b/.evergreen/run-mongodb-oidc-remote-test.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +set +x # Disable debug trace +set -eu + +echo "Running MONGODB-OIDC remote tests" + +OIDC_ENV=${OIDC_ENV:-"test"} + +# Make sure DRIVERS_TOOLS is set. +if [ -z "$DRIVERS_TOOLS" ]; then + echo "Must specify DRIVERS_TOOLS" + exit 1 +fi + +# Set up the remote files to test. +git add . +git commit -m "add files" || true +export TEST_TAR_FILE=/tmp/mongo-python-driver.tgz +git archive -o $TEST_TAR_FILE HEAD + +pushd $DRIVERS_TOOLS + +if [ $OIDC_ENV == "test" ]; then + echo "Test OIDC environment does not support remote test!" + exit 1 + +elif [ $OIDC_ENV == "azure" ]; then + export AZUREOIDC_DRIVERS_TAR_FILE=$TEST_TAR_FILE + export AZUREOIDC_TEST_CMD="OIDC_ENV=azure ./.evergreen/run-mongodb-oidc-test.sh" + bash ./.evergreen/auth_oidc/azure/run-driver-test.sh + +elif [ $OIDC_ENV == "gcp" ]; then + export GCPOIDC_DRIVERS_TAR_FILE=$TEST_TAR_FILE + export GCPOIDC_TEST_CMD="OIDC_ENV=gcp ./.evergreen/run-mongodb-oidc-test.sh" + bash ./.evergreen/auth_oidc/gcp/run-driver-test.sh + +elif [ $OIDC_ENV == "k8s" ]; then + # Make sure K8S_VARIANT is set. + if [ -z "$K8S_VARIANT" ]; then + echo "Must specify K8S_VARIANT" + popd + exit 1 + fi + + bash ./.evergreen/auth_oidc/k8s/setup-pod.sh + bash ./.evergreen/auth_oidc/k8s/run-self-test.sh + export K8S_DRIVERS_TAR_FILE=$TEST_TAR_FILE + export K8S_TEST_CMD="OIDC_ENV=k8s ./.evergreen/run-mongodb-oidc-test.sh" + source ./.evergreen/auth_oidc/k8s/secrets-export.sh # for MONGODB_URI + bash ./.evergreen/auth_oidc/k8s/run-driver-test.sh + bash ./.evergreen/auth_oidc/k8s/teardown-pod.sh + +else + echo "Unrecognized OIDC_ENV $OIDC_ENV" + pod + exit 1 +fi + +popd diff --git a/.evergreen/run-mongodb-oidc-test.sh b/.evergreen/run-mongodb-oidc-test.sh index 0c34912c8a..22864528c0 100755 --- a/.evergreen/run-mongodb-oidc-test.sh +++ b/.evergreen/run-mongodb-oidc-test.sh @@ -21,6 +21,9 @@ elif [ $OIDC_ENV == "azure" ]; then elif [ $OIDC_ENV == "gcp" ]; then source ./secrets-export.sh +elif [ $OIDC_ENV == "k8s" ]; then + echo "Running oidc on k8s" + else echo "Unrecognized OIDC_ENV $OIDC_ENV" exit 1 diff --git a/.evergreen/scripts/generate_config.py b/.evergreen/scripts/generate_config.py index b8b8fa367c..9797ef1937 100644 --- a/.evergreen/scripts/generate_config.py +++ b/.evergreen/scripts/generate_config.py @@ -619,10 +619,14 @@ def create_serverless_variants(): def create_oidc_auth_variants(): variants = [] - for host in ["rhel8", "macos", "win64"]: + other_tasks = ["testazureoidc_task_group", "testgcpoidc_task_group", "testk8soidc_task_group"] + for host in ["ubuntu22", "macos", "win64"]: + tasks = ["testoidc_task_group"] + if host == "ubuntu22": + tasks += other_tasks variants.append( create_variant( - ["testoidc_task_group"], + tasks, get_display_name("OIDC Auth", host), host=host, batchtime=BATCHTIME_WEEK * 2, diff --git a/pymongo/auth_oidc_shared.py b/pymongo/auth_oidc_shared.py index 5e3603fa31..9e0acaf6c8 100644 --- a/pymongo/auth_oidc_shared.py +++ b/pymongo/auth_oidc_shared.py @@ -116,3 +116,17 @@ def __init__(self, token_resource: str) -> None: def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult: resp = _get_gcp_response(self.token_resource, context.timeout_seconds) return OIDCCallbackResult(access_token=resp["access_token"]) + + +class _OIDCK8SCallback(OIDCCallback): + def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult: + return OIDCCallbackResult(access_token=_get_k8s_token()) + + +def _get_k8s_token() -> str: + fname = "/var/run/secrets/kubernetes.io/serviceaccount/token" + for key in ["AZURE_FEDERATED_TOKEN_FILE", "AWS_WEB_IDENTITY_TOKEN_FILE"]: + if key in os.environ: + fname = os.environ[key] + with open(fname) as fid: + return fid.read() diff --git a/pymongo/auth_shared.py b/pymongo/auth_shared.py index 7e3acd9dfb..f454a2704a 100644 --- a/pymongo/auth_shared.py +++ b/pymongo/auth_shared.py @@ -26,6 +26,7 @@ from pymongo.auth_oidc_shared import ( _OIDCAzureCallback, _OIDCGCPCallback, + _OIDCK8SCallback, _OIDCProperties, _OIDCTestCallback, ) @@ -180,6 +181,9 @@ def _build_credentials_tuple( "GCP provider for MONGODB-OIDC requires a TOKEN_RESOURCE auth mechanism property" ) callback = _OIDCGCPCallback(token_resource) + elif environ == "k8s": + passwd = None + callback = _OIDCK8SCallback() else: raise ConfigurationError(f"unrecognized ENVIRONMENT for MONGODB-OIDC: {environ}") else: diff --git a/test/auth/legacy/connection-string.json b/test/auth/legacy/connection-string.json index 57fd9d4a11..61f9e548c8 100644 --- a/test/auth/legacy/connection-string.json +++ b/test/auth/legacy/connection-string.json @@ -625,6 +625,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 } ] } \ No newline at end of file diff --git a/test/auth_oidc/test_auth_oidc.py b/test/auth_oidc/test_auth_oidc.py index 6526391daf..a0127304c1 100644 --- a/test/auth_oidc/test_auth_oidc.py +++ b/test/auth_oidc/test_auth_oidc.py @@ -37,6 +37,7 @@ from pymongo import MongoClient from pymongo._azure_helpers import _get_azure_response from pymongo._gcp_helpers import _get_gcp_response +from pymongo.auth_oidc_shared import _get_k8s_token from pymongo.cursor_shared import CursorType from pymongo.errors import AutoReconnect, ConfigurationError, OperationFailure from pymongo.hello import HelloCompat @@ -84,6 +85,10 @@ def get_token(self, username=None): opts = parse_uri(self.uri_single)["options"] token_aud = opts["authmechanismproperties"]["TOKEN_RESOURCE"] return _get_gcp_response(token_aud, username)["access_token"] + elif ENVIRON == "k8s": + return _get_k8s_token() + else: + raise ValueError(f"Unknown ENVIRON: {ENVIRON}") @contextmanager def fail_point(self, command_args): @@ -758,7 +763,9 @@ def create_client(self, *args, **kwargs): kwargs["retryReads"] = False if not len(args): args = [self.uri_single] - return MongoClient(*args, authmechanismproperties=props, **kwargs) + client = MongoClient(*args, authmechanismproperties=props, **kwargs) + self.addCleanup(client.close) + return client def test_1_1_callback_is_called_during_reauthentication(self): # Create a ``MongoClient`` configured with a custom OIDC callback that @@ -768,8 +775,6 @@ def test_1_1_callback_is_called_during_reauthentication(self): client.test.test.find_one() # Assert that the callback was called 1 time. self.assertEqual(self.request_called, 1) - # Close the client. - client.close() def test_1_2_callback_is_called_once_for_multiple_connections(self): # Create a ``MongoClient`` configured with a custom OIDC callback that @@ -790,8 +795,6 @@ def target(): thread.join() # Assert that the callback was called 1 time. self.assertEqual(self.request_called, 1) - # Close the client. - client.close() def test_2_1_valid_callback_inputs(self): # Create a MongoClient configured with an OIDC callback that validates its inputs and returns a valid access token. @@ -800,8 +803,6 @@ def test_2_1_valid_callback_inputs(self): client.test.test.find_one() # Assert that the OIDC callback was called with the appropriate inputs, including the timeout parameter if possible. Ensure that there are no unexpected fields. self.assertEqual(self.request_called, 1) - # Close the client. - client.close() def test_2_2_oidc_callback_returns_null(self): # Create a MongoClient configured with an OIDC callback that returns null. @@ -813,8 +814,6 @@ def fetch(self, a): # Perform a find operation that fails. with self.assertRaises(ValueError): client.test.test.find_one() - # Close the client. - client.close() def test_2_3_oidc_callback_returns_missing_data(self): # Create a MongoClient configured with an OIDC callback that returns data not conforming to the OIDCCredential with missing fields. @@ -829,8 +828,6 @@ def fetch(self, a): # Perform a find operation that fails. with self.assertRaises(ValueError): client.test.test.find_one() - # Close the client. - client.close() def test_2_4_invalid_client_configuration_with_callback(self): # Create a MongoClient configured with an OIDC callback and auth mechanism property ENVIRONMENT:test. @@ -870,8 +867,6 @@ def test_3_1_authentication_failure_with_cached_tokens_fetch_a_new_token_and_ret client.test.test.find_one() # Verify that the callback was called 1 time. self.assertEqual(self.request_called, 1) - # Close the client. - client.close() def test_3_2_authentication_failures_without_cached_tokens_returns_an_error(self): # Create a MongoClient configured with retryReads=false and an OIDC callback that always returns invalid access tokens. @@ -889,8 +884,6 @@ def fetch(self, a): client.test.test.find_one() # Verify that the callback was called 1 time. self.assertEqual(callback.count, 1) - # Close the client. - client.close() def test_3_3_unexpected_error_code_does_not_clear_cache(self): # Create a ``MongoClient`` with a human callback that returns a valid token @@ -916,9 +909,6 @@ def test_3_3_unexpected_error_code_does_not_clear_cache(self): # Assert that the callback has been called once. self.assertEqual(self.request_called, 1) - # Close the client. - client.close() - def test_4_1_reauthentication_succeds(self): # Create a ``MongoClient`` configured with a custom OIDC callback that # implements the provider logic. @@ -938,9 +928,6 @@ def test_4_1_reauthentication_succeds(self): # handshake, and again during reauthentication). self.assertEqual(self.request_called, 2) - # Close the client. - client.close() - def test_4_2_read_commands_fail_if_reauthentication_fails(self): # Create a ``MongoClient`` whose OIDC callback returns one good token and then # bad tokens after the first call. @@ -977,9 +964,6 @@ def fetch(self, _): # Verify that the callback was called 2 times. self.assertEqual(callback.count, 2) - # Close the client. - client.close() - def test_4_3_write_commands_fail_if_reauthentication_fails(self): # Create a ``MongoClient`` whose OIDC callback returns one good token and then # bad token after the first call. @@ -1016,12 +1000,9 @@ def fetch(self, _): # Verify that the callback was called 2 times. self.assertEqual(callback.count, 2) - # Close the client. - client.close() - def test_4_4_speculative_authentication_should_be_ignored_on_reauthentication(self): # Create an OIDC configured client that can listen for `SaslStart` commands. - listener = OvertCommandListener() + listener = EventListener() client = self.create_client(event_listeners=[listener]) # Preload the *Client Cache* with a valid access token to enforce Speculative Authentication. @@ -1061,9 +1042,6 @@ def test_4_4_speculative_authentication_should_be_ignored_on_reauthentication(se # Assert there were `SaslStart` commands executed. assert any(event.command_name.lower() == "saslstart" for event in listener.started_events) - # Close the client. - client.close() - def test_5_1_azure_with_no_username(self): if ENVIRON != "azure": raise unittest.SkipTest("Test is only supported on Azure") @@ -1073,7 +1051,6 @@ def test_5_1_azure_with_no_username(self): props = dict(TOKEN_RESOURCE=resource, ENVIRONMENT="azure") client = self.create_client(authMechanismProperties=props) client.test.test.find_one() - client.close() def test_5_2_azure_with_bad_username(self): if ENVIRON != "azure": @@ -1086,7 +1063,6 @@ def test_5_2_azure_with_bad_username(self): client = self.create_client(username="bad", authmechanismproperties=props) with self.assertRaises(ValueError): client.test.test.find_one() - client.close() def test_speculative_auth_success(self): client1 = self.create_client() @@ -1108,10 +1084,6 @@ def test_speculative_auth_success(self): # Perform a find operation. client2.test.test.find_one() - # Close the clients. - client2.close() - client1.close() - def test_reauthentication_succeeds_multiple_connections(self): client1 = self.create_client() client2 = self.create_client() @@ -1151,8 +1123,6 @@ def test_reauthentication_succeeds_multiple_connections(self): client2.test.test.find_one() self.assertEqual(self.request_called, 3) - client1.close() - client2.close() if __name__ == "__main__": diff --git a/test/unified_format_shared.py b/test/unified_format_shared.py index f1b908a7a6..dc74afee0f 100644 --- a/test/unified_format_shared.py +++ b/test/unified_format_shared.py @@ -137,6 +137,8 @@ "ENVIRONMENT": "gcp", "TOKEN_RESOURCE": os.environ["GCPOIDC_AUDIENCE"], } +elif OIDC_ENV == "k8s": + PLACEHOLDER_MAP["/uriOptions/authMechanismProperties"] = {"ENVIRONMENT": "k8s"} def with_metaclass(meta, *bases):