Skip to content

Commit ee11436

Browse files
committed
PYTHON-764 SCRAM-SHA-1 automatic upgrade / downgrade.
1 parent 4b70b2a commit ee11436

15 files changed

+117
-68
lines changed

pymongo/auth.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737

3838
MECHANISMS = frozenset(
39-
['GSSAPI', 'MONGODB-CR', 'MONGODB-X509', 'PLAIN', 'SCRAM-SHA-1'])
39+
['GSSAPI', 'MONGODB-CR', 'MONGODB-X509', 'PLAIN', 'SCRAM-SHA-1', 'DEFAULT'])
4040
"""The authentication mechanisms supported by PyMongo."""
4141

4242

@@ -337,13 +337,21 @@ def _authenticate_mongo_cr(credentials, sock_info):
337337
sock_info.command(source, query)
338338

339339

340+
def _authenticate_default(credentials, sock_info):
341+
if sock_info.max_wire_version >= 3:
342+
return _authenticate_scram_sha1(credentials, sock_info)
343+
else:
344+
return _authenticate_mongo_cr(credentials, sock_info)
345+
346+
340347
_AUTH_MAP = {
341348
'CRAM-MD5': _authenticate_cram_md5,
342349
'GSSAPI': _authenticate_gssapi,
343350
'MONGODB-CR': _authenticate_mongo_cr,
344351
'MONGODB-X509': _authenticate_x509,
345352
'PLAIN': _authenticate_plain,
346353
'SCRAM-SHA-1': _authenticate_scram_sha1,
354+
'DEFAULT': _authenticate_default,
347355
}
348356

349357

pymongo/client_options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def _parse_credentials(username, password, database, options):
2828
"""Parse authentication credentials."""
2929
if username is None:
3030
return None
31-
mechanism = options.get('authmechanism', 'MONGODB-CR')
31+
mechanism = options.get('authmechanism', 'DEFAULT')
3232
source = options.get('authsource', database or 'admin')
3333
return _build_credentials_tuple(
3434
mechanism, source, _unicode(username), _unicode(password), options)

pymongo/database.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -851,7 +851,7 @@ def remove_user(self, name):
851851
raise
852852

853853
def authenticate(self, name, password=None,
854-
source=None, mechanism='MONGODB-CR', **kwargs):
854+
source=None, mechanism='DEFAULT', **kwargs):
855855
"""Authenticate to use this database.
856856
857857
Authentication lasts for the life of the underlying client
@@ -883,11 +883,15 @@ def authenticate(self, name, password=None,
883883
specified the current database is used.
884884
- `mechanism` (optional): See
885885
:data:`~pymongo.auth.MECHANISMS` for options.
886-
Defaults to MONGODB-CR (MongoDB Challenge Response protocol)
886+
By default, use SCRAM-SHA-1 with MongoDB 2.8 and later,
887+
MONGODB-CR (MongoDB Challenge Response protocol) for older servers.
887888
- `gssapiServiceName` (optional): Used with the GSSAPI mechanism
888889
to specify the service name portion of the service principal name.
889890
Defaults to 'mongodb'.
890891
892+
.. versionadded:: 2.8
893+
Use SCRAM-SHA-1 with MongoDB 2.8 and later.
894+
891895
.. versionchanged:: 2.5
892896
Added the `source` and `mechanism` parameters. :meth:`authenticate`
893897
now raises a subclass of :class:`~pymongo.errors.PyMongoError` if

pymongo/mongo_client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ def _cache_credentials(self, source, credentials, connect=False):
336336

337337
# get_socket() logs out of the database if logged in with old
338338
# credentials, and logs in with new ones.
339-
with server.pool.get_socket(all_credentials) as sock_info:
339+
with server.get_socket(all_credentials) as sock_info:
340340
sock_info.authenticate(credentials)
341341

342342
# If several threads run _cache_credentials at once, last one wins.
@@ -652,7 +652,7 @@ def alive(self):
652652

653653
# Avoid race when other threads log in or out.
654654
all_credentials = self.__all_credentials.copy()
655-
with server.pool.get_socket(all_credentials) as sock_info:
655+
with server.get_socket(all_credentials) as sock_info:
656656
return not pool._closed(sock_info.sock)
657657

658658
except (socket.error, ConnectionFailure):
@@ -1095,7 +1095,7 @@ def copy_database(self, from_name, to_name,
10951095

10961096
# Avoid race when other threads log in or out.
10971097
all_credentials = self.__all_credentials.copy()
1098-
with server.pool.get_socket(all_credentials) as sock:
1098+
with server.get_socket(all_credentials) as sock:
10991099
if username is not None:
11001100
get_nonce_cmd = SON([("copydbgetnonce", 1),
11011101
("fromhost", from_host)])

pymongo/monitor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def _check_once(self):
138138
Returns a ServerDescription, or None on error.
139139
"""
140140
try:
141-
with self._pool.get_socket(all_credentials={}) as sock_info:
141+
with self._pool.get_socket({}, 0, 0) as sock_info:
142142
response, round_trip_time = self._check_with_socket(sock_info)
143143
old_rtts = self._server_description.round_trip_times
144144
if old_rtts:

