Skip to content

Commit eda1e77

Browse files
committed
PYTHON-842 - SSL URI config support.
This commit cleans up and builds on the work in commit cc943f1.
1 parent cc943f1 commit eda1e77

File tree

6 files changed

+167
-76
lines changed

6 files changed

+167
-76
lines changed

pymongo/common.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,10 @@ def validate_cert_reqs(option, value):
124124
if value is None:
125125
return value
126126
if HAS_SSL:
127+
if isinstance(value, basestring) and hasattr(ssl, value):
128+
value = getattr(ssl, value)
127129
if value in (ssl.CERT_NONE, ssl.CERT_OPTIONAL, ssl.CERT_REQUIRED):
128130
return value
129-
elif isinstance(value, basestring) and hasattr(ssl, value) and \
130-
getattr(ssl, value) in (ssl.CERT_NONE, ssl.CERT_OPTIONAL, ssl.CERT_REQUIRED):
131-
return getattr(ssl, value)
132131
raise ConfigurationError("The value of %s must be one of: "
133132
"`ssl.CERT_NONE`, `ssl.CERT_OPTIONAL` or "
134133
"`ssl.CERT_REQUIRED" % (option,))

test/__init__.py

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

2121
import pymongo
2222
from nose.plugins.skip import SkipTest
23-
from pymongo.errors import OperationFailure
23+
from pymongo.errors import ConnectionFailure, OperationFailure
2424

2525
# hostnames retrieved by MongoReplicaSetClient from isMaster will be of unicode
2626
# type in Python 2, so ensure these hostnames are unicodes, too. It makes tests
@@ -42,30 +42,34 @@
4242
class AuthContext(object):
4343

4444
def __init__(self):
45-
self.client = pymongo.MongoClient(host, port)
4645
self.auth_enabled = False
4746
self.restricted_localhost = False
4847
try:
49-
command_line = self.client.admin.command('getCmdLineOpts')
50-
if self._server_started_with_auth(command_line):
51-
self.auth_enabled = True
52-
except OperationFailure, e:
53-
msg = e.details.get('errmsg', '')
54-
if e.code == 13 or 'unauthorized' in msg or 'login' in msg:
55-
self.auth_enabled = True
56-
self.restricted_localhost = True
57-
else:
58-
raise
59-
# See if the user has already been set up.
60-
try:
61-
self.client.admin.authenticate(db_user, db_pwd)
62-
self.user_provided = True
63-
except OperationFailure, e:
64-
msg = e.details.get('errmsg', '')
65-
if e.code == 18 or 'auth fails' in msg:
66-
self.user_provided = False
67-
else:
68-
raise
48+
self.client = pymongo.MongoClient(host, port)
49+
except ConnectionFailure:
50+
self.client = None
51+
else:
52+
try:
53+
command_line = self.client.admin.command('getCmdLineOpts')
54+
if self._server_started_with_auth(command_line):
55+
self.auth_enabled = True
56+
except OperationFailure, e:
57+
msg = e.details.get('errmsg', '')
58+
if e.code == 13 or 'unauthorized' in msg or 'login' in msg:
59+
self.auth_enabled = True
60+
self.restricted_localhost = True
61+
else:
62+
raise
63+
# See if the user has already been set up.
64+
try:
65+
self.client.admin.authenticate(db_user, db_pwd)
66+
self.user_provided = True
67+
except OperationFailure, e:
68+
msg = e.details.get('errmsg', '')
69+
if e.code == 18 or 'auth fails' in msg:
70+
self.user_provided = False
71+
else:
72+
raise
6973

7074
def _server_started_with_auth(self, command_line):
7175
# MongoDB >= 2.0

