diff --git a/pymongo/auth.py b/pymongo/auth.py new file mode 100644 index 000000000..a65113841 --- /dev/null +++ b/pymongo/auth.py @@ -0,0 +1,22 @@ +# Copyright 2024-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. + +"""Re-import of synchronous Auth API for compatibility.""" +from __future__ import annotations + +from pymongo.auth_shared import * # noqa: F403 +from pymongo.synchronous.auth import * # noqa: F403 +from pymongo.synchronous.auth import __doc__ as original_doc + +__doc__ = original_doc diff --git a/pymongo/synchronous/auth.py b/pymongo/synchronous/auth.py index 8bc4145ab..9a3477679 100644 --- a/pymongo/synchronous/auth.py +++ b/pymongo/synchronous/auth.py @@ -18,16 +18,12 @@ from __future__ import annotations import functools import hashlib import hmac -import os import socket -import typing from base64 import standard_b64decode, standard_b64encode -from collections import namedtuple from typing import ( TYPE_CHECKING, Any, Callable, - Dict, Mapping, MutableMapping, Optional, @@ -36,21 +32,23 @@ from typing import ( 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, - _OIDCAzureCallback, - _OIDCGCPCallback, - _OIDCProperties, - _OIDCTestCallback, +from pymongo.auth_shared import ( + MongoCredential, + _authenticate_scram_start, + _parse_scram_response, + _xor, ) from pymongo.errors import ConfigurationError, OperationFailure from pymongo.saslprep import saslprep +from pymongo.synchronous.auth_aws import _authenticate_aws +from pymongo.synchronous.auth_oidc import ( + _authenticate_oidc, + _get_authenticator, +) if TYPE_CHECKING: from pymongo.hello import Hello - from pymongo.pool import Connection + from pymongo.synchronous.pool import Connection HAVE_KERBEROS = True _USE_PRINCIPAL = False @@ -66,209 +64,7 @@ except ImportError: HAVE_KERBEROS = False -MECHANISMS = frozenset( - [ - "GSSAPI", - "MONGODB-CR", - "MONGODB-OIDC", - "MONGODB-X509", - "MONGODB-AWS", - "PLAIN", - "SCRAM-SHA-1", - "SCRAM-SHA-256", - "DEFAULT", - ] -) -"""The authentication mechanisms supported by PyMongo.""" - - -class _Cache: - __slots__ = ("data",) - - _hash_val = hash("_Cache") - - def __init__(self) -> None: - self.data = None - - def __eq__(self, other: object) -> bool: - # Two instances must always compare equal. - if isinstance(other, _Cache): - return True - return NotImplemented - - def __ne__(self, other: object) -> bool: - if isinstance(other, _Cache): - return False - return NotImplemented - - def __hash__(self) -> int: - return self._hash_val - - -MongoCredential = namedtuple( - "MongoCredential", - ["mechanism", "source", "username", "password", "mechanism_properties", "cache"], -) -"""A hashable namedtuple of values used for authentication.""" - - -GSSAPIProperties = namedtuple( - "GSSAPIProperties", ["service_name", "canonicalize_host_name", "service_realm"] -) -"""Mechanism properties for GSSAPI authentication.""" - - -_AWSProperties = namedtuple("_AWSProperties", ["aws_session_token"]) -"""Mechanism properties for MONGODB-AWS authentication.""" - - -def _build_credentials_tuple( - mech: str, - source: Optional[str], - user: str, - passwd: str, - extra: Mapping[str, Any], - database: Optional[str], -) -> MongoCredential: - """Build and return a mechanism specific credentials tuple.""" - if mech not in ("MONGODB-X509", "MONGODB-AWS", "MONGODB-OIDC") and user is None: - raise ConfigurationError(f"{mech} requires a username.") - if mech == "GSSAPI": - if source is not None and source != "$external": - raise ValueError("authentication source must be $external or None for GSSAPI") - properties = extra.get("authmechanismproperties", {}) - service_name = properties.get("SERVICE_NAME", "mongodb") - canonicalize = bool(properties.get("CANONICALIZE_HOST_NAME", False)) - service_realm = properties.get("SERVICE_REALM") - props = GSSAPIProperties( - service_name=service_name, - canonicalize_host_name=canonicalize, - service_realm=service_realm, - ) - # Source is always $external. - return MongoCredential(mech, "$external", user, passwd, props, None) - elif mech == "MONGODB-X509": - if passwd is not None: - raise ConfigurationError("Passwords are not supported by MONGODB-X509") - if source is not None and source != "$external": - raise ValueError("authentication source must be $external or None for MONGODB-X509") - # Source is always $external, user can be None. - return MongoCredential(mech, "$external", user, None, None, None) - elif mech == "MONGODB-AWS": - if user is not None and passwd is None: - raise ConfigurationError("username without a password is not supported by MONGODB-AWS") - if source is not None and source != "$external": - raise ConfigurationError( - "authentication source must be $external or None for MONGODB-AWS" - ) - - properties = extra.get("authmechanismproperties", {}) - aws_session_token = properties.get("AWS_SESSION_TOKEN") - aws_props = _AWSProperties(aws_session_token=aws_session_token) - # user can be None for temporary link-local EC2 credentials. - return MongoCredential(mech, "$external", user, passwd, aws_props, None) - elif mech == "MONGODB-OIDC": - properties = extra.get("authmechanismproperties", {}) - callback = properties.get("OIDC_CALLBACK") - human_callback = properties.get("OIDC_HUMAN_CALLBACK") - environ = properties.get("ENVIRONMENT") - token_resource = properties.get("TOKEN_RESOURCE", "") - default_allowed = [ - "*.mongodb.net", - "*.mongodb-dev.net", - "*.mongodb-qa.net", - "*.mongodbgov.net", - "localhost", - "127.0.0.1", - "::1", - ] - allowed_hosts = properties.get("ALLOWED_HOSTS", default_allowed) - msg = ( - "authentication with MONGODB-OIDC requires providing either a callback or a environment" - ) - if passwd is not None: - msg = "password is not supported by MONGODB-OIDC" - raise ConfigurationError(msg) - if callback or human_callback: - if environ 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 environ is not None: - if environ == "test": - if user is not None: - msg = "test environment for MONGODB-OIDC does not support username" - raise ConfigurationError(msg) - callback = _OIDCTestCallback() - elif environ == "azure": - passwd = None - if not token_resource: - raise ConfigurationError( - "Azure environment for MONGODB-OIDC requires a TOKEN_RESOURCE auth mechanism property" - ) - callback = _OIDCAzureCallback(token_resource) - elif environ == "gcp": - passwd = None - if not token_resource: - raise ConfigurationError( - "GCP provider for MONGODB-OIDC requires a TOKEN_RESOURCE auth mechanism property" - ) - callback = _OIDCGCPCallback(token_resource) - else: - raise ConfigurationError(f"unrecognized ENVIRONMENT for MONGODB-OIDC: {environ}") - else: - raise ConfigurationError(msg) - - oidc_props = _OIDCProperties( - callback=callback, - 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()) - - elif mech == "PLAIN": - source_database = source or database or "$external" - return MongoCredential(mech, source_database, user, passwd, None, None) - else: - source_database = source or database or "admin" - if passwd is None: - raise ConfigurationError("A password is required.") - return MongoCredential(mech, source_database, user, passwd, None, _Cache()) - - -def _xor(fir: bytes, sec: bytes) -> bytes: - """XOR two byte strings together.""" - return b"".join([bytes([x ^ y]) for x, y in zip(fir, sec)]) - - -def _parse_scram_response(response: bytes) -> Dict[bytes, bytes]: - """Split a scram response into key, value pairs.""" - return dict( - typing.cast(typing.Tuple[bytes, bytes], item.split(b"=", 1)) - for item in response.split(b",") - ) - - -def _authenticate_scram_start( - credentials: MongoCredential, mechanism: str -) -> tuple[bytes, bytes, MutableMapping[str, Any]]: - username = credentials.username - user = username.encode("utf-8").replace(b"=", b"=3D").replace(b",", b"=2C") - nonce = standard_b64encode(os.urandom(32)) - first_bare = b"n=" + user + b",r=" + nonce - - cmd = { - "saslStart": 1, - "mechanism": mechanism, - "payload": Binary(b"n,," + first_bare), - "autoAuthorize": 1, - "options": {"skipEmptyExchange": True}, - } - return nonce, first_bare, cmd +_IS_SYNC = True def _authenticate_scram(credentials: MongoCredential, conn: Connection, mechanism: str) -> None: @@ -553,7 +349,7 @@ def _authenticate_default(credentials: MongoCredential, conn: Connection) -> Non source = credentials.source cmd = conn.hello_cmd() cmd["saslSupportedMechs"] = source + "." + credentials.username - mechs = conn.command(source, cmd, publish_events=False).get("saslSupportedMechs", []) + mechs = (conn.command(source, cmd, publish_events=False)).get("saslSupportedMechs", []) if "SCRAM-SHA-256" in mechs: return _authenticate_scram(credentials, conn, "SCRAM-SHA-256") else: