Skip to content

Commit

Permalink
Merge pull request #187 from IlyaSkriblovsky/x509-auth
Browse files Browse the repository at this point in the history
X509 auth
  • Loading branch information
psi29a committed Oct 4, 2016
2 parents 6fa7b72 + 484efe3 commit eee4bb3
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 2 deletions.
3 changes: 3 additions & 0 deletions docs/source/NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Features
^^^^^^^^

- Full-text indexes can be used with new ``filter.TEXT()``
- Client authentication by X509 certificates. Use your client certificate when connecting
to MongoDB and then call ``Database.authenticate`` with certificate subject as username,
empty password and ``mechanism="MONGODB-X509"``.

Release 16.2.0 (2016-10-02)
---------------------------
Expand Down
2 changes: 2 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ setuptools
tox
check-manifest
mock
pyopenssl
service_identity
4 changes: 3 additions & 1 deletion tests/mongod.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class Mongod(object):
# so leaving this for now
success_message = b"waiting for connections on port"

def __init__(self, port=27017, auth=False, replset=None, dbpath=None):
def __init__(self, port=27017, auth=False, replset=None, dbpath=None, args=()):
self.__proc = None
self.__notify_waiting = []
self.__notify_stop = []
Expand All @@ -44,6 +44,7 @@ def __init__(self, port=27017, auth=False, replset=None, dbpath=None):
self.port = port
self.auth = auth
self.replset = replset
self.args = args

if dbpath is None:
self.__datadir = tempfile.mkdtemp()
Expand Down Expand Up @@ -71,6 +72,7 @@ def start(self):
]
if self.auth: args.append(b"--auth")
if self.replset: args.extend([b"--replSet", self.replset])
args.extend(arg.encode() for arg in self.args)
from os import environ
self.__proc = reactor.spawnProcess(self, b"mongod", args, env=environ)
return d
Expand Down
191 changes: 190 additions & 1 deletion tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@

from __future__ import absolute_import, division
from pymongo.errors import OperationFailure
import os
import shutil
import tempfile
from twisted.trial import unittest
from twisted.internet import defer
from twisted.internet import defer, ssl
from txmongo import connection
from txmongo.protocol import MongoAuthenticationError

Expand Down Expand Up @@ -347,3 +348,191 @@ def test_Fail(self):
yield self.assertFailure(query, OperationFailure)
finally:
yield conn.disconnect()


class TestX509(unittest.TestCase):

ca_subject = "CN=testing,O=txmongo"
ca_cert = """
-----BEGIN CERTIFICATE-----
MIICFjCCAX+gAwIBAgIJAMrkIW39mCd8MA0GCSqGSIb3DQEBCwUAMCQxEDAOBgNV
BAoMB3R4bW9uZ28xEDAOBgNVBAMMB3Rlc3RpbmcwHhcNMTYxMDAzMjA0MjMzWhcN
NDQwMjE5MjA0MjMzWjAkMRAwDgYDVQQKDAd0eG1vbmdvMRAwDgYDVQQDDAd0ZXN0
aW5nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNO105VaGOMGj0DTAob8gE
zjYDT5jUh7WLjOLRHLzxsOUxcREPkfY57x4+c2fscYzKTGMETcJnjPK1SUbtB5dx
f/f8uplSCbWwQDYc/tOW9FGsUYeocVnt079b72J8zkMnHOZ1e0ro9L8ThZvpbA8E
fEYqOaTqlxCrkhjjmXqVkQIDAQABo1AwTjAdBgNVHQ4EFgQUGLfQGshMNtsO6zSH
mnLMeBC2ZcAwHwYDVR0jBBgwFoAUGLfQGshMNtsO6zSHmnLMeBC2ZcAwDAYDVR0T
BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQAL9VqemuxEQGO3wpIfo+BNjLBZpXsi
n669J7l7vIZeLZOBIaghqGXs0pbxl16upbbFMeYD0zetpcc0J+vLUWorTRRAOSyD
dhIitOrvlbltKtvaGU8vS7Tssl0QWD9uE7ZjSZ+cR4wkvu9yB/JIC+VE0uK7C5uT
qGrjagfQugjW8w==
-----END CERTIFICATE-----
"""
# CA private key, just for reference
#
# -----BEGIN RSA PRIVATE KEY-----
# MIICXgIBAAKBgQDNO105VaGOMGj0DTAob8gEzjYDT5jUh7WLjOLRHLzxsOUxcREP
# kfY57x4+c2fscYzKTGMETcJnjPK1SUbtB5dxf/f8uplSCbWwQDYc/tOW9FGsUYeo
# cVnt079b72J8zkMnHOZ1e0ro9L8ThZvpbA8EfEYqOaTqlxCrkhjjmXqVkQIDAQAB
# AoGAAInjWL8syV6/J8TRF4oTkE+qPJ/82rHwfAlGnx3gMRIxx8twLAZKCyThg3By
# GWDC6dUBfYVmuTbZfDhRA1Y9w4FJGZ0ESVP9tnx77eUuZ4Ai0a81sAkpyJNn2Zlh
# A7DA/oyzm1HenD4Lfqbj8fZh/wYdb9Eg+mXYrqnyl9ViLDECQQD72T2IBzuwhDfN
# BP5OfF0kzyay29ZuvkeE3tEoSpDDH57mcwSP7xz6IQTyZORaEoqaEMsjVEfTUSnS
# NyibMMM/AkEA0J1osrhFkDO8nzl8Sq+DbM01NHFojsiwcfbcTCI8wq1/vCv3pv9Z
# 4zHbZazR+jve7W4H63UPrU2hjmIOYIMDLwJBAIqYmNYdNOoFOTgogVLr+c5h+agA
# d1dme7FRdcU4k8XtxuKHdYFIU6gLN8+1Wj1/aqsyhrggj45pYhx/omcVRL0CQQCj
# IKORNTT4OOyjGYGOqUY82w5irtfS5y3KP/4t7ovSs3bx/vON+5kfZoooLIaZhR2i
# TesVfJlArDbLrvONFoVzAkEAlFO+Jx429C9hg3+Plu7YwJ6W9BQ1x0l97GRHLlC0
# GbNoaMtCG4bKJ70OjHaCFHHiFHxtvFp5zxSALaxbUBm5Kg==
# -----END RSA PRIVATE KEY-----


server_subject = "DC=server,O=txmongo"
server_keycert = """
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDMzYRDPuQYiRrLvpF7QeKyi6djwARUHj6Yl/dZ7dE8Qah084sa
4x4a9UIE2YPg+Jq4Xr/cVCP4wHXq0Ok2tgTExsAX2uOGheU8AihEAAOL33LiBUq0
C1J9xxL5ZS6aFgGvyMAOReaBpce472kQJlCneCGH3P8vVph/1TPWiaWcQQIDAQAB
AoGAVVpdge0HANa7DSi51uWphgG/3EmdRDVqnwvOcXM0nWk7vKn3UlhPJqsKPZ0t
YigZyzbpvPhwGW6Udi1k1IFdUKoct8ciWu6GTwHh96/jIHDQdgGzEPI8eyqccaBA
Iy3t/jf2To/Wr9n8+oEfjHHHrjChU2YoMPR5ZZ/P3n2228ECQQDq9Xl0OdT8vnLD
ZXF3qNo4cL63JkbwyCTokUhhb1Bo7z22N/0Az9QJtrW7Z2hjdIvuTSn18mFtQWFR
kPLyGU25AkEA3ySzt1ou5f8YuZUwrLaVAnNYNp2l9NbCkM89cPFfRTqY7rtU9+t2
hOrLfXhdwBDGFVulABHm8Kg9dhjoqD9GyQJAYK/9d+eojw1sOp5PMDeq/VjgEoxM
2x7xmUbX60icZWI2Gfs2QRRFJG4soN7v5SV7w+e7IbvJfeVOv/sPDrN8+QJAdRl1
lkqlQd1UxE8edAR8vgR5zm98n7fz8rpOq+5+6H2Ps/hq5o+Sar4se3Om/xvOV3b4
Z8j9QF2Jo2f+8AwEwQJAPaAkfH4UJ/dxQ/6xmn5JGJj8w91hAB1vg4M37tPKBoHx
xm9+lxWGOq3vlPWI9U4mzzPZ0IaCc9Vh4kYoeUhQTA==
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIBxTCCAS4CCQDggej4Q/JZPTANBgkqhkiG9w0BAQsFADAkMRAwDgYDVQQKDAd0
eG1vbmdvMRAwDgYDVQQDDAd0ZXN0aW5nMB4XDTE2MTAwMzIwNDIzM1oXDTE5MDYz
MDIwNDIzM1owKjEQMA4GA1UECgwHdHhtb25nbzEWMBQGCgmSJomT8ixkARkWBnNl
cnZlcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzM2EQz7kGIkay76Re0Hi
sounY8AEVB4+mJf3We3RPEGodPOLGuMeGvVCBNmD4PiauF6/3FQj+MB16tDpNrYE
xMbAF9rjhoXlPAIoRAADi99y4gVKtAtSfccS+WUumhYBr8jADkXmgaXHuO9pECZQ
p3ghh9z/L1aYf9Uz1omlnEECAwEAATANBgkqhkiG9w0BAQsFAAOBgQC/0qiQeGpK
0dXsWb8M2UVxqmHDzcwSqBI55USfZ2BbwDVHm0ExMm03r2wrhuXE6Habmzf7nsRB
DT2+MoNAQPiaNOrgIzDvbXOI75vr1B2Id18GG9zA0J30CNyvUV9y/gQ52X4/7GvN
NQddF0MaGGpwCLi+Kc52ibq5yFBVv8n9Ww==
-----END CERTIFICATE-----
"""


client_subject = "DC=client,O=txmongo"
client_cert = """
-----BEGIN CERTIFICATE-----
MIIBxTCCAS4CCQDggej4Q/JZPjANBgkqhkiG9w0BAQsFADAkMRAwDgYDVQQKDAd0
eG1vbmdvMRAwDgYDVQQDDAd0ZXN0aW5nMB4XDTE2MTAwMzIwNDIzM1oXDTE5MDYz
MDIwNDIzM1owKjEQMA4GA1UECgwHdHhtb25nbzEWMBQGCgmSJomT8ixkARkWBmNs
aWVudDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAkiJg+hVMxyfHgbtCv4QM
r/95rol2IpF9GzsJDXRjN4jaf3iwaovS/gfJyaNDGo8aCb30o7hvrUIFHEk1BxK3
ebEIZYuCN3A2Sf/cqx9GRiY/fkWgXQJBE06MzDF/25afwvN0ea54A+bS3se3v59i
cSjxmV5CeX52ABq70YjHwAECAwEAATANBgkqhkiG9w0BAQsFAAOBgQB+zRvS8s6/
QBjSuJ74Wj/nA1aONOX1EgzpyWvYgoUeWWpLKyDb6SmSvgpScVD1n7oZR6TbuPkI
LXTwD5PKgkyRAIjdjR8F1QCkgCLm560l8xwB1p5IXMVflPBzEgS5GzwmQbpJqr16
3Ug8rLMyBWqrF9NRcBztBvp3V1m3po41kg==
-----END CERTIFICATE-----
"""
client_key = """
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQCSImD6FUzHJ8eBu0K/hAyv/3muiXYikX0bOwkNdGM3iNp/eLBq
i9L+B8nJo0MajxoJvfSjuG+tQgUcSTUHErd5sQhli4I3cDZJ/9yrH0ZGJj9+RaBd
AkETTozMMX/blp/C83R5rngD5tLex7e/n2JxKPGZXkJ5fnYAGrvRiMfAAQIDAQAB
AoGAN0Gyo72cG45KHR7+3UYEOiSDEWE+/1E+GibXhHPm9F/WJu8u3grjDFVLkugd
/pPvx5FBSQr7h2r4Xbq8x2DnaRUmzkWbqSOdvKEXER35zXmqF+J0p2nfoaSq2qh4
4rAuUVgn3P9p1dxZqllDBoiuTSMMERwnbaPklvRj5MuDUXECQQDByrJbthPlj1kK
XdANGIUtBTDUg9c5cH5uFr6Lz2ehd/dfyvE9COqIwPDfbwg4sHHHLZ4g3ZCi+fDu
ZYnQOPKFAkEAwQtQUdEB0expSlUriOhueCm6CmkQBYwbDnfRA6ERtsT/0C+8H6t6
Xm++YKQKQ9zRwkK23ChyhKotGdDmtp/2TQJAFqsRJe0scqPL9Ix4s690lImQ5qrt
WAiyoUoDy/Lc2mRgCVKB2XPbi1eWVWx1d7wb8wKBBrMkIgw+hIRYFIU0yQJBAIn2
QPfH7IoPcAwspEla+6ArCgdooIemYqvLW3hBg3xgfAZYJxVnIrQdHizI74EiblJs
BW2ABp/jUwoxLsFzvr0CQQCdvd/scIfqXlf/kxiOwtwoTQDS0MZYNWE1r1E5sAPV
V37FR1u/s35pM/csU6V/hNpOBhrZ4SjxhJy8vAOs9sHA
-----END RSA PRIVATE KEY-----
"""

