diff --git a/.evergreen/config.yml b/.evergreen/config.yml index dab8d51fb6..71df07dc45 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -454,6 +454,48 @@ functions: CRYPT_SHARED_LIB_PATH="${CRYPT_SHARED_LIB_PATH}" SERVERLESS=1 SSL=ssl RVM_RUBY="${RVM_RUBY}" SINGLE_MONGOS="${SINGLE_MONGOS}" SERVERLESS_URI="${SERVERLESS_URI}" FLE="${FLE}" SERVERLESS_MONGODB_VERSION="${SERVERLESS_MONGODB_VERSION}" .evergreen/run-tests-serverless.sh + "run oidc vm tests": + - command: subprocess.exec + type: test + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + RVM_RUBY: ${RVM_RUBY} + TEST_SCRIPT: ${TEST_SCRIPT} + args: + - .evergreen/${RUN_SCRIPT} + + "run oidc prose tests": + - command: subprocess.exec + type: test + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + ENVIRONMENT: ${ENVIRONMENT} + RVM_RUBY: ${RVM_RUBY} + args: + - .evergreen/run-tests-oidc-prose.sh + + "run oidc unified tests": + - command: subprocess.exec + type: test + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + ENVIRONMENT: ${ENVIRONMENT} + RVM_RUBY: ${RVM_RUBY} + args: + - .evergreen/run-tests-oidc-unified.sh + pre: - func: "fetch source" - func: "create expansions" @@ -721,6 +763,77 @@ task_groups: tasks: - testazurekms-task + - name: test_oidc_task_group + setup_group: + - func: fetch source + - func: create expansions + - command: ec2.assume_role + params: + role_arn: ${aws_test_secrets_role} + - command: subprocess.exec + params: + binary: bash + include_expansions_in_env: + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - AWS_SESSION_TOKEN + env: + MONGODB_VERSION: '8.0' + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/setup.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-latest + + - name: test_oidc_azure_task_group + setup_group: + - func: fetch source + - func: create expansions + - command: shell.exec + params: + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + export AZUREOIDC_VMNAME_PREFIX="RUBY_DRIVER" + $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/setup.sh + teardown_task: + - command: shell.exec + params: + shell: bash + script: |- + ${PREPARE_SHELL} + $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/teardown.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-azure-latest + + - name: test_oidc_gcp_task_group + setup_group: + - func: fetch source + - func: create expansions + - command: shell.exec + params: + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + export GCPOIDC_VMNAME_PREFIX="RUBY_DRIVER" + $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/setup.sh + teardown_task: + - command: shell.exec + params: + shell: bash + script: |- + ${PREPARE_SHELL} + $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/teardown.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-gcp-latest + tasks: - name: "test-atlas" commands: @@ -865,8 +978,41 @@ tasks: LAMBDA_STACK_NAME: "dbx-ruby-lambda" RVM_RUBY: ruby-3.2 MONGODB_URI: ${MONGODB_URI} -axes: + - name: oidc-auth-test-latest + commands: + - func: "run oidc vm tests" + vars: + TEST_SCRIPT: run-tests-oidc-prose.sh + RUN_SCRIPT: run-tests-oidc-test.sh + - func: "run oidc vm tests" + vars: + TEST_SCRIPT: run-tests-oidc-unified.sh + RUN_SCRIPT: run-tests-oidc-test.sh + + - name: oidc-auth-test-azure-latest + commands: + - func: "run oidc vm tests" + vars: + TEST_SCRIPT: run-tests-oidc-prose.sh + RUN_SCRIPT: run-tests-oidc-azure.sh + - func: "run oidc vm tests" + vars: + TEST_SCRIPT: run-tests-oidc-unified.sh + RUN_SCRIPT: run-tests-oidc-azure.sh + + - name: oidc-auth-test-gcp-latest + commands: + - func: "run oidc vm tests" + vars: + TEST_SCRIPT: run-tests-oidc-prose.sh + RUN_SCRIPT: run-tests-oidc-gcp.sh + - func: "run oidc vm tests" + vars: + TEST_SCRIPT: run-tests-oidc-unified.sh + RUN_SCRIPT: run-tests-oidc-gcp.sh + +axes: - id: preload display_name: Preload server values: @@ -1856,3 +2002,16 @@ buildvariants: display_name: "AWS Lambda" tasks: - name: test_aws_lambda_task_group + + - matrix_name: test-oidc-variant + matrix_spec: + ruby: "ruby-3.2" + fle: helper + topology: standalone + os: ubuntu2204 + mongodb-version: latest + display_name: "OIDC auth tests: latest ruby-3.2" + tasks: + - test_oidc_task_group + - test_oidc_azure_task_group + - test_oidc_gcp_task_group diff --git a/.evergreen/run-tests-oidc-azure.sh b/.evergreen/run-tests-oidc-azure.sh new file mode 100644 index 0000000000..eea7088651 --- /dev/null +++ b/.evergreen/run-tests-oidc-azure.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -o xtrace # Write all commands first to stderr +set -o errexit # Exit the script with error if any of the commands fail + +export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-ruby-driver.tgz +tar czf $AZUREOIDC_DRIVERS_TAR_FILE . +export AZUREOIDC_TEST_CMD="source ./env.sh && ENVIRONMENT=azure RVM_RUBY=${RVM_RUBY} ./.evergreen/${TEST_SCRIPT}" +export PROJECT_DIRECTORY=$PROJECT_DIRECTORY +bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh diff --git a/.evergreen/run-tests-oidc-gcp.sh b/.evergreen/run-tests-oidc-gcp.sh new file mode 100644 index 0000000000..636ed8e79a --- /dev/null +++ b/.evergreen/run-tests-oidc-gcp.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -o xtrace # Write all commands first to stderr +set -o errexit # Exit the script with error if any of the commands fail + +export GCPOIDC_DRIVERS_TAR_FILE=/tmp/mongo-ruby-driver.tgz +tar czf $GCPOIDC_DRIVERS_TAR_FILE . +export GCPOIDC_TEST_CMD="source ./secrets-export.sh drivers/gcpoidc && ENVIRONMENT=gcp RVM_RUBY=${RVM_RUBY} ./.evergreen/${TEST_SCRIPT}" +export PROJECT_DIRECTORY=$PROJECT_DIRECTORY +bash $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/run-driver-test.sh diff --git a/.evergreen/run-tests-oidc-prose.sh b/.evergreen/run-tests-oidc-prose.sh new file mode 100755 index 0000000000..4b86a96431 --- /dev/null +++ b/.evergreen/run-tests-oidc-prose.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -ex + +ENVIRONMENT=${ENVIRONMENT:-"test"} + +. `dirname "$0"`/../spec/shared/shlib/distro.sh +. `dirname "$0"`/../spec/shared/shlib/set_env.sh +. `dirname "$0"`/functions.sh + +set_env_vars +set_env_python +set_env_ruby + +sudo apt-get -y install libyaml-dev cmake + +bundle_install +bundle exec rspec -fd spec/integration/oidc/${ENVIRONMENT}_machine_auth_flow_prose_spec.rb + +test_status=$? + +kill_jruby + +exit ${test_status} diff --git a/.evergreen/run-tests-oidc-test.sh b/.evergreen/run-tests-oidc-test.sh new file mode 100644 index 0000000000..604e525cc2 --- /dev/null +++ b/.evergreen/run-tests-oidc-test.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -o xtrace # Write all commands first to stderr +set -o errexit # Exit the script with error if any of the commands fail + +source $DRIVERS_TOOLS/.evergreen/auth_oidc/secrets-export.sh +export PROJECT_DIRECTORY=$PROJECT_DIRECTORY +export ENVIRONMENT=$ENVIRONMENT +export AWS_WEB_IDENTITY_TOKEN_FILE=$OIDC_TOKEN_FILE +bash ./.evergreen/${TEST_SCRIPT} diff --git a/.evergreen/run-tests-oidc-unified.sh b/.evergreen/run-tests-oidc-unified.sh new file mode 100755 index 0000000000..e69de29bb2 diff --git a/.mod/drivers-evergreen-tools b/.mod/drivers-evergreen-tools index 8c948a286f..8831f5c580 160000 --- a/.mod/drivers-evergreen-tools +++ b/.mod/drivers-evergreen-tools @@ -1 +1 @@ -Subproject commit 8c948a286f1b82427b5a409b8c42475b599d4d6f +Subproject commit 8831f5c5806e124cb7a7dba56de59ed49c638ef8 diff --git a/lib/mongo/auth.rb b/lib/mongo/auth.rb index 65d77cd89f..01aefcebe3 100644 --- a/lib/mongo/auth.rb +++ b/lib/mongo/auth.rb @@ -27,6 +27,7 @@ require 'mongo/auth/cr' require 'mongo/auth/gssapi' require 'mongo/auth/ldap' +require 'mongo/auth/oidc' require 'mongo/auth/scram' require 'mongo/auth/scram256' require 'mongo/auth/x509' @@ -70,6 +71,7 @@ module Auth aws: Aws, gssapi: Gssapi, mongodb_cr: CR, + mongodb_oidc: Oidc, mongodb_x509: X509, plain: LDAP, scram: Scram, @@ -89,7 +91,7 @@ module Auth # value of speculativeAuthenticate field of hello response of # the handshake on the specified connection. # - # @return [ Auth::Aws | Auth::CR | Auth::Gssapi | Auth::LDAP | + # @return [ Auth::Aws | Auth::CR | Auth::Gssapi | Auth::LDAP | Auth::Oidc # Auth::Scram | Auth::Scram256 | Auth::X509 ] The authenticator. # # @since 2.0.0 diff --git a/lib/mongo/auth/oidc.rb b/lib/mongo/auth/oidc.rb new file mode 100644 index 0000000000..75eadfef84 --- /dev/null +++ b/lib/mongo/auth/oidc.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true +# rubocop:todo all + +# Copyright (C) 2024 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. + +module Mongo + module Auth + + # Defines behavior for OIDC authentication. + # + # @api private + class Oidc < Base + attr_reader :speculative_auth_result, :cache, :machine_workflow + + # The authentication mechanism string. + # + # @since 2.20.0 + MECHANISM = 'MONGODB-OIDC'.freeze + + # Initializes the OIDC authenticator. + # + # @param [ Auth::User ] user The user to authenticate. + # @param [ Mongo::Connection ] connection The connection to authenticate over. + # + # @option opts [ BSON::Document | nil ] speculative_auth_result The + # value of speculativeAuthenticate field of hello response of + # the handshake on the specified connection. + def initialize(user, connection, **opts) + super + @cache = TokenCache.new + @speculative_auth_result = opts[:speculative_auth_result] + @machine_workflow = MachineWorkflow::new( + auth_mech_properties: user.auth_mech_properties, + username: user.name + ) + end + + # Log the user in on the current connection. + # + # @return [ BSON::Document ] The document of the authentication response. + def login + execute_workflow(connection: connection, conversation: conversation) + end + + private + + def execute_workflow(connection:, conversation:) + # If there is a cached access token, try to authenticate with it. If + # authentication fails with an Authentication error (18), + # invalidate the access token, fetch a new access token, and try + # to authenticate again. + # If the server fails for any other reason, do not clear the cache. + if cache.access_token? + token = cache.access_token + msg = conversation.start(connection: connection, token: token) + begin + dispatch_msg(connection, conversation, msg) + rescue AuthError => error + cache.invalidate(token: token) + execute_workflow(connection: connection, conversation: conversation) + end + end + # This is the normal flow when no token is in the cache. Execute the + # machine callback to get the token, put it in the caches, and then + # send the saslStart to the server. + token = machine_workflow.execute + if token.nil? || !token[:access_token] + raise Error::OidcError, + "OIDC machine workflows must return a valid response with an access token but #{token} was returned" + end + cache.access_token = token[:access_token] + connection.access_token = token[:access_token] + msg = conversation.start(connection: connection, token: token[:access_token]) + dispatch_msg(connection, conversation, msg) + end + end + end +end + +require 'mongo/auth/oidc/conversation' +require 'mongo/auth/oidc/machine_workflow' +require 'mongo/auth/oidc/token_cache' diff --git a/lib/mongo/auth/oidc/conversation.rb b/lib/mongo/auth/oidc/conversation.rb new file mode 100644 index 0000000000..e9b73522e7 --- /dev/null +++ b/lib/mongo/auth/oidc/conversation.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true +# rubocop:todo all + +# Copyright (C) 2024 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. + +module Mongo + module Auth + class Oidc + # Defines behaviour around a single OIDC conversation between the + # client and the server. + # + # @api private + class Conversation < ConversationBase + # The base client message. + START_MESSAGE = { saslStart: 1, mechanism: Oidc::MECHANISM }.freeze + + # Create the new conversation. + # + # @example Create the new conversation. + # Conversation.new(user, 'test.example.com') + # + # @param [ Auth::User ] user The user to converse about. + # @param [ Mongo::Connection ] connection The connection to + # authenticate over. + # + # @since 2.20.0 + def initialize(user, connection, **opts) + super + end + + # OIDC machine workflow is always a saslStart with the payload being + # the serialized jwt token. + # + # @param [ String ] token The access token. + # + # @return [ Hash ] The start document. + def client_start_document(token:) + START_MESSAGE.merge(payload: finish_payload(token: token)) + end + + # Gets the serialized jwt payload for the token. + # + # @param [ String ] token The access token. + # + # @return [ BSON::Binary ] The serialized payload. + def finish_payload(token:) + payload = { jwt: token }.to_bson.to_s + BSON::Binary.new(payload) + end + + # Start the OIDC conversation. This returns the first message that + # needs to be sent to the server. + # + # @param [ Server::Connection ] connection The connection being authenticated. + # + # @return [ Protocol::Message ] The first OIDC conversation message. + def start(connection:, token:) + selector = client_start_document(token: token) + build_message(connection, '$external', selector) + end + end + end + end +end diff --git a/lib/mongo/auth/oidc/machine_workflow.rb b/lib/mongo/auth/oidc/machine_workflow.rb new file mode 100644 index 0000000000..7074686c9e --- /dev/null +++ b/lib/mongo/auth/oidc/machine_workflow.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true +# rubocop:todo all + +# Copyright (C) 2024 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. + +module Mongo + module Auth + class Oidc + # The machine callback workflow is a 1 step execution of the callback + # to get an OIDC token to connect with. + class MachineWorkflow + attr_reader :callback, :callback_lock, :last_executed, :username + + # The number of milliseconds to throttle the callback execution. + THROTTLE_MS = 100 + # The default timeout for callback execution. + TIMEOUT_MS = 60000 + # The current OIDC version. + OIDC_VERSION = 1 + + def initialize(auth_mech_properties: {}, username: nil) + @callback = CallbackFactory.get_callback(auth_mech_properties: auth_mech_properties) + @callback_lock = Mutex.new + @username = username + # Ensure the first execution happens immediately. + @last_executed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - THROTTLE_MS - 1 + end + + # Execute the machine callback. + def execute + # Aquire lock before executing the callback and throttle calling it + # to every 100ms. + callback_lock.synchronize do + difference = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - last_executed + if difference <= THROTTLE_MS + sleep(difference) + end + @last_executed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) + callback.execute(timeout: TIMEOUT_MS, version: OIDC_VERSION, username: username) + end + end + end + end + end +end + +require 'mongo/auth/oidc/machine_workflow/azure_callback' +require 'mongo/auth/oidc/machine_workflow/gcp_callback' +require 'mongo/auth/oidc/machine_workflow/test_callback' +require 'mongo/auth/oidc/machine_workflow/callback_factory' diff --git a/lib/mongo/auth/oidc/machine_workflow/azure_callback.rb b/lib/mongo/auth/oidc/machine_workflow/azure_callback.rb new file mode 100644 index 0000000000..b30d678914 --- /dev/null +++ b/lib/mongo/auth/oidc/machine_workflow/azure_callback.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true +# rubocop:todo all + +# Copyright (C) 2024 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. + +module Mongo + module Auth + class Oidc + class MachineWorkflow + class AzureCallback + # The base Azure endpoint + AZURE_BASE_URI = 'http://169.254.169.254/metadata/identity/oauth2/token' + # The Azure headers. + AZURE_HEADERS = { Metadata: 'true', Accept: 'application/json' }.freeze + + attr_reader :token_resource + + def initialize(auth_mech_properties: {}) + @token_resource = auth_mech_properties[:token_resource] + end + + # Hits the Azure endpoint in order to get the token. + # + # @params [ Integer ] timeout The timeout before cancelling. + # @params [ Integer ] version The OIDC version number. + # @params [ String ] username The optional username. + # + # @returns [ Hash ] A hash with the access token. + def execute(timeout:, version:, username: nil) + query = { resource: token_resource, 'api-version' => '2018-02-01' } + if username + query[:client_id] = username + end + uri = URI(AZURE_BASE_URI); + uri.query = ::URI.encode_www_form(query) + request = Net::HTTP::Get.new(uri, AZURE_HEADERS) + response = Timeout.timeout(timeout) do + Net::HTTP.start(uri.hostname, uri.port, use_ssl: false) do |http| + http.request(request) + end + end + if response.code != '200' + raise Error::OidcError, + "Azure metadata host responded with code #{response.code}" + end + result = JSON.parse(response.body) + { access_token: result['access_token'], expires_in: result['expires_in'] } + end + end + end + end + end +end diff --git a/lib/mongo/auth/oidc/machine_workflow/callback_factory.rb b/lib/mongo/auth/oidc/machine_workflow/callback_factory.rb new file mode 100644 index 0000000000..12818aa578 --- /dev/null +++ b/lib/mongo/auth/oidc/machine_workflow/callback_factory.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true +# rubocop:todo all + +# Copyright (C) 2024 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. + +module Mongo + module Auth + class Oidc + class MachineWorkflow + module CallbackFactory + # Map of environment name to the workflow callbacks. + CALLBACKS = { + 'azure' => AzureCallback, + 'gcp' => GcpCallback, + 'test' => TestCallback + }.freeze + + # Gets the callback based on the auth mechanism properties. + # + # @params [ Hash ] auth_mech_properties The auth mech properties. + # + # @returns [ Callback ] The machine callback. + module_function def get_callback(auth_mech_properties: {}) + if auth_mech_properties[:oidc_callback] + auth_mech_properties[:oidc_callback] + else + callback = CALLBACKS[auth_mech_properties[:environment]] + if !callback + raise Error::OidcError, "No OIDC machine callback found for environment: #{auth_mech_properties[:environment]}" + end + callback.new(auth_mech_properties: auth_mech_properties) + end + end + end + end + end + end +end + diff --git a/lib/mongo/auth/oidc/machine_workflow/gcp_callback.rb b/lib/mongo/auth/oidc/machine_workflow/gcp_callback.rb new file mode 100644 index 0000000000..195950ef3e --- /dev/null +++ b/lib/mongo/auth/oidc/machine_workflow/gcp_callback.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +# rubocop:todo all + +# Copyright (C) 2024 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. + +module Mongo + module Auth + class Oidc + class MachineWorkflow + class GcpCallback + # The base GCP endpoint + GCP_BASE_URI = 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity'.freeze + # The GCP headers. + GCP_HEADERS = { 'Metadata-Flavor': 'Google' }.freeze + + attr_reader :token_resource + + # Initialize the Gcp callback. + # + # @params [ Hash ] auth_mech_properties The auth mech properties. + def initialize(auth_mech_properties: {}) + @token_resource = auth_mech_properties[:token_resource] + end + + # Hits the GCP endpoint in order to get the token. The token_resource will + # become the audience parameter in the URI. + # + # @params [ Integer ] timeout The timeout before cancelling. + # @params [ Integer ] version The OIDC version number. + # @params [ String ] username The optional username. + # + # @returns [ Hash ] A hash with the access token. + def execute(timeout:, version:, username: nil) + uri = URI(GCP_BASE_URI); + uri.query = ::URI.encode_www_form({ audience: token_resource }) + request = Net::HTTP::Get.new(uri, GCP_HEADERS) + response = Timeout.timeout(timeout) do + Net::HTTP.start(uri.hostname, uri.port, use_ssl: false) do |http| + http.request(request) + end + end + if response.code != '200' + raise Error::OidcError, + "GCP metadata host responded with code #{response.code}" + end + { access_token: response.body } + end + end + end + end + end +end diff --git a/lib/mongo/auth/oidc/machine_workflow/test_callback.rb b/lib/mongo/auth/oidc/machine_workflow/test_callback.rb new file mode 100644 index 0000000000..944c2b05f6 --- /dev/null +++ b/lib/mongo/auth/oidc/machine_workflow/test_callback.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +# rubocop:todo all + +# Copyright (C) 2024 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. + +module Mongo + module Auth + class Oidc + class MachineWorkflow + class TestCallback + # We don't need to do anything with the auth mech properties + # passed in here. + def initialize(auth_mech_properties: {}) + end + + # Loads the token from the filesystem based on the OIDC_TOKEN_FILE + # environment variable. + # + # @params [ Integer ] timeout The timeout before cancelling. + # @params [ Integer ] version The OIDC version number. + # @params [ String ] username The optional username. + # + # @returns [ Hash ] The access token. + def execute(timeout:, version:, username: nil) + Timeout.timeout(timeout) do + location = ENV.fetch('OIDC_TOKEN_FILE') + token = File.read(location) + { access_token: token } + end + end + end + end + end + end +end diff --git a/lib/mongo/auth/oidc/token_cache.rb b/lib/mongo/auth/oidc/token_cache.rb new file mode 100644 index 0000000000..238a8e1fde --- /dev/null +++ b/lib/mongo/auth/oidc/token_cache.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true +# rubocop:todo all + +# Copyright (C) 2024 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. + +module Mongo + module Auth + class Oidc + # Represents a cache of the OIDC access token. + class TokenCache + attr_accessor :access_token + attr_reader :lock + + def initialize + @lock = Mutex.new + end + + # Is there an access token present in the cache? + # + # @returns [ Boolean ] True if present, false if not. + def access_token? + !!@access_token + end + + # Invalidate the token. Will only invalidate if the token + # matches the existing one and only one thread at a time + # may invalidate the token. + # + # @params [ String ] token The access token to invalidate. + def invalidate(token:) + lock.synchronize do + if (access_token == token) + @access_token = nil + end + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/mongo/auth/user.rb b/lib/mongo/auth/user.rb index 3c0df251f2..ac36a87717 100644 --- a/lib/mongo/auth/user.rb +++ b/lib/mongo/auth/user.rb @@ -212,7 +212,7 @@ def spec # @api private def self.default_auth_source(options) case options[:auth_mech] - when :aws, :gssapi, :mongodb_x509 + when :aws, :gssapi, :mongodb_x509, :mongodb_oidc '$external' when :plain options[:database] || '$external' diff --git a/lib/mongo/client.rb b/lib/mongo/client.rb index e06231d4ad..739f47c57c 100644 --- a/lib/mongo/client.rb +++ b/lib/mongo/client.rb @@ -526,6 +526,7 @@ def initialize(addresses_or_uri, options = nil) @srv_records = nil end + options = self.class.canonicalize_ruby_options(options) # The server API version is specified to be a string. @@ -1515,23 +1516,57 @@ def validate_authentication_options! raise Mongo::Auth::InvalidMechanism.new(auth_mech) end - if user.nil? && !%i(aws mongodb_x509).include?(auth_mech) + if user.nil? && !%i(aws mongodb_x509 mongodb_oidc).include?(auth_mech) raise Mongo::Auth::InvalidConfiguration, "Username is required for auth mechanism #{auth_mech}" end - if password.nil? && !%i(aws gssapi mongodb_x509).include?(auth_mech) + if password.nil? && !%i(aws gssapi mongodb_oidc mongodb_x509).include?(auth_mech) raise Mongo::Auth::InvalidConfiguration, "Password is required for auth mechanism #{auth_mech}" end - if password && auth_mech == :mongodb_x509 - raise Mongo::Auth::InvalidConfiguration, 'Password is not supported for :mongodb_x509 auth mechanism' + if password && (auth_mech == :mongodb_x509 || auth_mech == :mongodb_oidc) + raise Mongo::Auth::InvalidConfiguration, "Password is not supported for #{auth_mech} auth mechanism" end if auth_mech == :aws && user && !password raise Mongo::Auth::InvalidConfiguration, 'Username is provided but password is not provided for :aws auth mechanism' end - if %i(aws gssapi mongodb_x509).include?(auth_mech) + if auth_mech == :mongodb_oidc + if mech_properties + if mech_properties[:environment].nil? && mech_properties[:oidc_callback].nil? + raise Mongo::Auth::InvalidConfiguration, 'An OIDC callback or environment must be provided in the auth mechanism properties for OIDC authentication.' + end + + if %w(azure gcp).include?(mech_properties[:environment]) && mech_properties[:token_resource].nil? + raise Mongo::Auth::InvalidConfiguration, "Token resource is required when using OIDC machine workflow: #{mech_properties[:environment]}" + end + + if mech_properties[:environment] && mech_properties[:oidc_callback] + raise Mongo::Auth::InvalidConfiguration, 'Cannot supply both an oidc callback and environment for OIDC authentication.' + end + + mech_properties.each_pair do |property, value| + if !%w(oidc_callback environment token_resource).include?(property) + raise Mongo::Auth::InvalidConfiguration, "Auth mechanism property #{property} is not supported with OIDC authentication." + end + + if property === 'environment' + if !%w(azure gcp test).include?(value) + raise Mongo::Auth::InvalidConfiguration, "Auth mechanism property #{property} must be one of azure or gcp, got #{value}" + end + + if value === 'test' && user + raise Mongo::Auth::InvalidConfiguration, 'Username cannot be provided for test machine workflow with OIDC authentication.' + end + end + end + else + raise Mongo::Auth::InvalidConfiguration, 'An OIDC callback or environment must be provided in the auth mechanism properties for OIDC authentication.' + end + end + + if %i(aws gssapi mongodb_x509 mongodb_oidc).include?(auth_mech) if !['$external', nil].include?(auth_source) raise Mongo::Auth::InvalidConfiguration, "#{auth_source} is an invalid auth source for #{auth_mech}; valid options are $external and nil" end @@ -1542,7 +1577,7 @@ def validate_authentication_options! end end - if mech_properties && !%i(aws gssapi).include?(auth_mech) + if mech_properties && !%i(aws gssapi mongodb_oidc).include?(auth_mech) raise Mongo::Auth::InvalidConfiguration, ":mechanism_properties are not supported for auth mechanism #{auth_mech}" end end diff --git a/lib/mongo/error.rb b/lib/mongo/error.rb index 92d6d5f4b3..741774c91a 100644 --- a/lib/mongo/error.rb +++ b/lib/mongo/error.rb @@ -190,6 +190,7 @@ def write_concern_error_labels require 'mongo/error/no_service_connection_available' require 'mongo/error/no_server_available' require 'mongo/error/no_srv_records' +require 'mongo/error/oidc_error' require 'mongo/error/session_ended' require 'mongo/error/sessions_not_supported' require 'mongo/error/session_not_materialized' diff --git a/lib/mongo/error/oidc_error.rb b/lib/mongo/error/oidc_error.rb new file mode 100644 index 0000000000..d502456eec --- /dev/null +++ b/lib/mongo/error/oidc_error.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +# rubocop:todo all + +# Copyright (C) 2024 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. + +module Mongo + class Error + class OidcError < Error + end + end +end diff --git a/lib/mongo/server/connection_common.rb b/lib/mongo/server/connection_common.rb index 82bc91d75e..74c1d35c11 100644 --- a/lib/mongo/server/connection_common.rb +++ b/lib/mongo/server/connection_common.rb @@ -36,6 +36,11 @@ class ConnectionCommon # @return [ String | nil ] The compressor. attr_reader :compressor + # The access token associated with the connection. Will be nil + # until the authentication workflow has completed. + # @api private + attr_accessor :access_token + # Determine if the connection is currently connected. # # @example Is the connection connected? diff --git a/lib/mongo/server/pending_connection.rb b/lib/mongo/server/pending_connection.rb index 1981e32812..50888c6cb6 100644 --- a/lib/mongo/server/pending_connection.rb +++ b/lib/mongo/server/pending_connection.rb @@ -182,6 +182,7 @@ def handshake!(speculative_auth_doc: nil) # @param [ BSON::Document | nil ] speculative_auth_result The # value of speculativeAuthenticate field of hello response of # the handshake on this connection. + # @todo: durran: auth providers must sit on the cluster. def authenticate!( speculative_auth_client_nonce: nil, speculative_auth_mech: nil, diff --git a/lib/mongo/uri.rb b/lib/mongo/uri.rb index 0b891a858c..e83f70ccf5 100644 --- a/lib/mongo/uri.rb +++ b/lib/mongo/uri.rb @@ -199,6 +199,7 @@ class URI # MONGODB-CR is deprecated and will be removed in driver version 3.0 'MONGODB-CR' => :mongodb_cr, 'MONGODB-X509' => :mongodb_x509, + 'MONGODB-OIDC' => :mongodb_oidc, 'PLAIN' => :plain, 'SCRAM-SHA-1' => :scram, 'SCRAM-SHA-256' => :scram256, diff --git a/lib/mongo/uri/options_mapper.rb b/lib/mongo/uri/options_mapper.rb index bc7e8f16dc..9d4a509f96 100644 --- a/lib/mongo/uri/options_mapper.rb +++ b/lib/mongo/uri/options_mapper.rb @@ -848,7 +848,9 @@ def stringify_zlib_compression_level(value) def hash_extractor(name, value) h = {} value.split(',').each do |tag| - k, v = tag.split(':') + # Auth mech properties can have a : in them with the introduction of OIDC, + # so ensure here to split only into 2 strings. + k, v = tag.split(':', 2) if v.nil? log_warn("Invalid hash value for #{name}: key `#{k}` does not have a value: #{value}") next diff --git a/lib/mongo/uri/srv_protocol.rb b/lib/mongo/uri/srv_protocol.rb index 164bbf4210..fbd68cfbd1 100644 --- a/lib/mongo/uri/srv_protocol.rb +++ b/lib/mongo/uri/srv_protocol.rb @@ -50,6 +50,14 @@ class SRVProtocol < URI # @since 2.5.0 def client_options opts = @txt_options.merge(ssl: true) + # There is a special case with MONGODB-OIDC where the TXT records for each host can set + # an authSource of admin because SCRAM is also enabled on the Atlas cluster to handle + # cluster admin. We want to ensure in this case that if the URI option for auth mech + # is MONGODB-OIDC that the auth source is not getting overriden to be admin and we allow + # the proper default of $external later in the mongo client constructor. + if uri_options['auth_mech'] == :mongodb_oidc + opts.delete(:auth_source) + end opts = opts.merge(uri_options).merge(:database => database) @user ? opts.merge(credentials) : opts end diff --git a/spec/integration/oidc/azure_machine_auth_flow_prose_spec.rb b/spec/integration/oidc/azure_machine_auth_flow_prose_spec.rb new file mode 100644 index 0000000000..25e8d216f9 --- /dev/null +++ b/spec/integration/oidc/azure_machine_auth_flow_prose_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true +# rubocop:todo all + +require 'spec_helper' + +describe 'OIDC Authentication Prose Tests' do + require_oidc 'azure' + + # Note that MONGODB_URI_SINGLE in the environment contains a valid Azure URI + # with the correct ENVIRONMENT and TOKEN_RESOURCE auth mech properties. This + # is populated by the drivers tools scripts that get the drivers/azureoidc + # secrets from the AWS secrets manager. + describe 'Azure Machine Authentication Flow Prose Tests' do + # 5.1 Azure With No Username + context 'when no username is provided' do + let(:client) do + Mongo::Client.new(ENV.fetch('MONGODB_URI_SINGLE'), database: 'test') + end + + let(:collection) do + client['test'] + end + + after(:each) do + client.close + end + + it 'successfully authenticates' do + expect(collection.find.to_a).to_not be_empty + end + end + + # 5.2 Azure With Bad Username + context 'when a bad username is provided' do + let(:client) do + Mongo::Client.new(ENV.fetch('MONGODB_URI_SINGLE'), database: 'test', user: 'bad') + end + + let(:collection) do + client['test'] + end + + after(:each) do + client.close + end + + it 'fails authentication' do + expect { + collection.find.to_a + }.to raise_error(Mongo::Error::OidcError) + end + end + + # No prose test in spec for this but is a valid test case. + context 'when a valid username is provided' do + let(:client) do + Mongo::Client.new(ENV.fetch('MONGODB_URI_SINGLE'), + database: 'test', + user: ENV.fetch('AZUREOIDC_USERNAME') + ) + end + + let(:collection) do + client['test'] + end + + after(:each) do + client.close + end + + it 'successfully authenticates' do + expect(collection.find.to_a).to_not be_empty + end + end + end +end diff --git a/spec/integration/oidc/gcp_machine_auth_flow_prose_spec.rb b/spec/integration/oidc/gcp_machine_auth_flow_prose_spec.rb new file mode 100644 index 0000000000..e5a84d304d --- /dev/null +++ b/spec/integration/oidc/gcp_machine_auth_flow_prose_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true +# rubocop:todo all + +require 'spec_helper' + +describe 'OIDC Authentication Prose Tests' do + require_oidc 'gcp' + + # Note that MONGODB_URI_SINGLE in the environment contains a valid GCP URI + # with the correct ENVIRONMENT and TOKEN_RESOURCE auth mech properties. This + # is populated by the drivers tools scripts that get the drivers/gcpoidc + # secrets from the AWS secrets manager. + describe 'GCP Machine Authentication Flow Prose Tests' do + # No prose tests in the spec for GCP, testing the two cases + context 'when the token resource is valid' do + let(:client) do + Mongo::Client.new(ENV.fetch('MONGODB_URI_SINGLE'), database: 'test') + end + + let(:collection) do + client['test'] + end + + after(:each) do + client.close + end + + it 'successfully authenticates' do + expect(collection.find.to_a).to_not be_empty + end + end + + context 'when the token resource is invalid' do + let(:client) do + Mongo::Client.new(ENV.fetch('MONGODB_URI_SINGLE'), + database: 'test', + auth_mech_properties: { + environment: 'gcp', + token_resource: 'bad' + } + ) + end + + let(:collection) do + client['test'] + end + + after(:each) do + client.close + end + + it 'fails authentication' do + expect { + collection.find.to_a + }.to raise_error(Mongo::Auth::Unauthorized) + end + end + end +end diff --git a/spec/integration/oidc/test_machine_auth_flow_prose_spec.rb b/spec/integration/oidc/test_machine_auth_flow_prose_spec.rb new file mode 100644 index 0000000000..4dae4f268f --- /dev/null +++ b/spec/integration/oidc/test_machine_auth_flow_prose_spec.rb @@ -0,0 +1,336 @@ +# frozen_string_literal: true +# rubocop:todo all + +require 'spec_helper' + +# Generic test callback that pulls tokens from the env. +class TestCallback + attr_reader :token_file + + def initialize(token_file: 'test_user1') + @token_file = token_file + end + + def execute(timeout:, version:, username: nil) + location = File.join(ENV.fetch('OIDC_TOKEN_DIR'), token_file) + token = File.read(location) + { access_token: token } + end +end + +describe 'OIDC Authentication Prose Tests' do + require_oidc 'test' + + describe 'Machine Authentication Flow Prose Tests' do + let(:uri) do + ENV.fetch('MONGODB_URI_SINGLE') + end + + context 'when using callback authentication' do + # 1.1 Callback is called during authentication + context 'when executing an operation' do + let(:callback) do + TestCallback.new(token_file: 'test_machine') + end + + let(:client) do + Mongo::Client.new(uri, + database: 'test', + retry_reads: false, + auth_mech_properties: { + oidc_callback: callback + } + ) + end + + let(:collection) do + client['test'] + end + + before(:each) do + allow(callback).to receive(:execute).and_call_original + end + + after(:each) do + client.close + end + + it 'successfully authenticates' do + # Create an OIDC configured client. + # Perform a find operation that succeeds. + # Assert that the callback was called 1 time. + # Close the client. + expect(collection.find.to_a).to be_empty + expect(callback).to have_received(:execute).once + end + end + + # 1.2 Callback is called once for multiple connections + context 'when using multiple connections' do + let!(:callback) do + TestCallback.new(token_file: 'test_machine') + end + + let!(:client) do + Mongo::Client.new(uri, + database: 'test', + retry_reads: false, + auth_mech_properties: { + oidc_callback: callback + } + ) + end + + let!(:collection) do + client['test'] + end + + before(:each) do + allow(callback).to receive(:execute).and_call_original + end + + after(:each) do + client.close + end + + it 'only calls the callback once for each thread' do + # Start 10 threads and run 100 find operations in each thread that all succeed. + # Assert that the callback was called 1 time for each thread. + threads = [] + 10.times do + threads << Thread.new do + 100.times do + expect(collection.find.to_a).to be_empty + end + end + end + threads.each do |thread| + thread.join + end + expect(callback).to have_received(:execute).exactly(10).times + end + end + end + + context 'when validating callbacks' do + # 2.1 Valid Callback Inputs + context 'when callback inputs are valid' do + let(:callback) do + TestCallback.new(token_file: 'test_machine') + end + + let(:client) do + Mongo::Client.new(uri, + database: 'test', + retry_reads: false, + auth_mech_properties: { + oidc_callback: callback + } + ) + end + + let(:collection) do + client['test'] + end + + before(:each) do + allow(callback).to receive(:execute).and_call_original + end + + after(:each) do + client.close + end + + it 'successfully authenticates' do + # Create an OIDC configured client with an OIDC callback that validates its inputs and returns a valid access token. + # Perform a find operation that succeeds. + # Assert that the OIDC callback was called with the appropriate inputs, including the timeout parameter if possible. + # Close the client. + expect(collection.find.to_a).to be_empty + expect(callback).to have_received(:execute).with(timeout: 60000, version: 1, username: nil).once + end + end + + # 2.2 OIDC Callback Returns Null + context 'when the callback returns null' do + let(:callback) do + TestCallback.new() + end + + let(:client) do + Mongo::Client.new(uri, + database: 'test', + retry_reads: false, + auth_mech_properties: { + oidc_callback: callback + } + ) + end + + let(:collection) do + client['test'] + end + + before(:each) do + allow(callback).to receive(:execute).and_return(nil) + end + + after(:each) do + client.close + end + + it 'fails authentication' do + # Create an OIDC configured client with an OIDC callback that returns null. + # Perform a find operation that fails. + # Close the client. + expect { + collection.find.to_a + }.to raise_error(Mongo::Error::OidcError) + end + end + + # 2.3 OIDC Callback Returns Missing Data + context 'when the callback returns missing data' do + let(:callback) do + TestCallback.new() + end + + let(:client) do + Mongo::Client.new(uri, + database: 'test', + retry_reads: false, + auth_mech_properties: { + oidc_callback: callback + } + ) + end + + let(:collection) do + client['test'] + end + + before(:each) do + allow(callback).to receive(:execute).and_return({ field: 'value' }) + end + + after(:each) do + client.close + end + + it 'fails authentication' do + # Create an OIDC configured client with an OIDC callback that returns data not conforming to the OIDCCredential with missing fields. + # Perform a find operation that fails. + # Close the client. + expect { + collection.find.to_a + }.to raise_error(Mongo::Error::OidcError) + end + end + + # 2.4 Invalid Client Configuration with Callback + context 'when the client is misconfigured' do + let(:callback) do + TestCallback.new() + end + + it 'fails on client configuration' do + # Create an OIDC configured client with an OIDC callback and auth mechanism property ENVIRONMENT:test. + # Assert it returns a client configuration error. + expect { + Mongo::Client.new(uri, + database: 'test', + retry_reads: false, + auth_mech_properties: { + oidc_callback: callback, + environment: 'test' + } + ) + }.to raise_error(Mongo::Auth::InvalidConfiguration) + end + end + + # 2.5 Invalid use of ALLOWED_HOSTS + context 'when allowed hosts are misconfigured' do + let(:callback) do + TestCallback.new() + end + + it 'fails on client configuration' do + # Create an OIDC configured client with auth mechanism properties {"ENVIRONMENT": "azure", "ALLOWED_HOSTS": []}. + # Assert it returns a client configuration error upon client creation, or client connect if your driver validates on connection. + expect { + Mongo::Client.new(uri, + database: 'test', + retry_reads: false, + auth_mech_properties: { + environment: 'azure', + allowed_hosts: [] + } + ) + }.to raise_error(Mongo::Auth::InvalidConfiguration) + end + end + end + + context 'when authentication fails' do + # 3.1 Authentication failure with cached tokens fetch a new token and retry auth + context 'when tokens are cached' do + let(:callback) do + TestCallback.new() + end + + let(:client) do + Mongo::Client.new(uri, + database: 'test', + retry_reads: false, + auth_mech_properties: { + oidc_callback: callback + } + ) + end + + let(:collection) do + client['test'] + end + + let(:cache) do + end + + before(:each) do + end + + # Create an OIDC configured client. + # Poison the Client Cache with an invalid access token. + # Perform a find operation that succeeds. + # Assert that the callback was called 1 time. + # Close the client. + it 'successfully authenticates' do + end + end + + # 3.2 Authentication failures without cached tokens return an error + context 'when no tokens are cached' do + end + + # 3.3 Unexpected error code does not clear the cache + context 'when error code is unexpected' do + end + end + + context 'when reauthenticating' do + # 4.1 Reauthentication Succeeds + context 'when reauthentication succeeds' do + end + + context 'when reauthentication fails' do + # 4.2 Read Commands Fail If Reauthentication Fails + context 'when executing a read' do + end + + # 4.3 Write Commands Fail If Reauthentication Fails + context 'when executing a write' do + end + end + end + end +end diff --git a/spec/lite_spec_helper.rb b/spec/lite_spec_helper.rb index 7f7862296c..725bfb6e47 100644 --- a/spec/lite_spec_helper.rb +++ b/spec/lite_spec_helper.rb @@ -154,6 +154,13 @@ def require_atlas end end + def require_oidc(environment) + env = ENV['ENVIRONMENT'] + before do + skip "Set ENVIRONMENT in the environment to #{environment} to run the OIDC #{environment} tests" if env != environment + end + end + if SpecConfig.instance.ci? SdamFormatterIntegration.subscribe config.add_formatter(JsonExtFormatter, File.join(File.dirname(__FILE__), '../tmp/rspec.json')) diff --git a/spec/mongo/auth/invalid_mechanism_spec.rb b/spec/mongo/auth/invalid_mechanism_spec.rb index ca82146b02..01706fdd3d 100644 --- a/spec/mongo/auth/invalid_mechanism_spec.rb +++ b/spec/mongo/auth/invalid_mechanism_spec.rb @@ -8,7 +8,7 @@ let(:exception) { described_class.new(:foo) } it 'includes all built in mechanisms' do - expect(exception.message).to eq(':foo is invalid, please use one of the following mechanisms: :aws, :gssapi, :mongodb_cr, :mongodb_x509, :plain, :scram, :scram256') + expect(exception.message).to eq(':foo is invalid, please use one of the following mechanisms: :aws, :gssapi, :mongodb_cr, :mongodb_oidc, :mongodb_x509, :plain, :scram, :scram256') end end end diff --git a/spec/mongo/auth/oidc/conversation_spec.rb b/spec/mongo/auth/oidc/conversation_spec.rb new file mode 100644 index 0000000000..a2d667ea29 --- /dev/null +++ b/spec/mongo/auth/oidc/conversation_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true +# rubocop:todo all + +require 'spec_helper' + +describe Mongo::Auth::Oidc::Conversation do + let(:user) do + Mongo::Auth::User.new(user: 'test') + end + + let(:connection) do + double('connection') + end + + let(:conversation) do + described_class.new(user, connection) + end + + let(:features) do + double('features') + end + + describe '#start' do + before do + expect(connection).to receive(:features).and_return(features) + expect(connection).to receive(:mongos?).and_return(false) + expect(features).to receive(:op_msg_enabled?).and_return(true) + end + + let(:token) do + 'token' + end + + let(:msg) do + conversation.start(connection: connection, token: token) + end + + let(:selector) do + msg.instance_variable_get(:@main_document) + end + + it 'sets the sasl start flag' do + expect(selector[:saslStart]).to eq(1) + end + + it 'sets the mechanism' do + expect(selector[:mechanism]).to eq('MONGODB-OIDC') + end + + it 'sets the payload' do + expect(selector[:payload].data).to eq({ jwt: token }.to_bson.to_s) + end + end +end diff --git a/spec/mongo/auth/oidc/machine_workflow/azure_callback_spec.rb b/spec/mongo/auth/oidc/machine_workflow/azure_callback_spec.rb new file mode 100644 index 0000000000..454996b7cc --- /dev/null +++ b/spec/mongo/auth/oidc/machine_workflow/azure_callback_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true +# rubocop:todo all + +require 'spec_helper' + +describe Mongo::Auth::Oidc::MachineWorkflow::AzureCallback do + + let(:properties) do + { token_resource: 'audience' } + end + + let(:callback) do + described_class.new(auth_mech_properties: properties) + end + + describe '#execute' do + context 'when the response is a 200' do + let(:response) do + double('response') + end + + before do + expect(response).to receive(:code).and_return('200') + expect(response).to receive(:body).and_return('{ "access_token": "token", "expires_in": 500 }') + allow(Net::HTTP).to receive(:start).with('169.254.169.254', 80, use_ssl: false).and_return(response) + end + + let(:result) do + callback.execute(timeout: 50, version: 1) + end + + it 'returns the token' do + expect(result[:access_token]).to eq('token') + end + end + + context 'when the response is not a 200' do + let(:response) do + double('response') + end + + before do + expect(response).to receive(:code).twice.and_return('500') + allow(Net::HTTP).to receive(:start).with('169.254.169.254', 80, use_ssl: false).and_return(response) + end + + let(:result) do + end + + it 'raises an error' do + expect { + callback.execute(timeout: 50, version: 1) + }.to raise_error(Mongo::Error::OidcError) + end + end + end +end diff --git a/spec/mongo/auth/oidc/machine_workflow/callback_factory_spec.rb b/spec/mongo/auth/oidc/machine_workflow/callback_factory_spec.rb new file mode 100644 index 0000000000..e587eb5812 --- /dev/null +++ b/spec/mongo/auth/oidc/machine_workflow/callback_factory_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +# rubocop:todo all + +require 'spec_helper' + +describe Mongo::Auth::Oidc::MachineWorkflow::CallbackFactory do + describe '.get_callback' do + context 'when an OIDC_CALLBACK auth mech property is provided' do + class OidcCallback + def execute(params: {}) + { access_token: 'test' } + end + end + + let(:callback) do + described_class.get_callback(auth_mech_properties: { oidc_callback: OidcCallback.new }) + end + + it 'returns the user provided callback' do + expect(callback).to be_a OidcCallback + end + end + + context 'when an environment auth mech property is provided' do + context 'when the value is azure' do + let(:callback) do + described_class.get_callback(auth_mech_properties: { environment: 'azure', token_resource: 'resource' }) + end + + it 'returns the azure callback' do + expect(callback).to be_a Mongo::Auth::Oidc::MachineWorkflow::AzureCallback + end + end + + context 'when the valie is gcp' do + let(:callback) do + described_class.get_callback(auth_mech_properties: { environment: 'gcp', token_resource: 'client_id' }) + end + + it 'returns the gcp callback' do + expect(callback).to be_a Mongo::Auth::Oidc::MachineWorkflow::GcpCallback + end + end + + context 'when the value is test' do + let(:callback) do + described_class.get_callback(auth_mech_properties: { environment: 'test' }) + end + + it 'returns the test callback' do + expect(callback).to be_a Mongo::Auth::Oidc::MachineWorkflow::TestCallback + end + end + + context 'when the value is unknown' do + it 'raises an oidc error' do + expect { + described_class.get_callback(auth_mech_properties: { environment: 'nothing' }) + }.to raise_error(Mongo::Error::OidcError) + end + end + end + end +end diff --git a/spec/mongo/auth/oidc/machine_workflow/gcp_callback_spec.rb b/spec/mongo/auth/oidc/machine_workflow/gcp_callback_spec.rb new file mode 100644 index 0000000000..2642837fae --- /dev/null +++ b/spec/mongo/auth/oidc/machine_workflow/gcp_callback_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +# rubocop:todo all + +require 'spec_helper' + +describe Mongo::Auth::Oidc::MachineWorkflow::GcpCallback do + let(:properties) do + { token_resource: 'audience' } + end + + let(:callback) do + described_class.new(auth_mech_properties: properties) + end + + describe '#execute' do + context 'when the response is a 200' do + let(:response) do + double('response') + end + + before do + expect(response).to receive(:code).and_return('200') + expect(response).to receive(:body).and_return('token') + allow(Net::HTTP).to receive(:start).with('metadata', 80, use_ssl: false).and_return(response) + end + + let(:result) do + callback.execute(timeout: 50, version: 1) + end + + it 'returns the token' do + expect(result[:access_token]).to eq('token') + end + end + + context 'when the response is not a 200' do + let(:response) do + double('response') + end + + before do + expect(response).to receive(:code).twice.and_return('500') + allow(Net::HTTP).to receive(:start).with('metadata', 80, use_ssl: false).and_return(response) + end + + let(:result) do + end + + it 'raises an error' do + expect { + callback.execute(timeout: 50, version: 1) + }.to raise_error(Mongo::Error::OidcError) + end + end + end +end diff --git a/spec/mongo/auth/oidc/machine_workflow/test_callback_spec.rb b/spec/mongo/auth/oidc/machine_workflow/test_callback_spec.rb new file mode 100644 index 0000000000..caddbc0021 --- /dev/null +++ b/spec/mongo/auth/oidc/machine_workflow/test_callback_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +# rubocop:todo all + +require 'spec_helper' + +describe Mongo::Auth::Oidc::MachineWorkflow::TestCallback do + let(:callback) do + described_class.new() + end + + describe '#execute' do + context 'when a token path is provided' do + let(:path) do + '/path/to/file' + end + + before do + allow(ENV).to receive(:fetch).with('OIDC_TOKEN_FILE').and_return(path) + allow(File).to receive(:read).with(path).and_return('token') + end + + let(:result) do + callback.execute(timeout: 50, version: 1) + end + + it 'returns the token' do + expect(result[:access_token]).to eq('token') + end + end + + context 'when a token path is not provided' do + it 'raises an error' do + expect { + callback.execute(timeout: 50, version: 1) + }.to raise_error(KeyError) + end + end + end +end diff --git a/spec/mongo/auth/oidc/machine_workflow_spec.rb b/spec/mongo/auth/oidc/machine_workflow_spec.rb new file mode 100644 index 0000000000..2f973aad7c --- /dev/null +++ b/spec/mongo/auth/oidc/machine_workflow_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true +# rubocop:todo all + +require 'spec_helper' + +describe Mongo::Auth::Oidc::MachineWorkflow do + let(:callback) do + double('callback') + end + + let(:properties) do + { oidc_callback: callback } + end + + describe '#start' do + context 'when executing for the first time' do + let(:workflow) do + described_class.new(auth_mech_properties: properties) + end + + let(:token) do + 'token' + end + + before do + expect(callback).to receive(:execute).with( + timeout: 60000, + version: 1, + username: nil + ).and_return({ access_token: token }) + end + + let(:result) do + workflow.execute + end + + it 'returns the token result' do + expect(result).to eq({ access_token: token }) + end + end + + context 'when executing multiple times in succession' do + let!(:workflow) do + described_class.new(auth_mech_properties: properties) + end + + let(:token) do + 'token' + end + + let!(:time) do + Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) + end + + before do + expect(callback).to receive(:execute).exactly(10).times.and_return({ access_token: token }) + end + + it 'throttles the execution at 100ms' do + 10.times do + workflow.execute + end + current_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) + # TODO: Best way to test throttling? + end + end + end +end diff --git a/spec/mongo/auth/oidc/token_cache_spec.rb b/spec/mongo/auth/oidc/token_cache_spec.rb new file mode 100644 index 0000000000..45e4904782 --- /dev/null +++ b/spec/mongo/auth/oidc/token_cache_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +# rubocop:todo all + +require 'spec_helper' + +describe Mongo::Auth::Oidc::TokenCache do + let(:cache) do + described_class.new + end + + describe '#invalidate' do + let(:token_one) do + 'token_one' + end + + let(:token_two) do + 'token_two' + end + + context 'when the token matches the existing token' do + before do + cache.access_token = token_one + cache.invalidate(token: token_one) + end + + it 'invalidates the token' do + expect(cache.access_token).to be_nil + end + end + + context 'when the token does not equal the existing token' do + before do + cache.access_token = token_one + cache.invalidate(token: token_two) + end + + it 'does not invalidate the token' do + expect(cache.access_token).to eq(token_one) + end + end + end +end diff --git a/spec/mongo/auth/user_spec.rb b/spec/mongo/auth/user_spec.rb index 5ccc1d3584..cd0ee730ee 100644 --- a/spec/mongo/auth/user_spec.rb +++ b/spec/mongo/auth/user_spec.rb @@ -50,7 +50,7 @@ it 'raises ArgumentError' do expect do user - end.to raise_error(Mongo::Auth::InvalidMechanism, ":invalid is invalid, please use one of the following mechanisms: :aws, :gssapi, :mongodb_cr, :mongodb_x509, :plain, :scram, :scram256") + end.to raise_error(Mongo::Auth::InvalidMechanism, ":invalid is invalid, please use one of the following mechanisms: :aws, :gssapi, :mongodb_cr, :mongodb_oidc, :mongodb_x509, :plain, :scram, :scram256") end end diff --git a/spec/mongo/auth_spec.rb b/spec/mongo/auth_spec.rb index a770aacdd6..67dc26b4ac 100644 --- a/spec/mongo/auth_spec.rb +++ b/spec/mongo/auth_spec.rb @@ -7,6 +7,21 @@ describe '#get' do + context 'when a mongodb_oidc user is provided' do + + let(:user) do + Mongo::Auth::User.new(auth_mech: :mongodb_oidc, auth_mech_properties: { environment: 'test' }) + end + + let(:oidc) do + described_class.get(user, double('connection')) + end + + it 'returns Oidc' do + expect(oidc).to be_a(Mongo::Auth::Oidc) + end + end + context 'when a mongodb_cr user is provided' do let(:user) do diff --git a/spec/spec_tests/data/auth/connection-string.yml b/spec/spec_tests/data/auth/connection-string.yml index 41dca8fabd..40aa5faf5a 100644 --- a/spec/spec_tests/data/auth/connection-string.yml +++ b/spec/spec_tests/data/auth/connection-string.yml @@ -1,366 +1,483 @@ +--- 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!@#$%^&*()_+" +- 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: 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 test (MONGODB-OIDC) + uri: mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=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 url-encoded TOKEN_RESOURCE (MONGODB-OIDC) + uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:mongodb%3A%2F%2Ftest-cluster + valid: true + credential: + username: user + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: azure + TOKEN_RESOURCE: 'mongodb://test-cluster' +- description: should accept an un-encoded TOKEN_RESOURCE (MONGODB-OIDC) + uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:mongodb://test-cluster + valid: true + credential: + username: user + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: azure + TOKEN_RESOURCE: 'mongodb://test-cluster' +- description: should handle a complicated url-encoded TOKEN_RESOURCE (MONGODB-OIDC) + uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:abcd%25ef%3Ag%26hi + valid: true + credential: + username: user + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: azure + TOKEN_RESOURCE: 'abcd%ef:g&hi' +- description: should url-encode a TOKEN_RESOURCE (MONGODB-OIDC) + uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:a$b + valid: true + credential: + username: user + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: azure + TOKEN_RESOURCE: a$b +- 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/spec/support/spec_config.rb b/spec/support/spec_config.rb index 34d6c6d478..54d50a60df 100644 --- a/spec/support/spec_config.rb +++ b/spec/support/spec_config.rb @@ -172,8 +172,14 @@ def serverless? !!ENV['SERVERLESS'] end + def oidc? + !!ENV['ENVIRONMENT'] + end + def kill_all_server_sessions? - !serverless? && # Serverless instances do not support killAllSessions command. + # Serverless instances do not support killAllSessions command and OIDC + # does not use SCRAM for the command and the tests do not need it. + !serverless? && !oidc? && ClusterConfig.instance.fcv_ish >= '3.6' end