Skip to content

Commit 1ac56ba

Browse files
committed
Allow forcing basic auth challenge via REQUESTAUTH param
1 parent 975dab4 commit 1ac56ba

File tree

4 files changed

+34
-20
lines changed

4 files changed

+34
-20
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,11 @@ Example:
241241
},
242242
```
243243

244+
To force the `qwc-ogc-service` to return a `401 Unauthorized` response if not authenticated, pass `REQUIREAUTH=1` to the `WMS` or `WFS` request args, example:
245+
```
246+
http://qwc-ogc-service/<service>?SERVICE=WMS&REQUEST=GetCapabilities&REQUIREAUTH=1
247+
```
248+
244249
### Marker params
245250

246251
The OGC service supports specifying marker parameters to insert a SLD styled marker into GetMap requests via QGIS Server `HIGHLIGHT_SYMBOL` and `HIGHLIGHT_GEOM`. To use this feature, provide a SLD template and parameter definitions in the ogc service config, for example:

src/ogc_service.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from xml.etree import ElementTree
66
from xml.sax.saxutils import escape as xml_escape
77

8-
from flask import abort, Response, stream_with_context, url_for, current_app
8+
from flask import abort, Response, stream_with_context, url_for, current_app, make_response
99
import requests
1010

1111
from qwc_services_core.permissions_reader import PermissionsReader
@@ -84,6 +84,14 @@ def request(self, identity, method, service_name, params, data):
8484
# normalize parameter keys to upper case
8585
params = {k.upper(): v for k, v in params.items()}
8686

87+
# Check if basic auth challenge should be sent
88+
require_auth = params.get('REQUIREAUTH', '').lower() in ["1", "true"]
89+
if not identity and require_auth:
90+
# Return WWW-Authenticate header, e.g. for browser password prompt
91+
response = make_response("Unauthorized", 401)
92+
response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"'
93+
return response
94+
8795
# Inject identity parameter if configured
8896
if self.qgis_server_identity_parameter is not None:
8997
parameter_name = self.qgis_server_identity_parameter.upper()

src/server.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,6 @@ def get_identity_or_auth(ogc_service):
7070
json_resp = json.loads(resp.text)
7171
app.logger.debug(json_resp)
7272
return json_resp.get('identity')
73-
# Return WWW-Authenticate header, e.g. for browser password prompt
74-
# raise Unauthorized(
75-
# www_authenticate='Basic realm="Login Required"')
7673
return identity
7774

7875

@@ -124,6 +121,7 @@ class OGC(Resource):
124121
@api.param('REQUEST', 'Request', default='GetCapabilities')
125122
@api.param('VERSION', 'Version', default='1.1.1')
126123
@api.param('filename', 'Output file name')
124+
@api.param('REQUIREAUTH', 'Whether to send a 401 Unauthorized response if not authenticated')
127125
@optional_auth
128126
def get(self, service_name):
129127
"""OGC service request
@@ -136,7 +134,7 @@ def get(self, service_name):
136134
response = ogc_service.request(
137135
identity, 'GET', service_name, request.args, None)
138136

139-
filename = request.values.get('filename')
137+
filename = request.args.get('filename')
140138
if filename:
141139
response.headers['content-disposition'] = 'attachment; filename=' + filename
142140

@@ -146,6 +144,7 @@ def get(self, service_name):
146144
@api.param('SERVICE', 'Service', _in='formData', default='WMS')
147145
@api.param('REQUEST', 'Request', _in='formData', default='GetCapabilities')
148146
@api.param('VERSION', 'Version', _in='formData', default='1.1.1')
147+
@api.param('REQUIREAUTH', 'Whether to send a 401 Unauthorized response if not authenticated')
149148
@api.param('filename', 'Output file name')
150149
@optional_auth
151150
def post(self, service_name):

src/wms_handler.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def filter_response(self, request, response, params, permissions):
167167
:param obj permissions: OGC service permission
168168
"""
169169
if request in ['GETCAPABILITIES', 'GETPROJECTSETTINGS']:
170-
return self.__filter_getcapabilities(response, permissions)
170+
return self.__filter_getcapabilities(response, permissions, params)
171171
elif request == 'GETFEATUREINFO':
172172
return self.__filter_getfeatureinfo(response, permissions)
173173
else:
@@ -267,12 +267,13 @@ def __rewrite_external_wms_url(self, layer, params):
267267
lambda name: name in permitted_layers, ext_layers
268268
)))
269269

270-
def __filter_getcapabilities(self, response, permissions):
270+
def __filter_getcapabilities(self, response, permissions, params):
271271
"""Return WMS GetCapabilities or GetProjectSettings filtered by
272272
permissions.
273273
274274
:param requests.Response response: Response object
275275
:param obj permissions: OGC service permissions
276+
:param obj params: Request parameters
276277
"""
277278
xml = response.text
278279
# Strip control characters
@@ -318,15 +319,15 @@ def __filter_getcapabilities(self, response, permissions):
318319
online_resources += root.findall('.//%sGetPrint//%sOnlineResource' % (np, np), ns)
319320
online_resources += root.findall('.//%sLegendURL//%sOnlineResource' % (np, np), ns)
320321

321-
self.__update_online_resources(online_resources, service_url, xlinkns)
322+
self.__update_online_resources(online_resources, service_url, xlinkns, params)
322323

323324
info_url = permissions['online_resources'].get('feature_info')
324325
if info_url:
325326
# override GetFeatureInfo OnlineResources
326327
online_resources = root.findall(
327328
'.//%sGetFeatureInfo//%sOnlineResource' % (np, np), ns
328329
)
329-
self.__update_online_resources(online_resources, info_url, xlinkns)
330+
self.__update_online_resources(online_resources, info_url, xlinkns, params)
330331

331332
legend_url = permissions['online_resources'].get('legend')
332333
if legend_url:
@@ -338,7 +339,7 @@ def __filter_getcapabilities(self, response, permissions):
338339
'.//{%s}GetLegendGraphic//%sOnlineResource' % (sldns, np),
339340
ns
340341
)
341-
self.__update_online_resources(online_resources, legend_url, xlinkns)
342+
self.__update_online_resources(online_resources, legend_url, xlinkns, params)
342343

343344
# HACK: Inject LegendURL for group layers (which are missing LegendURL)
344345
# Pending proper upstream QGIS server fix
@@ -480,12 +481,13 @@ def __filter_getcapabilities(self, response, permissions):
480481
status=response.status_code
481482
)
482483

483-
def __update_online_resources(self, elements, new_url, xlinkns):
484+
def __update_online_resources(self, elements, new_url, xlinkns, params):
484485
"""Update OnlineResource URLs.
485486
486487
:param list(Element) elements: List of OnlineResource elements
487488
:param str new_url: New OnlineResource URL
488489
:param str xlinkns: XML namespace for OnlineResource href
490+
:param obj params: Request parameters
489491
"""
490492

491493
qgis_server_url_parts = urlparse(self.qgis_server_url)
@@ -501,15 +503,15 @@ def __update_online_resources(self, elements, new_url, xlinkns):
501503
if new_url.startswith("/"):
502504
new_url = request.host_url.rstrip("/") + new_url
503505

504-
if url_parts.query or "?" in old_url:
505-
# Drop MAP query parameter, it is never useful for services served through qwc-qgis-server
506-
query = parse_qsl(url_parts.query)
507-
query = list(filter(lambda kv: kv[0].lower() != "map", query))
508-
# querystring = urlencode(query, doseq=True)
509-
querystring = "&".join(map(lambda kv: f"{kv[0]}={quote(kv[1], safe=' /')}", query))
510-
online_resource.set('{%s}href' % xlinkns, new_url.removesuffix("?") + "?" + querystring)
511-
else:
512-
online_resource.set('{%s}href' % xlinkns, new_url)
506+
# Drop MAP query parameter, it is never useful for services served through qwc-qgis-server
507+
query = parse_qsl(url_parts.query)
508+
query = list(filter(lambda kv: kv[0].lower() != "map", query))
509+
query = list(filter(lambda kv: kv[0].lower() != "requireauth", query))
510+
if params.get('REQUIREAUTH'):
511+
query.append(('REQUIREAUTH', params['REQUIREAUTH']))
512+
# querystring = urlencode(query, doseq=True)
513+
querystring = "&".join(map(lambda kv: f"{kv[0]}={quote(kv[1], safe=' /')}", query))
514+
online_resource.set('{%s}href' % xlinkns, new_url.removesuffix("?") + "?" + querystring)
513515

514516

515517
def __filter_getfeatureinfo(self, response, permissions):

0 commit comments

Comments
 (0)