PYTHON-3467 OIDC: Automatic token acquisition for Azure Identity Provider (#1443)

Co-authored-by: Jib <Jibzade@gmail.com>
This commit is contained in:
Steven Silvester 2024-02-02 09:53:48 -06:00 committed by GitHub
parent 78ccdcb2b3
commit 4bc2a482d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1806 additions and 618 deletions

View File

@ -1010,6 +1010,32 @@ task_groups:
tasks:
- testazurekms-task
- name: testazureoidc_task_group
setup_group:
- func: fetch source
- func: prepare resources
- func: fix absolute paths
- func: make files executable
- command: shell.exec
params:
shell: bash
script: |-
set -o errexit
${PREPARE_SHELL}
export AZUREOIDC_VMNAME_PREFIX="PYTHON_DRIVER"
$DRIVERS_TOOLS/.evergreen/auth_oidc/azure/create-and-setup-vm.sh
teardown_task:
- command: shell.exec
params:
shell: bash
script: |-
${PREPARE_SHELL}
$DRIVERS_TOOLS/.evergreen/auth_oidc/azure/delete-vm.sh
setup_group_can_fail_task: true
setup_group_timeout_secs: 1800
tasks:
- oidc-auth-test-azure-latest
- name: test_aws_lambda_task_group
setup_group:
- func: fetch source
@ -1978,6 +2004,22 @@ tasks:
- func: "run load-balancer"
- func: "run tests"
- name: "oidc-auth-test-azure-latest"
commands:
- command: shell.exec
params:
shell: bash
script: |-
set -o errexit
${PREPARE_SHELL}
cd src
git add .
git commit -m "add files"
export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-python-driver.tgz
git archive -o $AZUREOIDC_DRIVERS_TAR_FILE HEAD
export AZUREOIDC_TEST_CMD="source ./env.sh && export OIDC_PROVIDER_NAME=azure && ./.evergreen/run-mongodb-oidc-test.sh"
bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh
- name: "test-fips-standalone"
tags: ["fips"]
commands:
@ -3036,6 +3078,13 @@ buildvariants:
tasks:
- name: "oidc-auth-test-latest"
- name: testazureoidc-variant
display_name: "Azure OIDC"
run_on: ubuntu2004-small
tasks:
- name: testazureoidc_task_group
batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README
- matrix_name: "aws-auth-test"
matrix_spec:
platform: [ubuntu-20.04]

View File

@ -5,44 +5,69 @@ set -o errexit # Exit the script with error if any of the commands fail
echo "Running MONGODB-OIDC authentication tests"
# Make sure DRIVERS_TOOLS is set.
if [ -z "$DRIVERS_TOOLS" ]; then
echo "Must specify DRIVERS_TOOLS"
exit 1
fi
OIDC_PROVIDER_NAME=${OIDC_PROVIDER_NAME:-"aws"}
# Get the drivers secrets. Use an existing secrets file first.
if [ ! -f "./secrets-export.sh" ]; then
bash ${DRIVERS_TOOLS}/.evergreen/auth_aws/setup_secrets.sh drivers/oidc
fi
source ./secrets-export.sh
if [ $OIDC_PROVIDER_NAME == "aws" ]; then
# Make sure DRIVERS_TOOLS is set.
if [ -z "$DRIVERS_TOOLS" ]; then
echo "Must specify DRIVERS_TOOLS"
exit 1
fi
# # If the file did not have our creds, get them from the vault.
if [ -z "$OIDC_ATLAS_URI_SINGLE" ]; then
bash ${DRIVERS_TOOLS}/.evergreen/auth_aws/setup_secrets.sh drivers/oidc
# Get the drivers secrets. Use an existing secrets file first.
if [ ! -f "./secrets-export.sh" ]; then
bash ${DRIVERS_TOOLS}/.evergreen/auth_aws/setup_secrets.sh drivers/oidc
fi
source ./secrets-export.sh
fi
# Make the OIDC tokens.
set -x
pushd ${DRIVERS_TOOLS}/.evergreen/auth_oidc
. ./oidc_get_tokens.sh
popd
# # If the file did not have our creds, get them from the vault.
if [ -z "$OIDC_ATLAS_URI_SINGLE" ]; then
bash ${DRIVERS_TOOLS}/.evergreen/auth_aws/setup_secrets.sh drivers/oidc
source ./secrets-export.sh
fi
# Set up variables and run the test.
if [ -n "$LOCAL_OIDC_SERVER" ]; then
export MONGODB_URI=${MONGODB_URI:-"mongodb://localhost"}
export MONGODB_URI_SINGLE="${MONGODB_URI}/?authMechanism=MONGODB-OIDC"
export MONGODB_URI_MULTI="${MONGODB_URI}:27018/?authMechanism=MONGODB-OIDC&directConnection=true"
else
set +x # turn off xtrace for this portion
export MONGODB_URI="$OIDC_ATLAS_URI_SINGLE"
export MONGODB_URI_SINGLE="$OIDC_ATLAS_URI_SINGLE/?authMechanism=MONGODB-OIDC"
export MONGODB_URI_MULTI="$OIDC_ATLAS_URI_MULTI/?authMechanism=MONGODB-OIDC"
# Make the OIDC tokens.
set -x
pushd ${DRIVERS_TOOLS}/.evergreen/auth_oidc
. ./oidc_get_tokens.sh
popd
# Set up variables and run the test.
if [ -n "$LOCAL_OIDC_SERVER" ]; then
export MONGODB_URI=${MONGODB_URI:-"mongodb://localhost"}
export MONGODB_URI_SINGLE="${MONGODB_URI}/?authMechanism=MONGODB-OIDC"
export MONGODB_URI_MULTI="${MONGODB_URI}:27018/?authMechanism=MONGODB-OIDC&directConnection=true"
else
set +x # turn off xtrace for this portion
export MONGODB_URI="$OIDC_ATLAS_URI_SINGLE"
export MONGODB_URI_SINGLE="$OIDC_ATLAS_URI_SINGLE/?authMechanism=MONGODB-OIDC"
export MONGODB_URI_MULTI="$OIDC_ATLAS_URI_MULTI/?authMechanism=MONGODB-OIDC"
set -x
fi
export AWS_WEB_IDENTITY_TOKEN_FILE="$OIDC_TOKEN_DIR/test_user1"
export OIDC_ADMIN_USER=$OIDC_ALTAS_USER
export OIDC_ADMIN_PWD=$OIDC_ATLAS_PASSWORD
elif [ $OIDC_PROVIDER_NAME == "azure" ]; then
if [ -z "${AZUREOIDC_AUDIENCE}" ]; then
echo "Must specify an AZUREOIDC_AUDIENCE"
exit 1
fi
set +x # turn off xtrace for this portion
export OIDC_ADMIN_USER=$AZUREOIDC_USERNAME
export OIDC_ADMIN_PWD=pwd123
set -x
export MONGODB_URI=${MONGODB_URI:-"mongodb://localhost"}
MONGODB_URI_SINGLE="${MONGODB_URI}/?authMechanism=MONGODB-OIDC"
MONGODB_URI_SINGLE="${MONGODB_URI_SINGLE}&authMechanismProperties=PROVIDER_NAME:azure"
export MONGODB_URI_SINGLE="${MONGODB_URI_SINGLE},TOKEN_AUDIENCE:${AZUREOIDC_AUDIENCE}"
export MONGODB_URI_MULTI=$MONGODB_URI_SINGLE
else
echo "Unrecognized OIDC_PROVIDER_NAME $OIDC_PROVIDER_NAME"
exit 1
fi
export TEST_AUTH_OIDC=1
export COVERAGE=1
export AUTH="auth"
bash ./.evergreen/tox.sh -m test-eg
bash ./.evergreen/tox.sh -m test-eg -- "${@:1}"

View File

@ -30,7 +30,7 @@ set -o xtrace
AUTH=${AUTH:-noauth}
SSL=${SSL:-nossl}
TEST_ARGS="$1"
TEST_ARGS="${*:1}"
PYTHON=$(which python)
export PIP_QUIET=1 # Quiet by default
@ -50,8 +50,9 @@ if [ "$AUTH" != "noauth" ]; then
export DB_USER=$SERVERLESS_ATLAS_USER
export DB_PASSWORD=$SERVERLESS_ATLAS_PASSWORD
elif [ ! -z "$TEST_AUTH_OIDC" ]; then
export DB_USER=$OIDC_ALTAS_USER
export DB_PASSWORD=$OIDC_ATLAS_PASSWORD
export DB_USER=$OIDC_ADMIN_USER
export DB_PASSWORD=$OIDC_ADMIN_PWD
export DB_IP="$MONGODB_URI"
else
export DB_USER="bob"
export DB_PASSWORD="pwd123"
@ -205,7 +206,6 @@ fi
if [ -n "$TEST_AUTH_OIDC" ]; then
python -m pip install ".[aws]"
TEST_ARGS="test/auth_oidc/test_auth_oidc.py"
fi

56
pymongo/_azure_helpers.py Normal file
View File

@ -0,0 +1,56 @@
# Copyright 2023-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.
"""Azure helpers."""
from __future__ import annotations
import json
from typing import Any, Optional
from urllib.request import Request, urlopen
def _get_azure_response(
resource: str, object_id: Optional[str] = None, timeout: float = 5
) -> dict[str, Any]:
url = "http://169.254.169.254/metadata/identity/oauth2/token"
url += "?api-version=2018-02-01"
url += f"&resource={resource}"
if object_id:
url += f"&object_id={object_id}"
headers = {"Metadata": "true", "Accept": "application/json"}
request = Request(url, headers=headers) # noqa: S310
print("fetching url", url) # noqa: T201
try:
with urlopen(request, timeout=timeout) as response: # noqa: S310
status = response.status
body = response.read().decode("utf8")
except Exception as e:
msg = "Failed to acquire IMDS access token: %s" % e
raise ValueError(msg) from None
if status != 200:
msg = "Failed to acquire IMDS access token."
raise ValueError(msg)
try:
data = json.loads(body)
except Exception:
raise ValueError("Azure IMDS response must be in JSON format.") from None
for key in ["access_token", "expires_in"]:
if not data.get(key):
msg = "Azure IMDS response must contain %s, but was %s."
msg = msg % (key, body)
raise ValueError(msg)
return data

View File

@ -37,7 +37,13 @@ from urllib.parse import quote
from bson.binary import Binary
from pymongo.auth_aws import _authenticate_aws
from pymongo.auth_oidc import _authenticate_oidc, _get_authenticator, _OIDCProperties
from pymongo.auth_oidc import (
_authenticate_oidc,
_get_authenticator,
_OIDCAWSCallback,
_OIDCAzureCallback,
_OIDCProperties,
)
from pymongo.errors import ConfigurationError, OperationFailure
from pymongo.saslprep import saslprep
@ -162,8 +168,10 @@ def _build_credentials_tuple(
return MongoCredential(mech, "$external", user, passwd, aws_props, None)
elif mech == "MONGODB-OIDC":
properties = extra.get("authmechanismproperties", {})
request_token_callback = properties.get("request_token_callback")
provider_name = properties.get("PROVIDER_NAME", "")
callback = properties.get("OIDC_CALLBACK")
human_callback = properties.get("OIDC_HUMAN_CALLBACK")
provider_name = properties.get("PROVIDER_NAME")
token_audience = properties.get("TOKEN_AUDIENCE", "")
default_allowed = [
"*.mongodb.net",
"*.mongodb-dev.net",
@ -173,13 +181,40 @@ def _build_credentials_tuple(
"127.0.0.1",
"::1",
]
allowed_hosts = properties.get("allowed_hosts", default_allowed)
if not request_token_callback and provider_name != "aws":
raise ConfigurationError(
"authentication with MONGODB-OIDC requires providing an request_token_callback or a provider_name of 'aws'"
)
allowed_hosts = properties.get("ALLOWED_HOSTS", default_allowed)
msg = "authentication with MONGODB-OIDC requires providing either a callback or a provider_name"
if passwd is not None:
msg = "password is not supported by MONGODB-OIDC"
raise ConfigurationError(msg)
if callback or human_callback:
if provider_name is not None:
raise ConfigurationError(msg)
if callback and human_callback:
msg = "cannot set both OIDC_CALLBACK and OIDC_HUMAN_CALLBACK"
raise ConfigurationError(msg)
elif provider_name is not None:
if provider_name == "aws":
if user is not None:
msg = "AWS provider for MONGODB-OIDC does not support username"
raise ConfigurationError(msg)
callback = _OIDCAWSCallback()
elif provider_name == "azure":
passwd = None
if not token_audience:
raise ConfigurationError(
"Azure provider for MONGODB-OIDC requires a TOKEN_AUDIENCE auth mechanism property"
)
callback = _OIDCAzureCallback(token_audience, user)
else:
raise ConfigurationError(
f"unrecognized provider_name for MONGODB-OIDC: {provider_name}"
)
else:
raise ConfigurationError(msg)
oidc_props = _OIDCProperties(
request_token_callback=request_token_callback,
callback=callback,
human_callback=human_callback,
provider_name=provider_name,
allowed_hosts=allowed_hosts,
)
@ -522,6 +557,7 @@ _AUTH_MAP: Mapping[str, Callable[..., None]] = {
"MONGODB-CR": _authenticate_mongo_cr,
"MONGODB-X509": _authenticate_x509,
"MONGODB-AWS": _authenticate_aws,
"MONGODB-OIDC": _authenticate_oidc, # type:ignore[dict-item]
"PLAIN": _authenticate_plain,
"SCRAM-SHA-1": functools.partial(_authenticate_scram, mechanism="SCRAM-SHA-1"),
"SCRAM-SHA-256": functools.partial(_authenticate_scram, mechanism="SCRAM-SHA-256"),
@ -582,7 +618,7 @@ class _X509Context(_AuthContext):
class _OIDCContext(_AuthContext):
def speculate_command(self) -> Optional[MutableMapping[str, Any]]:
authenticator = _get_authenticator(self.credentials, self.address)
cmd = authenticator.auth_start_cmd(False)
cmd = authenticator.get_spec_auth_cmd()
if cmd is None:
return None
cmd["db"] = self.credentials.source

View File

@ -15,13 +15,17 @@
"""MONGODB-OIDC Authentication helpers."""
from __future__ import annotations
import abc
import os
import threading
import time
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Callable, Mapping, MutableMapping, Optional
from typing import TYPE_CHECKING, Any, Mapping, MutableMapping, Optional, Union
import bson
from bson.binary import Binary
from bson.son import SON
from pymongo._azure_helpers import _get_azure_response
from pymongo._csot import remaining
from pymongo.errors import ConfigurationError, OperationFailure
if TYPE_CHECKING:
@ -29,18 +33,51 @@ if TYPE_CHECKING:
from pymongo.pool import Connection
@dataclass
class OIDCIdPInfo:
issuer: str
clientId: str
requestScopes: Optional[list[str]] = field(default=None)
@dataclass
class OIDCCallbackContext:
timeout_seconds: float
version: int
refresh_token: Optional[str] = field(default=None)
idp_info: Optional[OIDCIdPInfo] = field(default=None)
@dataclass
class OIDCCallbackResult:
access_token: str
expires_in_seconds: Optional[float] = field(default=None)
refresh_token: Optional[str] = field(default=None)
class OIDCCallback(abc.ABC):
"""A base class for defining OIDC callbacks."""
@abc.abstractmethod
def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
"""Convert the given BSON value into our own type."""
@dataclass
class _OIDCProperties:
request_token_callback: Optional[Callable[..., dict]]
provider_name: Optional[str]
allowed_hosts: list[str]
callback: Optional[OIDCCallback] = field(default=None)
human_callback: Optional[OIDCCallback] = field(default=None)
provider_name: Optional[str] = field(default=None)
allowed_hosts: list[str] = field(default_factory=list)
"""Mechanism properties for MONGODB-OIDC authentication."""
TOKEN_BUFFER_MINUTES = 5
CALLBACK_TIMEOUT_SECONDS = 5 * 60
HUMAN_CALLBACK_TIMEOUT_SECONDS = 5 * 60
CALLBACK_VERSION = 1
MACHINE_CALLBACK_TIMEOUT_SECONDS = 60
TIME_BETWEEN_CALLS_SECONDS = 0.1
def _get_authenticator(
@ -72,28 +109,126 @@ def _get_authenticator(
return credentials.cache.data
class _OIDCAWSCallback(OIDCCallback):
def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
token_file = os.environ.get("AWS_WEB_IDENTITY_TOKEN_FILE")
if not token_file:
raise RuntimeError(
'MONGODB-OIDC with an "aws" provider requires "AWS_WEB_IDENTITY_TOKEN_FILE" to be set'
)
with open(token_file) as fid:
return OIDCCallbackResult(access_token=fid.read().strip())
class _OIDCAzureCallback(OIDCCallback):
def __init__(self, token_audience: str, username: Optional[str]) -> None:
self.token_audience = token_audience
self.username = username
def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
resp = _get_azure_response(self.token_audience, self.username, context.timeout_seconds)
return OIDCCallbackResult(
access_token=resp["access_token"], expires_in_seconds=resp["expires_in"]
)
@dataclass
class _OIDCAuthenticator:
username: str
properties: _OIDCProperties
refresh_token: Optional[str] = field(default=None)
access_token: Optional[str] = field(default=None)
idp_info: Optional[dict] = field(default=None)
idp_info: Optional[OIDCIdPInfo] = field(default=None)
token_gen_id: int = field(default=0)
lock: threading.Lock = field(default_factory=threading.Lock)
last_call_time: float = field(default=0)
def get_current_token(self, use_callback: bool = True) -> Optional[str]:
def reauthenticate(self, conn: Connection) -> Optional[Mapping[str, Any]]:
"""Handle a reauthenticate from the server."""
# Invalidate the token for the connection.
self._invalidate(conn)
# Call the appropriate auth logic for the callback type.
if self.properties.callback:
return self._authenticate_machine(conn)
return self._authenticate_human(conn)
def authenticate(self, conn: Connection) -> Optional[Mapping[str, Any]]:
"""Handle an initial authenticate request."""
# First handle speculative auth.
# If it succeeded, we are done.
ctx = conn.auth_ctx
if ctx and ctx.speculate_succeeded():
resp = ctx.speculative_authenticate
if resp and resp["done"]:
conn.oidc_token_gen_id = self.token_gen_id
return resp
# If spec auth failed, call the appropriate auth logic for the callback type.
# We cannot assume that the token is invalid, because a proxy may have been
# involved that stripped the speculative auth information.
if self.properties.callback:
return self._authenticate_machine(conn)
return self._authenticate_human(conn)
def get_spec_auth_cmd(self) -> Optional[MutableMapping[str, Any]]:
"""Get the appropriate speculative auth command."""
if not self.access_token:
return None
return self._get_start_command({"jwt": self.access_token})
def _authenticate_machine(self, conn: Connection) -> Mapping[str, Any]:
# If there is a cached access token, try to authenticate with it. If
# authentication fails, it's possible the cached access token is expired. In
# that case, invalidate the access token, fetch a new access token, and try
# to authenticate again.
if self.access_token:
try:
return self._sasl_start_jwt(conn)
except Exception: # noqa: S110
pass
return self._sasl_start_jwt(conn)
def _authenticate_human(self, conn: Connection) -> Optional[Mapping[str, Any]]:
# If we have a cached access token, try a JwtStepRequest.
if self.access_token:
try:
return self._sasl_start_jwt(conn)
except Exception: # noqa: S110
pass
# If we have a cached refresh token, try a JwtStepRequest with that.
if self.refresh_token:
try:
return self._sasl_start_jwt(conn)
except Exception: # noqa: S110
pass
# Start a new Two-Step SASL conversation.
# Run a PrincipalStepRequest to get the IdpInfo.
cmd = self._get_start_command(None)
start_resp = self._run_command(conn, cmd)
# Attempt to authenticate with a JwtStepRequest.
return self._sasl_continue_jwt(conn, start_resp)
def _get_access_token(self) -> Optional[str]:
properties = self.properties
cb: Union[None, OIDCCallback]
resp: OIDCCallbackResult
# TODO: DRIVERS-2672, handle machine callback here as well.
cb = properties.request_token_callback if use_callback else None
cb_type = "human"
is_human = properties.human_callback is not None
if is_human and self.idp_info is None:
return None
if properties.callback:
cb = properties.callback
if properties.human_callback:
cb = properties.human_callback
prev_token = self.access_token
if prev_token:
return prev_token
if not use_callback and not prev_token:
if cb is None and not prev_token:
return None
if not prev_token and cb is not None:
@ -104,163 +239,85 @@ class _OIDCAuthenticator:
if new_token != prev_token:
return new_token
# TODO: DRIVERS-2672 handle machine callback here.
if cb_type == "human":
context = {
"timeout_seconds": CALLBACK_TIMEOUT_SECONDS,
"version": CALLBACK_VERSION,
"refresh_token": self.refresh_token,
}
resp = cb(self.idp_info, context)
self.validate_request_token_response(resp)
# Ensure that we are waiting a min time between callback invocations.
delta = time.time() - self.last_call_time
if delta < TIME_BETWEEN_CALLS_SECONDS:
time.sleep(TIME_BETWEEN_CALLS_SECONDS - delta)
self.last_call_time = time.time()
if is_human:
timeout = HUMAN_CALLBACK_TIMEOUT_SECONDS
assert self.idp_info is not None
else:
timeout = int(remaining() or MACHINE_CALLBACK_TIMEOUT_SECONDS)
context = OIDCCallbackContext(
timeout_seconds=timeout,
version=CALLBACK_VERSION,
refresh_token=self.refresh_token,
idp_info=self.idp_info,
)
resp = cb.fetch(context)
if not isinstance(resp, OIDCCallbackResult):
raise ValueError("Callback result must be of type OIDCCallbackResult")
self.refresh_token = resp.refresh_token
self.access_token = resp.access_token
self.token_gen_id += 1
return self.access_token
def validate_request_token_response(self, resp: Mapping[str, Any]) -> None:
# Validate callback return value.
if not isinstance(resp, dict):
raise ValueError("OIDC callback returned invalid result")
if "access_token" not in resp:
raise ValueError("OIDC callback did not return an access_token")
expected = ["access_token", "refresh_token", "expires_in_seconds"]
for key in resp:
if key not in expected:
raise ValueError(f'Unexpected field in callback result "{key}"')
self.access_token = resp["access_token"]
self.refresh_token = resp.get("refresh_token")
def principal_step_cmd(self) -> SON[str, Any]:
"""Get a SASL start command with an optional principal name"""
# Send the SASL start with the optional principal name.
payload = {}
principal_name = self.username
if principal_name:
payload["n"] = principal_name
return SON(
[
("saslStart", 1),
("mechanism", "MONGODB-OIDC"),
("payload", Binary(bson.encode(payload))),
("autoAuthorize", 1),
]
)
def auth_start_cmd(self, use_callback: bool = True) -> Optional[SON[str, Any]]:
# TODO: DRIVERS-2672, check for provider_name in self.properties here.
if self.idp_info is None:
return self.principal_step_cmd()
token = self.get_current_token(use_callback)
if not token:
return None
bin_payload = Binary(bson.encode({"jwt": token}))
return SON(
[
("saslStart", 1),
("mechanism", "MONGODB-OIDC"),
("payload", bin_payload),
]
)
def run_command(
self, conn: Connection, cmd: MutableMapping[str, Any]
) -> Optional[Mapping[str, Any]]:
def _run_command(self, conn: Connection, cmd: MutableMapping[str, Any]) -> Mapping[str, Any]:
try:
return conn.command("$external", cmd, no_reauth=True) # type: ignore[call-arg]
except OperationFailure:
self.access_token = None
self._invalidate(conn)
raise
def reauthenticate(self, conn: Connection) -> Optional[Mapping[str, Any]]:
"""Handle a reauthenticate from the server."""
# First see if we have the a newer token on the authenticator.
prev_id = conn.oidc_token_gen_id or 0
# If we've already changed tokens, make one optimistic attempt.
if (prev_id < self.token_gen_id) and self.access_token:
try:
return self.authenticate(conn)
except OperationFailure:
pass
def _invalidate(self, conn: Connection) -> None:
# Ignore the invalidation if a token gen id is given and is less than our
# current token gen id.
token_gen_id = conn.oidc_token_gen_id or 0
if token_gen_id is not None and token_gen_id < self.token_gen_id:
return
self.access_token = None
# TODO: DRIVERS-2672, check for provider_name in self.properties here.
# If so, we clear the access token and return finish_auth.
# Next see if the idp info has changed.
prev_idp_info = self.idp_info
self.idp_info = None
cmd = self.principal_step_cmd()
resp = self.run_command(conn, cmd)
assert resp is not None
server_resp: dict = bson.decode(resp["payload"])
if "issuer" in server_resp:
self.idp_info = server_resp
# Handle the case of changed idp info.
if self.idp_info != prev_idp_info:
self.access_token = None
self.refresh_token = None
# If we have a refresh token, try using that.
if self.refresh_token:
try:
return self.finish_auth(resp, conn)
except OperationFailure:
self.refresh_token = None
# If that fails, try again without the refresh token.
return self.authenticate(conn)
# If we don't have a refresh token, just try once.
return self.finish_auth(resp, conn)
def authenticate(self, conn: Connection) -> Optional[Mapping[str, Any]]:
ctx = conn.auth_ctx
cmd = None
if ctx and ctx.speculate_succeeded():
resp = ctx.speculative_authenticate
else:
cmd = self.auth_start_cmd()
assert cmd is not None
resp = self.run_command(conn, cmd)
assert resp is not None
if resp["done"]:
conn.oidc_token_gen_id = self.token_gen_id
return None
server_resp: dict = bson.decode(resp["payload"])
if "issuer" in server_resp:
self.idp_info = server_resp
return self.finish_auth(resp, conn)
def finish_auth(
self, orig_resp: Mapping[str, Any], conn: Connection
) -> Optional[Mapping[str, Any]]:
conversation_id = orig_resp["conversationId"]
token = self.get_current_token()
def _sasl_continue_jwt(
self, conn: Connection, start_resp: Mapping[str, Any]
) -> Mapping[str, Any]:
self.access_token = None
self.refresh_token = None
start_payload: dict = bson.decode(start_resp["payload"])
if "issuer" in start_payload:
self.idp_info = OIDCIdPInfo(**start_payload)
access_token = self._get_access_token()
conn.oidc_token_gen_id = self.token_gen_id
bin_payload = Binary(bson.encode({"jwt": token}))
cmd = {
cmd = self._get_continue_command({"jwt": access_token}, start_resp)
return self._run_command(conn, cmd)
def _sasl_start_jwt(self, conn: Connection) -> Mapping[str, Any]:
access_token = self._get_access_token()
conn.oidc_token_gen_id = self.token_gen_id
cmd = self._get_start_command({"jwt": access_token})
return self._run_command(conn, cmd)
def _get_start_command(self, payload: Optional[Mapping[str, Any]]) -> MutableMapping[str, Any]:
if payload is None:
principal_name = self.username
if principal_name:
payload = {"n": principal_name}
else:
payload = {}
bin_payload = Binary(bson.encode(payload))
return {"saslStart": 1, "mechanism": "MONGODB-OIDC", "payload": bin_payload}
def _get_continue_command(
self, payload: Mapping[str, Any], start_resp: Mapping[str, Any]
) -> MutableMapping[str, Any]:
bin_payload = Binary(bson.encode(payload))
return {
"saslContinue": 1,
"conversationId": conversation_id,
"payload": bin_payload,
"conversationId": start_resp["conversationId"],
}
resp = self.run_command(conn, cmd)
assert resp is not None
if not resp["done"]:
raise OperationFailure("SASL conversation failed to complete.")
return resp
def _authenticate_oidc(

View File

@ -17,7 +17,6 @@
from __future__ import annotations
import datetime
import inspect
import warnings
from collections import OrderedDict, abc
from difflib import get_close_matches
@ -42,6 +41,7 @@ from bson.binary import UuidRepresentation
from bson.codec_options import CodecOptions, DatetimeConversion, TypeRegistry
from bson.raw_bson import RawBSONDocument
from pymongo.auth import MECHANISMS
from pymongo.auth_oidc import OIDCCallback
from pymongo.compression_support import (
validate_compressors,
validate_zlib_compression_level,
@ -425,6 +425,8 @@ _MECHANISM_PROPS = frozenset(
"SERVICE_REALM",
"AWS_SESSION_TOKEN",
"PROVIDER_NAME",
"TOKEN_AUDIENCE",
"ALLOWED_HOSTS",
]
)
@ -440,22 +442,14 @@ def validate_auth_mechanism_properties(option: str, value: Any) -> dict[str, Uni
props[key] = value
elif isinstance(value, bool):
props[key] = str(value).lower()
elif key in ["allowed_hosts"] and isinstance(value, list):
elif key in ["ALLOWED_HOSTS"] and isinstance(value, list):
props[key] = value
elif inspect.isfunction(value):
signature = inspect.signature(value)
if key == "request_token_callback":
expected_params = 2
else:
raise ValueError(f"Unrecognized Auth mechanism function {key}")
if len(signature.parameters) != expected_params:
msg = f"{key} must accept {expected_params} parameters"
raise ValueError(msg)
elif key in ["OIDC_CALLBACK", "OIDC_HUMAN_CALLBACK"]:
if not isinstance(value, OIDCCallback):
raise ValueError("callback must be an OIDCCallback object")
props[key] = value
else:
raise ValueError(
"Auth mechanism property values must be strings or callback functions"
)
raise ValueError(f"Invalid type for auth mechanism property {key}, {type(value)}")
return props
value = validate_string(option, value)

View File

@ -222,6 +222,7 @@ exclude_lines = [
"return NotImplemented",
"_use_c = true",
"if __name__ == '__main__':",
"if TYPE_CHECKING:"
]
partial_branches = ["if (.*and +)*not _use_c( and.*)*:"]

View File

@ -67,6 +67,7 @@ if hasattr(gc, "set_debug"):
# for a replica set.
host = os.environ.get("DB_IP", "localhost")
port = int(os.environ.get("DB_PORT", 27017))
IS_SRV = "mongodb+srv" in host
db_user = os.environ.get("DB_USER", "user")
db_pwd = os.environ.get("DB_PASSWORD", "password")
@ -384,7 +385,7 @@ class ClientContext:
self.auth_enabled = self._server_started_with_auth()
if self.auth_enabled:
if not self.serverless:
if not self.serverless and not IS_SRV:
# See if db_user already exists.
if not self._check_user_provided():
_create_user(self.client.admin, db_user, db_pwd)
@ -452,7 +453,7 @@ class ClientContext:
else:
self.server_parameters = self.client.admin.command("getParameter", "*")
assert self.cmd_line is not None
if "enableTestCommands=1" in self.cmd_line["argv"]:
if self.server_parameters["enableTestCommands"]:
self.test_commands_enabled = True
elif "parsed" in self.cmd_line:
params = self.cmd_line["parsed"].get("setParameter", [])
@ -488,14 +489,14 @@ class ClientContext:
@property
def host(self):
if self.is_rs:
if self.is_rs and not IS_SRV:
primary = self.client.primary
return str(primary[0]) if primary is not None else host
return host
@property
def port(self):
if self.is_rs:
if self.is_rs and not IS_SRV:
primary = self.client.primary
return primary[1] if primary is not None else port
return port
@ -520,6 +521,10 @@ class ClientContext:
# Raised if self.server_status is None.
return None
def check_auth_type(self, auth_type):
auth_mechs = self.server_parameters.get("authenticationMechanisms", [])
return auth_type in auth_mechs
def _check_user_provided(self):
"""Return True if db_user/db_password is already an admin user."""
client: MongoClient = pymongo.MongoClient(

View File

@ -446,52 +446,7 @@
}
},
{
"description": "should recognise the mechanism and request callback (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC",
"callback": ["oidcRequest"],
"valid": true,
"credential": {
"username": null,
"password": null,
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"REQUEST_TOKEN_CALLBACK": true
}
}
},
{
"description": "should recognise the mechanism when auth source is explicitly specified and with request callback (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external",
"callback": ["oidcRequest"],
"valid": true,
"credential": {
"username": null,
"password": null,
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"REQUEST_TOKEN_CALLBACK": true
}
}
},
{
"description": "should recognise the mechanism and username with request callback (MONGODB-OIDC)",
"uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC",
"callback": ["oidcRequest"],
"valid": true,
"credential": {
"username": "principalName",
"password": null,
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"REQUEST_TOKEN_CALLBACK": true
}
}
},
{
"description": "should recognise the mechanism with aws device (MONGODB-OIDC)",
"description": "should recognise the mechanism with aws provider (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws",
"valid": true,
"credential": {
@ -500,12 +455,12 @@
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"PROVIDER_NAME": "aws"
"PROVIDER_NAME": "aws"
}
}
},
{
"description": "should recognise the mechanism when auth source is explicitly specified and with aws device (MONGODB-OIDC)",
"description": "should recognise the mechanism when auth source is explicitly specified and with provider (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=PROVIDER_NAME:aws",
"valid": true,
"credential": {
@ -514,38 +469,79 @@
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"PROVIDER_NAME": "aws"
"PROVIDER_NAME": "aws"
}
}
},
{
"description": "should throw an exception if username and password are specified (MONGODB-OIDC)",
"uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC",
"callback": ["oidcRequest"],
"description": "should throw an exception if username and password is specified for aws provider (MONGODB-OIDC)",
"uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws",
"valid": false,
"credential": null
},
{
"description": "should throw an exception if username and deviceName are specified (MONGODB-OIDC)",
"uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&PROVIDER_NAME:gcp",
"description": "should throw an exception if username is specified for aws provider (MONGODB-OIDC)",
"uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&PROVIDER_NAME:aws",
"valid": false,
"credential": null
},
{
"description": "should throw an exception if specified deviceName is not supported (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:unexisted",
"description": "should throw an exception if specified provider is not supported (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:invalid",
"valid": false,
"credential": null
},
{
"description": "should throw an exception if neither deviceName nor callback specified (MONGODB-OIDC)",
"description": "should throw an exception custom callback is chosen but no callback is provided (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:custom",
"valid": false,
"credential": null
},
{
"description": "should throw an exception if neither provider 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",
"description": "should recognise the mechanism with azure provider (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:foo",
"valid": true,
"credential": {
"username": null,
"password": null,
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"PROVIDER_NAME": "azure",
"TOKEN_AUDIENCE": "foo"
}
}
},
{
"description": "should accept a username with azure provider (MONGODB-OIDC)",
"uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:foo",
"valid": true,
"credential": {
"username": "user",
"password": null,
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"PROVIDER_NAME": "azure",
"TOKEN_AUDIENCE": "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=PROVIDER_NAME:azure,TOKEN_AUDIENCE:foo",
"valid": false,
"credential": null
},
{
"description": "should throw and exception if no token audience is given for azure provider (MONGODB-OIDC)",
"uri": "mongodb://username@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure",
"valid": false,
"credential": null
}

View File

@ -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"
}
}
]
}
]
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@ import glob
import json
import os
import sys
import warnings
sys.path[0:0] = [""]
@ -26,6 +27,7 @@ from test import unittest
from test.unified_format import generate_test_classes
from pymongo import MongoClient
from pymongo.auth_oidc import OIDCCallback
_TEST_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "auth")
@ -34,6 +36,11 @@ class TestAuthSpec(unittest.TestCase):
pass
class SampleHumanCallback(OIDCCallback):
def fetch(self, context):
pass
def create_test(test_case):
def run_test(self):
uri = test_case["uri"]
@ -41,14 +48,15 @@ def create_test(test_case):
credential = test_case.get("credential")
if not valid:
self.assertRaises(Exception, MongoClient, uri, connect=False)
with warnings.catch_warnings():
warnings.simplefilter("default")
self.assertRaises(Exception, MongoClient, uri, connect=False)
else:
props = {}
if credential:
props = credential["mechanism_properties"] or {}
if props.get("REQUEST_TOKEN_CALLBACK"):
props["request_token_callback"] = lambda x, y: 1
del props["REQUEST_TOKEN_CALLBACK"]
if props.get("CALLBACK"):
props["callback"] = SampleHumanCallback()
client = MongoClient(uri, connect=False, authmechanismproperties=props)
credentials = client.options.pool_options._credentials
if credential is None:
@ -80,10 +88,8 @@ def create_test(test_case):
)
elif "PROVIDER_NAME" in expected:
self.assertEqual(actual.provider_name, expected["PROVIDER_NAME"])
elif "request_token_callback" in expected:
self.assertEqual(
actual.request_token_callback, expected["request_token_callback"]
)
elif "callback" in expected:
self.assertEqual(actual.callback, expected["callback"])
else:
self.fail(f"Unhandled property: {key}")
else:

