Skip to content

Sql view params #146

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

Merged
merged 3 commits into from
Apr 12, 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
51 changes: 48 additions & 3 deletions geo/Geoserver.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# inbuilt libraries
import os
from typing import List, Optional, Set, Union
from typing import List, Optional, Set, Union, Dict, Iterable
from pathlib import Path

# third-party libraries
Expand All @@ -10,7 +10,7 @@
# custom functions
from .Calculation_gdal import raster_value
from .Style import catagorize_xml, classified_xml, coverage_style_xml, outline_only_xml
from .supports import prepare_zip_file, is_valid_xml
from .supports import prepare_zip_file, is_valid_xml, is_surrounded_by_quotes


# Custom exceptions.
Expand Down Expand Up @@ -2025,25 +2025,48 @@ def publish_featurestore_sqlview(
name: str,
store_name: str,
sql: str,
parameters: Optional[Iterable[Dict]] = None,
key_column: Optional[str] = None,
geom_name: str = "geom",
geom_type: str = "Geometry",
srid: Optional[int] = 4326,
workspace: Optional[str] = None,
):
"""
Publishes an SQL query as a layer, optionally with parameters

Parameters
----------
name : str
store_name : str
sql : str
parameters : iterable of dicts, optional
key_column : str, optional
geom_name : str, optional
geom_type : str, optional
workspace : str, optional

Notes
-----
With regards to SQL view parameters, it is advised to read the relevant section from the geoserver docs:
https://docs.geoserver.org/main/en/user/data/database/sqlview.html#parameterizing-sql-views

An integer-based parameter must have a default value

You should be VERY careful with the `regexp_validator`, as it can open you to SQL injection attacks. If you do
not supply one for a parameter, it will use the geoserver default `^[\w\d\s]+$`.

The `parameters` iterable must contain dictionaries with this structure:

```
{
"name": "<name of parameter (required)>"
"rexegpValidator": "<string containing regex validator> (optional)"
"defaultValue" : "<default value of parameter if not specified (required only for non-string parameters)>"
}
```
"""

if workspace is None:
workspace = "default"

Expand All @@ -2054,6 +2077,27 @@ def publish_featurestore_sqlview(
else:
key_column_xml = """"""

parameters_xml = ""
if parameters is not None:
for parameter in parameters:

# non-string parameters MUST have a default value supplied
if not is_surrounded_by_quotes(sql, parameter["name"]) and not "defaultValue" in parameter:
raise ValueError(f"Parameter `{parameter['name']}` appears to be a non-string in the supplied query"
", but does not have a default value specified. You must supply a default value "
"for non-string parameters using the `defaultValue` key.")

param_name = parameter.get("name", "")
default_value = parameter.get("defaultValue", "")
regexp_validator = parameter.get("regexpValidator", r"^[\w\d\s]+$")
parameters_xml += (f"""
<parameter>
<name>{param_name}</name>
<defaultValue>{default_value}</defaultValue>
<regexpValidator>{regexp_validator}</regexpValidator>
</parameter>\n
""".strip())

layer_xml = """<featureType>
<name>{0}</name>
<enabled>true</enabled>
Expand All @@ -2073,11 +2117,12 @@ def publish_featurestore_sqlview(
<type>{3}</type>
<srid>{5}</srid>
</geometry>{6}
{7}
</virtualTable>
</entry>
</metadata>
</featureType>""".format(
name, sql, geom_name, geom_type, workspace, srid, key_column_xml
name, sql, geom_name, geom_type, workspace, srid, key_column_xml, parameters_xml
)

# rest API url
Expand Down
14 changes: 14 additions & 0 deletions geo/supports.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import re
from tempfile import mkstemp
from typing import Dict
from zipfile import ZipFile
Expand Down Expand Up @@ -60,3 +61,16 @@ def is_valid_xml(xml_string: str) -> bool:
return True
except ET.ParseError:
return False


def is_surrounded_by_quotes(text, param):
# The regex pattern searches for '%foo%' surrounded by single quotes.
# It uses \'%foo%\' to match '%foo%' literally, including the single quotes.
pattern = rf"\'%{param}%\'"

# re.search() searches the string for the first location where the regex pattern produces a match.
# If a match is found, re.search() returns a match object. Otherwise, it returns None.
match = re.search(pattern, text)

# Return True if a match is found, False otherwise.
return bool(match)
4 changes: 3 additions & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ flake8
sphinx>=1.7
pre-commit
environs
ddt
ddt
sqlalchemy>=2.0.29
psycopg2>=2.9.9
19 changes: 17 additions & 2 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

from geo.Geoserver import Geoserver

GEO_URL = os.getenv("GEO_URL", "http://localhost:8080/geoserver")
GEO_URL = os.getenv("GEO_URL", "http://localhost:8080/geoserver") # relative to test machine

geo = Geoserver(GEO_URL, username="admin", password="geoserver")
geo = Geoserver(GEO_URL, username=os.getenv("GEO_USER", "admin"), password=os.getenv("GEO_PASS", "geoserver"))

postgis_params = {
"host": os.getenv("DB_HOST", "localhost"), # relative to the geoserver instance
"port": os.getenv("DB_PORT", "5432"), # relative to the geoserver instance
"db": os.getenv("DB_NAME", "geodb"),
"pg_user": os.getenv("DB_USER", "geodb_user"),
"pg_password": os.getenv("DB_PASS", "geodb_pass")
}

# in case you are using docker or something, and the location of the database is different relative to your host machine
postgis_params_local_override = {
"host": os.getenv("DB_HOST_LOCAL", "localhost"), # relative to the test machine
"port": os.getenv("DB_PORT_LOCAL", "5432"), # relative to the test machine
}
postgis_params_local = {**postgis_params, **postgis_params_local_override}
Loading