diff --git a/doc/changelog.rst b/doc/changelog.rst index 543e99c19..de85ea14a 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -14,6 +14,8 @@ Highlights include: - Unicode aware string comparison using :doc:`examples/collations`. - Support for the new :class:`~bson.decimal128.Decimal128` BSON type. - A new maxStalenessSeconds read preference option. + - A username is no longer required for the MONGODB-X509 authentication + mechanism when connected to MongoDB >= 3.4. - :meth:`~pymongo.collection.Collection.parallel_scan` supports maxTimeMS. - :attr:`~pymongo.write_concern.WriteConcern` is automatically applied by all helpers for commands that write to the database when diff --git a/doc/examples/authentication.rst b/doc/examples/authentication.rst index b424e4230..993d544b7 100644 --- a/doc/examples/authentication.rst +++ b/doc/examples/authentication.rst @@ -95,10 +95,10 @@ and newer:: >>> import ssl >>> from pymongo import MongoClient >>> client = MongoClient('example.com', - ... ssl=True, - ... ssl_certfile='/path/to/client.pem', - ... ssl_cert_reqs=ssl.CERT_REQUIRED, - ... ssl_ca_certs='/path/to/ca.pem') + ... ssl=True, + ... ssl_certfile='/path/to/client.pem', + ... ssl_cert_reqs=ssl.CERT_REQUIRED, + ... ssl_ca_certs='/path/to/ca.pem') >>> client.the_database.authenticate("", ... mechanism='MONGODB-X509') True @@ -109,12 +109,15 @@ do not have to specify a database in the URI:: >>> uri = "mongodb://@example.com/?authMechanism=MONGODB-X509" >>> client = MongoClient(uri, - ... ssl=True, - ... ssl_certfile='/path/to/client.pem', - ... ssl_cert_reqs=ssl.CERT_REQUIRED, - ... ssl_ca_certs='/path/to/ca.pem') + ... ssl=True, + ... ssl_certfile='/path/to/client.pem', + ... ssl_cert_reqs=ssl.CERT_REQUIRED, + ... ssl_ca_certs='/path/to/ca.pem') >>> +.. versionchanged:: 3.4 + When connected to MongoDB >= 3.4 the username is no longer required. + .. _use_kerberos: GSSAPI (Kerberos) diff --git a/pymongo/auth.py b/pymongo/auth.py index fac0a8c45..04f119e3d 100644 --- a/pymongo/auth.py +++ b/pymongo/auth.py @@ -58,7 +58,7 @@ GSSAPIProperties = namedtuple('GSSAPIProperties', def _build_credentials_tuple(mech, source, user, passwd, extra): """Build and return a mechanism specific credentials tuple. """ - user = _unicode(user) + user = _unicode(user) if user is not None else None password = passwd if passwd is None else _unicode(passwd) if mech == 'GSSAPI': properties = extra.get('authmechanismproperties', {}) @@ -71,6 +71,7 @@ def _build_credentials_tuple(mech, source, user, passwd, extra): # Source is always $external. return MongoCredential(mech, '$external', user, password, props) elif mech == 'MONGODB-X509': + # user can be None. return MongoCredential(mech, '$external', user, None, None) else: if passwd is None: @@ -415,8 +416,13 @@ def _authenticate_x509(credentials, sock_info): """Authenticate using MONGODB-X509. """ query = SON([('authenticate', 1), - ('mechanism', 'MONGODB-X509'), - ('user', credentials.username)]) + ('mechanism', 'MONGODB-X509')]) + if credentials.username is not None: + query['user'] = credentials.username + elif sock_info.max_wire_version < 5: + raise ConfigurationError( + "A username is required for MONGODB-X509 authentication " + "when connected to MongoDB versions older than 3.4.") sock_info.command('$external', query) diff --git a/pymongo/client_options.py b/pymongo/client_options.py index e5baf3e10..b9313db39 100644 --- a/pymongo/client_options.py +++ b/pymongo/client_options.py @@ -29,9 +29,9 @@ from pymongo.write_concern import WriteConcern def _parse_credentials(username, password, database, options): """Parse authentication credentials.""" - if username is None: - return None 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) diff --git a/pymongo/database.py b/pymongo/database.py index 3fd68ef24..b997eddc2 100644 --- a/pymongo/database.py +++ b/pymongo/database.py @@ -966,7 +966,7 @@ class Database(common.BaseObject): return raise - def authenticate(self, name, password=None, + def authenticate(self, name=None, password=None, source=None, mechanism='DEFAULT', **kwargs): """Authenticate to use this database. @@ -992,7 +992,9 @@ class Database(common.BaseObject): distinct client instances. :Parameters: - - `name`: the name of the user to authenticate. + - `name`: the name of the user to authenticate. Optional when + `mechanism` is MONGODB-X509 and the MongoDB server version is + >= 3.4. - `password` (optional): the password of the user to authenticate. Not used with GSSAPI or MONGODB-X509 authentication. - `source` (optional): the database to authenticate on. If not @@ -1017,7 +1019,7 @@ class Database(common.BaseObject): .. mongodoc:: authenticate """ - if not isinstance(name, string_type): + if name is not None and not isinstance(name, string_type): raise TypeError("name must be an " "instance of %s" % (string_type.__name__,)) if password is not None and not isinstance(password, string_type): diff --git a/test/test_ssl.py b/test/test_ssl.py index 1f7917e2d..cfe1cbbf3 100644 --- a/test/test_ssl.py +++ b/test/test_ssl.py @@ -518,24 +518,47 @@ class TestSSL(IntegrationTest): coll = ssl_client.pymongo_test.test self.assertRaises(OperationFailure, coll.count) + + if client_context.version.at_least(3, 3, 12): + self.assertTrue( + ssl_client.admin.authenticate(mechanism='MONGODB-X509')) + # No error + coll.find_one() + # MONGODB_X509_USERNAME and None aren't the same user, so we + # have to log out before continuing. + ssl_client.admin.logout() + else: + # Should require a username + with self.assertRaises(ConfigurationError): + ssl_client.admin.authenticate(mechanism='MONGODB-X509') + self.assertTrue(ssl_client.admin.authenticate( MONGODB_X509_USERNAME, mechanism='MONGODB-X509')) - coll.drop() + # No error + coll.find_one() + uri = ('mongodb://%s@%s:%d/?authMechanism=' 'MONGODB-X509' % ( quote_plus(MONGODB_X509_USERNAME), host, port)) - # SSL options aren't supported in the URI... - self.assertTrue(MongoClient(uri, ssl=True, - ssl_cert_reqs=ssl.CERT_NONE, - ssl_certfile=CLIENT_PEM)) + client = MongoClient(uri, + ssl=True, + ssl_cert_reqs=ssl.CERT_NONE, + ssl_certfile=CLIENT_PEM) + # No error + client.pymongo_test.test.find_one() - # Should require a username - uri = ('mongodb://%s:%d/?authMechanism=MONGODB-X509' % (host, - port)) - client_bad = MongoClient( - uri, ssl=True, ssl_cert_reqs="CERT_NONE", ssl_certfile=CLIENT_PEM) - self.assertRaises(OperationFailure, - client_bad.pymongo_test.test.delete_one, {}) + uri = 'mongodb://%s:%d/?authMechanism=MONGODB-X509' % (host, port) + client = MongoClient(uri, + ssl=True, + ssl_cert_reqs=ssl.CERT_NONE, + ssl_certfile=CLIENT_PEM) + if client_context.version.at_least(3, 3, 12): + # No error + client.pymongo_test.test.find_one() + else: + # Should require a username + with self.assertRaises(ConfigurationError): + client.pymongo_test.test.find_one() # Auth should fail if username and certificate do not match uri = ('mongodb://%s@%s:%d/?authMechanism='