View File

@ -140,7 +140,7 @@ KMS_TLS_OPTS = {
}
# Build up a placeholder map.
# Build up a placeholder maps.
PLACEHOLDER_MAP = {}
for provider_name, provider_data in [
("local", {"key": LOCAL_MASTER_KEY}),
@ -159,6 +159,15 @@ for provider_name, provider_data in [
placeholder = f"/clientEncryptionOpts/kmsProviders/{provider_name}/{key}"
PLACEHOLDER_MAP[placeholder] = value
PROVIDER_NAME = os.environ.get("OIDC_PROVIDER_NAME", "aws")
if PROVIDER_NAME == "aws":
PLACEHOLDER_MAP["/uriOptions/authMechanismProperties"] = {"PROVIDER_NAME": "aws"}
elif PROVIDER_NAME == "azure":
PLACEHOLDER_MAP["/uriOptions/authMechanismProperties"] = {
"PROVIDER_NAME": "azure",
"TOKEN_AUDIENCE": os.environ["AZUREOIDC_AUDIENCE"],
}
def interrupt_loop():
global IS_INTERRUPTED
@ -233,6 +242,8 @@ def is_run_on_requirement_satisfied(requirement):
if req_auth is not None:
if req_auth:
auth_satisfied = client_context.auth_enabled
if auth_satisfied and "authMechanism" in requirement:
auth_satisfied = client_context.check_auth_type(requirement["authMechanism"])
else:
auth_satisfied = not client_context.auth_enabled
@ -933,7 +944,7 @@ class UnifiedSpecTestMixinV1(IntegrationTest):
a class attribute ``TEST_SPEC``.
"""
SCHEMA_VERSION = Version.from_string("1.18")
SCHEMA_VERSION = Version.from_string("1.19")
RUN_ON_LOAD_BALANCER = True
RUN_ON_SERVERLESS = True
TEST_SPEC: Any

View File

@ -558,7 +558,7 @@ def _mongo_client(host, port, authenticate=True, directConnection=None, **kwargs
client_options.update(kwargs)
uri = _connection_string(host)
if client_context.auth_enabled and authenticate:
if client_context.auth_enabled and authenticate and "authMechanism" not in kwargs:
# Only add the default username or password if one is not provided.
res = parse_uri(uri)
if (