diff --git a/pymongo/database.py b/pymongo/database.py index 2d085de64..92551a5bb 100644 --- a/pymongo/database.py +++ b/pymongo/database.py @@ -615,6 +615,50 @@ class Database(common.BaseObject): def next(self): raise TypeError("'Database' object is not iterable") + def _create_user(self, name, password, **kwargs): + """Uses v2 commands for creating a new user. + """ + create_opts = {} + if password is not None: + create_opts["pwd"] = password + if "roles" not in kwargs: + create_opts["roles"] = [] + create_opts["writeConcern"] = self._get_wc_override() + create_opts.update(kwargs) + + self.command("createUser", name, **create_opts) + + def _update_user(self, name, password, **kwargs): + """Uses v2 commands for updating a user. + """ + update_opts = {} + if password is not None: + update_opts["pwd"] = password + update_opts["writeConcern"] = self._get_wc_override() + update_opts.update(kwargs) + + self.command("updateUser", name, **update_opts) + + def _legacy_add_user(self, name, password, read_only, **kwargs): + """Uses v1 system to add users, i.e. saving to system.users. + """ + user = self.system.users.find_one({"user": name}) or {"user": name} + if password is not None: + user["pwd"] = auth._password_digest(name, password) + if read_only is not None: + user["readOnly"] = common.validate_boolean('read_only', read_only) + user.update(kwargs) + + try: + self.system.users.save(user, **self._get_wc_override()) + except OperationFailure, e: + # First admin user add fails gle in MongoDB >= 2.1.2 + # See SERVER-4225 for more information. + if 'login' in str(e): + pass + else: + raise + def add_user(self, name, password=None, read_only=None, **kwargs): """Create user `name` with password `password`. @@ -644,22 +688,19 @@ class Database(common.BaseObject): .. versionadded:: 1.4 """ - user = self.system.users.find_one({"user": name}) or {"user": name} - if password is not None: - user["pwd"] = auth._password_digest(name, password) - if read_only is not None: - user["readOnly"] = common.validate_boolean('read_only', read_only) - user.update(kwargs) - try: - self.system.users.save(user, **self._get_wc_override()) - except OperationFailure, e: - # First admin user add fails gle in MongoDB >= 2.1.2 - # See SERVER-4225 for more information. - if 'login' in str(e): - pass - else: - raise + uinfo = self.command("usersInfo", name) + + except OperationFailure, exc: + if exc.code is None: + self._legacy_add_user(name, password, read_only, **kwargs) + return + raise + + if uinfo["users"]: + self._update_user(name, password, **kwargs) + else: + self._create_user(name, password, **kwargs) def remove_user(self, name): """Remove user `name` from this :class:`Database`. @@ -672,7 +713,16 @@ class Database(common.BaseObject): .. versionadded:: 1.4 """ - self.system.users.remove({"user": name}, **self._get_wc_override()) + + try: + self.command("removeUser", name, + writeConcern=self._get_wc_override()) + except OperationFailure, exc: + if exc.code is None: + self.system.users.remove({"user": name}, + **self._get_wc_override()) + return + raise def authenticate(self, name, password=None, source=None, mechanism='MONGODB-CR', **kwargs): diff --git a/test/test_auth.py b/test/test_auth.py index 65ce5bf54..cfcbfcdf7 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -201,8 +201,14 @@ class TestAuthURIOptions(unittest.TestCase): raise SkipTest('Authentication is not enabled on server') response = client.admin.command('ismaster') self.set_name = str(response.get('setName', '')) - client.pymongo_test.add_user('user', 'pass') - client.admin.add_user('admin', 'pass') + client.admin.add_user('admin', 'pass', roles=['userAdminAnyDatabase', + 'dbAdminAnyDatabase', + 'readWriteAnyDatabase', + 'clusterAdmin']) + client.admin.authenticate('admin', 'pass') + client.pymongo_test.add_user('user', 'pass', + roles=['userAdmin', 'readWrite']) + if self.set_name: # GLE requires authentication. client.admin.authenticate('admin', 'pass') @@ -214,8 +220,9 @@ class TestAuthURIOptions(unittest.TestCase): def tearDown(self): self.client.admin.authenticate('admin', 'pass') - self.client.pymongo_test.system.users.remove() - self.client.admin.system.users.remove() + 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 @@ -275,7 +282,9 @@ class TestDelegatedAuth(unittest.TestCase): raise SkipTest('Delegated authentication requires MongoDB >= 2.4.0') if not server_started_with_auth(self.client): raise SkipTest('Authentication is not enabled on server') - # Give admin all priviledges. + if version.at_least(self.client, (2, 5, 3, -1)): + raise SkipTest('Delegated auth does not exist in MongoDB >= 2.5.3') + # Give admin all privileges. self.client.admin.add_user('admin', 'pass', roles=['readAnyDatabase', 'readWriteAnyDatabase', @@ -285,10 +294,10 @@ class TestDelegatedAuth(unittest.TestCase): def tearDown(self): self.client.admin.authenticate('admin', 'pass') - self.client.pymongo_test.system.users.remove() - self.client.pymongo_test2.system.users.remove() + self.client.pymongo_test.remove_user('user') + self.client.pymongo_test2.remove_user('user') self.client.pymongo_test2.foo.remove() - self.client.admin.system.users.remove() + self.client.admin.remove_user('admin') self.client.admin.logout() self.client = None diff --git a/test/test_client.py b/test/test_client.py index a1033bfe7..80ff73975 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -43,6 +43,7 @@ from test import version, host, port from test.utils import (assertRaisesExactly, delay, is_mongos, + remove_all_users, server_is_master_with_slave, server_started_with_auth, TestRequestMixin) @@ -238,7 +239,8 @@ class TestClient(unittest.TestCase, TestRequestMixin): self.assertTrue("pymongo_test2" in c.database_names()) self.assertEqual("bar", c.pymongo_test2.test.find_one()["foo"]) - if version.at_least(c, (1, 3, 3, 1)): + if (version.at_least(c, (1, 3, 3, 1)) + and not version.at_least(c, (2, 5, 3, -1))): c.drop_database("pymongo_test1") c.pymongo_test.add_user("mike", "password") @@ -315,13 +317,17 @@ class TestClient(unittest.TestCase, TestRequestMixin): if is_mongos(c) and not version.at_least(c, (2, 0, 0)): raise SkipTest("Auth with sharding requires MongoDB >= 2.0.0") - c.admin.system.users.remove({}) - c.pymongo_test.system.users.remove({}) + remove_all_users(c.pymongo_test) + remove_all_users(c.admin) try: - c.admin.add_user("admin", "pass") + c.admin.add_user("admin", "pass", + roles=['readWriteAnyDatabase', + 'userAdminAnyDatabase', + 'dbAdminAnyDatabase', + 'userAdmin']) c.admin.authenticate("admin", "pass") - c.pymongo_test.add_user("user", "pass") + c.pymongo_test.add_user("user", "pass", roles=['userAdmin', 'readWrite']) self.assertRaises(ConfigurationError, MongoClient, "mongodb://foo:bar@%s:%d" % (host, port)) @@ -358,8 +364,8 @@ class TestClient(unittest.TestCase, TestRequestMixin): finally: # Clean up. - c.admin.system.users.remove({}) - c.pymongo_test.system.users.remove({}) + remove_all_users(c.pymongo_test) + remove_all_users(c.admin) def test_lazy_auth_raises_operation_failure(self): # Check if we have the prerequisites to run this test. diff --git a/test/test_database.py b/test/test_database.py index 0726dbf51..e0a7a061f 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -41,7 +41,8 @@ from pymongo.son_manipulator import (AutoReference, NamespaceInjector, ObjectIdShuffler) from test import version -from test.utils import is_mongos, server_started_with_auth +from test.utils import (is_mongos, server_started_with_auth, + remove_all_users) from test.test_client import get_client @@ -330,6 +331,8 @@ class TestDatabase(unittest.TestCase): if (is_mongos(self.client) and not version.at_least(self.client, (2, 0, 0))): raise SkipTest("Auth with sharding requires MongoDB >= 2.0.0") + if version.at_least(self.client, (2, 5, 3, -1)): + raise SkipTest("Old auth requires MongoDB < 2.5.3") db = self.client.pymongo_test db.system.users.remove({}) db.remove_user("mike") @@ -373,13 +376,36 @@ class TestDatabase(unittest.TestCase): self.assertTrue(db.system.users.find({"readOnly": True}).count()) db.logout() + def test_new_user_cmds(self): + if not version.at_least(self.client, (2, 5, 3, -1)): + raise SkipTest("User manipulation commands " + "require MongoDB >= 2.5.3") + + db = self.client.pymongo_test + remove_all_users(db) + db.add_user("amalia", "password", roles=["userAdmin"]) + db.authenticate("amalia", "password") + # This tests the ability to update user attributes. + db.add_user("amalia", "new_password", customData={"secret": "koalas"}) + + user_info = db.command("usersInfo", "amalia") + self.assertTrue(user_info["users"]) + amalia_user = user_info["users"][0] + self.assertEqual(amalia_user["name"], "amalia") + self.assertEqual(amalia_user["customData"], {"secret": "koalas"}) + + db.remove_user("amalia") + db.logout() + def test_authenticate_and_safe(self): if (is_mongos(self.client) and not version.at_least(self.client, (2, 0, 0))): raise SkipTest("Auth with sharding requires MongoDB >= 2.0.0") db = self.client.auth_test - db.system.users.remove({}) - db.add_user("bernie", "password") + remove_all_users(db) + + db.add_user("bernie", "password", + roles=["userAdmin", "dbAdmin", "readWrite"]) db.authenticate("bernie", "password") db.test.remove({}) @@ -394,8 +420,8 @@ class TestDatabase(unittest.TestCase): db.test.remove({}).get('n')) self.assertEqual(0, db.test.count()) - self.client.drop_database("auth_test") - + db.remove_user("bernie") + db.logout() def test_authenticate_and_request(self): if (is_mongos(self.client) and not @@ -407,9 +433,9 @@ class TestDatabase(unittest.TestCase): # (in or not in a request) properly when it's finished. self.assertFalse(self.client.auto_start_request) db = self.client.pymongo_test - db.system.users.remove({}) - db.remove_user("mike") - db.add_user("mike", "password") + remove_all_users(db) + db.add_user("mike", "password", + roles=["userAdmin", "dbAdmin", "readWrite"]) self.assertFalse(self.client.in_request()) self.assertTrue(db.authenticate("mike", "password")) self.assertFalse(self.client.in_request()) @@ -421,10 +447,9 @@ class TestDatabase(unittest.TestCase): self.assertTrue(request_cx.in_request()) # just make sure there are no exceptions here + db.remove_user("mike") db.logout() - db.collection.find_one() request_db.logout() - request_db.collection.find_one() def test_authenticate_multiple(self): client = get_client() @@ -438,16 +463,25 @@ class TestDatabase(unittest.TestCase): users_db = client.pymongo_test admin_db = client.admin other_db = client.pymongo_test1 - users_db.system.users.remove() - admin_db.system.users.remove() + remove_all_users(users_db) + remove_all_users(admin_db) + remove_all_users(other_db) users_db.test.remove() other_db.test.remove() - admin_db.add_user('admin', 'pass') + admin_db.add_user('admin', 'pass', + roles=["userAdminAnyDatabase", "dbAdmin", + "clusterAdmin", "readWrite"]) self.assertTrue(admin_db.authenticate('admin', 'pass')) - admin_db.add_user('ro-admin', 'pass', read_only=True) - users_db.add_user('user', 'pass') + if version.at_least(self.client, (2, 5, 3, -1)): + admin_db.add_user('ro-admin', 'pass', + roles=["userAdmin", "readAnyDatabase"]) + else: + admin_db.add_user('ro-admin', 'pass', read_only=True) + + users_db.add_user('user', 'pass', + roles=["userAdmin", "readWrite"]) admin_db.logout() self.assertRaises(OperationFailure, users_db.test.find_one) @@ -480,9 +514,9 @@ class TestDatabase(unittest.TestCase): admin_db.logout() users_db.logout() self.assertTrue(admin_db.authenticate('admin', 'pass')) - self.assertTrue(admin_db.system.users.remove()) - self.assertEqual(0, admin_db.system.users.count()) - self.assertTrue(users_db.system.users.remove()) + users_db.remove_user('user') + admin_db.remove_user('ro-admin') + admin_db.remove_user('admin') def test_id_ordering(self): # PyMongo attempts to have _id show up first @@ -761,6 +795,5 @@ class TestDatabase(unittest.TestCase): self.assertEqual([], db.outgoing_manipulators) self.assertEqual(['AutoReference'], db.outgoing_copying_manipulators) - if __name__ == "__main__": unittest.main() diff --git a/test/test_threads.py b/test/test_threads.py index ceefd9d0c..e9afa034c 100644 --- a/test/test_threads.py +++ b/test/test_threads.py @@ -20,7 +20,8 @@ import traceback from nose.plugins.skip import SkipTest -from test.utils import server_started_with_auth, joinall, RendezvousThread +from test.utils import (joinall, remove_all_users, + server_started_with_auth, RendezvousThread) from test.test_client import get_client from pymongo.mongo_client import MongoClient from pymongo.replica_set_connection import MongoReplicaSetClient @@ -330,18 +331,23 @@ class BaseTestThreadsAuth(object): if not server_started_with_auth(client): raise SkipTest("Authentication is not enabled on server") self.client = client - self.client.admin.system.users.remove({}) - self.client.admin.add_user('admin-user', 'password') + remove_all_users(self.client.admin) + self.client.admin.add_user('admin-user', 'password', + roles=['clusterAdmin', + 'dbAdminAnyDatabase', + 'readWriteAnyDatabase', + 'userAdminAnyDatabase']) self.client.admin.authenticate("admin-user", "password") - self.client.auth_test.system.users.remove({}) - self.client.auth_test.add_user("test-user", "password") + remove_all_users(self.client.auth_test) + self.client.auth_test.add_user("test-user", "password", + roles=['readWrite']) def tearDown(self): # Remove auth users from databases self.client.admin.authenticate("admin-user", "password") - self.client.admin.system.users.remove({}) - self.client.auth_test.system.users.remove({}) + remove_all_users(self.client.auth_test) self.client.drop_database('auth_test') + remove_all_users(self.client.admin) # Clear client reference so that RSC's monitor thread # dies. self.client = None diff --git a/test/utils.py b/test/utils.py index 0782c11f2..bbc9bce85 100644 --- a/test/utils.py +++ b/test/utils.py @@ -19,7 +19,7 @@ import threading from pymongo import MongoClient, MongoReplicaSetClient from pymongo.errors import AutoReconnect from pymongo.pool import NO_REQUEST, NO_SOCKET_YET, SocketInfo -from test import host, port +from test import host, port, version def one(s): @@ -50,6 +50,13 @@ def drop_collections(db): if not coll.startswith('system'): db.drop_collection(coll) +def remove_all_users(db): + if version.at_least(db.connection, (2, 5, 3, -1)): + db.command({"removeUsersFromDatabase": 1}) + else: + db.system.users.remove({}) + + def joinall(threads): """Join threads with a 5-minute timeout, assert joins succeeded""" for t in threads: