Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 71b0977

Browse files
elpasogithub-actions[bot]
authored andcommittedMar 2, 2021
Merge pull request #41823 from elpaso/bugfix-gh41800-server-geprint-accesscontrol
Server WMS GetPrint accesscontrol support
1 parent da9a255 commit 71b0977

31 files changed

+1992
-21
lines changed
 

‎python/core/auto_generated/layout/qgslayoutrendercontext.sip.in

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,26 @@ Returns the current list of predefined scales for use with the layout.
311311
.. seealso:: :py:func:`setPredefinedScales`
312312

313313
.. versionadded:: 3.10
314+
%End
315+
316+
QgsFeatureFilterProvider *featureFilterProvider() const;
317+
%Docstring
318+
Returns the possibly NULL feature filter provider.
319+
320+
A feature filter provider for filtering visible features or attributes.
321+
It is currently used by QGIS Server Access Control Plugins.
322+
323+
.. versionadded:: 3.18
324+
%End
325+
326+
void setFeatureFilterProvider( QgsFeatureFilterProvider *featureFilterProvider );
327+
%Docstring
328+
Sets feature filter provider to ``featureFilterProvider``.
329+
330+
A feature filter provider for filtering visible features or attributes.
331+
It is currently used by QGIS Server Access Control Plugins.
332+
333+
.. versionadded:: 3.18
314334
%End
315335

316336
signals:

‎python/core/auto_generated/qgsfeaturefilterprovider.sip.in

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
class QgsFeatureFilterProvider
1717
{
1818
%Docstring
19-
Abstract interface for use by classes that filter the features of a layer.
19+
Abstract interface for use by classes that filter the features or attributes of a layer.
2020

2121
A :py:class:`QgsFeatureFilterProvider` provides a method for modifying a :py:class:`QgsFeatureRequest` in place to apply
22-
additional filters to the request.
22+
additional filters to the request, since QGIS 3.18 a method to filter allowed attributes is also available.
2323

2424
.. versionadded:: 2.14
2525
%End
@@ -37,6 +37,13 @@ Derived classes must implement this method.
3737

3838
:param layer: the layer to filter
3939
:param featureRequest: the feature request to update
40+
%End
41+
42+
virtual QStringList layerAttributes( const QgsVectorLayer *layer, const QStringList &attributes ) const = 0;
43+
%Docstring
44+
Returns the list of visible attribute names from a list of ``attributes`` names for the given ``layer``
45+
46+
.. versionadded:: 3.18
4047
%End
4148

4249
virtual QgsFeatureFilterProvider *clone() const = 0 /Factory/;

‎python/server/auto_generated/qgsaccesscontrol.sip.in

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ Returns the layer delete right
108108
:return: ``True`` if we can do a delete
109109
%End
110110

111-
QStringList layerAttributes( const QgsVectorLayer *layer, const QStringList &attributes ) const;
111+
virtual QStringList layerAttributes( const QgsVectorLayer *layer, const QStringList &attributes ) const;
112+
112113
%Docstring
113114
Returns the authorized layer attributes
114115

‎python/server/auto_generated/qgsfeaturefilter.sip.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ Filter the features of the layer
3737
:param filterFeatures: the request to fill
3838
%End
3939

40+
virtual QStringList layerAttributes( const QgsVectorLayer *layer, const QStringList &attributes ) const;
41+
42+
4043
virtual QgsFeatureFilterProvider *clone() const /Factory/;
4144

4245
%Docstring

‎python/server/auto_generated/qgsfeaturefilterprovidergroup.sip.in

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@ Constructor
3030
virtual void filterFeatures( const QgsVectorLayer *layer, QgsFeatureRequest &filterFeatures ) const;
3131

3232
%Docstring
33-
Filter the features of the layer
33+
Filter the features of the layer.
3434

3535
:param layer: the layer to control
3636
:param filterFeatures: the request to fill
3737
%End
3838

39+
virtual QStringList layerAttributes( const QgsVectorLayer *layer, const QStringList &attributes ) const;
40+
41+
3942
virtual QgsFeatureFilterProvider *clone() const /Factory/;
4043

4144
%Docstring
@@ -53,6 +56,7 @@ Add another filter provider to the group
5356
:return: itself
5457
%End
5558

59+
5660
};
5761

5862
/************************************************************************

‎src/core/layout/qgslayoutatlas.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,13 @@ int QgsLayoutAtlas::updateFeatures()
288288
req.setFilterExpression( mFilterExpression );
289289
}
290290

291+
#ifdef HAVE_SERVER_PYTHON_PLUGINS
292+
if ( mLayout->renderContext().featureFilterProvider() )
293+
{
294+
mLayout->renderContext().featureFilterProvider()->filterFeatures( mCoverageLayer.get(), req );
295+
}
296+
#endif
297+
291298
QgsFeatureIterator fit = mCoverageLayer->getFeatures( req );
292299

293300
std::unique_ptr<QgsExpression> nameExpression;

‎src/core/layout/qgslayoutitemattributetable.cpp

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "qgsexception.h"
3030
#include "qgsmapsettings.h"
3131
#include "qgsexpressioncontextutils.h"
32+
#include "qgsexpressionnodeimpl.h"
3233
#include "qgsgeometryengine.h"
3334
#include "qgsconditionalstyle.h"
3435

@@ -176,6 +177,7 @@ void QgsLayoutItemAttributeTable::resetColumns()
176177
//rebuild columns list from vector layer fields
177178
int idx = 0;
178179
const QgsFields sourceFields = source->fields();
180+
179181
for ( const auto &field : sourceFields )
180182
{
181183
QString currentAlias = source->attributeDisplayName( idx );
@@ -329,8 +331,9 @@ void QgsLayoutItemAttributeTable::setDisplayedFields( const QStringList &fields,
329331
{
330332
int attrIdx = layerFields.lookupField( field );
331333
if ( attrIdx < 0 )
334+
{
332335
continue;
333-
336+
}
334337
QString currentAlias = source->attributeDisplayName( attrIdx );
335338
QgsLayoutTableColumn col;
336339
col.setAttribute( layerFields.at( attrIdx ).name() );
@@ -413,6 +416,13 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont
413416
}
414417
}
415418

419+
#ifdef HAVE_SERVER_PYTHON_PLUGINS
420+
if ( mLayout->renderContext().featureFilterProvider() )
421+
{
422+
mLayout->renderContext().featureFilterProvider()->filterFeatures( layer, req );
423+
}
424+
#endif
425+
416426
QgsRectangle selectionRect;
417427
QgsGeometry visibleRegion;
418428
std::unique_ptr< QgsGeometryEngine > visibleMapEngine;
@@ -541,6 +551,9 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont
541551
// We also need a list of just the cell contents, so that we can do a quick check for row uniqueness (when the
542552
// corresponding option is enabled)
543553
QVector< Cell > currentRow;
554+
#ifdef HAVE_SERVER_PYTHON_PLUGINS
555+
mColumns = filteredColumns();
556+
#endif
544557
currentRow.reserve( mColumns.count() );
545558
QgsLayoutTableRow rowContents;
546559
rowContents.reserve( mColumns.count() );
@@ -693,6 +706,66 @@ QVariant QgsLayoutItemAttributeTable::replaceWrapChar( const QVariant &variant )
693706
return replaced;
694707
}
695708

709+
#ifdef HAVE_SERVER_PYTHON_PLUGINS
710+
QgsLayoutTableColumns QgsLayoutItemAttributeTable::filteredColumns()
711+
{
712+
QgsLayoutTableColumns allowedColumns { mColumns };
713+
714+
QgsVectorLayer *source { sourceLayer() };
715+
716+
if ( ! source )
717+
{
718+
return allowedColumns;
719+
}
720+
721+
QHash<const QString, QSet<QString>> columnAttributesMap;
722+
QSet<QString> allowedAttributes;
723+
724+
for ( const auto &c : qgis::as_const( allowedColumns ) )
725+
{
726+
if ( ! c.attribute().isEmpty() && ! columnAttributesMap.contains( c.attribute() ) )
727+
{
728+
columnAttributesMap[ c.attribute() ] = QSet<QString>();
729+
const QgsExpression columnExp { c.attribute() };
730+
const auto constRefs { columnExp.findNodes<QgsExpressionNodeColumnRef>() };
731+
for ( const auto &cref : constRefs )
732+
{
733+
columnAttributesMap[ c.attribute() ].insert( cref->name() );
734+
allowedAttributes.insert( cref->name() );
735+
}
736+
}
737+
}
738+
739+
if ( mLayout->renderContext().featureFilterProvider() )
740+
{
741+
const QStringList filteredAttributes { layout()->renderContext().featureFilterProvider()->layerAttributes( source, allowedAttributes.values() ) };
742+
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
743+
const QSet<QString> filteredAttributesSet( filteredAttributes.constBegin(), filteredAttributes.constEnd() );
744+
#else
745+
const QSet<QString> filteredAttributesSet { filteredAttributes.toSet() };
746+
#endif
747+
if ( filteredAttributesSet != allowedAttributes )
748+
{
749+
const auto forbidden { allowedAttributes.subtract( filteredAttributesSet ) };
750+
allowedColumns.erase( std::remove_if( allowedColumns.begin(), allowedColumns.end(), [ &columnAttributesMap, &forbidden ]( QgsLayoutTableColumn & c ) -> bool
751+
{
752+
for ( const auto &f : qgis::as_const( forbidden ) )
753+
{
754+
if ( columnAttributesMap[ c.attribute() ].contains( f ) )
755+
{
756+
return true;
757+
}
758+
}
759+
return false;
760+
} ), allowedColumns.end() );
761+
762+
}
763+
}
764+
765+
return allowedColumns;
766+
}
767+
#endif
768+
696769
QgsVectorLayer *QgsLayoutItemAttributeTable::sourceLayer() const
697770
{
698771
switch ( mSource )

‎src/core/layout/qgslayoutitemattributetable.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,14 @@ class CORE_EXPORT QgsLayoutItemAttributeTable: public QgsLayoutTable
390390
*/
391391
QVariant replaceWrapChar( const QVariant &variant ) const;
392392

393+
#ifdef HAVE_SERVER_PYTHON_PLUGINS
394+
395+
/**
396+
* Returns the list of visible columns filtered by the access control filter rules.
397+
*/
398+
QgsLayoutTableColumns filteredColumns( );
399+
#endif
400+
393401
private slots:
394402
//! Checks if this vector layer will be removed (and sets mVectorLayer to 0 if yes)
395403
void removeLayer( const QString &layerId );

‎src/core/layout/qgslayoutitemmap.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,10 @@ void QgsLayoutItemMap::drawMap( QPainter *painter, const QgsRectangle &extent, Q
13461346
}
13471347

13481348
QgsMapRendererCustomPainterJob job( ms, painter );
1349+
#ifdef HAVE_SERVER_PYTHON_PLUGINS
1350+
job.setFeatureFilterProvider( mLayout->renderContext().featureFilterProvider() );
1351+
#endif
1352+
13491353
// Render the map in this thread. This is done because of problems
13501354
// with printing to printer on Windows (printing to PDF is fine though).
13511355
// Raster images were not displayed - see #10599