pymongo/pool.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ def __init__(self, sock, pool, host):
150150
self.last_checkout = time.time()
151151
self.pool_ref = weakref.ref(pool)
152152

153+
self._min_wire_version = None
154+
self._max_wire_version = None
155+
153156
# The pool's pool_id changes with each reset() so we can close sockets
154157
# created before the last reset.
155158
self.pool_id = pool.pool_id
@@ -257,6 +260,20 @@ def close(self):
257260
self.sock.close()
258261
except:
259262
pass
263+
264+
def set_wire_version_range(self, min_wire_version, max_wire_version):
265+
self._min_wire_version = min_wire_version
266+
self._max_wire_version = max_wire_version
267+
268+
@property
269+
def min_wire_version(self):
270+
assert self._min_wire_version is not None
271+
return self._min_wire_version
272+
273+
@property
274+
def max_wire_version(self):
275+
assert self._max_wire_version is not None
276+
return self._max_wire_version
260277

261278
def exhaust(self, exhaust):
262279
self._exhaust = exhaust
@@ -425,19 +442,25 @@ def connect(self):
425442
sock.settimeout(self.opts.socket_timeout)
426443
return SocketInfo(sock, self, hostname)
427444

428-
def get_socket(self, all_credentials):
445+
def get_socket(self, all_credentials, min_wire_version, max_wire_version):
429446
"""Get a socket from the pool.
430447
431448
Returns a :class:`SocketInfo` object wrapping a connected
432-
:class:`socket.socket`, and a bool saying whether the socket was from
433-
the pool or freshly created.
449+
:class:`socket.socket`.
450+
451+
The socket is logged in or out as necessary to match ``all_credentials``
452+
using the correct authentication mechanism for the server's wire
453+
protocol version.
434454
435455
:Parameters:
436456
- `all_credentials`: dict, maps auth source to MongoCredential.
457+
- `min_wire_version`: int, minimum protocol the server supports.
458+
- `max_wire_version`: int, maximum protocol the server supports.
437459
"""
438460
# First get a socket, then attempt authentication. Simplifies
439461
# semaphore management in the face of network errors during auth.
440462
sock_info = self._get_socket_no_auth()
463+
sock_info.set_wire_version_range(min_wire_version, max_wire_version)
441464
try:
442465
sock_info.check_auth(all_credentials)
443466
return sock_info

