Skip to content

Commit

Permalink
Merge pull request #30076 from m-kuhn/ssl_certificate_check
Browse files Browse the repository at this point in the history
Only check server SSL certificate if requested
  • Loading branch information
m-kuhn committed Jun 6, 2019
2 parents 800cef2 + 936c330 commit 6b8aa01
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 150 deletions.
8 changes: 7 additions & 1 deletion .ci/travis/linux/docker-compose.travis.yml
@@ -1,9 +1,15 @@
version: '3'
services:
postgres:
image: kartoza/postgis:9.5-2.2
build:
dockerfile: Dockerfile-postgis
context: ../../../tests/testdata
environment:
- ALLOW_IP_RANGE="172.18.0.0/16"
# The following files are added in Dockerfile-postgis
- SSL_CERT_FILE=/etc/ssl/certs/postgres_cert.crt
- SSL_KEY_FILE=/etc/ssl/private/postgres_key.key
- SSL_CA_FILE=/etc/ssl/certs/issuer_ca_cert.pem

mssql:
image: microsoft/mssql-server-linux:2017-latest
Expand Down
1 change: 0 additions & 1 deletion .ci/travis/linux/scripts/test_blacklist.txt
Expand Up @@ -12,7 +12,6 @@ qgis_sip_uptodate

# Need a local postgres installation
PyQgsAuthManagerPKIPostgresTest
PyQgsAuthManagerPasswordPostgresTest
PyQgsAuthManagerOgrPostgresTest
PyQgsDbManagerPostgis

Expand Down
35 changes: 27 additions & 8 deletions src/auth/basic/qgsauthbasicmethod.cpp
Expand Up @@ -105,18 +105,36 @@ bool QgsAuthBasicMethod::updateDataSourceUriItems( QStringList &connectionItems,
return false;
}

QString sslMode = QStringLiteral( "prefer" );
int sslModeIdx = connectionItems.indexOf( QRegExp( "^sslmode=.*" ) );
if ( sslModeIdx != -1 )
{
sslMode = connectionItems.at( sslModeIdx ).split( '=' ).at( 1 );
}

// SSL Extra CAs
QString caparam;
QList<QSslCertificate> cas;
cas = QgsApplication::authManager()->trustedCaCerts();
// save CAs to temp file
QString tempFileBase = QStringLiteral( "tmp_basic_%1.pem" );
QString caFilePath = QgsAuthCertUtils::pemTextToTempFile(
tempFileBase.arg( QUuid::createUuid().toString() ),
QgsAuthCertUtils::certsToPemText( cas ) );
if ( ! caFilePath.isEmpty() )
if ( sslMode.startsWith( QStringLiteral( "verify-" ) ) )
{
caparam = "sslrootcert='" + caFilePath + "'";
cas = QgsApplication::authManager()->trustedCaCerts();
// save CAs to temp file
QString tempFileBase = QStringLiteral( "tmp_basic_%1.pem" );
QString caFilePath = QgsAuthCertUtils::pemTextToTempFile(
tempFileBase.arg( QUuid::createUuid().toString() ),
QgsAuthCertUtils::certsToPemText( cas ) );
if ( ! caFilePath.isEmpty() )
{
caparam = "sslrootcert='" + caFilePath + "'";
}
QFile f( caFilePath );
if ( !f.open( QFile::ReadOnly | QFile::Text ) )
{
qWarning() << "Could not open ca cert file!!";
}
QTextStream in( &f );
qWarning() << caparam;
qWarning() << f.size() << in.readAll();
}

// Branch for OGR
Expand Down Expand Up @@ -272,6 +290,7 @@ bool QgsAuthBasicMethod::updateDataSourceUriItems( QStringList &connectionItems,
else
{
connectionItems.append( caparam );
qWarning() << QStringLiteral( "Connection items after appending %1" ).arg( connectionItems.join( "&" ) );
}
}
}
Expand Down
213 changes: 73 additions & 140 deletions tests/src/python/test_authmanager_password_postgres.py
Expand Up @@ -6,18 +6,13 @@
checks if QGIS can use a stored auth manager auth configuration to access
a Password protected postgres.
Configuration from the environment:
It uses a docker container as postgres/postgis server with certificates from tests/testdata/auth_system/certs_keys
* QGIS_POSTGRES_SERVER_PORT (default: 55432)
* QGIS_POSTGRES_EXECUTABLE_PATH (default: /usr/lib/postgresql/9.4/bin)
Use docker-compose -f .ci/travis/linux/docker-compose.travis.yml up postgres to start the server.
From build dir, run: ctest -R PyQgsAuthManagerPasswordPostgresTest -V
or, if your PostgreSQL path differs from the default:
QGIS_POSTGRES_EXECUTABLE_PATH=/usr/lib/postgresql/<your_version_goes_here>/bin \
ctest -R PyQgsAuthManagerPasswordPostgresTest -V
TODO:
- Document how to restore the server data
- Document how to use docker inspect to find the IP of the docker postgres server and set a host alias (or some other smart idea to do the same)
.. note:: This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
Expand All @@ -32,6 +27,7 @@
import tempfile