@staticmethod
def __create_keyfile(content):
tmp = tempfile.NamedTemporaryFile(delete=False)
tmp.write(content.encode())
tmp.close()
return tmp.name

@defer.inlineCallbacks
def setUp(self):
self.dbpath = tempfile.mkdtemp()

self.server_keyfile = self.__create_keyfile(self.server_keycert)
self.ca_certfile = self.__create_keyfile(self.ca_cert)
self.client_keyfile = self.__create_keyfile(self.client_key)
self.client_certfile = self.__create_keyfile(self.client_cert)

self.ssl_factory = ssl.DefaultOpenSSLContextFactory(
privateKeyFileName = self.client_keyfile,
certificateFileName = self.client_certfile,
)

mongod_noauth = Mongod(port=mongo_port, auth=False, dbpath=self.dbpath)
yield mongod_noauth.start()

try:
conn = connection.MongoConnection("localhost", mongo_port)

yield conn["$external"].command("createUser", self.client_subject,
roles=[{"role": "root", "db": "admin"}])
finally:
yield conn.disconnect()
yield mongod_noauth.stop()

self.mongod = Mongod(port=mongo_port, auth=True, dbpath=self.dbpath,
args=["--clusterAuthMode", "x509", "--sslMode", "requireSSL",
"--sslPEMKeyFile", self.server_keyfile,
"--sslCAFile", self.ca_certfile])
try:
yield self.mongod.start()
except:
print(self.mongod.output())
raise


@defer.inlineCallbacks
def tearDown(self):
yield self.mongod.stop()
shutil.rmtree(self.dbpath)
os.unlink(self.server_keyfile)
os.unlink(self.ca_certfile)
os.unlink(self.client_keyfile)
os.unlink(self.client_certfile)

@defer.inlineCallbacks
def test_auth(self):
conn = connection.MongoConnection(port=mongo_port, ssl_context_factory=self.ssl_factory)
yield self.assertFailure(conn.db.coll.find(), OperationFailure)
try:
yield conn.db.authenticate(self.client_subject, '', mechanism="MONGODB-X509")
yield conn.db.coll.insert_one({'x': 42})
cnt = yield conn.db.coll.count()
self.assertEqual(cnt, 1)
finally:
yield conn.disconnect()

@defer.inlineCallbacks
def test_fail(self):
conn = connection.MongoConnection(port=mongo_port, ssl_context_factory=self.ssl_factory)
yield self.assertFailure(conn.db.coll.find(), OperationFailure)
try:
auth = conn.db.authenticate("DC=another,O=txmongo", '', mechanism="MONGODB-X509")
yield self.assertFailure(auth, MongoAuthenticationError)
finally:
yield conn.disconnect()

@defer.inlineCallbacks
def test_lazy_fail(self):
conn = connection.MongoConnection(port=mongo_port, ssl_context_factory=self.ssl_factory)
try:
yield conn.db.authenticate("DC=another,O=txmongo", '', mechanism="MONGODB-X509")
yield self.assertFailure(conn.db.coll.find(), OperationFailure)
finally:
yield conn.disconnect()
12 changes: 12 additions & 0 deletions txmongo/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,16 @@ def authenticate_scram_sha1(self, database_name, username, password):
if not result["done"]:
raise MongoAuthenticationError("TxMongo: SASL conversation failed to complete.")

@defer.inlineCallbacks
def authenticate_mongo_x509(self, database_name, username, password):
query = SON([("authenticate", 1),
("mechanism", "MONGODB-X509"),
("user", username)])
result = yield self.__run_command("$external", query)
if not result["ok"]:
raise MongoAuthenticationError(result["errmsg"])
defer.returnValue(result)

@defer.inlineCallbacks
def authenticate(self, database_name, username, password, mechanism):
database_name = str(database_name)
Expand All @@ -506,6 +516,8 @@ def authenticate(self, database_name, username, password, mechanism):
auth_func = self.authenticate_mongo_cr
elif mechanism == "SCRAM-SHA-1":
auth_func = self.authenticate_scram_sha1
elif mechanism == "MONGODB-X509":
auth_func = self.authenticate_mongo_x509
elif mechanism == "DEFAULT":
if self.max_wire_version >= 3:
auth_func = self.authenticate_scram_sha1
Expand Down

0 comments on commit eee4bb3

Please sign in to comment.