diff --git a/lizmap_server/lizmap_accesscontrol.py b/lizmap_server/lizmap_accesscontrol.py index 673ab186..8cc65b98 100755 --- a/lizmap_server/lizmap_accesscontrol.py +++ b/lizmap_server/lizmap_accesscontrol.py @@ -391,37 +391,83 @@ def get_lizmap_layer_filter(self, layer: QgsVectorLayer, filter_type: FilterType return login_filter - login_filter = self._filter_by_login(cfg_layer_login_filter, groups, user_login) + login_filter = self._filter_by_login( + cfg_layer_login_filter, + groups, + user_login, + layer.dataProvider().name(), + ) if polygon_filter: return f'{polygon_filter} AND {login_filter}' return login_filter @staticmethod - def _filter_by_login(cfg_layer_login_filter: dict, groups: tuple, login: str) -> str: + def _filter_by_login(cfg_layer_login_filter: dict, groups: tuple, login: str, provider: str) -> str: """ Build the string according to the filter by login configuration. :param cfg_layer_login_filter: The Lizmap Filter by login configuration. :param groups: List of groups for the current user :param login: The current user + :param provider: The layer data provider ('postgres' for example) """ - # List of quoted values for expression - quoted_values = [] + # List of values for expression + values = [] if to_bool(cfg_layer_login_filter['filterPrivate']): # If filter is private use user_login - quoted_values.append(QgsExpression.quotedString(login)) + values.append(login) else: # Else use user groups - quoted_values = [QgsExpression.quotedString(g) for g in groups] - - # Add all to quoted values - quoted_values.append(QgsExpression.quotedString('all')) - - # Build filter - layer_filter = '{} IN ({})'.format( - QgsExpression.quotedColumnRef(cfg_layer_login_filter['filterAttribute']), - ', '.join(quoted_values), - ) + values = list(groups) + + # Add all to values + values.append('all') + + # Since LWC 3.8, we allow to have a list of groups (or logins) + # separated by comma, with NO SPACES + # only for PostgreSQL layers and if the option allow_multiple_acl_values + # is set to True + # For example the field can contain 'group_a,group_b,group_c' + # To use only pure SQL allowed by QGIS, we can use LIKE items + # For big dataset, a GIN index with pg_trgm must be used for the + # filter field to improve performance + # We cannot use array_remove, string_to_array or regexp_replace + # as it should be SQL safe for QGIS Server + + value_filters = [] + + # Quoted attribute with double-quotes + quoted_field = QgsExpression.quotedColumnRef(cfg_layer_login_filter['filterAttribute']) + + # For each value (group, all, login, etc.), create a filter + # combining all the possibility: equality & LIKE + for value in values: + filters = [] + # Quote the value with single quotes + quoted_value = QgsExpression.quotedString(value) + + # equality + filters.append(f'{quoted_field} = {quoted_value}') + + # Add LIKE statements to manage multiple values separated by comma + if provider == 'postgres' and cfg_layer_login_filter.get('allow_multiple_acl_values'): + # begins with value & comma + quoted_like_value = QgsExpression.quotedString(f'{value},%') + filters.append(f'{quoted_field} LIKE {quoted_like_value}') + + # ends with comma & value + quoted_like_value = QgsExpression.quotedString(f'%,{value}') + filters.append(f'{quoted_field} LIKE {quoted_like_value}') + + # value between two commas + quoted_like_value = QgsExpression.quotedString(f'%,{value},%') + filters.append(f'{quoted_field} LIKE {quoted_like_value}') + + # Build the filter for this value + value_filters.append(' OR '.join(filters)) + + # Build filter for all values + layer_filter = ' OR '.join(value_filters) return layer_filter diff --git a/test/test_lizmap_accesscontrol.py b/test/test_lizmap_accesscontrol.py index 880273e8..af6ba8c6 100644 --- a/test/test_lizmap_accesscontrol.py +++ b/test/test_lizmap_accesscontrol.py @@ -11,6 +11,7 @@ from qgis.core import QgsVectorLayer +from lizmap_server.lizmap_accesscontrol import LizmapAccessControlFilter from lizmap_server.tos_definitions import ( BING_KEY, GOOGLE_KEY, @@ -581,13 +582,57 @@ def test_tos_strict_layers_false(client): assert "bing-satellite" not in content -def tet_tos_strict_layers_true(client): +def test_tos_strict_layers_true(client): """ Test TOS layers not restricted. """ + # TODO fixme rv = _make_get_capabilities_tos_layers(client, True) content = rv.content.decode('utf-8') layers = rv.xpath('//wms:Layer') - assert len(layers) == 5 + assert len(layers) == 2 assert "osm" in content - assert "google-satellite" in content - assert "bing-map" in content - assert "bing-satellite" in content + assert "google-satellite" not in content + assert "bing-map" not in content + assert "bing-satellite" not in content + + +def test_filter_by_login_simple_values(): + """ Test filter with simple values in field with the current user.""" + config = { + 'filterPrivate': ['a', 'b'], + 'filterAttribute': 'f', + 'allow_multiple_acl_values': False, + } + output = LizmapAccessControlFilter._filter_by_login(config, ('grp_1', 'grp_2'), 'a', 'other_provider') + assert ( + "\"f\" = 'a' OR \"f\" = 'all'" + ) == output, output + + +def test_filter_by_login_postgres_multiple_values(): + """ Test filter with comma separated list of values with the current user.""" + config = { + 'filterPrivate': ['a', 'b'], + 'filterAttribute': 'f', + 'allow_multiple_acl_values': True, + } + output = LizmapAccessControlFilter._filter_by_login(config, ('grp_1', 'grp_2'), 'a', 'postgres') + assert ( + "\"f\" = 'a' OR \"f\" LIKE 'a,%' OR \"f\" LIKE '%,a' OR \"f\" LIKE '%,a,%' OR \"f\" = 'all' " + "OR \"f\" LIKE 'all,%' OR \"f\" LIKE '%,all' OR \"f\" LIKE '%,all,%'" + ) == output, output + + +def test_filter_by_login_postgres_multiple_values_deactivated(): + """ + Test filter with comma separated list of values with the current user + but with option disabled + """ + config = { + 'filterPrivate': ['a', 'b'], + 'filterAttribute': 'f', + 'allow_multiple_acl_values': False, + } + output = LizmapAccessControlFilter._filter_by_login(config, ('grp_1', 'grp_2'), 'a', 'postgres') + assert ( + "\"f\" = 'a' OR \"f\" = 'all'" + ) == output, output diff --git a/test/utils.py b/test/utils.py index e47b4483..ec95397d 100644 --- a/test/utils.py +++ b/test/utils.py @@ -1,7 +1,5 @@ import io import json - -from urllib3 import request import xml.etree.ElementTree as ET from typing import Dict, Union @@ -10,6 +8,7 @@ from PIL import Image from qgis.server import QgsBufferServerResponse +from urllib3 import request __copyright__ = 'Copyright 2024, 3Liz' __license__ = 'GPL version 3'