~ubuntu-branches/ubuntu/wily/pymongo/wily-proposed

« back to all changes in this revision

Viewing changes to pymongo/auth.py

  • Committer: Package Import Robot
  • Author(s): Federico Ceratto
  • Date: 2015-04-26 22:43:13 UTC
  • mfrom: (24.1.5 sid)
  • Revision ID: package-import@ubuntu.com-20150426224313-0hga2jphvf0rrmfe
Tags: 3.0.1-1
New upstream release.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2013-2014 MongoDB, Inc.
 
1
# Copyright 2013-2015 MongoDB, Inc.
2
2
#
3
3
# Licensed under the Apache License, Version 2.0 (the "License");
4
4
# you may not use this file except in compliance with the License.
15
15
"""Authentication helpers."""
16
16
 
17
17
import hmac
18
 
try:
19
 
    import hashlib
20
 
    _MD5 = hashlib.md5
21
 
    _DMOD = _MD5
22
 
except ImportError:  # for Python < 2.5
23
 
    import md5
24
 
    _MD5 = md5.new
25
 
    _DMOD = md5
26
18
 
27
19
HAVE_KERBEROS = True
28
20
try:
30
22
except ImportError:
31
23
    HAVE_KERBEROS = False
32
24
 
 
25
from base64 import standard_b64decode, standard_b64encode
 
26
from collections import namedtuple
 
27
from hashlib import md5, sha1
 
28
from random import SystemRandom
 
29
 
33
30
from bson.binary import Binary
34
 
from bson.py3compat import b
 
31
from bson.py3compat import b, string_type, _unicode, PY3
35
32
from bson.son import SON
36
33
from pymongo.errors import ConfigurationError, OperationFailure
37
34
 
38
35
 
39
 
MECHANISMS = frozenset(['GSSAPI', 'MONGODB-CR', 'MONGODB-X509', 'PLAIN'])
 
36
MECHANISMS = frozenset(
 
37
    ['GSSAPI', 'MONGODB-CR', 'MONGODB-X509', 'PLAIN', 'SCRAM-SHA-1', 'DEFAULT'])
40
38
"""The authentication mechanisms supported by PyMongo."""
41
39
 
42
40
 
 
41
MongoCredential = namedtuple(
 
42
    'MongoCredential',
 
43
    ['mechanism', 'source', 'username', 'password', 'mechanism_properties'])
 
44
"""A hashable namedtuple of values used for authentication."""
 
45
 
 
46
 
 
47
GSSAPIProperties = namedtuple('GSSAPIProperties', ['service_name'])
 
48
"""Mechanism properties for GSSAPI authentication."""
 
49
 
 
50
 
43
51
def _build_credentials_tuple(mech, source, user, passwd, extra):
44
52
    """Build and return a mechanism specific credentials tuple.
45
53
    """
 
54
    user = _unicode(user)
46
55
    if mech == 'GSSAPI':
47
 
        gsn = extra.get('gssapiservicename', 'mongodb')
 
56
        properties = extra.get('authmechanismproperties', {})
 
57
        service_name = properties.get('SERVICE_NAME', 'mongodb')
 
58
        props = GSSAPIProperties(service_name=service_name)
48
59
        # No password, source is always $external.
49
 
        return (mech, '$external', user, gsn)
 
60
        return MongoCredential(mech, '$external', user, None, props)
50
61
    elif mech == 'MONGODB-X509':
51
 
        return (mech, '$external', user)
52
 
    return (mech, source, user, passwd)
 
62
        return MongoCredential(mech, '$external', user, None, None)
 
63
    else:
 
64
        if passwd is None:
 
65
            raise ConfigurationError("A password is required.")
 
66
        return MongoCredential(mech, source, user, _unicode(passwd), None)
 
67
 
 
68
 
 
69
if PY3:
 
70
    def _xor(fir, sec):
 
71
        """XOR two byte strings together (python 3.x)."""
 
72
        return b"".join([bytes([x ^ y]) for x, y in zip(fir, sec)])
 
