Server: add QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS settings var

This new feature allows to specify extra tokens allowed for
WMS FILTER definition.

The current list of accepted tokens is rather small and
this setting will allow the sysadmins to enlarge the list
of allowed tokens.
This commit is contained in:
Alessandro Pasotti 2022-09-07 12:52:09 +02:00
parent 1d9495b8a9
commit 2beed8345c
9 changed files with 160 additions and 3 deletions

View File

@ -136,6 +136,13 @@ Returns the service registry
%End
virtual void reloadSettings() = 0;
%Docstring
Reloads the server settings re-reading the configuration.
.. versionadded:: 3.28
%End
private:
QgsServerInterface();
};

View File

@ -61,6 +61,7 @@ Provides some enum describing the environment currently supported for configurat
QGIS_SERVER_LANDING_PAGE_PREFIX,
QGIS_SERVER_PROJECT_CACHE_CHECK_INTERVAL,
QGIS_SERVER_PROJECT_CACHE_STRATEGY,
QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS,
};
};
@ -317,6 +318,16 @@ Possible values are:
- 'off': Disable completely internal project's cache handling
.. versionadded:: 3.26
%End
QStringList allowedExtraSqlTokens() const;
%Docstring
Returns the list of strings that represent the allowed extra SQL tokens
accepted as componenst of a feature filter.
The default value is an empty string, the value can be changed by setting the environment
variable QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS.
.. versionadded:: 3.28
%End
static QString name( QgsServerSettingsEnv::EnvVar env );

View File

@ -170,6 +170,13 @@ class SERVER_EXPORT QgsServerInterface
*/
virtual QgsServerSettings *serverSettings() = 0 SIP_SKIP;
/**
* Reloads the server settings re-reading the configuration.
*
* \since QGIS 3.28
*/
virtual void reloadSettings() = 0;
private:
#ifdef SIP_RUN
QgsServerInterface();

View File

@ -118,3 +118,8 @@ QgsServerSettings *QgsServerInterfaceImpl::serverSettings()
{
return mServerSettings;
}
void QgsServerInterfaceImpl::reloadSettings()
{
mServerSettings->load();
}

View File

@ -87,6 +87,8 @@ class SERVER_EXPORT QgsServerInterfaceImpl : public QgsServerInterface
QgsServerSettings *serverSettings() override;
void reloadSettings() override;
private:
QString mConfigFilePath;
@ -97,6 +99,7 @@ class SERVER_EXPORT QgsServerInterfaceImpl : public QgsServerInterface
QgsRequestHandler *mRequestHandler = nullptr;
QgsServiceRegistry *mServiceRegistry = nullptr;
QgsServerSettings *mServerSettings = nullptr;
};
#endif // QGSSERVERINTERFACEIMPL_H

View File

@ -357,6 +357,16 @@ void QgsServerSettings::initSettings()
};
mSettings[ sProjectCacheStrategy.envVar ] = sProjectCacheStrategy;
const Setting sAllowedExtraSqlTokens = { QgsServerSettingsEnv::QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS,
QgsServerSettingsEnv::DEFAULT_VALUE,
QStringLiteral( "List of comma separated SQL tokens to be added to the list of allowed tokens that the services accespt when filtering features" ),
QStringLiteral( "/qgis/server_allowed_extra_sql_tokens" ),
QVariant::String,
QVariant( "" ),
QVariant()
};
mSettings[ sAllowedExtraSqlTokens.envVar ] = sAllowedExtraSqlTokens;
}
void QgsServerSettings::load()
@ -658,3 +668,8 @@ QString QgsServerSettings::projectCacheStrategy() const
return result;
}
QStringList QgsServerSettings::allowedExtraSqlTokens() const
{
return value( QgsServerSettingsEnv::QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS ).toString().split( ',' );
}

View File