test/certificates/crl.pem

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-----BEGIN X509 CRL-----
2+
MIIBazCB1QIBATANBgkqhkiG9w0BAQUFADCBkjELMAkGA1UEBhMCVVMxETAPBgNV
3+
BAgMCE5ldyBZb3JrMRYwFAYDVQQHDA1OZXcgWW9yayBDaXR5MQ4wDAYDVQQKDAUx
4+
MEdlbjEPMA0GA1UECwwGS2VybmVsMRowGAYDVQQDDBFNeSBDZXJ0IEF1dGhvcml0
5+
eTEbMBkGCSqGSIb3DQEJARYMcm9vdEBsYXphcnVzFw0xMjEyMTIxODQ3NDFaFw00
6+
MDA0MjgxODQ3NDFaoA4wDDAKBgNVHRQEAwIBCzANBgkqhkiG9w0BAQUFAAOBgQAu
7+
PlPDGei2q6kdkoHe8vmDuts7Hm/o9LFbBmn0XUcfHisCJCPsJTyGCsgnfIiBcXJY
8+
1LMKsQFnYGv28rE2ZPpFg2qNxL+6qUEzCvqaHLX9q1V0F+f8hHDxucNYu52oo/h0
9+
uNZxB1KPFI2PReG5d3oUYqJ2+EctKkrGtxSPzbN0gg==
10+
-----END X509 CRL-----

test/certificates/server.pem

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAK53miP9GczBWXnq
3+
NxHwQkgVqsDuesjwJbWilMK4gf3fjnf2PN3qDpnGbZbPD0ij8975pIKtSPoDycFm
4+
A8Mogip0yU2Lv2lL56CWthSBftOFDL2CWIsmuuURFXZPiVLtLytfI9oLASZFlywW
5+
Cs83qEDTvdW8VoVhVsxV1JFDnpXLAgMBAAECgYBoGBgxrMt97UazhNkCrPT/CV5t
6+
6lv8E7yMGMrlOyzkCkR4ssQyK3o2qbutJTGbR6czvIM5LKbD9Qqlh3ZrNHokWmTR
7+
VQQpJxt8HwP5boQvwRHg9+KSGr4JvRko1qxFs9C7Bzjt4r9VxdjhwZPdy0McGI/z
8+
yPXyQHjqBayrHV1EwQJBANorfCKeIxLhH3LAeUZuRS8ACldJ2N1kL6Ov43/v+0S/
9+
OprQeBTODuTds3sv7FCT1aYDTOe6JLNOwN2i4YVOMBsCQQDMuCozrwqftD17D06P
10+
9+lRXUekY5kFBs5j28Xnl8t8jnuxsXtQUTru660LD0QrmDNSauhpEmlpJknicnGt
11+
hmwRAkEA12MI6bBPlir0/jgxQqxI1w7mJqj8Vg27zpEuO7dzzLoyJHddpcSNBbwu
12+
npaAakiZK42klj26T9+XHvjYRuAbMwJBAJ5WnwWEkGH/pUHGEAyYQdSVojDKe/MA
13+
Vae0tzguFswK5C8GyArSGRPsItYYA7D4MlG/sGx8Oh2C6MiFndkJzBECQDcP1y4r
14+
Qsek151t1zArLKH4gG5dQAeZ0Lc2VeC4nLMUqVwrHcZDdd1RzLlSaH3j1MekFVfT
15+
6v6rrcNLEVbeuk4=
16+
-----END PRIVATE KEY-----
17+
-----BEGIN CERTIFICATE-----
18+
MIIC7jCCAlegAwIBAgIBCjANBgkqhkiG9w0BAQUFADCBkjELMAkGA1UEBhMCVVMx
19+
ETAPBgNVBAgMCE5ldyBZb3JrMRYwFAYDVQQHDA1OZXcgWW9yayBDaXR5MQ4wDAYD
20+
VQQKDAUxMEdlbjEPMA0GA1UECwwGS2VybmVsMRowGAYDVQQDDBFNeSBDZXJ0IEF1
21+
dGhvcml0eTEbMBkGCSqGSIb3DQEJARYMcm9vdEBsYXphcnVzMB4XDTEzMTIwNTEz
22+
MjU0MFoXDTQxMDQyMTEzMjU0MFowajELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5l
23+
dyBZb3JrMRYwFAYDVQQHDA1OZXcgWW9yayBDaXR5MQ4wDAYDVQQKDAUxMEdlbjEP
24+
MA0GA1UECwwGS2VybmVsMQ8wDQYDVQQDDAZzZXJ2ZXIwgZ8wDQYJKoZIhvcNAQEB
25+
BQADgY0AMIGJAoGBAK53miP9GczBWXnqNxHwQkgVqsDuesjwJbWilMK4gf3fjnf2
26+
PN3qDpnGbZbPD0ij8975pIKtSPoDycFmA8Mogip0yU2Lv2lL56CWthSBftOFDL2C
27+
WIsmuuURFXZPiVLtLytfI9oLASZFlywWCs83qEDTvdW8VoVhVsxV1JFDnpXLAgMB
28+
AAGjezB5MAkGA1UdEwQCMAAwLAYJYIZIAYb4QgENBB8WHU9wZW5TU0wgR2VuZXJh
29+
dGVkIENlcnRpZmljYXRlMB0GA1UdDgQWBBQgCkKiZhUV9/Zo7RwYYwm2cNK6tzAf
30+
BgNVHSMEGDAWgBQHQRk6n37FtyJOt7zV3+T8CbhkFjANBgkqhkiG9w0BAQUFAAOB
31+
gQCbsfr+Q4pty4Fy38lSxoCgnbB4pX6+Ex3xyw5zxDYR3xUlb/uHBiNZ1dBrXBxU
32+
ekU8dEvf+hx4iRDSW/C5N6BGnBBhCHcrPabo2bEEWKVsbUC3xchTB5rNGkvnMt9t
33+
G9ol7vanuzjL3S8/2PB33OshkBH570CxqqPflQbdjwt9dg==
34+
-----END CERTIFICATE-----

test/test_common.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,11 @@
2727
from bson.objectid import ObjectId
2828
from bson.son import SON
2929
from pymongo.connection import Connection
30-
from pymongo import common
3130
from pymongo.mongo_client import MongoClient
3231
from pymongo.mongo_replica_set_client import MongoReplicaSetClient
3332
from pymongo.errors import ConfigurationError, OperationFailure
3433
from test import host, port, pair, version, skip_restricted_localhost
3534
from test.utils import catch_warnings, drop_collections
36-
from ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED
3735

3836
have_uuid = True
3937
try:
@@ -522,20 +520,6 @@ def test_mongo_replica_set_client(self):
522520
finally:
523521
ctx.exit()
524522

525-
def test_validate_cert_reqs(self):
526-
self.assertRaises(ConfigurationError, common.validate_cert_reqs, 'ssl_cert_reqs', 3)
527-
self.assertRaises(ConfigurationError, common.validate_cert_reqs, 'ssl_cert_reqs', -1)
528-
self.assertEqual(common.validate_cert_reqs('ssl_cert_reqs', None), None)
529-
self.assertEqual(common.validate_cert_reqs('ssl_cert_reqs', CERT_NONE), CERT_NONE)
530-
self.assertEqual(common.validate_cert_reqs('ssl_cert_reqs', CERT_OPTIONAL), CERT_OPTIONAL)
531-
self.assertEqual(common.validate_cert_reqs('ssl_cert_reqs', CERT_REQUIRED), CERT_REQUIRED)
532-
self.assertEqual(common.validate_cert_reqs('ssl_cert_reqs', 0), CERT_NONE)
533-
self.assertEqual(common.validate_cert_reqs('ssl_cert_reqs', 1), CERT_OPTIONAL)
534-
self.assertEqual(common.validate_cert_reqs('ssl_cert_reqs', 2), CERT_REQUIRED)
535-
self.assertEqual(common.validate_cert_reqs('ssl_cert_reqs', 'CERT_NONE'), CERT_NONE)
536-
self.assertEqual(common.validate_cert_reqs('ssl_cert_reqs', 'CERT_OPTIONAL'), CERT_OPTIONAL)
537-
self.assertEqual(common.validate_cert_reqs('ssl_cert_reqs', 'CERT_REQUIRED'), CERT_REQUIRED)
538-
539523

