Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attribute filter - Allow to have a comma separated list of groups or users #86

Merged
merged 3 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 61 additions & 15 deletions lizmap_server/lizmap_accesscontrol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
55 changes: 50 additions & 5 deletions test/test_lizmap_accesscontrol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
3 changes: 1 addition & 2 deletions test/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import io
import json

from urllib3 import request
import xml.etree.ElementTree as ET

from typing import Dict, Union
Expand All @@ -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'
Expand Down