From 72e61277b2ebb1d582fcd0ccfe594b089767ce9d Mon Sep 17 00:00:00 2001 From: "A. Jesse Jiryu Davis" Date: Tue, 1 Aug 2017 10:13:20 -0400 Subject: [PATCH] Deprecate db.authenticate and db.eval PYTHON-1313, and PYTHON-1315. --- doc/changelog.rst | 7 ++ doc/examples/authentication.rst | 92 +++++--------- doc/examples/copydb.rst | 7 +- pymongo/database.py | 55 ++++---- test/__init__.py | 26 +++- test/test_auth.py | 215 ++++++++++++++------------------ test/test_client.py | 2 + test/test_database.py | 103 ++++++++------- test/test_ssl.py | 44 ++++--- test/utils.py | 17 ++- 10 files changed, 278 insertions(+), 290 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index ebd6cf4da..6b0dfa5a2 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -22,6 +22,13 @@ Changes and Deprecations: was deprecated in MongoDB 3.4 and is expected to be removed in MongoDB 3.6. Applications should use :meth:`~pymongo.collection.Collection.aggregate` with the `$group` pipeline stage instead. +- Deprecated :meth:`~pymongo.database.Database.authenticate`. Authenticating + multiple users conflicts with support for logical sessions in MongoDB 3.6. + To authenticate as multiple users, create multiple instances of + :class:`~pymongo.mongo_client.MongoClient`. +- Deprecated :meth:`~pymongo.database.Database.eval`. The eval command + was deprecated in MongoDB 3.0 and will be removed in a future server version. +- Deprecated :class:`~pymongo.database.SystemJS`. - Deprecated :meth:`~pymongo.mongo_client.MongoClient.get_default_database`. Applications should use :meth:`~pymongo.mongo_client.MongoClient.get_database` without the `name` diff --git a/doc/examples/authentication.rst b/doc/examples/authentication.rst index 16ad519e6..399f2f935 100644 --- a/doc/examples/authentication.rst +++ b/doc/examples/authentication.rst @@ -28,15 +28,21 @@ SCRAM-SHA-1 (RFC 5802) .. versionadded:: 2.8 SCRAM-SHA-1 is the default authentication mechanism supported by a cluster -configured for authentication with MongoDB 3.0 or later. Authentication is -per-database and credentials can be specified through the MongoDB URI or -passed to the :meth:`~pymongo.database.Database.authenticate` method:: +configured for authentication with MongoDB 3.0 or later. Authentication +requires a username, a password, and a database name. The default database +name is "admin", this can be overidden with the ``authSource`` option. +Credentials can be specified as arguments to +:class:`~pymongo.mongo_client.MongoClient`:: >>> from pymongo import MongoClient - >>> client = MongoClient('example.com') - >>> client.the_database.authenticate('user', 'password', mechanism='SCRAM-SHA-1') - True - >>> + >>> client = MongoClient('example.com', + ... user='user', + ... password='password', + ... authSource='the_database', + ... authMechanism='SCRAM-SHA-1') + +Or through the MongoDB URI:: + >>> uri = "mongodb://user:password@example.com/the_database?authMechanism=SCRAM-SHA-1" >>> client = MongoClient(uri) @@ -52,9 +58,10 @@ Before MongoDB 3.0 the default authentication mechanism was MONGODB-CR, the "MongoDB Challenge-Response" protocol:: >>> from pymongo import MongoClient - >>> client = MongoClient('example.com') - >>> client.the_database.authenticate('user', 'password', mechanism='MONGODB-CR') - True + >>> client = MongoClient('example.com', + ... user='user', + ... password='password', + ... authMechanism='MONGODB-CR') >>> >>> uri = "mongodb://user:password@example.com/the_database?authMechanism=MONGODB-CR" >>> client = MongoClient(uri) @@ -66,24 +73,22 @@ If no mechanism is specified, PyMongo automatically uses MONGODB-CR when connected to a pre-3.0 version of MongoDB, and SCRAM-SHA-1 when connected to a recent version. -Delegated Authentication ------------------------- -.. versionadded: 2.5 +Default Database and "authSource" +--------------------------------- -If your user is defined in one database with a role that gives it -authorization on a different database use the `source` option to specify -the database in which the user is defined:: +You can specify both a default database and the authentication database in the +URI:: - >>> from pymongo import MongoClient - >>> client = MongoClient('example.com') - >>> db = client.the_database - >>> db.authenticate('user', 'password', source='source_database') - True + >>> uri = "mongodb://user:password@example.com/default_db?authSource=admin" + >>> client = MongoClient(uri) -Or the `authSource` URI option:: +PyMongo will authenticate on the "admin" database, but the default database +will be "default_db":: - >>> uri = "mongodb://user:password@example.com/?authSource=source_database" - >>> db = MongoClient(uri).the_database + >>> # get_database with no "name" argument chooses the DB from the URI + >>> db = MongoClient(uri).get_database() + >>> print(db.name) + 'default_db' MONGODB-X509 ------------ @@ -98,14 +103,12 @@ and newer:: >>> import ssl >>> from pymongo import MongoClient >>> client = MongoClient('example.com', + ... username="" + ... authMechanism="MONGODB-X509", ... 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 - >>> MONGODB-X509 authenticates against the $external virtual database, so you do not have to specify a database in the URI:: @@ -156,27 +159,12 @@ URI:: >>> client = MongoClient(uri) >>> -or using :meth:`~pymongo.database.Database.authenticate`:: - - >>> from pymongo import MongoClient - >>> client = MongoClient('example.com') - >>> db = client.test - >>> db.authenticate('mongodbuser@EXAMPLE.COM', mechanism='GSSAPI') - True - The default service name used by MongoDB and PyMongo is `mongodb`. You can specify a custom service name with the ``authMechanismProperties`` option:: >>> from pymongo import MongoClient >>> uri = "mongodb://mongodbuser%40EXAMPLE.COM@example.com/?authMechanism=GSSAPI&authMechanismProperties=SERVICE_NAME:myservicename" >>> client = MongoClient(uri) - >>> - >>> client = MongoClient('example.com') - >>> db = client.test - >>> db.authenticate( - ... 'mongodbuser@EXAMPLE.COM', mechanism='GSSAPI', - ... authMechanismProperties='SERVICE_NAME:myservicename') - True Windows (SSPI) ~~~~~~~~~~~~~~ @@ -215,13 +203,6 @@ to an LDAP server. Using the PLAIN mechanism is very similar to MONGODB-CR. These examples use the $external virtual database for LDAP support:: >>> from pymongo import MongoClient - >>> client = MongoClient('example.com') - >>> client.the_database.authenticate('user', - ... 'password', - ... source='$external', - ... mechanism='PLAIN') - True - >>> >>> uri = "mongodb://user:password@example.com/?authMechanism=PLAIN&authSource=$external" >>> client = MongoClient(uri) >>> @@ -232,17 +213,6 @@ the SASL PLAIN mechanism:: >>> 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') - >>> client.the_database.authenticate('user', - ... 'password', - ... source='$external', - ... mechanism='PLAIN') - True - >>> >>> uri = "mongodb://user:password@example.com/?authMechanism=PLAIN&authSource=$external" >>> client = MongoClient(uri, ... ssl=True, diff --git a/doc/examples/copydb.rst b/doc/examples/copydb.rst index 3a19ce907..5cf5c66de 100644 --- a/doc/examples/copydb.rst +++ b/doc/examples/copydb.rst @@ -19,10 +19,11 @@ To copy from a different mongod server that is not password-protected:: fromhost='source.example.com') If the target server is password-protected, authenticate to the "admin" -database first:: +database:: - >>> client.admin.authenticate('administrator', 'pwd') - True + >>> client = MongoClient('target.example.com', + ... username='administrator', + ... password='pwd') >>> client.admin.command('copydb', fromdb='source_db_name', todb='target_db_name', diff --git a/pymongo/database.py b/pymongo/database.py index cc7b25822..c51b99346 100644 --- a/pymongo/database.py +++ b/pymongo/database.py @@ -142,7 +142,7 @@ class Database(common.BaseObject): @property def system_js(self): - """A :class:`SystemJS` helper for this :class:`Database`. + """**DEPRECATED**: :class:`SystemJS` helper for this :class:`Database`. See the documentation for :class:`SystemJS` for more details. """ @@ -990,7 +990,7 @@ class Database(common.BaseObject): def authenticate(self, name=None, password=None, source=None, mechanism='DEFAULT', **kwargs): - """Authenticate to use this database. + """**DEPRECATED**: Authenticate to use this database. Authentication lasts for the life of the underlying client instance, or until :meth:`logout` is called. @@ -1030,6 +1030,11 @@ class Database(common.BaseObject): name for GSSAPI authentication pass authMechanismProperties='SERVICE_NAME:' + .. versionchanged:: 3.5 + Deprecated. Authenticating multiple users conflicts with support for + logical sessions in MongoDB 3.6. To authenticate as multiple users, + create multiple instances of MongoClient. + .. versionadded:: 2.8 Use SCRAM-SHA-1 with MongoDB 3.0 and later. @@ -1072,7 +1077,10 @@ class Database(common.BaseObject): return True def logout(self): - """Deauthorize use of this database for this client instance.""" + """**DEPRECATED**: Deauthorize use of this database.""" + warnings.warn("Database.logout() is deprecated", + DeprecationWarning, stacklevel=2) + # Sockets will be deauthenticated as they are used. self.client._purge_credentials(self.name) @@ -1101,18 +1109,7 @@ class Database(common.BaseObject): return self[dbref.collection].find_one({"_id": dbref.id}, **kwargs) def eval(self, code, *args): - """Evaluate a JavaScript expression in MongoDB. - - Useful if you need to touch a lot of data lightly; in such a - scenario the network transfer of the data could be a - bottleneck. The `code` argument must be a JavaScript - function. Additional positional arguments will be passed to - that function when it is run on the server. - - Raises :class:`TypeError` if `code` is not an instance of - :class:`basestring` (:class:`str` in python 3) or `Code`. - Raises :class:`~pymongo.errors.OperationFailure` if the eval - fails. Returns the result of the evaluation. + """**DEPRECATED**: Evaluate a JavaScript expression in MongoDB. :Parameters: - `code`: string representation of JavaScript code to be @@ -1123,6 +1120,9 @@ class Database(common.BaseObject): .. warning:: the eval command is deprecated in MongoDB 3.0 and will be removed in a future server version. """ + warnings.warn("Database.eval() is deprecated", + DeprecationWarning, stacklevel=2) + if not isinstance(code, Code): code = Code(code) @@ -1139,30 +1139,17 @@ class Database(common.BaseObject): class SystemJS(object): - """Helper class for dealing with stored JavaScript. + """**DEPRECATED**: Helper class for dealing with stored JavaScript. """ def __init__(self, database): - """Get a system js helper for the database `database`. + """**DEPRECATED**: Get a system js helper for the database `database`. - An instance of :class:`SystemJS` can be created with an instance - of :class:`Database` through :attr:`Database.system_js`, - manual instantiation of this class should not be necessary. - - :class:`SystemJS` instances allow for easy manipulation and - access to server-side JavaScript: - - .. doctest:: - - >>> db.system_js.add1 = "function (x) { return x + 1; }" - >>> db.system.js.find({"_id": "add1"}).count() - 1 - >>> db.system_js.add1(5) - 6.0 - >>> del db.system_js.add1 - >>> db.system.js.find({"_id": "add1"}).count() - 0 + SystemJS will be removed in PyMongo 4.0. """ + warnings.warn("SystemJS is deprecated", + DeprecationWarning, stacklevel=2) + if not database.write_concern.acknowledged: database = database.client.get_database( database.name, write_concern=WriteConcern()) diff --git a/test/__init__.py b/test/__init__.py index 72a64d3c4..2cac4122e 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -146,6 +146,14 @@ class client_knobs(object): self.disable() +def _all_users(db): + if Version.from_client(db.client).at_least(2, 5, 3, -1): + return set(u['user'] + for u in db.command('usersInfo').get('users', [])) + else: + return set(u['user'] for u in db.system.users.find()) + + class ClientContext(object): def __init__(self): @@ -226,7 +234,12 @@ class ClientContext(object): if self.version.at_least(2, 5, 3, -1): roles = {'roles': ['root']} self.client.admin.add_user(db_user, db_pwd, **roles) - self.client.admin.authenticate(db_user, db_pwd) + + self.client = _connect(host, + port, + username=db_user, + password=db_pwd, + **self.ssl_client_options) # May not have this if OperationFailure was raised earlier. self.cmd_line = self.client.admin.command('getCmdLineOpts') @@ -270,9 +283,16 @@ class ClientContext(object): return bool(len(self.client.secondaries)) def _check_user_provided(self): + """Return True if db_user/db_password is already an admin user.""" + client = pymongo.MongoClient( + host, port, + username=db_user, + password=db_pwd, + serverSelectionTimeoutMS=100, + **self.ssl_client_options) + try: - self.client.admin.authenticate(db_user, db_pwd) - return True + return db_user in _all_users(client.admin) except pymongo.errors.OperationFailure as e: msg = e.details.get('errmsg', '') if e.code == 18 or 'auth fails' in msg: diff --git a/test/test_auth.py b/test/test_auth.py index ab5fac0f5..345c50a3d 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -31,7 +31,10 @@ from pymongo.auth import HAVE_KERBEROS, _build_credentials_tuple from pymongo.errors import OperationFailure from pymongo.read_preferences import ReadPreference from test import client_context, SkipTest, unittest, Version -from test.utils import delay, rs_or_single_client_noauth, single_client_noauth +from test.utils import (delay, + ignore_deprecations, + rs_or_single_client_noauth, + single_client_noauth) # YOU MUST RUN KINIT BEFORE RUNNING GSSAPI TESTS ON UNIX. GSSAPI_HOST = os.environ.get('GSSAPI_HOST') @@ -109,9 +112,8 @@ class TestGSSAPI(unittest.TestCase): self.assertEqual(1, len(set([creds1, creds2]))) self.assertEqual(3, len(set([creds0, creds1, creds2, creds3]))) + @ignore_deprecations def test_gssapi_simple(self): - client = MongoClient(GSSAPI_HOST, GSSAPI_PORT) - db = client[GSSAPI_DB] if GSSAPI_PASS is not None: uri = ('mongodb://%s:%s@%s:%d/?authMechanism=' 'GSSAPI' % (quote_plus(GSSAPI_PRINCIPAL), @@ -125,25 +127,28 @@ class TestGSSAPI(unittest.TestCase): GSSAPI_PORT)) if not self.service_realm_required: - # Call authenticate() without authMechanismProperties. - self.assertTrue(db.authenticate(GSSAPI_PRINCIPAL, - GSSAPI_PASS, - mechanism='GSSAPI')) - db.collection.find_one() + # Without authMechanismProperties. + client = MongoClient(GSSAPI_HOST, + GSSAPI_PORT, + username=GSSAPI_PRINCIPAL, + password=GSSAPI_PASS, + authMechanism='GSSAPI') + + client[GSSAPI_DB].collection.find_one() # Log in using URI, without authMechanismProperties. client = MongoClient(uri) - db = client[GSSAPI_DB] - db.collection.find_one() + client[GSSAPI_DB].collection.find_one() + # Authenticate with authMechanismProperties. + client = MongoClient(GSSAPI_HOST, + GSSAPI_PORT, + username=GSSAPI_PRINCIPAL, + password=GSSAPI_PASS, + authMechanism='GSSAPI', + authMechanismProperties=self.mech_properties) - # Call authenticate() with authMechanismProperties. - self.assertTrue(db.authenticate( - GSSAPI_PRINCIPAL, - GSSAPI_PASS, - mechanism='GSSAPI', - authMechanismProperties=self.mech_properties)) - db.collection.find_one() + client[GSSAPI_DB].collection.find_one() # Log in using URI, with authMechanismProperties. mech_uri = uri + '&authMechanismProperties=%s' % (self.mech_properties,) @@ -152,43 +157,48 @@ class TestGSSAPI(unittest.TestCase): set_name = client.admin.command('ismaster').get('setName') if set_name: - client = MongoClient(GSSAPI_HOST, - GSSAPI_PORT, - replicaSet=set_name) - db = client[GSSAPI_DB] - if not self.service_realm_required: # Without authMechanismProperties - self.assertTrue(db.authenticate(GSSAPI_PRINCIPAL, - GSSAPI_PASS, - mechanism='GSSAPI')) - db.collection_names() + client = MongoClient(GSSAPI_HOST, + GSSAPI_PORT, + username=GSSAPI_PRINCIPAL, + password=GSSAPI_PASS, + authMechanism='GSSAPI', + replicaSet=set_name) + + client[GSSAPI_DB].collection_names() + uri = uri + '&replicaSet=%s' % (str(set_name),) client = MongoClient(uri) - db = client[GSSAPI_DB] - db.collection_names() + client[GSSAPI_DB].collection_names() # With authMechanismProperties - self.assertTrue(db.authenticate( - GSSAPI_PRINCIPAL, - GSSAPI_PASS, - mechanism='GSSAPI', - authMechanismProperties=self.mech_properties)) - db.collection_names() + client = MongoClient(GSSAPI_HOST, + GSSAPI_PORT, + username=GSSAPI_PRINCIPAL, + password=GSSAPI_PASS, + authMechanism='GSSAPI', + authMechanismProperties=self.mech_properties, + replicaSet=set_name) + + client[GSSAPI_DB].collection_names() + mech_uri = mech_uri + '&replicaSet=%s' % (str(set_name),) client = MongoClient(mech_uri) client[GSSAPI_DB].collection_names() + @ignore_deprecations def test_gssapi_threaded(self): + client = MongoClient(GSSAPI_HOST, + GSSAPI_PORT, + username=GSSAPI_PRINCIPAL, + password=GSSAPI_PASS, + authMechanism='GSSAPI', + authMechanismProperties=self.mech_properties) - client = MongoClient(GSSAPI_HOST, GSSAPI_PORT) + # Authentication succeeded? + client.server_info() db = client[GSSAPI_DB] - self.assertTrue( - db.authenticate(GSSAPI_PRINCIPAL, - GSSAPI_PASS, - mechanism='GSSAPI', - authMechanismProperties=self.mech_properties)) - # Need one document in the collection. AutoAuthenticateThread does # collection.find_one with a 1-second delay, forcing it to check out @@ -215,14 +225,14 @@ class TestGSSAPI(unittest.TestCase): if set_name: client = MongoClient(GSSAPI_HOST, GSSAPI_PORT, - replicaSet=set_name, - readPreference='secondary') - db = client[GSSAPI_DB] - self.assertTrue( - db.authenticate(GSSAPI_PRINCIPAL, - GSSAPI_PASS, - mechanism='GSSAPI', - authMechanismProperties=self.mech_properties)) + username=GSSAPI_PRINCIPAL, + password=GSSAPI_PASS, + authMechanism='GSSAPI', + authMechanismProperties=self.mech_properties, + replicaSet=set_name) + + # Succeeded? + client.server_info() threads = [] for _ in range(4): @@ -244,9 +254,12 @@ class TestSASLPlain(unittest.TestCase): def test_sasl_plain(self): - client = MongoClient(SASL_HOST, SASL_PORT) - self.assertTrue(client.ldap.authenticate(SASL_USER, SASL_PASS, - SASL_DB, 'PLAIN')) + client = MongoClient(SASL_HOST, + SASL_PORT, + username=SASL_USER, + password=SASL_PASS, + authSource=SASL_DB, + authMechanism='PLAIN') client.ldap.test.find_one() uri = ('mongodb://%s:%s@%s:%d/?authMechanism=PLAIN;' @@ -259,10 +272,12 @@ class TestSASLPlain(unittest.TestCase): set_name = client.admin.command('ismaster').get('setName') if set_name: client = MongoClient(SASL_HOST, - port=SASL_PORT, - replicaSet=set_name) - self.assertTrue(client.ldap.authenticate(SASL_USER, SASL_PASS, - SASL_DB, 'PLAIN')) + SASL_PORT, + replicaSet=set_name, + username=SASL_USER, + password=SASL_PASS, + authSource=SASL_DB, + authMechanism='PLAIN') client.ldap.test.find_one() uri = ('mongodb://%s:%s@%s:%d/?authMechanism=PLAIN;' @@ -275,21 +290,22 @@ class TestSASLPlain(unittest.TestCase): def test_sasl_plain_bad_credentials(self): - client = MongoClient(SASL_HOST, SASL_PORT) + with ignore_deprecations(): + client = MongoClient(SASL_HOST, SASL_PORT) - # Bad username - self.assertRaises(OperationFailure, client.ldap.authenticate, - 'not-user', SASL_PASS, SASL_DB, 'PLAIN') - self.assertRaises(OperationFailure, client.ldap.test.find_one) - self.assertRaises(OperationFailure, client.ldap.test.insert_one, - {"failed": True}) + # Bad username + self.assertRaises(OperationFailure, client.ldap.authenticate, + 'not-user', SASL_PASS, SASL_DB, 'PLAIN') + self.assertRaises(OperationFailure, client.ldap.test.find_one) + self.assertRaises(OperationFailure, client.ldap.test.insert_one, + {"failed": True}) - # Bad password - self.assertRaises(OperationFailure, client.ldap.authenticate, - SASL_USER, 'not-pwd', SASL_DB, 'PLAIN') - self.assertRaises(OperationFailure, client.ldap.test.find_one) - self.assertRaises(OperationFailure, client.ldap.test.insert_one, - {"failed": True}) + # Bad password + self.assertRaises(OperationFailure, client.ldap.authenticate, + SASL_USER, 'not-pwd', SASL_DB, 'PLAIN') + self.assertRaises(OperationFailure, client.ldap.test.find_one) + self.assertRaises(OperationFailure, client.ldap.test.insert_one, + {"failed": True}) def auth_string(user, password): uri = ('mongodb://%s:%s@%s:%d/?authMechanism=PLAIN;' @@ -326,10 +342,12 @@ class TestSCRAMSHA1(unittest.TestCase): def test_scram_sha1(self): host, port = client_context.host, client_context.port - client = rs_or_single_client_noauth() - self.assertTrue(client.pymongo_test.authenticate( - 'user', 'pass', mechanism='SCRAM-SHA-1')) - client.pymongo_test.command('dbstats') + + with ignore_deprecations(): + client = rs_or_single_client_noauth() + self.assertTrue(client.pymongo_test.authenticate( + 'user', 'pass', mechanism='SCRAM-SHA-1')) + client.pymongo_test.command('dbstats') client = rs_or_single_client_noauth( 'mongodb://user:pass@%s:%d/pymongo_test?authMechanism=SCRAM-SHA-1' @@ -355,13 +373,12 @@ class TestAuthURIOptions(unittest.TestCase): @client_context.require_auth def setUp(self): - client = rs_or_single_client_noauth() client_context.client.admin.add_user('admin', 'pass', roles=['userAdminAnyDatabase', 'dbAdminAnyDatabase', 'readWriteAnyDatabase', 'clusterAdmin']) - client.admin.authenticate('admin', 'pass') + client = rs_or_single_client_noauth(username='admin', password='pass') client.pymongo_test.add_user('user', 'pass', roles=['userAdmin', 'readWrite']) @@ -372,11 +389,8 @@ class TestAuthURIOptions(unittest.TestCase): self.client = client def tearDown(self): - self.client.admin.authenticate('admin', 'pass') self.client.pymongo_test.remove_user('user') self.client.admin.remove_user('admin') - self.client.pymongo_test.logout() - self.client.admin.logout() self.client = None def test_uri_options(self): @@ -433,50 +447,5 @@ class TestAuthURIOptions(unittest.TestCase): self.assertTrue(db.command('dbstats')) -class TestDelegatedAuth(unittest.TestCase): - - @client_context.require_auth - @client_context.require_version_max(2, 5, 3) - @client_context.require_version_min(2, 4, 0) - def setUp(self): - self.client = client_context.client - - def tearDown(self): - self.client.pymongo_test.remove_user('user') - self.client.pymongo_test2.remove_user('user') - self.client.pymongo_test2.foo.drop() - - def test_delegated_auth(self): - self.client.pymongo_test2.foo.drop() - self.client.pymongo_test2.foo.insert_one({}) - # User definition with no roles in pymongo_test. - self.client.pymongo_test.add_user('user', 'pass', roles=[]) - # Delegate auth to pymongo_test. - self.client.pymongo_test2.add_user('user', - userSource='pymongo_test', - roles=['read']) - auth_c = rs_or_single_client_noauth() - self.assertRaises(OperationFailure, - auth_c.pymongo_test2.foo.find_one) - # Auth must occur on the db where the user is defined. - self.assertRaises(OperationFailure, - auth_c.pymongo_test2.authenticate, - 'user', 'pass') - # Auth directly - self.assertTrue(auth_c.pymongo_test.authenticate('user', 'pass')) - self.assertTrue(auth_c.pymongo_test2.foo.find_one()) - auth_c.pymongo_test.logout() - self.assertRaises(OperationFailure, - auth_c.pymongo_test2.foo.find_one) - # Auth using source - self.assertTrue(auth_c.pymongo_test2.authenticate( - 'user', 'pass', source='pymongo_test')) - self.assertTrue(auth_c.pymongo_test2.foo.find_one()) - # Must logout from the db authenticate was called on. - auth_c.pymongo_test2.logout() - self.assertRaises(OperationFailure, - auth_c.pymongo_test2.foo.find_one) - - if __name__ == "__main__": unittest.main() diff --git a/test/test_client.py b/test/test_client.py index 4e83fd3a9..6b0e0eae8 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -67,6 +67,7 @@ from test.utils import (assertRaisesExactly, connected, delay, get_pool, + ignore_deprecations, is_greenthread_patched, lazy_client_trial, NTHREADS, @@ -611,6 +612,7 @@ class TestClient(IntegrationTest): rs_or_single_client(username="ad min", password="foo").server_info() @client_context.require_auth + @ignore_deprecations def test_multiple_logins(self): self.client.pymongo_test.add_user('user1', 'pass', roles=['readWrite']) self.client.pymongo_test.add_user('user2', 'pass', roles=['readWrite']) diff --git a/test/test_database.py b/test/test_database.py index b9f7a894d..6cadc359e 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -388,7 +388,14 @@ class TestDatabase(IntegrationTest): def test_authenticate_add_remove_user(self): # "self.client" is logged in as root. auth_db = self.client.pymongo_test - db = rs_or_single_client_noauth().pymongo_test + + def check_auth(username, password): + c = rs_or_single_client_noauth( + username=username, + password=password, + authSource="pymongo_test") + + c.pymongo_test.collection.find_one() # Configuration errors self.assertRaises(ValueError, auth_db.add_user, "user", '') @@ -411,45 +418,38 @@ class TestDatabase(IntegrationTest): "user", "password", digestPassword=True) # Add / authenticate / remove - auth_db.add_user("mike", "password", roles=["dbOwner"]) + auth_db.add_user("mike", "password", roles=["read"]) self.addCleanup(remove_all_users, auth_db) - self.assertRaises(TypeError, db.authenticate, 5, "password") - self.assertRaises(TypeError, db.authenticate, "mike", 5) + self.assertRaises(TypeError, check_auth, 5, "password") + self.assertRaises(TypeError, check_auth, "mike", 5) self.assertRaises(OperationFailure, - db.authenticate, "mike", "not a real password") - self.assertRaises(OperationFailure, - db.authenticate, "faker", "password") - db.authenticate("mike", "password") - db.logout() + check_auth, "mike", "not a real password") + self.assertRaises(OperationFailure, check_auth, "faker", "password") + check_auth("mike", "password") # Unicode name and password. - db.authenticate(u"mike", u"password") - db.logout() + check_auth(u"mike", u"password") auth_db.remove_user("mike") self.assertRaises(OperationFailure, - db.authenticate, "mike", "password") + check_auth, "mike", "password") # Add / authenticate / change password - self.assertRaises(OperationFailure, - db.authenticate, "Gustave", u"Dor\xe9") - auth_db.add_user("Gustave", u"Dor\xe9", roles=["dbOwner"]) - db.authenticate("Gustave", u"Dor\xe9") + self.assertRaises(OperationFailure, check_auth, "Gustave", u"Dor\xe9") + auth_db.add_user("Gustave", u"Dor\xe9", roles=["read"]) + check_auth("Gustave", u"Dor\xe9") # Change password. - auth_db.add_user("Gustave", "password", roles=["dbOwner"]) - db.logout() - self.assertRaises(OperationFailure, - db.authenticate, "Gustave", u"Dor\xe9") - self.assertTrue(db.authenticate("Gustave", u"password")) + auth_db.add_user("Gustave", "password", roles=["read"]) + self.assertRaises(OperationFailure, check_auth, "Gustave", u"Dor\xe9") + check_auth("Gustave", u"password") if not client_context.version.at_least(2, 5, 3, -1): # Add a readOnly user with ignore_deprecations(): auth_db.add_user("Ross", "password", read_only=True) - db.logout() - db.authenticate("Ross", u"password") + check_auth("Ross", u"password") self.assertTrue( auth_db.system.users.find({"readOnly": True}).count()) @@ -457,22 +457,28 @@ class TestDatabase(IntegrationTest): def test_make_user_readonly(self): # "self.client" is logged in as root. auth_db = self.client.pymongo_test - db = rs_or_single_client_noauth().pymongo_test # Make a read-write user. auth_db.add_user('jesse', 'pw') self.addCleanup(remove_all_users, auth_db) # Check that we're read-write by default. - db.authenticate('jesse', 'pw') - db.collection.insert_one({}) - db.logout() + c = rs_or_single_client_noauth(username='jesse', + password='pw', + authSource='pymongo_test') + + c.pymongo_test.collection.insert_one({}) # Make the user read-only. auth_db.add_user('jesse', 'pw', read_only=True) - db.authenticate('jesse', 'pw') - self.assertRaises(OperationFailure, db.collection.insert_one, {}) + c = rs_or_single_client_noauth(username='jesse', + password='pw', + authSource='pymongo_test') + + self.assertRaises(OperationFailure, + c.pymongo_test.collection.insert_one, + {}) @client_context.require_version_min(2, 5, 3, -1) @client_context.require_auth @@ -512,8 +518,9 @@ class TestDatabase(IntegrationTest): auth_db.add_user("amalia", "password", roles=["userAdmin"]) self.addCleanup(auth_db.remove_user, "amalia") - db = rs_or_single_client_noauth().pymongo_test - db.authenticate("amalia", "password") + db = rs_or_single_client_noauth(username="amalia", + password="password", + authSource="pymongo_test").pymongo_test # This tests the ability to update user attributes. db.add_user("amalia", "new_password", @@ -526,6 +533,7 @@ class TestDatabase(IntegrationTest): self.assertEqual(amalia_user["customData"], {"secret": "koalas"}) @client_context.require_auth + @ignore_deprecations def test_authenticate_multiple(self): # "self.client" is logged in as root. self.client.drop_database("pymongo_test") @@ -636,27 +644,28 @@ class TestDatabase(IntegrationTest): db = self.client.pymongo_test db.test.drop() - self.assertRaises(TypeError, db.eval, None) - self.assertRaises(TypeError, db.eval, 5) - self.assertRaises(TypeError, db.eval, []) + with ignore_deprecations(): + self.assertRaises(TypeError, db.eval, None) + self.assertRaises(TypeError, db.eval, 5) + self.assertRaises(TypeError, db.eval, []) - self.assertEqual(3, db.eval("function (x) {return x;}", 3)) - self.assertEqual(3, db.eval(u"function (x) {return x;}", 3)) + self.assertEqual(3, db.eval("function (x) {return x;}", 3)) + self.assertEqual(3, db.eval(u"function (x) {return x;}", 3)) - self.assertEqual(None, - db.eval("function (x) {db.test.save({y:x});}", 5)) - self.assertEqual(db.test.find_one()["y"], 5) + self.assertEqual(None, + db.eval("function (x) {db.test.save({y:x});}", 5)) + self.assertEqual(db.test.find_one()["y"], 5) - self.assertEqual(5, db.eval("function (x, y) {return x + y;}", 2, 3)) - self.assertEqual(5, db.eval("function () {return 5;}")) - self.assertEqual(5, db.eval("2 + 3;")) + self.assertEqual(5, db.eval("function (x, y) {return x + y;}", 2, 3)) + self.assertEqual(5, db.eval("function () {return 5;}")) + self.assertEqual(5, db.eval("2 + 3;")) - self.assertEqual(5, db.eval(Code("2 + 3;"))) - self.assertRaises(OperationFailure, db.eval, Code("return i;")) - self.assertEqual(2, db.eval(Code("return i;", {"i": 2}))) - self.assertEqual(5, db.eval(Code("i + 3;", {"i": 2}))) + self.assertEqual(5, db.eval(Code("2 + 3;"))) + self.assertRaises(OperationFailure, db.eval, Code("return i;")) + self.assertEqual(2, db.eval(Code("return i;", {"i": 2}))) + self.assertEqual(5, db.eval(Code("i + 3;", {"i": 2}))) - self.assertRaises(OperationFailure, db.eval, "5 ++ 5;") + self.assertRaises(OperationFailure, db.eval, "5 ++ 5;") # TODO some of these tests belong in the collection level testing. def test_insert_find_one(self): diff --git a/test/test_ssl.py b/test/test_ssl.py index 5a31a2eba..4bb8d7592 100644 --- a/test/test_ssl.py +++ b/test/test_ssl.py @@ -507,28 +507,28 @@ class TestSSL(IntegrationTest): {'role': 'readWriteAnyDatabase', 'db': 'admin'}, {'role': 'userAdminAnyDatabase', 'db': 'admin'}]) - ssl_client.admin.logout() + noauth = MongoClient( + client_context.pair, + ssl=True, + ssl_cert_reqs=ssl.CERT_NONE, + ssl_certfile=CLIENT_PEM) - coll = ssl_client.pymongo_test.test - self.assertRaises(OperationFailure, coll.count) + self.assertRaises(OperationFailure, noauth.pymongo_test.test.count) + + auth = MongoClient( + client_context.pair, + authMechanism='MONGODB-X509', + ssl=True, + ssl_cert_reqs=ssl.CERT_NONE, + ssl_certfile=CLIENT_PEM) 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() + auth.pymongo_test.test.find_one() 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')) - # No error - coll.find_one() + auth.pymongo_test.test.find_one() uri = ('mongodb://%s@%s:%d/?authMechanism=' 'MONGODB-X509' % ( @@ -560,12 +560,20 @@ class TestSSL(IntegrationTest): bad_client = MongoClient( uri, ssl=True, ssl_cert_reqs="CERT_NONE", ssl_certfile=CLIENT_PEM) + with self.assertRaises(OperationFailure): bad_client.pymongo_test.test.find_one() - self.assertRaises(OperationFailure, ssl_client.admin.authenticate, - "not the username", - mechanism="MONGODB-X509") + bad_client = MongoClient( + client_context.pair, + username="not the username", + authMechanism='MONGODB-X509', + ssl=True, + ssl_cert_reqs=ssl.CERT_NONE, + ssl_certfile=CLIENT_PEM) + + with self.assertRaises(OperationFailure): + bad_client.pymongo_test.test.find_one() # Invalid certificate (using CA certificate as client certificate) uri = ('mongodb://%s@%s:%d/?authMechanism=' diff --git a/test/utils.py b/test/utils.py index 7c40ec441..891d15e5f 100644 --- a/test/utils.py +++ b/test/utils.py @@ -16,6 +16,7 @@ """ import contextlib +import functools import os import struct import sys @@ -348,12 +349,26 @@ def assertRaisesExactly(cls, fn, *args, **kwargs): @contextlib.contextmanager -def ignore_deprecations(): +def _ignore_deprecations(): with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) yield +def ignore_deprecations(wrapped=None): + """A context manager or a decorator.""" + if wrapped: + @functools.wraps(wrapped) + def wrapper(*args, **kwargs): + with _ignore_deprecations(): + return wrapped(*args, **kwargs) + + return wrapper + + else: + return _ignore_deprecations() + + def read_from_which_host( client, pref,