540524
if __name__ == "__main__":
541525
unittest.main()

test/test_ssl.py

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,20 @@
1919
import sys
2020
import unittest
2121

22+
sys.path[0:0] = [""]
23+
2224
try:
2325
from ssl import CertificateError
2426
except ImportError:
2527
# Backport.
2628
from pymongo.ssl_match_hostname import CertificateError
2729

28-
sys.path[0:0] = [""]
29-
3030
from urllib import quote_plus
3131

3232
from nose.plugins.skip import SkipTest
3333

3434
from pymongo import MongoClient, MongoReplicaSetClient
35-
from pymongo.common import HAS_SSL
35+
from pymongo.common import HAS_SSL, validate_cert_reqs
3636
from pymongo.errors import (ConfigurationError,
3737
ConnectionFailure,
3838
OperationFailure)
@@ -52,9 +52,9 @@
5252

5353
# To fully test this start a mongod instance (built with SSL support) like so:
5454
# mongod --dbpath /path/to/data/directory --sslOnNormalPorts \
55-
# --sslPEMKeyFile /path/to/mongo/jstests/libs/server.pem \
56-
# --sslCAFile /path/to/mongo/jstests/libs/ca.pem \
57-
# --sslCRLFile /path/to/mongo/jstests/libs/crl.pem \
55+
# --sslPEMKeyFile /path/to/pymongo/test/certificates/server.pem \
56+
# --sslCAFile /path/to/pymongo/test/certificates/ca.pem \
57+
# --sslCRLFile /path/to/pymongo/test/certificates/crl.pem \
5858
# --sslWeakCertificateValidation
5959
# Also, make sure you have 'server' as an alias for localhost in /etc/hosts
6060
#
@@ -85,17 +85,19 @@ def is_server_resolvable():
8585
MongoClient(host, port, connectTimeoutMS=100, ssl=True)
8686
SIMPLE_SSL = True
8787
except ConnectionFailure:
88-
# Is MongoDB configured with server.pem, ca.pem, and crl.pem from
89-
# mongodb jstests/lib?
90-
try:
91-
MongoClient(host, port, connectTimeoutMS=100, ssl=True,
92-
ssl_certfile=CLIENT_PEM)
93-
CERT_SSL = True
94-
except ConnectionFailure:
95-
pass
88+
pass
89+
90+
# Is MongoDB configured with server.pem, ca.pem, and crl.pem from
91+
# mongodb jstests/lib?
92+
try:
93+
MongoClient(host, port, connectTimeoutMS=100, ssl=True,
94+
ssl_certfile=CLIENT_PEM)
95+
CERT_SSL = True
96+
except ConnectionFailure:
97+
pass
9698

97-
if CERT_SSL:
98-
SERVER_IS_RESOLVABLE = is_server_resolvable()
99+
if CERT_SSL:
100+
SERVER_IS_RESOLVABLE = is_server_resolvable()
99101

100102

101103
class TestClientSSL(unittest.TestCase):
@@ -123,7 +125,6 @@ def test_no_ssl_module(self):
123125
ssl_certfile=CLIENT_PEM)
124126

125127
def test_config_ssl(self):
126-
"""Tests various ssl configurations"""
127128
self.assertRaises(ConfigurationError, MongoClient, ssl='foo')
128129
self.assertRaises(ConfigurationError,
129130
MongoClient,
@@ -199,6 +200,39 @@ def test_config_ssl(self):
199200
ssl_keyfile=CLIENT_PEM,
200201
ssl_certfile=CLIENT_PEM)
201202