pymongo/server.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def send_message(self, message, all_credentials):
6060
"""
6161
request_id, data = self._check_bson_size(message)
6262
try:
63-
with self._pool.get_socket(all_credentials) as sock_info:
63+
with self.get_socket(all_credentials) as sock_info:
6464
sock_info.send_message(data)
6565
except socket.error as exc:
6666
self._raise_connection_failure(exc)
@@ -82,7 +82,7 @@ def send_message_with_response(
8282
"""
8383
request_id, data = self._check_bson_size(message)
8484
try:
85-
with self._pool.get_socket(all_credentials) as sock_info:
85+
with self.get_socket(all_credentials) as sock_info:
8686
sock_info.exhaust(exhaust)
8787
sock_info.send_message(data)
8888
response_data = sock_info.receive_message(1, request_id)
@@ -99,6 +99,20 @@ def send_message_with_response(
9999
except socket.error as exc:
100100
self._raise_connection_failure(exc)
101101

102+
def get_socket(self, all_credentials):
103+
sd = self.description
104+
sock_info = self.pool.get_socket(all_credentials=all_credentials,
105+
min_wire_version=sd.min_wire_version,
106+
max_wire_version=sd.max_wire_version)
107+
108+
return sock_info
109+
110+
def maybe_return_socket(self, sock_info):
111+
self.pool.maybe_return_socket(sock_info)
112+
113+
def discard_socket(self, sock_info):
114+
self.pool.discard_socket(sock_info)
115+
102116
def start_request(self):
103117
# TODO: Remove implicit threadlocal requests, use explicit requests.
104118
self.pool.start_request()

test/pymongo_mocks.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def __init__(self, client, pair, *args, **kwargs):
3838
Pool.__init__(self,
3939
(default_host, default_port), PoolOptions(connect_timeout=20))
4040

41-
def get_socket(self, all_credentials):
41+
def get_socket(self, all_credentials, min_wire_version, max_wire_version):
4242
client = self.client
4343
host_and_port = '%s:%s' % (self.mock_host, self.mock_port)
4444
if host_and_port in client.mock_down_hosts:
@@ -49,7 +49,9 @@ def get_socket(self, all_credentials):
4949
+ client.mock_members
5050
+ client.mock_mongoses), "bad host: %s" % host_and_port
5151

52-
sock_info = Pool.get_socket(self, all_credentials)
52+
sock_info = Pool.get_socket(
53+
self, all_credentials, min_wire_version, max_wire_version)
54+
5355
sock_info.mock_host = self.mock_host
5456
sock_info.mock_port = self.mock_port
5557
return sock_info

test/test_auth.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from pymongo.auth import HAVE_KERBEROS, _build_credentials_tuple
3131
from pymongo.errors import OperationFailure, ConfigurationError
3232
from pymongo.read_preferences import ReadPreference
33-
from test import client_context, host, port, SkipTest, unittest
33+
from test import client_context, host, port, SkipTest, unittest, Version
3434

3535
# YOU MUST RUN KINIT BEFORE RUNNING GSSAPI TESTS.
3636
GSSAPI_HOST = os.environ.get('GSSAPI_HOST')
@@ -256,11 +256,13 @@ class TestSCRAMSHA1(unittest.TestCase):
256256
def setUp(self):
257257
self.set_name = client_context.setname
258258

259-
cmd_line = client_context.cmd_line
260-
if 'SCRAM-SHA-1' not in cmd_line.get(
261-
'parsed', {}).get('setParameter',
262-
{}).get('authenticationMechanisms', ''):
263-
raise SkipTest('SCRAM-SHA-1 mechanism not enabled')
259+
# Before 2.7.7, SCRAM-SHA-1 had to be enabled from the command line.
260+
if client_context.version < Version(2, 7, 7):
261+
cmd_line = client_context.cmd_line
262+
if 'SCRAM-SHA-1' not in cmd_line.get(
263+
'parsed', {}).get('setParameter',
264+
{}).get('authenticationMechanisms', ''):
265+
raise SkipTest('SCRAM-SHA-1 mechanism not enabled')
264266

265267
client = client_context.rs_or_standalone_client
266268
client.pymongo_test.add_user(
@@ -294,9 +296,7 @@ def test_scram_sha1(self):
294296
client.pymongo_test.command('dbstats')
295297

296298
def tearDown(self):
297-
client_context.rs_or_standalone_client.pymongo_test.remove_user(
298-
'user',
299-
w=client_context.w)
299+
client_context.rs_or_standalone_client.pymongo_test.remove_user('user')
300300

301301

302302
class TestAuthURIOptions(unittest.TestCase):

test/test_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -916,7 +916,7 @@ def test_auth_network_error(self):
916916

917917
# Simulate an authenticate() call on a different socket.
918918
credentials = auth._build_credentials_tuple(
919-
'MONGODB-CR', 'admin', db_user, db_pwd, {})
919+
'DEFAULT', 'admin', db_user, db_pwd, {})
920920

921921
c._cache_credentials('test', credentials, connect=False)
922922

0 commit comments

Comments
 (0)