@ -79,6 +79,7 @@ class SERVER_EXPORT QgsServerSettingsEnv : public QObject
QGIS_SERVER_LANDING_PAGE_PREFIX, //! Prefix of the path component of the landing page base URL, default is empty (since QGIS 3.20).
QGIS_SERVER_PROJECT_CACHE_CHECK_INTERVAL, //! Set the interval for cache invalidation strategy 'interval', default to 0 which select the legacy File system watcher (since QGIS 3.26).
QGIS_SERVER_PROJECT_CACHE_STRATEGY, //! Set the project cache strategy. Possible values are 'filesystem', 'periodic' or 'off' (since QGIS 3.26).
QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS, //! Adds these tokens to the list of allowed tokens that the services accespt when filtering features (since QGIS 3.28).
};
Q_ENUM( EnvVar )
};
@ -317,6 +318,16 @@ class SERVER_EXPORT QgsServerSettings
*/
QString projectCacheStrategy() const;
/**
* Returns the list of strings that represent the allowed extra SQL tokens
* accepted as componenst of a feature filter.
* The default value is an empty string, the value can be changed by setting the environment
* variable QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS.
*
* \since QGIS 3.28
*/
QStringList allowedExtraSqlTokens() const;
/**
* Returns the string representation of a setting.
* \since QGIS 3.16

View File

@ -2172,7 +2172,8 @@ namespace QgsWms
|| tokenIt->compare( QLatin1String( "LIKE" ), Qt::CaseInsensitive ) == 0
|| tokenIt->compare( QLatin1String( "ILIKE" ), Qt::CaseInsensitive ) == 0
|| tokenIt->compare( QLatin1String( "DMETAPHONE" ), Qt::CaseInsensitive ) == 0
|| tokenIt->compare( QLatin1String( "SOUNDEX" ), Qt::CaseInsensitive ) == 0 )
|| tokenIt->compare( QLatin1String( "SOUNDEX" ), Qt::CaseInsensitive ) == 0
|| mContext.settings().allowedExtraSqlTokens().contains( *tokenIt, Qt::CaseSensitivity::CaseInsensitive ) )
{
continue;
}
@ -3127,6 +3128,13 @@ namespace QgsWms
void QgsRenderer::setLayerFilter( QgsMapLayer *layer, const QList<QgsWmsParametersFilter> &filters )
{
// quick exit if no filters are set
if ( filters.isEmpty() )
{
return;
}
if ( layer->type() == QgsMapLayerType::VectorLayer )
{
QgsVectorLayer *filteredLayer = qobject_cast<QgsVectorLayer *>( layer );
@ -3161,9 +3169,11 @@ namespace QgsWms
" Note: Text strings have to be enclosed in single or double quotes."
" A space between each word / special character is mandatory."
" Allowed Keywords and special characters are "
" IS,NOT,NULL,AND,OR,IN,=,<,>=,>,>=,!=,',',(,),DMETAPHONE,SOUNDEX."
" IS,NOT,NULL,AND,OR,IN,=,<,>=,>,>=,!=,',',(,),DMETAPHONE,SOUNDEX%2."
" Not allowed are semicolons in the filter expression." ).arg(
filter.mFilter ) );
filter.mFilter, mContext.settings().allowedExtraSqlTokens().isEmpty() ?
QString() :
mContext.settings().allowedExtraSqlTokens().join( ',' ).prepend( ',' ) ) );
}
QString newSubsetString = filter.mFilter;

View File

@ -34,6 +34,7 @@ import osgeo.gdal # NOQA
from test_qgsserver_wms import TestQgsServerWMSTestBase
from qgis.core import QgsProject
from qgis.server import QgsBufferServerRequest, QgsBufferServerResponse
class TestQgsServerWMSGetFeatureInfo(TestQgsServerWMSTestBase):
@ -41,6 +42,10 @@ class TestQgsServerWMSGetFeatureInfo(TestQgsServerWMSTestBase):
# regenerate_reference = True
def tearDown(self):
super().tearDown()
os.environ.putenv('QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS', '')
def testGetFeatureInfo(self):
# Test getfeatureinfo response xml
self.wms_request_compare('GetFeatureInfo',
@ -861,6 +866,89 @@ class TestQgsServerWMSGetFeatureInfo(TestQgsServerWMSTestBase):
self.assertEqual(response_body.decode('utf8'), '<ServiceExceptionReport xmlns="http://www.opengis.net/ogc" version="1.3.0">\n <ServiceException code="InvalidParameterValue">Filter not valid for layer testlayer èé: check the filter syntax and the field names.</ServiceException>\n</ServiceExceptionReport>\n')
def testGetFeatureInfoFilterAllowedExtraTokens(self):
"""Test GetFeatureInfo with forbidden and extra tokens
set by QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS
"""
project_path = self.testdata_path + "test_project_values.qgz"
project = QgsProject()
self.assertTrue(project.read(project_path))
req_params = {
'SERVICE': 'WMS',
'REQUEST': 'GetFeatureInfo',
'VERSION': '1.3.0',
'LAYERS': 'layer4',
'STYLES': '',
'INFO_FORMAT': r'application%2Fjson',
'WIDTH': '926',
'HEIGHT': '787',
'SRS': r'EPSG%3A4326',
'BBOX': '912217,5605059,914099,5606652',
'CRS': 'EPSG:3857',
'FEATURE_COUNT': '10',
'QUERY_LAYERS': 'layer4',
'FILTER': 'layer4:"utf8nameè" != \'\'',
}
req = QgsBufferServerRequest('?' + '&'.join(["%s=%s" % (k, v) for k, v in req_params.items()]))
res = QgsBufferServerResponse()
self.server.handleRequest(req, res, project)
j_body = json.loads(bytes(res.body()).decode())
self.assertEqual(len(j_body['features']), 3)
req_params['FILTER'] = 'layer4:"utf8nameè" = \'three èé↓\''
req = QgsBufferServerRequest('?' + '&'.join(["%s=%s" % (k, v) for k, v in req_params.items()]))
res = QgsBufferServerResponse()
self.server.handleRequest(req, res, project)
j_body = json.loads(bytes(res.body()).decode())
self.assertEqual(len(j_body['features']), 1)
req_params['FILTER'] = 'layer4:"utf8nameè" != \'three èé↓\''
req = QgsBufferServerRequest('?' + '&'.join(["%s=%s" % (k, v) for k, v in req_params.items()]))
res = QgsBufferServerResponse()
self.server.handleRequest(req, res, project)
j_body = json.loads(bytes(res.body()).decode())
self.assertEqual(len(j_body['features']), 2)
# REPLACE filter
req_params['FILTER'] = 'layer4:REPLACE ( "utf8nameè" , \'three\' , \'____\' ) != \'____ èé↓\''
req = QgsBufferServerRequest('?' + '&'.join(["%s=%s" % (k, v) for k, v in req_params.items()]))
res = QgsBufferServerResponse()
self.server.handleRequest(req, res, project)
self.assertEqual(res.statusCode(), 403)
os.environ.putenv('QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS', 'RePlAcE')
self.server.serverInterface().reloadSettings()
req = QgsBufferServerRequest('?' + '&'.join(["%s=%s" % (k, v) for k, v in req_params.items()]))
res = QgsBufferServerResponse()
self.server.handleRequest(req, res, project)
j_body = json.loads(bytes(res.body()).decode())
self.assertEqual(len(j_body['features']), 2)
os.environ.putenv('QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS', '')
self.server.serverInterface().reloadSettings()
req_params['FILTER'] = 'layer4:REPLACE ( "utf8nameè" , \'three\' , \'____\' ) != \'____ èé↓\''
req = QgsBufferServerRequest('?' + '&'.join(["%s=%s" % (k, v) for k, v in req_params.items()]))
res = QgsBufferServerResponse()
self.server.handleRequest(req, res, project)
self.assertEqual(res.statusCode(), 403)
# Multiple filters
os.environ.putenv('QGIS_SERVER_ALLOWED_EXTRA_SQL_TOKENS', 'RePlAcE,LowEr')
self.server.serverInterface().reloadSettings()
req_params['FILTER'] = 'layer4:LOWER ( REPLACE ( "utf8nameè" , \'three\' , \'THREE\' ) ) = \'three èé↓\''
req = QgsBufferServerRequest('?' + '&'.join(["%s=%s" % (k, v) for k, v in req_params.items()]))
res = QgsBufferServerResponse()
self.server.handleRequest(req, res, project)
j_body = json.loads(bytes(res.body()).decode())
self.assertEqual(len(j_body['features']), 1)
def testGetFeatureInfoSortedByDesignerWithJoinLayer(self):
"""Test GetFeatureInfo resolves DRAG&DROP Designer order when use attribute form settings for GetFeatureInfo
with a column from a Joined Layer when the option is checked, see https://github.com/qgis/QGIS/pull/41031