203+
self.assertRaises(
204+
ConfigurationError, validate_cert_reqs, 'ssl_cert_reqs', 3)
205+
self.assertRaises(
206+
ConfigurationError, validate_cert_reqs, 'ssl_cert_reqs', -1)
207+
self.assertRaises(
208+
ConfigurationError, validate_cert_reqs, 'ssl_cert_reqs', 'foo')
209+
self.assertEqual(
210+
validate_cert_reqs('ssl_cert_reqs', None), None)
211+
self.assertEqual(
212+
validate_cert_reqs('ssl_cert_reqs', ssl.CERT_NONE),
213+
ssl.CERT_NONE)
214+
self.assertEqual(
215+
validate_cert_reqs('ssl_cert_reqs', ssl.CERT_OPTIONAL),
216+
ssl.CERT_OPTIONAL)
217+
self.assertEqual(
218+
validate_cert_reqs('ssl_cert_reqs', ssl.CERT_REQUIRED),
219+
ssl.CERT_REQUIRED)
220+
self.assertEqual(
221+
validate_cert_reqs('ssl_cert_reqs', 0), ssl.CERT_NONE)
222+
self.assertEqual(
223+
validate_cert_reqs('ssl_cert_reqs', 1), ssl.CERT_OPTIONAL)
224+
self.assertEqual(
225+
validate_cert_reqs('ssl_cert_reqs', 2), ssl.CERT_REQUIRED)
226+
self.assertEqual(
227+
validate_cert_reqs('ssl_cert_reqs', 'CERT_NONE'),
228+
ssl.CERT_NONE)
229+
self.assertEqual(
230+
validate_cert_reqs('ssl_cert_reqs', 'CERT_OPTIONAL'),
231+
ssl.CERT_OPTIONAL)
232+
self.assertEqual(
233+
validate_cert_reqs('ssl_cert_reqs', 'CERT_REQUIRED'),
234+
ssl.CERT_REQUIRED)
235+
202236

203237
class TestSSL(unittest.TestCase):
204238

@@ -234,9 +268,9 @@ def test_cert_ssl(self):
234268
# Expects the server to be running with the server.pem, ca.pem
235269
# and crl.pem provided in mongodb and the server tests eg:
236270
#
237-
# --sslPEMKeyFile=jstests/libs/server.pem
238-
# --sslCAFile=jstests/libs/ca.pem
239-
# --sslCRLFile=jstests/libs/crl.pem
271+
# --sslPEMKeyFile=/path/to/pymongo/test/certificates/server.pem
272+
# --sslCAFile=/path/to/pymongo/test/certificates/ca.pem
273+
# --sslCRLFile=/path/to/pymongo/test/certificates/crl.pem
240274
#
241275
# Also requires an /etc/hosts entry where "server" is resolvable
242276
if not CERT_SSL:
@@ -260,9 +294,9 @@ def test_cert_ssl_implicitly_set(self):
260294
# Expects the server to be running with the server.pem, ca.pem
261295
# and crl.pem provided in mongodb and the server tests eg:
262296
#
263-
# --sslPEMKeyFile=jstests/libs/server.pem
264-
# --sslCAFile=jstests/libs/ca.pem
265-
# --sslCRLFile=jstests/libs/crl.pem
297+
# --sslPEMKeyFile=/path/to/pymongo/test/certificates/server.pem
298+
# --sslCAFile=/path/to/pymongo/test/certificates/ca.pem
299+
# --sslCRLFile=/path/to/pymongo/test/certificates/crl.pem
266300
#
267301
# Also requires an /etc/hosts entry where "server" is resolvable
268302
if not CERT_SSL:
@@ -286,9 +320,9 @@ def test_cert_ssl_validation(self):
286320
# Expects the server to be running with the server.pem, ca.pem
287321
# and crl.pem provided in mongodb and the server tests eg:
288322
#
289-
# --sslPEMKeyFile=jstests/libs/server.pem
290-
# --sslCAFile=jstests/libs/ca.pem
291-
# --sslCRLFile=jstests/libs/crl.pem
323+
# --sslPEMKeyFile=/path/to/pymongo/test/certificates/server.pem
324+
# --sslCAFile=/path/to/pymongo/test/certificates/ca.pem
325+
# --sslCRLFile=/path/to/pymongo/test/certificates/crl.pem
292326
#
293327
# Also requires an /etc/hosts entry where "server" is resolvable
294328
if not CERT_SSL:
@@ -323,13 +357,39 @@ def test_cert_ssl_validation(self):
323357
self.assertTrue(db.test.find_one()['ssl'])
324358
client.drop_database('pymongo_ssl_test')
325359

