PYTHON-1425 - Implement the auth spec and tests

This commit is contained in:
Bernie Hackett 2018-04-06 12:10:22 -07:00
parent 94c680c7da
commit 984977ba9e
8 changed files with 586 additions and 18 deletions

View File

@ -10,6 +10,8 @@ Version 3.7 adds support for MongoDB 4.0. Highlights include:
- Support for the SCRAM-SHA-256 authentication mechanism.
- Support for Python 3.7.
- MD5 is now optional in GridFS.
- If not specified, the authSource for the PLAIN authentication mechanism
defaults to $external.
- wtimeoutMS is once again supported as a URI option.
- Deprecate the snapshot option of :meth:`~pymongo.collection.Collection.find`
and :meth:`~pymongo.collection.Collection.find_one`. The option was

View File

@ -71,10 +71,15 @@ GSSAPIProperties = namedtuple('GSSAPIProperties',
"""Mechanism properties for GSSAPI authentication."""
def _build_credentials_tuple(mech, source, user, passwd, extra):
def _build_credentials_tuple(mech, source, user, passwd, extra, database):
"""Build and return a mechanism specific credentials tuple.
"""
if mech != 'MONGODB-X509' and user is None:
raise ConfigurationError("%s requires a username." % (mech,))
if mech == 'GSSAPI':
if source is not None and source != '$external':
raise ValueError(
"authentication source must be $external or None for GSSAPI")
properties = extra.get('authmechanismproperties', {})
service_name = properties.get('SERVICE_NAME', 'mongodb')
canonicalize = properties.get('CANONICALIZE_HOST_NAME', False)
@ -85,12 +90,23 @@ def _build_credentials_tuple(mech, source, user, passwd, extra):
# Source is always $external.
return MongoCredential(mech, '$external', user, passwd, props)
elif mech == 'MONGODB-X509':
if passwd is not None:
raise ConfigurationError(
"Passwords are not supported by MONGODB-X509")
if source is not None and source != '$external':
raise ValueError(
"authentication source must be "
"$external or None for MONGODB-X509")
# user can be None.
return MongoCredential(mech, '$external', user, None, None)
elif mech == 'PLAIN':
source_database = source or database or '$external'
return MongoCredential(mech, source_database, user, passwd, None)
else:
source_database = source or database or 'admin'
if passwd is None:
raise ConfigurationError("A password is required.")
return MongoCredential(mech, source, user, passwd, None)
return MongoCredential(mech, source_database, user, passwd, None)
if PY3:

View File

@ -30,12 +30,12 @@ from pymongo.write_concern import WriteConcern
def _parse_credentials(username, password, database, options):
"""Parse authentication credentials."""
mechanism = options.get('authmechanism', 'DEFAULT')
if username is None and mechanism != 'MONGODB-X509':
return None
source = options.get('authsource', database or 'admin')
return _build_credentials_tuple(
mechanism, source, username, password, options)
mechanism = options.get('authmechanism', 'DEFAULT' if username else None)
source = options.get('authsource')
if username or mechanism:
return _build_credentials_tuple(
mechanism, source, username, password, options, database)
return None
def _parse_read_preference(options):

View File

@ -1166,10 +1166,11 @@ class Database(common.BaseObject):
credentials = auth._build_credentials_tuple(
mechanism,
source or self.name,
source,
name,
password,
validated_options)
validated_options,
self.name)
self.client._cache_credentials(
self.name,

View File

@ -0,0 +1,453 @@
{
"tests": [
{
"description": "should use the default source and mechanism",
"uri": "mongodb://user:password@localhost",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "admin"
},
"options": null
},
{
"description": "should use the database when no authSource is specified",
"uri": "mongodb://user:password@localhost/foo",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "foo"
},
"options": null
},
{
"description": "should use the authSource when specified",
"uri": "mongodb://user:password@localhost/foo?authSource=bar",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "bar"
},
"options": null
},
{
"description": "should recognise the mechanism (GSSAPI)",
"uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user@DOMAIN.COM",
"password": null,
"db": "$external"
},
"options": {
"authmechanism": "GSSAPI"
}
},
{
"description": "should ignore the database (GSSAPI)",
"uri": "mongodb://user%40DOMAIN.COM@localhost/foo?authMechanism=GSSAPI",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user@DOMAIN.COM",
"password": null,
"db": "$external"
},
"options": {
"authmechanism": "GSSAPI"
}
},
{
"description": "should accept valid authSource (GSSAPI)",
"uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=$external",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user@DOMAIN.COM",
"password": null,
"db": "$external"
},
"options": {
"authmechanism": "GSSAPI"
}
},
{
"description": "should accept generic mechanism property (GSSAPI)",
"uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:other,CANONICALIZE_HOST_NAME:true",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user@DOMAIN.COM",
"password": null,
"db": "$external"
},
"options": {
"authmechanism": "GSSAPI",
"authmechanismproperties": {
"SERVICE_NAME": "other",
"CANONICALIZE_HOST_NAME": true
}
}
},
{
"description": "should accept the password (GSSAPI)",
"uri": "mongodb://user%40DOMAIN.COM:password@localhost/?authMechanism=GSSAPI&authSource=$external",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user@DOMAIN.COM",
"password": "password",
"db": "$external"
},
"options": {
"authmechanism": "GSSAPI"
}
},
{
"description": "may support deprecated gssapiServiceName option (GSSAPI)",
"uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&gssapiServiceName=other",
"hosts": null,
"valid": true,
"warning": false,
"optional": true,
"auth": {
"username": "user@DOMAIN.COM",
"password": null,
"db": "$external"
},
"options": {
"authmechanism": "GSSAPI",
"authmechanismproperties": {
"SERVICE_NAME": "other"
}
}
},
{
"description": "should throw an exception if authSource is invalid (GSSAPI)",
"uri": "mongodb://user%40DOMAIN.COM@localhost/?authMechanism=GSSAPI&authSource=foo",
"hosts": null,
"valid": false,
"warning": false,
"auth": null,
"options": null
},
{
"description": "should throw an exception if no username (GSSAPI)",
"uri": "mongodb://localhost/?authMechanism=GSSAPI",
"hosts": null,
"valid": false,
"warning": false,
"auth": null,
"options": null
},
{
"description": "should recognize the mechanism (MONGODB-CR)",
"uri": "mongodb://user:password@localhost/?authMechanism=MONGODB-CR",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "admin"
},
"options": {
"authmechanism": "MONGODB-CR"
}
},
{
"description": "should use the database when no authSource is specified (MONGODB-CR)",
"uri": "mongodb://user:password@localhost/foo?authMechanism=MONGODB-CR",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "foo"
},
"options": {
"authmechanism": "MONGODB-CR"
}
},
{
"description": "should use the authSource when specified (MONGODB-CR)",
"uri": "mongodb://user:password@localhost/foo?authMechanism=MONGODB-CR&authSource=bar",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "bar"
},
"options": {
"authmechanism": "MONGODB-CR"
}
},
{
"description": "should throw an exception if no username is supplied (MONGODB-CR)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-CR",
"hosts": null,
"valid": false,
"warning": false,
"auth": null,
"options": null
},
{
"description": "should recognize the mechanism (MONGODB-X509)",
"uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry",
"password": null,
"db": "$external"
},
"options": {
"authmechanism": "MONGODB-X509"
}
},
{
"description": "should ignore the database (MONGODB-X509)",
"uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry",
"password": null,
"db": "$external"
},
"options": {
"authmechanism": "MONGODB-X509"
}
},
{
"description": "should accept valid authSource (MONGODB-X509)",
"uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/?authMechanism=MONGODB-X509&authSource=$external",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "CN=myName,OU=myOrgUnit,O=myOrg,L=myLocality,ST=myState,C=myCountry",
"password": null,
"db": "$external"
},
"options": {
"authmechanism": "MONGODB-X509"
}
},
{
"description": "should recognize the mechanism with no username (MONGODB-X509)",
"uri": "mongodb://localhost/?authMechanism=MONGODB-X509",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": null,
"password": null,
"db": "$external"
},
"options": {
"authmechanism": "MONGODB-X509"
}
},
{
"description": "should throw an exception if supplied a password (MONGODB-X509)",
"uri": "mongodb://user:password@localhost/?authMechanism=MONGODB-X509",
"hosts": null,
"valid": false,
"warning": false,
"auth": null,
"options": null
},
{
"description": "should throw an exception if authSource is invalid (MONGODB-X509)",
"uri": "mongodb://CN%3DmyName%2COU%3DmyOrgUnit%2CO%3DmyOrg%2CL%3DmyLocality%2CST%3DmyState%2CC%3DmyCountry@localhost/foo?authMechanism=MONGODB-X509&authSource=bar",
"hosts": null,
"valid": false,
"warning": false,
"auth": null,
"options": null
},
{
"description": "should recognize the mechanism (PLAIN)",
"uri": "mongodb://user:password@localhost/?authMechanism=PLAIN",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "$external"
},
"options": {
"authmechanism": "PLAIN"
}
},
{
"description": "should use the database when no authSource is specified (PLAIN)",
"uri": "mongodb://user:password@localhost/foo?authMechanism=PLAIN",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "foo"
},
"options": {
"authmechanism": "PLAIN"
}
},
{
"description": "should use the authSource when specified (PLAIN)",
"uri": "mongodb://user:password@localhost/foo?authMechanism=PLAIN&authSource=bar",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "bar"
},
"options": {
"authmechanism": "PLAIN"
}
},
{
"description": "should throw an exception if no username (PLAIN)",
"uri": "mongodb://localhost/?authMechanism=PLAIN",
"hosts": null,
"valid": false,
"warning": false,
"auth": null,
"options": null
},
{
"description": "should recognize the mechanism (SCRAM-SHA-1)",
"uri": "mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-1",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "admin"
},
"options": {
"authmechanism": "SCRAM-SHA-1"
}
},
{
"description": "should use the database when no authSource is specified (SCRAM-SHA-1)",
"uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "foo"
},
"options": {
"authmechanism": "SCRAM-SHA-1"
}
},
{
"description": "should accept valid authSource (SCRAM-SHA-1)",
"uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-1&authSource=bar",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "bar"
},
"options": {
"authmechanism": "SCRAM-SHA-1"
}
},
{
"description": "should throw an exception if no username (SCRAM-SHA-1)",
"uri": "mongodb://localhost/?authMechanism=SCRAM-SHA-1",
"hosts": null,
"valid": false,
"warning": false,
"auth": null,
"options": null
},
{
"description": "should recognize the mechanism (SCRAM-SHA-256)",
"uri": "mongodb://user:password@localhost/?authMechanism=SCRAM-SHA-256",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "admin"
},
"options": {
"authmechanism": "SCRAM-SHA-256"
}
},
{
"description": "should use the database when no authSource is specified (SCRAM-SHA-256)",
"uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "foo"
},
"options": {
"authmechanism": "SCRAM-SHA-256"
}
},
{
"description": "should accept valid authSource (SCRAM-SHA-256)",
"uri": "mongodb://user:password@localhost/foo?authMechanism=SCRAM-SHA-256&authSource=bar",
"hosts": null,
"valid": true,
"warning": false,
"auth": {
"username": "user",
"password": "password",
"db": "bar"
},
"options": {
"authmechanism": "SCRAM-SHA-256"
}
},
{
"description": "should throw an exception if no username (SCRAM-SHA-256)",
"uri": "mongodb://localhost/?authMechanism=SCRAM-SHA-256",
"hosts": null,
"valid": false,
"warning": false,
"auth": null,
"options": null
}
]
}

