Skip to content

Commit e90263a

Browse files
authored
Merge pull request #3724 from elpaso/auth_tests_2_14
[tests] Authmanager tests for username/pwd and PKI
2 parents 5fed35a + 6fd3f74 commit e90263a

11 files changed

+973
-111
lines changed

tests/src/python/CMakeLists.txt

+3
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,7 @@ ENDIF (WITH_APIDOC)
114114
IF (WITH_SERVER)
115115
ADD_PYTHON_TEST(PyQgsServer test_qgsserver.py)
116116
ADD_PYTHON_TEST(PyQgsServerAccessControl test_qgsserver_accesscontrol.py)
117+
ADD_PYTHON_TEST(PyQgsAuthManagerPasswordOWSTest test_authmanager_password_ows.py)
118+
#ADD_PYTHON_TEST(PyQgsAuthManagerPKIOWSTest test_authmanager_pki_ows.py)
119+
ADD_PYTHON_TEST(PyQgsAuthManagerPKIPostgresTest test_authmanager_pki_postgres.py)
117120
ENDIF (WITH_SERVER)
+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
QGIS Server HTTP wrapper
4+
5+
This script launches a QGIS Server listening on port 8081 or on the port
6+
specified on the environment variable QGIS_SERVER_PORT.
7+
QGIS_SERVER_HOST (defaults to 127.0.0.1)
8+
9+
For testing purposes, HTTP Basic can be enabled by setting the following
10+
environment variables:
11+
12+
* QGIS_SERVER_HTTP_BASIC_AUTH (default not set, set to anything to enable)
13+
* QGIS_SERVER_USERNAME (default ="username")
14+
* QGIS_SERVER_PASSWORD (default ="password")
15+
16+
PKI authentication with HTTPS can be enabled with:
17+
18+
* QGIS_SERVER_PKI_CERTIFICATE (server certificate)
19+
* QGIS_SERVER_PKI_KEY (server private key)
20+
* QGIS_SERVER_PKI_AUTHORITY (root CA)
21+
* QGIS_SERVER_PKI_USERNAME (valid username)
22+
23+
Sample run:
24+
25+
QGIS_SERVER_PKI_USERNAME=Gerardus QGIS_SERVER_PORT=47547 QGIS_SERVER_HOST=localhost \
26+
QGIS_SERVER_PKI_KEY=/home/dev/QGIS/tests/testdata/auth_system/certs_keys/localhost_ssl_key.pem \
27+
QGIS_SERVER_PKI_CERTIFICATE=/home/dev/QGIS/tests/testdata/auth_system/certs_keys/localhost_ssl_cert.pem \
28+
QGIS_SERVER_PKI_AUTHORITY=/home/dev/QGIS/tests/testdata/auth_system/certs_keys/chains_subissuer-issuer-root_issuer2-root2.pem \
29+
python /home/dev/QGIS/tests/src/python/qgis_wrapped_server.py
30+
31+
.. note:: This program is free software; you can redistribute it and/or modify
32+
it under the terms of the GNU General Public License as published by
33+
the Free Software Foundation; either version 2 of the License, or
34+
(at your option) any later version.
35+
"""
36+
from __future__ import print_function
37+
from future import standard_library
38+
standard_library.install_aliases()
39+
40+
__author__ = 'Alessandro Pasotti'
41+
__date__ = '05/15/2016'
42+
__copyright__ = 'Copyright 2016, The QGIS Project'
43+
# This will get replaced with a git SHA1 when you do a git archive
44+
__revision__ = '$Format:%H$'
45+
46+
47+
import os
48+
import sys
49+
import ssl
50+
import urllib.parse
51+
from http.server import BaseHTTPRequestHandler, HTTPServer
52+
from qgis.server import QgsServer, QgsServerFilter
53+
54+
QGIS_SERVER_PORT = int(os.environ.get('QGIS_SERVER_PORT', '8081'))
55+
QGIS_SERVER_HOST = os.environ.get('QGIS_SERVER_HOST', '127.0.0.1')
56+
# PKI authentication
57+
QGIS_SERVER_PKI_CERTIFICATE = os.environ.get('QGIS_SERVER_PKI_CERTIFICATE')
58+
QGIS_SERVER_PKI_KEY = os.environ.get('QGIS_SERVER_PKI_KEY')
59+
QGIS_SERVER_PKI_AUTHORITY = os.environ.get('QGIS_SERVER_PKI_AUTHORITY')
60+
QGIS_SERVER_PKI_USERNAME = os.environ.get('QGIS_SERVER_PKI_USERNAME')
61+
62+
# Check if PKI - https is enabled
63+
https = (QGIS_SERVER_PKI_CERTIFICATE is not None and
64+
os.path.isfile(QGIS_SERVER_PKI_CERTIFICATE) and
65+
QGIS_SERVER_PKI_KEY is not None and
66+
os.path.isfile(QGIS_SERVER_PKI_KEY) and
67+
QGIS_SERVER_PKI_AUTHORITY is not None and
68+
os.path.isfile(QGIS_SERVER_PKI_AUTHORITY) and
69+
QGIS_SERVER_PKI_USERNAME)
70+
71+
qgs_server = QgsServer()
72+
73+
if os.environ.get('QGIS_SERVER_HTTP_BASIC_AUTH') is not None:
74+
import base64
75+
76+
class HTTPBasicFilter(QgsServerFilter):
77+
78+
def responseComplete(self):
79+
request = self.serverInterface().requestHandler()
80+
if self.serverInterface().getEnv('HTTP_AUTHORIZATION'):
81+
username, password = base64.b64decode(self.serverInterface().getEnv('HTTP_AUTHORIZATION')[6:]).split(':')
82+
if (username == os.environ.get('QGIS_SERVER_USERNAME', 'username')
83+
and password == os.environ.get('QGIS_SERVER_PASSWORD', 'password')):
84+
return
85+
# No auth ...
86+
request.clearHeaders()
87+
request.setHeader('Status', '401 Authorization required')
88+
request.setHeader('WWW-Authenticate', 'Basic realm="QGIS Server"')
89+
request.clearBody()
90+
request.appendBody('<h1>Authorization required</h1>')
91+
92+
filter = HTTPBasicFilter(qgs_server.serverInterface())
93+
qgs_server.serverInterface().registerFilter(filter)
94+
95+
96+
class Handler(BaseHTTPRequestHandler):
97+
98+
def do_GET(self):
99+
# For PKI: check the username from client certificate
100+
if https:
101+
try:
102+
ssl.match_hostname(self.connection.getpeercert(), QGIS_SERVER_PKI_USERNAME)
103+
except Exception as ex:
104+
print("SSL Exception %s" % ex)
105+
self.send_response(401)
106+
self.end_headers()
107+
self.wfile.write('UNAUTHORIZED')
108+
return
109+
# CGI vars:
110+
for k, v in self.headers.items():
111+
# Uncomment to print debug info about env vars passed into QGIS Server env
112+
#print('Setting ENV var %s to %s' % ('HTTP_%s' % k.replace(' ', '-').replace('-', '_').replace(' ', '-').upper(), v))
113+
qgs_server.putenv('HTTP_%s' % k.replace(' ', '-').replace('-', '_').replace(' ', '-').upper(), v)
114+
qgs_server.putenv('SERVER_PORT', str(self.server.server_port))
115+
qgs_server.putenv('SERVER_NAME', self.server.server_name)
116+
qgs_server.putenv('REQUEST_URI', self.path)
117+
parsed_path = urllib.parse.urlparse(self.path)
118+
headers, body = qgs_server.handleRequest(parsed_path.query)
119+
headers_dict = dict(h.split(': ', 1) for h in headers.decode().split('\n') if h)
120+
try:
121+
self.send_response(int(headers_dict['Status'].split(' ')[0]))
122+
except:
123+
self.send_response(200)
124+
for k, v in headers_dict.items():
125+
self.send_header(k, v)
126+
self.end_headers()
127+
self.wfile.write(body)
128+
return
129+
130+
def do_POST(self):
131+
content_len = int(self.headers.get('content-length', 0))
132+
post_body = self.rfile.read(content_len).decode()
133+
request = post_body[1:post_body.find(' ')]
134+
self.path = self.path + '&REQUEST_BODY=' + \
135+
post_body.replace('&amp;', '') + '&REQUEST=' + request
136+
return self.do_GET()
137+
138+
139+
if __name__ == '__main__':
140+
server = HTTPServer((QGIS_SERVER_HOST, QGIS_SERVER_PORT), Handler)
141+
if https:
142+
server.socket = ssl.wrap_socket(server.socket,
143+
certfile=QGIS_SERVER_PKI_CERTIFICATE,
144+
keyfile=QGIS_SERVER_PKI_KEY,
145+
ca_certs=QGIS_SERVER_PKI_AUTHORITY,
146+
cert_reqs=ssl.CERT_REQUIRED,
147+
server_side=True,
148+
ssl_version=ssl.PROTOCOL_TLSv1)
149+
message = 'Starting server on %s://%s:%s, use <Ctrl-C> to stop' % \
150+
('https' if https else 'http', QGIS_SERVER_HOST, server.server_port)
151+
try:
152+
print(message, flush=True)
153+
except:
154+
print(message)
155+
sys.stdout.flush()
156+
server.serve_forever()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Tests for auth manager WMS/WFS using QGIS Server through HTTP Basic
4+
enabled qgis_wrapped_server.py.
5+
6+
This is an integration test for QGIS Desktop Auth Manager WFS and WMS provider
7+
and QGIS Server WFS/WMS that check if QGIS can use a stored auth manager auth
8+
configuration to access an HTTP Basic protected endpoint.
9+
10+
11+
From build dir, run: ctest -R PyQgsAuthManagerPasswordOWSTest -V
12+
13+
.. note:: This program is free software; you can redistribute it and/or modify
14+
it under the terms of the GNU General Public License as published by
15+
the Free Software Foundation; either version 2 of the License, or
16+
(at your option) any later version.
17+
"""
18+
import os
19+
import sys
20+
import re
21+
import subprocess
22+
import tempfile
23+
import random
24+
import string
25+
try:
26+
from urllib.parse import quote
27+
except:
28+
from urllib import quote
29+
30+
__author__ = 'Alessandro Pasotti'
31+
__date__ = '18/09/2016'
32+
__copyright__ = 'Copyright 2016, The QGIS Project'
33+
# This will get replaced with a git SHA1 when you do a git archive
34+
__revision__ = '$Format:%H$'
35+
36+
from shutil import rmtree
37+
38+
from utilities import unitTestDataPath, waitServer
39+
from qgis.core import (
40+
QgsAuthManager,
41+
QgsAuthMethodConfig,
42+
QgsVectorLayer,
43+
QgsRasterLayer,
44+
)
45+
from qgis.testing import (
46+
start_app,
47+
unittest,
48+
)
49+
50+
try:
51+
QGIS_SERVER_ENDPOINT_PORT = os.environ['QGIS_SERVER_ENDPOINT_PORT']
52+
except:
53+
QGIS_SERVER_ENDPOINT_PORT = '0' # Auto
54+
55+
56+
QGIS_AUTH_DB_DIR_PATH = tempfile.mkdtemp()
57+
58+
os.environ['QGIS_AUTH_DB_DIR_PATH'] = QGIS_AUTH_DB_DIR_PATH
59+
60+
qgis_app = start_app()
61+
62+
63+
class TestAuthManager(unittest.TestCase):
64+
65+
@classmethod
66+
def setUpClass(cls):
67+
"""Run before all tests:
68+
Creates an auth configuration"""
69+
cls.port = QGIS_SERVER_ENDPOINT_PORT
70+
# Clean env just to be sure
71+
env_vars = ['QUERY_STRING', 'QGIS_PROJECT_FILE']
72+
for ev in env_vars:
73+
try:
74+
del os.environ[ev]
75+
except KeyError:
76+
pass
77+
cls.testdata_path = unitTestDataPath('qgis_server') + '/'
78+
cls.project_path = cls.testdata_path + "test_project.qgs"
79+
# Enable auth
80+
#os.environ['QGIS_AUTH_PASSWORD_FILE'] = QGIS_AUTH_PASSWORD_FILE
81+
authm = QgsAuthManager.instance()
82+
assert (authm.setMasterPassword('masterpassword', True))
83+
cls.auth_config = QgsAuthMethodConfig('Basic')
84+
cls.auth_config.setName('test_auth_config')
85+
cls.username = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(6))
86+
cls.password = cls.username[::-1] # reversed
87+
cls.auth_config.setConfig('username', cls.username)
88+
cls.auth_config.setConfig('password', cls.password)
89+
assert (authm.storeAuthenticationConfig(cls.auth_config)[0])
90+
cls.hostname = '127.0.0.1'
91+
cls.protocol = 'http'
92+
93+
os.environ['QGIS_SERVER_HTTP_BASIC_AUTH'] = '1'
94+
os.environ['QGIS_SERVER_USERNAME'] = cls.username
95+
os.environ['QGIS_SERVER_PASSWORD'] = cls.password
96+
os.environ['QGIS_SERVER_PORT'] = str(cls.port)
97+
os.environ['QGIS_SERVER_HOST'] = cls.hostname
98+
server_path = os.path.dirname(os.path.realpath(__file__)) + \
99+
'/qgis_wrapped_server.py'
100+
cls.server = subprocess.Popen([sys.executable, server_path],
101+
env=os.environ, stdout=subprocess.PIPE)
102+
103+
line = cls.server.stdout.readline()
104+
cls.port = int(re.findall(b':(\d+)', line)[0])
105+
assert cls.port != 0
106+
# Wait for the server process to start
107+
assert waitServer('%s://%s:%s' % (cls.protocol, cls.hostname, cls.port)), "Server is not responding! '%s://%s:%s" % (cls.protocol, cls.hostname, cls.port)
108+
109+
@classmethod
110+
def tearDownClass(cls):
111+
"""Run after all tests"""
112+
cls.server.terminate()
113+
rmtree(QGIS_AUTH_DB_DIR_PATH)
114+
del cls.server
115+
116+
def setUp(self):
117+
"""Run before each test."""
118+
pass
119+
120+
def tearDown(self):
121+
"""Run after each test."""
122+
pass
123+
124+
@classmethod
125+
def _getWFSLayer(cls, type_name, layer_name=None, authcfg=None):
126+
"""
127+
WFS layer factory
128+
"""
129+
if layer_name is None:
130+
layer_name = 'wfs_' + type_name
131+
parms = {
132+
'srsname': 'EPSG:4326',
133+
'typename': type_name.decode('utf-8').replace(' ', '_'),
134+
'url': '%s://%s:%s/?map=%s' % (cls.protocol, cls.hostname, cls.port, cls.project_path),
135+
'version': '1.0.0',
136+
'table': '',
137+
}
138+
uri = u'%(url)s&SERVICE=WFS&VERSION=%(version)s&REQUEST=GetFeature&TYPENAME=%(typename)s&SRSNAME=%(srsname)s' % parms
139+
if authcfg is not None:
140+
uri = uri + "&authcfg=%s" % authcfg
141+
wfs_layer = QgsVectorLayer(uri, layer_name, 'WFS')
142+
return wfs_layer
143+
144+
@classmethod
145+
def _getWMSLayer(cls, layers, layer_name=None, authcfg=None):
146+
"""
147+
WMS layer factory
148+
"""
149+
if layer_name is None:
150+
layer_name = 'wms_' + layers.replace(',', '')
151+
parms = {
152+
'crs': 'EPSG:4326',
153+
'url': '%s://%s:%s/?map=%s' % (cls.protocol, cls.hostname, cls.port, cls.project_path),
154+
'format': 'image/png',
155+
'layers': quote(layers),
156+
'styles': '',
157+
'version': 'auto',
158+
#'sql': '',
159+
}
160+
if authcfg is not None:
161+
parms.update({'authcfg': authcfg})
162+
uri = '&'.join([("%s=%s" % (k, v.replace('=', '%3D'))) for k, v in list(parms.items())])
163+
wms_layer = QgsRasterLayer(uri, layer_name, 'wms')
164+
return wms_layer
165+
166+
def testValidAuthAccess(self):
167+
"""
168+
Access the HTTP Basic protected layer with valid credentials
169+
"""
170+
wfs_layer = self._getWFSLayer('testlayer èé', authcfg=self.auth_config.id())
171+
self.assertTrue(wfs_layer.isValid())
172+
wms_layer = self._getWMSLayer('testlayer èé', authcfg=self.auth_config.id())
173+
self.assertTrue(wms_layer.isValid())
174+
175+
def testInvalidAuthAccess(self):
176+
"""
177+
Access the HTTP Basic protected layer with no credentials
178+
"""
179+
wfs_layer = self._getWFSLayer('testlayer èé')
180+
self.assertFalse(wfs_layer.isValid())
181+
wms_layer = self._getWMSLayer('testlayer èé')
182+
self.assertFalse(wms_layer.isValid())
183+
184+
185+
if __name__ == '__main__':
186+
unittest.main()

0 commit comments

Comments
 (0)