360+
def test_cert_ssl_uri_support(self):
361+
# Expects the server to be running with the server.pem, ca.pem
362+
# and crl.pem provided in mongodb and the server tests eg:
363+
#
364+
# --sslPEMKeyFile=/path/to/pymongo/test/certificates/server.pem
365+
# --sslCAFile=/path/to/pymongo/test/certificates/ca.pem
366+
# --sslCRLFile=/path/to/pymongo/test/certificates/crl.pem
367+
#
368+
# Also requires an /etc/hosts entry where "server" is resolvable
369+
if not CERT_SSL:
370+
raise SkipTest("No mongod available over SSL with certs")
371+
372+
if not SERVER_IS_RESOLVABLE:
373+
raise SkipTest("No hosts entry for 'server'. Cannot validate "
374+
"hostname in the certificate")
375+
376+
uri_fmt = ("mongodb://server/?ssl=true&ssl_certfile=%s&ssl_cert_reqs"
377+
"=%s&ssl_ca_certs=%s")
378+
client = MongoClient(uri_fmt % (CLIENT_PEM, 'CERT_REQUIRED', CA_PEM))
379+
380+
db = client.pymongo_ssl_test
381+
db.test.drop()
382+
db.test.insert({'ssl': True})
383+
self.assertTrue(db.test.find_one()['ssl'])
384+
client.drop_database('pymongo_ssl_test')
385+
326386
def test_cert_ssl_validation_optional(self):
327387
# Expects the server to be running with the server.pem, ca.pem
328388
# and crl.pem provided in mongodb and the server tests eg:
329389
#
330-
# --sslPEMKeyFile=jstests/libs/server.pem
331-
# --sslCAFile=jstests/libs/ca.pem
332-
# --sslCRLFile=jstests/libs/crl.pem
390+
# --sslPEMKeyFile=/path/to/pymongo/test/certificates/server.pem
391+
# --sslCAFile=/path/to/pymongo/test/certificates/ca.pem
392+
# --sslCRLFile=/path/to/pymongo/test/certificates/crl.pem
333393
#
334394
# Also requires an /etc/hosts entry where "server" is resolvable
335395
if not CERT_SSL:
@@ -369,9 +429,9 @@ def test_cert_ssl_validation_hostname_fail(self):
369429
# Expects the server to be running with the server.pem, ca.pem
370430
# and crl.pem provided in mongodb and the server tests eg:
371431
#
372-
# --sslPEMKeyFile=jstests/libs/server.pem
373-
# --sslCAFile=jstests/libs/ca.pem
374-
# --sslCRLFile=jstests/libs/crl.pem
432+
# --sslPEMKeyFile=/path/to/pymongo/test/certificates/server.pem
433+
# --sslCAFile=/path/to/pymongo/test/certificates/ca.pem
434+
# --sslCRLFile=/path/to/pymongo/test/certificates/crl.pem
375435
if not CERT_SSL:
376436
raise SkipTest("No mongod available over SSL with certs")
377437

@@ -406,9 +466,9 @@ def test_mongodb_x509_auth(self):
406466
# and crl.pem provided in mongodb and the server tests as well as
407467
# --auth
408468
#
409-
# --sslPEMKeyFile=jstests/libs/server.pem
410-
# --sslCAFile=jstests/libs/ca.pem
411-
# --sslCRLFile=jstests/libs/crl.pem
469+
# --sslPEMKeyFile=/path/to/pymongo/test/certificates/server.pem
470+
# --sslCAFile=/path/to/pymongo/test/certificates/ca.pem
471+
# --sslCRLFile=/path/to/pymongo/test/certificates/crl.pem
412472
# --auth
413473
if not CERT_SSL:
414474
raise SkipTest("No mongod available over SSL with certs")

0 commit comments

Comments
 (0)