Skip to content

Commit 4e54fed

Browse files
authored
Merge pull request #603 from crim-ca/wps-header-filter
2 parents fcec447 + d234e11 commit 4e54fed

File tree

6 files changed

+81
-7
lines changed

6 files changed

+81
-7
lines changed

CHANGES.rst

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ Changes
1212

1313
Changes:
1414
--------
15-
- No change.
15+
- Add ``weaver.wps_client_headers_filter`` setting that allows filtering of specific `WPS` request headers from the
16+
incoming request to be passed down to the `WPS` client employed to interact with the `WPS` provider
17+
(fixes `#600 <https://github.com/crim-ca/weaver/issues/600>`_).
1618

1719
Fixes:
1820
------

config/weaver.ini.example

+4
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ weaver.wps_output_s3_bucket =
113113
weaver.wps_output_s3_region =
114114
weaver.wps_workdir =
115115

116+
# List of comma-separated case-insensitive headers that will be removed from incoming requests before
117+
# passing them down to invoke an operation with the corresponding WPS provider through the WPS client.
118+
weaver.wps_client_headers_filter = Host,
119+
116120
# --- Weaver WPS metadata ---
117121
# all attributes under "metadata:main" can be specified as 'weaver.wps_metadata_<field>'
118122
# (reference: https://pywps.readthedocs.io/en/master/configuration.html#metadata-main)

docs/source/configuration.rst

+10
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,16 @@ they are optional and which default value or operation is applied in each situat
199199
|
200200
| Prefix where process :term:`Job` worker should execute the :term:`Process` from.
201201
202+
- | ``weaver.wps_client_headers_filter = <headers>``
203+
| (default: ``Host,``)
204+
|
205+
| List of comma-separated case-insensitive headers that will be removed from incoming requests before
206+
| passing them down to invoke an operation with the corresponding :term:`WPS` provider through the :term:`WPS` client.
207+
208+
.. seealso::
209+
- :func:`weaver.wps.utils.get_wps_client_filtered_headers`
210+
- :func:`weaver.wps.utils.get_wps_client`
211+
202212
- | ``weaver.wps_restapi = true|false`` [:class:`bool`-like]
203213
| (default: ``true``)
204214
|

tests/wps/test_utils.py

+32-2
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
from weaver import xml_util
1515
from weaver.formats import AcceptLanguage, ContentType
1616
from weaver.owsexceptions import OWSAccessForbidden, OWSException, OWSMissingParameterValue, OWSNoApplicableCode
17+
from weaver.utils import null
1718
from weaver.wps.utils import (
1819
get_exception_from_xml_status,
1920
get_wps_client,
21+
get_wps_client_filtered_headers,
2022
get_wps_output_context,
2123
map_wps_output_location,
2224
set_wps_language
@@ -91,14 +93,17 @@ def test_get_wps_client_headers_preserved():
9193
"""
9294
Validate that original request headers are not modified following WPS client sub-requests.
9395
"""
94-
test_wps_url = "http://dont-care.com/wps"
96+
test_wps_host = "dont-care.com"
97+
test_wps_url = f"http://{test_wps_host}/wps"
9598
test_headers = {
99+
"Host": test_wps_host, # this should be filtered out
96100
"Content-Type": ContentType.APP_XML,
97101
"Content-Length": "0",
98102
"Accept-Language": AcceptLanguage.FR_CA,
99103
"Accept": ContentType.APP_JSON,
100104
"Authorization": "Bearer: FAKE", # nosec
101105
}
106+
test_settings = {"weaver.wps_client_headers_filter": "Host,"}
102107
test_copy_headers = copy.deepcopy(test_headers)
103108
# following are removed for sub-request
104109
test_wps_headers = {
@@ -112,7 +117,7 @@ def test_get_wps_client_headers_preserved():
112117
for patch in patches:
113118
mocks.append(stack.enter_context(patch))
114119

115-
wps = get_wps_client(test_wps_url, headers=test_headers)
120+
wps = get_wps_client(test_wps_url, headers=test_headers, container=test_settings)
116121

117122
for mocked in mocks:
118123
assert mocked.called
@@ -122,6 +127,31 @@ def test_get_wps_client_headers_preserved():
122127
assert wps.url == test_wps_url
123128

124129

130+
@pytest.mark.parametrize(
131+
["test_headers", "test_filter", "expect_result"],
132+
[
133+
# 'null' not supported by function itself, only for us to consider no settings container at all
134+
({}, null, {}),
135+
({"Host": "test.com"}, null, {}), # Host is default if no settings
136+
({}, None, {}),
137+
({"Host": "test.com"}, None, {}),
138+
({}, "", {}),
139+
({}, [], {}),
140+
({"Authorization": "random"}, [], {"Authorization": "random"}),
141+
({"Authorization": "random"}, "Host", {"Authorization": "random"}),
142+
({"X-Test": "OK", "x-custom": "bye", "Host": "test.com"}, ["Host", "X-custom"], {"X-Test": "OK"}),
143+
({"Host": "example.com", "Authorization": "random"}, "host", {"Authorization": "random"}),
144+
({"Host": "example.com", "Authorization": "random"}, "Host,", {"Authorization": "random"}),
145+
({"Host": "example.com", "Authorization": "random"}, "HOST,", {"Authorization": "random"}),
146+
({"Host": "example.com", "Authorization": "random"}, ["Host"], {"Authorization": "random"}),
147+
]
148+
)
149+
def test_get_wps_client_filtered_headers(test_headers, test_filter, expect_result):
150+
settings = {"weaver.wps_client_headers_filter": test_filter} if test_filter is not null else None
151+
result = get_wps_client_filtered_headers(test_headers, settings)
152+
assert result == expect_result
153+
154+
125155
def test_get_wps_output_context_validation():
126156
bad_cases = [
127157
"test/////test",

weaver/utils.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@
149149
"headers": NotRequired[AnyHeadersContainer],
150150
"cookies": NotRequired[AnyCookiesContainer],
151151
"stream": NotRequired[bool],
152+
"cache": NotRequired[bool],
153+
"cache_enabled": NotRequired[bool],
152154
}, total=False)
153155
RequestCachingKeywords = Dict[str, AnyValueType]
154156
RequestCachingFunction = Callable[[AnyRequestMethod, str, RequestCachingKeywords], Response]
@@ -1670,7 +1672,7 @@ def invalidate_region(caching_args):
16701672

16711673

16721674
def get_ssl_verify_option(method, url, settings, request_options=None):
1673-
# type: (str, str, AnySettingsContainer, Optional[SettingsType]) -> bool
1675+
# type: (str, str, AnySettingsContainer, Optional[RequestOptions]) -> bool
16741676
"""
16751677
Obtains the SSL verification option considering multiple setting definitions and the provided request context.
16761678
@@ -1695,9 +1697,9 @@ def get_ssl_verify_option(method, url, settings, request_options=None):
16951697

16961698

16971699
def get_no_cache_option(request_headers, **cache_options):
1698-
# type: (HeadersType, **bool) -> bool
1700+
# type: (HeadersType, **bool | RequestOptions) -> bool
16991701
"""
1700-
Obtains the No-Cache result from request headers and configured request options.
1702+
Obtains the ``No-Cache`` result from request headers and configured :term:`Request Options`.
17011703
17021704
.. seealso::
17031705
- :meth:`Request.headers`
@@ -1717,7 +1719,7 @@ def get_no_cache_option(request_headers, **cache_options):
17171719
def get_request_options(method, url, settings):
17181720
# type: (str, str, AnySettingsContainer) -> RequestOptions
17191721
"""
1720-
Obtains the *request options* corresponding to the request from the configuration file.
1722+
Obtains the :term:`Request Options` corresponding to the request from the configuration file.
17211723
17221724
The configuration file specified is expected to be pre-loaded within setting ``weaver.request_options``.
17231725
If no file was pre-loaded or no match is found for the request, an empty options dictionary is returned.

weaver/wps/utils.py

+26
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from owslib.wps import WebProcessingService, WPSExecution
1313
from pyramid.httpexceptions import HTTPNotFound, HTTPOk, HTTPUnprocessableEntity
1414
from pywps import configuration as pywps_config
15+
from requests.structures import CaseInsensitiveDict
1516
from webob.acceptparse import create_accept_language_header
1617

1718
from weaver import owsexceptions, xml_util
@@ -281,6 +282,7 @@ def get_wps_client(url, container=None, verify=None, headers=None, language=None
281282
language = str(language)
282283
if headers is not None and not isinstance(headers, dict):
283284
headers = dict(headers)
285+
headers = get_wps_client_filtered_headers(headers, container)
284286
request_args = (url, headers, verify, language)
285287
if get_no_cache_option(headers, request_options=opts):
286288
for func in (_get_wps_client_cached, _describe_process_cached):
@@ -290,6 +292,30 @@ def get_wps_client(url, container=None, verify=None, headers=None, language=None
290292
return wps
291293

292294

295+
def get_wps_client_filtered_headers(headers, container):
296+
# type: (Optional[HeadersType], AnySettingsContainer) -> HeadersType
297+
"""
298+
Filters out any headers configured for the :term:`WPS` client by the ``weaver.wps_client_headers_filter`` setting.
299+
300+
:param headers: Headers to filter as applicable.
301+
:param container: Any settings container to retrieve application settings.
302+
:return: Filtered :term:`WPS` headers.
303+
"""
304+
if not headers:
305+
return {}
306+
settings = get_settings(container) or {}
307+
hdr_spec = settings.get("weaver.wps_client_headers_filter") or "Host," # default if missing (match docs/example)
308+
if isinstance(hdr_spec, str):
309+
hdr_spec = [hdr.strip() for hdr in hdr_spec.split(",")]
310+
hdr_spec = [hdr for hdr in hdr_spec if hdr]
311+
if not hdr_spec:
312+
return headers
313+
headers = CaseInsensitiveDict(headers.copy())
314+
for hdr in hdr_spec:
315+
headers.pop(hdr, None)
316+
return dict(headers)
317+
318+
293319
def check_wps_status(location=None, # type: Optional[str]
294320
response=None, # type: Optional[xml_util.XML]
295321
sleep_secs=2, # type: int

0 commit comments

Comments
 (0)