‎src/core/layout/qgslayoutrendercontext.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,13 @@ void QgsLayoutRenderContext::setPredefinedScales( const QVector<qreal> &scales )
133133
std::sort( mPredefinedScales.begin(), mPredefinedScales.end() ); // clazy:exclude=detaching-member
134134
emit predefinedScalesChanged();
135135
}
136+
137+
QgsFeatureFilterProvider *QgsLayoutRenderContext::featureFilterProvider() const
138+
{
139+
return mFeatureFilterProvider;
140+
}
141+
142+
void QgsLayoutRenderContext::setFeatureFilterProvider( QgsFeatureFilterProvider *featureFilterProvider )
143+
{
144+
mFeatureFilterProvider = featureFilterProvider;
145+
}

‎src/core/layout/qgslayoutrendercontext.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,26 @@ class CORE_EXPORT QgsLayoutRenderContext : public QObject
297297
*/
298298
QVector<qreal> predefinedScales() const { return mPredefinedScales; }
299299

300+
/**
301+
* Returns the possibly NULL feature filter provider.
302+
*
303+
* A feature filter provider for filtering visible features or attributes.
304+
* It is currently used by QGIS Server Access Control Plugins.
305+
*
306+
* \since QGIS 3.18
307+
*/
308+
QgsFeatureFilterProvider *featureFilterProvider() const;
309+
310+
/**
311+
* Sets feature filter provider to \a featureFilterProvider.
312+
*
313+
* A feature filter provider for filtering visible features or attributes.
314+
* It is currently used by QGIS Server Access Control Plugins.
315+
*
316+
* \since QGIS 3.18
317+
*/
318+
void setFeatureFilterProvider( QgsFeatureFilterProvider *featureFilterProvider );
319+
300320
signals:
301321

302322
/**
@@ -342,6 +362,8 @@ class CORE_EXPORT QgsLayoutRenderContext : public QObject
342362

343363
QVector<qreal> mPredefinedScales;
344364

365+
QgsFeatureFilterProvider *mFeatureFilterProvider = nullptr;
366+
345367
friend class QgsLayoutExporter;
346368
friend class TestQgsLayout;
347369
friend class LayoutContextPreviewSettingRestorer;

‎src/core/qgsfeaturefilterprovider.h

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#define QGSFEATUREFILTERPROVIDER_H
2020

2121
#include <QtGlobal>
22+
#include <QStringList>
2223
#include "qgis_sip.h"
2324

2425
#include "qgis_core.h"
@@ -31,10 +32,11 @@ class QgsFeatureRequest;
3132
/**
3233
* \ingroup core
3334
* \class QgsFeatureFilterProvider
34-
* \brief Abstract interface for use by classes that filter the features of a layer.
35+
* \brief Abstract interface for use by classes that filter the features or attributes of a layer.
3536
*
3637
* A QgsFeatureFilterProvider provides a method for modifying a QgsFeatureRequest in place to apply
37-
* additional filters to the request.
38+
* additional filters to the request, since QGIS 3.18 a method to filter allowed attributes is also available.
39+
*
3840
* \since QGIS 2.14
3941
*/
4042

@@ -59,6 +61,12 @@ class CORE_EXPORT QgsFeatureFilterProvider
5961
*/
6062
virtual void filterFeatures( const QgsVectorLayer *layer, QgsFeatureRequest &featureRequest ) const = 0;
6163

64+
/**
65+
* Returns the list of visible attribute names from a list of \a attributes names for the given \a layer
66+
* \since QGIS 3.18
67+
*/
68+
virtual QStringList layerAttributes( const QgsVectorLayer *layer, const QStringList &attributes ) const = 0;
69+
6270
/**
6371
* Create a clone of the feature filter provider
6472
* \returns a new clone

‎src/server/qgsaccesscontrol.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ void QgsAccessControl::filterFeatures( const QgsVectorLayer *layer, QgsFeatureRe
6464

6565
QString expression;
6666

67-
if ( mResolved && mFilterFeaturesExpressions.keys().contains( layer->id() ) )
67+
if ( mResolved && mFilterFeaturesExpressions.contains( layer->id() ) )
6868
{
6969
expression = mFilterFeaturesExpressions[layer->id()];
7070
}

‎src/server/qgsaccesscontrol.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ class SERVER_EXPORT QgsAccessControl : public QgsFeatureFilterProvider
134134
* \param attributes the list of attribute
135135
* \returns the list of visible attributes
136136
*/
137-
QStringList layerAttributes( const QgsVectorLayer *layer, const QStringList &attributes ) const;
137+
QStringList layerAttributes( const QgsVectorLayer *layer, const QStringList &attributes ) const override;
138138

139139
/**
140140
* Are we authorized to modify the following geometry
@@ -166,6 +166,7 @@ class SERVER_EXPORT QgsAccessControl : public QgsFeatureFilterProvider
166166

167167
QMap<QString, QString> mFilterFeaturesExpressions;
168168
bool mResolved;
169+
169170
};
170171

171172
#endif

‎src/server/qgsfeaturefilter.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ void QgsFeatureFilter::filterFeatures( const QgsVectorLayer *layer, QgsFeatureRe
2929
}
3030
}
3131

32+
QStringList QgsFeatureFilter::layerAttributes( const QgsVectorLayer *, const QStringList &attributes ) const
33+
{
34+
// Do nothing
35+
return attributes;
36+
}
37+
3238
QgsFeatureFilterProvider *QgsFeatureFilter::clone() const
3339
{
3440
auto result = new QgsFeatureFilter();

‎src/server/qgsfeaturefilter.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ class SERVER_EXPORT QgsFeatureFilter : public QgsFeatureFilterProvider
4444
*/
4545
void filterFeatures( const QgsVectorLayer *layer, QgsFeatureRequest &filterFeatures ) const override;
4646