73
 
 
74
 
 
75
    _from_bytes = int.from_bytes
 
76
    _to_bytes = int.to_bytes
 
77
else:
 
78
    from binascii import (hexlify as _hexlify,
 
79
                          unhexlify as _unhexlify)
 
80
 
 
81
 
 
82
    def _xor(fir, sec):
 
83
        """XOR two byte strings together (python 2.x)."""
 
84
        return b"".join([chr(ord(x) ^ ord(y)) for x, y in zip(fir, sec)])
 
85
 
 
86
 
 
87
    def _from_bytes(value, dummy, int=int, _hexlify=_hexlify):
 
88
        """An implementation of int.from_bytes for python 2.x."""
 
89
        return int(_hexlify(value), 16)
 
90
 
 
91
 
 
92
    def _to_bytes(value, dummy0, dummy1, _unhexlify=_unhexlify):
 
93
        """An implementation of int.to_bytes for python 2.x."""
 
94
        return _unhexlify('%040x' % value)
 
95
 
 
96
 
 
97
try:
 
98
    # The fastest option, if it's been compiled to use OpenSSL's HMAC.
 
99
    from backports.pbkdf2 import pbkdf2_hmac
 
100
 
 
101
    def _hi(data, salt, iterations):
 
102
        return pbkdf2_hmac('sha1', data, salt, iterations)
 
103
 
 
104
except ImportError:
 
105
    try:
 
106
        # Python 2.7.8+, or Python 3.4+.
 
107
        from hashlib import pbkdf2_hmac
 
108
 
 
109
        def _hi(data, salt, iterations):
 
110
            return pbkdf2_hmac('sha1', data, salt, iterations)
 
111
 
 
112
    except ImportError:
 
113
 
 
114
        def _hi(data, salt, iterations):
 
115
            """A simple implementation of PBKDF2."""
 
116
            mac = hmac.HMAC(data, None, sha1)
 
117
 
 
118
            def _digest(msg, mac=mac):
 
119
                """Get a digest for msg."""
 
120
                _mac = mac.copy()
 
121
                _mac.update(msg)
 
122
                return _mac.digest()
 
123
 
 
124
            from_bytes = _from_bytes
 
125
            to_bytes = _to_bytes
 
126
 
 
127
            _u1 = _digest(salt + b'\x00\x00\x00\x01')
 
128
            _ui = from_bytes(_u1, 'big')
 
129
            for _ in range(iterations - 1):
 
130
                _u1 = _digest(_u1)
 
131
                _ui ^= from_bytes(_u1, 'big')
 
132
            return to_bytes(_ui, 20, 'big')
 
133
 
 
134
 
 
135
def _parse_scram_response(response):
 
136
    """Split a scram response into key, value pairs."""
 
137
    return dict(item.split(b"=", 1) for item in response.split(b","))
 
138
 
 
139
 
 
140
def _authenticate_scram_sha1(credentials, sock_info):
 
141
    """Authenticate using SCRAM-SHA-1."""
 
142
    username = credentials.username
 
143
    password = credentials.password
 
144
    source = credentials.source
 
145
 
 
146
    # Make local
 
147
    _hmac = hmac.HMAC
 
148
    _sha1 = sha1
 
149
 
 
150
    user = username.encode("utf-8").replace(b"=", b"=3D").replace(b",", b"=2C")
 
151
    nonce = standard_b64encode(
 
152
        (("%s" % (SystemRandom().random(),))[2:]).encode("utf-8"))
 
153
    first_bare = b"n=" + user + b",r=" + nonce
 
154
 
 
155
    cmd = SON([('saslStart', 1),
 
156
               ('mechanism', 'SCRAM-SHA-1'),
 
157
               ('payload', Binary(b"n,," + first_bare)),
 
158
               ('autoAuthorize', 1)])
 
159
    res = sock_info.command(source, cmd)
 
160
 
 
161
    server_first = res['payload']
 
162
    parsed = _parse_scram_response(server_first)
 
163
    iterations = int(parsed[b'i'])
 
