Skip to content

Commit a02e37b

Browse files
hawkabehackett
authored andcommitted
PYTHON-552: Manipulate user objects exclusively via commands for 2.6
1 parent 6bd590a commit a02e37b

File tree

6 files changed

+169
-58
lines changed

6 files changed

+169
-58
lines changed

pymongo/database.py

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,50 @@ def __iter__(self):
615615
def next(self):
616616
raise TypeError("'Database' object is not iterable")
617617

618+
def _create_user(self, name, password, **kwargs):
619+
"""Uses v2 commands for creating a new user.
620+
"""
621+
create_opts = {}
622+
if password is not None:
623+
create_opts["pwd"] = password
624+
if "roles" not in kwargs:
625+
create_opts["roles"] = []
626+
create_opts["writeConcern"] = self._get_wc_override()
627+
create_opts.update(kwargs)
628+
629+
self.command("createUser", name, **create_opts)
630+
631+
def _update_user(self, name, password, **kwargs):
632+
"""Uses v2 commands for updating a user.
633+
"""
634+
update_opts = {}
635+
if password is not None:
636+
update_opts["pwd"] = password
637+
update_opts["writeConcern"] = self._get_wc_override()
638+
update_opts.update(kwargs)
639+
640+
self.command("updateUser", name, **update_opts)
641+
642+
def _legacy_add_user(self, name, password, read_only, **kwargs):
643+
"""Uses v1 system to add users, i.e. saving to system.users.
644+
"""
645+
user = self.system.users.find_one({"user": name}) or {"user": name}
646+
if password is not None:
647+
user["pwd"] = auth._password_digest(name, password)
648+
if read_only is not None:
649+
user["readOnly"] = common.validate_boolean('read_only', read_only)
650+
user.update(kwargs)
651+
652+
try:
653+
self.system.users.save(user, **self._get_wc_override())
654+
except OperationFailure, e:
655+
# First admin user add fails gle in MongoDB >= 2.1.2
656+
# See SERVER-4225 for more information.
657+
if 'login' in str(e):
658+
pass
659+
else:
660+
raise
661+
618662
def add_user(self, name, password=None, read_only=None, **kwargs):
619663
"""Create user `name` with password `password`.
620664
@@ -644,22 +688,19 @@ def add_user(self, name, password=None, read_only=None, **kwargs):
644688
.. versionadded:: 1.4
645689
"""
646690

647-
user = self.system.users.find_one({"user": name}) or {"user": name}
648-
if password is not None:
649-
user["pwd"] = auth._password_digest(name, password)
650-
if read_only is not None:
651-
user["readOnly"] = common.validate_boolean('read_only', read_only)
652-
user.update(kwargs)
653-
654691
try:
655-
self.system.users.save(user, **self._get_wc_override())
656-
except OperationFailure, e:
657-
# First admin user add fails gle in MongoDB >= 2.1.2
658-
# See SERVER-4225 for more information.
659-
if 'login' in str(e):
660-
pass
661-
else:
662-
raise
692+
uinfo = self.command("usersInfo", name)
693+
694+
except OperationFailure, exc:
695+
if exc.code is None:
696+
self._legacy_add_user(name, password, read_only, **kwargs)
697+
return
698+
raise
699+
700+
if uinfo["users"]:
701+
self._update_user(name, password, **kwargs)
702+
else:
703+
self._create_user(name, password, **kwargs)
663704

664705
def remove_user(self, name):
665706
"""Remove user `name` from this :class:`Database`.
@@ -672,7 +713,16 @@ def remove_user(self, name):
672713
673714
.. versionadded:: 1.4
674715
"""
675-
self.system.users.remove({"user": name}, **self._get_wc_override())
716+
717+
try:
718+
self.command("removeUser", name,
719+
writeConcern=self._get_wc_override())
720+
except OperationFailure, exc:
721+
if exc.code is None:
722+
self.system.users.remove({"user": name},
723+
**self._get_wc_override())
724+
return
725+
raise
676726

677727
def authenticate(self, name, password=None,
678728
source=None, mechanism='MONGODB-CR', **kwargs):

