diff --git a/CHANGELOG.md b/CHANGELOG.md index f3f304db..e220ca04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -- Improve compatibility with Python 3.7 when exporting SLD for raster layers -- Fix network access manager not using correct timeout for layer uploads +- Add support for HTTP Basic Auth when connecting to GeoNode deployments featuring version 3.3.0 or later ## [0.9.3] - 2022-01-20 diff --git a/docs/images/user_guide/basic_auth_authentication_example.png b/docs/images/user_guide/basic_auth_authentication_example.png new file mode 100644 index 00000000..5fc6c150 Binary files /dev/null and b/docs/images/user_guide/basic_auth_authentication_example.png differ diff --git a/docs/user-guide.md b/docs/user-guide.md index 8721084c..3f1bc54b 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -23,10 +23,10 @@ In order to add a new GeoNode connection: the GeoNode connection being created: | Parameter | Description | - | --------- | ----------- | + |---------- | ----------- | | Name | The name used by QGIS to refer to this connection | | GeoNode URL | The base URL of the GeoNode being connected to (_e.g._ ) | - | Authentication | Whether to use authentication to connect to GeoNode or not. See the [Configuring authentication](#configuring-authentication) section below for more details on how to configure authenticated access to GeoNode | + | Authentication | Whether to use authentication to connect to GeoNode or not. See the [Configure authentication](#configure-authentication) section below for more details on how to configure authenticated access to GeoNode | | Page size | How many search results per page shall be shown by QGIS. This defaults to `10` | 5. Optionally you may now click the `Test Connection` button. QGIS will then @@ -65,20 +65,59 @@ connection. Upon acceptance of this dialogue, the connection will be removed. ### Configure authentication +The plugin is able to authenticate to remote GeoNode instances by using either HTTP Basic Auth (recommended) or OAuth2 + +#### Authentication with Basic Auth + +![Basic Authentication example](images/user_guide/basic_auth_authentication_example.png) + +Using HTTP Basic Auth is the recommended authentication method, as it is easier to set up. In order to +configure Basic Auth: + +1. Open the main QGIS authentication settings dialogue by going to + `Settings -> Options...` in the main QGIS menu bar and then access the + `Authentication` section + +2. Press the `Add new authentication configuration button`. A new dialogue is + shown. In this dialogue, fill in the following details: + + | Parameter | Description | + |-----------|-------------| + | Name | The name used by QGIS to refer to the authentication configuration | + | Authentication type | Select the `Basic authentication` option from the dropdown | + | Username | Your GeoNode username | + | Password | Your GeoNode user password | + + The remaining fields can be left at their default values + +3. Now when [configuring a new GeoNode connection](#add-a-new-geonode-connection), select this newly created + authentication configuration in order to have the GeoNode connection use it + + +#### Authentication with OAuth2 + +This option is not recommended in most cases, since it involves a more advanced set up and also +requires requesting additional information from the remote GeoNode administrators. + +!!! Note + Just in case you missed it, we recommend connecting to GeoNode using + [HTTP Basic Auth](#authentication-with-basic-auth) instead + +This option may be viable when connecting to GeoNode using shared computing +resources, where you do not want to store your GeoNode user credentials locally. + !!! note - In order to be able to gain authenticated access to a GeoNode connection - you will need to request that one of the GeoNode administrators create an - **OAuth2** application and provide you with the following relevant details: + In order to be able to gain authenticated access to a GeoNode connection + via OAuth2 you will need to request that one of the GeoNode administrators + create an **OAuth2** application and provide you with the following relevant details: - _Client ID_ - _Client Secret_ ![Authentication example](images/user_guide/authentication_example.png) -The plugin is able to authenticate to remote GeoNode instances by using -OAuth2 authentication. Most OAuth2 grant types implemented in QGIS are -supported. We recommend using the `Authorization Code` grant type. In order -to configure such an authentication: +Most OAuth2 grant types implemented in QGIS are supported. We recommend using +the `Authorization Code` grant type. In order to configure such an authentication: 1. Open the main QGIS authentication settings dialogue by going to `Settings -> Options...` in the main QGIS menu bar and then access the @@ -98,7 +137,7 @@ to configure such an authentication: The remaining fields can be left at their default values -4. Now when +3Now when [configuring a new GeoNode connection](#add-a-new-geonode-connection), select this newly created authentication configuration in order to have the GeoNode connection use it @@ -292,8 +331,8 @@ different actions. These are classified as a set of capabilities. | MODIFY_LAYER_METADATA | >= 3.3.0 | Upload metadata fields of a loaded QGIS layer back to GeoNode | | LOAD_VECTOR_LAYER_STYLE | >= 3.3.0 | Load SLD style onto QGIS when loading GeoNode dataset as a QGIS vector layer | | LOAD_RASTER_LAYER_STYLE | - | Load SLD style onto QGIS when loading GeoNode dataset as QGIS raster layer | -| MODIFY_VECTOR_LAYER_STYLE | >= 3.3.0 | Upload vector layer symbology back to GeoNode | -| MODIFY_RASTER_LAYER_STYLE | >= 3.3.0 | Upload raster layer symbology back to GeoNode | +| MODIFY_VECTOR_LAYER_STYLE | >= 3.3.0 | Upload vector layer symbology back to GeoNode