View File

@ -97,19 +97,19 @@ class TestGSSAPI(unittest.TestCase):
def test_credentials_hashing(self):
# GSSAPI credentials are properly hashed.
creds0 = _build_credentials_tuple(
'GSSAPI', '', 'user', 'pass', {})
'GSSAPI', None, 'user', 'pass', {}, None)
creds1 = _build_credentials_tuple(
'GSSAPI', '', 'user', 'pass',
{'authmechanismproperties': {'SERVICE_NAME': 'A'}})
'GSSAPI', None, 'user', 'pass',
{'authmechanismproperties': {'SERVICE_NAME': 'A'}}, None)
creds2 = _build_credentials_tuple(
'GSSAPI', '', 'user', 'pass',
{'authmechanismproperties': {'SERVICE_NAME': 'A'}})
'GSSAPI', None, 'user', 'pass',
{'authmechanismproperties': {'SERVICE_NAME': 'A'}}, None)
creds3 = _build_credentials_tuple(
'GSSAPI', '', 'user', 'pass',
{'authmechanismproperties': {'SERVICE_NAME': 'B'}})
'GSSAPI', None, 'user', 'pass',
{'authmechanismproperties': {'SERVICE_NAME': 'B'}}, None)
self.assertEqual(1, len(set([creds1, creds2])))
self.assertEqual(3, len(set([creds0, creds1, creds2, creds3])))

