auth.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. # Copyright 2013-present MongoDB, 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. """Authentication helpers."""
  15. import functools
  16. import hashlib
  17. import hmac
  18. import os
  19. import socket
  20. try:
  21. from urllib import quote
  22. except ImportError:
  23. from urllib.parse import quote
  24. HAVE_KERBEROS = True
  25. _USE_PRINCIPAL = False
  26. try:
  27. import winkerberos as kerberos
  28. if tuple(map(int, kerberos.__version__.split('.')[:2])) >= (0, 5):
  29. _USE_PRINCIPAL = True
  30. except ImportError:
  31. try:
  32. import kerberos
  33. except ImportError:
  34. HAVE_KERBEROS = False
  35. from base64 import standard_b64decode, standard_b64encode
  36. from collections import namedtuple
  37. from bson.binary import Binary
  38. from bson.py3compat import string_type, _unicode, PY3
  39. from bson.son import SON
  40. from pymongo.auth_aws import _authenticate_aws
  41. from pymongo.errors import ConfigurationError, OperationFailure
  42. from pymongo.saslprep import saslprep
  43. MECHANISMS = frozenset(
  44. ['GSSAPI',
  45. 'MONGODB-CR',
  46. 'MONGODB-X509',
  47. 'MONGODB-AWS',
  48. 'PLAIN',
  49. 'SCRAM-SHA-1',
  50. 'SCRAM-SHA-256',
  51. 'DEFAULT'])
  52. """The authentication mechanisms supported by PyMongo."""
  53. class _Cache(object):
  54. __slots__ = ("data",)
  55. _hash_val = hash('_Cache')
  56. def __init__(self):
  57. self.data = None
  58. def __eq__(self, other):
  59. # Two instances must always compare equal.
  60. if isinstance(other, _Cache):
  61. return True
  62. return NotImplemented
  63. def __ne__(self, other):
  64. if isinstance(other, _Cache):
  65. return False
  66. return NotImplemented
  67. def __hash__(self):
  68. return self._hash_val
  69. MongoCredential = namedtuple(
  70. 'MongoCredential',
  71. ['mechanism',
  72. 'source',
  73. 'username',
  74. 'password',
  75. 'mechanism_properties',
  76. 'cache'])
  77. """A hashable namedtuple of values used for authentication."""
  78. GSSAPIProperties = namedtuple('GSSAPIProperties',
  79. ['service_name',
  80. 'canonicalize_host_name',
  81. 'service_realm'])
  82. """Mechanism properties for GSSAPI authentication."""
  83. _AWSProperties = namedtuple('AWSProperties', ['aws_session_token'])
  84. """Mechanism properties for MONGODB-AWS authentication."""
  85. def _build_credentials_tuple(mech, source, user, passwd, extra, database):
  86. """Build and return a mechanism specific credentials tuple.
  87. """
  88. if mech not in ('MONGODB-X509', 'MONGODB-AWS') and user is None:
  89. raise ConfigurationError("%s requires a username." % (mech,))
  90. if mech == 'GSSAPI':
  91. if source is not None and source != '$external':
  92. raise ValueError(
  93. "authentication source must be $external or None for GSSAPI")
  94. properties = extra.get('authmechanismproperties', {})
  95. service_name = properties.get('SERVICE_NAME', 'mongodb')
  96. canonicalize = properties.get('CANONICALIZE_HOST_NAME', False)
  97. service_realm = properties.get('SERVICE_REALM')
  98. props = GSSAPIProperties(service_name=service_name,
  99. canonicalize_host_name=canonicalize,
  100. service_realm=service_realm)
  101. # Source is always $external.
  102. return MongoCredential(mech, '$external', user, passwd, props, None)
  103. elif mech == 'MONGODB-X509':
  104. if passwd is not None:
  105. raise ConfigurationError(
  106. "Passwords are not supported by MONGODB-X509")
  107. if source is not None and source != '$external':
  108. raise ValueError(
  109. "authentication source must be "
  110. "$external or None for MONGODB-X509")
  111. # Source is always $external, user can be None.
  112. return MongoCredential(mech, '$external', user, None, None, None)
  113. elif mech == 'MONGODB-AWS':
  114. if user is not None and passwd is None:
  115. raise ConfigurationError(
  116. "username without a password is not supported by MONGODB-AWS")
  117. if source is not None and source != '$external':
  118. raise ConfigurationError(
  119. "authentication source must be "
  120. "$external or None for MONGODB-AWS")
  121. properties = extra.get('authmechanismproperties', {})
  122. aws_session_token = properties.get('AWS_SESSION_TOKEN')
  123. props = _AWSProperties(aws_session_token=aws_session_token)
  124. # user can be None for temporary link-local EC2 credentials.
  125. return MongoCredential(mech, '$external', user, passwd, props, None)
  126. elif mech == 'PLAIN':
  127. source_database = source or database or '$external'
  128. return MongoCredential(mech, source_database, user, passwd, None, None)
  129. else:
  130. source_database = source or database or 'admin'
  131. if passwd is None:
  132. raise ConfigurationError("A password is required.")
  133. return MongoCredential(
  134. mech, source_database, user, passwd, None, _Cache())
  135. if PY3:
  136. def _xor(fir, sec):
  137. """XOR two byte strings together (python 3.x)."""
  138. return b"".join([bytes([x ^ y]) for x, y in zip(fir, sec)])
  139. _from_bytes = int.from_bytes
  140. _to_bytes = int.to_bytes
  141. else:
  142. from binascii import (hexlify as _hexlify,
  143. unhexlify as _unhexlify)
  144. def _xor(fir, sec):
  145. """XOR two byte strings together (python 2.x)."""
  146. return b"".join([chr(ord(x) ^ ord(y)) for x, y in zip(fir, sec)])
  147. def _from_bytes(value, dummy, _int=int, _hexlify=_hexlify):
  148. """An implementation of int.from_bytes for python 2.x."""
  149. return _int(_hexlify(value), 16)
  150. def _to_bytes(value, length, dummy, _unhexlify=_unhexlify):
  151. """An implementation of int.to_bytes for python 2.x."""
  152. fmt = '%%0%dx' % (2 * length,)
  153. return _unhexlify(fmt % value)
  154. try:
  155. # The fastest option, if it's been compiled to use OpenSSL's HMAC.
  156. from backports.pbkdf2 import pbkdf2_hmac as _hi
  157. except ImportError:
  158. try:
  159. # Python 2.7.8+, or Python 3.4+.
  160. from hashlib import pbkdf2_hmac as _hi
  161. except ImportError:
  162. def _hi(hash_name, data, salt, iterations):
  163. """A simple implementation of PBKDF2-HMAC."""
  164. mac = hmac.HMAC(data, None, getattr(hashlib, hash_name))
  165. def _digest(msg, mac=mac):
  166. """Get a digest for msg."""
  167. _mac = mac.copy()
  168. _mac.update(msg)
  169. return _mac.digest()
  170. from_bytes = _from_bytes
  171. to_bytes = _to_bytes
  172. _u1 = _digest(salt + b'\x00\x00\x00\x01')
  173. _ui = from_bytes(_u1, 'big')
  174. for _ in range(iterations - 1):
  175. _u1 = _digest(_u1)
  176. _ui ^= from_bytes(_u1, 'big')
  177. return to_bytes(_ui, mac.digest_size, 'big')
  178. try:
  179. from hmac import compare_digest
  180. except ImportError:
  181. if PY3:
  182. def _xor_bytes(a, b):
  183. return a ^ b
  184. else:
  185. def _xor_bytes(a, b, _ord=ord):
  186. return _ord(a) ^ _ord(b)
  187. # Python 2.x < 2.7.7
  188. # Note: This method is intentionally obtuse to prevent timing attacks. Do
  189. # not refactor it!
  190. # References:
  191. # - http://bugs.python.org/issue14532
  192. # - http://bugs.python.org/issue14955
  193. # - http://bugs.python.org/issue15061
  194. def compare_digest(a, b, _xor_bytes=_xor_bytes):
  195. left = None
  196. right = b
  197. if len(a) == len(b):
  198. left = a
  199. result = 0
  200. if len(a) != len(b):
  201. left = b
  202. result = 1
  203. for x, y in zip(left, right):
  204. result |= _xor_bytes(x, y)
  205. return result == 0
  206. def _parse_scram_response(response):
  207. """Split a scram response into key, value pairs."""
  208. return dict(item.split(b"=", 1) for item in response.split(b","))
  209. def _authenticate_scram_start(credentials, mechanism):
  210. username = credentials.username
  211. user = username.encode("utf-8").replace(b"=", b"=3D").replace(b",", b"=2C")
  212. nonce = standard_b64encode(os.urandom(32))
  213. first_bare = b"n=" + user + b",r=" + nonce
  214. cmd = SON([('saslStart', 1),
  215. ('mechanism', mechanism),
  216. ('payload', Binary(b"n,," + first_bare)),
  217. ('autoAuthorize', 1),
  218. ('options', {'skipEmptyExchange': True})])
  219. return nonce, first_bare, cmd
  220. def _authenticate_scram(credentials, sock_info, mechanism):
  221. """Authenticate using SCRAM."""
  222. username = credentials.username
  223. if mechanism == 'SCRAM-SHA-256':
  224. digest = "sha256"
  225. digestmod = hashlib.sha256
  226. data = saslprep(credentials.password).encode("utf-8")
  227. else:
  228. digest = "sha1"
  229. digestmod = hashlib.sha1
  230. data = _password_digest(username, credentials.password).encode("utf-8")
  231. source = credentials.source
  232. cache = credentials.cache
  233. # Make local
  234. _hmac = hmac.HMAC
  235. ctx = sock_info.auth_ctx.get(credentials)
  236. if ctx and ctx.speculate_succeeded():
  237. nonce, first_bare = ctx.scram_data
  238. res = ctx.speculative_authenticate
  239. else:
  240. nonce, first_bare, cmd = _authenticate_scram_start(
  241. credentials, mechanism)
  242. res = sock_info.command(source, cmd)
  243. server_first = res['payload']
  244. parsed = _parse_scram_response(server_first)
  245. iterations = int(parsed[b'i'])
  246. if iterations < 4096:
  247. raise OperationFailure("Server returned an invalid iteration count.")
  248. salt = parsed[b's']
  249. rnonce = parsed[b'r']
  250. if not rnonce.startswith(nonce):
  251. raise OperationFailure("Server returned an invalid nonce.")
  252. without_proof = b"c=biws,r=" + rnonce
  253. if cache.data:
  254. client_key, server_key, csalt, citerations = cache.data
  255. else:
  256. client_key, server_key, csalt, citerations = None, None, None, None
  257. # Salt and / or iterations could change for a number of different
  258. # reasons. Either changing invalidates the cache.
  259. if not client_key or salt != csalt or iterations != citerations:
  260. salted_pass = _hi(
  261. digest, data, standard_b64decode(salt), iterations)
  262. client_key = _hmac(salted_pass, b"Client Key", digestmod).digest()
  263. server_key = _hmac(salted_pass, b"Server Key", digestmod).digest()
  264. cache.data = (client_key, server_key, salt, iterations)
  265. stored_key = digestmod(client_key).digest()
  266. auth_msg = b",".join((first_bare, server_first, without_proof))
  267. client_sig = _hmac(stored_key, auth_msg, digestmod).digest()
  268. client_proof = b"p=" + standard_b64encode(_xor(client_key, client_sig))
  269. client_final = b",".join((without_proof, client_proof))
  270. server_sig = standard_b64encode(
  271. _hmac(server_key, auth_msg, digestmod).digest())
  272. cmd = SON([('saslContinue', 1),
  273. ('conversationId', res['conversationId']),
  274. ('payload', Binary(client_final))])
  275. res = sock_info.command(source, cmd)
  276. parsed = _parse_scram_response(res['payload'])
  277. if not compare_digest(parsed[b'v'], server_sig):
  278. raise OperationFailure("Server returned an invalid signature.")
  279. # A third empty challenge may be required if the server does not support
  280. # skipEmptyExchange: SERVER-44857.
  281. if not res['done']:
  282. cmd = SON([('saslContinue', 1),
  283. ('conversationId', res['conversationId']),
  284. ('payload', Binary(b''))])
  285. res = sock_info.command(source, cmd)
  286. if not res['done']:
  287. raise OperationFailure('SASL conversation failed to complete.')
  288. def _password_digest(username, password):
  289. """Get a password digest to use for authentication.
  290. """
  291. if not isinstance(password, string_type):
  292. raise TypeError("password must be an "
  293. "instance of %s" % (string_type.__name__,))
  294. if len(password) == 0:
  295. raise ValueError("password can't be empty")
  296. if not isinstance(username, string_type):
  297. raise TypeError("password must be an "
  298. "instance of %s" % (string_type.__name__,))
  299. md5hash = hashlib.md5()
  300. data = "%s:mongo:%s" % (username, password)
  301. md5hash.update(data.encode('utf-8'))
  302. return _unicode(md5hash.hexdigest())
  303. def _auth_key(nonce, username, password):
  304. """Get an auth key to use for authentication.
  305. """
  306. digest = _password_digest(username, password)
  307. md5hash = hashlib.md5()
  308. data = "%s%s%s" % (nonce, username, digest)
  309. md5hash.update(data.encode('utf-8'))
  310. return _unicode(md5hash.hexdigest())
  311. def _canonicalize_hostname(hostname):
  312. """Canonicalize hostname following MIT-krb5 behavior."""
  313. # https://github.com/krb5/krb5/blob/d406afa363554097ac48646a29249c04f498c88e/src/util/k5test.py#L505-L520
  314. af, socktype, proto, canonname, sockaddr = socket.getaddrinfo(
  315. hostname, None, 0, 0, socket.IPPROTO_TCP, socket.AI_CANONNAME)[0]
  316. try:
  317. name = socket.getnameinfo(sockaddr, socket.NI_NAMEREQD)
  318. except socket.gaierror:
  319. return canonname.lower()
  320. return name[0].lower()
  321. def _authenticate_gssapi(credentials, sock_info):
  322. """Authenticate using GSSAPI.
  323. """
  324. if not HAVE_KERBEROS:
  325. raise ConfigurationError('The "kerberos" module must be '
  326. 'installed to use GSSAPI authentication.')
  327. try:
  328. username = credentials.username
  329. password = credentials.password
  330. props = credentials.mechanism_properties
  331. # Starting here and continuing through the while loop below - establish
  332. # the security context. See RFC 4752, Section 3.1, first paragraph.
  333. host = sock_info.address[0]
  334. if props.canonicalize_host_name:
  335. host = _canonicalize_hostname(host)
  336. service = props.service_name + '@' + host
  337. if props.service_realm is not None:
  338. service = service + '@' + props.service_realm
  339. if password is not None:
  340. if _USE_PRINCIPAL:
  341. # Note that, though we use unquote_plus for unquoting URI
  342. # options, we use quote here. Microsoft's UrlUnescape (used
  343. # by WinKerberos) doesn't support +.
  344. principal = ":".join((quote(username), quote(password)))
  345. result, ctx = kerberos.authGSSClientInit(
  346. service, principal, gssflags=kerberos.GSS_C_MUTUAL_FLAG)
  347. else:
  348. if '@' in username:
  349. user, domain = username.split('@', 1)
  350. else:
  351. user, domain = username, None
  352. result, ctx = kerberos.authGSSClientInit(
  353. service, gssflags=kerberos.GSS_C_MUTUAL_FLAG,
  354. user=user, domain=domain, password=password)
  355. else:
  356. result, ctx = kerberos.authGSSClientInit(
  357. service, gssflags=kerberos.GSS_C_MUTUAL_FLAG)
  358. if result != kerberos.AUTH_GSS_COMPLETE:
  359. raise OperationFailure('Kerberos context failed to initialize.')
  360. try:
  361. # pykerberos uses a weird mix of exceptions and return values
  362. # to indicate errors.
  363. # 0 == continue, 1 == complete, -1 == error
  364. # Only authGSSClientStep can return 0.
  365. if kerberos.authGSSClientStep(ctx, '') != 0:
  366. raise OperationFailure('Unknown kerberos '
  367. 'failure in step function.')
  368. # Start a SASL conversation with mongod/s
  369. # Note: pykerberos deals with base64 encoded byte strings.
  370. # Since mongo accepts base64 strings as the payload we don't
  371. # have to use bson.binary.Binary.
  372. payload = kerberos.authGSSClientResponse(ctx)
  373. cmd = SON([('saslStart', 1),
  374. ('mechanism', 'GSSAPI'),
  375. ('payload', payload),
  376. ('autoAuthorize', 1)])
  377. response = sock_info.command('$external', cmd)
  378. # Limit how many times we loop to catch protocol / library issues
  379. for _ in range(10):
  380. result = kerberos.authGSSClientStep(ctx,
  381. str(response['payload']))
  382. if result == -1:
  383. raise OperationFailure('Unknown kerberos '
  384. 'failure in step function.')
  385. payload = kerberos.authGSSClientResponse(ctx) or ''
  386. cmd = SON([('saslContinue', 1),
  387. ('conversationId', response['conversationId']),
  388. ('payload', payload)])
  389. response = sock_info.command('$external', cmd)
  390. if result == kerberos.AUTH_GSS_COMPLETE:
  391. break
  392. else:
  393. raise OperationFailure('Kerberos '
  394. 'authentication failed to complete.')
  395. # Once the security context is established actually authenticate.
  396. # See RFC 4752, Section 3.1, last two paragraphs.
  397. if kerberos.authGSSClientUnwrap(ctx,
  398. str(response['payload'])) != 1:
  399. raise OperationFailure('Unknown kerberos '
  400. 'failure during GSS_Unwrap step.')
  401. if kerberos.authGSSClientWrap(ctx,
  402. kerberos.authGSSClientResponse(ctx),
  403. username) != 1:
  404. raise OperationFailure('Unknown kerberos '
  405. 'failure during GSS_Wrap step.')
  406. payload = kerberos.authGSSClientResponse(ctx)
  407. cmd = SON([('saslContinue', 1),
  408. ('conversationId', response['conversationId']),
  409. ('payload', payload)])
  410. sock_info.command('$external', cmd)
  411. finally:
  412. kerberos.authGSSClientClean(ctx)
  413. except kerberos.KrbError as exc:
  414. raise OperationFailure(str(exc))
  415. def _authenticate_plain(credentials, sock_info):
  416. """Authenticate using SASL PLAIN (RFC 4616)
  417. """
  418. source = credentials.source
  419. username = credentials.username
  420. password = credentials.password
  421. payload = ('\x00%s\x00%s' % (username, password)).encode('utf-8')
  422. cmd = SON([('saslStart', 1),
  423. ('mechanism', 'PLAIN'),
  424. ('payload', Binary(payload)),
  425. ('autoAuthorize', 1)])
  426. sock_info.command(source, cmd)
  427. def _authenticate_cram_md5(credentials, sock_info):
  428. """Authenticate using CRAM-MD5 (RFC 2195)
  429. """
  430. source = credentials.source
  431. username = credentials.username
  432. password = credentials.password
  433. # The password used as the mac key is the
  434. # same as what we use for MONGODB-CR
  435. passwd = _password_digest(username, password)
  436. cmd = SON([('saslStart', 1),
  437. ('mechanism', 'CRAM-MD5'),
  438. ('payload', Binary(b'')),
  439. ('autoAuthorize', 1)])
  440. response = sock_info.command(source, cmd)
  441. # MD5 as implicit default digest for digestmod is deprecated
  442. # in python 3.4
  443. mac = hmac.HMAC(key=passwd.encode('utf-8'), digestmod=hashlib.md5)
  444. mac.update(response['payload'])
  445. challenge = username.encode('utf-8') + b' ' + mac.hexdigest().encode('utf-8')
  446. cmd = SON([('saslContinue', 1),
  447. ('conversationId', response['conversationId']),
  448. ('payload', Binary(challenge))])
  449. sock_info.command(source, cmd)
  450. def _authenticate_x509(credentials, sock_info):
  451. """Authenticate using MONGODB-X509.
  452. """
  453. ctx = sock_info.auth_ctx.get(credentials)
  454. if ctx and ctx.speculate_succeeded():
  455. # MONGODB-X509 is done after the speculative auth step.
  456. return
  457. cmd = _X509Context(credentials).speculate_command()
  458. if credentials.username is None and sock_info.max_wire_version < 5:
  459. raise ConfigurationError(
  460. "A username is required for MONGODB-X509 authentication "
  461. "when connected to MongoDB versions older than 3.4.")
  462. sock_info.command('$external', cmd)
  463. def _authenticate_mongo_cr(credentials, sock_info):
  464. """Authenticate using MONGODB-CR.
  465. """
  466. source = credentials.source
  467. username = credentials.username
  468. password = credentials.password
  469. # Get a nonce
  470. response = sock_info.command(source, {'getnonce': 1})
  471. nonce = response['nonce']
  472. key = _auth_key(nonce, username, password)
  473. # Actually authenticate
  474. query = SON([('authenticate', 1),
  475. ('user', username),
  476. ('nonce', nonce),
  477. ('key', key)])
  478. sock_info.command(source, query)
  479. def _authenticate_default(credentials, sock_info):
  480. if sock_info.max_wire_version >= 7:
  481. if credentials in sock_info.negotiated_mechanisms:
  482. mechs = sock_info.negotiated_mechanisms[credentials]
  483. else:
  484. source = credentials.source
  485. cmd = sock_info.hello_cmd()
  486. cmd['saslSupportedMechs'] = source + '.' + credentials.username
  487. mechs = sock_info.command(
  488. source, cmd, publish_events=False).get(
  489. 'saslSupportedMechs', [])
  490. if 'SCRAM-SHA-256' in mechs:
  491. return _authenticate_scram(credentials, sock_info, 'SCRAM-SHA-256')
  492. else:
  493. return _authenticate_scram(credentials, sock_info, 'SCRAM-SHA-1')
  494. elif sock_info.max_wire_version >= 3:
  495. return _authenticate_scram(credentials, sock_info, 'SCRAM-SHA-1')
  496. else:
  497. return _authenticate_mongo_cr(credentials, sock_info)
  498. _AUTH_MAP = {
  499. 'CRAM-MD5': _authenticate_cram_md5,
  500. 'GSSAPI': _authenticate_gssapi,
  501. 'MONGODB-CR': _authenticate_mongo_cr,
  502. 'MONGODB-X509': _authenticate_x509,
  503. 'MONGODB-AWS': _authenticate_aws,
  504. 'PLAIN': _authenticate_plain,
  505. 'SCRAM-SHA-1': functools.partial(
  506. _authenticate_scram, mechanism='SCRAM-SHA-1'),
  507. 'SCRAM-SHA-256': functools.partial(
  508. _authenticate_scram, mechanism='SCRAM-SHA-256'),
  509. 'DEFAULT': _authenticate_default,
  510. }
  511. class _AuthContext(object):
  512. def __init__(self, credentials):
  513. self.credentials = credentials
  514. self.speculative_authenticate = None
  515. @staticmethod
  516. def from_credentials(creds):
  517. spec_cls = _SPECULATIVE_AUTH_MAP.get(creds.mechanism)
  518. if spec_cls:
  519. return spec_cls(creds)
  520. return None
  521. def speculate_command(self):
  522. raise NotImplementedError
  523. def parse_response(self, hello):
  524. self.speculative_authenticate = hello.speculative_authenticate
  525. def speculate_succeeded(self):
  526. return bool(self.speculative_authenticate)
  527. class _ScramContext(_AuthContext):
  528. def __init__(self, credentials, mechanism):
  529. super(_ScramContext, self).__init__(credentials)
  530. self.scram_data = None
  531. self.mechanism = mechanism
  532. def speculate_command(self):
  533. nonce, first_bare, cmd = _authenticate_scram_start(
  534. self.credentials, self.mechanism)
  535. # The 'db' field is included only on the speculative command.
  536. cmd['db'] = self.credentials.source
  537. # Save for later use.
  538. self.scram_data = (nonce, first_bare)
  539. return cmd
  540. class _X509Context(_AuthContext):
  541. def speculate_command(self):
  542. cmd = SON([('authenticate', 1),
  543. ('mechanism', 'MONGODB-X509')])
  544. if self.credentials.username is not None:
  545. cmd['user'] = self.credentials.username
  546. return cmd
  547. _SPECULATIVE_AUTH_MAP = {
  548. 'MONGODB-X509': _X509Context,
  549. 'SCRAM-SHA-1': functools.partial(_ScramContext, mechanism='SCRAM-SHA-1'),
  550. 'SCRAM-SHA-256': functools.partial(_ScramContext,
  551. mechanism='SCRAM-SHA-256'),
  552. 'DEFAULT': functools.partial(_ScramContext, mechanism='SCRAM-SHA-256'),
  553. }
  554. def authenticate(credentials, sock_info):
  555. """Authenticate sock_info."""
  556. mechanism = credentials.mechanism
  557. auth_func = _AUTH_MAP.get(mechanism)
  558. auth_func(credentials, sock_info)
  559. def logout(source, sock_info):
  560. """Log out from a database."""
  561. sock_info.command(source, {'logout': 1})