Skip to content

Commit 4125454

Browse files
committed
Auth refactor to prepare for PYTHON-465.
This also addresses PYTHON-464 and makes logout sane.
1 parent 4ec457d commit 4125454

File tree

6 files changed

+187
-194
lines changed

6 files changed

+187
-194
lines changed

pymongo/auth.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright 2013 10gen, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Authentication helpers."""
16+
17+
try:
18+
import hashlib
19+
_MD5 = hashlib.md5
20+
except ImportError: # for Python < 2.5
21+
import md5
22+
_MD5 = md5.new
23+
24+
from bson.son import SON
25+
26+
27+
def _password_digest(username, password):
28+
"""Get a password digest to use for authentication.
29+
"""
30+
if not isinstance(password, basestring):
31+
raise TypeError("password must be an instance "
32+
"of %s" % (basestring.__name__,))
33+
if len(password) == 0:
34+
raise TypeError("password can't be empty")
35+
if not isinstance(username, basestring):
36+
raise TypeError("username must be an instance "
37+
"of %s" % (basestring.__name__,))
38+
39+
md5hash = _MD5()
40+
data = "%s:mongo:%s" % (username, password)
41+
md5hash.update(data.encode('utf-8'))
42+
return unicode(md5hash.hexdigest())
43+
44+
45+
def _auth_key(nonce, username, password):
46+
"""Get an auth key to use for authentication.
47+
"""
48+
digest = _password_digest(username, password)
49+
md5hash = _MD5()
50+
data = "%s%s%s" % (nonce, unicode(username), digest)
51+
md5hash.update(data.encode('utf-8'))
52+
return unicode(md5hash.hexdigest())
53+
54+
55+
def authenticate(credentials, sock_info, cmd_func):
56+
"""Authenticate sock_info using credentials.
57+
"""
58+
source, username, password = credentials
59+
# Get a nonce
60+
response, _ = cmd_func(sock_info, source, {'getnonce': 1})
61+
nonce = response['nonce']
62+
key = _auth_key(nonce, username, password)
63+
64+
# Actually authenticate
65+
query = SON([('authenticate', 1),
66+
('user', username),
67+
('nonce', nonce),
68+
('key', key)])
69+
cmd_func(sock_info, source, query)
70+

pymongo/database.py

Lines changed: 26 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,11 @@
1414

1515
"""Database level operations."""
1616

17-
import warnings
18-
1917
from bson.binary import OLD_UUID_SUBTYPE
2018
from bson.code import Code
2119
from bson.dbref import DBRef
2220
from bson.son import SON
23-
from pymongo import common, helpers
21+
from pymongo import auth, common, helpers
2422
from pymongo.collection import Collection
2523
from pymongo.errors import (CollectionInvalid,
2624
InvalidName,
@@ -471,7 +469,7 @@ def validate_collection(self, name_or_collection,
471469
raise CollectionInvalid("%s invalid: %s" % (name, info))
472470
# Sharded results
473471
elif "raw" in result:
474-
for repl, res in result["raw"].iteritems():
472+
for _, res in result["raw"].iteritems():
475473
if "result" in res:
476474
info = res["result"]
477475
if (info.find("exception") != -1 or
@@ -627,7 +625,7 @@ def add_user(self, name, password, read_only=False):
627625
"""
628626

629627
user = self.system.users.find_one({"user": name}) or {"user": name}
630-
user["pwd"] = helpers._password_digest(name, password)
628+
user["pwd"] = auth._password_digest(name, password)
631629
user["readOnly"] = common.validate_boolean('read_only', read_only)
632630

