diff --git a/README.md b/README.md index 8feb2b77f..9a8f1fb16 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Software releases follow theese main branches as described in the compatibility | Branch | Python | Django | QGIS | [client] | First release | Status | |------------|----------------|----------------|---------------|----------|---------------|----------------| | [dev] | 3.10 | 3.2 | 3.28 | dev | Unreleased | ⚠️️ Unstable | -| [v.3.6.x] | 3.10 | 3.2 | 3.28 | 3.8.7 | May 2023 | 🪲️ Bug fixing | +| [v.3.6.x] | 3.10 | 3.2 | 3.28 | 3.8.10 | May 2023 | 🪲️ Bug fixing | | [v.3.5.x] | 3.10 | 2.2 | 3.22 | 3.7 | Nov 2022 | 🪲️ Bug fixing | | [v.3.4.x] | 3.8 | 2.2 | 3.22 | 3.4 | Mar 2022 | 🚨 End of Life | | [v.3.3.x] | 3.6 | 2.2 | 3.16 | 3.3 | Sep 2021 | 🚨 End of Life | diff --git a/g3w-admin/caching/tests/test_api.py b/g3w-admin/caching/tests/test_api.py index a12af58cd..e54a9a6ed 100644 --- a/g3w-admin/caching/tests/test_api.py +++ b/g3w-admin/caching/tests/test_api.py @@ -80,6 +80,8 @@ def test_tilestache_api(self): client = Client() layer = Layer.objects.get(project=self.project.instance, qgs_layer_id='spatialite_points20190604101052075') assign_perm('view_project', self.anonymoususer, self.project.instance) + for l in self.project.instance.layer_set.all(): + assign_perm("view_layer", self.anonymoususer, l) # active caching for layer cachinglayer = G3WCachingLayer.objects.create(app_name='qdjango', layer_id=layer.pk) diff --git a/g3w-admin/core/tests/test_api.py b/g3w-admin/core/tests/test_api.py index d6bddeecf..5ba165cac 100644 --- a/g3w-admin/core/tests/test_api.py +++ b/g3w-admin/core/tests/test_api.py @@ -633,7 +633,7 @@ def testCoreInterfaceOwsView(self): self.assertTrue('text/html' in jres['info_formats']) self.assertEqual(len(jres['layers']), 21) - self.assertEqual(jres['layers'][1]['title'], 'Acque - AdT Catasto Terreni') + self.assertEqual(jres['layers'][1]['title'].lower(), 'Acque - AdT Catasto Terreni'.lower()) self.assertEqual(len(jres['layers'][1]['crss']), 20) diff --git a/g3w-admin/editing/api/base/views.py b/g3w-admin/editing/api/base/views.py index ee6397953..2fb47a45b 100644 --- a/g3w-admin/editing/api/base/views.py +++ b/g3w-admin/editing/api/base/views.py @@ -204,8 +204,9 @@ def save_vector_data(self, metadata_layer, post_layer_data, has_transactions, po # case relation data ADD, if father referenced field is pk if is_referenced_field_is_pk: for newid in kwargs['referenced_layer_insert_ids']: - if geojson_feature['properties'][metadata_layer.referencing_field] == newid['clientid']: - geojson_feature['properties'][metadata_layer.referencing_field] = newid['id'] + for referencing_field in metadata_layer.referencing_field: + if geojson_feature['properties'][referencing_field] == newid['clientid']: + geojson_feature['properties'][referencing_field] = newid['id'] if mode_editing == EDITING_POST_DATA_UPDATED: # control feature locked diff --git a/g3w-admin/qdjango/auth.py b/g3w-admin/qdjango/auth.py index 6ac45feee..0ab70f6b2 100644 --- a/g3w-admin/qdjango/auth.py +++ b/g3w-admin/qdjango/auth.py @@ -25,6 +25,7 @@ def auth_request(self, **kwargs): try: ba = BasicAuthentication() user, other = ba.authenticate(self.request) + self.request.user = user return user.has_perm('qdjango.view_project', self.project) except Exception as e: print(e) diff --git a/g3w-admin/qdjango/server_filters/accesscontrol/layer_acl.py b/g3w-admin/qdjango/server_filters/accesscontrol/layer_acl.py new file mode 100644 index 000000000..0aa90bbf6 --- /dev/null +++ b/g3w-admin/qdjango/server_filters/accesscontrol/layer_acl.py @@ -0,0 +1,50 @@ +# coding=utf-8 +"""" Che layer acl +.. note:: This program is free software; you can redistribute it and/or modify + it under the terms of the Mozilla Public License 2.0. + +""" + +__author__ = "lorenzetti@gis3w.it" +__date__ = "2023-09-25" +__copyright__ = "Copyright 2015 - 2023, Gis3w" +__license__ = "MPL 2.0" + +from guardian.shortcuts import get_perms +from qgis.server import QgsAccessControlFilter +from qgis.core import QgsMessageLog, Qgis +from qdjango.apps import QGS_SERVER +from qdjango.models import Layer + + +class LayerAclAccessControlFilter(QgsAccessControlFilter): + """Filter layer by ACL properties""" + + def __init__(self, server_iface): + super().__init__(server_iface) + + def layerPermissions(self, layer): + + rights = QgsAccessControlFilter.LayerPermissions() + + try: + qdjango_layer = Layer.objects.get( + project=QGS_SERVER.project, qgs_layer_id=layer.id()) + + # Check permission + perms = get_perms(QGS_SERVER.user, qdjango_layer) + rights.canRead = "view_layer" in perms + rights.canInsert = "add_layer" in perms + rights.canUpdate = "change_layer" in perms + rights.canDelete = "delete_layer" in perms + + except Layer.DoesNotExist: + pass + + return rights + + +# Register the filter, keep a reference because of the garbage collector +layeracl_filter = LayerAclAccessControlFilter(QGS_SERVER.serverInterface()) +# Note: this should be the last filter, set the priority to 10000 +QGS_SERVER.serverInterface().registerAccessControl(layeracl_filter, 10010) \ No newline at end of file diff --git a/g3w-admin/qdjango/tests/data/geodata/qgis_widget_test_data.gpkg b/g3w-admin/qdjango/tests/data/geodata/qgis_widget_test_data.gpkg index 0dac454a1..69b5fbbd8 100644 Binary files a/g3w-admin/qdjango/tests/data/geodata/qgis_widget_test_data.gpkg and b/g3w-admin/qdjango/tests/data/geodata/qgis_widget_test_data.gpkg differ diff --git a/g3w-admin/qdjango/tests/test_constraints.py b/g3w-admin/qdjango/tests/test_constraints.py index 5d6deacf7..f215193ca 100644 --- a/g3w-admin/qdjango/tests/test_constraints.py +++ b/g3w-admin/qdjango/tests/test_constraints.py @@ -164,6 +164,31 @@ def _check_subset_string(self, login=True): return is_rome + def _check_wfs_getfeature(self, login=True): + """Check for ROME in the returned content""" + + ows_url = reverse('OWS:ows', kwargs={'group_slug': self.qdjango_project.group.slug, + 'project_type': 'qdjango', 'project_id': self.qdjango_project.id}) + + c = Client() + if login: + self.assertTrue(c.login(username='admin01', password='admin01')) + response = c.get(ows_url, { + 'REQUEST': 'GetFeature', + 'SERVICE': 'WFS', + 'VERSION': '1.1.0', + 'TYPENAME': 'world' + }) + + is_rome = b"ROME" in response.content + # Now query another location to make sure the whole layer was not invalidated + assert b"BERLIN" in response.content + + if login: + c.logout() + + return is_rome + class SingleLayerSubsetStringConstraints(TestSingleLayerConstraintsBase): """Test single layer subset string constraints""" @@ -172,6 +197,7 @@ def test_user_constraint(self): """Test model with user constraint""" self.assertTrue(self._check_subset_string()) + self.assertTrue(self._check_wfs_getfeature()) admin01 = self.test_user1 constraint = SingleLayerConstraint(layer=self.world, active=True) @@ -196,6 +222,7 @@ def test_user_constraint(self): admin01, self.world.pk), "(NAME != 'ITALY')") self.assertFalse(self._check_subset_string()) + self.assertFalse(self._check_wfs_getfeature()) self.assertEqual(constraint.layer_name, 'world') self.assertEqual(constraint.qgs_layer_id, 'world20181008111156525') @@ -222,6 +249,7 @@ def test_user_constraint(self): admin01, self.world.pk), "(NAME != 'ITALY')") self.assertFalse(self._check_subset_string()) + self.assertFalse(self._check_wfs_getfeature()) self.assertEqual(constraint.layer_name, 'world') self.assertEqual(constraint.qgs_layer_id, 'world20181008111156525') @@ -252,12 +280,16 @@ def test_user_constraint(self): # for OGC service only in v an ve context self.assertTrue(self._check_subset_string()) + self.assertTrue(self._check_wfs_getfeature()) def test_anonymoususer_constraint(self): """Test for anonymous user""" # For AnonymousUser assign_perm('view_project', get_anonymous_user(), self.qdjango_project) + for l in self.qdjango_project.layer_set.all(): + assign_perm('view_layer', get_anonymous_user(), l) + self.assertTrue(self._check_subset_string(login=False)) constraint_anonymous = SingleLayerConstraint(layer=self.world, active=True) @@ -267,11 +299,13 @@ def test_anonymoususer_constraint(self): rule_anonymous.save() self.assertFalse(self._check_subset_string(login=False)) + self.assertFalse(self._check_wfs_getfeature(login=False)) def test_group_constraint(self): """Test model with group constraint""" self.assertTrue(self._check_subset_string()) + self.assertTrue(self._check_wfs_getfeature()) admin01 = self.test_user1 group1 = admin01.groups.all()[0] @@ -298,6 +332,7 @@ def test_group_constraint(self): admin01, world.pk), "(NAME != 'ITALY')") self.assertFalse(self._check_subset_string()) + self.assertFalse(self._check_wfs_getfeature()) @skipIf(IS_QGIS_3_10, "In QGIS 3.10 setSubsetString() always returns True") def test_validate_sql(self): @@ -395,6 +430,7 @@ def test_user_constraint(self): """Test model with user constraint""" self.assertTrue(self._check_subset_string()) + self.assertTrue(self._check_wfs_getfeature()) admin01 = self.test_user1 world = self.world @@ -420,6 +456,7 @@ def test_user_constraint(self): admin01, world.pk), "(NAME != 'ITALY')") self.assertFalse(self._check_subset_string()) + self.assertFalse(self._check_wfs_getfeature()) self.assertEqual(constraint.layer_name, 'world') self.assertEqual(constraint.qgs_layer_id, 'world20181008111156525') @@ -438,6 +475,7 @@ def test_user_constraint(self): admin01, world.pk, context='e'), "(NAME != 'ITALY')") self.assertFalse(self._check_subset_string()) + self.assertFalse(self._check_wfs_getfeature()) self.assertEqual(constraint.layer_name, 'world') self.assertEqual(constraint.qgs_layer_id, 'world20181008111156525') @@ -458,6 +496,7 @@ def test_user_constraint(self): admin01, world.pk, context='e'), "(NAME != 'ITALY')") self.assertTrue(self._check_subset_string()) + self.assertTrue(self._check_wfs_getfeature()) self.assertEqual(constraint.layer_name, 'world') self.assertEqual(constraint.qgs_layer_id, 'world20181008111156525') @@ -468,7 +507,11 @@ def test_anonymoususer_constraint(self): # For AnonymousUser assign_perm('view_project', get_anonymous_user(), self.qdjango_project) + for l in self.qdjango_project.layer_set.all(): + assign_perm('view_layer', get_anonymous_user(), l) + self.assertTrue(self._check_subset_string(login=False)) + self.assertTrue(self._check_wfs_getfeature(login=False)) constraint_anonymous = SingleLayerConstraint(layer=self.world, active=True) constraint_anonymous.save() @@ -477,11 +520,13 @@ def test_anonymoususer_constraint(self): rule_anonymous.save() self.assertFalse(self._check_subset_string(login=False)) + self.assertFalse(self._check_wfs_getfeature(login=False)) def test_group_constraint(self): """Test model with group constraint""" self.assertTrue(self._check_subset_string()) + self.assertTrue(self._check_wfs_getfeature()) admin01 = self.test_user1 group1 = admin01.groups.all()[0] @@ -508,6 +553,7 @@ def test_group_constraint(self): admin01, world.pk), "(NAME != 'ITALY')") self.assertFalse(self._check_subset_string()) + self.assertFalse(self._check_wfs_getfeature()) # context view + editing ve # ========================= @@ -529,6 +575,7 @@ def test_group_constraint(self): admin01, world.pk), "(NAME != 'ITALY')") self.assertFalse(self._check_subset_string()) + self.assertFalse(self._check_wfs_getfeature()) # context editing e # ========================= @@ -554,6 +601,7 @@ def test_group_constraint(self): # for OWS service only for context v and ve self.assertTrue(self._check_subset_string()) + self.assertTrue(self._check_wfs_getfeature()) def test_validate_sql(self): @@ -768,6 +816,8 @@ def test_shp_api(self): # ============================= assign_perm('view_project', get_anonymous_user(), self.qdjango_project) + for l in self.qdjango_project.layer_set.all(): + assign_perm('view_layer', get_anonymous_user(), l) rule = ConstraintExpressionRule( constraint=constraint, user=get_anonymous_user(), rule="NAME != 'ITALY'", anonymoususer=True) @@ -982,6 +1032,8 @@ def test_xls_api(self): # ----------------- assign_perm('view_project', get_anonymous_user(), self.qdjango_project) + for l in self.qdjango_project.layer_set.all(): + assign_perm('view_layer', get_anonymous_user(), l) rule = ConstraintExpressionRule( constraint=constraint, user=get_anonymous_user(), rule="NAME != 'ITALY'", anonymoususer=True) @@ -1614,6 +1666,7 @@ def test_bbox_filter(self): rule="intersects_bbox( $geometry, geom_from_wkt( 'POLYGON((8 51, 11 51, 11 52, 11 52, 8 51))') )") rule.save() self.assertFalse(self._check_subset_string()) + self.assertFalse(self._check_wfs_getfeature()) rule.delete() @@ -1773,12 +1826,12 @@ def test_geoconstraint_filter(self): constraint.save() # assign permissions - assign_perm('view_project', self.test_viewer1, self.qdjango_project) - assign_perm('view_project', self.test_viewer1_3, self.qdjango_project) - assign_perm('view_project', self.test_gu_viewer1, self.qdjango_project) - # also to Anonymous user - assign_perm('view_project', get_anonymous_user(), self.qdjango_project) + for u in (self.test_viewer1, self.test_viewer1_3, self.test_gu_viewer1, get_anonymous_user()): + assign_perm('view_project', u, self.qdjango_project) + for l in self.qdjango_project.layer_set.all(): + assign_perm("view_layer", u, l) + ows_url = reverse('OWS:ows', kwargs={'group_slug': self.qdjango_project.group.slug, 'project_type': 'qdjango', 'project_id': self.qdjango_project.id}) diff --git a/g3w-admin/qdjango/tests/test_ows.py b/g3w-admin/qdjango/tests/test_ows.py index a0071dd11..b3ee298ed 100644 --- a/g3w-admin/qdjango/tests/test_ows.py +++ b/g3w-admin/qdjango/tests/test_ows.py @@ -50,12 +50,19 @@ class OwsTest(QdjangoTestBase): def setUpTestData(cls): super().setUpTestData() - cls.qdjango_project = Project( - qgis_file=cls.project.qgisProjectFile, - title='Test qdjango project', - group=cls.project_group, - ) - cls.qdjango_project.save() + #cls.qdjango_project = Project( + # qgis_file=cls.project.qgisProjectFile, + # title='Test qdjango project', + # group=cls.project_group, + #) + #cls.qdjango_project.save() + + cls.project2 = QgisProject(cls.project.qgisProjectFile) + cls.project2.title = "Test qdjango project" + cls.project2.group = cls.project_group + cls.project2.save() + + cls.qdjango_project = cls.project2.instance qgis_project_file_widget = File(open('{}{}{}'.format( CURRENT_PATH, TEST_BASE_PATH, QGS310_WIDGET_FILE), 'r')) @@ -174,6 +181,8 @@ def test_authorizzer(self): # give permission to user assign_perm('view_project', self.test_viewer1, self.qdjango_project) + for l in self.qdjango_project.layer_set.all(): + assign_perm("view_layer", self.test_viewer1, l) response = c.get(ows_url, { 'REQUEST': 'GetCapabilities', @@ -188,7 +197,7 @@ def test_authorizzer(self): # try basic authentication # for viewer1 c = Client(HTTP_AUTHORIZATION='Basic dmlld2VyMTp2aWV3ZXIx') - esponse = c.get(ows_url, { + response = c.get(ows_url, { 'REQUEST': 'GetCapabilities', 'SERVICE': 'WMS' }) @@ -196,6 +205,51 @@ def test_authorizzer(self): self.assertEqual(response.status_code, 200) self.assertTrue(b'bluemarble' in response.content) + # Filter layer by user + for l in self.qdjango_project.layer_set.filter(name__in=['bluemarble', 'world']): + remove_perm("view_layer", self.test_viewer1, l) + + response = c.get(ows_url, { + "REQUEST": "GetCapabilities", + "SERVICE": "WMS" + }) + + self.assertEqual(response.status_code, 200) + self.assertFalse(b'bluemarble' in response.content) + self.assertFalse(b"world" in response.content) + self.assertTrue(b"spatialite_points" in response.content) + + # For WFS + response = c.get(ows_url, { + "REQUEST": "GetCapabilities", + "SERVICE": "WFS", + "VERSION": "1.1.0", + "TYPENAME": "world" + }) + + self.assertEqual(response.status_code, 200) + self.assertFalse(b"world" in response.content) + self.assertTrue(b"spatialite_points" in response.content) + + response = c.get(ows_url, { + "REQUEST": "GetCapabilities", + "SERVICE": "WFS" + }) + + self.assertEqual(response.status_code, 200) + + for l in self.qdjango_project.layer_set.filter(name='world'): + assign_perm("view_layer", self.test_viewer1, l) + + response = c.get(ows_url, { + "REQUEST": "GetCapabilities", + "SERVICE": "WFS" + }) + + self.assertEqual(response.status_code, 200) + self.assertTrue(b"world" in response.content) + + def test_get_getfeatureinfo(self): """Test GetFeatureInfo for QGIS widget""" diff --git a/g3w-admin/qdjango/tests/test_utils.py b/g3w-admin/qdjango/tests/test_utils.py index 2038864c4..7cd8b594e 100644 --- a/g3w-admin/qdjango/tests/test_utils.py +++ b/g3w-admin/qdjango/tests/test_utils.py @@ -136,7 +136,7 @@ def test_qgis_project(self): # check layerRelations # ------------------------------------------ - layer_relations_to_check = '[{"referencingLayer": "cities10000eu20171228095720113", "strength": "Association", "referencedLayer": "countries_simpl20171228095706310", "name": "countries-citites", "id": "cities1000_ISO2_CODE_countries__ISOCODE", "fieldRef": {"referencingField": "ISO2_CODE", "referencedField": "ISOCODE"}}]' + layer_relations_to_check = '[{"referencingLayer": "cities10000eu20171228095720113", "strength": "Association", "referencedLayer": "countries_simpl20171228095706310", "name": "countries-citites", "id": "cities1000_ISO2_CODE_countries__ISOCODE", "fieldRef": {"referencingField": ["ISO2_CODE"], "referencedField": ["ISOCODE"]}}]' self.assertEqual(self.project.layerRelations, json.loads(layer_relations_to_check)) diff --git a/g3w-admin/qdjango/utils/data.py b/g3w-admin/qdjango/utils/data.py index f0315f61c..64c3e7ca0 100644 --- a/g3w-admin/qdjango/utils/data.py +++ b/g3w-admin/qdjango/utils/data.py @@ -1253,13 +1253,15 @@ def _getDataLayerRelations(self): 'referencingLayer': relation.referencingLayerId(), } # get only first pair relation - field_refs = [] + fields_referenging = [] + fields_referenced = [] for referencingField, referencedField in relation.fieldPairs().items(): - field_refs.append([referencingField, referencedField]) + fields_referenging.append(referencingField) + fields_referenced.append(referencedField) attrib.update({ 'fieldRef': { - 'referencingField': field_refs[0][0], - 'referencedField': field_refs[0][1] + 'referencingField': fields_referenging, + 'referencedField': fields_referenced } }) diff --git a/g3w-admin/qdjango/vector.py b/g3w-admin/qdjango/vector.py index 0b13496b9..9e47f9d30 100644 --- a/g3w-admin/qdjango/vector.py +++ b/g3w-admin/qdjango/vector.py @@ -144,7 +144,7 @@ def set_metadata_relations(self, request, **kwargs): # qgis_layer is the referenced layer qgis_layer = self.layer.qgis_layer referenced_field_is_pk = [qgis_layer.fields().indexFromName( - relation['fieldRef']['referencedField'])] == qgis_layer.primaryKeyAttributes() + rf) for rf in relation['fieldRef']['referencedField']] == qgis_layer.primaryKeyAttributes() # It's an old and buggy QGIS version so we cannot trust primaryKeyAttributes() and we go guessing if IS_QGIS_3_10: