* Examples work * setup.py kinda updasted * Fork of txmongo but with new pymongo embedded
216 lines
7.8 KiB
Python
216 lines
7.8 KiB
Python
# Copyright 2013 10gen, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Authentication helpers."""
|
|
|
|
try:
|
|
import hashlib
|
|
_MD5 = hashlib.md5
|
|
except ImportError: # for Python < 2.5
|
|
import md5
|
|
_MD5 = md5.new
|
|
|
|
HAVE_KERBEROS = True
|
|
try:
|
|
import kerberos
|
|
except ImportError:
|
|
HAVE_KERBEROS = False
|
|
|
|
from asyncio_mongo._bson.binary import Binary
|
|
from asyncio_mongo._bson.son import SON
|
|
from asyncio_mongo._pymongo.errors import ConfigurationError, OperationFailure
|
|
|
|
|
|
MECHANISMS = ('GSSAPI', 'MONGODB-CR', 'MONGODB-X509', 'PLAIN')
|
|
"""The authentication mechanisms supported by PyMongo."""
|
|
|
|
|
|
def _build_credentials_tuple(mech, source, user, passwd, extra):
|
|
"""Build and return a mechanism specific credentials tuple.
|
|
"""
|
|
if mech == 'GSSAPI':
|
|
gsn = extra.get('gssapiservicename', 'mongodb')
|
|
# No password, source is always $external.
|
|
return (mech, '$external', user, gsn)
|
|
elif mech == 'MONGODB-X509':
|
|
return (mech, '$external', user)
|
|
return (mech, source, user, passwd)
|
|
|
|
|
|
def _password_digest(username, password):
|
|
"""Get a password digest to use for authentication.
|
|
"""
|
|
if not isinstance(password, str):
|
|
raise TypeError("password must be an instance "
|
|
"of %s" % (str.__name__,))
|
|
if len(password) == 0:
|
|
raise TypeError("password can't be empty")
|
|
if not isinstance(username, str):
|
|
raise TypeError("username must be an instance "
|
|
"of %s" % (str.__name__,))
|
|
|
|
md5hash = _MD5()
|
|
data = "%s:mongo:%s" % (username, password)
|
|
md5hash.update(data.encode('utf-8'))
|
|
return str(md5hash.hexdigest())
|
|
|
|
|
|
def _auth_key(nonce, username, password):
|
|
"""Get an auth key to use for authentication.
|
|
"""
|
|
digest = _password_digest(username, password)
|
|
md5hash = _MD5()
|
|
data = "%s%s%s" % (nonce, str(username), digest)
|
|
md5hash.update(data.encode('utf-8'))
|
|
return str(md5hash.hexdigest())
|
|
|
|
|
|
def _authenticate_gssapi(credentials, sock_info, cmd_func):
|
|
"""Authenticate using GSSAPI.
|
|
"""
|
|
try:
|
|
dummy, username, gsn = credentials
|
|
# Starting here and continuing through the while loop below - establish
|
|
# the security context. See RFC 4752, Section 3.1, first paragraph.
|
|
result, ctx = kerberos.authGSSClientInit(gsn + '@' + sock_info.host,
|
|
kerberos.GSS_C_MUTUAL_FLAG)
|
|
if result != kerberos.AUTH_GSS_COMPLETE:
|
|
raise OperationFailure('Kerberos context failed to initialize.')
|
|
|
|
try:
|
|
# pykerberos uses a weird mix of exceptions and return values
|
|
# to indicate errors.
|
|
# 0 == continue, 1 == complete, -1 == error
|
|
# Only authGSSClientStep can return 0.
|
|
if kerberos.authGSSClientStep(ctx, '') != 0:
|
|
raise OperationFailure('Unknown kerberos '
|
|
'failure in step function.')
|
|
|
|
# Start a SASL conversation with mongod/s
|
|
# Note: pykerberos deals with base64 encoded byte strings.
|
|
# Since mongo accepts base64 strings as the payload we don't
|
|
# have to use bson.binary.Binary.
|
|
payload = kerberos.authGSSClientResponse(ctx)
|
|
cmd = SON([('saslStart', 1),
|
|
('mechanism', 'GSSAPI'),
|
|
('payload', payload),
|
|
('autoAuthorize', 1)])
|
|
response, _ = cmd_func(sock_info, '$external', cmd)
|
|
|
|
# Limit how many times we loop to catch protocol / library issues
|
|
for _ in range(10):
|
|
result = kerberos.authGSSClientStep(ctx,
|
|
str(response['payload']))
|
|
if result == -1:
|
|
raise OperationFailure('Unknown kerberos '
|
|
'failure in step function.')
|
|
|
|
payload = kerberos.authGSSClientResponse(ctx) or ''
|
|
|
|
cmd = SON([('saslContinue', 1),
|
|
('conversationId', response['conversationId']),
|
|
('payload', payload)])
|
|
response, _ = cmd_func(sock_info, '$external', cmd)
|
|
|
|
if result == kerberos.AUTH_GSS_COMPLETE:
|
|
break
|
|
else:
|
|
raise OperationFailure('Kerberos '
|
|
'authentication failed to complete.')
|
|
|
|
# Once the security context is established actually authenticate.
|
|
# See RFC 4752, Section 3.1, last two paragraphs.
|
|
if kerberos.authGSSClientUnwrap(ctx,
|
|
str(response['payload'])) != 1:
|
|
raise OperationFailure('Unknown kerberos '
|
|
'failure during GSS_Unwrap step.')
|
|
|
|
if kerberos.authGSSClientWrap(ctx,
|
|
kerberos.authGSSClientResponse(ctx),
|
|
username) != 1:
|
|
raise OperationFailure('Unknown kerberos '
|
|
'failure during GSS_Wrap step.')
|
|
|
|
payload = kerberos.authGSSClientResponse(ctx)
|
|
cmd = SON([('saslContinue', 1),
|
|
('conversationId', response['conversationId']),
|
|
('payload', payload)])
|
|
response, _ = cmd_func(sock_info, '$external', cmd)
|
|
|
|
finally:
|
|
kerberos.authGSSClientClean(ctx)
|
|
|
|
except kerberos.KrbError as exc:
|
|
raise OperationFailure(str(exc))
|
|
|
|
|
|
def _authenticate_plain(credentials, sock_info, cmd_func):
|
|
"""Authenticate using SASL PLAIN (RFC 4616)
|
|
"""
|
|
source, username, password = credentials
|
|
payload = ('\x00%s\x00%s' % (username, password)).encode('utf-8')
|
|
cmd = SON([('saslStart', 1),
|
|
('mechanism', 'PLAIN'),
|
|
('payload', Binary(payload)),
|
|
('autoAuthorize', 1)])
|
|
cmd_func(sock_info, source, cmd)
|
|
|
|
|
|
def _authenticate_x509(credentials, sock_info, cmd_func):
|
|
"""Authenticate using MONGODB-X509.
|
|
"""
|
|
dummy, username = credentials
|
|
query = SON([('authenticate', 1),
|
|
('mechanism', 'MONGODB-X509'),
|
|
('user', username)])
|
|
cmd_func(sock_info, '$external', query)
|
|
|
|
|
|
def _authenticate_mongo_cr(credentials, sock_info, cmd_func):
|
|
"""Authenticate using MONGODB-CR.
|
|
"""
|
|
source, username, password = credentials
|
|
# Get a nonce
|
|
response, _ = cmd_func(sock_info, source, {'getnonce': 1})
|
|
nonce = response['nonce']
|
|
key = _auth_key(nonce, username, password)
|
|
|
|
# Actually authenticate
|
|
query = SON([('authenticate', 1),
|
|
('user', username),
|
|
('nonce', nonce),
|
|
('key', key)])
|
|
cmd_func(sock_info, source, query)
|
|
|
|
|
|
_AUTH_MAP = {
|
|
'GSSAPI': _authenticate_gssapi,
|
|
'MONGODB-CR': _authenticate_mongo_cr,
|
|
'MONGODB-X509': _authenticate_x509,
|
|
'PLAIN': _authenticate_plain,
|
|
}
|
|
|
|
|
|
def authenticate(credentials, sock_info, cmd_func):
|
|
"""Authenticate sock_info.
|
|
"""
|
|
mechanism = credentials[0]
|
|
if mechanism == 'GSSAPI':
|
|
if not HAVE_KERBEROS:
|
|
raise ConfigurationError('The "kerberos" module must be '
|
|
'installed to use GSSAPI authentication.')
|
|
auth_func = _AUTH_MAP.get(mechanism)
|
|
auth_func(credentials[1:], sock_info, cmd_func)
|
|
|