47+
QStringList layerAttributes( const QgsVectorLayer *layer, const QStringList &attributes ) const override;
48+
4749
/**
4850
* Returns a clone of the object
4951
* \returns A clone
@@ -59,6 +61,7 @@ class SERVER_EXPORT QgsFeatureFilter : public QgsFeatureFilterProvider
5961

6062
private:
6163
QMap<QString, QString> mFilters;
64+
6265
};
6366

6467
#endif

‎src/server/qgsfeaturefilterprovidergroup.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121
void QgsFeatureFilterProviderGroup::filterFeatures( const QgsVectorLayer *layer, QgsFeatureRequest &filterFeatures ) const
2222
{
23-
filterFeatures.disableFilter();
2423
for ( const QgsFeatureFilterProvider *provider : mProviders )
2524
{
2625
QgsFeatureRequest temp;
@@ -32,6 +31,17 @@ void QgsFeatureFilterProviderGroup::filterFeatures( const QgsVectorLayer *layer,
3231
}
3332
}
3433

34+
QStringList QgsFeatureFilterProviderGroup::layerAttributes( const QgsVectorLayer *layer, const QStringList &attributes ) const
35+
{
36+
QStringList allowedAttributes { attributes };
37+
for ( const QgsFeatureFilterProvider *provider : mProviders )
38+
{
39+
QgsFeatureRequest temp;
40+
allowedAttributes = provider->layerAttributes( layer, allowedAttributes );
41+
}
42+
return allowedAttributes;
43+
}
44+
3545
QgsFeatureFilterProvider *QgsFeatureFilterProviderGroup::clone() const
3646
{
3747
auto result = new QgsFeatureFilterProviderGroup();

‎src/server/qgsfeaturefilterprovidergroup.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@ class SERVER_EXPORT QgsFeatureFilterProviderGroup : public QgsFeatureFilterProvi
3636
QgsFeatureFilterProviderGroup() = default;
3737

3838
/**
39-
* Filter the features of the layer
39+
* Filter the features of the layer.
4040
* \param layer the layer to control
4141
* \param filterFeatures the request to fill
4242
*/
4343
void filterFeatures( const QgsVectorLayer *layer, QgsFeatureRequest &filterFeatures ) const override;
4444

45+
QStringList layerAttributes( const QgsVectorLayer *layer, const QStringList &attributes ) const override;
46+
4547
/**
4648
* Returns a clone of the object
4749
* \returns A clone
@@ -55,8 +57,11 @@ class SERVER_EXPORT QgsFeatureFilterProviderGroup : public QgsFeatureFilterProvi
5557
*/
5658
QgsFeatureFilterProviderGroup &addProvider( const QgsFeatureFilterProvider *provider );
5759

60+
5861
private:
5962
QList<const QgsFeatureFilterProvider *> mProviders;
63+
64+
6065
};
6166

6267
#endif

‎src/server/services/wms/qgswmsgetprint.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ namespace QgsWms
6464
context.setFlag( QgsWmsRenderContext::SetAccessControl );
6565
context.setFlag( QgsWmsRenderContext::AddHighlightLayers );
6666
context.setFlag( QgsWmsRenderContext::AddExternalLayers );
67+
context.setFlag( QgsWmsRenderContext::AddAllLayers );
6768
context.setParameters( parameters );
6869

6970
// rendering

‎src/server/services/wms/qgswmsrendercontext.cpp

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ qreal QgsWmsRenderContext::dotsPerMm() const
182182
return dpm / 1000.0;
183183
}
184184

185-
QStringList QgsWmsRenderContext::flattenedQueryLayers() const
185+
QStringList QgsWmsRenderContext::flattenedQueryLayers( const QStringList &layerNames ) const
186186
{
187187
QStringList result;
188188
std::function <QStringList( const QString &name )> findLeaves = [ & ]( const QString & name ) -> QStringList
@@ -211,8 +211,8 @@ QStringList QgsWmsRenderContext::flattenedQueryLayers() const
211211
}
212212
return _result;
213213
};
214-
const auto constNicks { mParameters.queryLayersNickname() };
215-
for ( const auto &name : constNicks )
214+
215+
for ( const auto &name : qgis::as_const( layerNames ) )
216216
{
217217
result.append( findLeaves( name ) );
218218
}
@@ -429,7 +429,21 @@ void QgsWmsRenderContext::searchLayersToRender()
429429