633631
try:
@@ -656,12 +654,10 @@ def remove_user(self, name):
656654
def authenticate(self, name, password):
657655
"""Authenticate to use this database.
658656
659-
Once authenticated, the user has full read and write access to
660-
this database. Raises :class:`TypeError` if either `name` or
661-
`password` is not an instance of :class:`basestring`
662-
(:class:`str` in python 3). Authentication lasts for the life
663-
of the underlying :class:`~pymongo.connection.Connection`, or
664-
until :meth:`logout` is called.
657+
Raises :class:`TypeError` if either `name` or `password` is not
658+
an instance of :class:`basestring` (:class:`str` in python 3).
659+
Authentication lasts for the life of the underlying client
660+
instance, or until :meth:`logout` is called.
665661
666662
The "admin" database is special. Authenticating on "admin"
667663
gives access to *all* databases. Effectively, "admin" access
@@ -670,28 +666,20 @@ def authenticate(self, name, password):
670666
.. note::
671667
This method authenticates the current connection, and
672668
will also cause all new :class:`~socket.socket` connections
673-
in the underlying :class:`~pymongo.connection.Connection` to
674-
be authenticated automatically.
675-
676-
- When sharing a :class:`~pymongo.connection.Connection`
677-
between multiple threads, all threads will share the
678-
authentication. If you need different authentication profiles
679-
for different purposes (e.g. admin users) you must use
680-
distinct instances of :class:`~pymongo.connection.Connection`.
669+
in the underlying client instance to be authenticated automatically.
681670
682-
- To get authentication to apply immediately to all
683-
existing sockets you may need to reset this Connection's
684-
sockets using :meth:`~pymongo.connection.Connection.disconnect`.
671+
- Authenticating more than once on the same database with different
672+
credentials is not supported. You must call :meth:`logout` before
673+
authenticating with new credentials.
685674
686-
.. warning::
675+
- When sharing a client instance between multiple threads, all
676+
threads will share the authentication. If you need different
677+
authentication profiles for different purposes you must use
678+
distinct client instances.
687679
688-
Currently, calls to
689-
:meth:`~pymongo.connection.Connection.end_request` will
690-
lead to unpredictable behavior in combination with
691-
auth. The :class:`~socket.socket` owned by the calling
692-
thread will be returned to the pool, so whichever thread
693-
uses that :class:`~socket.socket` next will have whatever
694-
permissions were granted to the calling thread.
680+
- To get authentication to apply immediately to all
681+
existing sockets you may need to reset this client instance's
682+
sockets using :meth:`~pymongo.mongo_client.MongoClient.disconnect`.
695683
696684
:Parameters:
697685
- `name`: the name of the user to authenticate
@@ -706,42 +694,22 @@ def authenticate(self, name, password):
706694
raise TypeError("password must be an instance "
707695
"of %s" % (basestring.__name__,))
708696

709-
# So we can authenticate during a failover. The start_request()
710-
# call below will pin the host used for getnonce so we use the
711-
# same host for authenticate.
712-
read_pref = rp.ReadPreference.PRIMARY_PREFERRED
713-
714-
in_request = self.connection.in_request()
715697
try:
716-
if not in_request:
717-
self.connection.start_request()
718-
719-
nonce = self.command("getnonce",
720-
read_preference=read_pref)["nonce"]
721-
key = helpers._auth_key(nonce, name, password)
722-
try:
723-
self.command("authenticate", user=unicode(name),
724-
nonce=nonce, key=key, read_preference=read_pref)
725-
self.connection._cache_credentials(self.name,
726-
unicode(name),
727-
unicode(password))
728-
return True
729-
except OperationFailure:
730-
return False
731-
finally:
732-
if not in_request:
733-
self.connection.end_request()
698+
credentials = (self.name, unicode(name), unicode(password))
699+
self.connection._cache_credentials(self.name, credentials)
700+
return True
701+
except OperationFailure:
702+
return False
734703

735704
def logout(self):
736-
"""Deauthorize use of this database for this connection
737-
and future connections.
705+
"""Deauthorize use of this database for this client instance.
738706
739707
.. note:: Other databases may still be authenticated, and other
740708
existing :class:`~socket.socket` connections may remain
741709
authenticated for this database unless you reset all sockets
742-
with :meth:`~pymongo.connection.Connection.disconnect`.
710+
with :meth:`~pymongo.mongo_client.MongoClient.disconnect`.
743711
"""
744-
self.command("logout")
712+
# Sockets will be deauthenticated as they are used.
745713
self.connection._purge_credentials(self.name)
746714

747715
def dereference(self, dbref):

pymongo/helpers.py

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,6 @@
1414

1515
"""Bits and pieces used by the driver that don't really fit elsewhere."""
1616

17-
try:
18-
import hashlib
19-
_md5func = hashlib.md5
20-
except: # for Python < 2.5
21-
import md5
22-
_md5func = md5.new
2317
import random
2418
import struct
2519

@@ -76,8 +70,8 @@ def _index_document(index_list):
7670
return index
7771

7872

79-
def _unpack_response(response, cursor_id=None,
80-
as_class=dict, tz_aware=False, uuid_subtype=OLD_UUID_SUBTYPE):
73+
def _unpack_response(response, cursor_id=None, as_class=dict,
74+
tz_aware=False, uuid_subtype=OLD_UUID_SUBTYPE):
8175
"""Unpack a response from the database.
8276
8377
Check the response for errors and unpack, returning a dictionary
@@ -115,7 +109,8 @@ def _unpack_response(response, cursor_id=None,
115109

116110

117111
def _check_command_response(response, reset, msg="%s", allowable_errors=[]):
118-
112+
"""Check the response to a command for errors.
113+
"""
119114
if not response["ok"]:
120115
if "wtimeout" in response and response["wtimeout"]:
121116
raise TimeoutError(msg % response["errmsg"])
@@ -147,34 +142,6 @@ def _check_command_response(response, reset, msg="%s", allowable_errors=[]):
147142
raise OperationFailure(msg % errmsg)
148143

149144

150-
def _password_digest(username, password):
151-
"""Get a password digest to use for authentication.
152-
"""
153-
if not isinstance(password, basestring):
154-
raise TypeError("password must be an instance "
155-
"of %s" % (basestring.__name__,))
156-
if len(password) == 0:
157-
raise TypeError("password can't be empty")
158-
if not isinstance(username, basestring):
159-
raise TypeError("username must be an instance "
160-
"of %s" % (basestring.__name__,))
161-
162-
md5hash = _md5func()
163-
data = "%s:mongo:%s" % (username, password)
164-
md5hash.update(data.encode('utf-8'))
165-
return unicode(md5hash.hexdigest())
166-
167-
168-
def _auth_key(nonce, username, password):
169-
"""Get an auth key to use for authentication.
170-
"""
171-
digest = _password_digest(username, password)
172-
md5hash = _md5func()
173-
data = "%s%s%s" % (nonce, unicode(username), digest)
174-
md5hash.update(data.encode('utf-8'))
175-
return unicode(md5hash.hexdigest())
176-
177-
178145
def _fields_list_to_dict(fields):
179146
"""Takes a list of field names and returns a matching dictionary.
180147
@@ -192,6 +159,7 @@ def _fields_list_to_dict(fields):
192159
as_dict[field] = 1
193160
return as_dict
194161

162+
195163
def shuffled(sequence):
196164
"""Returns a copy of the sequence (as a :class:`list`) which has been
197165
shuffled by :func:`random.shuffle`.

0 commit comments

Comments
 (0)