164
    salt = parsed[b's']
 
165
    rnonce = parsed[b'r']
 
166
    if not rnonce.startswith(nonce):
 
167
        raise OperationFailure("Server returned an invalid nonce.")
 
168
 
 
169
    without_proof = b"c=biws,r=" + rnonce
 
170
    salted_pass = _hi(_password_digest(username, password).encode("utf-8"),
 
171
                      standard_b64decode(salt),
 
172
                      iterations)
 
173
    client_key = _hmac(salted_pass, b"Client Key", _sha1).digest()
 
174
    stored_key = _sha1(client_key).digest()
 
175
    auth_msg = b",".join((first_bare, server_first, without_proof))
 
176
    client_sig = _hmac(stored_key, auth_msg, _sha1).digest()
 
177
    client_proof = b"p=" + standard_b64encode(_xor(client_key, client_sig))
 
178
    client_final = b",".join((without_proof, client_proof))
 
179
 
 
180
    server_key = _hmac(salted_pass, b"Server Key", _sha1).digest()
 
181
    server_sig = standard_b64encode(
 
182
        _hmac(server_key, auth_msg, _sha1).digest())
 
183
 
 
184
    cmd = SON([('saslContinue', 1),
 
185
               ('conversationId', res['conversationId']),
 
186
               ('payload', Binary(client_final))])
 
187
    res = sock_info.command(source, cmd)
 
188
 
 
189
    parsed = _parse_scram_response(res['payload'])
 
190
    if parsed[b'v'] != server_sig:
 
191
        raise OperationFailure("Server returned an invalid signature.")
 
192
 
 
193
    # Depending on how it's configured, Cyrus SASL (which the server uses)
 
194
    # requires a third empty challenge.
 
195
    if not res['done']:
 
196
        cmd = SON([('saslContinue', 1),
 
197
                   ('conversationId', res['conversationId']),
 
198
                   ('payload', Binary(b''))])
 
199
        res = sock_info.command(source, cmd)
 
200
        if not res['done']:
 
201
            raise OperationFailure('SASL conversation failed to complete.')
53
202
 
54
203
 
55
204
def _password_digest(username, password):
56
205
    """Get a password digest to use for authentication.
57
206
    """
58
 
    if not isinstance(password, basestring):
59
 
        raise TypeError("password must be an instance "
60
 
                        "of %s" % (basestring.__name__,))
 
207
    if not isinstance(password, string_type):
 
208
        raise TypeError("password must be an "
 
209
                        "instance of %s" % (string_type.__name__,))
61
210
    if len(password) == 0:
62
211
        raise ValueError("password can't be empty")
63
 
    if not isinstance(username, basestring):
64
 
        raise TypeError("username must be an instance "
65
 
                        "of %s" % (basestring.__name__,))
 
212
    if not isinstance(username, string_type):
 
213
        raise TypeError("password must be an "
 
214
                        "instance of  %s" % (string_type.__name__,))
66
215
 
67
 
    md5hash = _MD5()
 
216
    md5hash = md5()
68
217
    data = "%s:mongo:%s" % (username, password)
69
218
    md5hash.update(data.encode('utf-8'))
70
 
    return unicode(md5hash.hexdigest())
 
219
    return _unicode(md5hash.hexdigest())
71
220
 
72
221
 
73
222
def _auth_key(nonce, username, password):
74
223
    """Get an auth key to use for authentication.
75
224
    """
76
225
    digest = _password_digest(username, password)
77
 
    md5hash = _MD5()
78
 
    data = "%s%s%s" % (nonce, unicode(username), digest)
 
226
    md5hash = md5()
 
227
    data = "%s%s%s" % (nonce, username, digest)
79
228
    md5hash.update(data.encode('utf-8'))
80
 
    return unicode(md5hash.hexdigest())
81
 
 
82
 
 
83
 
def _authenticate_gssapi(credentials, sock_info, cmd_func):
 
229
    return _unicode(md5hash.hexdigest())
 