430430
if ( mFlags & AddQueryLayers )
431431
{
432-
const QStringList queryLayerNames { flattenedQueryLayers() };
432+
const QStringList queryLayerNames = flattenedQueryLayers( mParameters.queryLayersNickname() );
433+
for ( const QString &layerName : queryLayerNames )
434+
{
435+
const QList<QgsMapLayer *> layers = mNicknameLayers.values( layerName );
436+
for ( QgsMapLayer *lyr : layers )
437+
if ( !mLayersToRender.contains( lyr ) )
438+
{
439+
mLayersToRender.append( lyr );
440+
}
441+
}
442+
}
443+
444+
if ( mFlags & AddAllLayers )
445+
{
446+
const QStringList queryLayerNames = flattenedQueryLayers( mParameters.allLayersNickname() );
433447
for ( const QString &layerName : queryLayerNames )
434448
{
435449
const QList<QgsMapLayer *> layers = mNicknameLayers.values( layerName );

‎src/server/services/wms/qgswmsrendercontext.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ namespace QgsWms
4848
UseWfsLayersOnly = 0x100,
4949
AddExternalLayers = 0x200,
5050
UseSrcWidthHeight = 0x400,
51-
UseTileBuffer = 0x800
51+
UseTileBuffer = 0x800,
52+
AddAllLayers = 0x1000 //!< For GetPrint: add layers from LAYER(S) parameter
5253
};
5354
Q_DECLARE_FLAGS( Flags, Flag )
5455

@@ -209,7 +210,7 @@ namespace QgsWms
209210
* Returns a list of query layer names where group names are replaced by the names of their layer components.
210211
* \since QGIS 3.8
211212
*/
212-
QStringList flattenedQueryLayers() const;
213+
QStringList flattenedQueryLayers( const QStringList &layerNames ) const;
213214

214215
#ifdef HAVE_SERVER_PYTHON_PLUGINS
215216

‎src/server/services/wms/qgswmsrenderer.cpp

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ namespace QgsWms
394394
{
395395
filterString.append( " AND " );
396396
}
397-
filterString.append( QString( "\"%1\" = %2" ).arg( pkAttributeNames.at( j ) ).arg( atlasPk.at( currentAtlasPk ) ) );
397+
filterString.append( QString( "\"%1\" = %2" ).arg( pkAttributeNames.at( j ), atlasPk.at( currentAtlasPk ) ) );
398398
++currentAtlasPk;
399399
}
400400

@@ -427,6 +427,14 @@ namespace QgsWms
427427
// configure layout
428428
configurePrintLayout( layout.get(), mapSettings, atlas );
429429

