|
18 | 18 | import functools
|
19 | 19 | import hashlib
|
20 | 20 | import hmac
|
21 |
| -import os |
22 | 21 | import socket
|
23 |
| -import typing |
24 | 22 | from base64 import standard_b64decode, standard_b64encode
|
25 |
| -from collections import namedtuple |
26 | 23 | from typing import (
|
27 | 24 | TYPE_CHECKING,
|
28 | 25 | Any,
|
29 | 26 | Callable,
|
30 |
| - Dict, |
31 | 27 | Mapping,
|
32 | 28 | MutableMapping,
|
33 | 29 | Optional,
|
|
36 | 32 | from urllib.parse import quote
|
37 | 33 |
|
38 | 34 | from bson.binary import Binary
|
39 |
| -from pymongo.auth_aws import _authenticate_aws |
40 |
| -from pymongo.auth_oidc import ( |
41 |
| - _authenticate_oidc, |
42 |
| - _get_authenticator, |
43 |
| - _OIDCAzureCallback, |
44 |
| - _OIDCGCPCallback, |
45 |
| - _OIDCProperties, |
46 |
| - _OIDCTestCallback, |
| 35 | +from pymongo.auth_shared import ( |
| 36 | + MongoCredential, |
| 37 | + _authenticate_scram_start, |
| 38 | + _parse_scram_response, |
| 39 | + _xor, |
47 | 40 | )
|
48 | 41 | from pymongo.errors import ConfigurationError, OperationFailure
|
49 | 42 | from pymongo.saslprep import saslprep
|
| 43 | +from pymongo.synchronous.auth_aws import _authenticate_aws |
| 44 | +from pymongo.synchronous.auth_oidc import ( |
| 45 | + _authenticate_oidc, |
| 46 | + _get_authenticator, |
| 47 | +) |
50 | 48 |
|
51 | 49 | if TYPE_CHECKING:
|
52 | 50 | from pymongo.hello import Hello
|
53 |
| - from pymongo.pool import Connection |
| 51 | + from pymongo.synchronous.pool import Connection |
54 | 52 |
|
55 | 53 | HAVE_KERBEROS = True
|
56 | 54 | _USE_PRINCIPAL = False
|
|
66 | 64 | HAVE_KERBEROS = False
|
67 | 65 |
|
68 | 66 |
|
69 |
| -MECHANISMS = frozenset( |
70 |
| - [ |
71 |
| - "GSSAPI", |
72 |
| - "MONGODB-CR", |
73 |
| - "MONGODB-OIDC", |
74 |
| - "MONGODB-X509", |
75 |
| - "MONGODB-AWS", |
76 |
| - "PLAIN", |
77 |
| - "SCRAM-SHA-1", |
78 |
| - "SCRAM-SHA-256", |
79 |
| - "DEFAULT", |
80 |
| - ] |
81 |
| -) |
82 |
| -"""The authentication mechanisms supported by PyMongo.""" |
83 |
| - |
84 |
| - |
85 |
| -class _Cache: |
86 |
| - __slots__ = ("data",) |
87 |
| - |
88 |
| - _hash_val = hash("_Cache") |
89 |
| - |
90 |
| - def __init__(self) -> None: |
91 |
| - self.data = None |
92 |
| - |
93 |
| - def __eq__(self, other: object) -> bool: |
94 |
| - # Two instances must always compare equal. |
95 |
| - if isinstance(other, _Cache): |
96 |
| - return True |
97 |
| - return NotImplemented |
98 |
| - |
99 |
| - def __ne__(self, other: object) -> bool: |
100 |
| - if isinstance(other, _Cache): |
101 |
| - return False |
102 |
| - return NotImplemented |
103 |
| - |
104 |
| - def __hash__(self) -> int: |
105 |
| - return self._hash_val |
106 |
| - |
107 |
| - |
108 |
| -MongoCredential = namedtuple( |
109 |
| - "MongoCredential", |
110 |
| - ["mechanism", "source", "username", "password", "mechanism_properties", "cache"], |
111 |
| -) |
112 |
| -"""A hashable namedtuple of values used for authentication.""" |
113 |
| - |
114 |
| - |
115 |
| -GSSAPIProperties = namedtuple( |
116 |
| - "GSSAPIProperties", ["service_name", "canonicalize_host_name", "service_realm"] |
117 |
| -) |
118 |
| -"""Mechanism properties for GSSAPI authentication.""" |
119 |
| - |
120 |
| - |
121 |
| -_AWSProperties = namedtuple("_AWSProperties", ["aws_session_token"]) |
122 |
| -"""Mechanism properties for MONGODB-AWS authentication.""" |
123 |
| - |
124 |
| - |
125 |
| -def _build_credentials_tuple( |
126 |
| - mech: str, |
127 |
| - source: Optional[str], |
128 |
| - user: str, |
129 |
| - passwd: str, |
130 |
| - extra: Mapping[str, Any], |
131 |
| - database: Optional[str], |
132 |
| -) -> MongoCredential: |
133 |
| - """Build and return a mechanism specific credentials tuple.""" |
134 |
| - if mech not in ("MONGODB-X509", "MONGODB-AWS", "MONGODB-OIDC") and user is None: |
135 |
| - raise ConfigurationError(f"{mech} requires a username.") |
136 |
| - if mech == "GSSAPI": |
137 |
| - if source is not None and source != "$external": |
138 |
| - raise ValueError("authentication source must be $external or None for GSSAPI") |
139 |
| - properties = extra.get("authmechanismproperties", {}) |
140 |
| - service_name = properties.get("SERVICE_NAME", "mongodb") |
141 |
| - canonicalize = bool(properties.get("CANONICALIZE_HOST_NAME", False)) |
142 |
| - service_realm = properties.get("SERVICE_REALM") |
143 |
| - props = GSSAPIProperties( |
144 |
| - service_name=service_name, |
145 |
| - canonicalize_host_name=canonicalize, |
146 |
| - service_realm=service_realm, |
147 |
| - ) |
148 |
| - # Source is always $external. |
149 |
| - return MongoCredential(mech, "$external", user, passwd, props, None) |
150 |
| - elif mech == "MONGODB-X509": |
151 |
| - if passwd is not None: |
152 |
| - raise ConfigurationError("Passwords are not supported by MONGODB-X509") |
153 |
| - if source is not None and source != "$external": |
154 |
| - raise ValueError("authentication source must be $external or None for MONGODB-X509") |
155 |
| - # Source is always $external, user can be None. |
156 |
| - return MongoCredential(mech, "$external", user, None, None, None) |
157 |
| - elif mech == "MONGODB-AWS": |
158 |
| - if user is not None and passwd is None: |
159 |
| - raise ConfigurationError("username without a password is not supported by MONGODB-AWS") |
160 |
| - if source is not None and source != "$external": |
161 |
| - raise ConfigurationError( |
162 |
| - "authentication source must be $external or None for MONGODB-AWS" |
163 |
| - ) |
164 |
| - |
165 |
| - properties = extra.get("authmechanismproperties", {}) |
166 |
| - aws_session_token = properties.get("AWS_SESSION_TOKEN") |
167 |
| - aws_props = _AWSProperties(aws_session_token=aws_session_token) |
168 |
| - # user can be None for temporary link-local EC2 credentials. |
169 |
| - return MongoCredential(mech, "$external", user, passwd, aws_props, None) |
170 |
| - elif mech == "MONGODB-OIDC": |
171 |
| - properties = extra.get("authmechanismproperties", {}) |
172 |
| - callback = properties.get("OIDC_CALLBACK") |
173 |
| - human_callback = properties.get("OIDC_HUMAN_CALLBACK") |
174 |
| - environ = properties.get("ENVIRONMENT") |
175 |
| - token_resource = properties.get("TOKEN_RESOURCE", "") |
176 |
| - default_allowed = [ |
177 |
| - "*.mongodb.net", |
178 |
| - "*.mongodb-dev.net", |
179 |
| - "*.mongodb-qa.net", |
180 |
| - "*.mongodbgov.net", |
181 |
| - "localhost", |
182 |
| - "127.0.0.1", |
183 |
| - "::1", |
184 |
| - ] |
185 |
| - allowed_hosts = properties.get("ALLOWED_HOSTS", default_allowed) |
186 |
| - msg = ( |
187 |
| - "authentication with MONGODB-OIDC requires providing either a callback or a environment" |
188 |
| - ) |
189 |
| - if passwd is not None: |
190 |
| - msg = "password is not supported by MONGODB-OIDC" |
191 |
| - raise ConfigurationError(msg) |
192 |
| - if callback or human_callback: |
193 |
| - if environ is not None: |
194 |
| - raise ConfigurationError(msg) |
195 |
| - if callback and human_callback: |
196 |
| - msg = "cannot set both OIDC_CALLBACK and OIDC_HUMAN_CALLBACK" |
197 |
| - raise ConfigurationError(msg) |
198 |
| - elif environ is not None: |
199 |
| - if environ == "test": |
200 |
| - if user is not None: |
201 |
| - msg = "test environment for MONGODB-OIDC does not support username" |
202 |
| - raise ConfigurationError(msg) |
203 |
| - callback = _OIDCTestCallback() |
204 |
| - elif environ == "azure": |
205 |
| - passwd = None |
206 |
| - if not token_resource: |
207 |
| - raise ConfigurationError( |
208 |
| - "Azure environment for MONGODB-OIDC requires a TOKEN_RESOURCE auth mechanism property" |
209 |
| - ) |
210 |
| - callback = _OIDCAzureCallback(token_resource) |
211 |
| - elif environ == "gcp": |
212 |
| - passwd = None |
213 |
| - if not token_resource: |
214 |
| - raise ConfigurationError( |
215 |
| - "GCP provider for MONGODB-OIDC requires a TOKEN_RESOURCE auth mechanism property" |
216 |
| - ) |
217 |
| - callback = _OIDCGCPCallback(token_resource) |
218 |
| - else: |
219 |
| - raise ConfigurationError(f"unrecognized ENVIRONMENT for MONGODB-OIDC: {environ}") |
220 |
| - else: |
221 |
| - raise ConfigurationError(msg) |
222 |
| - |
223 |
| - oidc_props = _OIDCProperties( |
224 |
| - callback=callback, |
225 |
| - human_callback=human_callback, |
226 |
| - environment=environ, |
227 |
| - allowed_hosts=allowed_hosts, |
228 |
| - token_resource=token_resource, |
229 |
| - username=user, |
230 |
| - ) |
231 |
| - return MongoCredential(mech, "$external", user, passwd, oidc_props, _Cache()) |
232 |
| - |
233 |
| - elif mech == "PLAIN": |
234 |
| - source_database = source or database or "$external" |
235 |
| - return MongoCredential(mech, source_database, user, passwd, None, None) |
236 |
| - else: |
237 |
| - source_database = source or database or "admin" |
238 |
| - if passwd is None: |
239 |
| - raise ConfigurationError("A password is required.") |
240 |
| - return MongoCredential(mech, source_database, user, passwd, None, _Cache()) |
241 |
| - |
242 |
| - |
243 |
| -def _xor(fir: bytes, sec: bytes) -> bytes: |
244 |
| - """XOR two byte strings together.""" |
245 |
| - return b"".join([bytes([x ^ y]) for x, y in zip(fir, sec)]) |
246 |
| - |
247 |
| - |
248 |
| -def _parse_scram_response(response: bytes) -> Dict[bytes, bytes]: |
249 |
| - """Split a scram response into key, value pairs.""" |
250 |
| - return dict( |
251 |
| - typing.cast(typing.Tuple[bytes, bytes], item.split(b"=", 1)) |
252 |
| - for item in response.split(b",") |
253 |
| - ) |
254 |
| - |
255 |
| - |
256 |
| -def _authenticate_scram_start( |
257 |
| - credentials: MongoCredential, mechanism: str |
258 |
| -) -> tuple[bytes, bytes, MutableMapping[str, Any]]: |
259 |
| - username = credentials.username |
260 |
| - user = username.encode("utf-8").replace(b"=", b"=3D").replace(b",", b"=2C") |
261 |
| - nonce = standard_b64encode(os.urandom(32)) |
262 |
| - first_bare = b"n=" + user + b",r=" + nonce |
263 |
| - |
264 |
| - cmd = { |
265 |
| - "saslStart": 1, |
266 |
| - "mechanism": mechanism, |
267 |
| - "payload": Binary(b"n,," + first_bare), |
268 |
| - "autoAuthorize": 1, |
269 |
| - "options": {"skipEmptyExchange": True}, |
270 |
| - } |
271 |
| - return nonce, first_bare, cmd |
| 67 | +_IS_SYNC = True |
272 | 68 |
|
273 | 69 |
|
274 | 70 | def _authenticate_scram(credentials: MongoCredential, conn: Connection, mechanism: str) -> None:
|
@@ -553,7 +349,7 @@ def _authenticate_default(credentials: MongoCredential, conn: Connection) -> Non
|
553 | 349 | source = credentials.source
|
554 | 350 | cmd = conn.hello_cmd()
|
555 | 351 | cmd["saslSupportedMechs"] = source + "." + credentials.username
|
556 |
| - mechs = conn.command(source, cmd, publish_events=False).get("saslSupportedMechs", []) |
| 352 | + mechs = (conn.command(source, cmd, publish_events=False)).get("saslSupportedMechs", []) |
557 | 353 | if "SCRAM-SHA-256" in mechs:
|
558 | 354 | return _authenticate_scram(credentials, conn, "SCRAM-SHA-256")
|
559 | 355 | else:
|
|
0 commit comments