test/test_auth.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,14 @@ def setUp(self):
201201
raise SkipTest('Authentication is not enabled on server')
202202
response = client.admin.command('ismaster')
203203
self.set_name = str(response.get('setName', ''))
204-
client.pymongo_test.add_user('user', 'pass')
205-
client.admin.add_user('admin', 'pass')
204+
client.admin.add_user('admin', 'pass', roles=['userAdminAnyDatabase',
205+
'dbAdminAnyDatabase',
206+
'readWriteAnyDatabase',
207+
'clusterAdmin'])
208+
client.admin.authenticate('admin', 'pass')
209+
client.pymongo_test.add_user('user', 'pass',
210+
roles=['userAdmin', 'readWrite'])
211+
206212
if self.set_name:
207213
# GLE requires authentication.
208214
client.admin.authenticate('admin', 'pass')
@@ -214,8 +220,9 @@ def setUp(self):
214220

215221
def tearDown(self):
216222
self.client.admin.authenticate('admin', 'pass')
217-
self.client.pymongo_test.system.users.remove()
218-
self.client.admin.system.users.remove()
223+
self.client.pymongo_test.remove_user('user')
224+
self.client.admin.remove_user('admin')
225+
self.client.pymongo_test.logout()
219226
self.client.admin.logout()
220227
self.client = None
221228

@@ -275,7 +282,9 @@ def setUp(self):
275282
raise SkipTest('Delegated authentication requires MongoDB >= 2.4.0')
276283
if not server_started_with_auth(self.client):
277284
raise SkipTest('Authentication is not enabled on server')
278-
# Give admin all priviledges.
285+
if version.at_least(self.client, (2, 5, 3, -1)):
286+
raise SkipTest('Delegated auth does not exist in MongoDB >= 2.5.3')
287+
# Give admin all privileges.
279288
self.client.admin.add_user('admin', 'pass',
280289
roles=['readAnyDatabase',
281290
'readWriteAnyDatabase',
@@ -285,10 +294,10 @@ def setUp(self):
285294

286295
def tearDown(self):
287296
self.client.admin.authenticate('admin', 'pass')
288-
self.client.pymongo_test.system.users.remove()
289-
self.client.pymongo_test2.system.users.remove()
297+
self.client.pymongo_test.remove_user('user')
298+
self.client.pymongo_test2.remove_user('user')
290299
self.client.pymongo_test2.foo.remove()
291-
self.client.admin.system.users.remove()
300+
self.client.admin.remove_user('admin')
292301
self.client.admin.logout()
293302
self.client = None
294303

test/test_client.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from test.utils import (assertRaisesExactly,
4444
delay,
4545
is_mongos,
46+
remove_all_users,
4647
server_is_master_with_slave,
4748
server_started_with_auth,
4849
TestRequestMixin)
@@ -238,7 +239,8 @@ def test_copy_db(self):
238239
self.assertTrue("pymongo_test2" in c.database_names())
239240
self.assertEqual("bar", c.pymongo_test2.test.find_one()["foo"])
240241

241-
if version.at_least(c, (1, 3, 3, 1)):
242+
if (version.at_least(c, (1, 3, 3, 1))
243+
and not version.at_least(c, (2, 5, 3, -1))):
242244
c.drop_database("pymongo_test1")
243245

244246
c.pymongo_test.add_user("mike", "password")
@@ -315,13 +317,17 @@ def test_auth_from_uri(self):
315317
if is_mongos(c) and not version.at_least(c, (2, 0, 0)):
316318
raise SkipTest("Auth with sharding requires MongoDB >= 2.0.0")
317319

318-
c.admin.system.users.remove({})
319-
c.pymongo_test.system.users.remove({})
320+
remove_all_users(c.pymongo_test)
321+
remove_all_users(c.admin)
320322

321323
try:
322-
c.admin.add_user("admin", "pass")
324+
c.admin.add_user("admin", "pass",
325+
roles=['readWriteAnyDatabase',
326+
'userAdminAnyDatabase',
327+
'dbAdminAnyDatabase',
328+
'userAdmin'])
323329
c.admin.authenticate("admin", "pass")
324-
c.pymongo_test.add_user("user", "pass")
330+
c.pymongo_test.add_user("user", "pass", roles=['userAdmin', 'readWrite'])
325331

326332
self.assertRaises(ConfigurationError, MongoClient,
327333
"mongodb://foo:bar@%s:%d" % (host, port))
@@ -358,8 +364,8 @@ def test_auth_from_uri(self):
358364

359365
finally:
360366
# Clean up.
361-
c.admin.system.users.remove({})
362-
c.pymongo_test.system.users.remove({})
367+
remove_all_users(c.pymongo_test)
368+
remove_all_users(c.admin)
363369

364370
def test_lazy_auth_raises_operation_failure(self):
365371
# Check if we have the prerequisites to run this test.

test/test_database.py

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
NamespaceInjector,
4242
ObjectIdShuffler)
4343
from test import version
44-
from test.utils import is_mongos, server_started_with_auth
44+
from test.utils import (is_mongos, server_started_with_auth,
45+
remove_all_users)
4546
from test.test_client import get_client
4647

4748

@@ -330,6 +331,8 @@ def test_authenticate_add_remove_user(self):
330331
if (is_mongos(self.client) and not
331332
version.at_least(self.client, (2, 0, 0))):
332333
raise SkipTest("Auth with sharding requires MongoDB >= 2.0.0")
334+
if version.at_least(self.client, (2, 5, 3, -1)):
335+
raise SkipTest("Old auth requires MongoDB < 2.5.3")
333336
db = self.client.pymongo_test
334337
db.system.users.remove({})
335338
db.remove_user("mike")
@@ -373,13 +376,36 @@ def test_authenticate_add_remove_user(self):
373376
self.assertTrue(db.system.users.find({"readOnly": True}).count())
374377
db.logout()
375378

379+
def test_new_user_cmds(self):
380+
if not version.at_least(self.client, (2, 5, 3, -1)):
381+
raise SkipTest("User manipulation commands "
382+
"require MongoDB >= 2.5.3")
383+
384+
db = self.client.pymongo_test
385+
remove_all_users(db)
386+
db.add_user("amalia", "password", roles=["userAdmin"])
387+
db.authenticate("amalia", "password")
388+
# This tests the ability to update user attributes.
389+
db.add_user("amalia", "new_password", customData={"secret": "koalas"})
390+
391+
user_info = db.command("usersInfo", "amalia")
392+
self.assertTrue(user_info["users"])
393+
amalia_user = user_info["users"][0]
394+
self.assertEqual(amalia_user["name"], "amalia")
395+
self.assertEqual(amalia_user["customData"], {"secret": "koalas"})
396+
397+
db.remove_user("amalia")
398+
db.logout()
399+
376400
def test_authenticate_and_safe(self):
377401
if (is_mongos(self.client) and not
378402
version.at_least(self.client, (2, 0, 0))):
379403
raise SkipTest("Auth with sharding requires MongoDB >= 2.0.0")
380404
db = self.client.auth_test
381-
db.system.users.remove({})
382-
db.add_user("bernie", "password")
405+
remove_all_users(db)
406+
407+
db.add_user("bernie", "password",
408+
roles=["userAdmin", "dbAdmin", "readWrite"])
383409
db.authenticate("bernie", "password")
384410

385411
db.test.remove({})
@@ -394,8 +420,8 @@ def test_authenticate_and_safe(self):
394420
db.test.remove({}).get('n'))
395421

