From 68177953c974874291e64bd6b7540ba64eab9ba6 Mon Sep 17 00:00:00 2001 From: Oleksandr Poliakov <31327136+sanych-sun@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:21:31 -0700 Subject: [PATCH] CSHARP-4448: Implement OIDC SASL mechanism (#1259) --- build.cake | 9 +- evergreen/evergreen.yml | 110 +- evergreen/evergreen.yml.bak | 2496 +++++++++++++++++ evergreen/run-mongodb-oidc-azure-tests.sh | 17 + evergreen/run-mongodb-oidc-tests.sh | 51 + specifications/auth/tests/README.md | 47 + specifications/auth/tests/README.rst | 53 - .../auth/tests/connection-string.yml | 366 --- .../tests/{ => legacy}/connection-string.json | 167 +- .../auth/tests/legacy/connection-string.yml | 442 +++ specifications/auth/tests/mongodb-aws.rst | 94 - .../tests/unified/mongodb-oidc-no-retry.json | 601 ++++ .../tests/unified/mongodb-oidc-no-retry.yml | 313 +++ .../Authentication/DefaultAuthenticator.cs | 8 +- .../Authentication/GssapiAuthenticator.cs | 67 +- .../Core/Authentication/IAuthenticator.cs | 3 +- .../Authentication/MongoAWSAuthenticator.cs | 41 +- .../Authentication/MongoDBCRAuthenticator.cs | 6 +- .../MongoDBX509Authenticator.cs | 2 +- .../NoTransitionClientLastSaslStep.cs | 58 + .../Authentication/Oidc/AzureOidcCallback.cs | 89 + .../Authentication/Oidc/FileOidcCallback.cs | 57 + .../Core/Authentication/Oidc/IOidcCallback.cs | 97 + .../Oidc/MongoOidcAuthenticator.cs | 182 ++ .../Oidc/OidcCallbackAdapter.cs | 139 + .../Oidc/OidcCallbackAdapterFactory.cs | 64 + .../Authentication/Oidc/OidcConfiguration.cs | 143 + .../Authentication/Oidc/OidcCredentials.cs | 36 + .../Authentication/Oidc/OidcSaslMechanism.cs | 84 + .../Core/Authentication/PlainAuthenticator.cs | 15 +- .../Core/Authentication/SaslAuthenticator.cs | 201 +- .../Authentication/ScramShaAuthenticator.cs | 27 +- .../Configuration/ClusterBuilderExtensions.cs | 22 +- .../ExclusiveConnectionPool.Helpers.cs | 16 + .../Core/Connections/BinaryConnection.cs | 46 +- .../ConnectionDescriptionExtensions.cs | 26 + .../Core/Connections/ConnectionInitializer.cs | 96 +- .../Core/Connections/HelloHelper.cs | 4 +- .../Core/Connections/IConnection.cs | 13 + .../Connections/IConnectionInitializer.cs | 4 +- .../Core/Operations/RetryabilityHelper.cs | 22 + .../CommandUsingCommandMessageWireProtocol.cs | 139 +- src/MongoDB.Driver.Core/ServerErrorCode.cs | 2 + src/MongoDB.Driver/ClusterRegistry.cs | 4 +- src/MongoDB.Driver/MongoCredential.cs | 86 +- src/MongoDB.Driver/MongoOidcIdentity.cs | 31 + .../FailPoint.cs | 1 + .../MockConnection.cs | 46 +- .../XunitExtensions/RequireServer.cs | 4 + .../AuthenticationHelperTests.cs | 2 - .../Oidc/OidcConfigurationTests.cs | 220 ++ .../ScramSha1AuthenticatorTests.cs | 2 +- .../ScramSha256AuthenticatorTests.cs | 2 +- .../Configuration/ConnectionStringTests.cs | 29 + .../Core/Connections/BinaryConnectionTests.cs | 57 +- .../BinaryConnection_CommandEventTests.cs | 2 +- .../Connections/ConnectionInitializerTests.cs | 6 +- .../Operations/RetryabilityHelperTests.cs | 18 + .../DriverTestConfiguration.cs | 6 + .../MongoCredentialTests.cs | 33 +- .../Specifications/UnifiedTestSpecRunner.cs | 4 + .../Specifications/auth/AuthTestRunner.cs | 149 +- .../auth/OidcAuthenticationProseTests.cs | 413 +++ .../Matchers/UnifiedErrorMatcher.cs | 12 + .../UnifiedTestOperations/UnifiedEntityMap.cs | 43 + .../UnifiedTestRunner.cs | 2 +- .../XunitExtensions/RequireEnvironment.cs | 15 + 67 files changed, 6809 insertions(+), 853 deletions(-) create mode 100644 evergreen/evergreen.yml.bak create mode 100644 evergreen/run-mongodb-oidc-azure-tests.sh create mode 100644 evergreen/run-mongodb-oidc-tests.sh create mode 100644 specifications/auth/tests/README.md delete mode 100644 specifications/auth/tests/README.rst delete mode 100644 specifications/auth/tests/connection-string.yml rename specifications/auth/tests/{ => legacy}/connection-string.json (69%) create mode 100644 specifications/auth/tests/legacy/connection-string.yml delete mode 100644 specifications/auth/tests/mongodb-aws.rst create mode 100644 specifications/auth/tests/unified/mongodb-oidc-no-retry.json create mode 100644 specifications/auth/tests/unified/mongodb-oidc-no-retry.yml create mode 100644 src/MongoDB.Driver.Core/Core/Authentication/NoTransitionClientLastSaslStep.cs create mode 100644 src/MongoDB.Driver.Core/Core/Authentication/Oidc/AzureOidcCallback.cs create mode 100644 src/MongoDB.Driver.Core/Core/Authentication/Oidc/FileOidcCallback.cs create mode 100644 src/MongoDB.Driver.Core/Core/Authentication/Oidc/IOidcCallback.cs create mode 100644 src/MongoDB.Driver.Core/Core/Authentication/Oidc/MongoOidcAuthenticator.cs create mode 100644 src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcCallbackAdapter.cs create mode 100644 src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcCallbackAdapterFactory.cs create mode 100644 src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcConfiguration.cs create mode 100644 src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcCredentials.cs create mode 100644 src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcSaslMechanism.cs create mode 100644 src/MongoDB.Driver.Core/Core/Connections/ConnectionDescriptionExtensions.cs create mode 100644 src/MongoDB.Driver/MongoOidcIdentity.cs create mode 100644 tests/MongoDB.Driver.Core.Tests/Core/Authentication/Oidc/OidcConfigurationTests.cs create mode 100644 tests/MongoDB.Driver.Tests/Specifications/auth/OidcAuthenticationProseTests.cs diff --git a/build.cake b/build.cake index d767d0b96e5..4b69b219d6d 100644 --- a/build.cake +++ b/build.cake @@ -257,6 +257,13 @@ Task("TestGssapiNetStandard20").IsDependentOn("TestGssapi"); Task("TestGssapiNetStandard21").IsDependentOn("TestGssapi"); Task("TestGssapiNet60").IsDependentOn("TestGssapi"); +Task("TestMongoDbOidc") + .IsDependentOn("Build") + .DoesForEach( + items: GetFiles("./**/MongoDB.Driver.Tests.csproj"), + action: (BuildConfig buildConfig, Path testProject) => + RunTests(buildConfig, testProject, filter: "Category=\"MongoDbOidc\"")); + Task("TestServerless") .IsDependentOn("Build") .DoesForEach( @@ -692,7 +699,7 @@ public class BuildConfig string[] CreateLoggers(string projectName) { var testResultsFile = outputDirectory.Combine("test-results").Combine($"TEST-{projectName}-{target.ToLowerInvariant()}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.xml"); - + // Evergreen CI server requires JUnit output format to display test results var junitLogger = $"junit;LogFilePath={testResultsFile};FailureBodyFormat=Verbose"; var consoleLogger = "console;verbosity=detailed"; diff --git a/evergreen/evergreen.yml b/evergreen/evergreen.yml index d3a36320050..880292a6258 100644 --- a/evergreen/evergreen.yml +++ b/evergreen/evergreen.yml @@ -101,7 +101,7 @@ functions: params: script: | ${PREPARE_SHELL} - bash ${PROJECT_DIRECTORY}/evergreen/install-dotnet.sh + OS=${OS} bash ${PROJECT_DIRECTORY}/evergreen/install-dotnet.sh prepare-resources: - command: shell.exec @@ -452,6 +452,11 @@ functions: params: file: mongo-csharp-driver/benchmarks/MongoDB.Driver.Benchmarks/Benchmark.Artifacts/results/evergreen-results.json + assume-ec2-role: + - command: ec2.assume_role + params: + role_arn: ${aws_test_secrets_role} + add-aws-auth-variables-to-file: - command: ec2.assume_role params: @@ -707,6 +712,19 @@ functions: -v \ --fault revoked + run-mongodb-oidc-tests: + - command: subprocess.exec + type: test + params: + working_dir: mongo-csharp-driver + binary: bash + include_expansions_in_env: + - "DRIVERS_TOOLS" + - "OS" + - "FRAMEWORK" + args: + - evergreen/run-mongodb-oidc-tests.sh + run-serverless-tests: - command: shell.exec type: test @@ -1237,6 +1255,27 @@ tasks: commands: - func: run-atlas-search-index-helpers-test + - name: test-oidc-auth + commands: + - func: run-mongodb-oidc-tests + + - name: test-oidc-azure + commands: + - command: shell.exec + params: + shell: bash + working_dir: mongo-csharp-driver + script: |- + set -o errexit + ${PREPARE_SHELL} + + dotnet build ./tests/MongoDB.Driver.Tests/MongoDB.Driver.Tests.csproj + tar czf /tmp/mongo-csharp-driver.tgz ./tests/MongoDB.Driver.Tests/bin/Debug/net6.0 ./evergreen/run-mongodb-oidc-azure-tests.sh + + export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-csharp-driver.tgz + export AZUREOIDC_TEST_CMD="./evergreen/run-mongodb-oidc-azure-tests.sh" + bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh + - name: test-serverless exec_timeout_secs: 2700 # 45 minutes: 15 for setup + 30 for tests commands: @@ -2101,6 +2140,61 @@ task_groups: tasks: - test-aws-lambda-deployed + - name: oidc-auth-test-task-group + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 # 30 minutes + setup_group: + - func: fetch-source + - func: prepare-resources + - func: fix-absolute-paths + - func: init-test-results + - func: make-files-executable + - func: assume-ec2-role + - command: subprocess.exec + params: + binary: bash + include_expansions_in_env: + - "AWS_ACCESS_KEY_ID" + - "AWS_SECRET_ACCESS_KEY" + - "AWS_SESSION_TOKEN" + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/setup.sh + teardown_group: + - func: upload-test-results + - command: subprocess.exec + params: + binary: bash + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/teardown.sh + tasks: + - test-oidc-auth + + - name: oidc-auth-azure-task-group + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 # 30 minutes + setup_group: + - func: fetch-source + - func: prepare-resources + - func: fix-absolute-paths + - func: make-files-executable + - func: install-dotnet + - command: subprocess.exec + params: + binary: bash + env: + AZUREOIDC_VMNAME_PREFIX: "CSHARP_DRIVER" + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/azure/create-and-setup-vm.sh + teardown_group: + - func: upload-test-results + - command: subprocess.exec + params: + binary: bash + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/azure/delete-vm.sh + tasks: + - test-oidc-azure + buildvariants: - matrix_name: stable-api-tests matrix_spec: { version: ["5.0", "6.0", "7.0", "rapid", "latest"], topology: "standalone", auth: "auth", ssl: "nossl", os: "windows-64" } @@ -2218,6 +2312,20 @@ buildvariants: tasks: - name: plain-auth-tests +- matrix_name: mongodb-oidc-test-tests + matrix_spec: { os: [ "ubuntu-2004", "macos-1100" ] } + display_name: "MongoDB-OIDC Auth (test) - ${os}" + batchtime: 20160 # 14 days + tasks: + - name: oidc-auth-test-task-group + +- matrix_name: mongodb-oidc-azure-tests + matrix_spec: { os: [ "ubuntu-2004" ] } + display_name: "MongoDB-OIDC Auth (azure) - ${os}" + batchtime: 20160 # 14 days + tasks: + - name: oidc-auth-azure-task-group + - matrix_name: "ocsp-tests" matrix_spec: { version: ["4.4", "5.0", "6.0", "7.0", "rapid", "latest"], auth: "noauth", ssl: "ssl", topology: "standalone", os: "windows-64" } display_name: "OCSP ${version} ${os}" diff --git a/evergreen/evergreen.yml.bak b/evergreen/evergreen.yml.bak new file mode 100644 index 00000000000..b3f5aaa5805 --- /dev/null +++ b/evergreen/evergreen.yml.bak @@ -0,0 +1,2496 @@ +######################################## +# Evergreen Template for MongoDB Drivers +######################################## + +# When a task that used to pass starts to fail +# Go through all versions that may have been skipped to detect +# when the task started failing +stepback: true + +# Mark a failure as a system/bootstrap failure (purple box) rather then a task +# failure by default. +# Actual testing tasks are marked with `type: test` +command_type: system + +# Protect ourself against rogue test case, or curl gone wild, that runs forever +# 60 minutes: 20 minutes is a normal test run + up to 10 minutes for test setup + 15 minutes for longer macOS tests + 15 minutes for longer macOS 1100 tests +exec_timeout_secs: 3600 + +# What to do when evergreen hits the timeout (`post:` tasks are run automatically) +timeout: + - command: shell.exec + params: + script: | + ls -la + df -h + +functions: + + fetch-source: + # Executes git clone and applies the submitted patch, if any + - command: git.get_project + params: + directory: mongo-csharp-driver + # Applies the submitted patch, if any + # Deprecated. Should be removed. But still needed for certain agents (ZAP) + - command: git.apply_patch + # Make an evergreen expansion file with dynamic values + - command: shell.exec + params: + working_dir: mongo-csharp-driver + script: | + # Get the current unique version of this checkout + if [ "${is_patch}" = "true" ]; then + CURRENT_VERSION=$(git describe)-patch-${version_id} + else + CURRENT_VERSION=latest + fi + + if [ "${BUILD_TARGET}" = "release" ]; then + PACKAGE_VERSION=$(bash ./evergreen/get-version.sh) + fi + + export DOTNET_SDK_PATH="$(pwd)/../.dotnet" + export DRIVERS_TOOLS="$(pwd)/../drivers-tools" + + if [ "Windows_NT" = "$OS" ]; then # Magic variable in cygwin + # Python has cygwin path problems on Windows. Detect prospective mongo-orchestration home directory + export DRIVERS_TOOLS=$(cygpath -m $DRIVERS_TOOLS) + else + # non windows OSs don't have dotnet in the PATH + export PATH=$PATH:/usr/share/dotnet + fi + + export MONGO_ORCHESTRATION_HOME="$DRIVERS_TOOLS/.evergreen/orchestration" + export MONGODB_BINARIES="$DRIVERS_TOOLS/mongodb/bin" + export UPLOAD_BUCKET="${project}" + export PROJECT_DIRECTORY="$(pwd)" + + cat < expansion.yml + CURRENT_VERSION: "$CURRENT_VERSION" + DRIVERS_TOOLS: "$DRIVERS_TOOLS" + MONGO_ORCHESTRATION_HOME: "$MONGO_ORCHESTRATION_HOME" + MONGODB_BINARIES: "$MONGODB_BINARIES" + UPLOAD_BUCKET: "$UPLOAD_BUCKET" + PROJECT_DIRECTORY: "$PROJECT_DIRECTORY" + PACKAGE_VERSION: "$PACKAGE_VERSION" + DOTNET_SDK_PATH: "$DOTNET_SDK_PATH" + PREPARE_SHELL: | + set -o errexit + set -o xtrace + export DRIVERS_TOOLS="$DRIVERS_TOOLS" + export MONGO_ORCHESTRATION_HOME="$MONGO_ORCHESTRATION_HOME" + export MONGODB_BINARIES="$MONGODB_BINARIES" + export UPLOAD_BUCKET="$UPLOAD_BUCKET" + export PROJECT_DIRECTORY="$PROJECT_DIRECTORY" + export PACKAGE_VERSION="$PACKAGE_VERSION" + export TMPDIR="$MONGO_ORCHESTRATION_HOME/db" + export PATH="$DOTNET_SDK_PATH:$MONGODB_BINARIES:$PATH" + export PROJECT="${project}" + EOT + # See what we've done + cat expansion.yml + + # Load the expansion file to make an evergreen variable with the current unique version + - command: expansions.update + params: + file: mongo-csharp-driver/expansion.yml + + install-dotnet: + - command: shell.exec + params: + script: | + ${PREPARE_SHELL} + bash ${PROJECT_DIRECTORY}/evergreen/install-dotnet.sh + + prepare-resources: + - command: shell.exec + params: + script: | + ${PREPARE_SHELL} + rm -rf $DRIVERS_TOOLS + if [ "${project}" = "drivers-tools" ]; then + # If this was a patch build, doing a fresh clone would not actually test the patch + cp -R ${PROJECT_DIRECTORY}/ $DRIVERS_TOOLS + else + git clone https://github.com/mongodb-labs/drivers-evergreen-tools.git $DRIVERS_TOOLS + fi + echo "{ \"releases\": { \"default\": \"$MONGODB_BINARIES\" }}" > $MONGO_ORCHESTRATION_HOME/orchestration.config + + # Upload build artifacts that other tasks may depend on + # Note this URL needs to be totally unique, while predictable for the next task + # so it can automatically download the artifacts + upload-build: + # Compress and upload the entire build directory + - command: archive.targz_pack + params: + # Example: mongo_c_driver_releng_9dfb7d741efbca16faa7859b9349d7a942273e43_16_11_08_19_29_52.tar.gz + target: "${build_id}.tar.gz" + source_dir: ${PROJECT_DIRECTORY}/ + include: + - "./**" + - command: s3.put + params: + aws_key: ${aws_key} + aws_secret: ${aws_secret} + local_file: ${build_id}.tar.gz + # Example: /mciuploads/${UPLOAD_BUCKET}/gcc49/9dfb7d741efbca16faa7859b9349d7a942273e43/debug-compile-nosasl-nossl/mongo_c_driver_releng_9dfb7d741efbca16faa7859b9349d7a942273e43_16_11_08_19_29_52.tar.gz + remote_file: ${UPLOAD_BUCKET}/${build_variant}/${revision}/${task_name}/${build_id}.tar.gz + bucket: mciuploads + permissions: public-read + content_type: ${content_type|application/x-gzip} + + exec-script: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + ${PROJECT_DIRECTORY}/${file} + + upload-mo-artifacts: + - command: shell.exec + params: + script: | + ${PREPARE_SHELL} + find $MONGO_ORCHESTRATION_HOME -name \*.log | xargs tar czf mongodb-logs.tar.gz + - command: s3.put + params: + aws_key: ${aws_key} + aws_secret: ${aws_secret} + local_file: mongodb-logs.tar.gz + remote_file: ${UPLOAD_BUCKET}/${build_variant}/${revision}/${version_id}/${build_id}/logs/${task_id}-${execution}-mongodb-logs.tar.gz + bucket: mciuploads + permissions: public-read + content_type: ${content_type|application/x-gzip} + display_name: "mongodb-logs.tar.gz" + - command: s3.put + params: + aws_key: ${aws_key} + aws_secret: ${aws_secret} + local_file: drivers-tools/.evergreen/orchestration/server.log + remote_file: ${UPLOAD_BUCKET}/${build_variant}/${revision}/${version_id}/${build_id}/logs/${task_id}-${execution}-orchestration.log + bucket: mciuploads + permissions: public-read + content_type: ${content_type|text/plain} + display_name: "orchestration.log" + + upload-working-dir: + - command: archive.targz_pack + params: + target: "working-dir.tar.gz" + source_dir: ${PROJECT_DIRECTORY}/ + include: + - "./**" + - command: s3.put + params: + aws_key: ${aws_key} + aws_secret: ${aws_secret} + local_file: working-dir.tar.gz + remote_file: ${UPLOAD_BUCKET}/${build_variant}/${revision}/${version_id}/${build_id}/artifacts/${task_id}-${execution}-working-dir.tar.gz + bucket: mciuploads + permissions: public-read + content_type: ${content_type|application/x-gzip} + display_name: "working-dir.tar.gz" + - command: archive.targz_pack + params: + target: "drivers-dir.tar.gz" + source_dir: ${DRIVERS_TOOLS} + include: + - "./**" + - command: s3.put + params: + aws_key: ${aws_key} + aws_secret: ${aws_secret} + local_file: drivers-dir.tar.gz + remote_file: ${UPLOAD_BUCKET}/${build_variant}/${revision}/${version_id}/${build_id}/artifacts/${task_id}-${execution}-drivers-dir.tar.gz + bucket: mciuploads + permissions: public-read + content_type: ${content_type|application/x-gzip} + display_name: "drivers-dir.tar.gz" + + upload-test-results: + - command: attach.xunit_results + params: + file: ./mongo-csharp-driver/build/test-results/TEST-*.xml + + bootstrap-mongo-orchestration: + - command: shell.exec + params: + script: | + ${PREPARE_SHELL} + REQUIRE_API_VERSION=${REQUIRE_API_VERSION} \ + LOAD_BALANCER=${LOAD_BALANCER} \ + MONGODB_VERSION=${VERSION} \ + TOPOLOGY=${TOPOLOGY} \ + AUTH=${AUTH} \ + SSL=${SSL} \ + STORAGE_ENGINE=${STORAGE_ENGINE} \ + ORCHESTRATION_FILE=${ORCHESTRATION_FILE} \ + SKIP_LEGACY_SHELL=${SKIP_LEGACY_SHELL} \ + bash ${DRIVERS_TOOLS}/.evergreen/run-orchestration.sh + # run-orchestration generates expansion file with the MONGODB_URI for the cluster + - command: expansions.update + params: + file: mo-expansion.yml + + ocsp-bootstrap-mongo-orchestration: + - command: shell.exec + params: + script: | + ${PREPARE_SHELL} + MONGODB_VERSION=${VERSION} \ + TOPOLOGY=${TOPOLOGY} \ + AUTH=${AUTH} \ + SSL=${SSL} \ + ORCHESTRATION_FILE=${ORCHESTRATION_FILE} \ + bash ${DRIVERS_TOOLS}/.evergreen/run-orchestration.sh + - command: expansions.update + params: + file: mo-expansion.yml + + bootstrap-mongohoused: + - command: shell.exec + params: + script: | + DRIVERS_TOOLS="${DRIVERS_TOOLS}" bash ${DRIVERS_TOOLS}/.evergreen/atlas_data_lake/pull-mongohouse-image.sh + - command: shell.exec + params: + script: | + DRIVERS_TOOLS="${DRIVERS_TOOLS}" bash ${DRIVERS_TOOLS}/.evergreen/atlas_data_lake/run-mongohouse-image.sh + + run-load-balancer: + - command: shell.exec + params: + script: | + DRIVERS_TOOLS=${DRIVERS_TOOLS} MONGODB_URI=${MONGODB_URI} bash ${DRIVERS_TOOLS}/.evergreen/run-load-balancer.sh start + - command: expansions.update + params: + file: lb-expansion.yml + + run-load-balancer-tests: + - command: shell.exec + type: test + params: + working_dir: "mongo-csharp-driver" + script: | + ${PREPARE_SHELL} + OS=${OS} evergreen/add-ca-certs.sh + AUTH="${AUTH}" SSL="${SSL}" \ + FRAMEWORK=${FRAMEWORK} \ + OS=${OS} \ + SINGLE_MONGOS_LB_URI="${SINGLE_MONGOS_LB_URI}" \ + MULTI_MONGOS_LB_URI="${MULTI_MONGOS_LB_URI}" \ + evergreen/run-load-balancer-tests.sh + + stop-load-balancer: + - command: shell.exec + params: + script: | + cd ${DRIVERS_TOOLS}/.evergreen + DRIVERS_TOOLS=${DRIVERS_TOOLS} MONGODB_URI=${MONGODB_URI} bash ${DRIVERS_TOOLS}/.evergreen/run-load-balancer.sh stop + + run-tests: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + include_expansions_in_env: + - "FLE_AWS_ACCESS_KEY_ID" + - "FLE_AWS_SECRET_ACCESS_KEY" + - "FLE_AZURE_TENANT_ID" + - "FLE_AZURE_CLIENT_ID" + - "FLE_AZURE_CLIENT_SECRET" + - "FLE_GCP_EMAIL" + - "FLE_GCP_PRIVATE_KEY" + script: | + . ./evergreen/set-virtualenv.sh + . ./evergreen/set-temp-fle-aws-creds.sh + ${PREPARE_SHELL} + OS=${OS} \ + evergreen/add-ca-certs.sh + AUTH=${AUTH} \ + SSL=${SSL} \ + MONGODB_URI="${MONGODB_URI}" \ + TOPOLOGY=${TOPOLOGY} \ + OS=${OS} \ + COMPRESSOR=${COMPRESSOR} \ + CLIENT_PEM=${DRIVERS_TOOLS}/.evergreen/x509gen/client.pem \ + REQUIRE_API_VERSION=${REQUIRE_API_VERSION} \ + FRAMEWORK=${FRAMEWORK} \ + CRYPT_SHARED_LIB_PATH=${CRYPT_SHARED_LIB_PATH} \ + evergreen/run-tests.sh + echo "Skipping certificate removal..." + OS=${OS} \ + evergreen/cleanup-test-resources.sh + + run-csfle-with-mocked-kms-tests: + - command: shell.exec + type: test + params: + working_dir: "mongo-csharp-driver" + include_expansions_in_env: + - "FLE_AWS_ACCESS_KEY_ID" + - "FLE_AWS_SECRET_ACCESS_KEY" + - "FLE_AZURE_TENANT_ID" + - "FLE_AZURE_CLIENT_ID" + - "FLE_AZURE_CLIENT_SECRET" + - "FLE_GCP_EMAIL" + - "FLE_GCP_PRIVATE_KEY" + script: | + export KMS_MOCK_SERVERS_ENABLED=true + export GCE_METADATA_HOST="localhost:5000" + export AZURE_IMDS_MOCK_ENDPOINT="localhost:8080" + ${PREPARE_SHELL} + OS=${OS} \ + evergreen/add-ca-certs.sh + AUTH=${AUTH} \ + SSL=${SSL} \ + MONGODB_URI="${MONGODB_URI}" \ + TOPOLOGY=${TOPOLOGY} \ + OS=${OS} \ + CLIENT_PEM=${DRIVERS_TOOLS}/.evergreen/x509gen/client.pem \ + FRAMEWORK=${FRAMEWORK} \ + TARGET="TestCsfleWithMockedKms" \ + CRYPT_SHARED_LIB_PATH=${CRYPT_SHARED_LIB_PATH} \ + evergreen/run-tests.sh + OS=${OS} \ + evergreen/cleanup-test-resources.sh + + run-csfle-with-mongocryptd-tests: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + include_expansions_in_env: + - "FLE_AWS_ACCESS_KEY_ID" + - "FLE_AWS_SECRET_ACCESS_KEY" + - "FLE_AZURE_TENANT_ID" + - "FLE_AZURE_CLIENT_ID" + - "FLE_AZURE_CLIENT_SECRET" + - "FLE_GCP_EMAIL" + - "FLE_GCP_PRIVATE_KEY" + script: | + . ./evergreen/set-virtualenv.sh + . ./evergreen/set-temp-fle-aws-creds.sh + ${PREPARE_SHELL} + OS=${OS} \ + evergreen/add-ca-certs.sh + AUTH=${AUTH} \ + SSL=${SSL} \ + MONGODB_URI="${MONGODB_URI}" \ + TOPOLOGY=${TOPOLOGY} \ + OS=${OS} \ + COMPRESSOR=${COMPRESSOR} \ + CLIENT_PEM=${DRIVERS_TOOLS}/.evergreen/x509gen/client.pem \ + REQUIRE_API_VERSION=${REQUIRE_API_VERSION} \ + TARGET="TestCsfleWithMongocryptd" \ + FRAMEWORK=${FRAMEWORK} \ + CRYPT_SHARED_LIB_PATH="" \ + evergreen/run-tests.sh + echo "Skipping certificate removal..." + OS=${OS} \ + evergreen/cleanup-test-resources.sh + + run-atlas-connectivity-tests: + - command: shell.exec + type: test + params: + silent: true + working_dir: mongo-csharp-driver + include_expansions_in_env: + - "ATLAS_FREE" + - "ATLAS_FREE_SRV" + - "ATLAS_REPLICA" + - "ATLAS_REPLICA_SRV" + - "ATLAS_SHARDED" + - "ATLAS_SHARDED_SRV" + - "ATLAS_TLS11" + - "ATLAS_TLS11_SRV" + - "ATLAS_TLS12" + - "ATLAS_TLS12_SRV" + - "ATLAS_SERVERLESS" + - "ATLAS_SERVERLESS_SRV" + script: | + . evergreen/run-atlas-connectivity-tests.sh + + run-gssapi-auth-tests: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + include_expansions_in_env: + - "AUTH_GSSAPI" + - "AUTH_HOST" + script: | + PROJECT_DIRECTORY=${PROJECT_DIRECTORY} \ + FRAMEWORK=${FRAMEWORK} \ + evergreen/run-gssapi-auth-tests.sh + + run-plain-auth-tests: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + env: + MONGODB_URI: ${plain_auth_mongodb_uri} + script: | + ${PREPARE_SHELL} + OS=${OS} \ + evergreen/run-plain-auth-tests.sh + + run-performance-tests: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver/benchmarks/MongoDB.Driver.Benchmarks + script: | + ${PREPARE_SHELL} + MONGODB_URI="${MONGODB_URI}" ../../evergreen/run-perf-tests.sh + - command: perf.send + params: + file: mongo-csharp-driver/benchmarks/MongoDB.Driver.Benchmarks/Benchmark.Artifacts/results/evergreen-results.json + + assume-ec2-role: + - command: ec2.assume_role + params: + role_arn: ${aws_test_secrets_role} + + add-aws-auth-variables-to-file: + - command: ec2.assume_role + params: + role_arn: ${aws_test_secrets_role} + - command: shell.exec + type: test + params: + shell: "bash" + working_dir: mongo-csharp-driver + include_expansions_in_env: + - "AWS_ACCESS_KEY_ID" + - "AWS_SECRET_ACCESS_KEY" + - "AWS_SESSION_TOKEN" + script: | + ${PREPARE_SHELL} + cd $DRIVERS_TOOLS/.evergreen/auth_aws + ./setup_secrets.sh drivers/aws_auth + + run-aws-auth-test-with-regular-aws-credentials: + - command: shell.exec + type: test + params: + env: + IAM_AUTH_ECS_ACCOUNT: ${iam_auth_ecs_account} + IAM_AUTH_ECS_SECRET_ACCESS_KEY: ${iam_auth_ecs_secret_access_key} + working_dir: mongo-csharp-driver + script: | + DRIVERS_TOOLS=${DRIVERS_TOOLS} OS=${OS} evergreen/run-mongodb-aws-test.sh regular + + run-aws-auth-test-with-assume-role-credentials: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + script: | + DRIVERS_TOOLS=${DRIVERS_TOOLS} OS=${OS} evergreen/run-mongodb-aws-test.sh assume-role + + run-aws-auth-test-with-aws-EC2-credentials: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + if [ "${skip_EC2_auth_test}" = "true" ]; then + echo "This platform does not support the EC2 auth test, skipping..." + exit 0 + fi + DRIVERS_TOOLS=${DRIVERS_TOOLS} OS=${OS} ASSERT_NO_URI_CREDS=true evergreen/run-mongodb-aws-test.sh ec2 + + run-aws-auth-test-with-aws-ECS-credentials: + - command: shell.exec + type: test + params: + shell: "bash" + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + if [ "${skip_ECS_auth_test}" = "true" ]; then + echo "This platform does not support the ECS auth test, skipping..." + exit 0 + fi + echo "Project Directory: $PROJECT_DIRECTORY" + # SRC_DIRECTORY is workaround since EG_TOOLS expects "src" folder as a root + SRC_DIRECTORY=$(dirname $PROJECT_DIRECTORY)/src + echo "Src Directory: $SRC_DIRECTORY" + cp -r $PROJECT_DIRECTORY $SRC_DIRECTORY + # Workaround. EG_TOOLS scripts for ECS assume that a folder with EG scripts in the driver is named ".evergreen" + mkdir $SRC_DIRECTORY/.evergreen + cp -r $SRC_DIRECTORY/evergreen/run-mongodb-aws-ecs-test.sh $SRC_DIRECTORY/.evergreen/run-mongodb-aws-ecs-test.sh + + export PROJECT_DIRECTORY="$SRC_DIRECTORY" + ${DRIVERS_TOOLS}/.evergreen/auth_aws/aws_setup.sh ecs + + run-aws-auth-test-with-aws-web-identity-credentials: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + if [ "${skip_web_identity_auth_test}" = "true" ]; then + echo "This platform does not support the web identity auth test, skipping..." + exit 0 + fi + DRIVERS_TOOLS=${DRIVERS_TOOLS} OS=$OS ASSERT_NO_URI_CREDS=true evergreen/run-mongodb-aws-test.sh web-identity + - command: shell.exec + type: test + params: + shell: "bash" + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + if [ "${skip_web_identity_auth_test}" = "true" ]; then + echo "This platform does not support the web identity auth test, skipping..." + exit 0 + fi + export AWS_ROLE_SESSION_NAME="test" + DRIVERS_TOOLS=${DRIVERS_TOOLS} OS=$OS ASSERT_NO_URI_CREDS=true evergreen/run-mongodb-aws-test.sh web-identity + + run-aws-auth-test-with-aws-credentials-as-environment-variables: + - command: shell.exec + type: test + params: + env: + IAM_AUTH_ECS_ACCOUNT: ${iam_auth_ecs_account} + IAM_AUTH_ECS_SECRET_ACCESS_KEY: ${iam_auth_ecs_secret_access_key} + working_dir: mongo-csharp-driver + script: | + DRIVERS_TOOLS=${DRIVERS_TOOLS} OS=${OS} ASSERT_NO_URI_CREDS=true evergreen/run-mongodb-aws-test.sh env-creds + + run-aws-auth-test-with-aws-credentials-and-session-token-as-environment-variables: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + script: | + DRIVERS_TOOLS=${DRIVERS_TOOLS} OS=${OS} ASSERT_NO_URI_CREDS=true evergreen/run-mongodb-aws-test.sh session-creds + + run-atlas-data-lake-test: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + evergreen/run-atlas-data-lake-test.sh + + run-atlas-search-test: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + include_expansions_in_env: + - "ATLAS_SEARCH" + script: | + ${PREPARE_SHELL} + evergreen/run-atlas-search-test.sh + + run-atlas-search-index-helpers-test: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + MONGODB_URI="${MONGODB_URI}" evergreen/run-atlas-search-index-helpers-test.sh + + run-ocsp-test: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + OCSP_TLS_SHOULD_SUCCEED="${OCSP_TLS_SHOULD_SUCCEED}" \ + OCSP_ALGORITHM=${OCSP_ALGORITHM} \ + evergreen/add-ca-certs.sh + set +o xtrace + AUTH="${AUTH}" \ + SSL="ssl" \ + TOPOLOGY="${TOPOLOGY}" \ + MONGODB_URI="${MONGODB_URI}" \ + OCSP_TLS_SHOULD_SUCCEED="${OCSP_TLS_SHOULD_SUCCEED}" \ + evergreen/run-tests.sh + echo "Skipping certificate removal..." + + run-valid-ocsp-server-ca-responder: + - command: shell.exec + params: + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + evergreen/prepare-ocsp.sh + - command: shell.exec + params: + background: true + shell: "bash" + script: | + set -o xtrace + cd ${DRIVERS_TOOLS}/.evergreen/ocsp + . ./activate-ocspvenv.sh + nohup python ocsp_mock.py \ + --ca_file ${OCSP_ALGORITHM}/ca.pem \ + --ocsp_responder_cert ${OCSP_ALGORITHM}/ca.crt \ + --ocsp_responder_key ${OCSP_ALGORITHM}/ca.key \ + -p 8100 -v + + run-valid-ocsp-server-delegate-responder: + - command: shell.exec + params: + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + evergreen/prepare-ocsp.sh + - command: shell.exec + params: + background: true + shell: "bash" + script: | + set -o xtrace + cd ${DRIVERS_TOOLS}/.evergreen/ocsp + . ./activate-ocspvenv.sh + nohup python ocsp_mock.py \ + --ca_file ${OCSP_ALGORITHM}/ca.pem \ + --ocsp_responder_cert ${OCSP_ALGORITHM}/ocsp-responder.crt \ + --ocsp_responder_key ${OCSP_ALGORITHM}/ocsp-responder.key \ + -p 8100 -v + + run-revoked-ocsp-server-ca-responder: + - command: shell.exec + params: + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + evergreen/prepare-ocsp.sh + - command: shell.exec + params: + background: true + shell: "bash" + script: | + set -o xtrace + cd ${DRIVERS_TOOLS}/.evergreen/ocsp + . ./activate-ocspvenv.sh + nohup python ocsp_mock.py \ + --ca_file ${OCSP_ALGORITHM}/ca.pem \ + --ocsp_responder_cert ${OCSP_ALGORITHM}/ca.crt \ + --ocsp_responder_key ${OCSP_ALGORITHM}/ca.key \ + -p 8100 \ + -v \ + --fault revoked + + run-revoked-ocsp-server-delegate-responder: + - command: shell.exec + params: + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + evergreen/prepare-ocsp.sh + - command: shell.exec + params: + background: true + shell: "bash" + script: | + set -o xtrace + cd ${DRIVERS_TOOLS}/.evergreen/ocsp + . ./activate-ocspvenv.sh + nohup python ocsp_mock.py \ + --ca_file ${OCSP_ALGORITHM}/ca.pem \ + --ocsp_responder_cert ${OCSP_ALGORITHM}/ocsp-responder.crt \ + --ocsp_responder_key ${OCSP_ALGORITHM}/ocsp-responder.key \ + -p 8100 \ + -v \ + --fault revoked + + run-mongodb-oidc-tests: + - command: subprocess.exec + type: test + params: + working_dir: mongo-csharp-driver + binary: bash + include_expansions_in_env: + - "DRIVERS_TOOLS" + - "OS" + - "FRAMEWORK" + args: + - evergreen/run-mongodb-oidc-tests.sh + + run-serverless-tests: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + include_expansions_in_env: + - "FLE_AWS_ACCESS_KEY_ID" + - "FLE_AWS_SECRET_ACCESS_KEY" + - "FLE_AZURE_TENANT_ID" + - "FLE_AZURE_CLIENT_ID" + - "FLE_AZURE_CLIENT_SECRET" + - "FLE_GCP_EMAIL" + - "FLE_GCP_PRIVATE_KEY" + - "SERVERLESS_ATLAS_USER" + - "SERVERLESS_ATLAS_PASSWORD" + - "SERVERLESS_URI" + script: | + ${PREPARE_SHELL} + AUTH=${AUTH} \ + FRAMEWORK=${FRAMEWORK} \ + SSL=${SSL} \ + CRYPT_SHARED_LIB_PATH=${CRYPT_SHARED_LIB_PATH} \ + evergreen/run-serverless-tests.sh + + run-smoke-tests: + - command: shell.exec + type: test + params: + working_dir: mongo-csharp-driver + script: | + set +x + ${PREPARE_SHELL} + AUTH=${AUTH} \ + SSL=${SSL} \ + MONGODB_URI="${MONGODB_URI}" \ + TOPOLOGY=${TOPOLOGY} \ + OS=${OS} \ + COMPRESSOR=${COMPRESSOR} \ + CLIENT_PEM=${DRIVERS_TOOLS}/.evergreen/x509gen/client.pem \ + REQUIRE_API_VERSION=${REQUIRE_API_VERSION} \ + TARGET="SmokeTests" \ + FRAMEWORK=${FRAMEWORK} \ + evergreen/run-tests.sh + + create-serverless-instance: + - command: shell.exec + params: + shell: bash + include_expansions_in_env: + - "SERVERLESS_API_PUBLIC_KEY" + - "SERVERLESS_API_PRIVATE_KEY" + script: | + ${PREPARE_SHELL} + if [ "Terminating" = "${SERVERLESS_PROXY_TYPE}" ]; then + SERVERLESS_GROUP="${TERMINATING_PROXY_SERVERLESS_DRIVERS_GROUP}" + else + SERVERLESS_GROUP="${SERVERLESS_DRIVERS_GROUP}" + fi + SERVERLESS_DRIVERS_GROUP="$SERVERLESS_GROUP" \ + LOADBALANCED=ON \ + bash ${DRIVERS_TOOLS}/.evergreen/serverless/create-instance.sh + - command: expansions.update + params: + file: serverless-expansion.yml + + delete-serverless-instance-if-configured: + - command: shell.exec + params: + shell: bash + include_expansions_in_env: + - "SERVERLESS_API_PUBLIC_KEY" + - "SERVERLESS_API_PRIVATE_KEY" + script: | + if [ "" != "${SERVERLESS}" ]; then + ${PREPARE_SHELL} + if [ "Terminating" = "${SERVERLESS_PROXY_TYPE}" ]; then + SERVERLESS_GROUP="${TERMINATING_PROXY_SERVERLESS_DRIVERS_GROUP}" + else + SERVERLESS_GROUP="${SERVERLESS_DRIVERS_GROUP}" + fi + SERVERLESS_DRIVERS_GROUP="$SERVERLESS_GROUP" \ + SERVERLESS_INSTANCE_NAME=${SERVERLESS_INSTANCE_NAME} \ + bash ${DRIVERS_TOOLS}/.evergreen/serverless/delete-instance.sh + fi + + start-kms-mock-servers: + - command: shell.exec + params: + shell: "bash" + script: | + ${PREPARE_SHELL} + cd ${DRIVERS_TOOLS}/.evergreen/csfle + . ./activate-kmstlsvenv.sh + - command: shell.exec + params: + background: true + shell: "bash" + script: | + #expired client cert + cd ${DRIVERS_TOOLS}/.evergreen/csfle + . ./activate-kmstlsvenv.sh + python -u kms_http_server.py -v --ca_file ../x509gen/ca.pem --cert_file ../x509gen/expired.pem --port 8000 + - command: shell.exec + params: + background: true + shell: "bash" + script: | + #wrong-host client cert + cd ${DRIVERS_TOOLS}/.evergreen/csfle + . ./activate-kmstlsvenv.sh + python -u kms_http_server.py -v --ca_file ../x509gen/ca.pem --cert_file ../x509gen/wrong-host.pem --port 8001 + - command: shell.exec + params: + background: true + shell: "bash" + script: | + #server.pem client cert + cd ${DRIVERS_TOOLS}/.evergreen/csfle + . ./activate-kmstlsvenv.sh + python -u kms_http_server.py -v --ca_file ../x509gen/ca.pem --cert_file ../x509gen/server.pem --port 8002 --require_client_cert + + start-kms-mock-kmip-server: + - command: shell.exec + params: + shell: "bash" + script: | + ${PREPARE_SHELL} + cd ${DRIVERS_TOOLS}/.evergreen/csfle + . ./activate-kmstlsvenv.sh + - command: shell.exec + params: + shell: "bash" + background: true + script: | + cd ${DRIVERS_TOOLS}/.evergreen/csfle + . ./activate-kmstlsvenv.sh + python -u kms_kmip_server.py + + start-kms-mock-gcp-server: + - command: shell.exec + params: + shell: "bash" + script: | + ${PREPARE_SHELL} + cd ${DRIVERS_TOOLS}/.evergreen/csfle + . ./activate-kmstlsvenv.sh + - command: shell.exec + params: + background: true + shell: "bash" + script: | + cd ${DRIVERS_TOOLS}/.evergreen/csfle/gcpkms + . ./activate-kmstlsvenv.sh + python -m pip install PyJWT + mkdir ${DRIVERS_TOOLS}/tmp + echo '${GOOGLE_APPLICATION_CREDENTIALS_CONTENT}' > ${DRIVERS_TOOLS}/tmp/testgcpkms_key_file.json + export GOOGLE_APPLICATION_CREDENTIALS=${DRIVERS_TOOLS}/tmp/testgcpkms_key_file.json + python -u mock_server.py + + start-kms-mock-azure-imds-server: + - command: shell.exec + params: + shell: "bash" + script: | + ${PREPARE_SHELL} + cd ${DRIVERS_TOOLS}/.evergreen/csfle + . ./activate-kmstlsvenv.sh + - command: shell.exec + params: + background: true + shell: "bash" + script: | + cd ${DRIVERS_TOOLS}/.evergreen/csfle + . ./activate-kmstlsvenv.sh + python bottle.py fake_azure:imds + + cleanup: + - command: shell.exec + params: + shell: "bash" + script: | + ${PREPARE_SHELL} + cd "${DRIVERS_TOOLS}/.evergreen/auth_aws" + if [ -f "./aws_e2e_setup.json" ]; then + . ./activate-authawsvenv.sh + python ./lib/aws_assign_instance_profile.py + fi + - command: shell.exec + params: + script: | + ${PREPARE_SHELL} + cd "$MONGO_ORCHESTRATION_HOME" + # source the mongo-orchestration virtualenv if it exists + if [ -f venv/bin/activate ]; then + . venv/bin/activate + elif [ -f venv/Scripts/activate ]; then + . venv/Scripts/activate + fi + mongo-orchestration stop + cd - + rm -rf $DRIVERS_TOOLS || true + + fix-absolute-paths: + - command: shell.exec + params: + script: | + ${PREPARE_SHELL} + for filename in $(find ${DRIVERS_TOOLS} -name \*.json); do + perl -p -i -e "s|ABSOLUTE_PATH_REPLACEMENT_TOKEN|${DRIVERS_TOOLS}|g" $filename + done + + windows-fix: + - command: shell.exec + params: + script: | + if [ "Windows_NT" = "$OS" ]; then + ${PREPARE_SHELL} + for i in $(find ${DRIVERS_TOOLS}/.evergreen ${PROJECT_DIRECTORY}/evergreen -name \*.sh); do + cat $i | tr -d '\r' > $i.new + mv $i.new $i + done + # Copy client certificate because symlinks do not work on Windows. + cp ${DRIVERS_TOOLS}/.evergreen/x509gen/client.pem ${MONGO_ORCHESTRATION_HOME}/lib/client.pem + fi + + make-files-executable: + - command: shell.exec + params: + script: | + ${PREPARE_SHELL} + for i in $(find ${DRIVERS_TOOLS}/.evergreen ${PROJECT_DIRECTORY}/evergreen -name \*.sh); do + chmod +x $i + done + + init-test-results: + - command: shell.exec + params: + script: | + ${PREPARE_SHELL} + echo '{"results": [{ "status": "FAIL", "test_file": "Build", "log_raw": "No test-results.json found was created" } ]}' > ${PROJECT_DIRECTORY}/test-results.json + + build-packages: + - command: shell.exec + params: + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + . ./evergreen/build-packages.sh + + push-packages: + - command: shell.exec + params: + working_dir: mongo-csharp-driver + env: + PACKAGES_SOURCE: ${PACKAGES_SOURCE} + PACKAGES_SOURCE_KEY: ${PACKAGES_SOURCE_KEY} + script: | + ${PREPARE_SHELL} + . ./evergreen/push-packages.sh + + upload-package: + - command: s3.put + params: + aws_key: ${aws_key} + aws_secret: ${aws_secret} + local_file: mongo-csharp-driver/artifacts/nuget/${PACKAGE_ID}.${PACKAGE_VERSION}.nupkg + remote_file: ${UPLOAD_BUCKET}/${revision}/${PACKAGE_ID}.${PACKAGE_VERSION}.nupkg + bucket: mciuploads + permissions: public-read + content_type: ${content_type|application/octet-stream} + - command: s3.put + params: + aws_key: ${aws_key} + aws_secret: ${aws_secret} + local_file: mongo-csharp-driver/artifacts/nuget/${PACKAGE_ID}.${PACKAGE_VERSION}.snupkg + remote_file: ${UPLOAD_BUCKET}/${revision}/${PACKAGE_ID}.${PACKAGE_VERSION}.snupkg + bucket: mciuploads + permissions: public-read + content_type: ${content_type|application/octet-stream} + + download-package: + - command: s3.get + params: + aws_key: ${aws_key} + aws_secret: ${aws_secret} + local_file: mongo-csharp-driver/artifacts/nuget/${PACKAGE_ID}.${PACKAGE_VERSION}.nupkg + remote_file: ${UPLOAD_BUCKET}/${revision}/${PACKAGE_ID}.${PACKAGE_VERSION}.nupkg + bucket: mciuploads + - command: s3.get + params: + aws_key: ${aws_key} + aws_secret: ${aws_secret} + local_file: mongo-csharp-driver/artifacts/nuget/${PACKAGE_ID}.${PACKAGE_VERSION}.snupkg + remote_file: ${UPLOAD_BUCKET}/${revision}/${PACKAGE_ID}.${PACKAGE_VERSION}.snupkg + bucket: mciuploads + + build-apidocs: + - command: shell.exec + params: + shell: bash + working_dir: mongo-csharp-driver + script: | + ${PREPARE_SHELL} + if ! [[ "$PACKAGE_VERSION" =~ ^[0-9]+\.[0-9]+\.0$ ]]; then + echo "Skip api docs generating for the patch release" + exit 0 + fi + ./evergreen/build-apidocs.sh + + upload-apidocs: + - command: shell.exec + params: + shell: bash + working_dir: mongo-csharp-driver + env: + GITHUB_USER: ${github_user} + GITHUB_APIKEY: ${github_apikey} + script: | + ${PREPARE_SHELL} + if ! [[ "$PACKAGE_VERSION" =~ ^[0-9]+\.[0-9]+\.0$ ]]; then + echo "Skip api docs generating for the patch release" + exit 0 + fi + ./evergreen/upload-apidocs.sh + +pre: + - func: fetch-source + - func: prepare-resources + - func: windows-fix + - func: fix-absolute-paths + - func: init-test-results + - func: make-files-executable + +post: + # Removed, causing timeouts + # - func: upload-working-dir + - func: delete-serverless-instance-if-configured + - func: upload-mo-artifacts + - func: upload-test-results + - func: cleanup + +tasks: + - name: test-net472 + commands: + - func: bootstrap-mongo-orchestration + - func: run-tests + vars: + FRAMEWORK: net472 + + - name: test-netstandard20 + commands: + - func: bootstrap-mongo-orchestration + - func: run-tests + vars: + FRAMEWORK: netstandard20 + + - name: test-netstandard21 + commands: + - func: bootstrap-mongo-orchestration + - func: run-tests + vars: + FRAMEWORK: netstandard21 + + - name: test-csfle-with-mongocryptd-net472 + commands: + - func: bootstrap-mongo-orchestration + - func: run-csfle-with-mongocryptd-tests + vars: + FRAMEWORK: net472 + + - name: test-csfle-with-mongocryptd-netstandard20 + commands: + - func: bootstrap-mongo-orchestration + - func: run-csfle-with-mongocryptd-tests + vars: + FRAMEWORK: netstandard20 + + - name: test-csfle-with-mongocryptd-netstandard21 + commands: + - func: bootstrap-mongo-orchestration + - func: run-csfle-with-mongocryptd-tests + vars: + FRAMEWORK: netstandard21 + + - name: test-csfle-with-mocked-kms-tls-net472 + commands: + - func: start-kms-mock-servers + - func: start-kms-mock-kmip-server + - func: start-kms-mock-gcp-server + - func: start-kms-mock-azure-imds-server + - func: bootstrap-mongo-orchestration + - func: run-csfle-with-mocked-kms-tests + vars: + FRAMEWORK: net472 + + - name: test-csfle-with-mocked-kms-tls-netstandard20 + commands: + - func: start-kms-mock-servers + - func: start-kms-mock-kmip-server + - func: start-kms-mock-gcp-server + - func: start-kms-mock-azure-imds-server + - func: bootstrap-mongo-orchestration + - func: run-csfle-with-mocked-kms-tests + vars: + FRAMEWORK: netstandard20 + + - name: test-csfle-with-mocked-kms-tls-netstandard21 + commands: + - func: start-kms-mock-servers + - func: start-kms-mock-kmip-server + - func: start-kms-mock-gcp-server + - func: start-kms-mock-azure-imds-server + - func: bootstrap-mongo-orchestration + - func: run-csfle-with-mocked-kms-tests + vars: + FRAMEWORK: netstandard21 + + - name: test-load-balancer-netstandard20 + commands: + - func: bootstrap-mongo-orchestration + vars: + LOAD_BALANCER: 'true' + - func: run-load-balancer + - func: run-load-balancer-tests + vars: + FRAMEWORK: netstandard20 + - func: stop-load-balancer + + - name: test-load-balancer-netstandard21 + commands: + - func: bootstrap-mongo-orchestration + vars: + LOAD_BALANCER: 'true' + - func: run-load-balancer + - func: run-load-balancer-tests + vars: + FRAMEWORK: netstandard21 + - func: stop-load-balancer + + - name: atlas-connectivity-tests + commands: + - func: run-atlas-connectivity-tests + + - name: test-gssapi + commands: + - func: run-gssapi-auth-tests + + - name: test-gssapi-net472 + commands: + - func: run-gssapi-auth-tests + vars: + FRAMEWORK: net472 + + - name: test-gssapi-netstandard20 + commands: + - func: run-gssapi-auth-tests + vars: + FRAMEWORK: netstandard20 + + - name: test-gssapi-netstandard21 + commands: + - func: run-gssapi-auth-tests + vars: + FRAMEWORK: netstandard21 + + - name: plain-auth-tests + commands: + - func: run-plain-auth-tests + + - name: aws-auth-tests + commands: + - func: bootstrap-mongo-orchestration + vars: + AUTH: "auth" + ORCHESTRATION_FILE: "auth-aws.json" + TOPOLOGY: "server" + - func: add-aws-auth-variables-to-file + # This step also creates test related users, so don't avoid this step in order to run other tests + - func: run-aws-auth-test-with-regular-aws-credentials + - func: run-aws-auth-test-with-assume-role-credentials + - func: run-aws-auth-test-with-aws-credentials-as-environment-variables + # This step requires running run-aws-auth-test-with-assume-role-credentials before to explicitly set up instance profile + - func: run-aws-auth-test-with-aws-credentials-and-session-token-as-environment-variables + - func: run-aws-auth-test-with-aws-EC2-credentials + - func: run-aws-auth-test-with-aws-ECS-credentials + - func: run-aws-auth-test-with-aws-web-identity-credentials + + - name: stable-api-tests-net472 + commands: + - func: bootstrap-mongo-orchestration + vars: + REQUIRE_API_VERSION: true + - func: run-tests + vars: + FRAMEWORK: net472 + REQUIRE_API_VERSION: true + + - name: stable-api-tests-netstandard20 + commands: + - func: bootstrap-mongo-orchestration + vars: + REQUIRE_API_VERSION: true + - func: run-tests + vars: + FRAMEWORK: netstandard20 + REQUIRE_API_VERSION: true + + - name: stable-api-tests-netstandard21 + commands: + - func: bootstrap-mongo-orchestration + vars: + REQUIRE_API_VERSION: true + - func: run-tests + vars: + FRAMEWORK: netstandard21 + REQUIRE_API_VERSION: true + + - name: atlas-data-lake-test + commands: + - func: bootstrap-mongohoused + - func: run-atlas-data-lake-test + + - name: atlas-search-test + commands: + - func: run-atlas-search-test + + - name: atlas-search-index-helpers-test + commands: + - func: run-atlas-search-index-helpers-test + + - name: test-oidc-auth-aws + commands: + - func: assume-ec2-role + - func: run-mongodb-oidc-tests + + - name: test-serverless + exec_timeout_secs: 2700 # 45 minutes: 15 for setup + 30 for tests + commands: + - func: create-serverless-instance + - func: run-serverless-tests + + - name: test-ocsp-rsa-valid-cert-server-staples-ca-responder + tags: ["ocsp"] + commands: + - func: run-valid-ocsp-server-ca-responder + vars: + OCSP_ALGORITHM: "rsa" + - func: ocsp-bootstrap-mongo-orchestration + vars: + ORCHESTRATION_FILE: "rsa-basic-tls-ocsp-mustStaple.json" + - func: run-ocsp-test + vars: + OCSP_ALGORITHM: "rsa" + OCSP_TLS_SHOULD_SUCCEED: "true" + + - name: test-ocsp-rsa-invalid-cert-server-staples-ca-responder + tags: ["ocsp"] + commands: + - func: run-revoked-ocsp-server-ca-responder + vars: + OCSP_ALGORITHM: "rsa" + - func: ocsp-bootstrap-mongo-orchestration + vars: + ORCHESTRATION_FILE: "rsa-basic-tls-ocsp-mustStaple.json" + - func: run-ocsp-test + vars: + OCSP_ALGORITHM: "rsa" + OCSP_TLS_SHOULD_SUCCEED: "false" + + - name: test-ocsp-rsa-valid-cert-server-does-not-staple-ca-responder + tags: ["ocsp"] + commands: + - func: run-valid-ocsp-server-ca-responder + vars: + OCSP_ALGORITHM: "rsa" + - func: ocsp-bootstrap-mongo-orchestration + vars: + ORCHESTRATION_FILE: "rsa-basic-tls-ocsp-disableStapling.json" + - func: run-ocsp-test + vars: + OCSP_ALGORITHM: "rsa" + OCSP_TLS_SHOULD_SUCCEED: "true" + + - name: test-ocsp-rsa-invalid-cert-server-does-not-staple-ca-responder + tags: ["ocsp"] + commands: + - func: run-revoked-ocsp-server-ca-responder + vars: + OCSP_ALGORITHM: "rsa" + - func: ocsp-bootstrap-mongo-orchestration + vars: + ORCHESTRATION_FILE: "rsa-basic-tls-ocsp-disableStapling.json" + - func: run-ocsp-test + vars: + OCSP_ALGORITHM: "rsa" + OCSP_TLS_SHOULD_SUCCEED: "false" + + - name: test-ocsp-rsa-soft-fail + tags: ["ocsp"] + commands: + - func: ocsp-bootstrap-mongo-orchestration + vars: + ORCHESTRATION_FILE: "rsa-basic-tls-ocsp-disableStapling.json" + - func: run-ocsp-test + vars: + OCSP_ALGORITHM: "rsa" + OCSP_TLS_SHOULD_SUCCEED: "false" # Spec mandates true, but .NET on Windows hard fails in this case + + - name: test-ocsp-rsa-malicious-invalid-cert-mustStaple-server-does-not-staple-ca-responder + tags: ["ocsp"] + commands: + - func: run-revoked-ocsp-server-ca-responder + vars: + OCSP_ALGORITHM: "rsa" + - func: ocsp-bootstrap-mongo-orchestration + vars: + ORCHESTRATION_FILE: "rsa-basic-tls-ocsp-mustStaple-disableStapling.json" + - func: run-ocsp-test + vars: + OCSP_ALGORITHM: "rsa" + OCSP_TLS_SHOULD_SUCCEED: "false" + + - name: test-ocsp-rsa-malicious-no-responder-mustStaple-server-does-not-staple + tags: ["ocsp"] + commands: + - func: ocsp-bootstrap-mongo-orchestration + vars: + ORCHESTRATION_FILE: "rsa-basic-tls-ocsp-mustStaple-disableStapling.json" + - func: run-ocsp-test + vars: + OCSP_ALGORITHM: "rsa" + OCSP_TLS_SHOULD_SUCCEED: "false" + + - name: test-ocsp-rsa-valid-cert-server-staples-delegate-responder + tags: ["ocsp"] + commands: + - func: run-valid-ocsp-server-delegate-responder + vars: + OCSP_ALGORITHM: "rsa" + - func: ocsp-bootstrap-mongo-orchestration + vars: + ORCHESTRATION_FILE: "rsa-basic-tls-ocsp-mustStaple.json" + - func: run-ocsp-test + vars: + OCSP_ALGORITHM: "rsa" + OCSP_TLS_SHOULD_SUCCEED: "true" + + - name: test-ocsp-rsa-invalid-cert-server-staples-delegate-responder + tags: ["ocsp"] + commands: + - func: run-revoked-ocsp-server-delegate-responder + vars: + OCSP_ALGORITHM: "rsa" + - func: ocsp-bootstrap-mongo-orchestration + vars: + ORCHESTRATION_FILE: "rsa-basic-tls-ocsp-mustStaple.json" + - func: run-ocsp-test + vars: + OCSP_ALGORITHM: "rsa" + OCSP_TLS_SHOULD_SUCCEED: "false" + + - name: test-ocsp-rsa-valid-cert-server-does-not-staple-delegate-responder + tags: ["ocsp"] + commands: + - func: run-valid-ocsp-server-delegate-responder + vars: + OCSP_ALGORITHM: "rsa" + - func: ocsp-bootstrap-mongo-orchestration + vars: + ORCHESTRATION_FILE: "rsa-basic-tls-ocsp-disableStapling.json" + - func: run-ocsp-test + vars: + OCSP_ALGORITHM: "rsa" + OCSP_TLS_SHOULD_SUCCEED: "true" + + - name: test-ocsp-rsa-invalid-cert-server-does-not-staple-delegate-responder + tags: ["ocsp"] + commands: + - func: run-revoked-ocsp-server-delegate-responder + vars: + OCSP_ALGORITHM: "rsa" + - func: ocsp-bootstrap-mongo-orchestration + vars: + ORCHESTRATION_FILE: "rsa-basic-tls-ocsp-disableStapling.json" + - func: run-ocsp-test + vars: + OCSP_ALGORITHM: "rsa" + OCSP_TLS_SHOULD_SUCCEED: "false" + + - name: test-ocsp-rsa-malicious-invalid-cert-mustStaple-server-does-not-staple-delegate-responder + tags: ["ocsp"] + commands: + - func: run-revoked-ocsp-server-delegate-responder + vars: + OCSP_ALGORITHM: "rsa" + - func: ocsp-bootstrap-mongo-orchestration + vars: + ORCHESTRATION_FILE: "rsa-basic-tls-ocsp-mustStaple-disableStapling.json" + - func: run-ocsp-test + vars: + OCSP_ALGORITHM: "rsa" + OCSP_TLS_SHOULD_SUCCEED: "false" + + - name: test-smoke-tests-net472 + commands: + - func: bootstrap-mongo-orchestration + - func: run-smoke-tests + vars: + FRAMEWORK: net472 + + - name: test-smoke-tests-netcoreapp21 + commands: + - func: bootstrap-mongo-orchestration + - func: run-smoke-tests + vars: + FRAMEWORK: netcoreapp21 + + - name: test-smoke-tests-netcoreapp31 + commands: + - func: bootstrap-mongo-orchestration + - func: run-smoke-tests + vars: + FRAMEWORK: netcoreapp31 + + - name: test-smoke-tests-net50 + commands: + - func: bootstrap-mongo-orchestration + - func: run-smoke-tests + vars: + FRAMEWORK: net50 + + - name: test-smoke-tests-net60 + commands: + - func: bootstrap-mongo-orchestration + - func: run-smoke-tests + vars: + FRAMEWORK: net60 + + - name: test-smoke-tests-net80 + commands: + - func: bootstrap-mongo-orchestration + - func: run-smoke-tests + vars: + FRAMEWORK: net80 + + - name: performance-tests-net60-server-v6.0 + commands: + - func: bootstrap-mongo-orchestration + vars: + VERSION: "v6.0-perf" + TOPOLOGY: "server" + SSL: "nossl" + AUTH: "noauth" + SKIP_LEGACY_SHELL: "true" + - func: install-dotnet + - func: run-performance-tests + + - name: test-aws-lambda-deployed + commands: + - command: ec2.assume_role + params: + role_arn: ${LAMBDA_AWS_ROLE_ARN} + duration_seconds: 3600 + - command: shell.exec + params: + working_dir: mongo-csharp-driver + add_expansions_to_env: true + script: | + ${PREPARE_SHELL} + evergreen/run-deployed-lambda-aws-tests.sh + env: + TEST_LAMBDA_DIRECTORY: ${PROJECT_DIRECTORY}/tests/FaasTests/LambdaTests + AWS_REGION: us-east-1 + + # ECDSA tests + # Disabled until https://jira.mongodb.org/browse/SPEC-1589 is resolved + # - name: test-ocsp-ecdsa-valid-cert-server-staples-ca-responder + # tags: ["ocsp"] + # commands: + # - func: run-valid-ocsp-server-ca-responder + # vars: + # OCSP_ALGORITHM: "ecdsa" + # - func: ocsp-bootstrap-mongo-orchestration + # vars: + # ORCHESTRATION_FILE: "ecdsa-basic-tls-ocsp-mustStaple.json" + # - func: run-ocsp-test + # vars: + # OCSP_ALGORITHM: "ecdsa" + # OCSP_TLS_SHOULD_SUCCEED: "true" + + # - name: test-ocsp-ecdsa-invalid-cert-server-staples-ca-responder + # tags: ["ocsp"] + # commands: + # - func: run-revoked-ocsp-server-ca-responder + # vars: + # OCSP_ALGORITHM: "ecdsa" + # - func: ocsp-bootstrap-mongo-orchestration + # vars: + # ORCHESTRATION_FILE: "ecdsa-basic-tls-ocsp-mustStaple.json" + # - func: run-ocsp-test + # vars: + # OCSP_ALGORITHM: "ecdsa" + # OCSP_TLS_SHOULD_SUCCEED: "false" + + # - name: test-ocsp-ecdsa-valid-cert-server-does-not-staple-ca-responder + # tags: ["ocsp"] + # commands: + # - func: run-valid-ocsp-server-ca-responder + # vars: + # OCSP_ALGORITHM: "ecdsa" + # - func: ocsp-bootstrap-mongo-orchestration + # vars: + # ORCHESTRATION_FILE: "ecdsa-basic-tls-ocsp-disableStapling.json" + # - func: run-ocsp-test + # vars: + # OCSP_ALGORITHM: "ecdsa" + # OCSP_TLS_SHOULD_SUCCEED: "true" + + # - name: test-ocsp-ecdsa-invalid-cert-server-does-not-staple-ca-responder + # tags: ["ocsp"] + # commands: + # - func: run-revoked-ocsp-server-ca-responder + # vars: + # OCSP_ALGORITHM: "ecdsa" + # - func: ocsp-bootstrap-mongo-orchestration + # vars: + # ORCHESTRATION_FILE: "ecdsa-basic-tls-ocsp-disableStapling.json" + # - func: run-ocsp-test + # vars: + # OCSP_ALGORITHM: "ecdsa" + # OCSP_TLS_SHOULD_SUCCEED: "false" + + # - name: test-ocsp-ecdsa-soft-fail + # tags: ["ocsp"] + # commands: + # - func: ocsp-bootstrap-mongo-orchestration + # vars: + # ORCHESTRATION_FILE: "ecdsa-basic-tls-ocsp-disableStapling.json" + # - func: run-ocsp-test + # vars: + # OCSP_ALGORITHM: "ecdsa" + # OCSP_TLS_SHOULD_SUCCEED: "false" # Spec mandates true but .NET on Windows hard fails in this case + + # - name: test-ocsp-ecdsa-malicious-invalid-cert-mustStaple-server-does-not-staple-ca-responder + # tags: ["ocsp"] + # commands: + # - func: run-revoked-ocsp-server-ca-responder + # vars: + # OCSP_ALGORITHM: "ecdsa" + # - func: ocsp-bootstrap-mongo-orchestration + # vars: + # ORCHESTRATION_FILE: "ecdsa-basic-tls-ocsp-mustStaple-disableStapling.json" + # - func: run-ocsp-test + # vars: + # OCSP_ALGORITHM: "ecdsa" + # OCSP_TLS_SHOULD_SUCCEED: "false" + + # - name: test-ocsp-ecdsa-malicious-no-responder-mustStaple-server-does-not-staple + # tags: ["ocsp"] + # commands: + # - func: ocsp-bootstrap-mongo-orchestration + # vars: + # ORCHESTRATION_FILE: "ecdsa-basic-tls-ocsp-mustStaple-disableStapling.json" + # - func: run-ocsp-test + # vars: + # OCSP_ALGORITHM: "ecdsa" + # OCSP_TLS_SHOULD_SUCCEED: "false" + + # - name: test-ocsp-ecdsa-valid-cert-server-staples-delegate-responder + # tags: ["ocsp"] + # commands: + # - func: run-valid-ocsp-server-delegate-responder + # vars: + # OCSP_ALGORITHM: "ecdsa" + # - func: ocsp-bootstrap-mongo-orchestration + # vars: + # ORCHESTRATION_FILE: "ecdsa-basic-tls-ocsp-mustStaple.json" + # - func: run-ocsp-test + # vars: + # OCSP_ALGORITHM: "ecdsa" + # OCSP_TLS_SHOULD_SUCCEED: "true" + + # - name: test-ocsp-ecdsa-invalid-cert-server-staples-delegate-responder + # tags: ["ocsp"] + # commands: + # - func: run-revoked-ocsp-server-delegate-responder + # vars: + # OCSP_ALGORITHM: "ecdsa" + # - func: ocsp-bootstrap-mongo-orchestration + # vars: + # ORCHESTRATION_FILE: "ecdsa-basic-tls-ocsp-mustStaple.json" + # - func: run-ocsp-test + # vars: + # OCSP_ALGORITHM: "ecdsa" + # OCSP_TLS_SHOULD_SUCCEED: "false" + + # - name: test-ocsp-ecdsa-valid-cert-server-does-not-staple-delegate-responder + # tags: ["ocsp"] + # commands: + # - func: run-valid-ocsp-server-delegate-responder + # vars: + # OCSP_ALGORITHM: "ecdsa" + # - func: ocsp-bootstrap-mongo-orchestration + # vars: + # ORCHESTRATION_FILE: "ecdsa-basic-tls-ocsp-disableStapling.json" + # - func: run-ocsp-test + # vars: + # OCSP_ALGORITHM: "ecdsa" + # OCSP_TLS_SHOULD_SUCCEED: "true" + + # - name: test-ocsp-ecdsa-invalid-cert-server-does-not-staple-delegate-responder + # tags: ["ocsp"] + # commands: + # - func: run-revoked-ocsp-server-delegate-responder + # vars: + # OCSP_ALGORITHM: "ecdsa" + # - func: ocsp-bootstrap-mongo-orchestration + # vars: + # ORCHESTRATION_FILE: "ecdsa-basic-tls-ocsp-disableStapling.json" + # - func: run-ocsp-test + # vars: + # OCSP_ALGORITHM: "ecdsa" + # OCSP_TLS_SHOULD_SUCCEED: "false" + + # - name: test-ocsp-ecdsa-malicious-invalid-cert-mustStaple-server-does-not-staple-delegate-responder + # tags: ["ocsp"] + # commands: + # - func: run-revoked-ocsp-server-delegate-responder + # vars: + # OCSP_ALGORITHM: "ecdsa" + # - func: ocsp-bootstrap-mongo-orchestration + # vars: + # ORCHESTRATION_FILE: "ecdsa-basic-tls-ocsp-mustStaple-disableStapling.json" + # - func: run-ocsp-test + # vars: + # OCSP_ALGORITHM: "ecdsa" + # OCSP_TLS_SHOULD_SUCCEED: "false" + + - name: test-csfle-with-azure-kms + commands: + - command: shell.exec + type: setup + params: + working_dir: mongo-csharp-driver + shell: "bash" + script: | + ${PREPARE_SHELL} + echo "Copying files ... begin" + export AZUREKMS_RESOURCEGROUP=${testazurekms_resourcegroup} + export AZUREKMS_VMNAME=${AZUREKMS_VMNAME} + export AZUREKMS_PRIVATEKEYPATH=/tmp/testazurekms_privatekey + tar czf /tmp/mongo-csharp-driver.tgz . + AZUREKMS_SRC=/tmp/mongo-csharp-driver.tgz AZUREKMS_DST="~/" $DRIVERS_TOOLS/.evergreen/csfle/azurekms/copy-file.sh + echo "Copying files ... end" + echo "Untarring file ... begin" + AZUREKMS_CMD="tar xf mongo-csharp-driver.tgz" $DRIVERS_TOOLS/.evergreen/csfle/azurekms/run-command.sh + echo "Untarring file ... end" + + - command: shell.exec + type: test + params: + working_dir: "mongo-csharp-driver" + shell: "bash" + script: | + ${PREPARE_SHELL} + export AZUREKMS_RESOURCEGROUP=${testazurekms_resourcegroup} + export AZUREKMS_VMNAME=${AZUREKMS_VMNAME} + export AZUREKMS_PRIVATEKEYPATH=/tmp/testazurekms_privatekey + AZUREKMS_CMD="MONGODB_URI='mongodb://localhost:27017' KEY_NAME='${testazurekms_keyname}' KEY_VAULT_ENDPOINT='${testazurekms_keyvaultendpoint}' ./evergreen/run-csfle-azure-tests.sh" $DRIVERS_TOOLS/.evergreen/csfle/azurekms/run-command.sh + + - name: test-csfle-with-gcp-kms + commands: + - command: shell.exec + type: setup + params: + working_dir: mongo-csharp-driver + shell: "bash" + script: | + ${PREPARE_SHELL} + echo "Copying files ... begin" + export GCPKMS_GCLOUD=${GCPKMS_GCLOUD} + export GCPKMS_PROJECT=${GCPKMS_PROJECT} + export GCPKMS_ZONE=${GCPKMS_ZONE} + export GCPKMS_INSTANCENAME=${GCPKMS_INSTANCENAME} + tar czf /tmp/mongo-csharp-driver.tgz . + GCPKMS_SRC=/tmp/mongo-csharp-driver.tgz GCPKMS_DST=$GCPKMS_INSTANCENAME: $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/copy-file.sh + echo "Copying files ... end" + echo "Untarring file ... begin" + GCPKMS_CMD="tar xf mongo-csharp-driver.tgz" $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh + echo "Untarring file ... end" + + - command: shell.exec + type: test + params: + working_dir: "mongo-csharp-driver" + shell: "bash" + script: | + ${PREPARE_SHELL} + export GCPKMS_GCLOUD=${GCPKMS_GCLOUD} + export GCPKMS_PROJECT=${GCPKMS_PROJECT} + export GCPKMS_ZONE=${GCPKMS_ZONE} + export GCPKMS_INSTANCENAME=${GCPKMS_INSTANCENAME} + GCPKMS_CMD="MONGODB_URI='mongodb://localhost:27017' ./evergreen/run-csfle-gcp-tests.sh" $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh + + - name: build-packages + commands: + - func: install-dotnet + - func: build-packages + - func: upload-package + vars: + PACKAGE_ID: "MongoDB.Bson" + - func: upload-package + vars: + PACKAGE_ID: "MongoDB.Driver" + - func: upload-package + vars: + PACKAGE_ID: "MongoDB.Driver.Core" + - func: upload-package + vars: + PACKAGE_ID: "MongoDB.Driver.GridFS" + - func: upload-package + vars: + PACKAGE_ID: "mongocsharpdriver" + + - name: push-packages + commands: + - func: install-dotnet + - func: download-package + vars: + PACKAGE_ID: "MongoDB.Bson" + - func: download-package + vars: + PACKAGE_ID: "MongoDB.Driver" + - func: download-package + vars: + PACKAGE_ID: "MongoDB.Driver.Core" + - func: download-package + vars: + PACKAGE_ID: "MongoDB.Driver.GridFS" + - func: download-package + vars: + PACKAGE_ID: "mongocsharpdriver" + - func: push-packages + vars: + PACKAGES_SOURCE: "https://api.nuget.org/v3/index.json" + PACKAGES_SOURCE_KEY: ${nuget_api_key} + + - name: generate-apidocs + commands: + - func: install-dotnet + - func: build-apidocs + - func: upload-apidocs + +axes: + - id: version + display_name: MongoDB Version + values: + - id: "latest" + display_name: "latest" + variables: + VERSION: "latest" + - id: "rapid" + display_name: "rapid" + variables: + VERSION: "rapid" + - id: "7.0" + display_name: "7.0" + variables: + VERSION: "7.0" + - id: "6.0" + display_name: "6.0" + variables: + VERSION: "6.0" + - id: "5.0" + display_name: "5.0" + variables: + VERSION: "5.0" + - id: "4.4" + display_name: "4.4" + variables: + VERSION: "4.4" + - id: "4.2" + display_name: "4.2" + variables: + VERSION: "4.2" + - id: "4.0" + display_name: "4.0" + variables: + VERSION: "4.0" + - id: "3.6" + display_name: "3.6" + variables: + VERSION: "3.6" + + - id: os + display_name: OS + values: + - id: "windows-64" + display_name: "Windows 64-bit" + variables: + OS: "windows-64" + python3_binary: "C:/python/Python38/python.exe" + skip_ECS_auth_test: true + skip_web_identity_auth_test: true + run_on: windows-64-vs2017-test + - id: "ubuntu-1804" + display_name: "Ubuntu 18.04" + variables: + OS: "ubuntu-1804" + python3_binary: "/opt/python/3.8/bin/python3" + run_on: ubuntu1804-test + - id: "ubuntu-2004" + display_name: "Ubuntu 20.04" + variables: + OS: "ubuntu-2004" + python3_binary: "/opt/python/3.8/bin/python3" + run_on: ubuntu2004-small + - id: "macos-1100" + display_name: "macOS 11.00" + variables: + OS: "macos-1100" + python3_binary: /Library/Frameworks/Python.framework/Versions/3.8/bin/python3 + skip_EC2_auth_test: true + skip_ECS_auth_test: true + skip_web_identity_auth_test: true + run_on: macos-1100 + - id: "macos-1100-arm64" + display_name: "macOS 11.00 M1" + variables: + OS: "macos-1100-arm64" + python3_binary: /Library/Frameworks/Python.framework/Versions/3.8/bin/python3 + skip_EC2_auth_test: true + skip_ECS_auth_test: true + skip_web_identity_auth_test: true + run_on: macos-1100-arm64 + + - id: topology + display_name: Topology + values: + - id: "standalone" + display_name: Standalone + variables: + TOPOLOGY: "server" + - id: "replicaset" + display_name: Replica Set + variables: + TOPOLOGY: "replica_set" + - id: "sharded-cluster" + display_name: Sharded Cluster + variables: + TOPOLOGY: "sharded_cluster" + + - id: auth + display_name: Authentication + values: + - id: "auth" + display_name: Auth + variables: + AUTH: "auth" + - id: "noauth" + display_name: NoAuth + variables: + AUTH: "noauth" + + - id: ssl + display_name: SSL + values: + - id: "ssl" + display_name: SSL + variables: + SSL: "ssl" + - id: "nossl" + display_name: NoSSL + variables: + SSL: "nossl" + + - id: compressor + display_name: Compressor + values: + - id: "zlib" + display_name: Zlib + variables: + COMPRESSOR: "zlib" + - id: "snappy" + display_name: Snappy + variables: + COMPRESSOR: "snappy" + - id: "zstandard" + display_name: Zstandard + variables: + COMPRESSOR: "zstd" + + - id: target_framework + display_name: Target .net framework + values: + - id: "net472" + display_name: net472 + variables: + FRAMEWORK: net472 + - id: "netstandard20" + display_name: netstandard20 + variables: + FRAMEWORK: netstandard20 + - id: "netstandard21" + display_name: netstandard21 + variables: + FRAMEWORK: netstandard21 + + - id: serverless_proxy_type + display_name: Serverless Proxy Type + values: + - id: "Passthrough" + display_name: "Serverless Passthrough Proxy" + variables: + SERVERLESS_PROXY_TYPE: Passthrough + - id: "Terminating" + display_name: "Serverless Terminating Proxy" + variables: + SERVERLESS_PROXY_TYPE: Terminating + + - id: build-target + display_name: CI build target + values: + - id: "tests" + display_name: "tests" + variables: + BUILD_TARGET: "tests" + - id: "release" + display_name: "release" + variables: + BUILD_TARGET: "release" + +task_groups: + - name: testazurekms-task-group + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 # 30 minutes + setup_group: + - func: fetch-source + - func: prepare-resources + - func: windows-fix + - func: fix-absolute-paths + - func: init-test-results + - func: make-files-executable + - command: shell.exec + params: + shell: "bash" + silent: true + env: + AZUREKMS_CLIENTID : ${testazurekms_clientid} + AZUREKMS_TENANTID : ${testazurekms_tenantid} + AZUREKMS_SECRET= : ${testazurekms_secret} + AZUREKMS_RESOURCEGROUP: ${testazurekms_resourcegroup} + AZUREKMS_SCOPE : ${testazurekms_scope} + script: | + ${PREPARE_SHELL} + echo '${testazurekms_publickey}' > /tmp/testazurekms_publickey + echo '${testazurekms_privatekey}' > /tmp/testazurekms_privatekey + # Set 600 permissions on private key file. Otherwise ssh / scp may error with permissions "are too open". + chmod 600 /tmp/testazurekms_privatekey + + export AZUREKMS_DRIVERS_TOOLS=$DRIVERS_TOOLS + export AZUREKMS_PUBLICKEYPATH=/tmp/testazurekms_publickey + export AZUREKMS_PRIVATEKEYPATH=/tmp/testazurekms_privatekey + export AZUREKMS_VMNAME_PREFIX=CSHARPDRIVER + $DRIVERS_TOOLS/.evergreen/csfle/azurekms/create-and-setup-vm.sh + - command: expansions.update + params: + file: testazurekms-expansions.yml + teardown_group: + - func: upload-test-results + # Load expansions again. The setup task may have failed before running `expansions.update`. + - command: expansions.update + params: + file: testazurekms-expansions.yml + - command: shell.exec + params: + shell: "bash" + env: + AZUREKMS_VMNAME : ${AZUREKMS_VMNAME} + AZUREKMS_RESOURCEGROUP : ${testazurekms_resourcegroup} + script: | + ${PREPARE_SHELL} + $DRIVERS_TOOLS/.evergreen/csfle/azurekms/delete-vm.sh + tasks: + - test-csfle-with-azure-kms + + - name: testgcpkms-task-group + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 # 30 minutes + setup_group: + - func: fetch-source + - func: prepare-resources + - func: windows-fix + - func: fix-absolute-paths + - func: init-test-results + - func: make-files-executable + - command: shell.exec + params: + shell: "bash" + silent: true + include_expansions_in_env: + - "GCPKMS_SERVICEACCOUNT" + script: | + ${PREPARE_SHELL} + echo '${GOOGLE_APPLICATION_CREDENTIALS_CONTENT}' > /tmp/testgcpkms_key_file.json + export GCPKMS_KEYFILE=/tmp/testgcpkms_key_file.json + export GCPKMS_DRIVERS_TOOLS=$DRIVERS_TOOLS + export GCPKMS_MACHINETYPE="e2-standard-4" + $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/create-and-setup-instance.sh + # Load the GCPKMS_GCLOUD, GCPKMS_INSTANCE, GCPKMS_REGION, and GCPKMS_ZONE expansions. + - command: expansions.update + params: + file: testgcpkms-expansions.yml + teardown_group: + - func: upload-test-results + - command: shell.exec + params: + shell: "bash" + script: | + ${PREPARE_SHELL} + export GCPKMS_GCLOUD=${GCPKMS_GCLOUD} + export GCPKMS_PROJECT=${GCPKMS_PROJECT} + export GCPKMS_ZONE=${GCPKMS_ZONE} + export GCPKMS_INSTANCENAME=${GCPKMS_INSTANCENAME} + $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/delete-instance.sh + tasks: + - test-csfle-with-gcp-kms + + - name: atlas-search-index-helpers-task-group + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 # 30 minutes + setup_group: + - func: fetch-source + - func: prepare-resources + - func: fix-absolute-paths + - func: init-test-results + - func: make-files-executable + - command: shell.exec + params: + env: + DRIVERS_ATLAS_PUBLIC_API_KEY: "${DRIVERS_ATLAS_PUBLIC_API_KEY}" + DRIVERS_ATLAS_PRIVATE_API_KEY: "${DRIVERS_ATLAS_PRIVATE_API_KEY}" + DRIVERS_ATLAS_GROUP_ID: "${DRIVERS_ATLAS_GROUP_ID}" + DRIVERS_ATLAS_LAMBDA_USER: "${DRIVERS_ATLAS_LAMBDA_USER}" + DRIVERS_ATLAS_LAMBDA_PASSWORD: "${DRIVERS_ATLAS_LAMBDA_PASSWORD}" + LAMBDA_STACK_NAME: "${LAMBDA_STACK_NAME}" + add_expansions_to_env: true + shell: "bash" + script: | + ${PREPARE_SHELL} + $DRIVERS_TOOLS/.evergreen/atlas/setup-atlas-cluster.sh + - command: expansions.update + params: + file: atlas-expansion.yml + teardown_group: + - func: upload-test-results + - command: shell.exec + params: + env: + DRIVERS_ATLAS_PUBLIC_API_KEY: "${DRIVERS_ATLAS_PUBLIC_API_KEY}" + DRIVERS_ATLAS_PRIVATE_API_KEY: "${DRIVERS_ATLAS_PRIVATE_API_KEY}" + DRIVERS_ATLAS_GROUP_ID: "${DRIVERS_ATLAS_GROUP_ID}" + DRIVERS_ATLAS_LAMBDA_USER: "${DRIVERS_ATLAS_LAMBDA_USER}" + DRIVERS_ATLAS_LAMBDA_PASSWORD: "${DRIVERS_ATLAS_LAMBDA_PASSWORD}" + LAMBDA_STACK_NAME: "${LAMBDA_STACK_NAME}" + add_expansions_to_env: true + shell: "bash" + script: | + ${PREPARE_SHELL} + $DRIVERS_TOOLS/.evergreen/atlas/teardown-atlas-cluster.sh + tasks: + - atlas-search-index-helpers-test + + - name: test-aws-lambda-task-group + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 # 30 minutes + setup_group: + - func: fetch-source + - func: prepare-resources + - func: install-dotnet + - command: subprocess.exec + params: + binary: bash + add_expansions_to_env: true + args: + - ${DRIVERS_TOOLS}/.evergreen/atlas/setup-atlas-cluster.sh + - command: expansions.update + params: + file: atlas-expansion.yml + teardown_group: + - command: subprocess.exec + params: + binary: bash + add_expansions_to_env: true + args: + - ${DRIVERS_TOOLS}/.evergreen/atlas/teardown-atlas-cluster.sh + tasks: + - test-aws-lambda-deployed + +buildvariants: +- matrix_name: stable-api-tests + matrix_spec: { version: ["5.0", "6.0", "7.0", "rapid", "latest"], topology: "standalone", auth: "auth", ssl: "nossl", os: "windows-64" } + display_name: "Stable API ${version} ${topology} ${auth} ${ssl} ${os}" + run_on: + - windows-64-vs2017-test + tasks: + - name: stable-api-tests-net472 + - name: stable-api-tests-netstandard20 + - name: stable-api-tests-netstandard21 + +# Secure tests +- matrix_name: "secure-tests-windows" + matrix_spec: { version: "*", topology: "*", auth: "auth", ssl: "ssl", os: "windows-64" } + display_name: "${version} ${topology} ${auth} ${ssl} ${os}" + tags: ["tests-variant"] + tasks: + - name: test-net472 + - name: test-netstandard20 + - name: test-netstandard21 + +- matrix_name: "secure-tests-macOS" + matrix_spec: { version: ["5.0", "6.0", "7.0", "rapid", "latest"], topology: "replicaset", auth: "auth", ssl: "ssl", os: ["macos-1100", "macos-1100-arm64"] } + display_name: "${version} ${topology} ${auth} ${ssl} ${os}" + tags: ["tests-variant"] + tasks: + - name: test-netstandard21 + +- matrix_name: "secure-tests-linux-1804" + matrix_spec: { version: ["4.0", "4.2", "4.4", "5.0", "6.0"], topology: "*", auth: "auth", ssl: "ssl", os: "ubuntu-1804" } + display_name: "${version} ${topology} ${auth} ${ssl} ${os}" + tags: ["tests-variant"] + tasks: + - name: test-netstandard20 + - name: test-netstandard21 + +- matrix_name: "secure-tests-linux-2004" + matrix_spec: { version: ["7.0", "rapid", "latest"], topology: "*", auth: "auth", ssl: "ssl", os: "ubuntu-2004" } + display_name: "${version} ${topology} ${auth} ${ssl} ${os}" + tags: ["tests-variant"] + tasks: + - name: test-netstandard20 + - name: test-netstandard21 + +# Unsecure tests +- matrix_name: "unsecure-tests-windows" + matrix_spec: { version: "*", topology: "*", auth: "noauth", ssl: "nossl", os: "windows-64" } + display_name: "${version} ${topology} ${auth} ${ssl} ${os}" + tags: ["tests-variant"] + tasks: + - name: test-net472 + - name: test-netstandard20 + - name: test-netstandard21 + +- matrix_name: "unsecure-tests-macOS" + matrix_spec: { version: ["5.0", "6.0", "7.0", "rapid", "latest"], topology: "replicaset", auth: "noauth", ssl: "nossl", os: ["macos-1100", "macos-1100-arm64"] } + display_name: "${version} ${topology} ${auth} ${ssl} ${os}" + tags: ["tests-variant"] + tasks: + - name: test-netstandard21 + +- matrix_name: "unsecure-tests-linux-1804" + matrix_spec: { version: ["3.6", "4.0", "4.2", "4.4", "5.0", "6.0"], topology: "*", auth: "noauth", ssl: "nossl", os: "ubuntu-1804" } + display_name: "${version} ${topology} ${auth} ${ssl} ${os}" + tags: ["tests-variant"] + tasks: + - name: test-netstandard20 + - name: test-netstandard21 + +- matrix_name: "unsecure-tests-linux-2004" + matrix_spec: { version: ["7.0", "rapid", "latest"], topology: "*", auth: "noauth", ssl: "nossl", os: "ubuntu-2004" } + display_name: "${version} ${topology} ${auth} ${ssl} ${os}" + tags: ["tests-variant"] + tasks: + - name: test-netstandard20 + - name: test-netstandard21 + +# Compression tests +- matrix_name: "tests-compression-windows" + matrix_spec: { compressor: "*", auth: "noauth", ssl: "nossl", version: "*", topology: "standalone", os: "windows-64" } + display_name: "${version} ${compressor} ${topology} ${auth} ${ssl} ${os} " + tags: ["tests-variant"] + tasks: + - name: test-net472 + - name: test-netstandard20 + - name: test-netstandard21 + +- matrix_name: "tests-compression-macOS" + matrix_spec: { compressor : ["snappy", "zstandard"], auth: "noauth", ssl: "nossl", version: ["5.0", "6.0", "7.0", "rapid", "latest"], topology: "standalone", os: ["macos-1100", "macos-1100-arm64"] } + display_name: "${version} ${compressor} ${topology} ${auth} ${ssl} ${os} " + tags: ["tests-variant"] + tasks: + - name: test-netstandard21 + +- matrix_name: "tests-compression-linux-1804" + matrix_spec: { compressor: "*", auth: "noauth", ssl: "nossl", version: ["3.6", "4.0", "4.2", "4.4", "5.0", "6.0"], topology: "standalone", os: "ubuntu-1804" } + display_name: "${version} ${compressor} ${topology} ${auth} ${ssl} ${os} " + tags: ["tests-variant"] + tasks: + - name: test-netstandard20 + - name: test-netstandard21 + +- matrix_name: "tests-compression-linux-2004" + matrix_spec: { compressor: "*", auth: "noauth", ssl: "nossl", version: ["7.0", "rapid", "latest"], topology: "standalone", os: "ubuntu-2004" } + display_name: "${version} ${compressor} ${topology} ${auth} ${ssl} ${os} " + tags: ["tests-variant"] + tasks: + - name: test-netstandard20 + - name: test-netstandard21 + +# Auth tests +- matrix_name: plain-auth-tests + matrix_spec: { os: "*" } + display_name: "PLAIN (LDAP) Auth Tests ${os}" + tasks: + - name: plain-auth-tests + +- matrix_name: mongodb-oidc-tests + matrix_spec: { os: [ "windows-64", "ubuntu-2004", "macos-1100" ] } + display_name: "MongoDB-OIDC Auth tests - ${os}" + tasks: + - name: test-oidc-auth-aws + +- matrix_name: "ocsp-tests" + matrix_spec: { version: ["4.4", "5.0", "6.0", "7.0", "rapid", "latest"], auth: "noauth", ssl: "ssl", topology: "standalone", os: "windows-64" } + display_name: "OCSP ${version} ${os}" + batchtime: 20160 # 14 days + tasks: + - name: ".ocsp" + +- matrix_name: aws-auth-tests-windows + matrix_spec: { version: ["4.4", "5.0", "6.0", "7.0", "rapid", "latest"], topology: "standalone", os: "windows-64" } + display_name: "MONGODB-AWS Auth test ${version} ${os}" + run_on: + - windows-64-vs2017-test + tasks: + - name: aws-auth-tests + +- matrix_name: aws-auth-tests-linux + matrix_spec: { version: ["6.0", "7.0", "rapid", "latest"], topology: "standalone", os: "ubuntu-2004" } + display_name: "MONGODB-AWS Auth test ${version} ${os}" + tasks: + - name: aws-auth-tests + +- matrix_name: aws-auth-tests-macos + matrix_spec: { version: ["6.0", "7.0", "rapid", "latest"], topology: "standalone", os: "macos-1100" } + display_name: "MONGODB-AWS Auth test ${version} ${os}" + run_on: + - macos-1100 + tasks: + - name: aws-auth-tests + +- name: gssapi-auth-tests-windows + run_on: + - windows-64-vs2017-test + display_name: "GSSAPI (Kerberos) Auth tests - Windows" + tasks: + - name: test-gssapi-net472 + - name: test-gssapi-netstandard20 + - name: test-gssapi-netstandard21 + +- name: gssapi-auth-tests-linux + run_on: + - ubuntu1804-test + display_name: "GSSAPI (Kerberos) Auth tests - Linux" + tasks: + - name: test-gssapi-netstandard20 + - name: test-gssapi-netstandard21 + +# Load balancer tests +- matrix_name: load-balancer-tests + matrix_spec: { version: ["5.0", "6.0", "7.0", "rapid", "latest"], auth: "noauth", ssl: "nossl", topology: "sharded-cluster", os: "ubuntu-2004" } + display_name: "Load Balancer ${version} ${auth} ${ssl} ${os}" + tasks: + - name: "test-load-balancer-netstandard20" + - name: "test-load-balancer-netstandard21" + +- matrix_name: load-balancer-tests-secure + matrix_spec: { version: ["5.0", "6.0", "7.0", "rapid", "latest"], auth: "auth", ssl: "ssl", topology: "sharded-cluster", os: "ubuntu-2004" } + display_name: "Load Balancer ${version} ${auth} ${ssl} ${os}" + tasks: + - name: "test-load-balancer-netstandard20" + - name: "test-load-balancer-netstandard21" + +# Serverless tests +- matrix_name: serverless-tests-windows + matrix_spec: { auth: "auth", ssl: "ssl", compressor: "zlib", os: "windows-64", target_framework: "*", serverless_proxy_type: "*" } + display_name: "${serverless_proxy_type} ${compressor} ${auth} ${ssl} ${os} ${target_framework}" + tasks: + - name: test-serverless + +- matrix_name: serverless-tests-ubuntu + matrix_spec: { auth: "auth", ssl: "ssl", compressor: "zlib", os: "ubuntu-2004", target_framework: ["netstandard20", "netstandard21"], serverless_proxy_type: "*" } + display_name: "${serverless_proxy_type} ${compressor} ${auth} ${ssl} ${os} ${target_framework}" + tasks: + - name: test-serverless + +# Performance tests +- name: driver-performance-tests + display_name: "Driver Performance Tests" + run_on: + - rhel90-dbx-perf-large + tasks: + - name: performance-tests-net60-server-v6.0 + +# AWS Lambda tests +- name: aws-lambda-tests + display_name: "AWS Lambda Tests" + run_on: + - ubuntu2204-small + tasks: + - name: test-aws-lambda-task-group + +# Atlas tests +- name: atlas-connectivity-tests + display_name: "Atlas Connectivity Tests" + run_on: + - windows-64-vs2017-test + tasks: + - name: atlas-connectivity-tests + +- name: atlas-data-lake-test + display_name: "Atlas Data Lake Tests" + run_on: + - ubuntu2004-large + tasks: + - name: atlas-data-lake-test + +- name: atlas-search-test + display_name: "Atlas Search Tests" + run_on: + - windows-64-vs2017-test + tasks: + - name: atlas-search-test + +- name: atlas-search-index-helpers-test + display_name: "Atlas Search Index Helpers Tests" + run_on: + - ubuntu1804-test + tasks: + - name: atlas-search-index-helpers-task-group + +# CSFLE tests +- matrix_name: "csfle-with-mocked-kms-tests-windows" + matrix_spec: { os: "windows-64", ssl: "nossl", version: ["4.2", "4.4", "5.0", "6.0", "7.0", "rapid", "latest"], topology: ["replicaset"] } + display_name: "CSFLE Mocked KMS ${version} ${os}" + tasks: + - name: test-csfle-with-mocked-kms-tls-net472 + - name: test-csfle-with-mocked-kms-tls-netstandard20 + - name: test-csfle-with-mocked-kms-tls-netstandard21 + - name: test-csfle-with-mongocryptd-net472 + - name: test-csfle-with-mongocryptd-netstandard20 + - name: test-csfle-with-mongocryptd-netstandard21 + +- matrix_name: "csfle-with-mocked-kms-tests-linux-1804" + matrix_spec: { os: "ubuntu-1804", ssl: "nossl", version: ["4.2", "4.4", "5.0", "6.0"], topology: ["replicaset"] } + display_name: "CSFLE Mocked KMS ${version} ${os}" + tasks: + - name: test-csfle-with-mocked-kms-tls-netstandard20 + - name: test-csfle-with-mocked-kms-tls-netstandard21 + - name: test-csfle-with-mongocryptd-netstandard20 + - name: test-csfle-with-mongocryptd-netstandard21 + +- matrix_name: "csfle-with-mocked-kms-tests-linux-2004" + matrix_spec: { os: "ubuntu-2004", ssl: "nossl", version: ["7.0", "rapid", "latest"], topology: ["replicaset"] } + display_name: "CSFLE Mocked KMS ${version} ${os}" + tasks: + - name: test-csfle-with-mocked-kms-tls-netstandard20 + - name: test-csfle-with-mocked-kms-tls-netstandard21 + - name: test-csfle-with-mongocryptd-netstandard20 + - name: test-csfle-with-mongocryptd-netstandard21 + +- matrix_name: "csfle-with-mocked-kms-tests-macOS" + matrix_spec: { os: ["macos-1100", "macos-1100-arm64"], ssl: "nossl", version: ["4.2", "4.4", "5.0", "6.0", "7.0", "rapid", "latest"], topology: ["replicaset"] } + display_name: "CSFLE Mocked KMS ${version} ${os}" + tasks: + - name: test-csfle-with-mocked-kms-tls-netstandard21 + - name: test-csfle-with-mongocryptd-netstandard21 + +- matrix_name: "csfle-with-azure-kms-tests-linux" + matrix_spec: { ssl: "nossl", os: "ubuntu-1804" } + display_name: "CSFLE with AZURE KMS ${os}" + batchtime: 20160 # 14 days + tasks: + - name: testazurekms-task-group + - name: test-csfle-with-mongocryptd-netstandard21 + +- matrix_name: "csfle-with-gcp-kms-tests-linux" + matrix_spec: { ssl: "nossl", os: "ubuntu-1804" } + display_name: "CSFLE with GCP KMS ${os}" + batchtime: 20160 # 14 days + tasks: + - name: testgcpkms-task-group + - name: test-csfle-with-mongocryptd-netstandard21 + +# Smoke tests +- matrix_name: "smoke-tests-windows" + matrix_spec: { os: "windows-64", ssl: "nossl", version: ["5.0", "6.0", "7.0", "latest"], topology: ["replicaset"] } + display_name: "smoke-tests ${version} ${os}" + batchtime: 1440 # 1 day + tasks: + - name: test-smoke-tests-net472 + - name: test-smoke-tests-netcoreapp21 + - name: test-smoke-tests-netcoreapp31 + - name: test-smoke-tests-net50 + - name: test-smoke-tests-net60 + - name: test-smoke-tests-net80 + +- matrix_name: "smoke-tests-linux" + matrix_spec: { os: "ubuntu-2004", ssl: "nossl", version: ["5.0", "6.0", "7.0", "latest"], topology: ["replicaset"] } + display_name: "smoke-tests ${version} ${os}" + batchtime: 1440 # 1 day + tasks: + - name: test-smoke-tests-netcoreapp21 + - name: test-smoke-tests-netcoreapp31 + - name: test-smoke-tests-net50 + - name: test-smoke-tests-net60 + - name: test-smoke-tests-net80 + +- matrix_name: "smoke-tests-macOS" + matrix_spec: { os: "macos-1100", ssl: "nossl", version: ["5.0", "6.0", "7.0", "latest"], topology: ["replicaset"] } + display_name: "smoke-tests ${version} ${os}" + batchtime: 1440 # 1 day + tasks: + - name: test-smoke-tests-netcoreapp21 + - name: test-smoke-tests-netcoreapp31 + - name: test-smoke-tests-net50 + - name: test-smoke-tests-net60 + +# Package release variants +- matrix_name: build-packages + matrix_spec: + build-target: "release" + os: "windows-64" # should produce package on Windows to make sure full framework binaries created. + display_name: "Package Pack" + tags: ["build-packages", "release-tag"] + tasks: + - name: build-packages + git_tag_only: true + priority: 10 + +- matrix_name: generate-apidocs + matrix_spec: + build-target: "release" + os: "ubuntu-2004" + display_name: "Generate API Documentation" + tags: ["build-apidocs", "release-tag"] + tasks: + - name: generate-apidocs + git_tag_only: true + priority: 10 + depends_on: + - name: build-packages + variant: ".build-packages" + ## add dependency onto packages smoke test once it implemented + +- matrix_name: push-packages + matrix_spec: + build-target: "release" + os: "ubuntu-2004" + display_name: "Package Push" + tags: ["push-packages", "release-tag"] + tasks: + - name: push-packages + git_tag_only: true + priority: 10 + depends_on: + - name: build-packages + variant: ".build-packages" + ## add dependency onto packages smoke test once it implemented diff --git a/evergreen/run-mongodb-oidc-azure-tests.sh b/evergreen/run-mongodb-oidc-azure-tests.sh new file mode 100644 index 00000000000..e68409824a5 --- /dev/null +++ b/evergreen/run-mongodb-oidc-azure-tests.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# Don't trace since the URI contains a password that shouldn't show up in the logs +set -o errexit # Exit the script with error if any of the commands fail + +DOTNET_SDK_PATH="$(pwd)/.dotnet" + +echo "Downloading .NET SDK installer into $DOTNET_SDK_PATH folder..." +curl -Lfo ./dotnet-install.sh https://dot.net/v1/dotnet-install.sh +echo "Installing .NET LTS SDK..." +bash ./dotnet-install.sh --channel 6.0 --install-dir "$DOTNET_SDK_PATH" --no-path +export PATH=$PATH:$DOTNET_SDK_PATH + +source ./env.sh +MONGODB_URI="mongodb://${OIDC_ADMIN_USER}:${OIDC_ADMIN_PWD}@${MONGODB_URI:10}?authSource=admin" + +dotnet test --no-build --framework net6.0 --filter Category=MongoDbOidc -e OIDC_ENV=azure -e TOKEN_RESOURCE="${AZUREOIDC_RESOURCE}" -e MONGODB_URI="${MONGODB_URI}" --results-directory ./build/test-results --logger "console;verbosity=detailed" ./tests/MongoDB.Driver.Tests/bin/Debug/net6.0/MongoDB.Driver.Tests.dll diff --git a/evergreen/run-mongodb-oidc-tests.sh b/evergreen/run-mongodb-oidc-tests.sh new file mode 100644 index 00000000000..9583f20f873 --- /dev/null +++ b/evergreen/run-mongodb-oidc-tests.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# Don't trace since the URI contains a password that shouldn't show up in the logs +set -o errexit # Exit the script with error if any of the commands fail + +############################################ +# Main Program # +############################################ + +if [ "$OS" = "Windows_NT" ]; then + for var in TMP TEMP NUGET_PACKAGES NUGET_HTTP_CACHE_PATH APPDATA; do + setx $var z:\\data\\tmp + export $var=z:\\data\\tmp + done +else + for var in TMP TEMP NUGET_PACKAGES NUGET_HTTP_CACHE_PATH APPDATA; do + export $var=/data/tmp; + done +fi + +# Make the OIDC tokens. +set -x +OIDC_ENV=${OIDC_ENV:-"test"} + +if [ $OIDC_ENV == "test" ]; then + # Make sure DRIVERS_TOOLS is set. + if [ -z "$DRIVERS_TOOLS" ]; then + echo "Must specify DRIVERS_TOOLS" + exit 1 + fi + + source ${DRIVERS_TOOLS}/.evergreen/auth_oidc/secrets-export.sh + if [[ ! $OS =~ ubuntu.* ]]; then + # Ubuntu uses local server with already build admin credentials in connection string + MONGODB_URI="mongodb+srv://${OIDC_ADMIN_USER}:${OIDC_ADMIN_PWD}@${MONGODB_URI:14}?authSource=admin" + fi +elif [ $OIDC_ENV == "azure" ]; then + source ./env.sh +else + echo "Unrecognized OIDC_ENV $OIDC_ENV" + exit 1 +fi + +export OIDC_ENV=$OIDC_ENV +export MONGODB_URI=$MONGODB_URI + +if [ "Windows_NT" = "$OS" ]; then + powershell.exe .\\build.ps1 --target "TestMongoDbOidc" +else + ./build.sh --target="TestMongoDbOidc" +fi diff --git a/specifications/auth/tests/README.md b/specifications/auth/tests/README.md new file mode 100644 index 00000000000..27e2aecd4bd --- /dev/null +++ b/specifications/auth/tests/README.md @@ -0,0 +1,47 @@ +# Auth Tests + +## Introduction + +This document describes the format of the driver spec tests included in the JSON and YAML files included in the `legacy` +sub-directory. Tests in the `unified` directory are written using the +[Unified Test Format](../../unified-test-format/unified-test-format.md). + +The YAML and JSON files in the `legacy` directory tree are platform-independent tests that drivers can use to prove +their conformance to the Auth Spec at least with respect to connection string URI input. + +Drivers should do additional unit testing if there are alternate ways of configuring credentials on a client. + +Driver must also conduct the prose tests in the Auth Spec test plan section. + +## Format + +Each YAML file contains an object with a single `tests` key. This key is an array of test case objects, each of which +have the following keys: + +- `description`: A string describing the test. +- `uri`: A string containing the URI to be parsed. +- `valid:` A boolean indicating if the URI should be considered valid. +- `credential`: If null, the credential must not be considered configured for the the purpose of deciding if the driver + should authenticate to the topology. If non-null, it is an object containing one or more of the following properties + of a credential: + - `username`: A string containing the username. For auth mechanisms that do not utilize a password, this may be the + entire `userinfo` token from the connection string. + - `password`: A string containing the password. + - `source`: A string containing the authentication database. + - `mechanism`: A string containing the authentication mechanism. A null value for this key is used to indicate that a + mechanism wasn't specified and that mechanism negotiation is required. Test harnesses should modify the mechanism + test as needed to assert this condition. + - `mechanism_properties`: A document containing mechanism-specific properties. It specifies a subset of properties + that must match. If a key exists in the test data, it must exist with the corresponding value in the credential. + Other values may exist in the credential without failing the test. + +If any key is missing, no assertion about that key is necessary. Except as specified explicitly above, if a key is +present, but the test value is null, the observed value for that key must be uninitialized (whatever that means for a +given driver and data type). + +## Implementation notes + +Testing whether a URI is valid or not should simply be a matter of checking whether URI parsing (or MongoClient +construction) raises an error or exception. + +If a credential is configured, its properties must be compared to the `credential` field. diff --git a/specifications/auth/tests/README.rst b/specifications/auth/tests/README.rst deleted file mode 100644 index 18eb3802d74..00000000000 --- a/specifications/auth/tests/README.rst +++ /dev/null @@ -1,53 +0,0 @@ -========== -Auth Tests -========== - -The YAML and JSON files in this directory tree are platform-independent tests -that drivers can use to prove their conformance to the Auth Spec at least with -respect to connection string URI input. - -Drivers should do additional unit testing if there are alternate ways of -configuring credentials on a client. - -Driver must also conduct the prose tests in the Auth Spec test plan section. - -Format ------- - -Each YAML file contains an object with a single ``tests`` key. This key is an -array of test case objects, each of which have the following keys: - -- ``description``: A string describing the test. -- ``uri``: A string containing the URI to be parsed. -- ``valid:`` A boolean indicating if the URI should be considered valid. -- ``credential``: If null, the credential must not be considered configured for the - the purpose of deciding if the driver should authenticate to the topology. If non-null, - it is an object containing one or more of the following properties of a credential: - - - ``username``: A string containing the username. For auth mechanisms - that do not utilize a password, this may be the entire ``userinfo`` token - from the connection string. - - ``password``: A string containing the password. - - ``source``: A string containing the authentication database. - - ``mechanism``: A string containing the authentication mechanism. A null value for - this key is used to indicate that a mechanism wasn't specified and that mechanism - negotiation is required. Test harnesses should modify the mechanism test as needed - to assert this condition. - - ``mechanism_properties``: A document containing mechanism-specific properties. It - specifies a subset of properties that must match. If a key exists in the test data, - it must exist with the corresponding value in the credential. Other values may - exist in the credential without failing the test. - -If any key is missing, no assertion about that key is necessary. Except as -specified explicitly above, if a key is present, but the test value is null, -the observed value for that key must be uninitialized (whatever that means for -a given driver and data type). - -Implementation notes -==================== - -Testing whether a URI is valid or not should simply be a matter of checking -whether URI parsing (or MongoClient construction) raises an error or exception. - -If a credential is configured, its properties must be compared to the -``credential`` field. \ No newline at end of file diff --git a/specifications/auth/tests/connection-string.yml b/specifications/auth/tests/connection-string.yml deleted file mode 100644 index 41dca8fabdb..00000000000 --- a/specifications/auth/tests/connection-string.yml +++ /dev/null @@ -1,366 +0,0 @@ -tests: - - - description: "should use the default source and mechanism" - uri: "mongodb://user:password@localhost" - valid: true - credential: - username: "user" - password: "password" - source: "admin" - mechanism: ~ - mechanism_properties: ~ - - - description: "should use the database when no authSource is specified" - uri: "mongodb://user:password@localhost/foo" - valid: true - credential: - username: "user" - password: "password" - source: "foo" - mechanism: ~ - mechanism_properties: ~ - - - description: "should use the authSource when specified" - uri: "mongodb://user:password@localhost/foo?authSource=bar" - valid: true - credential: - username: "user" - password: "password" - source: "bar" - mechanism: ~ - mechanism_properties: ~ - - - description: "should recognise the mechanism (GSSAPI)" - uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI" - valid: true - credential: - username: "user@DOMAIN.COM" - password: ~ - source: "$external" - mechanism: "GSSAPI" - mechanism_properties: - SERVICE_NAME: "mongodb" - - - description: "should ignore the database (GSSAPI)" - uri: "mongodb://user%40DOMAIN.COM@localhost/foo?authMechanism=GSSAPI" - valid: true - credential: - username: "user@DOMAIN.COM" - password: ~ - source: "$external" - mechanism: "GSSAPI" - mechanism_properties: - SERVICE_NAME: "mongodb" - - - description: "should accept valid authSource (GSSAPI)" - uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=$external" - valid: true - credential: - username: "user@DOMAIN.COM" - password: ~ - source: "$external" - mechanism: "GSSAPI" - mechanism_properties: - SERVICE_NAME: "mongodb" - - - description: "should accept generic mechanism property (GSSAPI)" - uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true" - valid: true - credential: - username: "user@DOMAIN.COM" - password: ~ - source: "$external" - mechanism: "GSSAPI" - mechanism_properties: - SERVICE_NAME: "other" - CANONICALIZE_HOST_NAME: true - - - description: "should accept the password (GSSAPI)" - uri: "mongodb://user%40DOMAIN.COM:password@localhost/?authMechanism=GSSAPI&authSource=$external" - valid: true - credential: - username: "user@DOMAIN.COM" - password: "password" - source: "$external" - mechanism: "GSSAPI" - mechanism_properties: - SERVICE_NAME: "mongodb" - - - description: "must raise an error when the authSource is empty" - uri: "mongodb://user:password@localhost/foo?authSource=" - valid: false - - - description: "must raise an error when the authSource is empty without credentials" - uri: "mongodb://localhost/admin?authSource=" - valid: false - - - description: "should throw an exception if authSource is invalid (GSSAPI)" - uri: "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=foo" - valid: false - - - description: "should throw an exception if no username (GSSAPI)" - uri: "mongodb://localhost/?authMechanism=GSSAPI" - valid: false - - - description: "should recognize the mechanism (MONGODB-CR)" - uri: "mongodb://user:password@localhost/?authMechanism=MONGODB-CR" - valid: true - credential: - username: "user" - password: "password" - source: "admin" - mechanism: "MONGODB-CR" - mechanism_properties: ~ - - - description: "should use the database when no authSource is specified (MONGODB-CR)" - uri: "mongodb://user:password@localhost/foo?authMechanism=MONGODB-CR" - valid: true - credential: - username: "user" - password: "password" - source: "foo" - mechanism: "MONGODB-CR" - mechanism_properties: ~ - - - description: "should use the authSource when specified (MONGODB-CR)" - uri: "mongodb://user:password@localhost/foo?authMechanism=MONGODB-CR&authSource=bar" - valid: true - credential: - username: "user" - password: "password" - source: "bar" - mechanism: "MONGODB-CR" - mechanism_properties: ~ - - - description: "should throw an exception if no username is supplied (MONGODB-CR)" - uri: "mongodb://localhost/?authMechanism=MONGODB-CR" - valid: false - - - description: "should recognize the mechanism (MONGODB-X509)" - uri: "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509" - valid: true - credential: - username: "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry" - password: ~ - source: "$external" - mechanism: "MONGODB-X509" - mechanism_properties: ~ - - - description: "should ignore the database (MONGODB-X509)" - uri: "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509" - valid: true - credential: - username: "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry" - password: ~ - source: "$external" - mechanism: "MONGODB-X509" - mechanism_properties: ~ - - - description: "should accept valid authSource (MONGODB-X509)" - uri: "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509&authSource=$external" - valid: true - credential: - username: "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry" - password: ~ - source: "$external" - mechanism: "MONGODB-X509" - mechanism_properties: ~ - - - description: "should recognize the mechanism with no username (MONGODB-X509)" - uri: "mongodb://localhost/?authMechanism=MONGODB-X509" - valid: true - credential: - username: ~ - password: ~ - source: "$external" - mechanism: "MONGODB-X509" - mechanism_properties: ~ - - - description: "should recognize the mechanism with no username when auth source is explicitly specified (MONGODB-X509)" - uri: "mongodb://localhost/?authMechanism=MONGODB-X509&authSource=$external" - valid: true - credential: - username: ~ - password: ~ - source: "$external" - mechanism: "MONGODB-X509" - mechanism_properties: ~ - - - description: "should throw an exception if supplied a password (MONGODB-X509)" - uri: "mongodb://user:password@localhost/?authMechanism=MONGODB-X509" - valid: false - - - description: "should throw an exception if authSource is invalid (MONGODB-X509)" - uri: "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509&authSource=bar" - valid: false - - - description: "should recognize the mechanism (PLAIN)" - uri: "mongodb://user:password@localhost/?authMechanism=PLAIN" - valid: true - credential: - username: "user" - password: "password" - source: "$external" - mechanism: "PLAIN" - mechanism_properties: ~ - - - description: "should use the database when no authSource is specified (PLAIN)" - uri: "mongodb://user:password@localhost/foo?authMechanism=PLAIN" - valid: true - credential: - username: "user" - password: "password" - source: "foo" - mechanism: "PLAIN" - mechanism_properties: ~ - - - description: "should use the authSource when specified (PLAIN)" - uri: "mongodb://user:password@localhost/foo?authMechanism=PLAIN&authSource=bar" - valid: true - credential: - username: "user" - password: "password" - source: "bar" - mechanism: "PLAIN" - mechanism_properties: ~ - - - description: "should throw an exception if no username (PLAIN)" - uri: "mongodb://localhost/?authMechanism=PLAIN" - valid: false - - - description: "should recognize the mechanism (SCRAM-SHA-1)" - uri: "mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-1" - valid: true - credential: - username: "user" - password: "password" - source: "admin" - mechanism: "SCRAM-SHA-1" - mechanism_properties: ~ - - - description: "should use the database when no authSource is specified (SCRAM-SHA-1)" - uri: "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1" - valid: true - credential: - username: "user" - password: "password" - source: "foo" - mechanism: "SCRAM-SHA-1" - mechanism_properties: ~ - - - description: "should accept valid authSource (SCRAM-SHA-1)" - uri: "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1&authSource=bar" - valid: true - credential: - username: "user" - password: "password" - source: "bar" - mechanism: "SCRAM-SHA-1" - mechanism_properties: ~ - - - description: "should throw an exception if no username (SCRAM-SHA-1)" - uri: "mongodb://localhost/?authMechanism=SCRAM-SHA-1" - valid: false - - - description: "should recognize the mechanism (SCRAM-SHA-256)" - uri: "mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-256" - valid: true - credential: - username: "user" - password: "password" - source: "admin" - mechanism: "SCRAM-SHA-256" - mechanism_properties: ~ - - - description: "should use the database when no authSource is specified (SCRAM-SHA-256)" - uri: "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256" - valid: true - credential: - username: "user" - password: "password" - source: "foo" - mechanism: "SCRAM-SHA-256" - mechanism_properties: ~ - - - description: "should accept valid authSource (SCRAM-SHA-256)" - uri: "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256&authSource=bar" - valid: true - credential: - username: "user" - password: "password" - source: "bar" - mechanism: "SCRAM-SHA-256" - mechanism_properties: ~ - - - description: "should throw an exception if no username (SCRAM-SHA-256)" - uri: "mongodb://localhost/?authMechanism=SCRAM-SHA-256" - valid: false - - - description: "URI with no auth-related info doesn't create credential" - uri: "mongodb://localhost/" - valid: true - credential: ~ - - - description: "database in URI path doesn't create credentials" - uri: "mongodb://localhost/foo" - valid: true - credential: ~ - - - description: "authSource without username doesn't create credential (default mechanism)" - uri: "mongodb://localhost/?authSource=foo" - valid: true - credential: ~ - - - description: "should throw an exception if no username provided (userinfo implies default mechanism)" - uri: "mongodb://@localhost.com/" - valid: false - - - description: "should throw an exception if no username/password provided (userinfo implies default mechanism)" - uri: "mongodb://:@localhost.com/" - valid: false - - - description: "should recognise the mechanism (MONGODB-AWS)" - uri: "mongodb://localhost/?authMechanism=MONGODB-AWS" - valid: true - credential: - username: ~ - password: ~ - source: "$external" - mechanism: "MONGODB-AWS" - mechanism_properties: ~ - - - description: "should recognise the mechanism when auth source is explicitly specified (MONGODB-AWS)" - uri: "mongodb://localhost/?authMechanism=MONGODB-AWS&authSource=$external" - valid: true - credential: - username: ~ - password: ~ - source: "$external" - mechanism: "MONGODB-AWS" - mechanism_properties: ~ - - - description: "should throw an exception if username and no password (MONGODB-AWS)" - uri: "mongodb://user@localhost/?authMechanism=MONGODB-AWS" - valid: false - credential: ~ - - - description: "should use username and password if specified (MONGODB-AWS)" - uri: "mongodb://user%21%40%23%24%25%5E%26%2A%28%29_%2B:pass%21%40%23%24%25%5E%26%2A%28%29_%2B@localhost/?authMechanism=MONGODB-AWS" - valid: true - credential: - username: "user!@#$%^&*()_+" - password: "pass!@#$%^&*()_+" - source: "$external" - mechanism: "MONGODB-AWS" - mechanism_properties: ~ - - - description: "should use username, password and session token if specified (MONGODB-AWS)" - uri: "mongodb://user:password@localhost/?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:token%21%40%23%24%25%5E%26%2A%28%29_%2B" - valid: true - credential: - username: "user" - password: "password" - source: "$external" - mechanism: "MONGODB-AWS" - mechanism_properties: - AWS_SESSION_TOKEN: "token!@#$%^&*()_+" diff --git a/specifications/auth/tests/connection-string.json b/specifications/auth/tests/legacy/connection-string.json similarity index 69% rename from specifications/auth/tests/connection-string.json rename to specifications/auth/tests/legacy/connection-string.json index 2a37ae8df47..3c7a6b1d148 100644 --- a/specifications/auth/tests/connection-string.json +++ b/specifications/auth/tests/legacy/connection-string.json @@ -80,7 +80,7 @@ }, { "description": "should accept generic mechanism property (GSSAPI)", - "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true", + "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forward,SERVICE_HOST:example.com", "valid": true, "credential": { "username": "user@DOMAIN.COM", @@ -89,10 +89,46 @@ "mechanism": "GSSAPI", "mechanism_properties": { "SERVICE_NAME": "other", - "CANONICALIZE_HOST_NAME": true + "SERVICE_HOST": "example.com", + "CANONICALIZE_HOST_NAME": "forward" } } }, + { + "description": "should accept forwardAndReverse hostname canonicalization (GSSAPI)", + "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forwardAndReverse", + "valid": true, + "credential": { + "username": "user@DOMAIN.COM", + "password": null, + "source": "$external", + "mechanism": "GSSAPI", + "mechanism_properties": { + "SERVICE_NAME": "other", + "CANONICALIZE_HOST_NAME": "forwardAndReverse" + } + } + }, + { + "description": "should accept no hostname canonicalization (GSSAPI)", + "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:none", + "valid": true, + "credential": { + "username": "user@DOMAIN.COM", + "password": null, + "source": "$external", + "mechanism": "GSSAPI", + "mechanism_properties": { + "SERVICE_NAME": "other", + "CANONICALIZE_HOST_NAME": "none" + } + } + }, + { + "description": "must raise an error when the hostname canonicalization is invalid", + "uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:invalid", + "valid": false + }, { "description": "should accept the password (GSSAPI)", "uri": "mongodb://user%40DOMAIN.COM:password@localhost/?authMechanism=GSSAPI&authSource=$external", @@ -444,6 +480,133 @@ "AWS_SESSION_TOKEN": "token!@#$%^&*()_+" } } + }, + { + "description": "should recognise the mechanism with test integration (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test", + "valid": true, + "credential": { + "username": null, + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "test" + } + } + }, + { + "description": "should recognise the mechanism when auth source is explicitly specified and with environment (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=ENVIRONMENT:test", + "valid": true, + "credential": { + "username": null, + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "test" + } + } + }, + { + "description": "should throw an exception if supplied a password (MONGODB-OIDC)", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test", + "valid": false, + "credential": null + }, + { + "description": "should throw an exception if username is specified for test (MONGODB-OIDC)", + "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&ENVIRONMENT:test", + "valid": false, + "credential": null + }, + { + "description": "should throw an exception if specified environment is not supported (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:invalid", + "valid": false, + "credential": null + }, + { + "description": "should throw an exception if neither environment nor callbacks specified (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC", + "valid": false, + "credential": null + }, + { + "description": "should throw an exception when unsupported auth property is specified (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=UnsupportedProperty:unexisted", + "valid": false, + "credential": null + }, + { + "description": "should recognise the mechanism with azure provider (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo", + "valid": true, + "credential": { + "username": null, + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "foo" + } + } + }, + { + "description": "should accept a username with azure provider (MONGODB-OIDC)", + "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo", + "valid": true, + "credential": { + "username": "user", + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "foo" + } + } + }, + { + "description": "should accept a username and throw an error for a password with azure provider (MONGODB-OIDC)", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo", + "valid": false, + "credential": null + }, + { + "description": "should throw an exception if no token audience is given for azure provider (MONGODB-OIDC)", + "uri": "mongodb://username@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure", + "valid": false, + "credential": null + }, + { + "description": "should recognise the mechanism with gcp provider (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo", + "valid": true, + "credential": { + "username": null, + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "gcp", + "TOKEN_RESOURCE": "foo" + } + } + }, + { + "description": "should throw an error for a username and password with gcp provider (MONGODB-OIDC)", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo", + "valid": false, + "credential": null + }, + { + "description": "should throw an error if not TOKEN_RESOURCE with gcp provider (MONGODB-OIDC)", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp", + "valid": false, + "credential": null } ] } diff --git a/specifications/auth/tests/legacy/connection-string.yml b/specifications/auth/tests/legacy/connection-string.yml new file mode 100644 index 00000000000..dcb90b27448 --- /dev/null +++ b/specifications/auth/tests/legacy/connection-string.yml @@ -0,0 +1,442 @@ +--- +tests: +- description: should use the default source and mechanism + uri: mongodb://user:password@localhost + valid: true + credential: + username: user + password: password + source: admin + mechanism: + mechanism_properties: +- description: should use the database when no authSource is specified + uri: mongodb://user:password@localhost/foo + valid: true + credential: + username: user + password: password + source: foo + mechanism: + mechanism_properties: +- description: should use the authSource when specified + uri: mongodb://user:password@localhost/foo?authSource=bar + valid: true + credential: + username: user + password: password + source: bar + mechanism: + mechanism_properties: +- description: should recognise the mechanism (GSSAPI) + uri: mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI + valid: true + credential: + username: user@DOMAIN.COM + password: + source: "$external" + mechanism: GSSAPI + mechanism_properties: + SERVICE_NAME: mongodb +- description: should ignore the database (GSSAPI) + uri: mongodb://user%40DOMAIN.COM@localhost/foo?authMechanism=GSSAPI + valid: true + credential: + username: user@DOMAIN.COM + password: + source: "$external" + mechanism: GSSAPI + mechanism_properties: + SERVICE_NAME: mongodb +- description: should accept valid authSource (GSSAPI) + uri: mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=$external + valid: true + credential: + username: user@DOMAIN.COM + password: + source: "$external" + mechanism: GSSAPI + mechanism_properties: + SERVICE_NAME: mongodb +- description: should accept generic mechanism property (GSSAPI) + uri: mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forward,SERVICE_HOST:example.com + valid: true + credential: + username: user@DOMAIN.COM + password: + source: "$external" + mechanism: GSSAPI + mechanism_properties: + SERVICE_NAME: other + SERVICE_HOST: example.com + CANONICALIZE_HOST_NAME: forward +- description: should accept forwardAndReverse hostname canonicalization (GSSAPI) + uri: mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:forwardAndReverse + valid: true + credential: + username: user@DOMAIN.COM + password: + source: "$external" + mechanism: GSSAPI + mechanism_properties: + SERVICE_NAME: other + CANONICALIZE_HOST_NAME: forwardAndReverse +- description: should accept no hostname canonicalization (GSSAPI) + uri: mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:none + valid: true + credential: + username: user@DOMAIN.COM + password: + source: "$external" + mechanism: GSSAPI + mechanism_properties: + SERVICE_NAME: other + CANONICALIZE_HOST_NAME: none +- description: must raise an error when the hostname canonicalization is invalid + uri: mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:invalid + valid: false +- description: should accept the password (GSSAPI) + uri: mongodb://user%40DOMAIN.COM:password@localhost/?authMechanism=GSSAPI&authSource=$external + valid: true + credential: + username: user@DOMAIN.COM + password: password + source: "$external" + mechanism: GSSAPI + mechanism_properties: + SERVICE_NAME: mongodb +- description: must raise an error when the authSource is empty + uri: mongodb://user:password@localhost/foo?authSource= + valid: false +- description: must raise an error when the authSource is empty without credentials + uri: mongodb://localhost/admin?authSource= + valid: false +- description: should throw an exception if authSource is invalid (GSSAPI) + uri: mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=foo + valid: false +- description: should throw an exception if no username (GSSAPI) + uri: mongodb://localhost/?authMechanism=GSSAPI + valid: false +- description: should recognize the mechanism (MONGODB-CR) + uri: mongodb://user:password@localhost/?authMechanism=MONGODB-CR + valid: true + credential: + username: user + password: password + source: admin + mechanism: MONGODB-CR + mechanism_properties: +- description: should use the database when no authSource is specified (MONGODB-CR) + uri: mongodb://user:password@localhost/foo?authMechanism=MONGODB-CR + valid: true + credential: + username: user + password: password + source: foo + mechanism: MONGODB-CR + mechanism_properties: +- description: should use the authSource when specified (MONGODB-CR) + uri: mongodb://user:password@localhost/foo?authMechanism=MONGODB-CR&authSource=bar + valid: true + credential: + username: user + password: password + source: bar + mechanism: MONGODB-CR + mechanism_properties: +- description: should throw an exception if no username is supplied (MONGODB-CR) + uri: mongodb://localhost/?authMechanism=MONGODB-CR + valid: false +- description: should recognize the mechanism (MONGODB-X509) + uri: mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509 + valid: true + credential: + username: CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry + password: + source: "$external" + mechanism: MONGODB-X509 + mechanism_properties: +- description: should ignore the database (MONGODB-X509) + uri: mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509 + valid: true + credential: + username: CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry + password: + source: "$external" + mechanism: MONGODB-X509 + mechanism_properties: +- description: should accept valid authSource (MONGODB-X509) + uri: mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509&authSource=$external + valid: true + credential: + username: CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry + password: + source: "$external" + mechanism: MONGODB-X509 + mechanism_properties: +- description: should recognize the mechanism with no username (MONGODB-X509) + uri: mongodb://localhost/?authMechanism=MONGODB-X509 + valid: true + credential: + username: + password: + source: "$external" + mechanism: MONGODB-X509 + mechanism_properties: +- description: should recognize the mechanism with no username when auth source is + explicitly specified (MONGODB-X509) + uri: mongodb://localhost/?authMechanism=MONGODB-X509&authSource=$external + valid: true + credential: + username: + password: + source: "$external" + mechanism: MONGODB-X509 + mechanism_properties: +- description: should throw an exception if supplied a password (MONGODB-X509) + uri: mongodb://user:password@localhost/?authMechanism=MONGODB-X509 + valid: false +- description: should throw an exception if authSource is invalid (MONGODB-X509) + uri: mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509&authSource=bar + valid: false +- description: should recognize the mechanism (PLAIN) + uri: mongodb://user:password@localhost/?authMechanism=PLAIN + valid: true + credential: + username: user + password: password + source: "$external" + mechanism: PLAIN + mechanism_properties: +- description: should use the database when no authSource is specified (PLAIN) + uri: mongodb://user:password@localhost/foo?authMechanism=PLAIN + valid: true + credential: + username: user + password: password + source: foo + mechanism: PLAIN + mechanism_properties: +- description: should use the authSource when specified (PLAIN) + uri: mongodb://user:password@localhost/foo?authMechanism=PLAIN&authSource=bar + valid: true + credential: + username: user + password: password + source: bar + mechanism: PLAIN + mechanism_properties: +- description: should throw an exception if no username (PLAIN) + uri: mongodb://localhost/?authMechanism=PLAIN + valid: false +- description: should recognize the mechanism (SCRAM-SHA-1) + uri: mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-1 + valid: true + credential: + username: user + password: password + source: admin + mechanism: SCRAM-SHA-1 + mechanism_properties: +- description: should use the database when no authSource is specified (SCRAM-SHA-1) + uri: mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1 + valid: true + credential: + username: user + password: password + source: foo + mechanism: SCRAM-SHA-1 + mechanism_properties: +- description: should accept valid authSource (SCRAM-SHA-1) + uri: mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1&authSource=bar + valid: true + credential: + username: user + password: password + source: bar + mechanism: SCRAM-SHA-1 + mechanism_properties: +- description: should throw an exception if no username (SCRAM-SHA-1) + uri: mongodb://localhost/?authMechanism=SCRAM-SHA-1 + valid: false +- description: should recognize the mechanism (SCRAM-SHA-256) + uri: mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-256 + valid: true + credential: + username: user + password: password + source: admin + mechanism: SCRAM-SHA-256 + mechanism_properties: +- description: should use the database when no authSource is specified (SCRAM-SHA-256) + uri: mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256 + valid: true + credential: + username: user + password: password + source: foo + mechanism: SCRAM-SHA-256 + mechanism_properties: +- description: should accept valid authSource (SCRAM-SHA-256) + uri: mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256&authSource=bar + valid: true + credential: + username: user + password: password + source: bar + mechanism: SCRAM-SHA-256 + mechanism_properties: +- description: should throw an exception if no username (SCRAM-SHA-256) + uri: mongodb://localhost/?authMechanism=SCRAM-SHA-256 + valid: false +- description: URI with no auth-related info doesn't create credential + uri: mongodb://localhost/ + valid: true + credential: +- description: database in URI path doesn't create credentials + uri: mongodb://localhost/foo + valid: true + credential: +- description: authSource without username doesn't create credential (default mechanism) + uri: mongodb://localhost/?authSource=foo + valid: true + credential: +- description: should throw an exception if no username provided (userinfo implies + default mechanism) + uri: mongodb://@localhost.com/ + valid: false +- description: should throw an exception if no username/password provided (userinfo + implies default mechanism) + uri: mongodb://:@localhost.com/ + valid: false +- description: should recognise the mechanism (MONGODB-AWS) + uri: mongodb://localhost/?authMechanism=MONGODB-AWS + valid: true + credential: + username: + password: + source: "$external" + mechanism: MONGODB-AWS + mechanism_properties: +- description: should recognise the mechanism when auth source is explicitly specified + (MONGODB-AWS) + uri: mongodb://localhost/?authMechanism=MONGODB-AWS&authSource=$external + valid: true + credential: + username: + password: + source: "$external" + mechanism: MONGODB-AWS + mechanism_properties: +- description: should throw an exception if username and no password (MONGODB-AWS) + uri: mongodb://user@localhost/?authMechanism=MONGODB-AWS + valid: false + credential: +- description: should use username and password if specified (MONGODB-AWS) + uri: mongodb://user%21%40%23%24%25%5E%26%2A%28%29_%2B:pass%21%40%23%24%25%5E%26%2A%28%29_%2B@localhost/?authMechanism=MONGODB-AWS + valid: true + credential: + username: user!@#$%^&*()_+ + password: pass!@#$%^&*()_+ + source: "$external" + mechanism: MONGODB-AWS + mechanism_properties: +- description: should use username, password and session token if specified (MONGODB-AWS) + uri: mongodb://user:password@localhost/?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:token%21%40%23%24%25%5E%26%2A%28%29_%2B + valid: true + credential: + username: user + password: password + source: "$external" + mechanism: MONGODB-AWS + mechanism_properties: + AWS_SESSION_TOKEN: token!@#$%^&*()_+ +- description: should recognise the mechanism with test environment (MONGODB-OIDC) + uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test + valid: true + credential: + username: + password: + source: "$external" + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: test +- description: should recognise the mechanism when auth source is explicitly specified and with environment (MONGODB-OIDC) + uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=ENVIRONMENT:test + valid: true + credential: + username: + password: + source: "$external" + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: test +- description: should throw an exception if supplied a password (MONGODB-OIDC) + uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test + valid: false + credential: +- description: should throw an exception if username is specified for aws (MONGODB-OIDC) + uri: mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&ENVIRONMENT:test + valid: false + credential: +- description: should throw an exception if specified environment is not supported (MONGODB-OIDC) + uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:invalid + valid: false + credential: +- description: should throw an exception if neither environment nor callbacks specified (MONGODB-OIDC) + uri: mongodb://localhost/?authMechanism=MONGODB-OIDC + valid: false + credential: +- description: should throw an exception when unsupported auth property is specified (MONGODB-OIDC) + uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=UnsupportedProperty:unexisted + valid: false + credential: +- description: should recognise the mechanism with azure provider (MONGODB-OIDC) + uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo + valid: true + credential: + username: null + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: azure + TOKEN_RESOURCE: foo +- description: should accept a username with azure provider (MONGODB-OIDC) + uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo + valid: true + credential: + username: user + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: azure + TOKEN_RESOURCE: foo +- description: should accept a username and throw an error for a password with azure provider (MONGODB-OIDC) + uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo + valid: false + credential: null +- description: should throw an exception if no token audience is given for azure provider (MONGODB-OIDC) + uri: mongodb://username@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure + valid: false + credential: null +- description: should recognise the mechanism with gcp provider (MONGODB-OIDC) + uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo + valid: true + credential: + username: null + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: gcp + TOKEN_RESOURCE: foo +- description: should throw an error for a username and password with gcp provider + (MONGODB-OIDC) + uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo + valid: false + credential: null +- description: should throw an error if not TOKEN_RESOURCE with gcp provider (MONGODB-OIDC) + uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp + valid: false + credential: null diff --git a/specifications/auth/tests/mongodb-aws.rst b/specifications/auth/tests/mongodb-aws.rst deleted file mode 100644 index 4f206279897..00000000000 --- a/specifications/auth/tests/mongodb-aws.rst +++ /dev/null @@ -1,94 +0,0 @@ -=========== -MongoDB AWS -=========== - -There are 5 scenarios drivers MUST test: - -#. ``Regular Credentials``: Auth via an ``ACCESS_KEY_ID`` and ``SECRET_ACCESS_KEY`` pair -#. ``EC2 Credentials``: Auth from an EC2 instance via temporary credentials assigned to the machine -#. ``ECS Credentials``: Auth from an ECS instance via temporary credentials assigned to the task -#. ``Assume Role``: Auth via temporary credentials obtained from an STS AssumeRole request -#. ``AWS Lambda``: Auth via environment variables ``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY``, and ``AWS_SESSION_TOKEN``. - -For brevity, this section gives the values ````, ```` and ```` in place of a valid access key ID, secret access key and session token (also known as a security token). Note that if these values are passed into the URI they MUST be URL encoded. Sample values are below. - -.. code-block:: - - AccessKeyId=AKIAI44QH8DHBEXAMPLE - SecretAccessKey=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY - Token=AQoDYXdzEJr... -| -.. sectnum:: - -Regular credentials -====================== - -Drivers MUST be able to authenticate by providing a valid access key id and secret access key pair as the username and password, respectively, in the MongoDB URI. An example of a valid URI would be: - -.. code-block:: - - mongodb://:@localhost/?authMechanism=MONGODB-AWS -| -EC2 Credentials -=============== - -Drivers MUST be able to authenticate from an EC2 instance via temporary credentials assigned to the machine. A sample URI on an EC2 machine would be: - -.. code-block:: - - mongodb://localhost/?authMechanism=MONGODB-AWS -| -.. note:: No username, password or session token is passed into the URI. Drivers MUST query the EC2 instance endpoint to obtain these credentials. - -ECS instance -============ - -Drivers MUST be able to authenticate from an ECS container via temporary credentials. A sample URI in an ECS container would be: - -.. code-block:: - - mongodb://localhost/?authMechanism=MONGODB-AWS -| -.. note:: No username, password or session token is passed into the URI. Drivers MUST query the ECS container endpoint to obtain these credentials. - -AssumeRole -========== - -Drivers MUST be able to authenticate using temporary credentials returned from an assume role request. These temporary credentials consist of an access key ID, a secret access key, and a security token passed into the URI. A sample URI would be: - -.. code-block:: - - mongodb://:@localhost/?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN: -| -AWS Lambda -========== - -Drivers MUST be able to authenticate via an access key ID, secret access key and optional session token taken from the environment variables, respectively: - -.. code-block:: - - AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY - AWS_SESSION_TOKEN -| - -Sample URIs both with and without optional session tokens set are shown below. Drivers MUST test both cases. - -.. code-block:: bash - - # without a session token - export AWS_ACCESS_KEY_ID="" - export AWS_SECRET_ACCESS_KEY="" - - URI="mongodb://localhost/?authMechanism=MONGODB-AWS" -| -.. code-block:: bash - - # with a session token - export AWS_ACCESS_KEY_ID="" - export AWS_SECRET_ACCESS_KEY="" - export AWS_SESSION_TOKEN="" - - URI="mongodb://localhost/?authMechanism=MONGODB-AWS" -| -.. note:: No username, password or session token is passed into the URI. Drivers MUST check the environment variables listed above for these values. If the session token is set Drivers MUST use it. \ No newline at end of file diff --git a/specifications/auth/tests/unified/mongodb-oidc-no-retry.json b/specifications/auth/tests/unified/mongodb-oidc-no-retry.json new file mode 100644 index 00000000000..83d73e4e509 --- /dev/null +++ b/specifications/auth/tests/unified/mongodb-oidc-no-retry.json @@ -0,0 +1,601 @@ +{ + "description": "MONGODB-OIDC authentication with retry disabled", + "schemaVersion": "1.19", + "runOnRequirements": [ + { + "minServerVersion": "7.0", + "auth": true, + "authMechanism": "MONGODB-OIDC" + } + ], + "createEntities": [ + { + "client": { + "id": "failPointClient", + "useMultipleMongoses": false + } + }, + { + "client": { + "id": "client0", + "uriOptions": { + "authMechanism": "MONGODB-OIDC", + "authMechanismProperties": { + "$$placeholder": 1 + }, + "retryReads": false, + "retryWrites": false + }, + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent", + "commandFailedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "test" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collName" + } + } + ], + "initialData": [ + { + "collectionName": "collName", + "databaseName": "test", + "documents": [ + + ] + } + ], + "tests": [ + { + "description": "A read operation should succeed", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + } + }, + "expectResult": [ + + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": { + } + } + } + }, + { + "commandSucceededEvent": { + "commandName": "find" + } + } + ] + } + ] + }, + { + "description": "A write operation should succeed", + "operations": [ + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Read commands should reauthenticate and retry when a ReauthenticationRequired error happens", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "find" + ], + "errorCode": 391 + } + } + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + } + }, + "expectResult": [ + + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": { + } + } + } + }, + { + "commandFailedEvent": { + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": { + } + } + } + }, + { + "commandSucceededEvent": { + "commandName": "find" + } + } + ] + } + ] + }, + { + "description": "Write commands should reauthenticate and retry when a ReauthenticationRequired error happens", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 391 + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandFailedEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Handshake with cached token should use speculative authentication", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "closeConnection": true + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + }, + "expectError": { + "isClientError": true + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": "alwaysOn", + "data": { + "failCommands": [ + "saslStart" + ], + "errorCode": 20 + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandFailedEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Handshake without cached token should not use speculative authentication", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": "alwaysOn", + "data": { + "failCommands": [ + "saslStart" + ], + "errorCode": 20 + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + }, + "expectError": { + "errorCode": 20 + } + } + ] + }, + { + "description": "Read commands should fail if reauthentication fails", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + } + }, + "expectResult": [ + + ] + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "find", + "saslStart" + ], + "errorCode": 391 + } + } + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + } + }, + "expectError": { + "errorCode": 391 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": { + } + } + } + }, + { + "commandSucceededEvent": { + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": { + } + } + } + }, + { + "commandFailedEvent": { + "commandName": "find" + } + } + ] + } + ] + }, + { + "description": "Write commands should fail if reauthentication fails", + "operations": [ + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "insert", + "saslStart" + ], + "errorCode": 391 + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 2, + "x": 2 + } + }, + "expectError": { + "errorCode": 391 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 2, + "x": 2 + } + ] + } + } + }, + { + "commandFailedEvent": { + "commandName": "insert" + } + } + ] + } + ] + } + ] +} diff --git a/specifications/auth/tests/unified/mongodb-oidc-no-retry.yml b/specifications/auth/tests/unified/mongodb-oidc-no-retry.yml new file mode 100644 index 00000000000..8108acb5019 --- /dev/null +++ b/specifications/auth/tests/unified/mongodb-oidc-no-retry.yml @@ -0,0 +1,313 @@ +--- +description: "MONGODB-OIDC authentication with retry disabled" +schemaVersion: "1.19" +runOnRequirements: +- minServerVersion: "7.0" + auth: true + authMechanism: "MONGODB-OIDC" +createEntities: +- client: + id: &failPointClient failPointClient + useMultipleMongoses: false +- client: + id: client0 + uriOptions: + authMechanism: "MONGODB-OIDC" + # The $$placeholder document should be replaced by auth mechanism + # properties that enable OIDC auth on the target cloud platform. For + # example, when running the test on EC2, replace the $$placeholder + # document with {"ENVIRONMENT": "test"}. + authMechanismProperties: { $$placeholder: 1 } + retryReads: false + retryWrites: false + observeEvents: + - commandStartedEvent + - commandSucceededEvent + - commandFailedEvent +- database: + id: database0 + client: client0 + databaseName: test +- collection: + id: collection0 + database: database0 + collectionName: collName +initialData: +- collectionName: collName + databaseName: test + documents: [] +tests: +- description: A read operation should succeed + operations: + - name: find + object: collection0 + arguments: + filter: {} + expectResult: [] + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collName + filter: {} + - commandSucceededEvent: + commandName: find +- description: A write operation should succeed + operations: + - name: insertOne + object: collection0 + arguments: + document: + _id: 1 + x: 1 + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandSucceededEvent: + commandName: insert +- description: Read commands should reauthenticate and retry when a ReauthenticationRequired error happens + operations: + - name: failPoint + object: testRunner + arguments: + client: failPointClient + failPoint: + configureFailPoint: failCommand + mode: + times: 1 + data: + failCommands: + - find + errorCode: 391 # ReauthenticationRequired + - name: find + object: collection0 + arguments: + filter: {} + expectResult: [] + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collName + filter: {} + - commandFailedEvent: + commandName: find + - commandStartedEvent: + command: + find: collName + filter: {} + - commandSucceededEvent: + commandName: find +- description: Write commands should reauthenticate and retry when a ReauthenticationRequired error happens + operations: + - name: failPoint + object: testRunner + arguments: + client: failPointClient + failPoint: + configureFailPoint: failCommand + mode: + times: 1 + data: + failCommands: + - insert + errorCode: 391 # ReauthenticationRequired + - name: insertOne + object: collection0 + arguments: + document: + _id: 1 + x: 1 + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandFailedEvent: + commandName: insert + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandSucceededEvent: + commandName: insert +- description: Handshake with cached token should use speculative authentication + operations: + - name: failPoint + object: testRunner + arguments: + client: failPointClient + failPoint: + configureFailPoint: failCommand + mode: + times: 1 + data: + failCommands: + - insert + closeConnection: true + - name: insertOne + object: collection0 + arguments: + document: + _id: 1 + x: 1 + expectError: + isClientError: true + - name: failPoint + object: testRunner + arguments: + client: failPointClient + failPoint: + configureFailPoint: failCommand + mode: "alwaysOn" + data: + failCommands: + - saslStart + errorCode: 20 # IllegalOperation + - name: insertOne + object: collection0 + arguments: + document: + _id: 1 + x: 1 + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandFailedEvent: + commandName: insert + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandSucceededEvent: + commandName: insert +- description: Handshake without cached token should not use speculative authentication + operations: + - name: failPoint + object: testRunner + arguments: + client: failPointClient + failPoint: + configureFailPoint: failCommand + mode: "alwaysOn" + data: + failCommands: + - saslStart + errorCode: 20 # IllegalOperation + - name: insertOne + object: collection0 + arguments: + document: + _id: 1 + x: 1 + expectError: + errorCode: 20 # IllegalOperation +- description: Read commands should fail if reauthentication fails + operations: + - name: find + object: collection0 + arguments: + filter: {} + expectResult: [] + - name: failPoint + object: testRunner + arguments: + client: failPointClient + failPoint: + configureFailPoint: failCommand + mode: + times: 2 + data: + failCommands: + - find + - saslStart + errorCode: 391 # ReauthenticationRequired + - name: find + object: collection0 + arguments: + filter: {} + expectError: { errorCode: 391 } + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collName + filter: {} + - commandSucceededEvent: + commandName: find + - commandStartedEvent: + command: + find: collName + filter: {} + - commandFailedEvent: + commandName: find +- description: Write commands should fail if reauthentication fails + operations: + - name: insertOne + object: collection0 + arguments: + document: + _id: 1 + x: 1 + - name: failPoint + object: testRunner + arguments: + client: failPointClient + failPoint: + configureFailPoint: failCommand + mode: + times: 2 + data: + failCommands: + - insert + - saslStart + errorCode: 391 # ReauthenticationRequired + - name: insertOne + object: collection0 + arguments: + document: + _id: 2 + x: 2 + expectError: { errorCode: 391 } + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandSucceededEvent: + commandName: insert + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 2 + x: 2 + - commandFailedEvent: + commandName: insert diff --git a/src/MongoDB.Driver.Core/Core/Authentication/DefaultAuthenticator.cs b/src/MongoDB.Driver.Core/Core/Authentication/DefaultAuthenticator.cs index f897cd250c2..a102faafc02 100644 --- a/src/MongoDB.Driver.Core/Core/Authentication/DefaultAuthenticator.cs +++ b/src/MongoDB.Driver.Core/Core/Authentication/DefaultAuthenticator.cs @@ -86,7 +86,7 @@ public void Authenticate(IConnection connection, ConnectionDescription descripti if (!description.HelloResult.HasSaslSupportedMechs && Feature.ScramSha256Authentication.IsSupported(description.MaxWireVersion)) { - var command = CustomizeInitialHelloCommand(HelloHelper.CreateCommand(_serverApi, loadBalanced: connection.Settings.LoadBalanced)); + var command = CustomizeInitialHelloCommand(HelloHelper.CreateCommand(_serverApi, loadBalanced: connection.Settings.LoadBalanced), cancellationToken); var helloProtocol = HelloHelper.CreateProtocol(command, _serverApi); var helloResult = HelloHelper.GetResult(connection, helloProtocol, cancellationToken); var mergedHelloResult = new HelloResult(description.HelloResult.Wrapped.Merge(helloResult.Wrapped)); @@ -111,7 +111,7 @@ public async Task AuthenticateAsync(IConnection connection, ConnectionDescriptio if (!description.HelloResult.HasSaslSupportedMechs && Feature.ScramSha256Authentication.IsSupported(description.MaxWireVersion)) { - var command = CustomizeInitialHelloCommand(HelloHelper.CreateCommand(_serverApi, loadBalanced: connection.Settings.LoadBalanced)); + var command = CustomizeInitialHelloCommand(HelloHelper.CreateCommand(_serverApi, loadBalanced: connection.Settings.LoadBalanced), cancellationToken); var helloProtocol = HelloHelper.CreateProtocol(command, _serverApi); var helloResult = await HelloHelper.GetResultAsync(connection, helloProtocol, cancellationToken).ConfigureAwait(false); var mergedHelloResult = new HelloResult(description.HelloResult.Wrapped.Merge(helloResult.Wrapped)); @@ -125,12 +125,12 @@ public async Task AuthenticateAsync(IConnection connection, ConnectionDescriptio } /// - public BsonDocument CustomizeInitialHelloCommand(BsonDocument helloCommand) + public BsonDocument CustomizeInitialHelloCommand(BsonDocument helloCommand, CancellationToken cancellationToken) { var saslSupportedMechs = CreateSaslSupportedMechsRequest(_credential.Source, _credential.Username); helloCommand = helloCommand.Merge(saslSupportedMechs); _speculativeAuthenticator = new ScramSha256Authenticator(_credential, _randomStringGenerator, _serverApi); - return _speculativeAuthenticator.CustomizeInitialHelloCommand(helloCommand); + return _speculativeAuthenticator.CustomizeInitialHelloCommand(helloCommand, cancellationToken); } private static BsonDocument CreateSaslSupportedMechsRequest(string authenticationDatabaseName, string userName) diff --git a/src/MongoDB.Driver.Core/Core/Authentication/GssapiAuthenticator.cs b/src/MongoDB.Driver.Core/Core/Authentication/GssapiAuthenticator.cs index 3827241d1b3..9ceb2a23ab1 100644 --- a/src/MongoDB.Driver.Core/Core/Authentication/GssapiAuthenticator.cs +++ b/src/MongoDB.Driver.Core/Core/Authentication/GssapiAuthenticator.cs @@ -18,6 +18,8 @@ using System.Net; using System.Security; using System.Text; +using System.Threading; +using System.Threading.Tasks; using MongoDB.Driver.Core.Connections; using MongoDB.Driver.Core.Misc; @@ -29,6 +31,14 @@ namespace MongoDB.Driver.Core.Authentication public sealed class GssapiAuthenticator : SaslAuthenticator { // constants + /// + /// The name of the mechanism. + /// + public const string MechanismName = "GSSAPI"; + /// + /// The default service name. + /// + public const string DefaultServiceName = "mongodb"; private const string __canonicalizeHostNamePropertyName = "CANONICALIZE_HOST_NAME"; private const string __realmPropertyName = "REALM"; private const string __serviceNamePropertyName = "SERVICE_NAME"; @@ -46,28 +56,6 @@ public static string CanonicalizeHostNamePropertyName get { return __canonicalizeHostNamePropertyName; } } - /// - /// Gets the default service name. - /// - /// - /// The default service name. - /// - public static string DefaultServiceName - { - get { return "mongodb"; } - } - - /// - /// Gets the name of the mechanism. - /// - /// - /// The name of the mechanism. - /// - public static string MechanismName - { - get { return "GSSAPI"; } - } - /// /// Gets the name of the realm property. /// @@ -224,22 +212,17 @@ public string Name get { return MechanismName; } } - public ISaslStep Initialize(IConnection connection, SaslConversation conversation, ConnectionDescription description) + public ISaslStep Initialize( + IConnection connection, + SaslConversation conversation, + ConnectionDescription description, + CancellationToken cancellationToken) { Ensure.IsNotNull(connection, nameof(connection)); Ensure.IsNotNull(description, nameof(description)); - string hostName; - var dnsEndPoint = connection.EndPoint as DnsEndPoint; - if (dnsEndPoint != null) - { - hostName = dnsEndPoint.Host; - } - else if (connection.EndPoint is IPEndPoint) - { - hostName = ((IPEndPoint)connection.EndPoint).Address.ToString(); - } - else + var hostName = connection.EndPoint.GetHostAndPort().Host; + if (string.IsNullOrEmpty(hostName)) { throw new MongoAuthenticationException(connection.ConnectionId, "Only DnsEndPoint and IPEndPoint are supported for GSSAPI authentication."); } @@ -255,6 +238,13 @@ public ISaslStep Initialize(IConnection connection, SaslConversation conversatio return new FirstStep(_serviceName, hostName, _realm, _username, _password, conversation); } + + public Task InitializeAsync( + IConnection connection, + SaslConversation conversation, + ConnectionDescription description, + CancellationToken cancellationToken) + => Task.FromResult(Initialize(connection, conversation, description, cancellationToken)); } private class FirstStep : ISaslStep @@ -315,6 +305,9 @@ public ISaslStep Transition(SaslConversation conversation, byte[] bytesReceivedF return new NegotiateStep(_authorizationId, _context, bytesToSendToServer); } + + public Task TransitionAsync(SaslConversation conversation, byte[] bytesReceivedFromServer, CancellationToken cancellationToken) + => Task.FromResult(Transition(conversation, bytesReceivedFromServer)); } private class InitializeStep : ISaslStep @@ -359,6 +352,9 @@ public ISaslStep Transition(SaslConversation conversation, byte[] bytesReceivedF return new NegotiateStep(_authorizationId, _context, bytesToSendToServer); } + + public Task TransitionAsync(SaslConversation conversation, byte[] bytesReceivedFromServer, CancellationToken cancellationToken) + => Task.FromResult(Transition(conversation, bytesReceivedFromServer)); } private class NegotiateStep : ISaslStep @@ -427,6 +423,9 @@ public ISaslStep Transition(SaslConversation conversation, byte[] bytesReceivedF return new CompletedStep(bytesToSendToServer); } + + public Task TransitionAsync(SaslConversation conversation, byte[] bytesReceivedFromServer, CancellationToken cancellationToken) + => Task.FromResult(Transition(conversation, bytesReceivedFromServer)); } } } diff --git a/src/MongoDB.Driver.Core/Core/Authentication/IAuthenticator.cs b/src/MongoDB.Driver.Core/Core/Authentication/IAuthenticator.cs index 37ed362b0d4..28bc75908f2 100644 --- a/src/MongoDB.Driver.Core/Core/Authentication/IAuthenticator.cs +++ b/src/MongoDB.Driver.Core/Core/Authentication/IAuthenticator.cs @@ -54,7 +54,8 @@ public interface IAuthenticator /// Optionally customizes hello or legacy hello command. /// /// Initial command. + /// The cancellation token. /// Optionally mutated command. - BsonDocument CustomizeInitialHelloCommand(BsonDocument helloCommand); + BsonDocument CustomizeInitialHelloCommand(BsonDocument helloCommand, CancellationToken cancellationToken); } } diff --git a/src/MongoDB.Driver.Core/Core/Authentication/MongoAWSAuthenticator.cs b/src/MongoDB.Driver.Core/Core/Authentication/MongoAWSAuthenticator.cs index 5858930dc9f..a027ce702b9 100644 --- a/src/MongoDB.Driver.Core/Core/Authentication/MongoAWSAuthenticator.cs +++ b/src/MongoDB.Driver.Core/Core/Authentication/MongoAWSAuthenticator.cs @@ -258,7 +258,11 @@ public string Name get { return MechanismName; } } - public ISaslStep Initialize(IConnection connection, SaslConversation conversation, ConnectionDescription description) + public ISaslStep Initialize( + IConnection connection, + SaslConversation conversation, + ConnectionDescription description, + CancellationToken cancellationToken) { Ensure.IsNotNull(connection, nameof(connection)); Ensure.IsNotNull(description, nameof(description)); @@ -276,6 +280,13 @@ public ISaslStep Initialize(IConnection connection, SaslConversation conversatio return new ClientFirst(clientMessageBytes, nonce, _awsCredentials, _clock); } + public Task InitializeAsync( + IConnection connection, + SaslConversation conversation, + ConnectionDescription description, + CancellationToken cancellationToken) + => Task.FromResult(Initialize(connection, conversation, description, cancellationToken)); + private byte[] GenerateRandomBytes() { return _randomByteGenerator.Generate(ClientNonceLength); @@ -352,33 +363,11 @@ public ISaslStep Transition(SaslConversation conversation, byte[] bytesReceivedF var clientSecondMessageBytes = document.ToBson(); - return new ClientLast(clientSecondMessageBytes); - } - } - - private class ClientLast : ISaslStep - { - private readonly byte[] _bytesToSendToServer; - - public ClientLast(byte[] bytesToSendToServer) - { - _bytesToSendToServer = bytesToSendToServer; - } - - public byte[] BytesToSendToServer - { - get { return _bytesToSendToServer; } + return new NoTransitionClientLastSaslStep(clientSecondMessageBytes); } - public bool IsComplete - { - get { return false; } - } - - public ISaslStep Transition(SaslConversation conversation, byte[] bytesReceivedFromServer) - { - return new CompletedStep(); - } + public Task TransitionAsync(SaslConversation conversation, byte[] bytesReceivedFromServer, CancellationToken cancellationToken) + => Task.FromResult(Transition(conversation, bytesReceivedFromServer)); } } } diff --git a/src/MongoDB.Driver.Core/Core/Authentication/MongoDBCRAuthenticator.cs b/src/MongoDB.Driver.Core/Core/Authentication/MongoDBCRAuthenticator.cs index d8adab9a021..f227e017227 100644 --- a/src/MongoDB.Driver.Core/Core/Authentication/MongoDBCRAuthenticator.cs +++ b/src/MongoDB.Driver.Core/Core/Authentication/MongoDBCRAuthenticator.cs @@ -118,10 +118,8 @@ public async Task AuthenticateAsync(IConnection connection, ConnectionDescriptio } /// - public BsonDocument CustomizeInitialHelloCommand(BsonDocument helloCommand) - { - return helloCommand; - } + public BsonDocument CustomizeInitialHelloCommand(BsonDocument helloCommand, CancellationToken cancellationToken) + => helloCommand; // private methods private CommandWireProtocol CreateAuthenticateProtocol(BsonDocument getNonceReply) diff --git a/src/MongoDB.Driver.Core/Core/Authentication/MongoDBX509Authenticator.cs b/src/MongoDB.Driver.Core/Core/Authentication/MongoDBX509Authenticator.cs index 633d971dac3..de110932123 100644 --- a/src/MongoDB.Driver.Core/Core/Authentication/MongoDBX509Authenticator.cs +++ b/src/MongoDB.Driver.Core/Core/Authentication/MongoDBX509Authenticator.cs @@ -120,7 +120,7 @@ public async Task AuthenticateAsync(IConnection connection, ConnectionDescriptio } /// - public BsonDocument CustomizeInitialHelloCommand(BsonDocument helloCommand) + public BsonDocument CustomizeInitialHelloCommand(BsonDocument helloCommand, CancellationToken cancellationToken) { helloCommand.Add("speculativeAuthenticate", CreateAuthenticateCommand()); return helloCommand; diff --git a/src/MongoDB.Driver.Core/Core/Authentication/NoTransitionClientLastSaslStep.cs b/src/MongoDB.Driver.Core/Core/Authentication/NoTransitionClientLastSaslStep.cs new file mode 100644 index 00000000000..7fb23ece7fe --- /dev/null +++ b/src/MongoDB.Driver.Core/Core/Authentication/NoTransitionClientLastSaslStep.cs @@ -0,0 +1,58 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MongoDB.Driver.Core.Authentication +{ + /// + /// Represents a last SASL step. + /// + internal sealed class NoTransitionClientLastSaslStep : SaslAuthenticator.ISaslStep + { + private readonly byte[] _bytesToSendToServer; + + /// + /// Initializes a new instance of the class. + /// + public NoTransitionClientLastSaslStep(byte[] bytesToSendToServer) + { + _bytesToSendToServer = bytesToSendToServer; + } + + /// + public byte[] BytesToSendToServer => _bytesToSendToServer; + + /// + public bool IsComplete => false; + + /// + public SaslAuthenticator.ISaslStep Transition(SaslAuthenticator.SaslConversation conversation, byte[] bytesReceivedFromServer) + { + if (bytesReceivedFromServer?.Length > 0) + { + // should not be reached + throw new InvalidOperationException($"Received an additional {bytesReceivedFromServer} bytes from the server in last SASL response that was not expected."); + } + + return new SaslAuthenticator.CompletedStep(); + } + + public Task TransitionAsync(SaslAuthenticator.SaslConversation conversation, byte[] bytesReceivedFromServer, CancellationToken cancellationToken) + => Task.FromResult(Transition(conversation, bytesReceivedFromServer)); + } +} diff --git a/src/MongoDB.Driver.Core/Core/Authentication/Oidc/AzureOidcCallback.cs b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/AzureOidcCallback.cs new file mode 100644 index 00000000000..96856af1a9c --- /dev/null +++ b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/AzureOidcCallback.cs @@ -0,0 +1,89 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace MongoDB.Driver.Core.Authentication.Oidc +{ + internal sealed class AzureOidcCallback : IOidcCallback + { + private readonly string _tokenResource; + + public AzureOidcCallback(string tokenResource) + { + _tokenResource = tokenResource; + } + + public OidcAccessToken GetOidcAccessToken(OidcCallbackParameters parameters, CancellationToken cancellationToken) + { + var request = CreateMetadataRequest(parameters); + using (cancellationToken.Register(() => request.Abort(), useSynchronizationContext: false)) + { + var response = request.GetResponse(); + return ParseMetadataResponse((HttpWebResponse)response); + } + } + + public async Task GetOidcAccessTokenAsync(OidcCallbackParameters parameters, CancellationToken cancellationToken) + { + var request = CreateMetadataRequest(parameters); + using (cancellationToken.Register(() => request.Abort(), useSynchronizationContext: false)) + { + var response = await request.GetResponseAsync().ConfigureAwait(false); + return ParseMetadataResponse((HttpWebResponse)response); + } + } + + private HttpWebRequest CreateMetadataRequest(OidcCallbackParameters parameters) + { + var metadataUrl = $"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource={_tokenResource}"; + if (!string.IsNullOrEmpty(parameters.UserName)) + { + metadataUrl += $"&client_id={parameters.UserName}"; + } + + var request = WebRequest.CreateHttp(new Uri(metadataUrl)); + request.Headers["Metadata"] = "true"; + request.Accept = "application/json"; + request.Method = "GET"; + + return request; + } + + private OidcAccessToken ParseMetadataResponse(HttpWebResponse response) + { + if (response.StatusCode != HttpStatusCode.OK) + { + throw new InvalidOperationException($"Response status code does not indicate success {response.StatusCode}:{response.StatusDescription}"); + } + + using var responseReader = new StreamReader(response.GetResponseStream()); + using var jsonReader = new JsonReader(responseReader); + + var context = BsonDeserializationContext.CreateRoot(jsonReader); + var document = BsonDocumentSerializer.Instance.Deserialize(context); + + var accessToken = document.GetValue("access_token"); + return new OidcAccessToken(accessToken.AsString, null); + } + } +} diff --git a/src/MongoDB.Driver.Core/Core/Authentication/Oidc/FileOidcCallback.cs b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/FileOidcCallback.cs new file mode 100644 index 00000000000..fc315d00e41 --- /dev/null +++ b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/FileOidcCallback.cs @@ -0,0 +1,57 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Core.Authentication.Oidc +{ + internal sealed class FileOidcCallback : IOidcCallback + { + #region static + public static FileOidcCallback CreateFromEnvironmentVariable(string environmentVariableName, IEnvironmentVariableProvider environmentVariableProvider) + { + var tokenPath = Ensure.IsNotNull(environmentVariableProvider, nameof(environmentVariableProvider)).GetEnvironmentVariable(environmentVariableName); + return new FileOidcCallback(tokenPath); + } + #endregion + + private readonly string _path; + + public FileOidcCallback(string path) + { + _path = Ensure.IsNotNullOrEmpty(path, nameof(path)); + } + + public OidcAccessToken GetOidcAccessToken(OidcCallbackParameters parameters, CancellationToken cancellationToken) + { + var accessToken = File.ReadAllText(_path); + return new(accessToken, expiresIn: null); + } + + public async Task GetOidcAccessTokenAsync(OidcCallbackParameters parameters, CancellationToken cancellationToken) + { +#if NETSTANDARD2_1_OR_GREATER + var accessToken = await File.ReadAllTextAsync(_path, cancellationToken).ConfigureAwait(false); +#else + using var streamReader = new StreamReader(_path, System.Text.Encoding.UTF8, detectEncodingFromByteOrderMarks: true); + var accessToken = await streamReader.ReadToEndAsync().ConfigureAwait(false); // no support for cancellationToken +#endif + return new(accessToken, expiresIn: null); + } + } +} diff --git a/src/MongoDB.Driver.Core/Core/Authentication/Oidc/IOidcCallback.cs b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/IOidcCallback.cs new file mode 100644 index 00000000000..d9982d7bf62 --- /dev/null +++ b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/IOidcCallback.cs @@ -0,0 +1,97 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MongoDB.Driver.Core.Authentication.Oidc +{ + /// + /// Represents OIDC callback request. + /// + public sealed class OidcCallbackParameters + { + /// + /// Initializes a new instance of the class. + /// + /// Callback API version number. + /// User name. + public OidcCallbackParameters(int version, string userName) + { + Version = version; + UserName = userName; + } + + /// + /// Callback API version number. + /// + public int Version { get; } + + /// + /// User name. + /// + public string UserName { get; } + } + + /// + /// Represents OIDC callback response. + /// + public sealed class OidcAccessToken + { + /// + /// Initializes a new instance of the class. + /// + /// OIDC Access Token string. + /// Expiration duration for the Access Token. + public OidcAccessToken(string accessToken, TimeSpan? expiresIn) + { + AccessToken = accessToken; + ExpiresIn = expiresIn; + } + + /// + /// OIDC Access Token string. + /// + public string AccessToken { get; set; } + + /// + /// Expiration duration for the Access Token. + /// + public TimeSpan? ExpiresIn { get; set; } + } + + /// + /// Represents OIDC callback provider. + /// + public interface IOidcCallback + { + /// + /// Get OIDC callback response. + /// + /// The information used by callbacks to authenticate with the Identity Provider. + /// The cancellation token. + /// OIDC callback response + OidcAccessToken GetOidcAccessToken(OidcCallbackParameters parameters, CancellationToken cancellationToken); + + /// + /// Get OIDC callback response. + /// + /// The information used by callbacks to authenticate with the Identity Provider. + /// The cancellation token. + /// OIDC callback response + Task GetOidcAccessTokenAsync(OidcCallbackParameters parameters, CancellationToken cancellationToken); + } +} diff --git a/src/MongoDB.Driver.Core/Core/Authentication/Oidc/MongoOidcAuthenticator.cs b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/MongoOidcAuthenticator.cs new file mode 100644 index 00000000000..221f11f24fa --- /dev/null +++ b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/MongoOidcAuthenticator.cs @@ -0,0 +1,182 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver.Core.Connections; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Core.Authentication.Oidc +{ + internal sealed class MongoOidcAuthenticator : SaslAuthenticator + { + #region static + public const string MechanismName = "MONGODB-OIDC"; + + public static MongoOidcAuthenticator CreateAuthenticator( + string source, + string principalName, + IEnumerable> properties, + IReadOnlyList endPoints, + ServerApi serverApi) + => CreateAuthenticator( + source, + principalName, + properties, + endPoints, + serverApi, + OidcCallbackAdapterCachingFactory.Instance); + + public static MongoOidcAuthenticator CreateAuthenticator( + string source, + string principalName, + IEnumerable> properties, + IReadOnlyList endPoints, + ServerApi serverApi, + IOidcCallbackAdapterFactory callbackAdapterFactory) + { + Ensure.IsNotNull(endPoints, nameof(endPoints)); + Ensure.IsNotNull(callbackAdapterFactory, nameof(callbackAdapterFactory)); + + if (source != "$external") + { + throw new ArgumentException("MONGODB-OIDC authentication must use the $external authentication source.", nameof(source)); + } + + var configuration = new OidcConfiguration(endPoints, principalName, properties); + var callbackAdapter = callbackAdapterFactory.Get(configuration); + var mechanism = new OidcSaslMechanism(callbackAdapter, principalName); + return new MongoOidcAuthenticator(mechanism, serverApi, configuration); + } + #endregion + + private MongoOidcAuthenticator( + OidcSaslMechanism mechanism, + ServerApi serverApi, + OidcConfiguration configuration) + : base(mechanism, serverApi) + { + OidcMechanism = mechanism; + Configuration = configuration; + } + + public override string DatabaseName => "$external"; + + public OidcConfiguration Configuration { get; } + + private OidcSaslMechanism OidcMechanism { get; } + + public override void Authenticate(IConnection connection, ConnectionDescription description, CancellationToken cancellationToken) + { + // Capture the cache state to decide if we want retry on auth error or not. + // Not the best solution, but let us not to introduce the retry logic into SaslAuthenticator to reduce affected areas for now. + // Consider to move this code into SaslAuthenticator when retry logic will be applicable not only for Oidc Auth. + var allowRetryOnAuthError = OidcMechanism.HasCachedCredentials; + TryAuthenticate(allowRetryOnAuthError); + + void TryAuthenticate(bool retryOnFailure) + { + try + { + base.Authenticate(connection, description, cancellationToken); + } + catch (Exception ex) + { + ClearCredentialsCache(); + + if (retryOnFailure && ShouldReauthenticateIfSaslError(ex, connection)) + { + Thread.Sleep(100); + TryAuthenticate(false); + } + else + { + throw UnwrapMongoAuthenticationException(ex); + } + } + } + } + + public override Task AuthenticateAsync(IConnection connection, ConnectionDescription description, CancellationToken cancellationToken) + { + // Capture the cache state to decide if we want retry on auth error or not. + // Not the best solution, but let us not to introduce the retry logic into SaslAuthenticator to reduce affected areas for now. + // Consider to move this code into SaslAuthenticator when retry logic will be applicable not only for Oidc Auth. + var allowRetryOnAuthError = OidcMechanism.HasCachedCredentials; + return TryAuthenticateAsync(allowRetryOnAuthError); + + async Task TryAuthenticateAsync(bool retryOnFailure) + { + try + { + await base.AuthenticateAsync(connection, description, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + ClearCredentialsCache(); + + if (retryOnFailure && ShouldReauthenticateIfSaslError(ex, connection)) + { + await Task.Delay(100).ConfigureAwait(false); + await TryAuthenticateAsync(false).ConfigureAwait(false); + } + else + { + throw UnwrapMongoAuthenticationException(ex); + } + } + } + } + + public override BsonDocument CustomizeInitialHelloCommand(BsonDocument helloCommand, CancellationToken cancellationToken) + { + var speculativeFirstStep = OidcMechanism.CreateSpeculativeAuthenticationStep(cancellationToken); + if (speculativeFirstStep != null) + { + _speculativeFirstStep = speculativeFirstStep; + var firstCommand = CreateStartCommand(speculativeFirstStep); + firstCommand.Add("db", DatabaseName); + helloCommand.Add("speculativeAuthenticate", firstCommand); + } + + return helloCommand; + } + + public void ClearCredentialsCache() => OidcMechanism.ClearCache(); + + private static bool ShouldReauthenticateIfSaslError(Exception ex, IConnection connection) + { + return ex is MongoAuthenticationException authenticationException && + authenticationException.InnerException is MongoCommandException mongoCommandException && + mongoCommandException.Code == (int)ServerErrorCode.AuthenticationFailed && + !connection.Description.IsInitialized(); + } + + private static Exception UnwrapMongoAuthenticationException(Exception ex) + { + if (ex is MongoAuthenticationException mongoAuthenticationException && + mongoAuthenticationException.InnerException != null) + { + return mongoAuthenticationException.InnerException; + } + + return ex; + } + } +} diff --git a/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcCallbackAdapter.cs b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcCallbackAdapter.cs new file mode 100644 index 00000000000..377975fe1bc --- /dev/null +++ b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcCallbackAdapter.cs @@ -0,0 +1,139 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Core.Authentication.Oidc +{ + internal interface IOidcCallbackAdapter + { + OidcCredentials CachedCredentials { get; } + + void InvalidateCachedCredentials(OidcCredentials credentials); + + OidcCredentials GetCredentials(OidcCallbackParameters parameters, CancellationToken cancellationToken); + + ValueTask GetCredentialsAsync(OidcCallbackParameters parameters, CancellationToken cancellationToken); + } + + internal sealed class OidcCallbackAdapter : IOidcCallbackAdapter, IDisposable + { + private readonly IClock _clock; + private readonly IOidcCallback _wrappedCallback; + private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + private OidcCredentials _cachedCredentials; + + public OidcCallbackAdapter(IOidcCallback wrappedCallback, IClock clock) + { + _wrappedCallback = wrappedCallback; + _clock = clock; + } + + public void Dispose() + { + _lock.Dispose(); + } + + public OidcCredentials CachedCredentials => _cachedCredentials; + + public void InvalidateCachedCredentials(OidcCredentials credentials) + { + if (credentials == null) + { + return; + } + + _lock.Wait(); + try + { + if (_cachedCredentials?.AccessToken == credentials.AccessToken) + { + _cachedCredentials = null; + } + } + finally + { + _lock.Release(); + } + } + + public OidcCredentials GetCredentials(OidcCallbackParameters parameters, CancellationToken cancellationToken) + { + var credentials = _cachedCredentials; + if (credentials?.IsExpired == false) + { + return credentials; + } + + _lock.Wait(cancellationToken); + + try + { + if (_cachedCredentials?.IsExpired == false) + { + return _cachedCredentials; + } + + var response = _wrappedCallback.GetOidcAccessToken(parameters, cancellationToken); + if (response == null) + { + throw new InvalidOperationException("Unexpected response from OIDC Callback."); + } + + _cachedCredentials = new OidcCredentials(response.AccessToken, response.ExpiresIn, _clock); + return _cachedCredentials; + } + finally + { + _lock.Release(); + } + } + + public async ValueTask GetCredentialsAsync(OidcCallbackParameters parameters, CancellationToken cancellationToken) + { + var credentials = _cachedCredentials; + if (credentials?.IsExpired == false) + { + return credentials; + } + + await _lock.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + if (_cachedCredentials?.IsExpired == false) + { + return _cachedCredentials; + } + + var response = await _wrappedCallback.GetOidcAccessTokenAsync(parameters, cancellationToken).ConfigureAwait(false); + if (response == null) + { + throw new InvalidOperationException("Unexpected response from OIDC Callback."); + } + + _cachedCredentials = new OidcCredentials(response.AccessToken, response.ExpiresIn, _clock); + return _cachedCredentials; + } + finally + { + _lock.Release(); + } + } + } +} diff --git a/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcCallbackAdapterFactory.cs b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcCallbackAdapterFactory.cs new file mode 100644 index 00000000000..963841df9b1 --- /dev/null +++ b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcCallbackAdapterFactory.cs @@ -0,0 +1,64 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Collections.Concurrent; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Core.Authentication.Oidc +{ + internal interface IOidcCallbackAdapterFactory + { + IOidcCallbackAdapter Get(OidcConfiguration configuration); + } + + internal class OidcCallbackAdapterCachingFactory : IOidcCallbackAdapterFactory + { + public static readonly OidcCallbackAdapterCachingFactory Instance = new(SystemClock.Instance, EnvironmentVariableProvider.Instance); + + private readonly IClock _clock; + private readonly IEnvironmentVariableProvider _environmentVariableProvider; + private readonly ConcurrentDictionary _cache = new(); + + public OidcCallbackAdapterCachingFactory(IClock clock, IEnvironmentVariableProvider environmentVariableProvider) + { + _clock = clock; + _environmentVariableProvider = environmentVariableProvider; + } + + public IOidcCallbackAdapter Get(OidcConfiguration configuration) + => _cache.GetOrAdd(configuration, CreateCallbackAdapter); + + internal void Reset() + => _cache.Clear(); + + private IOidcCallbackAdapter CreateCallbackAdapter(OidcConfiguration configuration) + { + var callback = configuration.Callback; + + if (!string.IsNullOrEmpty(configuration.Environment)) + { + callback = configuration.Environment switch + { + "test" => FileOidcCallback.CreateFromEnvironmentVariable("OIDC_TOKEN_FILE", _environmentVariableProvider), + "azure" => new AzureOidcCallback(configuration.TokenResource), + _ => throw new NotSupportedException($"Non supported {OidcConfiguration.EnvironmentMechanismPropertyName} value: {configuration.Environment}") + }; + } + + return new OidcCallbackAdapter(callback, _clock); + } + } +} diff --git a/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcConfiguration.cs b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcConfiguration.cs new file mode 100644 index 00000000000..e9e12bc486c --- /dev/null +++ b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcConfiguration.cs @@ -0,0 +1,143 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using MongoDB.Driver.Core.Misc; +using MongoDB.Shared; + +namespace MongoDB.Driver.Core.Authentication.Oidc +{ + internal sealed class OidcConfiguration + { + public const string CallbackMechanismPropertyName = "OIDC_CALLBACK"; + public const string EnvironmentMechanismPropertyName = "ENVIRONMENT"; + public const string TokenResourceMechanismPropertyName = "TOKEN_RESOURCE"; + + private static readonly ISet __supportedEnvironments = new HashSet { "test", "azure" }; + private readonly int _hashCode; + + public OidcConfiguration( + IEnumerable endPoints, + string principalName, + IEnumerable> authMechanismProperties) + { + EndPoints = Ensure.IsNotNullOrEmpty(endPoints, nameof(endPoints)); + Ensure.IsNotNull(authMechanismProperties, nameof(authMechanismProperties)); + PrincipalName = principalName; + + if (authMechanismProperties != null) + { + foreach (var authorizationProperty in authMechanismProperties) + { + switch (authorizationProperty.Key) + { + case CallbackMechanismPropertyName: + Callback = GetProperty(authorizationProperty); + break; + case EnvironmentMechanismPropertyName: + Environment = GetProperty(authorizationProperty); + break; + case TokenResourceMechanismPropertyName: + TokenResource = GetProperty(authorizationProperty); + break; + default: + throw new ArgumentException( + $"Unknown OIDC property '{authorizationProperty.Key}'.", + authorizationProperty.Key); + } + } + } + + ValidateOptions(); + _hashCode = CalculateHashCode(); + + static T GetProperty(KeyValuePair property) + { + if (property.Value is T result) + { + return result; + } + + throw new ArgumentException($"Cannot read {property.Key} property as {typeof(T).Name}", property.Key); + } + } + + public IOidcCallback Callback { get; } + public IEnumerable EndPoints { get; } + public string Environment { get; } + public string PrincipalName { get; } + public string TokenResource { get; } + + public override int GetHashCode() => _hashCode; + + public override bool Equals(object obj) + { + if (object.ReferenceEquals(obj, null)) { return false; } + if (object.ReferenceEquals(this, obj)) { return true; } + return + obj is OidcConfiguration other && + Environment == other.Environment && + PrincipalName == other.PrincipalName && + object.Equals(Callback, other.Callback) && + TokenResource == other.TokenResource && + EndPoints.SequenceEqual(other.EndPoints, EndPointHelper.EndPointEqualityComparer); + } + + private int CalculateHashCode() + => new Hasher() + .Hash(Environment) + .Hash(PrincipalName) + .HashElements(EndPoints) + .Hash(TokenResource) + .GetHashCode(); + + private void ValidateOptions() + { + if (Environment == null && Callback == null) + { + throw new ArgumentException($"{EnvironmentMechanismPropertyName} or {CallbackMechanismPropertyName} must be configured."); + } + + if (Environment != null && Callback != null) + { + throw new ArgumentException($"{CallbackMechanismPropertyName} is mutually exclusive with {EnvironmentMechanismPropertyName}."); + } + + if (Environment != null && !__supportedEnvironments.Contains(Environment)) + { + throw new ArgumentException( + $"Not supported value of {EnvironmentMechanismPropertyName} mechanism property: {Environment}.", + EnvironmentMechanismPropertyName); + } + + if (!string.IsNullOrEmpty(TokenResource) && Environment != "azure") + { + throw new ArgumentException( + $"{TokenResourceMechanismPropertyName} mechanism property supported only by azure environment.", + TokenResourceMechanismPropertyName); + } + + if (Environment == "azure" && string.IsNullOrEmpty(TokenResource)) + { + throw new ArgumentException( + $"{TokenResourceMechanismPropertyName} mechanism property is required by azure environment.", + TokenResourceMechanismPropertyName); + } + } + } +} diff --git a/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcCredentials.cs b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcCredentials.cs new file mode 100644 index 00000000000..0ea87802430 --- /dev/null +++ b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcCredentials.cs @@ -0,0 +1,36 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using MongoDB.Driver.Core.Misc; + +namespace MongoDB.Driver.Core.Authentication.Oidc +{ + internal sealed class OidcCredentials + { + private readonly IClock _clock; + + public OidcCredentials(string accessToken, TimeSpan? expiration, IClock clock) + { + AccessToken = Ensure.IsNotNullOrEmpty(accessToken, nameof(accessToken)); + Expiration = expiration.HasValue ? clock.UtcNow.Add(expiration.Value) : DateTime.MaxValue; + _clock = clock; + } + + public string AccessToken { get; } + public DateTime Expiration { get; } + public bool IsExpired => Expiration <= _clock.UtcNow; + } +} diff --git a/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcSaslMechanism.cs b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcSaslMechanism.cs new file mode 100644 index 00000000000..e34776ffb28 --- /dev/null +++ b/src/MongoDB.Driver.Core/Core/Authentication/Oidc/OidcSaslMechanism.cs @@ -0,0 +1,84 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Driver.Core.Connections; + +namespace MongoDB.Driver.Core.Authentication.Oidc +{ + internal sealed class OidcSaslMechanism : SaslAuthenticator.ISaslMechanism + { + private readonly IOidcCallbackAdapter _oidcCallback; + private readonly string _principalName; + private OidcCredentials _usedCredentials; + + public OidcSaslMechanism(IOidcCallbackAdapter oidcCallback, string principalName) + { + _oidcCallback = oidcCallback; + _principalName = principalName; + } + + public string Name => MongoOidcAuthenticator.MechanismName; + + public bool HasCachedCredentials => _oidcCallback.CachedCredentials != null; + + public SaslAuthenticator.ISaslStep Initialize( + IConnection connection, + SaslAuthenticator.SaslConversation conversation, + ConnectionDescription description, + CancellationToken cancellationToken) + { + var credentials = _oidcCallback.GetCredentials(new OidcCallbackParameters(1, _principalName), cancellationToken); + return CreateNoTransitionClientLastSaslStep(credentials); + } + + public async Task InitializeAsync( + IConnection connection, + SaslAuthenticator.SaslConversation conversation, + ConnectionDescription description, + CancellationToken cancellationToken) + { + var credentials = await _oidcCallback.GetCredentialsAsync(new OidcCallbackParameters(1, _principalName), cancellationToken); + return CreateNoTransitionClientLastSaslStep(credentials); + } + + public SaslAuthenticator.ISaslStep CreateSpeculativeAuthenticationStep(CancellationToken cancellationToken) + { + var cachedCredentials = _oidcCallback.CachedCredentials; + if (cachedCredentials == null) + { + return null; + } + + return CreateNoTransitionClientLastSaslStep(cachedCredentials); + } + + public void ClearCache() => _oidcCallback.InvalidateCachedCredentials(_usedCredentials); + + private SaslAuthenticator.ISaslStep CreateNoTransitionClientLastSaslStep(OidcCredentials oidcCredentials) + { + if (oidcCredentials == null) + { + throw new InvalidOperationException("OIDC credentials have not been provided."); + } + + _usedCredentials = oidcCredentials; + return new NoTransitionClientLastSaslStep(new BsonDocument("jwt", oidcCredentials.AccessToken).ToBson()); + } + } +} diff --git a/src/MongoDB.Driver.Core/Core/Authentication/PlainAuthenticator.cs b/src/MongoDB.Driver.Core/Core/Authentication/PlainAuthenticator.cs index 85f1d9c5d89..98781996f34 100644 --- a/src/MongoDB.Driver.Core/Core/Authentication/PlainAuthenticator.cs +++ b/src/MongoDB.Driver.Core/Core/Authentication/PlainAuthenticator.cs @@ -14,6 +14,8 @@ */ using System; +using System.Threading; +using System.Threading.Tasks; using MongoDB.Bson.IO; using MongoDB.Driver.Core.Connections; using MongoDB.Driver.Core.Misc; @@ -88,7 +90,11 @@ public string Name } // methods - public ISaslStep Initialize(IConnection connection, SaslConversation conversation, ConnectionDescription description) + public ISaslStep Initialize( + IConnection connection, + SaslConversation conversation, + ConnectionDescription description, + CancellationToken cancellationToken) { Ensure.IsNotNull(connection, nameof(connection)); Ensure.IsNotNull(description, nameof(description)); @@ -100,6 +106,13 @@ public ISaslStep Initialize(IConnection connection, SaslConversation conversatio var bytes = Utf8Encodings.Strict.GetBytes(dataString); return new CompletedStep(bytes); } + + public Task InitializeAsync( + IConnection connection, + SaslConversation conversation, + ConnectionDescription description, + CancellationToken cancellationToken) + => Task.FromResult(Initialize(connection, conversation, description, cancellationToken)); } } } diff --git a/src/MongoDB.Driver.Core/Core/Authentication/SaslAuthenticator.cs b/src/MongoDB.Driver.Core/Core/Authentication/SaslAuthenticator.cs index d05223480ca..bb99e6f5857 100644 --- a/src/MongoDB.Driver.Core/Core/Authentication/SaslAuthenticator.cs +++ b/src/MongoDB.Driver.Core/Core/Authentication/SaslAuthenticator.cs @@ -17,7 +17,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using DnsClient; using MongoDB.Bson; using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver.Core.Connections; @@ -31,6 +30,15 @@ namespace MongoDB.Driver.Core.Authentication /// public abstract class SaslAuthenticator : IAuthenticator { + /// + /// The SASL start command. + /// + public const string SaslStartCommand = "saslStart"; + /// + /// The SASL continue command. + /// + public const string SaslContinueCommand = "saslContinue"; + // fields private protected readonly ISaslMechanism _mechanism; private protected readonly ServerApi _serverApi; @@ -60,10 +68,7 @@ protected SaslAuthenticator(ISaslMechanism mechanism, ServerApi serverApi) // properties /// - public string Name - { - get { return _mechanism.Name; } - } + public string Name => _mechanism.Name; /// /// Gets the name of the database. @@ -83,32 +88,35 @@ public virtual void Authenticate(IConnection connection, ConnectionDescription d using (var conversation = new SaslConversation(description.ConnectionId)) { ISaslStep currentStep; - BsonDocument command; - var speculativeAuthenticateResult = description.HelloResult.SpeculativeAuthenticate; - if (_speculativeFirstStep != null && speculativeAuthenticateResult != null) + int? conversationId = null; + + if (TryGetSpeculativeFirstStep(description, out currentStep, out var result)) { - currentStep = Transition(conversation, _speculativeFirstStep, speculativeAuthenticateResult, out command); + currentStep = Transition(conversation, currentStep, result); + conversationId = result.GetValue("conversationId").AsInt32; } else { - currentStep = _mechanism.Initialize(connection, conversation, description); - command = CreateStartCommand(currentStep); + currentStep = _mechanism.Initialize(connection, conversation, description, cancellationToken); } while (currentStep != null) { - BsonDocument result; + var command = conversationId.HasValue + ? CreateContinueCommand(currentStep, conversationId.Value) + : CreateStartCommand(currentStep); try { var protocol = CreateCommandProtocol(command); result = protocol.Execute(connection, cancellationToken); + conversationId ??= result?.GetValue("conversationId").AsInt32; } catch (MongoException ex) { - throw CreateException(connection, ex); + throw CreateException(connection.ConnectionId, ex, command); } - currentStep = Transition(conversation, currentStep, result, out command); + currentStep = Transition(conversation, currentStep, result); } } } @@ -122,116 +130,155 @@ public virtual async Task AuthenticateAsync(IConnection connection, ConnectionDe using (var conversation = new SaslConversation(description.ConnectionId)) { ISaslStep currentStep; - BsonDocument command; - var speculativeAuthenticateResult = description.HelloResult.SpeculativeAuthenticate; - if (_speculativeFirstStep != null && speculativeAuthenticateResult != null) + int? conversationId = null; + + if (TryGetSpeculativeFirstStep(description, out currentStep, out var result)) { - currentStep = Transition(conversation, _speculativeFirstStep, speculativeAuthenticateResult, out command); + currentStep = await TransitionAsync(conversation, currentStep, result, cancellationToken).ConfigureAwait(false); + conversationId = result.GetValue("conversationId").AsInt32; } else { - currentStep = _mechanism.Initialize(connection, conversation, description); - command = CreateStartCommand(currentStep); + currentStep = await _mechanism.InitializeAsync(connection, conversation, description, cancellationToken).ConfigureAwait(false); } while (currentStep != null) { - BsonDocument result; + var command = conversationId.HasValue + ? CreateContinueCommand(currentStep, conversationId.Value) + : CreateStartCommand(currentStep); try { var protocol = CreateCommandProtocol(command); result = await protocol.ExecuteAsync(connection, cancellationToken).ConfigureAwait(false); + conversationId ??= result?.GetValue("conversationId").AsInt32; } catch (MongoException ex) { - throw CreateException(connection, ex); + throw CreateException(connection.ConnectionId, ex, command); } - currentStep = Transition(conversation, currentStep, result, out command); + currentStep = await TransitionAsync(conversation, currentStep, result, cancellationToken).ConfigureAwait(false); } } } /// - public virtual BsonDocument CustomizeInitialHelloCommand(BsonDocument helloCommand) - { - return helloCommand; - } + public virtual BsonDocument CustomizeInitialHelloCommand(BsonDocument helloCommand, CancellationToken cancellationToken) + => helloCommand; - private protected virtual BsonDocument CreateStartCommand(ISaslStep currentStep) + /// + /// Determines whether saslStart should be skipped. + /// + /// The connection description. + /// The first sasl step. + /// The result. + /// A flag whether saslStart command can be skipped. + private protected bool TryGetSpeculativeFirstStep(ConnectionDescription description, out ISaslStep firstStep, out BsonDocument result) { - var startCommand = new BsonDocument + var speculativeAuthenticateResult = description.HelloResult.SpeculativeAuthenticate; + if (_speculativeFirstStep != null && speculativeAuthenticateResult != null) { - { "saslStart", 1 }, - { "mechanism", _mechanism.Name }, - { "payload", currentStep.BytesToSendToServer } - }; + result = speculativeAuthenticateResult; + firstStep = _speculativeFirstStep; + return true; + } + else + { + result = null; + firstStep = null; + return false; + } + } - return startCommand; + private protected virtual MongoAuthenticationException CreateException(ConnectionId connectionId, Exception ex, BsonDocument command) + { + // Do NOT echo the full command into exception message + var message = $"Unable to authenticate using sasl protocol mechanism {Name}."; + return new MongoAuthenticationException(connectionId, message, ex); } private CommandWireProtocol CreateCommandProtocol(BsonDocument command) - { - return new CommandWireProtocol( + => new CommandWireProtocol( databaseNamespace: new DatabaseNamespace(DatabaseName), command: command, secondaryOk: true, resultSerializer: BsonDocumentSerializer.Instance, messageEncoderSettings: null, serverApi: _serverApi); - } - private BsonDocument CreateContinueCommand(ISaslStep currentStep, BsonDocument result) - { - return new BsonDocument + private protected virtual BsonDocument CreateStartCommand(ISaslStep currentStep) + => new BsonDocument + { + { SaslStartCommand, 1 }, + { "mechanism", _mechanism.Name }, + { "payload", currentStep.BytesToSendToServer } + }; + + + private BsonDocument CreateContinueCommand(ISaslStep currentStep, int conversationId) + => new BsonDocument { - { "saslContinue", 1 }, - { "conversationId", result["conversationId"].AsInt32 }, + { SaslContinueCommand, 1 }, + { "conversationId", conversationId }, { "payload", currentStep.BytesToSendToServer } }; - } - private MongoAuthenticationException CreateException(IConnection connection, Exception ex) + private bool IsCompleted(ISaslStep currentStep, BsonDocument result) => currentStep.IsComplete && result.GetValue("done", false).ToBoolean(); + + private ISaslStep Transition( + SaslConversation conversation, + ISaslStep currentStep, + BsonDocument result) { - var message = string.Format("Unable to authenticate using sasl protocol mechanism {0}.", Name); - return new MongoAuthenticationException(connection.ConnectionId, message, ex); + // we might be done here if the client is not expecting a reply from the server + if (IsCompleted(currentStep, result)) + { + return null; + } + + currentStep = currentStep.Transition(conversation, result["payload"].AsByteArray); + + // we might be done here if the client had some final verification it needed to do + if (IsCompleted(currentStep, result)) + { + return null; + } + + return currentStep; } - private ISaslStep Transition( + private async Task TransitionAsync( SaslConversation conversation, ISaslStep currentStep, BsonDocument result, - out BsonDocument command) + CancellationToken cancellationToken) { // we might be done here if the client is not expecting a reply from the server - if (result.GetValue("done", false).ToBoolean() && currentStep.IsComplete) + if (IsCompleted(currentStep, result)) { - command = null; return null; } - currentStep = currentStep.Transition(conversation, result["payload"].AsByteArray); + currentStep = await currentStep.TransitionAsync(conversation, result["payload"].AsByteArray, cancellationToken).ConfigureAwait(false); // we might be done here if the client had some final verification it needed to do - if (result.GetValue("done", false).ToBoolean() && currentStep.IsComplete) + if (IsCompleted(currentStep, result)) { - command = null; return null; } - command = CreateContinueCommand(currentStep, result); return currentStep; } - // nested classes /// /// Represents a SASL conversation. /// - protected sealed class SaslConversation : IDisposable + protected internal sealed class SaslConversation : IDisposable { // fields private readonly ConnectionId _connectionId; - private List _itemsNeedingDisposal; + private readonly List _itemsNeedingDisposal; private bool _isDisposed; // constructors @@ -286,7 +333,7 @@ public void Dispose() /// /// Represents a SASL mechanism. /// - protected interface ISaslMechanism + protected internal interface ISaslMechanism { // properties /// @@ -304,14 +351,25 @@ protected interface ISaslMechanism /// The connection. /// The SASL conversation. /// The connection description. + /// The cancellation token. + /// The initial SASL step. + ISaslStep Initialize(IConnection connection, SaslConversation conversation, ConnectionDescription description, CancellationToken cancellationToken); + + /// + /// Initializes the mechanism. + /// + /// The connection. + /// The SASL conversation. + /// The connection description. + /// The cancellation token. /// The initial SASL step. - ISaslStep Initialize(IConnection connection, SaslConversation conversation, ConnectionDescription description); + Task InitializeAsync(IConnection connection, SaslConversation conversation, ConnectionDescription description, CancellationToken cancellationToken); } /// /// Represents a SASL step. /// - protected interface ISaslStep + protected internal interface ISaslStep { // properties /// @@ -338,12 +396,21 @@ protected interface ISaslStep /// The bytes received from server. /// The next SASL step. ISaslStep Transition(SaslConversation conversation, byte[] bytesReceivedFromServer); + + /// + /// Transitions the SASL conversation to the next step. + /// + /// The SASL conversation. + /// The bytes received from server. + /// The cancellation token. + /// The next SASL step. + Task TransitionAsync(SaslConversation conversation, byte[] bytesReceivedFromServer, CancellationToken cancellationToken); } /// /// Represents a completed SASL step. /// - protected class CompletedStep : ISaslStep + protected internal class CompletedStep : ISaslStep { // fields private readonly byte[] _bytesToSendToServer; @@ -353,7 +420,7 @@ protected class CompletedStep : ISaslStep /// Initializes a new instance of the class. /// public CompletedStep() - : this(new byte[0]) + : this(Array.Empty()) { } @@ -382,8 +449,18 @@ public bool IsComplete /// public ISaslStep Transition(SaslConversation conversation, byte[] bytesReceivedFromServer) { + if (bytesReceivedFromServer?.Length > 0) + { + // should not be reached + throw new InvalidOperationException("Not all authentication response has been handled."); + } + throw new InvalidOperationException("Sasl conversation has completed."); } + + /// + public Task TransitionAsync(SaslConversation conversation, byte[] bytesReceivedFromServer, CancellationToken cancellationToken) + => Task.FromResult(Transition(conversation, bytesReceivedFromServer)); } } } diff --git a/src/MongoDB.Driver.Core/Core/Authentication/ScramShaAuthenticator.cs b/src/MongoDB.Driver.Core/Core/Authentication/ScramShaAuthenticator.cs index 7843c62d621..331cd2ec213 100644 --- a/src/MongoDB.Driver.Core/Core/Authentication/ScramShaAuthenticator.cs +++ b/src/MongoDB.Driver.Core/Core/Authentication/ScramShaAuthenticator.cs @@ -16,6 +16,8 @@ using System; using System.Security.Cryptography; using System.Text; +using System.Threading; +using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Bson.IO; using MongoDB.Driver.Core.Connections; @@ -126,10 +128,10 @@ public abstract class ScramShaAuthenticator : SaslAuthenticator public override string DatabaseName => _databaseName; /// - public override BsonDocument CustomizeInitialHelloCommand(BsonDocument helloCommand) + public override BsonDocument CustomizeInitialHelloCommand(BsonDocument helloCommand, CancellationToken cancellationToken) { - helloCommand = base.CustomizeInitialHelloCommand(helloCommand); - _speculativeFirstStep = _mechanism.Initialize(connection: null, conversation: null, description: null); + helloCommand = base.CustomizeInitialHelloCommand(helloCommand, cancellationToken); + _speculativeFirstStep = _mechanism.Initialize(null, null, null, cancellationToken); var firstCommand = CreateStartCommand(_speculativeFirstStep); firstCommand.Add("db", DatabaseName); helloCommand.Add("speculativeAuthenticate", firstCommand); @@ -178,7 +180,11 @@ private class ScramShaMechanism : ISaslMechanism public string Name => _name; - public ISaslStep Initialize(IConnection connection, SaslConversation conversation, ConnectionDescription description) + public ISaslStep Initialize( + IConnection connection, + SaslConversation conversation, + ConnectionDescription description, + CancellationToken cancellationToken) { const string gs2Header = "n,,"; var username = "n=" + PrepUsername(_credential.Username); @@ -192,6 +198,13 @@ public ISaslStep Initialize(IConnection connection, SaslConversation conversatio return new ClientFirst(clientFirstMessageBytes, clientFirstMessageBare, _credential, r, _h, _hi, _hmac, _cache); } + public Task InitializeAsync( + IConnection connection, + SaslConversation conversation, + ConnectionDescription description, + CancellationToken cancellationToken) + => Task.FromResult(Initialize(connection, conversation, description, cancellationToken)); + private string GenerateRandomString() { const string legalCharacters = "!\"#$%&'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; @@ -292,6 +305,9 @@ public ISaslStep Transition(SaslConversation conversation, byte[] bytesReceivedF return new ClientLast(encoding.GetBytes(clientFinalMessage), serverSignature); } + public Task TransitionAsync(SaslConversation conversation, byte[] bytesReceivedFromServer, CancellationToken cancellationToken) + => Task.FromResult(Transition(conversation, bytesReceivedFromServer)); + private byte[] XOR(byte[] a, byte[] b) { var result = new byte[a.Length]; @@ -333,6 +349,9 @@ public ISaslStep Transition(SaslConversation conversation, byte[] bytesReceivedF return new CompletedStep(); } + public Task TransitionAsync(SaslConversation conversation, byte[] bytesReceivedFromServer, CancellationToken cancellationToken) + => Task.FromResult(Transition(conversation, bytesReceivedFromServer)); + private bool ConstantTimeEquals(byte[] a, byte[] b) { var diff = a.Length ^ b.Length; diff --git a/src/MongoDB.Driver.Core/Core/Configuration/ClusterBuilderExtensions.cs b/src/MongoDB.Driver.Core/Core/Configuration/ClusterBuilderExtensions.cs index 790285ebc2a..3007bc16a11 100644 --- a/src/MongoDB.Driver.Core/Core/Configuration/ClusterBuilderExtensions.cs +++ b/src/MongoDB.Driver.Core/Core/Configuration/ClusterBuilderExtensions.cs @@ -14,16 +14,17 @@ */ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net.Security; using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using MongoDB.Driver.Core.Authentication; +using MongoDB.Driver.Core.Authentication.Oidc; using MongoDB.Driver.Core.Clusters; using MongoDB.Driver.Core.Events.Diagnostics; using MongoDB.Driver.Core.Misc; -using MongoDB.Shared; namespace MongoDB.Driver.Core.Configuration { @@ -265,7 +266,8 @@ private static string GetAuthSource(ConnectionString connectionString) { var defaultSource = GetDefaultAuthSource(connectionString); - if (connectionString.AuthMechanism != null && connectionString.AuthMechanism == MongoAWSAuthenticator.MechanismName) + if (connectionString.AuthMechanism == MongoAWSAuthenticator.MechanismName || + connectionString.AuthMechanism == MongoOidcAuthenticator.MechanismName) { return connectionString.AuthSource ?? defaultSource; } @@ -277,7 +279,8 @@ private static string GetDefaultAuthSource(ConnectionString connectionString) { if (connectionString.AuthMechanism != null && ( connectionString.AuthMechanism == GssapiAuthenticator.MechanismName || - connectionString.AuthMechanism == MongoAWSAuthenticator.MechanismName)) + connectionString.AuthMechanism == MongoAWSAuthenticator.MechanismName || + connectionString.AuthMechanism == MongoOidcAuthenticator.MechanismName)) { return "$external"; } @@ -324,6 +327,10 @@ private static IAuthenticator CreateAuthenticator(ConnectionString connectionStr { return new MongoAWSAuthenticator(credential, connectionString.AuthMechanismProperties, serverApi); } + else if (connectionString.AuthMechanism == MongoOidcAuthenticator.MechanismName) + { + throw new NotSupportedException("OIDC authenticator cannot be constructed with password."); + } } else { @@ -339,6 +346,15 @@ private static IAuthenticator CreateAuthenticator(ConnectionString connectionStr { return new MongoAWSAuthenticator(connectionString.Username, connectionString.AuthMechanismProperties, serverApi); } + else if (connectionString.AuthMechanism == MongoOidcAuthenticator.MechanismName) + { + return MongoOidcAuthenticator.CreateAuthenticator( + connectionString.AuthSource, + connectionString.Username, + connectionString.AuthMechanismProperties?.Select(pair => new KeyValuePair(pair.Key, pair.Value)), + connectionString.Hosts, + serverApi); + } } throw new NotSupportedException("Unable to create an authenticator."); diff --git a/src/MongoDB.Driver.Core/Core/ConnectionPools/ExclusiveConnectionPool.Helpers.cs b/src/MongoDB.Driver.Core/Core/ConnectionPools/ExclusiveConnectionPool.Helpers.cs index afa2a0faca7..300d502bb33 100644 --- a/src/MongoDB.Driver.Core/Core/ConnectionPools/ExclusiveConnectionPool.Helpers.cs +++ b/src/MongoDB.Driver.Core/Core/ConnectionPools/ExclusiveConnectionPool.Helpers.cs @@ -442,6 +442,10 @@ public async Task OpenAsync(CancellationToken cancellationToken) } } + public void Reauthenticate(CancellationToken cancellationToken) => _connection.Reauthenticate(cancellationToken); + + public Task ReauthenticateAsync(CancellationToken cancellationToken) => _connection.ReauthenticateAsync(cancellationToken); + public ResponseMessage ReceiveMessage(int responseTo, IMessageEncoderSelector encoderSelector, MessageEncoderSettings messageEncoderSettings, CancellationToken cancellationToken) { try @@ -606,6 +610,18 @@ public Task OpenAsync(CancellationToken cancellationToken) return _reference.Instance.OpenAsync(cancellationToken); } + public void Reauthenticate(CancellationToken cancellationToken) + { + ThrowIfDisposed(); + _reference.Instance.Reauthenticate(cancellationToken); + } + + public Task ReauthenticateAsync(CancellationToken cancellationToken) + { + ThrowIfDisposed(); + return _reference.Instance.ReauthenticateAsync(cancellationToken); + } + public Task ReceiveMessageAsync(int responseTo, IMessageEncoderSelector encoderSelector, MessageEncoderSettings messageEncoderSettings, CancellationToken cancellationToken) { ThrowIfDisposed(); diff --git a/src/MongoDB.Driver.Core/Core/Connections/BinaryConnection.cs b/src/MongoDB.Driver.Core/Core/Connections/BinaryConnection.cs index 705e07e8af8..a8ca7f65ee5 100644 --- a/src/MongoDB.Driver.Core/Core/Connections/BinaryConnection.cs +++ b/src/MongoDB.Driver.Core/Core/Connections/BinaryConnection.cs @@ -25,6 +25,7 @@ using System.Buffers.Binary; using Microsoft.Extensions.Logging; using MongoDB.Bson.IO; +using MongoDB.Driver.Core.Authentication.Oidc; using MongoDB.Driver.Core.Compression; using MongoDB.Driver.Core.Configuration; using MongoDB.Driver.Core.Events; @@ -47,6 +48,7 @@ internal class BinaryConnection : IConnection private readonly ICompressorSource _compressorSource; private ConnectionId _connectionId; private readonly IConnectionInitializer _connectionInitializer; + private ConnectionInitializerContext _connectionInitializerContext; private EndPoint _endPoint; private ConnectionDescription _description; private readonly Dropbox _dropbox = new Dropbox(); @@ -261,21 +263,23 @@ public Task OpenAsync(CancellationToken cancellationToken) private void OpenHelper(CancellationToken cancellationToken) { var helper = new OpenConnectionHelper(this); - + ConnectionDescription handshakeDescription = null; try { helper.OpeningConnection(); _stream = _streamFactory.CreateStream(_endPoint, cancellationToken); helper.InitializingConnection(); - var connectionInitializerContext = _connectionInitializer.SendHello(this, cancellationToken); - _description = connectionInitializerContext.Description; - _description = _connectionInitializer.Authenticate(this, connectionInitializerContext, cancellationToken); + _connectionInitializerContext = _connectionInitializer.SendHello(this, cancellationToken); + handshakeDescription = _connectionInitializerContext.Description; + _connectionInitializerContext = _connectionInitializer.Authenticate(this, _connectionInitializerContext, cancellationToken); + _description = _connectionInitializerContext.Description; _sendCompressorType = ChooseSendCompressorTypeIfAny(_description); helper.OpenedConnection(); } catch (Exception ex) { + _description ??= handshakeDescription; var wrappedException = WrapExceptionIfRequired(ex, "opening a connection to the server"); helper.FailedOpeningConnection(wrappedException ?? ex); if (wrappedException == null) { throw; } else { throw wrappedException; } @@ -285,27 +289,51 @@ private void OpenHelper(CancellationToken cancellationToken) private async Task OpenHelperAsync(CancellationToken cancellationToken) { var helper = new OpenConnectionHelper(this); - + ConnectionDescription handshakeDescription = null; try { helper.OpeningConnection(); _stream = await _streamFactory.CreateStreamAsync(_endPoint, cancellationToken).ConfigureAwait(false); helper.InitializingConnection(); - var connectionInitializerContext = await _connectionInitializer.SendHelloAsync(this, cancellationToken).ConfigureAwait(false); - _description = connectionInitializerContext.Description; - _description = await _connectionInitializer.AuthenticateAsync(this, connectionInitializerContext, cancellationToken).ConfigureAwait(false); + _connectionInitializerContext = await _connectionInitializer.SendHelloAsync(this, cancellationToken).ConfigureAwait(false); + handshakeDescription = _connectionInitializerContext.Description; + _connectionInitializerContext = await _connectionInitializer.AuthenticateAsync(this, _connectionInitializerContext, cancellationToken).ConfigureAwait(false); + _description = _connectionInitializerContext.Description; _sendCompressorType = ChooseSendCompressorTypeIfAny(_description); - helper.OpenedConnection(); } catch (Exception ex) { + _description ??= handshakeDescription; var wrappedException = WrapExceptionIfRequired(ex, "opening a connection to the server"); helper.FailedOpeningConnection(wrappedException ?? ex); if (wrappedException == null) { throw; } else { throw wrappedException; } } } + public void Reauthenticate(CancellationToken cancellationToken) + { + InvalidateAuthenticators(); + _connectionInitializerContext = _connectionInitializer.Authenticate(this, _connectionInitializerContext, cancellationToken); + } + + public async Task ReauthenticateAsync(CancellationToken cancellationToken) + { + InvalidateAuthenticators(); + _connectionInitializerContext = await _connectionInitializer.AuthenticateAsync(this, _connectionInitializerContext, cancellationToken).ConfigureAwait(false); + } + + private void InvalidateAuthenticators() + { + foreach (var authenticator in _connectionInitializerContext.Authenticators) + { + if (authenticator is MongoOidcAuthenticator oidcAuthenticator) + { + oidcAuthenticator.ClearCredentialsCache(); + } + } + } + private IByteBuffer ReceiveBuffer(CancellationToken cancellationToken) { try diff --git a/src/MongoDB.Driver.Core/Core/Connections/ConnectionDescriptionExtensions.cs b/src/MongoDB.Driver.Core/Core/Connections/ConnectionDescriptionExtensions.cs new file mode 100644 index 00000000000..3115ac4cd6c --- /dev/null +++ b/src/MongoDB.Driver.Core/Core/Connections/ConnectionDescriptionExtensions.cs @@ -0,0 +1,26 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +namespace MongoDB.Driver.Core.Connections +{ + /// + /// Represents internal ConnectionDescription extension methods. + /// + internal static class ConnectionDescriptionExtensions + { + public static bool IsInitialized(this ConnectionDescription description) + => description?.ConnectionId?.LongServerValue.HasValue ?? false; + } +} diff --git a/src/MongoDB.Driver.Core/Core/Connections/ConnectionInitializer.cs b/src/MongoDB.Driver.Core/Core/Connections/ConnectionInitializer.cs index 32e7aff0c5b..cad6a1bcb4a 100644 --- a/src/MongoDB.Driver.Core/Core/Connections/ConnectionInitializer.cs +++ b/src/MongoDB.Driver.Core/Core/Connections/ConnectionInitializer.cs @@ -47,7 +47,7 @@ internal class ConnectionInitializer : IConnectionInitializer _serverApi = serverApi; } - public ConnectionDescription Authenticate(IConnection connection, ConnectionInitializerContext connectionInitializerContext, CancellationToken cancellationToken) + public ConnectionInitializerContext Authenticate(IConnection connection, ConnectionInitializerContext connectionInitializerContext, CancellationToken cancellationToken) { Ensure.IsNotNull(connection, nameof(connection)); Ensure.IsNotNull(connectionInitializerContext, nameof(connectionInitializerContext)); @@ -56,30 +56,34 @@ public ConnectionDescription Authenticate(IConnection connection, ConnectionInit AuthenticationHelper.Authenticate(connection, description, authenticators, cancellationToken); - var connectionIdServerValue = description.HelloResult.ConnectionIdServerValue; - if (connectionIdServerValue.HasValue) + // Connection description should be updated only on the initial handshake and not after reauthentication + if (!description.IsInitialized()) { - description = UpdateConnectionIdWithServerValue(description, connectionIdServerValue.Value); - } - else if (!description.HelloResult.IsMongocryptd) // mongocryptd doesn't provide ConnectionId - { - try + var connectionIdServerValue = description.HelloResult.ConnectionIdServerValue; + if (connectionIdServerValue.HasValue) { - var getLastErrorProtocol = CreateGetLastErrorProtocol(_serverApi); - var getLastErrorResult = getLastErrorProtocol.Execute(connection, cancellationToken); - - description = UpdateConnectionIdWithServerValue(description, getLastErrorResult); + description = UpdateConnectionIdWithServerValue(description, connectionIdServerValue.Value); } - catch + else if (!description.HelloResult.IsMongocryptd) // mongocryptd doesn't provide ConnectionId { - // if we couldn't get the server's connection id, so be it. + try + { + var getLastErrorProtocol = CreateGetLastErrorProtocol(_serverApi); + var getLastErrorResult = getLastErrorProtocol.Execute(connection, cancellationToken); + + description = UpdateConnectionIdWithServerValue(description, getLastErrorResult); + } + catch + { + // if we couldn't get the server's connection id, so be it. + } } } - return description; + return new ConnectionInitializerContext(description, authenticators); } - public async Task AuthenticateAsync(IConnection connection, ConnectionInitializerContext connectionInitializerContext, CancellationToken cancellationToken) + public async Task AuthenticateAsync(IConnection connection, ConnectionInitializerContext connectionInitializerContext, CancellationToken cancellationToken) { Ensure.IsNotNull(connection, nameof(connection)); Ensure.IsNotNull(connectionInitializerContext, nameof(connectionInitializerContext)); @@ -88,36 +92,40 @@ public async Task AuthenticateAsync(IConnection connectio await AuthenticationHelper.AuthenticateAsync(connection, description, authenticators, cancellationToken).ConfigureAwait(false); - var connectionIdServerValue = description.HelloResult.ConnectionIdServerValue; - if (connectionIdServerValue.HasValue) + // Connection description should be updated only on the initial handshake and not while reauthentication + if (!description.IsInitialized()) { - description = UpdateConnectionIdWithServerValue(description, connectionIdServerValue.Value); - } - else if (!description.HelloResult.IsMongocryptd) // mongocryptd doesn't provide ConnectionId - { - try + var connectionIdServerValue = description.HelloResult.ConnectionIdServerValue; + if (connectionIdServerValue.HasValue) { - var getLastErrorProtocol = CreateGetLastErrorProtocol(_serverApi); - var getLastErrorResult = await getLastErrorProtocol - .ExecuteAsync(connection, cancellationToken) - .ConfigureAwait(false); - - description = UpdateConnectionIdWithServerValue(description, getLastErrorResult); + description = UpdateConnectionIdWithServerValue(description, connectionIdServerValue.Value); } - catch + else if (!description.HelloResult.IsMongocryptd) // mongocryptd doesn't provide ConnectionId { - // if we couldn't get the server's connection id, so be it. + try + { + var getLastErrorProtocol = CreateGetLastErrorProtocol(_serverApi); + var getLastErrorResult = await getLastErrorProtocol + .ExecuteAsync(connection, cancellationToken) + .ConfigureAwait(false); + + description = UpdateConnectionIdWithServerValue(description, getLastErrorResult); + } + catch + { + // if we couldn't get the server's connection id, so be it. + } } } - return description; + return new ConnectionInitializerContext(description, authenticators); } public ConnectionInitializerContext SendHello(IConnection connection, CancellationToken cancellationToken) { Ensure.IsNotNull(connection, nameof(connection)); - var authenticators = GetAuthenticators(connection.Settings); - var helloCommand = CreateInitialHelloCommand(authenticators, connection.Settings.LoadBalanced); + var authenticators = CreateAuthenticators(connection); + var helloCommand = CreateInitialHelloCommand(authenticators, connection.Settings.LoadBalanced, cancellationToken); var helloProtocol = HelloHelper.CreateProtocol(helloCommand, _serverApi); var helloResult = HelloHelper.GetResult(connection, helloProtocol, cancellationToken); if (connection.Settings.LoadBalanced && !helloResult.ServiceId.HasValue) @@ -131,8 +139,8 @@ public ConnectionInitializerContext SendHello(IConnection connection, Cancellati public async Task SendHelloAsync(IConnection connection, CancellationToken cancellationToken) { Ensure.IsNotNull(connection, nameof(connection)); - var authenticators = GetAuthenticators(connection.Settings); - var helloCommand = CreateInitialHelloCommand(authenticators, connection.Settings.LoadBalanced); + var authenticators = CreateAuthenticators(connection); + var helloCommand = CreateInitialHelloCommand(authenticators, connection.Settings.LoadBalanced, cancellationToken); var helloProtocol = HelloHelper.CreateProtocol(helloCommand, _serverApi); var helloResult = await HelloHelper.GetResultAsync(connection, helloProtocol, cancellationToken).ConfigureAwait(false); if (connection.Settings.LoadBalanced && !helloResult.ServiceId.HasValue) @@ -157,15 +165,25 @@ private CommandWireProtocol CreateGetLastErrorProtocol(ServerApi s return getLastErrorProtocol; } - private BsonDocument CreateInitialHelloCommand(IReadOnlyList authenticators, bool loadBalanced = false) + private BsonDocument CreateInitialHelloCommand(IReadOnlyList authenticators, bool loadBalanced = false, CancellationToken cancellationToken = default) { var command = HelloHelper.CreateCommand(_serverApi, loadBalanced: loadBalanced); HelloHelper.AddClientDocumentToCommand(command, _clientDocument); HelloHelper.AddCompressorsToCommand(command, _compressors); - return HelloHelper.CustomizeCommand(command, authenticators); + return HelloHelper.CustomizeCommand(command, authenticators, cancellationToken); } - private List GetAuthenticators(ConnectionSettings settings) => settings.AuthenticatorFactories.Select(f => f.Create()).ToList(); + private List CreateAuthenticators(IConnection connection) + { + if (connection.Description.IsInitialized()) + { + // should never be here. + throw new InvalidOperationException(); + } + + var authenticatorFactories = connection.Settings.AuthenticatorFactories; + return authenticatorFactories.Select(c => c.Create()).ToList(); + } private ConnectionDescription UpdateConnectionIdWithServerValue(ConnectionDescription description, BsonDocument getLastErrorResult) { diff --git a/src/MongoDB.Driver.Core/Core/Connections/HelloHelper.cs b/src/MongoDB.Driver.Core/Core/Connections/HelloHelper.cs index 48484f48790..0840fb769fe 100644 --- a/src/MongoDB.Driver.Core/Core/Connections/HelloHelper.cs +++ b/src/MongoDB.Driver.Core/Core/Connections/HelloHelper.cs @@ -61,9 +61,9 @@ internal static BsonDocument CreateCommand(ServerApi serverApi, bool helloOk = f }; } - internal static BsonDocument CustomizeCommand(BsonDocument command, IReadOnlyList authenticators) + internal static BsonDocument CustomizeCommand(BsonDocument command, IReadOnlyList authenticators, CancellationToken cancellationToken) { - return authenticators.Count == 1 ? authenticators[0].CustomizeInitialHelloCommand(command) : command; + return authenticators.Count == 1 ? authenticators[0].CustomizeInitialHelloCommand(command, cancellationToken) : command; } internal static CommandWireProtocol CreateProtocol( diff --git a/src/MongoDB.Driver.Core/Core/Connections/IConnection.cs b/src/MongoDB.Driver.Core/Core/Connections/IConnection.cs index a16d201507c..5eef30a7e1e 100644 --- a/src/MongoDB.Driver.Core/Core/Connections/IConnection.cs +++ b/src/MongoDB.Driver.Core/Core/Connections/IConnection.cs @@ -98,6 +98,19 @@ public interface IConnection : IDisposable /// A Task. Task OpenAsync(CancellationToken cancellationToken); + /// + /// Reauthenticate the connection. + /// + /// The cancellation token. + void Reauthenticate(CancellationToken cancellationToken); + + /// + /// Reauthenticate the connection. + /// + /// The cancellation token. + /// A task. + Task ReauthenticateAsync(CancellationToken cancellationToken); + /// /// Receives a message. /// diff --git a/src/MongoDB.Driver.Core/Core/Connections/IConnectionInitializer.cs b/src/MongoDB.Driver.Core/Core/Connections/IConnectionInitializer.cs index e8f64eb00db..5aa20d4b1e7 100644 --- a/src/MongoDB.Driver.Core/Core/Connections/IConnectionInitializer.cs +++ b/src/MongoDB.Driver.Core/Core/Connections/IConnectionInitializer.cs @@ -35,8 +35,8 @@ public ConnectionInitializerContext(ConnectionDescription description, IReadOnly internal interface IConnectionInitializer { - ConnectionDescription Authenticate(IConnection connection, ConnectionInitializerContext connectionInitializerContext, CancellationToken cancellationToken); - Task AuthenticateAsync(IConnection connection, ConnectionInitializerContext connectionInitializerContext, CancellationToken cancellationToken); + ConnectionInitializerContext Authenticate(IConnection connection, ConnectionInitializerContext connectionInitializerContext, CancellationToken cancellationToken); + Task AuthenticateAsync(IConnection connection, ConnectionInitializerContext connectionInitializerContext, CancellationToken cancellationToken); ConnectionInitializerContext SendHello(IConnection connection, CancellationToken cancellationToken); Task SendHelloAsync(IConnection connection, CancellationToken cancellationToken); } diff --git a/src/MongoDB.Driver.Core/Core/Operations/RetryabilityHelper.cs b/src/MongoDB.Driver.Core/Core/Operations/RetryabilityHelper.cs index 8e19e3465d4..242b2c513a7 100644 --- a/src/MongoDB.Driver.Core/Core/Operations/RetryabilityHelper.cs +++ b/src/MongoDB.Driver.Core/Core/Operations/RetryabilityHelper.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using MongoDB.Bson; +using MongoDB.Driver.Core.Authentication; using MongoDB.Driver.Core.Connections; using MongoDB.Driver.Core.Misc; using MongoDB.Driver.Core.Servers; @@ -35,6 +36,7 @@ internal static class RetryabilityHelper private static readonly HashSet __retryableWriteExceptions; private static readonly HashSet __retryableReadErrorCodes; private static readonly HashSet __retryableWriteErrorCodes; + private static readonly HashSet __saslCommands; // static constructor static RetryabilityHelper() @@ -89,6 +91,12 @@ static RetryabilityHelper() ServerErrorCode.RetryChangeStream, ServerErrorCode.FailedToSatisfyReadPreference }; + + __saslCommands = new HashSet + { + SaslAuthenticator.SaslStartCommand, + SaslAuthenticator.SaslContinueCommand + }; } // public static methods @@ -143,6 +151,20 @@ public static bool IsResumableChangeStreamException(Exception exception, int max } } + /// + /// Value indicating whether the exception requests additional authentication attempt. + /// + /// The command exception. + /// The command. + /// The flag. + /// + /// This logic is completely separate from a standard retry mechanism and related only to authentication. + /// + public static bool IsReauthenticationRequested(MongoCommandException mongoCommandException, BsonDocument command) + => mongoCommandException.Code == (int)ServerErrorCode.ReauthenticationRequired && + // SASL commands should not be reauthenticated on sending level + !__saslCommands.Overlaps(command.Names); + public static bool IsRetryableReadException(Exception exception) { if (__retryableReadExceptions.Contains(exception.GetType()) || IsNetworkException(exception)) diff --git a/src/MongoDB.Driver.Core/Core/WireProtocol/CommandUsingCommandMessageWireProtocol.cs b/src/MongoDB.Driver.Core/Core/WireProtocol/CommandUsingCommandMessageWireProtocol.cs index eb02dc57126..f8d0fbea021 100644 --- a/src/MongoDB.Driver.Core/Core/WireProtocol/CommandUsingCommandMessageWireProtocol.cs +++ b/src/MongoDB.Driver.Core/Core/WireProtocol/CommandUsingCommandMessageWireProtocol.cs @@ -104,45 +104,28 @@ public TCommandResult Execute(IConnection connection, CancellationToken cancella { try { - bool responseExpected; int responseTo; + CommandRequestMessage message = null; + if (_moreToCome) { - responseExpected = true; responseTo = _previousRequestId; } else { - var message = CreateCommandMessage(connection.Description); + message = CreateCommandMessage(connection.Description); message = AutoEncryptFieldsIfNecessary(message, connection, cancellationToken); - - try - { - connection.SendMessage(message, _messageEncoderSettings, cancellationToken); - } - finally - { - if (message.WasSent) - { - MessageWasProbablySent(message); - } - } - - responseExpected = message.WrappedMessage.ResponseExpected; - responseTo = message.RequestId; + responseTo = message.WrappedMessage.RequestId; } - if (responseExpected) + try { - var encoderSelector = new CommandResponseMessageEncoderSelector(); - var response = (CommandResponseMessage)connection.ReceiveMessage(responseTo, encoderSelector, _messageEncoderSettings, cancellationToken); - SaveResponseInfo(response); - response = AutoDecryptFieldsIfNecessary(response, cancellationToken); - return ProcessResponse(connection.ConnectionId, response.WrappedMessage); + return SendMessageAndProcessResponse(message, responseTo, connection, cancellationToken); } - else + catch (MongoCommandException commandException) when (RetryabilityHelper.IsReauthenticationRequested(commandException, _command)) { - return default(TCommandResult); + connection.Reauthenticate(cancellationToken); + return SendMessageAndProcessResponse(message, responseTo, connection, cancellationToken); } } catch (Exception exception) @@ -158,45 +141,28 @@ public async Task ExecuteAsync(IConnection connection, Cancellat { try { - bool responseExpected; int responseTo; + CommandRequestMessage message = null; + if (_moreToCome) { - responseExpected = true; responseTo = _previousRequestId; } else { - var message = CreateCommandMessage(connection.Description); + message = CreateCommandMessage(connection.Description); message = await AutoEncryptFieldsIfNecessaryAsync(message, connection, cancellationToken).ConfigureAwait(false); - - try - { - await connection.SendMessageAsync(message, _messageEncoderSettings, cancellationToken).ConfigureAwait(false); - } - finally - { - if (message.WasSent) - { - MessageWasProbablySent(message); - } - } - - responseExpected = message.WrappedMessage.ResponseExpected; - responseTo = message.RequestId; + responseTo = message.WrappedMessage.RequestId; } - if (responseExpected) + try { - var encoderSelector = new CommandResponseMessageEncoderSelector(); - var response = (CommandResponseMessage)await connection.ReceiveMessageAsync(responseTo, encoderSelector, _messageEncoderSettings, cancellationToken).ConfigureAwait(false); - SaveResponseInfo(response); - response = await AutoDecryptFieldsIfNecessaryAsync(response, cancellationToken).ConfigureAwait(false); - return ProcessResponse(connection.ConnectionId, response.WrappedMessage); + return await SendMessageAndProcessResponseAsync(message, responseTo, connection, cancellationToken).ConfigureAwait(false); } - else + catch (MongoCommandException commandException) when (RetryabilityHelper.IsReauthenticationRequested(commandException, _command)) { - return default(TCommandResult); + await connection.ReauthenticateAsync(cancellationToken).ConfigureAwait(false); + return await SendMessageAndProcessResponseAsync(message, responseTo, connection, cancellationToken).ConfigureAwait(false); } } catch (Exception exception) @@ -574,6 +540,75 @@ private void SaveResponseInfo(CommandResponseMessage response) _moreToCome = response.WrappedMessage.MoreToCome; } + private TCommandResult SendMessageAndProcessResponse(CommandRequestMessage message, int responseTo, IConnection connection, CancellationToken cancellationToken) + { + var responseExpected = true; + if (message != null) + { + try + { + connection.SendMessage(message, _messageEncoderSettings, cancellationToken); + } + finally + { + if (message.WasSent) + { + MessageWasProbablySent(message); + } + } + + responseExpected = message.WrappedMessage.ResponseExpected; // mutable, read after sending + } + + if (responseExpected) + { + var encoderSelector = new CommandResponseMessageEncoderSelector(); + var response = (CommandResponseMessage)connection.ReceiveMessage(responseTo, encoderSelector, _messageEncoderSettings, cancellationToken); + response = AutoDecryptFieldsIfNecessary(response, cancellationToken); + var result = ProcessResponse(connection.ConnectionId, response.WrappedMessage); + SaveResponseInfo(response); + return result; + } + else + { + return default; + } + } + + private async Task SendMessageAndProcessResponseAsync(CommandRequestMessage message, int responseTo, IConnection connection, CancellationToken cancellationToken) + { + var responseExpected = true; + if (message != null) + { + try + { + await connection.SendMessageAsync(message, _messageEncoderSettings, cancellationToken).ConfigureAwait(false); + } + finally + { + if (message.WasSent) + { + MessageWasProbablySent(message); + } + } + responseExpected = message.WrappedMessage.ResponseExpected; // mutable, read after sending + } + + if (responseExpected) + { + var encoderSelector = new CommandResponseMessageEncoderSelector(); + var response = (CommandResponseMessage)await connection.ReceiveMessageAsync(responseTo, encoderSelector, _messageEncoderSettings, cancellationToken).ConfigureAwait(false); + response = await AutoDecryptFieldsIfNecessaryAsync(response, cancellationToken).ConfigureAwait(false); + var result = ProcessResponse(connection.ConnectionId, response.WrappedMessage); + SaveResponseInfo(response); + return result; + } + else + { + return default; + } + } + private bool ShouldAddTransientTransactionError(MongoException exception) { if (_session.IsInTransaction) diff --git a/src/MongoDB.Driver.Core/ServerErrorCode.cs b/src/MongoDB.Driver.Core/ServerErrorCode.cs index 99e32958b31..36be4cf388f 100644 --- a/src/MongoDB.Driver.Core/ServerErrorCode.cs +++ b/src/MongoDB.Driver.Core/ServerErrorCode.cs @@ -19,6 +19,7 @@ internal enum ServerErrorCode { // this is not a complete list, more will be added as needed // see: https://github.com/mongodb/mongo/blob/master/src/mongo/base/error_codes.yml + AuthenticationFailed = 18, CappedPositionLost = 136, CommandNotFound = 59, CursorKilled = 237, @@ -41,6 +42,7 @@ internal enum ServerErrorCode NotPrimaryOrSecondary = 13436, PrimarySteppedDown = 189, ReadConcernMajorityNotAvailableYet = 134, + ReauthenticationRequired = 391, RetryChangeStream = 234, ShutdownInProgress = 91, SocketException = 9001, diff --git a/src/MongoDB.Driver/ClusterRegistry.cs b/src/MongoDB.Driver/ClusterRegistry.cs index 077ce1c81e7..166d5a30a22 100644 --- a/src/MongoDB.Driver/ClusterRegistry.cs +++ b/src/MongoDB.Driver/ClusterRegistry.cs @@ -15,6 +15,7 @@ using System.Collections.Generic; using System.Linq; +using System.Net; using System.Net.Security; using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; @@ -117,7 +118,8 @@ private ConnectionPoolSettings ConfigureConnectionPool(ConnectionPoolSettings se private ConnectionSettings ConfigureConnection(ConnectionSettings settings, ClusterKey clusterKey) { - var authenticatorFactories = clusterKey.Credentials.Select(c => new AuthenticatorFactory(() => c.ToAuthenticator(clusterKey.ServerApi))); + var endPoints = clusterKey.Servers.Select(s => new DnsEndPoint(s.Host, s.Port)).ToArray(); + var authenticatorFactories = clusterKey.Credentials.Select(c => new AuthenticatorFactory(() => c.ToAuthenticator(endPoints, clusterKey.ServerApi))); return settings.With( authenticatorFactories: Optional.Enumerable(authenticatorFactories), compressors: Optional.Enumerable(clusterKey.Compressors), diff --git a/src/MongoDB.Driver/MongoCredential.cs b/src/MongoDB.Driver/MongoCredential.cs index d5d1b83c91d..6650c352eee 100644 --- a/src/MongoDB.Driver/MongoCredential.cs +++ b/src/MongoDB.Driver/MongoCredential.cs @@ -16,8 +16,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Security; using MongoDB.Driver.Core.Authentication; +using MongoDB.Driver.Core.Authentication.Oidc; using MongoDB.Driver.Core.Misc; using MongoDB.Shared; @@ -262,6 +264,26 @@ public static MongoCredential CreateMongoCRCredential(string databaseName, strin new PasswordEvidence(password)); } + /// + /// Creates a credential used with MONGODB-OIDC. + /// + /// The OIDC callback. + /// The principal name. + /// The OIDC credential. + public static MongoCredential CreateOidcCredential(IOidcCallback callback, string principalName = null) + => CreateRawOidcCredential(principalName) + .WithMechanismProperty(OidcConfiguration.CallbackMechanismPropertyName, callback); + + /// + /// Creates a credential used with MONGODB-OIDC. + /// + /// The built in environemnt. + /// User name. + /// The OIDC credential. + public static MongoCredential CreateOidcCredential(string environment, string userName = null) + => CreateRawOidcCredential(userName) + .WithMechanismProperty(OidcConfiguration.EnvironmentMechanismPropertyName, environment); + /// /// Creates a credential used with MONGODB-X509. /// @@ -396,7 +418,7 @@ public MongoCredential WithMechanismProperty(string key, object value) } // internal methods - internal IAuthenticator ToAuthenticator(ServerApi serverApi) + internal IAuthenticator ToAuthenticator(IReadOnlyList endPoints, ServerApi serverApi, IEnvironmentVariableProvider environmentVariableProvider = null) { var passwordEvidence = _evidence as PasswordEvidence; if (passwordEvidence != null) @@ -443,6 +465,10 @@ internal IAuthenticator ToAuthenticator(ServerApi serverApi) _mechanismProperties.Select(x => new KeyValuePair(x.Key, x.Value.ToString())), serverApi); } + if (_mechanism == MongoOidcAuthenticator.MechanismName) + { + throw new NotSupportedException("OIDC authenticator cannot be constructed with password."); + } } else if (_identity.Source == "$external" && _evidence is ExternalEvidence) { @@ -464,12 +490,41 @@ internal IAuthenticator ToAuthenticator(ServerApi serverApi) _mechanismProperties.Select(x => new KeyValuePair(x.Key, x.Value.ToString())), serverApi); } + if (_mechanism == MongoOidcAuthenticator.MechanismName) + { + IOidcCallbackAdapterFactory callbackFactory; + if (environmentVariableProvider == null || + environmentVariableProvider == EnvironmentVariableProvider.Instance) + { + callbackFactory = OidcCallbackAdapterCachingFactory.Instance; + } + else + { + callbackFactory = new OidcCallbackAdapterCachingFactory(SystemClock.Instance, environmentVariableProvider); + } + + return MongoOidcAuthenticator.CreateAuthenticator( + _identity.Source, + _identity.Username, + _mechanismProperties, + endPoints, + serverApi, + callbackFactory); + } } throw new NotSupportedException("Unable to create an authenticator."); } // internal static methods + internal static MongoCredential CreateRawOidcCredential(string userName) + => FromComponents( + mechanism: "MONGODB-OIDC", + source: "$external", + databaseName: null, + username: userName, + evidence: new ExternalEvidence()); + internal static MongoCredential FromComponents(string mechanism, string source, string username, string password) { return FromComponents(mechanism, source, databaseName: null, username, password); @@ -481,19 +536,6 @@ internal static MongoCredential FromComponents(string mechanism, string source, return FromComponents(mechanism, source, databaseName, username, evidence); } - // private methods - private void ValidatePassword(string password) - { - if (password == null) - { - throw new ArgumentNullException("password"); - } - if (password.Any(c => (int)c >= 128)) - { - throw new ArgumentException("Password must contain only ASCII characters."); - } - } - // private static methods private static void EnsureNullOrExternalSource(string mechanism, string source) { @@ -548,6 +590,22 @@ private static MongoCredential FromComponents(string mechanism, string source, s mechanism, new MongoExternalIdentity(username), evidence); + case "MONGODB-OIDC": + { + // MUST be "$external". Defaults to $external. + EnsureNullOrExternalSource(mechanism, source); + + if (evidence is not ExternalEvidence) + { + throw new ArgumentException("MONGODB-OIDC does not support a password."); + } + + // Callback will be set in the following step via WithMechanismProperty calls + return new MongoCredential( + mechanism, + new MongoOidcIdentity(username), + evidence); + } case "MONGODB-X509": // MUST be "$external". Defaults to $external. EnsureNullOrExternalSource(mechanism, source); diff --git a/src/MongoDB.Driver/MongoOidcIdentity.cs b/src/MongoDB.Driver/MongoOidcIdentity.cs new file mode 100644 index 00000000000..4904683d2c7 --- /dev/null +++ b/src/MongoDB.Driver/MongoOidcIdentity.cs @@ -0,0 +1,31 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace MongoDB.Driver +{ + /// + /// Represents an OIDC identity. + /// + public sealed class MongoOidcIdentity : MongoIdentity + { + /// + /// Initializes a new instance of the class. + /// + /// The username. + public MongoOidcIdentity(string username = null) + : base("$external", username, allowNullUsername: true) + { } + } +} diff --git a/tests/MongoDB.Driver.Core.TestHelpers/FailPoint.cs b/tests/MongoDB.Driver.Core.TestHelpers/FailPoint.cs index cffbd23efb0..319c138fcdf 100644 --- a/tests/MongoDB.Driver.Core.TestHelpers/FailPoint.cs +++ b/tests/MongoDB.Driver.Core.TestHelpers/FailPoint.cs @@ -30,6 +30,7 @@ namespace MongoDB.Driver.Core.TestHelpers public static class FailPointName { // public constants + public const string FailCommand = "failCommand"; public const string MaxTimeAlwaysTimeout = "maxTimeAlwaysTimeOut"; public const string OnPrimaryTransactionalWrite = "onPrimaryTransactionalWrite"; } diff --git a/tests/MongoDB.Driver.Core.TestHelpers/MockConnection.cs b/tests/MongoDB.Driver.Core.TestHelpers/MockConnection.cs index b4d70359e4f..e28f8974403 100644 --- a/tests/MongoDB.Driver.Core.TestHelpers/MockConnection.cs +++ b/tests/MongoDB.Driver.Core.TestHelpers/MockConnection.cs @@ -29,7 +29,7 @@ namespace MongoDB.Driver.Core.TestHelpers { - public class MockConnection : IConnection + public class MockConnection : IConnectionHandle { // fields private ConnectionId _connectionId; @@ -175,7 +175,7 @@ public void EnqueueReplyMessage(ReplyMessage replyMessage) _replyActions.Enqueue(new ActionQueueItem(replyMessage)); } - public IConnection Fork() + public IConnectionHandle Fork() { return this; } @@ -208,25 +208,29 @@ public Task OpenAsync(CancellationToken cancellationToken) _openedEventHandler?.Invoke(new ConnectionOpenedEvent(_connectionId, _connectionSettings, TimeSpan.Zero, null)); - return Task.FromResult(null); + return Task.CompletedTask; + } + + public void Reauthenticate(CancellationToken cancellationToken) + { + _replyActions.Dequeue().GetEffectiveMessage(); + } + + public async Task ReauthenticateAsync(CancellationToken cancellationToken) + { + await _replyActions.Dequeue().GetEffectiveMessageAsync().ConfigureAwait(false); } public ResponseMessage ReceiveMessage(int responseTo, IMessageEncoderSelector encoderSelector, MessageEncoderSettings messageEncoderSettings, CancellationToken cancellationToken) { var action = _replyActions.Dequeue(); - action.ThrowIfException(); - var message = (ResponseMessage)action.Message; - action.DelayIfRequired(); - return message; + return (ResponseMessage)action.GetEffectiveMessage(); } public async Task ReceiveMessageAsync(int responseTo, IMessageEncoderSelector encoderSelector, MessageEncoderSettings messageEncoderSettings, CancellationToken cancellationToken) { var action = _replyActions.Dequeue(); - action.ThrowIfException(); - var message = (ResponseMessage)action.Message; - await action.DelayIfRequiredAsync().ConfigureAwait(false); - return message; + return (ResponseMessage)await action.GetEffectiveMessageAsync().ConfigureAwait(false); } public void SendMessages(IEnumerable messages, MessageEncoderSettings messageEncoderSettings, CancellationToken cancellationToken) @@ -264,7 +268,21 @@ public ActionQueueItem(MongoDBMessage message, Exception exception = null, TimeS public Exception Exception { get; } public TimeSpan? Delay { get; } - public void ThrowIfException() + public MongoDBMessage GetEffectiveMessage() + { + ThrowIfException(); + DelayIfRequired(); + return Message; + } + + public async Task GetEffectiveMessageAsync() + { + ThrowIfException(); + await DelayIfRequiredAsync().ConfigureAwait(false); + return Message; + } + + private void ThrowIfException() { if (Exception != null) { @@ -272,7 +290,7 @@ public void ThrowIfException() } } - public void DelayIfRequired() + private void DelayIfRequired() { if (Delay.HasValue) { @@ -280,7 +298,7 @@ public void DelayIfRequired() } } - public async Task DelayIfRequiredAsync() + private async Task DelayIfRequiredAsync() { if (Delay.HasValue) { diff --git a/tests/MongoDB.Driver.Core.TestHelpers/XunitExtensions/RequireServer.cs b/tests/MongoDB.Driver.Core.TestHelpers/XunitExtensions/RequireServer.cs index b90c4c25f18..9ea6c5de4e1 100644 --- a/tests/MongoDB.Driver.Core.TestHelpers/XunitExtensions/RequireServer.cs +++ b/tests/MongoDB.Driver.Core.TestHelpers/XunitExtensions/RequireServer.cs @@ -313,6 +313,10 @@ private bool IsRequirementSatisfied(BsonElement requirement) var maxServerVersion = SemanticVersion.Parse(requirement.Value.AsString); return SemanticVersionCompareToAsReleased(actualVersion, maxServerVersion) <= 0; } + case "authMechanism": + var actualValue = CoreTestConfiguration.GetServerParameters().GetValue("authenticationMechanisms").AsBsonArray; + var requiredValue = requirement.Value.AsString; + return actualValue.Contains(requiredValue); case "serverless": var serverlessValue = requirement.Value.AsString; switch (serverlessValue) diff --git a/tests/MongoDB.Driver.Core.Tests/Core/Authentication/AuthenticationHelperTests.cs b/tests/MongoDB.Driver.Core.Tests/Core/Authentication/AuthenticationHelperTests.cs index 1d5d4ff401e..98dfe268294 100644 --- a/tests/MongoDB.Driver.Core.Tests/Core/Authentication/AuthenticationHelperTests.cs +++ b/tests/MongoDB.Driver.Core.Tests/Core/Authentication/AuthenticationHelperTests.cs @@ -57,7 +57,6 @@ public void MongoPasswordDigest_should_create_the_correct_hash(string username, var description = new ConnectionDescription( new ConnectionId(new ServerId(new ClusterId(), new DnsEndPoint("localhost", 27017))), new HelloResult(new BsonDocument("ok", 1).Add(new BsonElement("maxWireVersion", WireVersion.Server36)))); - var serverApi = new ServerApi(ServerApiVersion.V1, true, true); var mockAuthenticator = new Mock(); var settings = new ConnectionSettings(authenticatorFactories: new[] { new AuthenticatorFactory(() => mockAuthenticator.Object) }); @@ -90,7 +89,6 @@ public void MongoPasswordDigest_should_create_the_correct_hash(string username, var description = new ConnectionDescription( new ConnectionId(new ServerId(new ClusterId(), new DnsEndPoint("localhost", 27017))), new HelloResult(new BsonDocument("ok", 1).Add("setName", "rs").Add("arbiterOnly", true).Add("maxWireVersion", WireVersion.Server36))); - var serverApi = new ServerApi(ServerApiVersion.V1, true, true); var mockAuthenticator = new Mock(); var settings = new ConnectionSettings(authenticatorFactories: new[] { new AuthenticatorFactory(() => mockAuthenticator.Object) }); diff --git a/tests/MongoDB.Driver.Core.Tests/Core/Authentication/Oidc/OidcConfigurationTests.cs b/tests/MongoDB.Driver.Core.Tests/Core/Authentication/Oidc/OidcConfigurationTests.cs new file mode 100644 index 00000000000..a9a3f5e7f53 --- /dev/null +++ b/tests/MongoDB.Driver.Core.Tests/Core/Authentication/Oidc/OidcConfigurationTests.cs @@ -0,0 +1,220 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Net; +using FluentAssertions; +using MongoDB.Driver.Core.Authentication.Oidc; +using Moq; +using Xunit; + +namespace MongoDB.Driver.Core.Tests.Core.Authentication.Oidc +{ + public class OidcConfigurationTests + { + private static readonly IOidcCallback __callbackMock = new Mock().Object; + + [Theory] + [MemberData(nameof(ValidConfigurationTestCases))] + public void Constructor_should_accept_valid_arguments( + IReadOnlyList endPoints, + string principalName, + IReadOnlyDictionary mechanismProperties, + string expectedEnvironment, + IOidcCallback expectedCallback) + { + var configuration = new OidcConfiguration(endPoints, principalName, mechanismProperties); + + configuration.PrincipalName.Should().Be(principalName); + configuration.EndPoints.Should().BeEquivalentTo(endPoints); + configuration.Environment.Should().Be(expectedEnvironment); + configuration.Callback.Should().Be(expectedCallback); + } + + public static IEnumerable ValidConfigurationTestCases = new[] + { + new object[] { new[] { new IPEndPoint(IPAddress.Parse("127.0.0.1"), 27017) }, null, new Dictionary { ["ENVIRONMENT"] = "test" }, "test", null }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, null, new Dictionary { ["ENVIRONMENT"] = "test" }, "test", null }, + new object[] { new[] { new DnsEndPoint("localhost", 27017), new DnsEndPoint("localhost", 27018) }, null, new Dictionary { ["ENVIRONMENT"] = "test" }, "test", null }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, null, new Dictionary { ["ENVIRONMENT"] = "test" }, "test", null }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "test" }, "test", null }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "azure", ["TOKEN_RESOURCE"] = "tr" }, "azure", null }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock }, null, __callbackMock }, + }; + + [Theory] + [MemberData(nameof(InvalidConfigurationTestCases))] + public void Constructor_throws_on_invalid_arguments( + IReadOnlyList endPoints, + string principalName, + IReadOnlyDictionary mechanismProperties, + string paramName) + { + var exception = Record.Exception(() => + new OidcConfiguration(endPoints, principalName, mechanismProperties)); + + exception.Should().BeAssignableTo() + .Subject.ParamName.Should().Be(paramName); + } + + public static IEnumerable InvalidConfigurationTestCases = new[] + { + new object[] { null, null, new Dictionary { ["ENVIRONMENT"] = "test" }, "endPoints" }, + new object[] { Array.Empty(), null, new Dictionary { ["ENVIRONMENT"] = "test" }, "endPoints" }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, null, null, "authMechanismProperties" }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", null, "authMechanismProperties" }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, null, new Dictionary(), null }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary(), null }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["unknown_property"] = 42 }, "unknown_property" }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = null }, "ENVIRONMENT" }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "" }, "ENVIRONMENT" }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = 1 }, "ENVIRONMENT" }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "unknown provider" }, "ENVIRONMENT" }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = null }, "OIDC_CALLBACK" }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = "invalid type" }, "OIDC_CALLBACK" }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock, ["ENVIRONMENT"] = "test" }, null }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "test", ["TOKEN_RESOURCE"] = "tr" }, "TOKEN_RESOURCE" }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "azure" }, "TOKEN_RESOURCE" }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "azure", ["TOKEN_RESOURCE"] = null }, "TOKEN_RESOURCE" }, + new object[] { new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "azure", ["TOKEN_RESOURCE"] = "" }, "TOKEN_RESOURCE" }, + }; + + [Theory] + [MemberData(nameof(ComparisonTestCases))] + public void Equals_should_compare_by_values( + bool expectedResult, + IReadOnlyList endPoints1, + string principalName1, + IReadOnlyDictionary mechanismProperties1, + IReadOnlyList endPoints2, + string principalName2, + IReadOnlyDictionary mechanismProperties2) + { + var configuration1 = new OidcConfiguration(endPoints1, principalName1, mechanismProperties1); + var configuration2 = new OidcConfiguration(endPoints2, principalName2, mechanismProperties2); + + var result = configuration1.Equals(configuration2); + var hashCode1 = configuration1.GetHashCode(); + var hashCode2 = configuration1.GetHashCode(); + + result.Should().Be(expectedResult); + if (expectedResult) + { + hashCode1.Should().Be(hashCode2); + } + } + + public static IEnumerable ComparisonTestCases = new[] + { + new object[] + { + true, + new[] { new IPEndPoint(IPAddress.Parse("127.0.0.1"), 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock }, + new[] { new IPEndPoint(IPAddress.Parse("127.0.0.1"), 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock } + }, + new object[] + { + true, + new[] { new DnsEndPoint("localhost", 27017) }, null, new Dictionary { ["ENVIRONMENT"] = "test" }, + new[] { new DnsEndPoint("localhost", 27017) }, null, new Dictionary { ["ENVIRONMENT"] = "test" } + }, + new object[] + { + true, + new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "test" }, + new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "test" } + }, + new object[] + { + true, + new[] { new DnsEndPoint("localhost", 27017), new DnsEndPoint("localhost", 27018) }, "name", new Dictionary { ["ENVIRONMENT"] = "test" }, + new[] { new DnsEndPoint("localhost", 27017), new DnsEndPoint("localhost", 27018)}, "name", new Dictionary { ["ENVIRONMENT"] = "test" } + }, + new object[] + { + true, + new[] { new DnsEndPoint("localhost", 27017), new DnsEndPoint("other-host", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "test" }, + new[] { new DnsEndPoint("localhost", 27017), new DnsEndPoint("other-host", 27017)}, "name", new Dictionary { ["ENVIRONMENT"] = "test" } + }, + new object[] + { + true, + new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock }, + new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock } + }, + new object[] + { + true, + new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "azure", ["TOKEN_RESOURCE"] = "tr" }, + new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "azure", ["TOKEN_RESOURCE"] = "tr" } + }, + new object[] + { + false, + new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "azure", ["TOKEN_RESOURCE"] = "tr1" }, + new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "azure", ["TOKEN_RESOURCE"] = "tr2" } + }, + new object[] + { + false, + new[] { new DnsEndPoint("otherhost", 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock }, + new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock } + }, + new object[] + { + false, + new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock }, + new[] { new DnsEndPoint("localhost", 27018) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock } + }, + new object[] + { + false, + new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock }, + new[] { new DnsEndPoint("localhost", 27017), new DnsEndPoint("otherhost", 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock } + }, + new object[] + { + false, + new[] { new DnsEndPoint("localhost", 27017), new DnsEndPoint("otherhost", 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock }, + new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock } + }, + new object[] + { + false, + new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock }, + new[] { new IPEndPoint(IPAddress.Parse("127.0.0.1"), 27017) }, "name", new Dictionary { ["OIDC_CALLBACK"] = __callbackMock } + }, + new object[] + { + false, + new[] { new DnsEndPoint("localhost", 27017) }, null, new Dictionary { ["ENVIRONMENT"] = "test" }, + new[] { new DnsEndPoint("localhost", 27017) }, "name", new Dictionary { ["ENVIRONMENT"] = "test" } + }, + new object[] + { + false, + new[] { new DnsEndPoint("localhost", 27017) }, null, new Dictionary { ["ENVIRONMENT"] = "test" }, + new[] { new DnsEndPoint("localhost", 27017) }, null, new Dictionary { ["OIDC_CALLBACK"] = __callbackMock } + }, + new object[] + { + false, + new[] { new DnsEndPoint("localhost", 27017) }, null, new Dictionary { ["OIDC_CALLBACK"] = __callbackMock }, + new[] { new DnsEndPoint("localhost", 27017) }, null, new Dictionary { ["OIDC_CALLBACK"] = new Mock().Object } + }, + }; + } +} diff --git a/tests/MongoDB.Driver.Core.Tests/Core/Authentication/ScramSha1AuthenticatorTests.cs b/tests/MongoDB.Driver.Core.Tests/Core/Authentication/ScramSha1AuthenticatorTests.cs index c123aa8e79a..752dc7d5dc3 100644 --- a/tests/MongoDB.Driver.Core.Tests/Core/Authentication/ScramSha1AuthenticatorTests.cs +++ b/tests/MongoDB.Driver.Core.Tests/Core/Authentication/ScramSha1AuthenticatorTests.cs @@ -253,7 +253,7 @@ public void Constructor_should_throw_an_ArgumentNullException_when_credential_is { // Call CustomizeInitialHelloCommand so that the authenticator thinks its started to speculatively // authenticate - helloCommand = subject.CustomizeInitialHelloCommand(new BsonDocument { { OppressiveLanguageConstants.LegacyHelloCommandName, 1 } }); + helloCommand = subject.CustomizeInitialHelloCommand(new BsonDocument { { OppressiveLanguageConstants.LegacyHelloCommandName, 1 } }, default); } else { diff --git a/tests/MongoDB.Driver.Core.Tests/Core/Authentication/ScramSha256AuthenticatorTests.cs b/tests/MongoDB.Driver.Core.Tests/Core/Authentication/ScramSha256AuthenticatorTests.cs index 25997e7c929..9add1267689 100644 --- a/tests/MongoDB.Driver.Core.Tests/Core/Authentication/ScramSha256AuthenticatorTests.cs +++ b/tests/MongoDB.Driver.Core.Tests/Core/Authentication/ScramSha256AuthenticatorTests.cs @@ -338,7 +338,7 @@ public void Constructor_should_throw_an_ArgumentNullException_when_credential_is { // We must call CustomizeInitialHelloCommand so that the authenticator thinks its started to speculatively // authenticate - helloCommand = subject.CustomizeInitialHelloCommand(new BsonDocument { { OppressiveLanguageConstants.LegacyHelloCommandName, 1 } }); + helloCommand = subject.CustomizeInitialHelloCommand(new BsonDocument { { OppressiveLanguageConstants.LegacyHelloCommandName, 1 } }, default); } else { diff --git a/tests/MongoDB.Driver.Core.Tests/Core/Configuration/ConnectionStringTests.cs b/tests/MongoDB.Driver.Core.Tests/Core/Configuration/ConnectionStringTests.cs index f1e18c7d47c..4cefceb403f 100644 --- a/tests/MongoDB.Driver.Core.Tests/Core/Configuration/ConnectionStringTests.cs +++ b/tests/MongoDB.Driver.Core.Tests/Core/Configuration/ConnectionStringTests.cs @@ -130,6 +130,35 @@ public void Resolve_against_mongodb_aws_session_token_should_return_the_expected result.AuthMechanismProperties["AWS_SESSION_TOKEN"].Should().Be(awsSessionToken); } + [Theory] + [InlineData("mongodb://localhost/?authMechanism=MONGODB-OIDC", null)] + [InlineData("mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws", "PROVIDER_NAME:aws")] + public void Resolve_against_mongodb_oidc_configuration_should_return_the_expected_output(string connectionStringStr, string expectedAuthProperties) + { + const string authMechanism = "MONGODB-OIDC"; + + var connectionString = new ConnectionString(connectionStringStr); + + var result = connectionString.Resolve(); + + result.AuthMechanism.Should().Be(authMechanism); + var actualAuthProperties = result.AuthMechanismProperties.ToList(); + + var list = expectedAuthProperties?.Split(';').ToList(); + if (list != null) + { + for (int i = 0; i < list.Count; i++) + { + var expectedKeyValue = list[i].Split(':'); + var actualAuthProperty = actualAuthProperties[i]; + actualAuthProperty.Key.Should().Be(expectedKeyValue[0]); + actualAuthProperty.Value.Should().Be(expectedKeyValue[1]); + } + } + + actualAuthProperties.Count.Should().Be(list?.Count ?? 0); + } + [Theory] [InlineData("mongodb://test5.test.build.10gen.cc", false, "mongodb://test5.test.build.10gen.cc", false)] [InlineData("mongodb://test5.test.build.10gen.cc", false, "mongodb://test5.test.build.10gen.cc", true)] diff --git a/tests/MongoDB.Driver.Core.Tests/Core/Connections/BinaryConnectionTests.cs b/tests/MongoDB.Driver.Core.Tests/Core/Connections/BinaryConnectionTests.cs index 16d2bd0250f..e7043dae080 100644 --- a/tests/MongoDB.Driver.Core.Tests/Core/Connections/BinaryConnectionTests.cs +++ b/tests/MongoDB.Driver.Core.Tests/Core/Connections/BinaryConnectionTests.cs @@ -23,6 +23,7 @@ using FluentAssertions; using MongoDB.Bson; using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Bson.TestHelpers; using MongoDB.Driver.Core.Authentication; using MongoDB.Driver.Core.Clusters; using MongoDB.Driver.Core.Configuration; @@ -43,6 +44,7 @@ namespace MongoDB.Driver.Core.Connections public class BinaryConnectionTests : LoggableTestClass { private ConnectionInitializerContext _connectionInitializerContext; + private ConnectionInitializerContext _connectionInitializerContextAfterAuthentication; private Mock _mockConnectionInitializer; private ConnectionDescription _connectionDescription; private readonly IReadOnlyList __emptyAuthenticators = new IAuthenticator[0]; @@ -65,6 +67,7 @@ public BinaryConnectionTests(Xunit.Abstractions.ITestOutputHelper output) : base var helloResult = new HelloResult(new BsonDocument { { "ok", 1 }, { "maxMessageSizeBytes", 48000000 }, { "maxWireVersion", WireVersion.Server36 } }); _connectionDescription = new ConnectionDescription(connectionId, helloResult); _connectionInitializerContext = new ConnectionInitializerContext(_connectionDescription, __emptyAuthenticators); + _connectionInitializerContextAfterAuthentication = new ConnectionInitializerContext(_connectionDescription, __emptyAuthenticators); _mockConnectionInitializer = new Mock(); _mockConnectionInitializer @@ -72,13 +75,13 @@ public BinaryConnectionTests(Xunit.Abstractions.ITestOutputHelper output) : base .Returns(_connectionInitializerContext); _mockConnectionInitializer .Setup(i => i.Authenticate(It.IsAny(), It.IsAny(), CancellationToken.None)) - .Returns(_connectionDescription); + .Returns(_connectionInitializerContextAfterAuthentication); _mockConnectionInitializer .Setup(i => i.SendHelloAsync(It.IsAny(), CancellationToken.None)) .ReturnsAsync(_connectionInitializerContext); _mockConnectionInitializer .Setup(i => i.AuthenticateAsync(It.IsAny(), It.IsAny(), CancellationToken.None)) - .ReturnsAsync(_connectionDescription); + .ReturnsAsync(_connectionInitializerContextAfterAuthentication); _subject = new BinaryConnection( serverId: _serverId, @@ -143,10 +146,6 @@ public void Open_should_always_create_description_if_handshake_was_successful([V public async Task Open_should_create_authenticators_only_once( [Values(false, true)] bool async) { - var connectionDescription = new ConnectionDescription( - new ConnectionId(new ServerId(new ClusterId(), _endPoint)), - new HelloResult(new BsonDocument())); - using var memoryStream = new MemoryStream(); var clonedMessageEncoderSettings = _messageEncoderSettings.Clone(); var encoderFactory = new BinaryMessageEncoderFactory(memoryStream, clonedMessageEncoderSettings, compressorSource: null); @@ -166,10 +165,16 @@ public void Open_should_always_create_description_if_handshake_was_successful([V new CompressorConfiguration[0], new ServerApi(ServerApiVersion.V1), // use serverApi to choose command message protocol null); + + var authenticatorMock = new Mock(); + authenticatorMock + .Setup(a => a.CustomizeInitialHelloCommand(It.IsAny(), It.IsAny())) + .Returns(new BsonDocument(OppressiveLanguageConstants.LegacyHelloCommandName, 1)); + var authenticatorFactoryMock = new Mock(); authenticatorFactoryMock .Setup(a => a.Create()) - .Returns(Mock.Of(a => a.CustomizeInitialHelloCommand(It.IsAny()) == new BsonDocument(OppressiveLanguageConstants.LegacyHelloCommandName, 1))); + .Returns(authenticatorMock.Object); using var subject = new BinaryConnection( serverId: _serverId, @@ -328,6 +333,38 @@ ResponseMessage CreateResponseMessage() _capturedEvents.Any().Should().BeFalse(); } + [Theory] + [ParameterAttributeData] + public async Task Reauthentication_should_use_the_same_auth_context_as_in_initial_authentication( + [Values(false, true)] bool async) + { + _subject._connectionInitializerContext().Should().BeNull(); + + if (async) + { + await _subject.OpenAsync(CancellationToken.None); + } + else + { + _subject.Open(CancellationToken.None); + } + + _subject._connectionInitializerContext().Should().Be(_connectionInitializerContextAfterAuthentication); + + if (async) + { + await _subject.ReauthenticateAsync(CancellationToken.None); + _mockConnectionInitializer.Verify(c => c.AuthenticateAsync(It.IsAny(), It.Is(cxt => cxt == _connectionInitializerContext), CancellationToken.None), Times.Exactly(1)); + _mockConnectionInitializer.Verify(c => c.AuthenticateAsync(It.IsAny(), It.Is(cxt => cxt == _connectionInitializerContextAfterAuthentication), CancellationToken.None), Times.Exactly(1)); + } + else + { + _subject.Reauthenticate(CancellationToken.None); + _mockConnectionInitializer.Verify(c => c.Authenticate(It.IsAny(), It.Is(cxt => cxt == _connectionInitializerContext), CancellationToken.None), Times.Exactly(1)); + _mockConnectionInitializer.Verify(c => c.Authenticate(It.IsAny(), It.Is(cxt => cxt == _connectionInitializerContextAfterAuthentication), CancellationToken.None), Times.Exactly(1)); + } + } + [Theory] [ParameterAttributeData] public void ReceiveMessage_should_throw_a_FormatException_when_message_is_an_invalid_size( @@ -889,4 +926,10 @@ public override void Write(byte[] buffer, int offset, int count) } } } + + internal static class BinaryConnectionReflector + { + public static ConnectionInitializerContext _connectionInitializerContext(this BinaryConnection subject) + => (ConnectionInitializerContext)Reflector.GetFieldValue(subject, nameof(_connectionInitializerContext)); + } } diff --git a/tests/MongoDB.Driver.Core.Tests/Core/Connections/BinaryConnection_CommandEventTests.cs b/tests/MongoDB.Driver.Core.Tests/Core/Connections/BinaryConnection_CommandEventTests.cs index d87d14d4490..dbc469e4ef4 100644 --- a/tests/MongoDB.Driver.Core.Tests/Core/Connections/BinaryConnection_CommandEventTests.cs +++ b/tests/MongoDB.Driver.Core.Tests/Core/Connections/BinaryConnection_CommandEventTests.cs @@ -94,7 +94,7 @@ public BinaryConnection_CommandEventTests(ITestOutputHelper output) : base(outpu _mockConnectionInitializer.Setup(i => i.SendHelloAsync(It.IsAny(), CancellationToken.None)) .Returns(() => Task.FromResult(new ConnectionInitializerContext(connectionDescriptionFunc(), emptyAuthenticators))); _mockConnectionInitializer.Setup(i => i.AuthenticateAsync(It.IsAny(), It.IsAny(), CancellationToken.None)) - .Returns(() => Task.FromResult(connectionDescriptionFunc())); + .Returns(() => Task.FromResult(new ConnectionInitializerContext(connectionDescriptionFunc(), emptyAuthenticators))); _subject = new BinaryConnection( serverId: serverId, diff --git a/tests/MongoDB.Driver.Core.Tests/Core/Connections/ConnectionInitializerTests.cs b/tests/MongoDB.Driver.Core.Tests/Core/Connections/ConnectionInitializerTests.cs index eac41bfeb5e..b53b93084d2 100644 --- a/tests/MongoDB.Driver.Core.Tests/Core/Connections/ConnectionInitializerTests.cs +++ b/tests/MongoDB.Driver.Core.Tests/Core/Connections/ConnectionInitializerTests.cs @@ -409,13 +409,13 @@ private ConnectionDescription InitializeConnection(ConnectionInitializer connect { connectionInitializerContext = connectionInitializer.SendHelloAsync(connection, cancellationToken).GetAwaiter().GetResult(); connection.Description = connectionInitializerContext.Description; - return connectionInitializer.AuthenticateAsync(connection, connectionInitializerContext, cancellationToken).GetAwaiter().GetResult(); + return connectionInitializer.AuthenticateAsync(connection, connectionInitializerContext, cancellationToken).GetAwaiter().GetResult().Description; } else { connectionInitializerContext = connectionInitializer.SendHello(connection, cancellationToken); connection.Description = connectionInitializerContext.Description; - return connectionInitializer.Authenticate(connection, connectionInitializerContext, cancellationToken); + return connectionInitializer.Authenticate(connection, connectionInitializerContext, cancellationToken).Description; } } } @@ -426,6 +426,6 @@ internal static class ConnectionInitializerReflector this ConnectionInitializer initializer, IReadOnlyList authenticators, bool loadBalanced) => - (BsonDocument)Reflector.Invoke(initializer, nameof(CreateInitialHelloCommand), authenticators, loadBalanced); + (BsonDocument)Reflector.Invoke(initializer, nameof(CreateInitialHelloCommand), authenticators, loadBalanced, CancellationToken.None); } } diff --git a/tests/MongoDB.Driver.Core.Tests/Core/Operations/RetryabilityHelperTests.cs b/tests/MongoDB.Driver.Core.Tests/Core/Operations/RetryabilityHelperTests.cs index f4e05118945..b8dbcb7bd73 100644 --- a/tests/MongoDB.Driver.Core.Tests/Core/Operations/RetryabilityHelperTests.cs +++ b/tests/MongoDB.Driver.Core.Tests/Core/Operations/RetryabilityHelperTests.cs @@ -192,6 +192,24 @@ public void IsResumableChangeStreamException_should_return_expected_result_for_s result.Should().Be(isResumable); } + [Theory] + [InlineData(ServerErrorCode.ReauthenticationRequired, "saslStart", false)] + [InlineData(ServerErrorCode.ReauthenticationRequired, "saslContinue", false)] + [InlineData(ServerErrorCode.ReauthenticationRequired, "saslDummy", true)] + [InlineData(ServerErrorCode.ReauthenticationRequired, "dummy", true)] + [InlineData(1, "saslStart", false)] + [InlineData(1, "saslContinue", false)] + [InlineData(1, "saslNotExisted", false)] + [InlineData(1, "dummy", false)] + public void IsRetryableCommandAuthenticationException_should_return_expected_result_using_exception_type(int errorCode, string commandName, bool expectedResult) + { + var exception = CoreExceptionHelper.CreateMongoCommandException(errorCode); + + var result = RetryabilityHelper.IsReauthenticationRequested(exception, new BsonDocument(commandName, 1)); + + result.Should().Be(expectedResult); + } + [Theory] [InlineData(typeof(IOException), false)] [InlineData(typeof(MongoCursorNotFoundException), false)] diff --git a/tests/MongoDB.Driver.TestHelpers/DriverTestConfiguration.cs b/tests/MongoDB.Driver.TestHelpers/DriverTestConfiguration.cs index ca180abd692..e543171489f 100644 --- a/tests/MongoDB.Driver.TestHelpers/DriverTestConfiguration.cs +++ b/tests/MongoDB.Driver.TestHelpers/DriverTestConfiguration.cs @@ -19,6 +19,7 @@ using System.Threading; using Microsoft.Extensions.Logging; using MongoDB.Driver.Core; +using MongoDB.Driver.Core.Authentication.Oidc; using MongoDB.Driver.Core.Clusters; using MongoDB.Driver.Core.Configuration; using MongoDB.Driver.Core.Logging; @@ -161,6 +162,11 @@ public static DisposableMongoClient CreateDisposableClient(Action(credential.Evidence); } + [Fact] + public void CreateOidcCredential_should_initialize_all_required_properties_with_callback() + { + const string principalName = "principalName"; + var oidcTokenProvider = new Mock(); + + var credential = MongoCredential.CreateOidcCredential(oidcTokenProvider.Object, principalName); + + credential.Mechanism.Should().Be("MONGODB-OIDC"); + credential.Username.Should().Be(principalName); + credential.Evidence.Should().BeOfType(); + credential.GetMechanismProperty("OIDC_CALLBACK", null) + .Should().Be(oidcTokenProvider.Object); + } + + [Fact] + public void CreateOidcCredential_should_initialize_all_required_properties_with_environment() + { + const string environment = "env"; + + var credential = MongoCredential.CreateOidcCredential(environment); + + credential.Mechanism.Should().Be("MONGODB-OIDC"); + credential.Username.Should().BeNull(); + credential.Evidence.Should().BeOfType(); + credential.GetMechanismProperty("ENVIRONMENT", defaultValue: null).Should().Be(environment); + } + [Fact] public void TestEquals() { diff --git a/tests/MongoDB.Driver.Tests/Specifications/UnifiedTestSpecRunner.cs b/tests/MongoDB.Driver.Tests/Specifications/UnifiedTestSpecRunner.cs index 1f2a223c6fe..2e8dafa7c46 100644 --- a/tests/MongoDB.Driver.Tests/Specifications/UnifiedTestSpecRunner.cs +++ b/tests/MongoDB.Driver.Tests/Specifications/UnifiedTestSpecRunner.cs @@ -37,6 +37,10 @@ public UnifiedTestSpecRunner(ITestOutputHelper testOutputHelper) { } + [Category("Authentication", "MongoDbOidc")] + [UnifiedTestsTheory("auth.tests.unified")] + public void Auth(JsonDrivenTestCase testCase) => Run(testCase); + [Category("SupportLoadBalancing")] [UnifiedTestsTheory("change_streams.tests.unified")] public void ChangeStreams(JsonDrivenTestCase testCase) => Run(testCase); diff --git a/tests/MongoDB.Driver.Tests/Specifications/auth/AuthTestRunner.cs b/tests/MongoDB.Driver.Tests/Specifications/auth/AuthTestRunner.cs index d7b2e735127..ac0e90175de 100644 --- a/tests/MongoDB.Driver.Tests/Specifications/auth/AuthTestRunner.cs +++ b/tests/MongoDB.Driver.Tests/Specifications/auth/AuthTestRunner.cs @@ -1,4 +1,4 @@ -/* Copyright 2018-present MongoDB Inc. +/* Copyright 2010-present MongoDB Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,16 +16,22 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using FluentAssertions; using MongoDB.Bson; -using MongoDB.Bson.TestHelpers; using MongoDB.Bson.TestHelpers.JsonDrivenTests; -using MongoDB.TestHelpers.XunitExtensions; using MongoDB.Driver.Core.Authentication; +using MongoDB.Driver.Core.Authentication.Oidc; +using MongoDB.Driver.Core.Misc; +using MongoDB.TestHelpers.XunitExtensions; +using Moq; using Xunit; +using Xunit.Sdk; +using Reflector = MongoDB.Bson.TestHelpers.Reflector; namespace MongoDB.Driver.Tests.Specifications.auth { + [Category("Authentication")] public class AuthTestRunner { [Theory] @@ -37,9 +43,21 @@ public void RunTestDefinition(JsonDrivenTestCase testCase) MongoCredential mongoCredential = null; Exception parseException = null; + + var connectionString = (string)definition["uri"]; + if (connectionString.Contains("CANONICALIZE_HOST_NAME")) + { + // have to skip CANONICALIZE_HOST_NAME tests. Not implemented yet. See: https://jira.mongodb.org/browse/CSHARP-3796 + throw new SkipException("Test skipped because CANONICALIZE_HOST_NAME is not supported."); + } + + if (connectionString.Contains("ENVIRONMENT:gcp")) + { + throw new SkipException("Test skipped because ENVIRONMENT:gcp is not supported."); + } + try { - var connectionString = (string)definition["uri"]; mongoCredential = MongoClientSettings.FromConnectionString(connectionString).Credential; } catch (Exception ex) @@ -47,9 +65,18 @@ public void RunTestDefinition(JsonDrivenTestCase testCase) parseException = ex; } + IAuthenticator authenticator = null; + if (parseException == null && !SkipActualAuthenticatorCreating(testCase.Name)) + { + var dummyEndpoint = new DnsEndPoint("localhost", 27017); + var environmentVariablesProviderMock = new Mock(); + environmentVariablesProviderMock + .Setup(p => p.GetEnvironmentVariable("OIDC_TOKEN_FILE")).Returns("dummy_file_path"); + parseException = Record.Exception(() => authenticator = mongoCredential?.ToAuthenticator(new[] { dummyEndpoint }, serverApi: null, environmentVariableProvider: environmentVariablesProviderMock.Object)); + } if (parseException == null) { - AssertValid(mongoCredential, definition); + AssertValid(authenticator, mongoCredential, definition); } else { @@ -57,7 +84,7 @@ public void RunTestDefinition(JsonDrivenTestCase testCase) } } - private void AssertValid(MongoCredential mongoCredential, BsonDocument definition) + private void AssertValid(IAuthenticator authenticator, MongoCredential mongoCredential, BsonDocument definition) { if (!definition["valid"].ToBoolean()) { @@ -80,49 +107,77 @@ private void AssertValid(MongoCredential mongoCredential, BsonDocument definitio mongoCredential.Mechanism.Should().Be(ValueToString(expectedCredential["mechanism"])); var expectedMechanismProperties = expectedCredential["mechanism_properties"]; - if (mongoCredential.Mechanism == GssapiAuthenticator.MechanismName) + switch (mongoCredential.Mechanism) { - var gssapiAuthenticator = (GssapiAuthenticator)mongoCredential.ToAuthenticator(serverApi: null); - if (expectedMechanismProperties.IsBsonNull) - { - var serviceName = gssapiAuthenticator._mechanism_serviceName(); - serviceName.Should().Be("mongodb"); // The default is "mongodb". - var canonicalizeHostName = gssapiAuthenticator._mechanism_canonicalizeHostName(); - canonicalizeHostName.Should().BeFalse(); // The default is "false". - } - else - { - foreach (var expectedMechanismProperty in expectedMechanismProperties.AsBsonDocument) + case GssapiAuthenticator.MechanismName: { - var mechanismName = expectedMechanismProperty.Name; - switch (mechanismName) + var gssapiAuthenticator = (GssapiAuthenticator)authenticator; + if (expectedMechanismProperties.IsBsonNull) { - case "SERVICE_NAME": - var serviceName = gssapiAuthenticator._mechanism_serviceName(); - serviceName.Should().Be(ValueToString(expectedMechanismProperty.Value)); - break; - case "CANONICALIZE_HOST_NAME": - var canonicalizeHostName = gssapiAuthenticator._mechanism_canonicalizeHostName(); - canonicalizeHostName.Should().Be(expectedMechanismProperty.Value.ToBoolean()); - break; - default: - throw new Exception($"Invalid mechanism property '{mechanismName}'."); + var serviceName = gssapiAuthenticator._mechanism_serviceName(); + serviceName.Should().Be("mongodb"); // The default is "mongodb". + var canonicalizeHostName = gssapiAuthenticator._mechanism_canonicalizeHostName(); + canonicalizeHostName.Should().BeFalse(); // The default is "false". } + else + { + foreach (var expectedMechanismProperty in expectedMechanismProperties.AsBsonDocument) + { + var mechanismName = expectedMechanismProperty.Name; + switch (mechanismName) + { + case "SERVICE_NAME": + var serviceName = gssapiAuthenticator._mechanism_serviceName(); + serviceName.Should().Be(ValueToString(expectedMechanismProperty.Value)); + break; + case "CANONICALIZE_HOST_NAME": + var canonicalizeHostName = gssapiAuthenticator._mechanism_canonicalizeHostName(); + canonicalizeHostName.Should().Be(expectedMechanismProperty.Value.ToBoolean()); + break; + default: + throw new Exception($"Invalid mechanism property '{mechanismName}'."); + } + } + } + break; + } + case MongoOidcAuthenticator.MechanismName: + { + var oidcAuthenticator = (MongoOidcAuthenticator)authenticator; + foreach (var expectedMechanismProperty in expectedMechanismProperties.AsBsonDocument) + { + var mechanismName = expectedMechanismProperty.Name; + switch (mechanismName) + { + case OidcConfiguration.EnvironmentMechanismPropertyName: + var environment = oidcAuthenticator.Configuration.Environment; + environment.Should().Be(expectedMechanismProperty.Value.ToString()); + break; + case OidcConfiguration.TokenResourceMechanismPropertyName: + var resourceToken = oidcAuthenticator.Configuration.TokenResource; + resourceToken.Should().Be(expectedMechanismProperty.Value.ToString()); + break; + default: + throw new Exception($"Invalid mechanism property '{mechanismName}'."); + } + } + } + break; + default: + { + var actualMechanismProperties = mongoCredential._mechanismProperties(); + if (expectedMechanismProperties.IsBsonNull) + { + actualMechanismProperties.Should().BeEmpty(); + } + else + { + var authMechanismProperties = new BsonDocument(actualMechanismProperties.Select(kv => new BsonElement(kv.Key, BsonValue.Create(kv.Value)))); + authMechanismProperties.Should().BeEquivalentTo(expectedMechanismProperties.AsBsonDocument); + } + + break; } - } - } - else - { - var actualMechanismProperties = mongoCredential._mechanismProperties(); - if (expectedMechanismProperties.IsBsonNull) - { - actualMechanismProperties.Should().BeEmpty(); - } - else - { - var authMechanismProperties = new BsonDocument(actualMechanismProperties.Select(kv => new BsonElement(kv.Key, BsonValue.Create(kv.Value)))); - authMechanismProperties.Should().BeEquivalentTo(expectedMechanismProperties.AsBsonDocument); - } } } } @@ -135,6 +190,10 @@ private void AssertInvalid(Exception ex, BsonDocument definition) } } + private bool SkipActualAuthenticatorCreating(string testCaseName) => + // should be addressed in https://jira.mongodb.org/browse/CSHARP-4503 + testCaseName.Contains("MONGODB-AWS"); + private string ValueToString(BsonValue value) { return value == BsonNull.Value ? null : value.ToString(); @@ -143,7 +202,7 @@ private string ValueToString(BsonValue value) // nested types private class TestCaseFactory : JsonDrivenTestCaseFactory { - protected override string PathPrefix => "MongoDB.Driver.Tests.Specifications.auth.tests."; + protected override string PathPrefix => "MongoDB.Driver.Tests.Specifications.auth.tests.legacy"; } } diff --git a/tests/MongoDB.Driver.Tests/Specifications/auth/OidcAuthenticationProseTests.cs b/tests/MongoDB.Driver.Tests/Specifications/auth/OidcAuthenticationProseTests.cs new file mode 100644 index 00000000000..f69e76c24b9 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Specifications/auth/OidcAuthenticationProseTests.cs @@ -0,0 +1,413 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.TestHelpers; +using MongoDB.Driver.Core; +using MongoDB.Driver.Core.Authentication; +using MongoDB.Driver.Core.Authentication.Oidc; +using MongoDB.Driver.Core.Bindings; +using MongoDB.Driver.Core.Events; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Core.Servers; +using MongoDB.Driver.Core.TestHelpers; +using MongoDB.Driver.Core.TestHelpers.Logging; +using MongoDB.TestHelpers.XunitExtensions; +using Moq; +using Xunit; +using Xunit.Abstractions; + +namespace MongoDB.Driver.Tests.Specifications.auth +{ + [Category("Authentication", "MongoDbOidc")] + public class OidcAuthenticationProseTests : LoggableTestClass + { + // some auth configuration may support only this name + private const string DatabaseName = "test"; + private const string CollectionName = "collName"; + private const string OidcTokensDirEnvName = "OIDC_TOKEN_DIR"; + private const string TokenName = "test_user1"; + + public OidcAuthenticationProseTests(ITestOutputHelper output) : base(output) + { + OidcCallbackAdapterCachingFactory.Instance.Reset(); + } + + // https://github.com/mongodb/specifications/blob/611b12ccbdd012dcd9ab2877a32200b3835c97af/source/auth/tests/mongodb-oidc.md?plain=1#L37 + [Theory] + [ParameterAttributeData] + public async Task Callback_authentication_callback_called_during_authentication([Values(false, true)]bool async) + { + EnsureOidcIsConfigured("test"); + + var callbackMock = new Mock(); + ConfigureOidcCallback(callbackMock, GetAccessTokenValue()); + var credential = MongoCredential.CreateOidcCredential(callbackMock.Object); + var eventCapturer = new EventCapturer().CaptureCommandEvents(SaslAuthenticator.SaslStartCommand); + var collection = CreateMongoCollection(credential, eventCapturer); + + _ = async + ? await collection.FindAsync(Builders.Filter.Empty) + : collection.FindSync(Builders.Filter.Empty); + + VerifyCallbackUsage(callbackMock, async, Times.Once()); + eventCapturer.Next().Should().BeOfType(); + eventCapturer.Next().Should().BeOfType(); + } + + // https://github.com/mongodb/specifications/blob/611b12ccbdd012dcd9ab2877a32200b3835c97af/source/auth/tests/mongodb-oidc.md?plain=1#L44 + [Theory] + [ParameterAttributeData] + public async Task Callback_authentication_callback_called_once_for_multiple_connections([Values(false, true)]bool async) + { + EnsureOidcIsConfigured("test"); + + var callbackMock = new Mock(); + ConfigureOidcCallback(callbackMock, GetAccessTokenValue()); + var credential = MongoCredential.CreateOidcCredential(callbackMock.Object); + var collection = CreateMongoCollection(credential); + + await ThreadingUtilities.ExecuteTasksOnNewThreads(10, async t => + { + for (var i = 0; i < 100; i++) + { + _ = async + ? await collection.FindAsync(Builders.Filter.Empty) + : collection.FindSync(Builders.Filter.Empty); + } + }, (int)TimeSpan.FromSeconds(20).TotalMilliseconds); + + VerifyCallbackUsage(callbackMock, async, Times.Once()); + } + + // https://github.com/mongodb/specifications/blob/611b12ccbdd012dcd9ab2877a32200b3835c97af/source/auth/tests/mongodb-oidc.md?plain=1#L53 + [Theory] + [ParameterAttributeData] + public async Task Callback_validation_valid_callback_inputs([Values(false, true)] bool async) + { + EnsureOidcIsConfigured("test"); + + var callbackMock = new Mock(); + ConfigureOidcCallback(callbackMock, GetAccessTokenValue()); + var credential = MongoCredential.CreateOidcCredential(callbackMock.Object); + var eventCapturer = new EventCapturer().CaptureCommandEvents(SaslAuthenticator.SaslStartCommand); + var collection = CreateMongoCollection(credential, eventCapturer); + + _ = async + ? await collection.FindAsync(Builders.Filter.Empty) + : collection.FindSync(Builders.Filter.Empty); + + VerifyCallbackUsage(callbackMock, async, Times.Once()); + eventCapturer.Next().Should().BeOfType(); + eventCapturer.Next().Should().BeOfType(); + } + + // https://github.com/mongodb/specifications/blob/611b12ccbdd012dcd9ab2877a32200b3835c97af/source/auth/tests/mongodb-oidc.md?plain=1#L60 + [Theory] + [ParameterAttributeData] + public async Task Callback_validation_callback_returns_null([Values(false, true)] bool async) + { + EnsureOidcIsConfigured("test"); + + var callbackMock = new Mock(); + var credential = MongoCredential.CreateOidcCredential(callbackMock.Object); + var eventCapturer = new EventCapturer().CaptureCommandEvents(SaslAuthenticator.SaslStartCommand); + var collection = CreateMongoCollection(credential, eventCapturer); + + var exception = async + ? await Record.ExceptionAsync(() => collection.FindAsync(Builders.Filter.Empty)) + : Record.Exception(() => collection.FindSync(Builders.Filter.Empty)); + + exception.Should().BeOfType(); + VerifyCallbackUsage(callbackMock, async, Times.Once()); + eventCapturer.Events.Should().BeEmpty(); + } + + // https://github.com/mongodb/specifications/blob/611b12ccbdd012dcd9ab2877a32200b3835c97af/source/auth/tests/mongodb-oidc.md?plain=1#L66 + [Theory] + [ParameterAttributeData] + public async Task Callback_validation_callback_returns_missing_data([Values(false, true)] bool async) + { + EnsureOidcIsConfigured("test"); + + var callbackMock = new Mock(); + ConfigureOidcCallback(callbackMock, "wrong token"); + var credential = MongoCredential.CreateOidcCredential(callbackMock.Object); + var eventCapturer = new EventCapturer().CaptureCommandEvents(SaslAuthenticator.SaslStartCommand); + var collection = CreateMongoCollection(credential, eventCapturer); + + var exception = async + ? await Record.ExceptionAsync(() => collection.FindAsync(Builders.Filter.Empty)) + : Record.Exception(() => collection.FindSync(Builders.Filter.Empty)); + + exception.Should().BeOfType(); + VerifyCallbackUsage(callbackMock, async, Times.Once()); + + eventCapturer.Next().Should().BeOfType(); + eventCapturer.Next().Should().BeOfType(); + } + + // https://github.com/mongodb/specifications/blob/611b12ccbdd012dcd9ab2877a32200b3835c97af/source/auth/tests/mongodb-oidc.md?plain=1#L73 + [Theory] + [ParameterAttributeData] + public async Task Callback_validation_invalid_client_configuration([Values(false, true)] bool async) + { + EnsureOidcIsConfigured("test"); + + var callbackMock = new Mock(); + ConfigureOidcCallback(callbackMock, GetAccessTokenValue()); + var credential = MongoCredential.CreateOidcCredential(callbackMock.Object) + .WithMechanismProperty("ENVIRONMENT", "test"); + var eventCapturer = new EventCapturer().CaptureCommandEvents(SaslAuthenticator.SaslStartCommand); + var collection = CreateMongoCollection(credential, eventCapturer); + + var exception = async + ? await Record.ExceptionAsync(() => collection.FindAsync(Builders.Filter.Empty)) + : Record.Exception(() => collection.FindSync(Builders.Filter.Empty)); + + exception.Should().BeOfType(); + VerifyCallbackUsage(callbackMock, async, Times.Never()); + eventCapturer.Events.Should().BeEmpty(); + } + + // https://github.com/mongodb/specifications/blob/611b12ccbdd012dcd9ab2877a32200b3835c97af/source/auth/tests/mongodb-oidc.md?plain=1#L80 + [Theory] + [ParameterAttributeData] + public async Task Authentication_failure_with_cached_tokens_fetch_new_and_retry([Values(false, true)] bool async) + { + EnsureOidcIsConfigured("test"); + + var callbackMock = new Mock(); + var credential = MongoCredential.CreateOidcCredential(callbackMock.Object); + + // have to access to the adapter directly to poison the cached access token. + var callbackAdapter = OidcCallbackAdapterCachingFactory.Instance.Get(new OidcConfiguration( + CoreTestConfiguration.ConnectionString.Hosts, + credential.Username, + credential._mechanismProperties())); + + ConfigureOidcCallback(callbackMock, "wrong token"); + var callbackParameters = new OidcCallbackParameters(1, null); + _ = async + ? await callbackAdapter.GetCredentialsAsync(callbackParameters, default) + : callbackAdapter.GetCredentials(callbackParameters, default); + + // configure mock with valid access token + ConfigureOidcCallback(callbackMock, GetAccessTokenValue()); + + // callbackAdapter should have cached wrong access token at this point. + var eventCapturer = new EventCapturer().CaptureCommandEvents(SaslAuthenticator.SaslStartCommand); + var collection = CreateMongoCollection(credential, eventCapturer); + + _ = async + ? await collection.FindAsync(Builders.Filter.Empty) + : collection.FindSync(Builders.Filter.Empty); + + VerifyCallbackUsage(callbackMock, async, Times.Once()); + // commented out because the events validation does not work on Ubuntu somehow. Need to investigate and fix the test. + // eventCapturer.Next().Should().BeOfType(); + // eventCapturer.Next().Should().BeOfType(); + // eventCapturer.Next().Should().BeOfType(); + // eventCapturer.Next().Should().BeOfType(); + } + + // https://github.com/mongodb/specifications/blob/611b12ccbdd012dcd9ab2877a32200b3835c97af/source/auth/tests/mongodb-oidc.md?plain=1#L88 + [Theory] + [ParameterAttributeData] + public async Task Authentication_failure_without_cached_tokens_return_error([Values(false, true)] bool async) + { + EnsureOidcIsConfigured("test"); + + var callbackMock = new Mock(); + ConfigureOidcCallback(callbackMock, "wrong token"); + var credential = MongoCredential.CreateOidcCredential(callbackMock.Object); + var eventCapturer = new EventCapturer().CaptureCommandEvents(SaslAuthenticator.SaslStartCommand); + var collection = CreateMongoCollection(credential, eventCapturer); + + var exception = async + ? await Record.ExceptionAsync(() => collection.FindAsync(Builders.Filter.Empty)) + : Record.Exception(() => collection.FindSync(Builders.Filter.Empty)); + + exception.Should().BeOfType(); + VerifyCallbackUsage(callbackMock, async, Times.Once()); + + eventCapturer.Next().Should().BeOfType(); + eventCapturer.Next().Should().BeOfType(); + } + + // https://github.com/mongodb/specifications/blob/611b12ccbdd012dcd9ab2877a32200b3835c97af/source/auth/tests/mongodb-oidc.md?plain=1#L95 + [Theory] + [ParameterAttributeData] + public async Task ReAuthentication([Values(false, true)] bool async) + { + EnsureOidcIsConfigured("test"); + + var callbackMock = new Mock(); + ConfigureOidcCallback(callbackMock, GetAccessTokenValue()); + var credential = MongoCredential.CreateOidcCredential(callbackMock.Object); + var eventCapturer = new EventCapturer().CaptureCommandEvents(SaslAuthenticator.SaslStartCommand); + var collection = CreateMongoCollection(credential, eventCapturer); + + using (ConfigureFailPoint(1, (int)ServerErrorCode.ReauthenticationRequired, "find")) + { + _ = async + ? await collection.FindAsync(Builders.Filter.Empty) + : collection.FindSync(Builders.Filter.Empty); + } + + VerifyCallbackUsage(callbackMock, async, Times.Exactly(2)); + eventCapturer.Next().Should().BeOfType(); + eventCapturer.Next().Should().BeOfType(); + eventCapturer.Next().Should().BeOfType(); + eventCapturer.Next().Should().BeOfType(); + } + + // https://github.com/mongodb/specifications/blob/611b12ccbdd012dcd9ab2877a32200b3835c97af/source/auth/tests/mongodb-oidc.md?plain=1#L125 + [Theory] + [ParameterAttributeData] + public async Task Azure_auth_no_username([Values(false, true)] bool async) + { + EnsureOidcIsConfigured("azure"); + + var credential = MongoCredential.CreateOidcCredential("azure") + .WithMechanismProperty(OidcConfiguration.TokenResourceMechanismPropertyName, Environment.GetEnvironmentVariable("TOKEN_RESOURCE")); + var eventCapturer = new EventCapturer().CaptureCommandEvents(SaslAuthenticator.SaslStartCommand); + var collection = CreateMongoCollection(credential, eventCapturer); + + _ = async + ? await collection.FindAsync(Builders.Filter.Empty) + : collection.FindSync(Builders.Filter.Empty); + + eventCapturer.Next().Should().BeOfType(); + eventCapturer.Next().Should().BeOfType(); + } + + // https://github.com/mongodb/specifications/blob/611b12ccbdd012dcd9ab2877a32200b3835c97af/source/auth/tests/mongodb-oidc.md?plain=1#L131 + [Theory] + [ParameterAttributeData] + public async Task Azure_auth_bad_username_return_error([Values(false, true)] bool async) + { + EnsureOidcIsConfigured("azure"); + + var credential = MongoCredential.CreateOidcCredential("azure", "bad") + .WithMechanismProperty(OidcConfiguration.TokenResourceMechanismPropertyName, Environment.GetEnvironmentVariable("TOKEN_RESOURCE")); + var collection = CreateMongoCollection(credential); + + var exception = async + ? await Record.ExceptionAsync(() => collection.FindAsync(Builders.Filter.Empty)) + : Record.Exception(() => collection.FindSync(Builders.Filter.Empty)); + + exception.Should().BeOfType(); + } + + private void ConfigureOidcCallback(Mock callbackMock, string accessToken) + { + callbackMock.Reset(); + + var response = new OidcAccessToken(accessToken, null); + callbackMock + .Setup(c => c.GetOidcAccessToken(It.IsAny(), It.IsAny())) + .Returns(response); + callbackMock + .Setup(c => c.GetOidcAccessTokenAsync(It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(response)); + } + + private FailPoint ConfigureFailPoint( + int times, + int errorCode, + params string[] command) + { + var failPointCommand = new BsonDocument + { + { "configureFailPoint", FailPointName.FailCommand }, + { "mode", new BsonDocument("times", times) }, + { + "data", + new BsonDocument + { + { "failCommands", new BsonArray(command.Select(c => new BsonString(c))) }, + { "errorCode", errorCode } + } + } + }; + + var cluster = DriverTestConfiguration.Client.Cluster; + var session = NoCoreSession.NewHandle(); + + return FailPoint.Configure(cluster, session, failPointCommand); + } + + private MongoClientSettings CreateOidcMongoClientSettings(MongoCredential credential, EventCapturer eventCapturer = null) + { + var settings = DriverTestConfiguration.GetClientSettings(); + settings.RetryReads = false; + settings.RetryWrites = false; + settings.MinConnectionPoolSize = 0; + settings.Credential = credential; + settings.ServerMonitoringMode = ServerMonitoringMode.Poll; + settings.HeartbeatInterval = TimeSpan.FromSeconds(30); + if (eventCapturer != null) + { + settings.ClusterConfigurator = (builder) => builder.Subscribe(eventCapturer); + } + + return settings; + } + + private IMongoCollection CreateMongoCollection(MongoCredential credential, EventCapturer eventCapturer = null) + { + var clientSettings = CreateOidcMongoClientSettings(credential, eventCapturer); + var client = DriverTestConfiguration.CreateDisposableClient(clientSettings); + + var db = client.GetDatabase(DatabaseName); + return db.GetCollection(CollectionName); + } + + private void EnsureOidcIsConfigured(string environmentType) => + // EG also requires aws_test_secrets_role + RequireEnvironment + .Check() + .EnvironmentVariable("OIDC_ENV", environmentType); + + private string GetAccessTokenValue() + { + var tokenPath = Path.Combine(Environment.GetEnvironmentVariable(OidcTokensDirEnvName), TokenName); + Ensure.That(File.Exists(tokenPath), $"OIDC token {tokenPath} doesn't exist."); + + return File.ReadAllText(tokenPath); + } + + private void VerifyCallbackUsage(Mock callbackMock, bool async, Times times) + { + if (async) + { + callbackMock.Verify(x => x.GetOidcAccessToken(It.IsAny(), It.IsAny()), Times.Never()); + callbackMock.Verify(x => x.GetOidcAccessTokenAsync(It.Is(p => p.Version == 1), It.IsAny()), times); + } + else + { + callbackMock.Verify(x => x.GetOidcAccessToken(It.Is(p => p.Version == 1), It.IsAny()), times); + callbackMock.Verify(x => x.GetOidcAccessTokenAsync(It.IsAny(), It.IsAny()), Times.Never()); + } + } + } +} diff --git a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/Matchers/UnifiedErrorMatcher.cs b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/Matchers/UnifiedErrorMatcher.cs index d9dc77a4d57..e50ee9c62c4 100644 --- a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/Matchers/UnifiedErrorMatcher.cs +++ b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/Matchers/UnifiedErrorMatcher.cs @@ -67,12 +67,14 @@ public void AssertErrorsMatch(Exception actualException, BsonDocument expectedEr private void AssertErrorCode(Exception actualException, int expectedErrorCode) { + actualException = UnwrapConnectionException(actualException); var mongoCommandException = actualException.Should().BeAssignableTo().Subject; mongoCommandException.Code.Should().Be(expectedErrorCode); } private void AssertErrorCodeName(Exception actualException, string expectedErrorCodeName) { + actualException = UnwrapConnectionException(actualException); var mongoCommandException = actualException.Should().BeAssignableTo().Subject; mongoCommandException.CodeName.Should().Be(expectedErrorCodeName); } @@ -140,5 +142,15 @@ private void AssertIsError(Exception actualException, bool expectedIsError) actualException.Should().NotBeNull(); } + + private static Exception UnwrapConnectionException(Exception ex) + { + if (ex is MongoConnectionException connectionException) + { + ex = connectionException.InnerException; + } + + return ex; + } } } diff --git a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedEntityMap.cs b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedEntityMap.cs index 823f9e28006..392f5ceee8d 100644 --- a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedEntityMap.cs +++ b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedEntityMap.cs @@ -21,6 +21,7 @@ using Microsoft.Extensions.Logging; using MongoDB.Bson; using MongoDB.Driver.Core; +using MongoDB.Driver.Core.Authentication.Oidc; using MongoDB.Driver.Core.Clusters; using MongoDB.Driver.Core.Configuration; using MongoDB.Driver.Core.Events; @@ -496,6 +497,8 @@ private IGridFSBucket CreateBucket(BsonDocument entity, Dictionary ClientEventCapturers, Dictionary LoggingComponents) CreateClient(BsonDocument entity, bool async) { string appName = null; + string authMechanism = null; + var authMechanismProperties = new Dictionary(); var clientEventCapturers = new Dictionary(); Dictionary loggingComponents = null; string clientId = null; @@ -537,6 +540,29 @@ private IGridFSBucket CreateBucket(BsonDocument entity, Dictionary MongoCredential.CreateRawOidcCredential(null), + _ => throw new NotSupportedException($"Cannot create credential for {authMechanism} auth mechanism") + }; + + if (authMechanismProperties.Count > 0) + { + foreach (var mechanismProperty in authMechanismProperties) + { + settings.Credential = settings.Credential.WithMechanismProperty(mechanismProperty.Key, mechanismProperty.Value); + } + } + } }, _loggingSettings, useMultipleShardRouters); diff --git a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedTestRunner.cs b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedTestRunner.cs index 96358bb2469..b479b2c3648 100644 --- a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedTestRunner.cs +++ b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedTestRunner.cs @@ -110,7 +110,7 @@ public void Run(JsonDrivenTestCase testCase) var schemaSemanticVersion = SemanticVersion.Parse(schemaVersion); if (schemaSemanticVersion < new SemanticVersion(1, 0, 0) || - schemaSemanticVersion > new SemanticVersion(1, 17, 0)) + schemaSemanticVersion > new SemanticVersion(1, 19, 0)) { throw new FormatException($"Schema version '{schemaVersion}' is not supported."); } diff --git a/tests/MongoDB.TestHelpers/XunitExtensions/RequireEnvironment.cs b/tests/MongoDB.TestHelpers/XunitExtensions/RequireEnvironment.cs index 476dd3dc540..ac6692867b4 100644 --- a/tests/MongoDB.TestHelpers/XunitExtensions/RequireEnvironment.cs +++ b/tests/MongoDB.TestHelpers/XunitExtensions/RequireEnvironment.cs @@ -15,6 +15,7 @@ using System; using System.Diagnostics; +using System.Linq; using System.Net; using System.Net.Sockets; using Xunit.Sdk; @@ -41,6 +42,20 @@ public RequireEnvironment EnvironmentVariable(string name, bool isDefined = true throw new SkipException($"Test skipped because environment variable '{name}' {(actualIsDefined ? "is" : "is not")} defined."); } + public RequireEnvironment EnvironmentVariable(string name, params string[] matchValues) + { + var actualValue = Environment.GetEnvironmentVariable(name); + if (string.IsNullOrEmpty(actualValue)) + { + throw new SkipException($"Test skipped because environment variable '{name}' is not defined."); + } + if (matchValues.Contains(actualValue)) + { + return this; + } + throw new SkipException($"Test skipped because environment variable '{name}'={actualValue} does not satisfy expected values."); + } + public RequireEnvironment ProcessStarted(string processName) { if (Process.GetProcessesByName(processName).Length > 0)