From 4b37e884945481a5810fe94adc52ed99b8e1e633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Mon, 10 Sep 2018 10:39:04 +0200 Subject: [PATCH] Be able to run QGIS server in multi project mode --- doc/integrator/backend_qgis.rst | 58 +++++--- .../geomapfish_plugin/accesscontrol.py | 131 ++++++++++++++---- .../scaffolds/create/vars.yaml_tmpl | 6 +- 3 files changed, 149 insertions(+), 46 deletions(-) diff --git a/doc/integrator/backend_qgis.rst b/doc/integrator/backend_qgis.rst index ddc6f4219c..4fb7049a79 100644 --- a/doc/integrator/backend_qgis.rst +++ b/doc/integrator/backend_qgis.rst @@ -32,20 +32,22 @@ QGIS Desktop configuration OWS configuration ***************** -You should setup your OWS service in the QGIS project properties in the OWS -tab. +You should setup your OWS service in the QGIS project properties in the OWS tab. -You should take care to **uncheck** the checkbox *User Layer ids as names*. If -this checkbox is enabled you will have unreliable layers name. +You should **check** *Service Capabilities* and **fill** *Online resource* to correctly generate +the URL's in the capabilities. -You should **check** the checkbox *Add geometry to feature response* in order -to make the WMS GetFeatureInfo working correctly. +In the *WMS capabilities* section, you should take care to **uncheck** the checkbox *User layer ids as +names*. If this checkbox is enabled you will have unreliable layers name. -In the *WFS capabilities* section, you should check the layers that need to be -available in the WFS service. +In the *WMS capabilities* section, you should **check** the checkbox *Add geometry to feature response* in +order to make the WMS GetFeatureInfo working correctly. -Finally, your QGIS project layers CRS should all be in the same CSR. This is subject to -change. +In the *WFS capabilities* section, you should check the layers that need to be available in the WFS service. +Also take care on the Update Insert delete column to make the layer available throw the WFS transactional +API. + +Finally, your QGIS project layers CRS should all be in the same CSR. This is subject to change. Connect to Postgres database **************************** @@ -53,8 +55,7 @@ Connect to Postgres database This section is subject to change when the QGIS plugin is available. The way you should connect QGIS to the database is based on an external file -called ``pg_service.conf`` located in the home directory. The content of this file -is as follows: +called ``pg_service.conf`` located in the home directory. The content of this file is as follows: .. code:: @@ -79,8 +80,7 @@ You can have several sections. A section start with a name with [] and finish with a blank line. This file should be a unix file. On QGIS desktop, when creating a new PostGIS connection, give it a name and use -the service name (``geomapfish`` in our example) in the connection parameters -form. +the service name (``geomapfish`` in our example) in the connection parameters form. Copy-past this file in the server, change the parameters to fit with the server settings and add the variable environment setting in the Apache config: @@ -93,8 +93,8 @@ settings and add the variable environment setting in the Apache config: Don't forget to restart Apache. -Docker -****** +Run the client with Docker +************************** Create a tunnel to the database: @@ -154,3 +154,29 @@ and he should be configured as follow: * Authentication type: ``Standard auth`` * WFS support: recommended to be ``[X]`` * Is single tile: recommended to be ``[ ]`` + +Access Restriction +****************** + +The access restriction is available only for Docker projects + +We provide an Docker image named ``camptocamp/geomapfish-qgisserver`` with tag pattern: +``gmf: + ogc_server: + +```` should have exactly the same value a the ``MAP`` parameter in the ``Base URL`` +vavalue of the OGC sever. + +And finally you should provide the ``GEOMAPFISH_ACCESSCONTROL_CONFIG`` to point to config file e.-g. +``/etc/qgisserver/accesscontrol_config.yaml``, and ``QGIS_PROJECT_FILE`` to be empty. diff --git a/docker/qgisserver/geomapfish_plugin/accesscontrol.py b/docker/qgisserver/geomapfish_plugin/accesscontrol.py index 1ff2680621..234d6f5d58 100644 --- a/docker/qgisserver/geomapfish_plugin/accesscontrol.py +++ b/docker/qgisserver/geomapfish_plugin/accesscontrol.py @@ -10,23 +10,23 @@ """ +import c2cwsgiutils.broadcast import geoalchemy2 import json import os -from shapely import ops import sqlalchemy -from sqlalchemy.orm import configure_mappers, scoped_session, sessionmaker import sys -from threading import Lock import traceback +import yaml import zope.event.classhandler -from enum import Enum +from c2cgeoportal_commons.config import config +from enum import Enum from qgis.core import QgsMessageLog, QgsDataSourceUri, QgsProject from qgis.server import QgsAccessControlFilter - -import c2cwsgiutils.broadcast -from c2cgeoportal_commons.config import config +from shapely import ops +from sqlalchemy.orm import configure_mappers, scoped_session, sessionmaker +from threading import Lock class GMFException(Exception): @@ -41,11 +41,102 @@ class Access(Enum): class GeoMapFishAccessControl(QgsAccessControlFilter): + + def __init__(self, server_iface): + super().__init__(server_iface) + + self.server_iface = server_iface + + try: + config.init(os.environ.get('GEOMAPFISH_CONFIG', '/etc/qgisserver/geomapfish.yaml')) + self.srid = config.get('srid') + + c2cwsgiutils.broadcast.init(None) + + configure_mappers() + engine = sqlalchemy.create_engine(config.get('sqlalchemy_slave.url')) + session_factory = sessionmaker() + session_factory.configure(bind=engine) + self.DBSession = scoped_session(session_factory) + + if "GEOMAPFISH_OGCSERVER" in os.environ: + self.single = True + self.ogcserver_accesscontrol = OGCServerAccessControl( + server_iface, os.environ['GEOMAPFISH_OGCSERVER'] + ) + + QgsMessageLog.logMessage("Use OGC server named '{}'.".format( + os.environ["GEOMAPFISH_OGCSERVER"] + )) + elif "GEOMAPFISH_ACCESSCONTROL_CONFIG" in os.environ: + self.single = False + self.ogcserver_accesscontrols = {} + with open(os.environ["GEOMAPFISH_ACCESSCONTROL_CONFIG"]) as ac_config_file: + ac_config = yaml.load(ac_config_file.read()) + + for map, map_config in ac_config.get("map_config").items(): + map_config["access_control"] = OGCServerAccessControl( + server_iface, map_config["ogc_server"] + ) + self.ogcserver_accesscontrols[map] = map_config + QgsMessageLog.logMessage("Use config '{}'.".format( + os.environ["GEOMAPFISH_ACCESSCONTROL_CONFIG"] + )) + else: + raise GMFException( + "The environment variable 'GEOMAPFISH_OGCSERVER' or " + "'GEOMAPFISH_ACCESSCONTROL_CONFIG' is not defined." + ) + + except Exception: + QgsMessageLog.logMessage(''.join(traceback.format_exception(*sys.exc_info()))) + raise + + server_iface.registerAccessControl(self, int(os.environ.get("GEOMAPFISH_POSITION", 100))) + + def get_ogcserver_accesscontrol_config(self): + if self.single: + raise GMFException( + "The method 'get_ogcserver_accesscontrol_config' can't be called on 'single' server" + ) + + def get_ogcserver_accesscontrol(self): + if self.single: + return self.ogcserver_accesscontrol + else: + parameters = self.serverInterface().requestHandler().parameterMap() + return self.ogcserver_accesscontrols[parameters["MAP"]]["access_control"] + + def layerFilterSubsetString(self, layer): # NOQA + """ Return an additional subset string (typically SQL) filter """ + return self.get_ogcserver_accesscontrol().layerFilterSubsetString(layer) + + def layerFilterExpression(self, layer): # NOQA + """ Return an additional expression filter """ + return self.get_ogcserver_accesscontrol().layerFilterExpression(layer) + + def layerPermissions(self, layer): # NOQA + """ Return the layer rights """ + return self.get_ogcserver_accesscontrol().layerPermissions(layer) + + def authorizedLayerAttributes(self, layer, attributes): # NOQA + """ Return the authorised layer attributes """ + return self.get_ogcserver_accesscontrol().layauthorizedLayerAttributeserPermissions(layer, attributes) + + def allowToEdit(self, layer, feature): # NOQA + """ Are we authorise to modify the following geometry """ + return self.get_ogcserver_accesscontrol().allowToEdit(layer, feature) + + def cacheKey(self): # NOQA + return self.get_ogcserver_accesscontrol().cacheKey() + + +class OGCServerAccessControl(QgsAccessControlFilter): """ Implements GeoMapFish access restriction """ SUBSETSTRING_TYPE = ["GPKG", "PostgreSQL database with PostGIS extension"] - def __init__(self, server_iface): + def __init__(self, server_iface, ogcserver_name): super().__init__(server_iface) self.server_iface = server_iface @@ -55,16 +146,6 @@ def __init__(self, server_iface): self.project = None try: - if "GEOMAPFISH_OGCSERVER" not in os.environ: - raise GMFException("The environment variable 'GEOMAPFISH_OGCSERVER' is not defined.") - - QgsMessageLog.logMessage("Use OGC server named '{}'".format(os.environ["GEOMAPFISH_OGCSERVER"])) - - config.init(os.environ.get('GEOMAPFISH_CONFIG', '/etc/qgisserver/geomapfish.yaml')) - self.srid = config.get('srid') - - c2cwsgiutils.broadcast.init(None) - from c2cgeoportal_commons.models.main import InvalidateCacheEvent @zope.event.classhandler.handler(InvalidateCacheEvent) @@ -75,23 +156,15 @@ def handle(event: InvalidateCacheEvent): self.layers = None from c2cgeoportal_commons.models.main import OGCServer - configure_mappers() - - engine = sqlalchemy.create_engine(config.get('sqlalchemy_slave.url')) - session_factory = sessionmaker() - session_factory.configure(bind=engine) - self.DBSession = scoped_session(session_factory) self.ogcserver = self.DBSession.query(OGCServer) \ - .filter(OGCServer.name == os.environ["GEOMAPFISH_OGCSERVER"]) \ + .filter(OGCServer.name == ogcserver_name) \ .one() - except Exception as e: + except Exception: QgsMessageLog.logMessage(''.join(traceback.format_exception(*sys.exc_info()))) raise - server_iface.registerAccessControl(self, int(os.environ.get("GEOMAPFISH_POSITION", 100))) - def get_layers(self): with self.lock: from c2cgeoportal_commons.models.main import LayerWMS @@ -266,7 +339,7 @@ def layerFilterExpression(self, layer): # NOQA ) QgsMessageLog.logMessage("layerFilterExpression filter: {}".format(result)) return result - except Exception as e: + except Exception: QgsMessageLog.logMessage(''.join(traceback.format_exception(*sys.exc_info()))) raise diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl b/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl index 04e2e7e3e2..994e5ea3e6 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/vars.yaml_tmpl @@ -278,8 +278,12 @@ vars: docker_services: qgisserver: environment: + # Single QGIS project files + QGIS_PROJECT_FILE: /etc/qgisserver/project.qgz GEOMAPFISH_OGCSERVER: - + # Multiple QGIS project files + # QGIS_PROJECT_FILE: + # GEOMAPFISH_ACCESSCONTROL_CONFIG: /etc/qgisserver/accesscontrol_config.yaml # Checker configuration checker: fulltextsearch: