|
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