396422
self.assertEqual(0, db.test.count())
397-
self.client.drop_database("auth_test")
398-
423+
db.remove_user("bernie")
424+
db.logout()
399425

400426
def test_authenticate_and_request(self):
401427
if (is_mongos(self.client) and not
@@ -407,9 +433,9 @@ def test_authenticate_and_request(self):
407433
# (in or not in a request) properly when it's finished.
408434
self.assertFalse(self.client.auto_start_request)
409435
db = self.client.pymongo_test
410-
db.system.users.remove({})
411-
db.remove_user("mike")
412-
db.add_user("mike", "password")
436+
remove_all_users(db)
437+
db.add_user("mike", "password",
438+
roles=["userAdmin", "dbAdmin", "readWrite"])
413439
self.assertFalse(self.client.in_request())
414440
self.assertTrue(db.authenticate("mike", "password"))
415441
self.assertFalse(self.client.in_request())
@@ -421,10 +447,9 @@ def test_authenticate_and_request(self):
421447
self.assertTrue(request_cx.in_request())
422448

423449
# just make sure there are no exceptions here
450+
db.remove_user("mike")
424451
db.logout()
425-
db.collection.find_one()
426452
request_db.logout()
427-
request_db.collection.find_one()
428453

429454
def test_authenticate_multiple(self):
430455
client = get_client()
@@ -438,16 +463,25 @@ def test_authenticate_multiple(self):
438463
users_db = client.pymongo_test
439464
admin_db = client.admin
440465
other_db = client.pymongo_test1
441-
users_db.system.users.remove()
442-
admin_db.system.users.remove()
466+
remove_all_users(users_db)
467+
remove_all_users(admin_db)
468+
remove_all_users(other_db)
443469
users_db.test.remove()
444470
other_db.test.remove()
445471

446-
admin_db.add_user('admin', 'pass')
472+
admin_db.add_user('admin', 'pass',
473+
roles=["userAdminAnyDatabase", "dbAdmin",
474+
"clusterAdmin", "readWrite"])
447475
self.assertTrue(admin_db.authenticate('admin', 'pass'))
448476

449-
admin_db.add_user('ro-admin', 'pass', read_only=True)
450-
users_db.add_user('user', 'pass')
477+
if version.at_least(self.client, (2, 5, 3, -1)):
478+
admin_db.add_user('ro-admin', 'pass',
479+
roles=["userAdmin", "readAnyDatabase"])
480+
else:
481+
admin_db.add_user('ro-admin', 'pass', read_only=True)
482+
483+
users_db.add_user('user', 'pass',
484+
roles=["userAdmin", "readWrite"])
451485

452486
admin_db.logout()
453487
self.assertRaises(OperationFailure, users_db.test.find_one)
@@ -480,9 +514,9 @@ def test_authenticate_multiple(self):
480514
admin_db.logout()
481515
users_db.logout()
482516
self.assertTrue(admin_db.authenticate('admin', 'pass'))
483-
self.assertTrue(admin_db.system.users.remove())
484-
self.assertEqual(0, admin_db.system.users.count())
485-
self.assertTrue(users_db.system.users.remove())
517+
users_db.remove_user('user')
518+
admin_db.remove_user('ro-admin')
519+
admin_db.remove_user('admin')
486520

487521
def test_id_ordering(self):
488522
# PyMongo attempts to have _id show up first
@@ -761,6 +795,5 @@ def test_manipulator_properties(self):
761795
self.assertEqual([], db.outgoing_manipulators)
762796
self.assertEqual(['AutoReference'], db.outgoing_copying_manipulators)
763797

764-
765798
if __name__ == "__main__":
766799
unittest.main()

test/test_threads.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020

2121
from nose.plugins.skip import SkipTest
2222

23-
from test.utils import server_started_with_auth, joinall, RendezvousThread
23+
from test.utils import (joinall, remove_all_users,
24+
server_started_with_auth, RendezvousThread)
2425
from test.test_client import get_client
2526
from pymongo.mongo_client import MongoClient
2627
from pymongo.replica_set_connection import MongoReplicaSetClient
@@ -330,18 +331,23 @@ def setUp(self):
330331
if not server_started_with_auth(client):
331332
raise SkipTest("Authentication is not enabled on server")
332333
self.client = client
333-
self.client.admin.system.users.remove({})
334-
self.client.admin.add_user('admin-user', 'password')
334+
remove_all_users(self.client.admin)
335+
self.client.admin.add_user('admin-user', 'password',
336+
roles=['clusterAdmin',
337+
'dbAdminAnyDatabase',
338+
'readWriteAnyDatabase',
339+
'userAdminAnyDatabase'])
335340
self.client.admin.authenticate("admin-user", "password")
336-
self.client.auth_test.system.users.remove({})
337-
self.client.auth_test.add_user("test-user", "password")
341+
remove_all_users(self.client.auth_test)
342+
self.client.auth_test.add_user("test-user", "password",
343+
roles=['readWrite'])
338344

339345
def tearDown(self):
340346
# Remove auth users from databases
341347
self.client.admin.authenticate("admin-user", "password")
342-
self.client.admin.system.users.remove({})
343-
self.client.auth_test.system.users.remove({})
348+
remove_all_users(self.client.auth_test)
344349
self.client.drop_database('auth_test')
350+
remove_all_users(self.client.admin)
345351
# Clear client reference so that RSC's monitor thread
346352
# dies.
347353
self.client = None

0 commit comments

Comments
 (0)