PYTHON-4747 Sync auth.py to master
This commit is contained in:
parent
8b5479c746
commit
134d00a102
22
pymongo/auth.py
Normal file
22
pymongo/auth.py
Normal file
@ -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
|
||||
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user