from shutil import rmtree
from contextlib import contextmanager

from utilities import unitTestDataPath
from qgis.core import (
Expand All @@ -55,142 +51,59 @@
__date__ = '25/10/2016'
__copyright__ = 'Copyright 2016, The QGIS Project'

QGIS_POSTGRES_SERVER_PORT = os.environ.get('QGIS_POSTGRES_SERVER_PORT', '55432')
QGIS_POSTGRES_EXECUTABLE_PATH = os.environ.get('QGIS_POSTGRES_EXECUTABLE_PATH', '/usr/lib/postgresql/9.4/bin')

assert os.path.exists(QGIS_POSTGRES_EXECUTABLE_PATH)

QGIS_AUTH_DB_DIR_PATH = tempfile.mkdtemp()

# Postgres test path
QGIS_PG_TEST_PATH = tempfile.mkdtemp()

os.environ['QGIS_AUTH_DB_DIR_PATH'] = QGIS_AUTH_DB_DIR_PATH

qgis_app = start_app()

QGIS_POSTGRES_CONF_TEMPLATE = """
hba_file = '%(tempfolder)s/pg_hba.conf'
listen_addresses = '*'
port = %(port)s
max_connections = 100
unix_socket_directories = '%(tempfolder)s'
ssl = true
ssl_ciphers = 'DEFAULT:!LOW:!EXP:!MD5:@STRENGTH' # allowed SSL ciphers
ssl_cert_file = '%(server_cert)s'
ssl_key_file = '%(server_key)s'
ssl_ca_file = '%(sslrootcert_path)s'
password_encryption = on
"""

QGIS_POSTGRES_HBA_TEMPLATE = """
hostssl all all 0.0.0.0/0 md5
hostssl all all ::1/0 md5
host all all 127.0.0.1/32 trust
host all all ::1/32 trust
"""


class TestAuthManager(unittest.TestCase):

@classmethod
def setUpAuth(cls):
"""Run before all tests and set up authentication"""
authm = QgsApplication.authManager()
assert (authm.setMasterPassword('masterpassword', True))
cls.pg_conf = os.path.join(cls.tempfolder, 'postgresql.conf')
cls.pg_hba = os.path.join(cls.tempfolder, 'pg_hba.conf')
# Client side
cls.sslrootcert_path = os.path.join(cls.certsdata_path, 'chains_subissuer-issuer-root_issuer2-root2.pem')
assert os.path.isfile(cls.sslrootcert_path)
os.chmod(cls.sslrootcert_path, stat.S_IRUSR)
cls.auth_config = QgsAuthMethodConfig("Basic")
cls.auth_config.setConfig('username', cls.username)
cls.auth_config.setConfig('password', cls.password)
cls.auth_config.setName('test_password_auth_config')
cls.sslrootcert = QSslCertificate.fromPath(cls.sslrootcert_path)
assert cls.sslrootcert is not None
authm.storeCertAuthorities(cls.sslrootcert)
@contextmanager
def ScopedCertAuthority(username, password, sslrootcert_path=None):
"""
Sets up the certificate authority in the authentication manager
for the lifetime of this class and removes it when the class is deleted.
"""
authm = QgsApplication.authManager()
auth_config = QgsAuthMethodConfig("Basic")
auth_config.setConfig('username', username)
auth_config.setConfig('password', password)
auth_config.setName('test_password_auth_config')
if sslrootcert_path:
sslrootcert = QSslCertificate.fromPath(sslrootcert_path)
assert sslrootcert is not None
authm.storeCertAuthorities(sslrootcert)
authm.rebuildCaCertsCache()
authm.rebuildTrustedCaCertsCache()
authm.rebuildCertTrustCache()
assert (authm.storeAuthenticationConfig(cls.auth_config)[0])
assert cls.auth_config.isValid()

# Server side
cls.server_cert = os.path.join(cls.certsdata_path, 'localhost_ssl_cert.pem')
cls.server_key = os.path.join(cls.certsdata_path, 'localhost_ssl_key.pem')
cls.server_rootcert = cls.sslrootcert_path
os.chmod(cls.server_cert, stat.S_IRUSR)
os.chmod(cls.server_key, stat.S_IRUSR)
os.chmod(cls.server_rootcert, stat.S_IRUSR)

# Place conf in the data folder
with open(cls.pg_conf, 'w+') as f:
f.write(QGIS_POSTGRES_CONF_TEMPLATE % {
'port': cls.port,
'tempfolder': cls.tempfolder,
'server_cert': cls.server_cert,
'server_key': cls.server_key,
'sslrootcert_path': cls.sslrootcert_path,
})

with open(cls.pg_hba, 'w+') as f:
f.write(QGIS_POSTGRES_HBA_TEMPLATE)
assert (authm.storeAuthenticationConfig(auth_config)[0])
assert auth_config.isValid()
yield auth_config
if sslrootcert_path:
for cert in sslrootcert:
authm.removeCertAuthority(cert)
authm.rebuildCaCertsCache()
authm.rebuildTrustedCaCertsCache()
authm.rebuildCertTrustCache()


class TestAuthManager(unittest.TestCase):

@classmethod
def setUpClass(cls):
"""Run before all tests:
Creates an auth configuration"""
cls.port = QGIS_POSTGRES_SERVER_PORT
cls.username = 'username'
cls.password = 'password'
cls.dbname = 'test_password'
cls.tempfolder = QGIS_PG_TEST_PATH
cls.username = 'docker'
cls.password = 'docker'
cls.dbname = 'qgis_test'
cls.hostname = 'postgres'
cls.port = '5432'

authm = QgsApplication.authManager()
assert (authm.setMasterPassword('masterpassword', True))
cls.certsdata_path = os.path.join(unitTestDataPath('auth_system'), 'certs_keys')
cls.hostname = 'localhost'
cls.data_path = os.path.join(cls.tempfolder, 'data')
os.mkdir(cls.data_path)

# Disable SSL verification for setup operations
env = dict(os.environ)
env['PGSSLMODE'] = 'disable'

cls.setUpAuth()
subprocess.check_call([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'initdb'), '-D', cls.data_path])