96
test/test_auth_spec.py Normal file
View File

@ -0,0 +1,96 @@
# Copyright 2018-present MongoDB, 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.
"""Run the auth spec tests."""
import glob
import json
import os
import sys
sys.path[0:0] = [""]
from pymongo import MongoClient
from test import unittest
_TEST_PATH = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'auth')
class TestAuthSpec(unittest.TestCase):
pass
def create_test(test_case):
def run_test(self):
uri = test_case['uri']
valid = test_case['valid']
auth = test_case['auth']
options = test_case['options']
if not valid:
self.assertRaises(Exception, MongoClient, uri, connect=False)
else:
client = MongoClient(uri, connect=False)
credentials = client._MongoClient__options.credentials
if auth is not None:
self.assertEqual(credentials.username, auth['username'])
self.assertEqual(credentials.password, auth['password'])
self.assertEqual(credentials.source, auth['db'])
if options is not None:
if 'authmechanism' in options:
self.assertEqual(
credentials.mechanism, options['authmechanism'])
else:
self.assertEqual(credentials.mechanism, 'DEFAULT')
if 'authmechanismproperties' in options:
expected = options['authmechanismproperties']
actual = credentials.mechanism_properties
if 'SERVICE_NAME' in expected:
self.assertEqual(
actual.service_name, expected['SERVICE_NAME'])
if 'CANONICALIZE_HOST_NAME' in expected:
self.assertEqual(
actual.canonicalize_host_name,
expected['CANONICALIZE_HOST_NAME'])
if 'SERVICE_REALM' in expected:
self.assertEqual(
actual.service_realm, expected['SERVICE_REALM'])
return run_test
def create_tests():
for filename in glob.glob(os.path.join(_TEST_PATH, '*.json')):
test_suffix, _ = os.path.splitext(os.path.basename(filename))
with open(filename) as auth_tests:
test_cases = json.load(auth_tests)['tests']
for test_case in test_cases:
if test_case.get('optional', False):
continue
test_method = create_test(test_case)
name = str(test_case['description'].lower().replace(' ', '_'))
setattr(
TestAuthSpec,
'test_%s_%s' % (test_suffix, name),
test_method)
create_tests()
if __name__ == "__main__":
unittest.main()

View File

@ -1095,7 +1095,7 @@ class TestClient(IntegrationTest):
# Simulate an authenticate() call on a different socket.
credentials = auth._build_credentials_tuple(
'DEFAULT', 'admin', db_user, db_pwd, {})
'DEFAULT', 'admin', db_user, db_pwd, {}, None)
c._cache_credentials('test', credentials, connect=False)