430+
#ifdef HAVE_SERVER_PYTHON_PLUGINS
431+
QgsFeatureFilterProviderGroup filters;
432+
mContext.accessControl()->resolveFilterFeatures( mapSettings.layers() );
433+
filters.addProvider( mContext.accessControl() );
434+
QgsLayoutRenderContext &layoutRendererContext = layout->renderContext();
435+
layoutRendererContext.setFeatureFilterProvider( &filters );
436+
#endif
437+
430438
// Get the temporary output file
431439
const QgsWmsParameters::Format format = mWmsParameters.format();
432440
const QString extension = QgsWmsParameters::formatAsString( format ).toLower();
@@ -479,7 +487,7 @@ namespace QgsWms
479487
{
480488
bool ok;
481489
double _dpi = mWmsParameters.dpi().toDouble( &ok );
482-
if ( ! ok )
490+
if ( ok )
483491
dpi = _dpi;
484492
}
485493
exportSettings.dpi = dpi;
@@ -501,6 +509,10 @@ namespace QgsWms
501509
QgsLayoutExporter atlasPngExport( atlas->layout() );
502510
atlasPngExport.exportToImage( tempOutputFile.fileName(), exportSettings );
503511
}
512+
else
513+
{
514+
throw QgsServiceException( QStringLiteral( "Bad request" ), QStringLiteral( "Atlas error: empty atlas." ), QString(), 400 );
515+
}
504516
}
505517
else
506518
{
@@ -556,6 +568,7 @@ namespace QgsWms
556568

557569
bool QgsRenderer::configurePrintLayout( QgsPrintLayout *c, const QgsMapSettings &mapSettings, bool atlasPrint )
558570
{
571+
559572
c->renderContext().setSelectionColor( mapSettings.selectionColor() );
560573
// Maps are configured first
561574
QList<QgsLayoutItemMap *> maps;
@@ -1174,7 +1187,7 @@ namespace QgsWms
11741187
QDomDocument QgsRenderer::featureInfoDocument( QList<QgsMapLayer *> &layers, const QgsMapSettings &mapSettings,
11751188
const QImage *outputImage, const QString &version ) const
11761189
{
1177-
const QStringList queryLayers = mContext.flattenedQueryLayers( );
1190+
const QStringList queryLayers = mContext.flattenedQueryLayers( mContext.parameters().queryLayersNickname() );
11781191

11791192
bool ijDefined = ( !mWmsParameters.i().isEmpty() && !mWmsParameters.j().isEmpty() );
11801193

@@ -3246,7 +3259,7 @@ namespace QgsWms
32463259
if ( !( *mapIt )->renderingErrors().isEmpty() )
32473260
{
32483261
const QgsMapRendererJob::Error e = ( *mapIt )->renderingErrors().at( 0 );
3249-
throw QgsException( QStringLiteral( "Rendering error : '%1' in layer %2" ).arg( e.message ).arg( e.layerID ) );
3262+
throw QgsException( QStringLiteral( "Rendering error : '%1' in layer %2" ).arg( e.message, e.layerID ) );
32503263
}
32513264
}
32523265
}

‎tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ if (ENABLE_PGTEST)
384384
ADD_PYTHON_TEST(PyQgsProviderConnectionPostgres test_qgsproviderconnection_postgres.py)
385385
if (WITH_SERVER)
386386
ADD_PYTHON_TEST(PyQgsServerWMSGetFeatureInfoPG test_qgsserver_wms_getfeatureinfo_postgres.py)
387+
ADD_PYTHON_TEST(PyQgsServerAccessControlWMSGetPrintPG test_qgsserver_accesscontrol_wms_getprint_postgres.py)
387388
endif()
388389
endif()
389390

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
# -*- coding: utf-8 -*-
2+
"""QGIS Unit tests for QgsServer WMS GetPrint with postgres access control filters.
3+
4+
From build dir, run: ctest -R PyQgsServerAccessControlWMSGetPrintPG -V
5+
6+
7+
.. note:: This program is free software; you can redistribute it and/or modify
8+
it under the terms of the GNU General Public License as published by
9+
the Free Software Foundation; either version 2 of the License, or
10+
(at your option) any later version.
11+
12+
"""
13+
__author__ = 'Alessandro Pasotti'
14+
__date__ = '25/02/2021'
15+
__copyright__ = 'Copyright 2021, The QGIS Project'
16+
17+
import os
18+
import re
19+
import hashlib
20+
21+
# Needed on Qt 5 so that the serialization of XML is consistent among all executions
22+
os.environ['QT_HASH_SEED'] = '1'
23+
24+
import urllib.parse
25+
26+
from qgis.core import QgsProject, QgsProviderRegistry, QgsVectorLayer
27+
from qgis.PyQt.QtCore import QTemporaryDir
28+
from qgis.PyQt.QtGui import QImage
29+
from qgis.server import (QgsServer, QgsBufferServerRequest,
30+
QgsBufferServerResponse, QgsAccessControlFilter)
31+
from qgis.testing import unittest
32+
from test_qgsserver import QgsServerTestBase
33+
from utilities import unitTestDataPath
34+
35+
36+
class RestrictedAccessControl(QgsAccessControlFilter):
37+
"""Restricts access to pk1 = 1 AND pk2 = 1"""
38+
39+
# Be able to deactivate the access control to have a reference point
40+
active = {
41+
'layerFilterExpression': False,
42+
'layerFilterSubsetString': False,
43+
'authorizedLayerAttributes': False,
44+
'layerPermissions': False,
45+
}
46+
47+
def layerFilterExpression(self, layer):
48+
""" Return an additional expression filter """
49+
50+
if not self.active['layerFilterExpression']:
51+
return super().layerFilterExpression(layer)
52+
53+
if layer.name() == "multiple_pks":
54+
return "pk1 = 1 AND pk2 = 1"
55+
else:
56+
return None
57+
58+
def layerFilterSubsetString(self, layer):
59+
""" Return an additional subset string (typically SQL) filter """
60+
61+
if not self.active['layerFilterSubsetString']:
62+
return super().layerFilterSubsetString(layer)
63+
64+
if layer.name() == "multiple_pks":
65+
return "pk1 = 1 AND pk2 = 1"
66+
else:
67+
return None
68+
69+
def authorizedLayerAttributes(self, layer, attributes):
70+
""" Return the authorised layer attributes """
71+
72+
if not self.active['authorizedLayerAttributes']:
73+
return super().authorizedLayerAttributes(layer, attributes)
74+
75+
allowed = []
76+
77+
for attr in attributes:
78+
if "name" != attr and "virtual" != attr: # spellok
79+
allowed.append(attr) # spellok
80+
81+
return allowed
82+
83+
def layerPermissions(self, layer):
84+
""" Return the layer rights """
85+
86+
rights = QgsAccessControlFilter.LayerPermissions()
87+
rights.canRead = not self.active['layerPermissions']
88+
89+
return rights
90+
91+
92+
class TestQgsServerAccessControlWMSGetPrintPG(QgsServerTestBase):
93+
"""QGIS Server WMS Tests for GetPrint request"""
94+
95+
# Set to True in child classes to re-generate reference files for this class
96+
# regenerate_reference = True
97+
98+
@classmethod
99+
def setUpClass(cls):
100+
101+
super().setUpClass()
102+
103+
if 'QGIS_PGTEST_DB' in os.environ:
104+
cls.dbconn = os.environ['QGIS_PGTEST_DB']
105+
else:
106+
cls.dbconn = 'service=qgis_test dbname=qgis_test sslmode=disable '
107+
108+
# Test layer
109+
md = QgsProviderRegistry.instance().providerMetadata('postgres')
110+
uri = cls.dbconn + ' dbname=qgis_test sslmode=disable '
111+
conn = md.createConnection(uri, {})
112+
113+
project_path = os.path.join(unitTestDataPath('qgis_server_accesscontrol'), 'pg_multiple_pks.qgs')
114+
cls.temp_dir = QTemporaryDir()
115+
cls.temp_project_path = os.path.join(cls.temp_dir.path(), 'pg_multiple_pks.qgs')
116+
117+
# Create test layer
118+
119+
conn.executeSql("DROP TABLE IF EXISTS qgis_test.multiple_pks")
120+
conn.executeSql(
121+
"CREATE TABLE qgis_test.multiple_pks ( pk1 bigint not null, pk2 bigint not null, name text not null, geom geometry(POINT,4326), PRIMARY KEY ( pk1, pk2 ) )")
122+
conn.executeSql(
123+
"INSERT INTO qgis_test.multiple_pks VALUES ( 1, 1, '1-1', ST_GeomFromText('point(7 45)', 4326))")
124+
conn.executeSql(
125+
"INSERT INTO qgis_test.multiple_pks VALUES ( 1, 2, '1-2', ST_GeomFromText('point(8 46)', 4326))")
126+
127+
cls.layer_uri = uri + \
128+
" sslmode=disable key='pk1,pk2' estimatedmetadata=true srid=4326 type=Point checkPrimaryKeyUnicity='0' table=\"qgis_test\".\"multiple_pks\" (geom)"
129+
layer = QgsVectorLayer(cls.layer_uri, 'multiple_pks', 'postgres')
130+
131+
assert layer.isValid()
132+
133+
project = open(project_path, 'r').read()
134+
with open(cls.temp_project_path, 'w+') as f:
135+
f.write(re.sub(r'<datasource>.*</datasource>', '<datasource>%s</datasource>' % cls.layer_uri, project))
136+
137+
cls.test_project = QgsProject()
138+
cls.test_project.read(cls.temp_project_path)
139+
140+
# Setup access control
141+
cls._server = QgsServer()
142+
cls._server_iface = cls._server.serverInterface()
143+
cls._accesscontrol = RestrictedAccessControl(cls._server_iface)
144+
cls._server_iface.registerAccessControl(cls._accesscontrol, 100)
145+
146+
def _clear_constraints(self):
147+
self._accesscontrol.active['authorizedLayerAttributes'] = False
148+
self._accesscontrol.active['layerFilterExpression'] = False
149+
self._accesscontrol.active['layerFilterSubsetString'] = False
150+
self._accesscontrol.active['layerPermissions'] = False
151+
152+
def setUp(self):
153+
super().setUp()
154+
self._clear_constraints()
155+
156+
def _check_exception(self, qs, exception_text):
157+
"""Check that server throws"""
158+
159+
req = QgsBufferServerRequest('http://my_server/' + qs)
160+
res = QgsBufferServerResponse()
161+
self._server.handleRequest(req, res, self.test_project)
162+
self.assertEqual(res.statusCode(), 400)
163+
self.assertTrue(exception_text in bytes(res.body()).decode('utf8'))
164+
165+
def _check_white(self, qs):
166+
"""Check that output is a white image"""
167+
168+
req = QgsBufferServerRequest('http://my_server/' + qs)
169+
res = QgsBufferServerResponse()
170+
self._server.handleRequest(req, res, self.test_project)
171+
self.assertEqual(res.statusCode(), 200)
172+
173+
result_path = os.path.join(self.temp_dir.path(), 'white.png')
174+
with open(result_path, 'wb+') as f:
175+
f.write(res.body())
176+
177+
# A full white image is expected
178+
image = QImage(result_path)
179+
self.assertTrue(image.isGrayscale())
180+
color = image.pixelColor(100, 100)
181+
self.assertEqual(color.red(), 255)
182+
self.assertEqual(color.green(), 255)
183+
self.assertEqual(color.blue(), 255)
184+
185+
def test_wms_getprint_postgres(self):
186+
"""Test issue GH #41800 """
187+
188+
# Extent for feature where pk1 = 1, pk2 = 2
189+
qs = "?" + "&".join(["%s=%s" % i for i in list({
190+
'SERVICE': "WMS",
191+
'VERSION': "1.3.0",
192+
'REQUEST': "GetPrint",
193+
'CRS': 'EPSG:4326',
194+
'FORMAT': 'png',
195+
'LAYERS': 'multiple_pks',
196+
'DPI': 72,
197+
'TEMPLATE': "print1",
198+
'map0:EXTENT': '45.70487804878048621,7.67926829268292099,46.22987804878049189,8.42479674796748235',
199+
}.items())])
200+
201+
def _check_red():
202+
203+
req = QgsBufferServerRequest('http://my_server/' + qs)
204+
res = QgsBufferServerResponse()
205+
self._server.handleRequest(req, res, self.test_project)
206+
self.assertEqual(res.statusCode(), 200)
207+
208+
result_path = os.path.join(self.temp_dir.path(), 'red.png')
209+
with open(result_path, 'wb+') as f:
210+
f.write(res.body())
211+
212+
# A full red image is expected
213+
image = QImage(result_path)
214+
self.assertFalse(image.isGrayscale())
215+
color = image.pixelColor(100, 100)
216+
self.assertEqual(color.red(), 255)
217+
self.assertEqual(color.green(), 0)
218+
self.assertEqual(color.blue(), 0)
219+
220+
_check_red()
221+
222+
# Now activate the rule to exclude the feature where pk1 = 1, pk2 = 2
223+
# A white image is expected
224+
225+
self._accesscontrol.active['layerFilterExpression'] = True
226+
self._check_white(qs)
227+
228+
# Activate the other rule for subset string
229+
230+
self._accesscontrol.active['layerFilterExpression'] = False
231+
self._accesscontrol.active['layerFilterSubsetString'] = True
232+
self._check_white(qs)
233+
234+
# Activate the other rule for layer permission
235+
236+
self._accesscontrol.active['layerFilterSubsetString'] = False
237+
self._accesscontrol.active['layerPermissions'] = True
238+
239+
req = QgsBufferServerRequest('http://my_server/' + qs)
240+
res = QgsBufferServerResponse()
241+
self._server.handleRequest(req, res, self.test_project)
242+
self.assertEqual(res.statusCode(), 403)
243+
244+
# Test attribute table (template print2) with no rule
245+
self._accesscontrol.active['layerPermissions'] = False
246+
247+
req = QgsBufferServerRequest('http://my_server/' + qs.replace('print1', 'print2'))
248+
res = QgsBufferServerResponse()
249+
self._server.handleRequest(req, res, self.test_project)
250+
self.assertEqual(res.statusCode(), 200)
251+
252+
self._img_diff_error(res.body(), res.headers(), "WMS_GetPrint_postgres_print2")
253+
254+
# Test attribute table with rule
255+
self._accesscontrol.active['authorizedLayerAttributes'] = True
256+
257+
req = QgsBufferServerRequest('http://my_server/' + qs.replace('print1', 'print2'))
258+
res = QgsBufferServerResponse()
259+
self._server.handleRequest(req, res, self.test_project)
260+
self.assertEqual(res.statusCode(), 200)
261+
262+
self._img_diff_error(res.body(), res.headers(), "WMS_GetPrint_postgres_print2_filtered")
263+
264+
# Re-Test attribute table (template print2) with no rule
265+
self._accesscontrol.active['authorizedLayerAttributes'] = False
266+
267+
req = QgsBufferServerRequest('http://my_server/' + qs.replace('print1', 'print2'))
268+
res = QgsBufferServerResponse()
269+
self._server.handleRequest(req, res, self.test_project)
270+
self.assertEqual(res.statusCode(), 200)
271+
272+
self._img_diff_error(res.body(), res.headers(), "WMS_GetPrint_postgres_print2")
273+
274+
# Test with layer permissions
275+
self._accesscontrol.active['layerPermissions'] = True
276+
req = QgsBufferServerRequest('http://my_server/' + qs.replace('print1', 'print2'))
277+
res = QgsBufferServerResponse()
278+
self._server.handleRequest(req, res, self.test_project)
279+
self.assertEqual(res.statusCode(), 403)
280+
281+
# Test with subset string
282+
self._accesscontrol.active['layerPermissions'] = False
283+
self._accesscontrol.active['layerFilterSubsetString'] = True
284+
req = QgsBufferServerRequest('http://my_server/' + qs.replace('print1', 'print2'))
285+
res = QgsBufferServerResponse()
286+
self._server.handleRequest(req, res, self.test_project)
287+
self.assertEqual(res.statusCode(), 200)
288+
289+
self._img_diff_error(res.body(), res.headers(), "WMS_GetPrint_postgres_print2_subset")
290+
291+
# Test with filter expression
292+
self._accesscontrol.active['layerFilterExpression'] = True
293+
self._accesscontrol.active['layerFilterSubsetString'] = False
294+
req = QgsBufferServerRequest('http://my_server/' + qs.replace('print1', 'print2'))
295+
res = QgsBufferServerResponse()
296+
self._server.handleRequest(req, res, self.test_project)
297+
self.assertEqual(res.statusCode(), 200)
298+
299+
self._img_diff_error(res.body(), res.headers(), "WMS_GetPrint_postgres_print2_subset")
300+
301+
# Test attribute table with attribute filter
302+
self._accesscontrol.active['layerFilterExpression'] = False
303+
self._accesscontrol.active['authorizedLayerAttributes'] = True
304+
305+
req = QgsBufferServerRequest('http://my_server/' + qs.replace('print1', 'print2'))
306+
res = QgsBufferServerResponse()
307+
self._server.handleRequest(req, res, self.test_project)
308+
self.assertEqual(res.statusCode(), 200)
309+
310+
self._img_diff_error(res.body(), res.headers(), "WMS_GetPrint_postgres_print2_filtered")
311+
312+
# Clear constraints
313+
self._clear_constraints()
314+
_check_red()
315+
316+
req = QgsBufferServerRequest('http://my_server/' + qs.replace('print1', 'print2'))
317+
res = QgsBufferServerResponse()
318+
self._server.handleRequest(req, res, self.test_project)
319+
self.assertEqual(res.statusCode(), 200)
320+
321+
self._img_diff_error(res.body(), res.headers(), "WMS_GetPrint_postgres_print2")
322+
323+
def test_atlas(self):
324+
"""Test atlas"""
325+
326+
qs = "?" + "&".join(["%s=%s" % i for i in list({
327+
'SERVICE': "WMS",
328+
'VERSION': "1.3.0",
329+
'REQUEST': "GetPrint",
330+
'CRS': 'EPSG:4326',
331+
'FORMAT': 'png',
332+
'LAYERS': 'multiple_pks',
333+
'DPI': 72,
334+
'TEMPLATE': "print1",
335+
}.items())])
336+
337+
req = QgsBufferServerRequest('http://my_server/' + qs + '&ATLAS_PK=1,2')
338+
res = QgsBufferServerResponse()
339+
340+
self._server.handleRequest(req, res, self.test_project)
341+
self.assertEqual(res.statusCode(), 200)
342+
343+
result_path = os.path.join(self.temp_dir.path(), 'atlas_1_2.png')
344+
with open(result_path, 'wb+') as f:
345+
f.write(res.body())
346+
347+
# A full red image is expected
348+
image = QImage(result_path)
349+
self.assertFalse(image.isGrayscale())
350+
color = image.pixelColor(100, 100)
351+
self.assertEqual(color.red(), 255)
352+
self.assertEqual(color.green(), 0)
353+
self.assertEqual(color.blue(), 0)
354+
355+
# Forbid 1-1
356+
self._accesscontrol.active['layerFilterSubsetString'] = True
357+
self._check_exception(qs + '&ATLAS_PK=1,2', "Atlas error: empty atlas.")
358+
359+
self._accesscontrol.active['layerFilterSubsetString'] = False
360+
self._accesscontrol.active['layerFilterExpression'] = True
361+
self._check_exception(qs + '&ATLAS_PK=1,2', "Atlas error: empty atlas.")
362+
363+
# Remove all constraints
364+
self._clear_constraints()
365+
req = QgsBufferServerRequest('http://my_server/' + qs + '&ATLAS_PK=1,2')
366+
res = QgsBufferServerResponse()
367+
368+
self._server.handleRequest(req, res, self.test_project)
369+
self.assertEqual(res.statusCode(), 200)
370+
371+
result_path = os.path.join(self.temp_dir.path(), 'atlas_1_2.png')
372+
with open(result_path, 'wb+') as f:
373+
f.write(res.body())
374+
375+
# A full red image is expected
376+
image = QImage(result_path)
377+
self.assertFalse(image.isGrayscale())
378+
color = image.pixelColor(100, 100)
379+
self.assertEqual(color.red(), 255)
380+
self.assertEqual(color.green(), 0)
381+
self.assertEqual(color.blue(), 0)
382+
383+
384+
if __name__ == '__main__':
385+
unittest.main()

‎tests/testdata/qgis_server_accesscontrol/pg_multiple_pks.qgs

Lines changed: 1364 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.