cls.server = subprocess.Popen([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'postgres'), '-D',
cls.data_path, '-c',
"config_file=%s" % cls.pg_conf],
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
# Wait max 10 secs for the server to start
end = time.time() + 10
while True:
line = cls.server.stderr.readline()
print(line)
if line.find(b"database system is ready to accept") != -1:
break
if time.time() > end:
raise Exception("Timeout connecting to PostgreSQL")
# Create a DB
subprocess.check_call([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'createdb'), '-h', 'localhost', '-p', cls.port, 'test_password'], env=env)
# Inject test SQL from test path
test_sql = os.path.join(unitTestDataPath('provider'), 'testdata_pg.sql')
subprocess.check_call([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'psql'), '-h', 'localhost', '-p', cls.port, '-f', test_sql, cls.dbname], env=env)
# Create a role
subprocess.check_call([os.path.join(QGIS_POSTGRES_EXECUTABLE_PATH, 'psql'), '-h', 'localhost', '-p', cls.port, '-c', 'CREATE ROLE "%s" WITH SUPERUSER LOGIN PASSWORD \'%s\'' % (cls.username, cls.password), cls.dbname], env=env)
cls.sslrootcert_path = os.path.join(cls.certsdata_path, 'chains_subissuer-issuer-root_issuer2-root2.pem')

@classmethod
def tearDownClass(cls):
"""Run after all tests"""
cls.server.terminate()
os.kill(cls.server.pid, signal.SIGABRT)
del cls.server
time.sleep(2)
rmtree(QGIS_AUTH_DB_DIR_PATH)
rmtree(cls.tempfolder)
def printMessage(tag, msg, level):
with open('/tmp/fmt.log', 'a') as f:
f.write('{}: {}'.format(tag, msg))
QgsApplication.instance().messageLog().messageReceived.connect(printMessage)

def setUp(self):
"""Run before each test."""
Expand All @@ -201,15 +114,15 @@ def tearDown(self):
pass

@classmethod
def _getPostGISLayer(cls, type_name, layer_name=None, authcfg=None):
def _getPostGISLayer(cls, type_name, layer_name=None, authcfg=None, sslmode=QgsDataSourceUri.SslVerifyFull):
"""
PG layer factory
"""
if layer_name is None:
layer_name = 'pg_' + type_name
uri = QgsDataSourceUri()
uri.setWkbType(QgsWkbTypes.Point)
uri.setConnection("localhost", cls.port, cls.dbname, "", "", QgsDataSourceUri.SslVerifyFull, authcfg)
uri.setConnection(cls.hostname, cls.port, cls.dbname, "", "", sslmode, authcfg)
uri.setKeyColumn('pk')
uri.setSrid('EPSG:4326')
uri.setDataSource('qgis_test', 'someData', "geom", "", "pk")
Expand All @@ -221,15 +134,35 @@ def testValidAuthAccess(self):
"""
Access the protected layer with valid credentials
"""
pg_layer = self._getPostGISLayer('testlayer_èé', authcfg=self.auth_config.id())
self.assertTrue(pg_layer.isValid())
with ScopedCertAuthority(self.username, self.password, self.sslrootcert_path) as auth_config:
pg_layer = self._getPostGISLayer('testlayer_èé', authcfg=auth_config.id())
self.assertTrue(pg_layer.isValid())

