Merge branch 'master' of github.com:mongodb/mongo-python-driver

This commit is contained in:
Steven Silvester 2024-04-29 19:41:52 -05:00
commit 537ced6648
No known key found for this signature in database
GPG Key ID: B1BF5EC3A8B32F91
8 changed files with 100 additions and 72 deletions

View File

@ -408,8 +408,10 @@ Azure IMDS
^^^^^^^^^^
For an application running on an Azure VM or otherwise using the `Azure Internal Metadata Service`_,
you can use the built-in support for Azure, where "<client_id>" below is the client id of the Azure
managed identity, and ``<audience>`` is the url-encoded ``audience`` `configured on your MongoDB deployment`_.
you can use the built-in support for Azure. If using an Azure managed identity, the "<client_id>" is
the client ID. If using a service principal to represent an enterprise application, the "<client_id>" is
the application ID of the service principal. The ``<audience>`` value is the ``audience``
`configured on your MongoDB deployment`_.
.. code-block:: python
@ -430,11 +432,24 @@ managed identity, and ``<audience>`` is the url-encoded ``audience`` `configured
If the application is running on an Azure VM and only one managed identity is associated with the
VM, ``username`` can be omitted.
If providing the ``TOKEN_RESOURCE`` as part of a connection string, it can be given as follows.
If the ``TOKEN_RESOURCE`` contains any of the following characters [``,``, ``+``, ``&``], then
it MUST be url-encoded.
.. code-block:: python
import os
uri = f'{os.environ["MONGODB_URI"]}?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:<audience>'
c = MongoClient(uri)
c.test.test.insert_one({})
c.close()
GCP IMDS
^^^^^^^^
For an application running on an GCP VM or otherwise using the `GCP Internal Metadata Service`_,
you can use the built-in support for GCP, where ``<audience>`` below is the url-encoded ``audience``
you can use the built-in support for GCP, where ``<audience>`` below is the ``audience``
`configured on your MongoDB deployment`_.
.. code-block:: python
@ -448,6 +463,18 @@ you can use the built-in support for GCP, where ``<audience>`` below is the url-
c.test.test.insert_one({})
c.close()
If providing the ``TOKEN_RESOURCE`` as part of a connection string, it can be given as follows.
If the ``TOKEN_RESOURCE`` contains any of the following characters [``,``, ``+``, ``&``], then
it MUST be url-encoded.
.. code-block:: python
import os
uri = f'{os.environ["MONGODB_URI"]}?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:<audience>'
c = MongoClient(uri)
c.test.test.insert_one({})
c.close()
Custom Callbacks
~~~~~~~~~~~~~~~~

View File

@ -32,7 +32,6 @@ def _get_azure_response(
url += f"&client_id={client_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

View File

@ -33,7 +33,7 @@ from typing import (
Optional,
cast,
)
from urllib.parse import quote, unquote
from urllib.parse import quote
from bson.binary import Binary
from pymongo.auth_aws import _authenticate_aws
@ -138,7 +138,7 @@ def _build_credentials_tuple(
raise ValueError("authentication source must be $external or None for GSSAPI")
properties = extra.get("authmechanismproperties", {})
service_name = properties.get("SERVICE_NAME", "mongodb")
canonicalize = properties.get("CANONICALIZE_HOST_NAME", False)
canonicalize = bool(properties.get("CANONICALIZE_HOST_NAME", False))
service_realm = properties.get("SERVICE_REALM")
props = GSSAPIProperties(
service_name=service_name,
@ -173,8 +173,6 @@ def _build_credentials_tuple(
human_callback = properties.get("OIDC_HUMAN_CALLBACK")
environ = properties.get("ENVIRONMENT")
token_resource = properties.get("TOKEN_RESOURCE", "")
if unquote(token_resource) == token_resource:
token_resource = quote(token_resource)
default_allowed = [
"*.mongodb.net",
"*.mongodb-dev.net",
@ -227,6 +225,7 @@ def _build_credentials_tuple(
human_callback=human_callback,
environment=environ,
allowed_hosts=allowed_hosts,
token_resource=token_resource,
username=user,
)
return MongoCredential(mech, "$external", user, passwd, oidc_props, _Cache())

View File

@ -21,6 +21,7 @@ import threading
import time
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Mapping, MutableMapping, Optional, Union
from urllib.parse import quote
import bson
from bson.binary import Binary
@ -72,6 +73,7 @@ class _OIDCProperties:
human_callback: Optional[OIDCCallback] = field(default=None)
environment: Optional[str] = field(default=None)
allowed_hosts: list[str] = field(default_factory=list)
token_resource: Optional[str] = field(default=None)
username: str = ""
@ -126,7 +128,7 @@ class _OIDCTestCallback(OIDCCallback):
class _OIDCAzureCallback(OIDCCallback):
def __init__(self, token_resource: str) -> None:
self.token_resource = token_resource
self.token_resource = quote(token_resource)
def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
resp = _get_azure_response(self.token_resource, context.username, context.timeout_seconds)
@ -137,7 +139,7 @@ class _OIDCAzureCallback(OIDCCallback):
class _OIDCGCPCallback(OIDCCallback):
def __init__(self, token_resource: str) -> None:
self.token_resource = token_resource
self.token_resource = quote(token_resource)
def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
resp = _get_gcp_response(self.token_resource, context.timeout_seconds)

View File

@ -453,26 +453,21 @@ def validate_auth_mechanism_properties(option: str, value: Any) -> dict[str, Uni
value = validate_string(option, value)
for opt in value.split(","):
try:
key, val = opt.split(":")
except ValueError:
# Try not to leak the token.
if "AWS_SESSION_TOKEN" in opt:
opt = ( # noqa: PLW2901
"AWS_SESSION_TOKEN:<redacted token>, did you forget "
"to percent-escape the token with quote_plus?"
)
raise ValueError(
"auth mechanism properties must be "
"key:value pairs like SERVICE_NAME:"
f"mongodb, not {opt}."
) from None
key, _, val = opt.partition(":")
if key not in _MECHANISM_PROPS:
# Try not to leak the token.
if "AWS_SESSION_TOKEN" in key:
raise ValueError(
"auth mechanism properties must be "
"key:value pairs like AWS_SESSION_TOKEN:<token>"
)
raise ValueError(
f"{key} is not a supported auth "
"mechanism property. Must be one of "
f"{tuple(_MECHANISM_PROPS)}."
)
if key == "CANONICALIZE_HOST_NAME":
props[key] = validate_boolean_or_string(key, val)
else:

View File

@ -474,7 +474,7 @@
}
},
{
"description": "should throw an exception if username and password is specified for test environment (MONGODB-OIDC)",
"description": "should throw an exception if username and password is specified for test environment (MONGODB-OIDC)",
"uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test",
"valid": false,
"credential": null
@ -486,23 +486,11 @@
"credential": null
},
{
"description": "should throw an exception if specified provider is not supported (MONGODB-OIDC)",
"description": "should throw an exception if specified environment is not supported (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:invalid",
"valid": false,
"credential": null
},
{
"description": "should throw an exception custom callback is chosen but no callback is provided (MONGODB-OIDC)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:custom",
"valid": false,
"credential": null
},
{
"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",
@ -541,7 +529,7 @@
},
{
"description": "should accept a url-encoded TOKEN_RESOURCE (MONGODB-OIDC)",
"uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:mongodb%253A//test-cluster",
"uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:mongodb%3A%2F%2Ftest-cluster",
"valid": true,
"credential": {
"username": "user",
@ -550,7 +538,37 @@
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"ENVIRONMENT": "azure",
"TOKEN_RESOURCE": "mongodb%253A//test-cluster"
"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:abc%2Cd%25ef%3Ag%26hi",
"valid": true,
"credential": {
"username": "user",
"password": null,
"source": "$external",
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"ENVIRONMENT": "azure",
"TOKEN_RESOURCE": "abc,d%ef:g&hi"
}
}
},
@ -565,7 +583,7 @@
"mechanism": "MONGODB-OIDC",
"mechanism_properties": {
"ENVIRONMENT": "azure",
"TOKEN_RESOURCE": "a%24b"
"TOKEN_RESOURCE": "a$b"
}
}
},

View File

@ -52,12 +52,7 @@ def create_test(test_case):
warnings.simplefilter("default")
self.assertRaises(Exception, MongoClient, uri, connect=False)
else:
props = {}
if credential:
props = credential["mechanism_properties"] or {}
if props.get("CALLBACK"):
props["callback"] = SampleHumanCallback()
client = MongoClient(uri, connect=False, authmechanismproperties=props)
client = MongoClient(uri, connect=False)
credentials = client.options.pool_options._credentials
if credential is None:
self.assertIsNone(credentials)
@ -73,25 +68,8 @@ def create_test(test_case):
expected = credential["mechanism_properties"]
if expected is not None:
actual = credentials.mechanism_properties
for key, _val in expected.items():
if "SERVICE_NAME" in expected:
self.assertEqual(actual.service_name, expected["SERVICE_NAME"])
elif "CANONICALIZE_HOST_NAME" in expected:
self.assertEqual(
actual.canonicalize_host_name, expected["CANONICALIZE_HOST_NAME"]
)
elif "SERVICE_REALM" in expected:
self.assertEqual(actual.service_realm, expected["SERVICE_REALM"])
elif "AWS_SESSION_TOKEN" in expected:
self.assertEqual(
actual.aws_session_token, expected["AWS_SESSION_TOKEN"]
)
elif "ENVIRONMENT" in expected:
self.assertEqual(actual.environment, expected["ENVIRONMENT"])
elif "callback" in expected:
self.assertEqual(actual.callback, expected["callback"])
else:
self.fail(f"Unhandled property: {key}")
for key, value in expected.items():
self.assertEqual(getattr(actual, key.lower()), value)
else:
if credential["mechanism"] == "MONGODB-AWS":
self.assertIsNone(credentials.mechanism_properties.aws_session_token)

View File

@ -504,20 +504,30 @@ class TestURI(unittest.TestCase):
self.assertEqual(options, res["options"])
def test_redact_AWS_SESSION_TOKEN(self):
unquoted_colon = "token:"
token = "token"
uri = (
"mongodb://user:password@localhost/?authMechanism=MONGODB-AWS"
"&authMechanismProperties=AWS_SESSION_TOKEN:" + unquoted_colon
"&authMechanismProperties=AWS_SESSION_TOKEN-" + token
)
with self.assertRaisesRegex(
ValueError,
"auth mechanism properties must be key:value pairs like "
"SERVICE_NAME:mongodb, not AWS_SESSION_TOKEN:<redacted token>"
", did you forget to percent-escape the token with "
"quote_plus?",
"auth mechanism properties must be key:value pairs like AWS_SESSION_TOKEN:<token>",
):
parse_uri(uri)
def test_handle_colon(self):
token = "token:foo"
uri = (
"mongodb://user:password@localhost/?authMechanism=MONGODB-AWS"
"&authMechanismProperties=AWS_SESSION_TOKEN:" + token
)
res = parse_uri(uri)
options = {
"authmechanism": "MONGODB-AWS",
"authMechanismProperties": {"AWS_SESSION_TOKEN": token},
}
self.assertEqual(options, res["options"])
def test_special_chars(self):
user = "user@ /9+:?~!$&'()*+,;="
pwd = "pwd@ /9+:?~!$&'()*+,;="