diff --git a/CHANGELOG.md b/CHANGELOG.md index 49fc2c449..42c99ffa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ - [#765](https://github.com/LayerManager/layman/issues/765) Fix `issuer_id` value in `users` table that was broken since v1.21.0. - [#765](https://github.com/LayerManager/layman/issues/765) Remove `authn.txt` files from workspace directories. The same information as in `authn.txt` files is saved in prime DB schema. ### Changes -- [#868](https://github.com/LayerManager/layman/issues/868) Endpoints [GET Publications](doc/rest.md#get-publications), [GET Layers](doc/rest.md#get-layers), [GET Workspace Layers](doc/rest.md#get-workspace-layers) and [GET Maps](doc/rest.md#get-maps) [GET Workspace Maps](doc/rest.md#get-workspace-maps) respects [HTTP header `X-Forwarded-Prefix`](doc/client-proxy.md#x-forwarded-prefix-http-header) of the request in response in `url` key. Endpoint [GET Workspace Layer](doc/rest.md#get-workspace-layer) respects it in `url`, `thumbnail`.`url`, `metadata`.`comparison_url`, `wms`.`url`, `wfs`.`url` and `style`.`url` keys. Endpoint [GET Workspace Map](doc/rest.md#get-workspace-map) respects it in `url`, `file`.`url`, `thumbnail`.`url` and `metadata`.`comparison_url` keys. +- [#868](https://github.com/LayerManager/layman/issues/868) Endpoints [GET Publications](doc/rest.md#get-publications), [GET Layers](doc/rest.md#get-layers), [GET Workspace Layers](doc/rest.md#get-workspace-layers), [GET Maps](doc/rest.md#get-maps), [GET Workspace Maps](doc/rest.md#get-workspace-maps), [GET Workspace Layer](doc/rest.md#get-workspace-layer), [GET Workspace Map](doc/rest.md#get-workspace-map), [POST Workspace Layers](doc/rest.md#post-workspace-layers), [DELETE Workspace Layer](doc/rest.md#delete-workspace-layer) and [DELETE Workspace Layers](doc/rest.md#delete-workspace-layers) respects [HTTP header `X-Forwarded-Prefix`](doc/client-proxy.md#x-forwarded-prefix-http-header) of the request in the response. - [#880](https://github.com/LayerManager/layman/issues/880) Use Docker Compose v2 (`docker compose`) in Makefile without `compatibility` flag and remove `Makefile_docker-compose_v1` file. Docker containers are named according to Docker Compose v2 and may have different name after upgrade. - [#765](https://github.com/LayerManager/layman/issues/765) Stop saving OAuth2 claims in filesystem, use prime DB schema only. - [#893](https://github.com/LayerManager/layman/issues/893) It is possible to specify logging level by new environment variable [LAYMAN_LOGLEVEL](doc/env-settings.md#LAYMAN_LOGLEVEL). Default level is `INFO`. diff --git a/doc/client-proxy.md b/doc/client-proxy.md index d3c7421b6..d9e6af589 100644 --- a/doc/client-proxy.md +++ b/doc/client-proxy.md @@ -63,3 +63,9 @@ Currently, value of `X-Forwarded-Prefix` affects following URLs: * `file`.`url` key * `thumbnail`.`url` key * `metadata`.`comparison_url` key +* [POST Workspace Layers](rest.md#post-workspace-layers) + * `url` key +* [DELETE Workspace Layer](rest.md#delete-workspace-layer) + * `url` key +* [DELETE Workspace Layers](rest.md#delete-workspace-layers) + * `url` key diff --git a/src/layman/layer/rest_workspace_layer.py b/src/layman/layer/rest_workspace_layer.py index 3b4434fa9..becd8b761 100644 --- a/src/layman/layer/rest_workspace_layer.py +++ b/src/layman/layer/rest_workspace_layer.py @@ -270,8 +270,9 @@ def patch(workspace, layername): @util.lock_decorator def delete_layer(workspace, layername): app.logger.info(f"DELETE Layer, actor={g.user}") + x_forwarded_prefix = layman_util.get_x_forwarded_prefix(request.headers) - info = util.get_complete_layer_info(workspace, layername) + info = util.get_complete_layer_info(workspace, layername, x_forwarded_prefix=x_forwarded_prefix) util.abort_layer_chain(workspace, layername) diff --git a/src/layman/layer/rest_workspace_layers.py b/src/layman/layer/rest_workspace_layers.py index c9eecd376..545042270 100644 --- a/src/layman/layer/rest_workspace_layers.py +++ b/src/layman/layer/rest_workspace_layers.py @@ -40,6 +40,8 @@ def get(workspace): def post(workspace): app.logger.info(f"POST Layers, actor={g.user}") + x_forwarded_prefix = layman_util.get_x_forwarded_prefix(request.headers) + # FILE sent_file_streams = [] sent_file_paths = [] @@ -193,7 +195,7 @@ def post(workspace): task_options, ) - layerurl = url_for('rest_workspace_layer.get', layername=layername, workspace=workspace) + layerurl = url_for('rest_workspace_layer.get', layername=layername, workspace=workspace, x_forwarded_prefix=x_forwarded_prefix) layer_result = { 'name': layername, @@ -251,6 +253,7 @@ def post(workspace): def delete(workspace): app.logger.info(f"DELETE Layers, actor={g.user}") + x_forwarded_prefix = layman_util.get_x_forwarded_prefix(request.headers) infos = layman_util.delete_publications(workspace, LAYER_TYPE, util.is_layer_chain_ready, @@ -259,5 +262,6 @@ def delete(workspace): request.method, 'rest_workspace_layer.get', 'layername', + x_forwarded_prefix=x_forwarded_prefix, ) return infos, 200 diff --git a/src/layman/util.py b/src/layman/util.py index 182c132af..28a9e6ae6 100644 --- a/src/layman/util.py +++ b/src/layman/util.py @@ -425,6 +425,7 @@ def delete_publications(workspace, method, url_path, publ_param, + x_forwarded_prefix=None, ): from layman import authn actor_name = authn.get_authn_username() @@ -449,11 +450,11 @@ def delete_publications(workspace, { 'name': info["name"], 'title': info.get("title", None), - 'url': url_for(**{'endpoint': url_path, publ_param: name, 'workspace': workspace}), + 'url': url_for(**{'endpoint': url_path, publ_param: publication[2], 'workspace': publication[0], 'x_forwarded_prefix': x_forwarded_prefix}), 'uuid': info["uuid"], 'access_rights': info['access_rights'], } - for (name, info) in whole_infos.items() + for (publication, info) in whole_infos.items() ] return jsonify(infos) diff --git a/tests/dynamic_data/base_test.py b/tests/dynamic_data/base_test.py index 02b18b234..8e2f4d366 100644 --- a/tests/dynamic_data/base_test.py +++ b/tests/dynamic_data/base_test.py @@ -21,7 +21,7 @@ def pytest_generate_tests(metafunc): cls = metafunc.cls test_fn = metafunc.function arg_names = [a for a in inspect.getfullargspec(test_fn).args if a != 'self'] - arg_names.append('post_before_patch') + arg_names.append('post_before_test') publ_type_name = cls.publication_type.split('.')[-1] if cls.publication_type else 'publication' argvalues = [] ids = [] @@ -44,7 +44,7 @@ def pytest_generate_tests(metafunc): 'rest_method': rest_method, 'rest_args': rest_args, 'parametrization': parametrization, - 'post_before_patch': (test_case.publication, test_case.rest_method, test_case.post_before_patch_args), + 'post_before_test': (test_case.publication, test_case.rest_method, test_case.post_before_test_args), } arg_values = [arg_name_to_value[n] for n in arg_names] @@ -54,7 +54,7 @@ def pytest_generate_tests(metafunc): argnames=', '.join(arg_names), argvalues=argvalues, ids=ids, - indirect=['post_before_patch'], + indirect=['post_before_test'], ) @@ -79,7 +79,7 @@ class TestSingleRestPublication: usernames_to_reserve = [] - post_before_patch_scope = 'function' + post_before_test_scope = 'function' @classmethod @final @@ -137,7 +137,7 @@ def parametrize_test_cases(cls) -> [TestCaseType]: key=input_test_case.key, rest_method=rest_method, rest_args=rest_args, - post_before_patch_args=input_test_case.post_before_patch_args, + post_before_test_args=input_test_case.post_before_test_args, params=params, type=test_type, marks=input_test_case.marks, @@ -247,11 +247,11 @@ def function_cleanup(self, request): self.external_tables_to_cleanup_on_function_end.clear() @pytest.fixture(scope='function', autouse=True) - def post_before_patch(self, request): + def post_before_test(self, request): publication, method, patch_args = request.param - assert self.post_before_patch_scope in {'function', 'class'} - if method == RestMethod.PATCH: - if self.post_before_patch_scope == 'function': + assert self.post_before_test_scope in {'function', 'class'} + if method.name != RestMethod.POST.name: + if self.post_before_test_scope == 'function': self.post_publication(publication, args=patch_args) else: self.ensure_publication(publication, args=patch_args, scope='class') diff --git a/tests/dynamic_data/base_test_classes.py b/tests/dynamic_data/base_test_classes.py index b83c982c8..100ecae67 100644 --- a/tests/dynamic_data/base_test_classes.py +++ b/tests/dynamic_data/base_test_classes.py @@ -48,15 +48,17 @@ def __init__(self, name, domain, base_domain=None): self.base_domain = base_domain or domain -class RestMethod(Enum): - POST = ('post_publication', 'post') - PATCH = ('patch_publication', 'patch') - +class RestMethodBase(Enum): def __init__(self, function_name, publ_name_part): self.function_name = function_name self.publ_name_part = publ_name_part +class RestMethod(RestMethodBase): + POST = ('post_publication', 'post') + PATCH = ('patch_publication', 'patch') + + class PublicationByDefinitionBase(Enum): def __init__(self, publication_definition, publ_name_part): self.publication_definition = publication_definition @@ -116,7 +118,7 @@ def publication_definition(self) -> Optional[PublicationValues]: @property def rest_method(self) -> Optional[RestMethod]: # pylint: disable=no-member - return next((v for v in self._values if isinstance(v, RestMethod)), None) + return next((v for v in self._values if isinstance(v, RestMethodBase)), None) @property def style_file(self) -> Optional[StyleFileDomainBase]: @@ -147,4 +149,4 @@ class TestCaseType: specific_types: dict = field(default_factory=dict) marks: List[_pytest.mark.structures.Mark] = field(default_factory=list) parametrization: Parametrization = None - post_before_patch_args: dict = field(default_factory=dict) + post_before_test_args: dict = field(default_factory=dict) diff --git a/tests/dynamic_data/base_test_util.py b/tests/dynamic_data/base_test_util.py index 44612367d..19359487b 100644 --- a/tests/dynamic_data/base_test_util.py +++ b/tests/dynamic_data/base_test_util.py @@ -6,7 +6,7 @@ from tests import PublicationValues from . import base_test_classes -from .base_test_classes import RestArgs, RestMethod, PublicationByDefinitionBase, Parametrization, RestArgDomain +from .base_test_classes import RestArgs, RestMethodBase, PublicationByDefinitionBase, Parametrization, RestArgDomain def get_dimension_enum(dimension): @@ -20,7 +20,7 @@ def check_rest_parametrization(rest_parametrization): base_args = defaultdict(list) for val_idx, val in enumerate(rest_parametrization): - is_rest_method = val == RestMethod + is_rest_method = inspect.isclass(val) and issubclass(val, RestMethodBase) if is_rest_method: rest_methods.append(val) diff --git a/tests/dynamic_data/publications/layer_by_used_servers/layer_patch_without_data_test.py b/tests/dynamic_data/publications/layer_by_used_servers/layer_patch_without_data_test.py index f297bc719..c5b88d7e0 100644 --- a/tests/dynamic_data/publications/layer_by_used_servers/layer_patch_without_data_test.py +++ b/tests/dynamic_data/publications/layer_by_used_servers/layer_patch_without_data_test.py @@ -45,7 +45,7 @@ def generate_test_cases(): name=layer_by_server.publ_name_part, ), type=EnumTestTypes.OPTIONAL, - post_before_patch_args=post_args, + post_before_test_args=post_args, rest_method=base_test.RestMethod.PATCH, rest_args=rest_args, params=all_params, @@ -66,7 +66,7 @@ class TestLayer(base_test.TestSingleRestPublication): test_cases = generate_test_cases() - post_before_patch_scope = 'class' + post_before_test_scope = 'class' external_tables_to_create = base_test_classes.EXTERNAL_TABLE_FOR_LAYERS_BY_USED_SERVERS diff --git a/tests/dynamic_data/publications/multi_publications/test_get_multiendpoints.py b/tests/dynamic_data/publications/multi_publications/test_get_multiendpoints.py index 906b1ad62..c7ffca571 100644 --- a/tests/dynamic_data/publications/multi_publications/test_get_multiendpoints.py +++ b/tests/dynamic_data/publications/multi_publications/test_get_multiendpoints.py @@ -843,7 +843,7 @@ def pytest_generate_tests(metafunc): assert not test_case.specific_params, f"Not yet implemented" assert not test_case.specific_types, f"Not yet implemented" assert not test_case.parametrization, f"Not yet implemented" - assert not test_case.post_before_patch_args, f"Not yet implemented" + assert not test_case.post_before_test_args, f"Not yet implemented" assert test_case.type == EnumTestTypes.MANDATORY, f"Other types then MANDATORY are not implemented yet" arg_name_to_value = { 'params': copy.deepcopy(test_case.params), diff --git a/tests/dynamic_data/publications/wrong_input/wrong_input_test.py b/tests/dynamic_data/publications/wrong_input/wrong_input_test.py index 912d88e08..52091cf17 100644 --- a/tests/dynamic_data/publications/wrong_input/wrong_input_test.py +++ b/tests/dynamic_data/publications/wrong_input/wrong_input_test.py @@ -29,7 +29,7 @@ class Key(Enum): MANDATORY_CASES = 'mandatory_cases' RUN_ONLY_CASES = 'run_only_cases' SPECIFIC_CASES = 'specific_params' - POST_BEFORE_PATCH_ARGS = 'post_before_patch_args' + POST_BEFORE_TEST_ARGS = 'post_before_test_args' WORKSPACE = 'dynamic_test_workspace_wrong_input' @@ -169,7 +169,7 @@ class Key(Enum): Key.REST_ARGS: { 'file_paths': ['sample/layman.layer/sample_tif_grayscale_nodata_opaque.tif'], }, - Key.POST_BEFORE_PATCH_ARGS: { + Key.POST_BEFORE_TEST_ARGS: { 'file_paths': ['sample/layman.layer/sample_point_cz.geojson'], 'style_file': 'sample/layman.layer/sample_point_cz.qml', }, @@ -607,7 +607,7 @@ class Key(Enum): 'file_paths': [], 'overview_resampling': 'mode', }, - Key.POST_BEFORE_PATCH_ARGS: { + Key.POST_BEFORE_TEST_ARGS: { 'file_paths': ['sample/layman.layer/sample_tif_grayscale_nodata_opaque.tif'], 'overview_resampling': 'nearest', }, @@ -921,7 +921,7 @@ class Key(Enum): Key.REST_ARGS: { 'time_regex': r'[0-9]{8}T[0-9]{6}Z(\?!.\*[0-9]{8}T[0-9]{6}Z.\*)', }, - Key.POST_BEFORE_PATCH_ARGS: { + Key.POST_BEFORE_TEST_ARGS: { 'time_regex': r'[0-9]{8}', 'file_paths': [ 'tests/dynamic_data/publications/layer_timeseries/timeseries_tif/S2A_MSIL2A_20220316T100031_N0400_R122_T33UWR_20220316T134748_TCI_10m.tif', @@ -1331,7 +1331,7 @@ def generate_test_cases(): assert case not in specific_types, f'key={key},\ncase={case},\nspecific_types={specific_types}' specific_types[case] = EnumTestTypes.IGNORE - post_before_patch_args = test_case_params.pop(Key.POST_BEFORE_PATCH_ARGS, {}) + post_before_test_args = test_case_params.pop(Key.POST_BEFORE_TEST_ARGS, {}) publ_type = all_params.pop(Key.PUBLICATION_TYPE) publication_name = rest_args.pop('name', None) @@ -1348,7 +1348,7 @@ def generate_test_cases(): rest_args=rest_args, params=all_params, specific_params=all_params.pop(Key.SPECIFIC_CASES), - post_before_patch_args=post_before_patch_args, + post_before_test_args=post_before_test_args, marks=[pytest.mark.xfail(reason="Not yet implemented.")] if test_case_params.get('xfail') else [] ) diff --git a/tests/dynamic_data/publications/x_forwarded_prefix_test.py b/tests/dynamic_data/publications/x_forwarded_prefix_test.py new file mode 100644 index 000000000..801d8c139 --- /dev/null +++ b/tests/dynamic_data/publications/x_forwarded_prefix_test.py @@ -0,0 +1,39 @@ +from layman import settings +from test_tools import process_client +from tests import EnumTestTypes +from tests.dynamic_data import base_test, base_test_classes + +pytest_generate_tests = base_test.pytest_generate_tests + + +class RestMethodLocal(base_test_classes.RestMethodBase): + POST = ('post_publication', 'post') + DELETE = ('delete_workspace_publication', 'delete') + MULTI_DELETE = ('delete_workspace_publications', 'multi_delete') + + +class TestPublication(base_test.TestSingleRestPublication): + workspace = 'x_forwarded_prefix_post_workspace' + rest_parametrization = [ + RestMethodLocal + ] + + test_cases = [base_test.TestCaseType(key='layer', + publication_type=process_client.LAYER_TYPE, + type=EnumTestTypes.MANDATORY, + )] + + @classmethod + def delete_workspace_publication(cls, publication, args=None): + return process_client.delete_workspace_publication(publication.type, publication.workspace, publication.name, **args) + + @classmethod + def delete_workspace_publications(cls, publication, args=None): + return process_client.delete_workspace_publications(publication.type, publication.workspace, **args) + + @staticmethod + def test_publication(publication, rest_method): + proxy_prefix = '/layman-proxy' + response = rest_method(publication, args={'headers': {'X-Forwarded-Prefix': proxy_prefix}}) + publication_response = response[0] if isinstance(response, list) and len(response) == 1 else response + assert publication_response['url'] == f'http://{settings.LAYMAN_PROXY_SERVER_NAME}{proxy_prefix}/rest/workspaces/{publication.workspace}/layers/{publication.name}'