230
 
 
231
 
 
232
def _authenticate_gssapi(credentials, sock_info):
84
233
    """Authenticate using GSSAPI.
85
234
    """
 
235
    if not HAVE_KERBEROS:
 
236
        raise ConfigurationError('The "kerberos" module must be '
 
237
                                 'installed to use GSSAPI authentication.')
 
238
 
86
239
    try:
87
 
        dummy, username, gsn = credentials
 
240
        username = credentials.username
 
241
        gsn = credentials.mechanism_properties.service_name
88
242
        # Starting here and continuing through the while loop below - establish
89
243
        # the security context. See RFC 4752, Section 3.1, first paragraph.
90
244
        result, ctx = kerberos.authGSSClientInit(
111
265
                       ('mechanism', 'GSSAPI'),
112
266
                       ('payload', payload),
113
267
                       ('autoAuthorize', 1)])
114
 
            response, _ = cmd_func(sock_info, '$external', cmd)
 
268
            response = sock_info.command('$external', cmd)
115
269
 
116
270
            # Limit how many times we loop to catch protocol / library issues
117
 
            for _ in xrange(10):
 
271
            for _ in range(10):
118
272
                result = kerberos.authGSSClientStep(ctx,
119
273
                                                    str(response['payload']))
120
274
                if result == -1:
126
280
                cmd = SON([('saslContinue', 1),
127
281
                           ('conversationId', response['conversationId']),
128
282
                           ('payload', payload)])
129
 
                response, _ = cmd_func(sock_info, '$external', cmd)
 
283
                response = sock_info.command('$external', cmd)
130
284
 
131
285
                if result == kerberos.AUTH_GSS_COMPLETE:
132
286
                    break
151
305
            cmd = SON([('saslContinue', 1),
152
306
                       ('conversationId', response['conversationId']),
153
307
                       ('payload', payload)])
154
 
            response, _ = cmd_func(sock_info, '$external', cmd)
 
308
            sock_info.command('$external', cmd)
155
309
 
156
310
        finally:
157
311
            kerberos.authGSSClientClean(ctx)
158
312
 
159
 
    except kerberos.KrbError, exc:
 
313
    except kerberos.KrbError as exc:
160
314
        raise OperationFailure(str(exc))
161
315
 
162
316
 
163
 
def _authenticate_plain(credentials, sock_info, cmd_func):
 
317
def _authenticate_plain(credentials, sock_info):
164
318
    """Authenticate using SASL PLAIN (RFC 4616)
165
319
    """
166
 
    source, username, password = credentials
 
320
    source = credentials.source
 
321
    username = credentials.username
 
322
    password = credentials.password
167
323
    payload = ('\x00%s\x00%s' % (username, password)).encode('utf-8')
168
324
    cmd = SON([('saslStart', 1),
169
325
               ('mechanism', 'PLAIN'),
170
326
               ('payload', Binary(payload)),
171
327
               ('autoAuthorize', 1)])
172
 
    cmd_func(sock_info, source, cmd)
173
 
 
174
 
 
175
 
def _authenticate_cram_md5(credentials, sock_info, cmd_func):
 
328
    sock_info.command(source, cmd)
 
329
 
 
330
 
 
331
def _authenticate_cram_md5(credentials, sock_info):
176
332
    """Authenticate using CRAM-MD5 (RFC 2195)
177
333
    """
178
 
    source, username, password = credentials
 
334
    source = credentials.source
 
335
    username = credentials.username
 
336
    password = credentials.password
179
337
    # The password used as the mac key is the
180
338
    # same as what we use for MONGODB-CR
181
339
    passwd = _password_digest(username, password)
182
340
    cmd = SON([('saslStart', 1),
183
341
               ('mechanism', 'CRAM-MD5'),
184
 
               ('payload', Binary(b(''))),
 
342
               ('payload', Binary(b'')),
185
343
               ('autoAuthorize', 1)])
186
 
    response, _ = cmd_func(sock_info, source, cmd)
 
344
    response = sock_info.command(source, cmd)
187
345
    # MD5 as implicit default digest for digestmod is deprecated
188
346
    # in python 3.4
189
 
    mac = hmac.HMAC(key=passwd.encode('utf-8'), digestmod=_DMOD)
 
347
    mac = hmac.HMAC(key=passwd.encode('utf-8'), digestmod=md5)
190
348
    mac.update(response['payload'])
191
 
    challenge = username.encode('utf-8') + b(' ') + b(mac.hexdigest())
 
349
    challenge = username.encode('utf-8') + b' ' + b(mac.hexdigest())
192
350
    cmd = SON([('saslContinue', 1),
193
351
               ('conversationId', response['conversationId']),
194
352
               ('payload', Binary(challenge))])
195
 
    cmd_func(sock_info, source, cmd)
196
 
 
197
 
 
198
 
def _authenticate_x509(credentials, sock_info, cmd_func):
 
353
    sock_info.command(source, cmd)
 
354
 
 
355
 
 
356
def _authenticate_x509(credentials, sock_info):
199
357
    """Authenticate using MONGODB-X509.
200
358
    """
201
 
    dummy, username = credentials
202
359
    query = SON([('authenticate', 1),
203
360
                 ('mechanism', 'MONGODB-X509'),
204
 
                 ('user', username)])
205
 
    cmd_func(sock_info, '$external', query)
206
 
 
207
 
 
208
 
def _authenticate_mongo_cr(credentials, sock_info, cmd_func):
 
361
                 ('user', credentials.username)])
 
362
    sock_info.command('$external', query)
 
363
 
 
364
 
 
365
def _authenticate_mongo_cr(credentials, sock_info):
209
366
    """Authenticate using MONGODB-CR.
210
367
    """
211
 
    source, username, password = credentials
 
368
    source = credentials.source
 
369
    username = credentials.username
 
370
    password = credentials.password
212
371
    # Get a nonce
213
 
    response, _ = cmd_func(sock_info, source, {'getnonce': 1})
 
372
    response = sock_info.command(source, {'getnonce': 1})
214
373
    nonce = response['nonce']
215
374
    key = _auth_key(nonce, username, password)
216
375
 
219
378
                 ('user', username),
220
379
                 ('nonce', nonce),
221
380
                 ('key', key)])
222
 
    cmd_func(sock_info, source, query)
 
381
    sock_info.command(source, query)
 
382
 
 
383
 
 
384
def _authenticate_default(credentials, sock_info):
 
385
    if sock_info.max_wire_version >= 3:
 
386
        return _authenticate_scram_sha1(credentials, sock_info)
 
387
    else:
 
388
        return _authenticate_mongo_cr(credentials, sock_info)
223
389
 
224
390
 
225
391
_AUTH_MAP = {
228
394
    'MONGODB-CR': _authenticate_mongo_cr,
229
395
    'MONGODB-X509': _authenticate_x509,
230
396
    'PLAIN': _authenticate_plain,
 
397
    'SCRAM-SHA-1': _authenticate_scram_sha1,
 
398
    'DEFAULT': _authenticate_default,
231
399
}
232
400
 
233
401
 
234
 
def authenticate(credentials, sock_info, cmd_func):
235
 
    """Authenticate sock_info.
236
 
    """
237
 
    mechanism = credentials[0]
238
 
    if mechanism == 'GSSAPI':
239
 
        if not HAVE_KERBEROS:
240
 
            raise ConfigurationError('The "kerberos" module must be '
241
 
                                     'installed to use GSSAPI authentication.')
 
402
def authenticate(credentials, sock_info):
 
403
    """Authenticate sock_info."""
 
404
    mechanism = credentials.mechanism
242
405
    auth_func = _AUTH_MAP.get(mechanism)
243
 
    auth_func(credentials[1:], sock_info, cmd_func)
244
 
 
 
406
    auth_func(credentials, sock_info)
 
407
 
 
408
 
 
409
def logout(source, sock_info):
 
410
    """Log out from a database."""
 
411
    sock_info.command(source, {'logout': 1})