def testInvalidAuthAccess(self):
"""
Access the protected layer with not valid credentials
Access the protected layer with invalid credentials
"""
with ScopedCertAuthority(self.username, self.password, self.sslrootcert_path) as auth_config:
pg_layer = self._getPostGISLayer('testlayer_èé')
self.assertFalse(pg_layer.isValid())

def testSslRequireNoCaCheck(self):
"""
Access the protected layer with valid credentials and ssl require but without the required cert authority.
This should work.
"""
with ScopedCertAuthority(self.username, self.password) as auth_config:
pg_layer = self._getPostGISLayer('testlayer_èé', authcfg=auth_config.id(), sslmode=QgsDataSourceUri.SslRequire)
self.assertTrue(pg_layer.isValid())

def testSslVerifyFullCaCheck(self):
"""
Access the protected layer with valid credentials and ssl verify full but without the required cert authority.
This should not work.
"""
pg_layer = self._getPostGISLayer('testlayer_èé')
self.assertFalse(pg_layer.isValid())
with ScopedCertAuthority(self.username, self.password) as auth_config:
pg_layer = self._getPostGISLayer('testlayer_èé', authcfg=auth_config.id())
self.assertFalse(pg_layer.isValid())


if __name__ == '__main__':
Expand Down
9 changes: 9 additions & 0 deletions tests/testdata/Dockerfile-postgis
@@ -0,0 +1,9 @@
FROM kartoza/postgis:11.0-2.5

ADD auth_system/certs_keys/postgres.crt /etc/ssl/certs/postgres_cert.crt
ADD auth_system/certs_keys/postgres.key /etc/ssl/private/postgres_key.key
ADD auth_system/certs_keys/issuer_ca_cert.pem /etc/ssl/certs/issuer_ca_cert.pem

RUN chmod 400 /etc/ssl/private/postgres_key.key

ADD temp/setup-ssl.sh /
18 changes: 18 additions & 0 deletions tests/testdata/auth_system/certs_keys/postgres.crt
@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIC3zCCAkigAwIBAgIUIw1V6Ryvz+3F/eYzlydwzjmTsNYwDQYJKoZIhvcNAQEL
BQAwgacxCzAJBgNVBAYTAlVTMQ8wDQYDVQQIEwZBbGFza2ExEjAQBgNVBAcTCUFu
Y2hvcmFnZTEVMBMGA1UEChMMUUdJUyBUZXN0IENBMR4wHAYDVQQLExVDZXJ0aWZp
Y2F0ZSBBdXRob3JpdHkxGjAYBgNVBAMTEVFHSVMgVGVzdCBSb290IENBMSAwHgYJ
KoZIhvcNAQkBFhF0ZXN0Y2VydEBxZ2lzLm9yZzAeFw0xOTA2MDUyMDQyNTRaFw0y
MDA2MDQyMDQyNTRaMBMxETAPBgNVBAMMCHBvc3RncmVzMIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEAnBscESb+Xr5zVnC+lhQBTN++LkWFHRNqqJ2jk9K7
XavoOhKLE925XvQGxIJEPgYYlp0sxDvmfeHqsH86EpLyvhsGCYZ3/UoFKeRHotvW
e4sHQDyZSt6P/OwVgGl/fy24fWoKtL95MsDMUGwO6XftXTcNYO9fSlLLUOa0iovu
bjPJJupRHjBjouelRFOGAT+UkcN1j8VLxVVZUlrnwThxtIcqnMiGHTbGXq//Tbjc
amrnUhLTO33jJ3UKlMxG29eYpqKv9p0nv/xJHXpOpHW+OErYkI3TmALwu0JEnEwP
+0jkEP+H19/1OPqMzAkO1J684W/wTzfVdkIokM3oRqdkowIDAQABoxcwFTATBgNV
HREEDDAKgghwb3N0Z3JlczANBgkqhkiG9w0BAQsFAAOBgQB2fNbyVLGk9WHcKVb7
8buV81ILFo18KdoQFD4kzrr896YjvaD90o5OiYeF0EAHZdyKIA0Bzdl/8dXKG6/7
udGW81XxNU/1sI5H20sm1gDvj9BQ6UxT/oOalbJKolEhgtU2KA1Hdh2hxrrQ9Zqu
hB/ykiE7sCbaeeQl1iEZxgnyDA==
-----END CERTIFICATE-----

0 comments on commit 6b8aa01

Please sign in to comment.