From ce6381e078665d46aae3217633d89c1c0d2f4556 Mon Sep 17 00:00:00 2001 From: Michael Douchin Date: Wed, 14 Aug 2024 15:07:56 +0200 Subject: [PATCH 1/3] Attribute filter - Allow to have a comma separated list of groups or users --- lizmap_server/lizmap_accesscontrol.py | 58 ++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/lizmap_server/lizmap_accesscontrol.py b/lizmap_server/lizmap_accesscontrol.py index 673ab186..c3610200 100755 --- a/lizmap_server/lizmap_accesscontrol.py +++ b/lizmap_server/lizmap_accesscontrol.py @@ -405,23 +405,59 @@ def _filter_by_login(cfg_layer_login_filter: dict, groups: tuple, login: str) -> :param groups: List of groups for the current user :param login: The current user """ - # 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] + values = list(groups) - # Add all to quoted values - quoted_values.append(QgsExpression.quotedString('all')) + # Add all to values + values.append('all') - # Build filter - layer_filter = '{} IN ({})'.format( - QgsExpression.quotedColumnRef(cfg_layer_login_filter['filterAttribute']), - ', '.join(quoted_values), - ) + # Since LWC 3.8, we allow to have a list of groups (or logins) + # separated by comma, with NO SPACES + # e.g. field "filter_fiel" 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}') + + # 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 From 9bbd904279cb21658598e6b71a02fd574f128eaf Mon Sep 17 00:00:00 2001 From: Etienne Trimaille Date: Wed, 14 Aug 2024 16:46:37 +0200 Subject: [PATCH 2/3] Tests about comma separated list of groups --- lizmap_server/lizmap_accesscontrol.py | 2 +- test/test_lizmap_accesscontrol.py | 25 ++++++++++++++++++++----- test/utils.py | 3 +-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lizmap_server/lizmap_accesscontrol.py b/lizmap_server/lizmap_accesscontrol.py index c3610200..776c95ce 100755 --- a/lizmap_server/lizmap_accesscontrol.py +++ b/lizmap_server/lizmap_accesscontrol.py @@ -420,7 +420,7 @@ def _filter_by_login(cfg_layer_login_filter: dict, groups: tuple, login: str) -> # Since LWC 3.8, we allow to have a list of groups (or logins) # separated by comma, with NO SPACES - # e.g. field "filter_fiel" can contain 'group_a,group_b,group_c' + # e.g. field "filter_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 diff --git a/test/test_lizmap_accesscontrol.py b/test/test_lizmap_accesscontrol.py index 880273e8..bed13fff 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,27 @@ 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(): + """ Test about comma separated list of values with the current user.""" + config = { + 'filterPrivate': ['a', 'b'], + 'filterAttribute': 'f', + } + output = LizmapAccessControlFilter._filter_by_login(config, ('grp_1', 'grp_2'), 'a') + 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 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' From 510f522f493cf5822c94d7b0a7cb6272f43836aa Mon Sep 17 00:00:00 2001 From: Michael Douchin Date: Thu, 22 Aug 2024 16:45:48 +0200 Subject: [PATCH 3/3] Attribute filter - Allow multiple values for PostgreSQL layers and active option --- lizmap_server/lizmap_accesscontrol.py | 34 ++++++++++++++++--------- test/test_lizmap_accesscontrol.py | 36 ++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/lizmap_server/lizmap_accesscontrol.py b/lizmap_server/lizmap_accesscontrol.py index 776c95ce..8cc65b98 100755 --- a/lizmap_server/lizmap_accesscontrol.py +++ b/lizmap_server/lizmap_accesscontrol.py @@ -391,19 +391,25 @@ 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 values for expression values = [] @@ -420,7 +426,9 @@ def _filter_by_login(cfg_layer_login_filter: dict, groups: tuple, login: str) -> # Since LWC 3.8, we allow to have a list of groups (or logins) # separated by comma, with NO SPACES - # e.g. field "filter_field" can contain 'group_a,group_b,group_c' + # 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 @@ -442,17 +450,19 @@ def _filter_by_login(cfg_layer_login_filter: dict, groups: tuple, login: str) -> # equality filters.append(f'{quoted_field} = {quoted_value}') - # begins with value & comma - quoted_like_value = QgsExpression.quotedString(f'{value},%') - filters.append(f'{quoted_field} LIKE {quoted_like_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}') + # 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}') + # 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)) diff --git a/test/test_lizmap_accesscontrol.py b/test/test_lizmap_accesscontrol.py index bed13fff..af6ba8c6 100644 --- a/test/test_lizmap_accesscontrol.py +++ b/test/test_lizmap_accesscontrol.py @@ -595,14 +595,44 @@ def test_tos_strict_layers_true(client): assert "bing-satellite" not in content -def test_filter_by_login(): - """ Test about comma separated list of values with the current user.""" +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') + 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