**NOTE**: This functionality is currently not supported when using HTTP Basic Authentication. Check for more information | +| MODIFY_RASTER_LAYER_STYLE | >= 3.3.0 | Upload raster layer symbology back to GeoNode

**NOTE**: This functionality is currently not supported when using HTTP Basic Authentication. Check for more information | | LOAD_VECTOR_DATASET_VIA_WMS | All | Load GeoNode vector dataset as a QGIS layer via OGC WMS | | LOAD_VECTOR_DATASET_VIA_WFS | All | Load GeoNode vector dataset as a QGIS layer using via OGC WFS | | LOAD_RASTER_DATASET_VIA_WMS | All | Load GeoNode raster dataset as a QGIS layer via OGC WMS | diff --git a/src/qgis_geonode/apiclient/geonode_v3.py b/src/qgis_geonode/apiclient/geonode_v3.py index 3e6b3974..d31393e9 100644 --- a/src/qgis_geonode/apiclient/geonode_v3.py +++ b/src/qgis_geonode/apiclient/geonode_v3.py @@ -323,15 +323,44 @@ def handle_layer_upload(self, result: bool): "Could not upload layer to GeoNode" ) + def _get_sld_url(self, raw_style: typing.Dict) -> typing.Optional[str]: + auth_manager = qgis.core.QgsApplication.authManager() + auth_provider_name = auth_manager.configAuthMethodKey(self.auth_config).lower() + sld_url = raw_style.get("sld_url") + if auth_provider_name == "basic": + try: + sld_url = sld_url.replace("geoserver", "gs") + except AttributeError: + pass + return sld_url + + def _get_service_urls( + self, + raw_links: typing.Dict, + dataset_type: models.GeonodeResourceType, + ) -> typing.Dict[models.GeonodeService, str]: + result = {models.GeonodeService.OGC_WMS: _get_link(raw_links, "OGC:WMS")} + if dataset_type == models.GeonodeResourceType.VECTOR_LAYER: + result[models.GeonodeService.OGC_WFS] = _get_link(raw_links, "OGC:WFS") + elif dataset_type == models.GeonodeResourceType.RASTER_LAYER: + result[models.GeonodeService.OGC_WCS] = _get_link(raw_links, "OGC:WCS") + else: + log(f"Invalid dataset type: {dataset_type=}") + result = {} + auth_manager = qgis.core.QgsApplication.authManager() + auth_provider_name = auth_manager.configAuthMethodKey(self.auth_config).lower() + if auth_provider_name == "basic": + for service_type, retrieved_url in result.items(): + try: + result[service_type] = retrieved_url.replace("geoserver", "gs") + except AttributeError: + pass + return result + def _get_common_model_properties(self, raw_dataset: typing.Dict) -> typing.Dict: type_ = _get_resource_type(raw_dataset) raw_links = raw_dataset.get("links", []) - if type_ == models.GeonodeResourceType.VECTOR_LAYER: - service_urls = _get_vector_service_urls(raw_links) - elif type_ == models.GeonodeResourceType.RASTER_LAYER: - service_urls = _get_raster_service_urls(raw_links) - else: - service_urls = {} + service_urls = self._get_service_urls(raw_links, type_) raw_style = raw_dataset.get("default_style") or {} return { "pk": int(raw_dataset["pk"]), @@ -353,7 +382,7 @@ def _get_common_model_properties(self, raw_dataset: typing.Dict) -> typing.Dict: "keywords": [k["name"] for k in raw_dataset.get("keywords", [])], "category": (raw_dataset.get("category") or {}).get("identifier"), "default_style": models.BriefGeonodeStyle( - name=raw_style.get("name", ""), sld_url=raw_style.get("sld_url") + name=raw_style.get("name", ""), sld_url=self._get_sld_url(raw_style) ), } @@ -421,18 +450,48 @@ def build_search_query( query.removeQueryItem(subtype_key) return query + def _get_sld_url(self, raw_style: typing.Dict) -> typing.Optional[str]: + auth_manager = qgis.core.QgsApplication.authManager() + auth_provider_name = auth_manager.configAuthMethodKey(self.auth_config).lower() + sld_url = raw_style.get("sld_url") + if auth_provider_name == "basic": + try: + sld_url = sld_url.replace("geoserver", "gs") + except AttributeError: + pass + return sld_url + + def _get_service_urls( + self, + raw_dataset: typing.Dict, + dataset_type: models.GeonodeResourceType, + ) -> typing.Dict[models.GeonodeService, str]: + result = { + models.GeonodeService.OGC_WMS: raw_dataset["ows_url"], + } + if dataset_type == models.GeonodeResourceType.VECTOR_LAYER: + result[models.GeonodeService.OGC_WFS] = raw_dataset["ows_url"] + elif dataset_type == models.GeonodeResourceType.RASTER_LAYER: + result[models.GeonodeService.OGC_WCS] = raw_dataset["ows_url"] + else: + log(f"Invalid dataset type: {dataset_type=}") + result = {} + auth_manager = qgis.core.QgsApplication.authManager() + auth_provider_name = auth_manager.configAuthMethodKey(self.auth_config).lower() + if auth_provider_name == "basic": + for service_type, retrieved_url in result.items(): + try: + result[service_type] = retrieved_url.replace("geoserver", "gs") + except AttributeError: + pass + return result + def _get_common_model_properties(self, raw_dataset: typing.Dict) -> typing.Dict: type_ = { "coverageStore": models.GeonodeResourceType.RASTER_LAYER, "dataStore": models.GeonodeResourceType.VECTOR_LAYER, }.get(raw_dataset.get("storeType")) - service_urls = { - models.GeonodeService.OGC_WMS: raw_dataset["ows_url"], - } - if type_ == models.GeonodeResourceType.VECTOR_LAYER: - service_urls[models.GeonodeService.OGC_WFS] = raw_dataset["ows_url"] - elif type_ == models.GeonodeResourceType.RASTER_LAYER: - service_urls[models.GeonodeService.OGC_WCS] = raw_dataset["ows_url"] + service_urls = self._get_service_urls(raw_dataset, type_) raw_style = raw_dataset.get("default_style") or {} return { "pk": int(raw_dataset["pk"]), @@ -452,7 +511,7 @@ def _get_common_model_properties(self, raw_dataset: typing.Dict) -> typing.Dict: "keywords": [k["name"] for k in raw_dataset.get("keywords", [])], "category": (raw_dataset.get("category") or {}).get("identifier"), "default_style": models.BriefGeonodeStyle( - name=raw_style.get("name", ""), sld_url=raw_style.get("sld_url") + name=raw_style.get("name", ""), sld_url=self._get_sld_url(raw_style) ), } @@ -795,24 +854,6 @@ def _get_resource_type( return result -def _get_vector_service_urls( - raw_links: typing.Dict, -) -> typing.Dict[models.GeonodeService, str]: - return { - models.GeonodeService.OGC_WMS: _get_link(raw_links, "OGC:WMS"), - models.GeonodeService.OGC_WFS: _get_link(raw_links, "OGC:WFS"), - } - - -def _get_raster_service_urls( - raw_links: typing.Dict, -) -> typing.Dict[models.GeonodeService, str]: - return { - models.GeonodeService.OGC_WMS: _get_link(raw_links, "OGC:WMS"), - models.GeonodeService.OGC_WCS: _get_link(raw_links, "OGC:WCS"), - } - - def _get_spatial_extent( geojson_polygon_geometry: typing.Dict, ) -> qgis.core.QgsRectangle: