Skip to content
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

Improve user/group filter to allow multiple users/groups #4559

Merged
merged 3 commits into from
Aug 23, 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
39 changes: 14 additions & 25 deletions lizmap/modules/lizmap/lib/Form/QgisForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -1794,41 +1794,30 @@ protected function filterDataByLogin($layername)
return null;
}

// Optionnaly add a filter parameter
$lproj = $this->layer->getProject();
$loginFilteredConfig = $lproj->getLoginFilteredConfig($layername);
// Optionally add a filter parameter
$layerProject = $this->layer->getProject();
$loginFilteredConfig = $layerProject->getLoginFilteredConfig($layername);

if ($loginFilteredConfig) {
// Get filter type
$type = 'groups';
$attribute = $loginFilteredConfig->filterAttribute;

// check filter type
if (property_exists($loginFilteredConfig, 'filterPrivate')
&& $loginFilteredConfig->filterPrivate == 'True') {
&& $loginFilteredConfig->filterPrivate == 'True') {
$type = 'login';
}

// Check if a user is authenticated
$isConnected = $this->appContext->userIsConnected();
$cnx = $this->appContext->getDbConnection($this->layer->getId());
if ($isConnected) {
$user = $this->appContext->getUserSession();
$login = $user->login;
if ($type == 'login') {
$where = ' "'.$attribute."\" IN ( '".$login."' , 'all' )";
} else {
$userGroups = $this->appContext->aclUserPublicGroupsId();
// Set XML Filter if getFeature request
$flatGroups = implode("' , '", $userGroups);
$where = ' "'.$attribute."\" IN ( '".$flatGroups."' , 'all' )";
}
} else {
// The user is not authenticated: only show data with attribute = 'all'
$where = ' "'.$attribute.'" = '.$cnx->quote('all');
// Filter attribute
$attribute = $loginFilteredConfig->filterAttribute;

// SQL filter
$layerFilter = $layerProject->getLoginFilter($layername);
if (empty($layerFilter)) {
return null;
}

// Set filter when multiple layers concerned
return array(
'where' => $where,
'where' => $layerFilter['filter'],
'type' => $type,
'attribute' => $attribute,
);
Expand Down
87 changes: 82 additions & 5 deletions lizmap/modules/lizmap/lib/Project/Project.php
Original file line number Diff line number Diff line change
Expand Up @@ -1223,10 +1223,19 @@ public function getLoginFilteredConfig($layerName, $edition = false)
* Get login filters, get expressions for layers based on login filtered
* config.
*
* NOTE: We could delegate this completely to the lizmap_server plugin
* for all requests. The only request needed to have the SQL filter
* is the WFS GetFeature request, if the layer is a PostgreSQL layer.
* It this particular case, we could use a similar approach
* that the one use with qgisVectorLayer::requestPolygonFilter
* which calls the server plugin with SERVICE=Lizmap&REQUEST=GetSubsetString
*
mdouchin marked this conversation as resolved.
Show resolved Hide resolved
* @param string[] $layers : layers' name list
* @param bool $edition : get login filters for edition
*
* @return array
* @return array Array containing layers names as key and filter configuration
* and SQL filters as values. Array might be empty if no filter
* is configured for the layer.
*/
public function getLoginFilters($layers, $edition = false)
{
Expand Down Expand Up @@ -1258,22 +1267,90 @@ public function getLoginFilters($layers, $edition = false)
// attribute to filter
$attribute = $loginFilteredConfig->filterAttribute;

// Quoted attribute with double-quotes
$cnx = $this->appContext->getDbConnection();
$quotedField = $cnx->encloseName($attribute);

// Get QGIS vector layer provider
/** @var null|\qgisVectorLayer $qgisLayer The QGIS vector layer instance */
$qgisLayer = $this->qgis->getLayer($layerByTypeName->id, $this);
$provider = 'unknown';
if ($qgisLayer) {
$provider = $qgisLayer->getProvider();
}

// default no user connected
$filter = "\"{$attribute}\" = 'all'";
$filter = "{$quotedField} = 'all'";

// For PostgreSQL layers, allow multiple values in the filter field
// E.g. "groupe_a,other_group"
if ($provider == 'postgres'
&& property_exists($loginFilteredConfig, 'allow_multiple_acl_values')
&& $loginFilteredConfig->allow_multiple_acl_values
) {
$filter .= " OR {$quotedField} LIKE 'all,%'";
$filter .= " OR {$quotedField} LIKE '%,all'";
$filter .= " OR {$quotedField} LIKE '%,all,%'";
}

// A user is connected
if ($this->appContext->userIsConnected()) {
// Get the user
$user = $this->appContext->getUserSession();
$login = $user->login;

// List of values for expression
$values = array();
if (property_exists($loginFilteredConfig, 'filterPrivate')
&& $this->optionToBoolean($loginFilteredConfig->filterPrivate)
) {
$filter = "\"{$attribute}\" IN ( '".$login."' , 'all' )";
// If filter is private use user_login
$values[] = $login;
} else {
// Else use user groups
$userGroups = $this->appContext->aclUserPublicGroupsId();
$flatGroups = implode("' , '", $userGroups);
$filter = "\"{$attribute}\" IN ( '".$flatGroups."' , 'all' )";
$values = $userGroups;
}

// Add all to values
$values[] = 'all';
$allValuesFilters = array();

// For each value (group, all, login, etc.), create a filter
// combining all the possibility: equality & LIKE
foreach ($values as $value) {
$valueFilters = array();
// Quote the value with single quotes
$quotedValue = $cnx->quote($value);
Gustry marked this conversation as resolved.
Show resolved Hide resolved

// equality
$valueFilters[] = "{$quotedField} = {$quotedValue}";

// For PostgreSQL layers, allow multiple values in the filter field
// E.g. "groupe_a,other_group"
if ($provider == 'postgres'
&& property_exists($loginFilteredConfig, 'allow_multiple_acl_values')
&& $loginFilteredConfig->allow_multiple_acl_values
) {
// begins with value & comma
$quotedLikeValue = $cnx->quote("{$value},%");
$valueFilters[] = "{$quotedField} LIKE {$quotedLikeValue}";

// ends with comma & value
$quotedLikeValue = $cnx->quote("%,{$value}");
$valueFilters[] = "{$quotedField} LIKE {$quotedLikeValue}";

// value between two commas
$quotedLikeValue = $cnx->quote("%,{$value},%");
$valueFilters[] = "{$quotedField} LIKE {$quotedLikeValue}";
}

// Build the filter for this value
$allValuesFilters[] = implode(' OR ', $valueFilters);
}

// Build filter for all values
$filter = implode(' OR ', $allValuesFilters);
}

$filters[$layerName] = array_merge(
Expand Down
4 changes: 2 additions & 2 deletions lizmap/modules/lizmap/lib/Request/WFSRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -643,13 +643,13 @@ protected function parseExpFilter($cnx, $params)
if (strpos($validFilter, '$id') !== false) {
$key = $this->datasource->key;
if (count(explode(',', $key)) == 1) {
return ' AND '.str_replace('$id ', $cnx->encloseName($key).' ', $validFilter);
return ' AND ( '.str_replace('$id ', $cnx->encloseName($key).' ', $validFilter).' ) ';
}

return false;
}

return ' AND '.$validFilter;
return ' AND ( '.$validFilter.' ) ';
}

return '';
Expand Down
10 changes: 10 additions & 0 deletions tests/end2end/cypress/integration/requests-metadata-ghaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ describe('Request JSON metadata', function () {
"__anonymous",
"admins",
"group_a",
"group_b",
"publishers"
].sort()
);
Expand All @@ -49,6 +50,7 @@ describe('Request JSON metadata', function () {
"__anonymous",
"admins",
"group_a",
"group_b",
"publishers"
].sort()
);
Expand All @@ -72,6 +74,9 @@ describe('Request JSON metadata', function () {
"group_a": {
"label": "group_a"
},
"group_b": {
"label": "group_b"
},
"intranet": {
"label": "Intranet demos group"
},
Expand Down Expand Up @@ -145,6 +150,7 @@ describe('Request JSON metadata', function () {
"__anonymous",
"admins",
"group_a",
"group_b",
"publishers"
].sort()
);
Expand All @@ -153,6 +159,7 @@ describe('Request JSON metadata', function () {
"__anonymous",
"admins",
"group_a",
"group_b",
"publishers"
].sort()
);
Expand All @@ -167,6 +174,9 @@ describe('Request JSON metadata', function () {
"group_a": {
"label": "group_a"
},
"group_b": {
"label": "group_b"
},
"intranet": {
"label": "Intranet demos group"
},
Expand Down
99 changes: 75 additions & 24 deletions tests/end2end/playwright/filter-layer-by-user.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,34 @@ test.describe('Filter layer data by user - not connected', () => {
await page.locator('#dock-close').click();
});

test('GetMap', async ({ page }) => {
// Hide all elements but #newOlMap and its children
await page.$eval("*", el => el.style.visibility = 'hidden');
await page.$eval("#newOlMap, #newOlMap *", el => el.style.visibility = 'visible');
// DISABLED BECAUSE IT IS NOT RELIABLE, MAINTAINABLE AND CAUSES HEADACHE ;-)
// Instead, we use a WMS GetFeatureInfo in JSON format below
// test('GetMap', async ({ page }) => {
// // Hide all elements but #newOlMap and its children
// await page.$eval("*", el => el.style.visibility = 'hidden');
// await page.$eval("#newOlMap, #newOlMap *", el => el.style.visibility = 'visible');

// expect(await page.locator('#newOlMap').screenshot()).toMatchSnapshot('map_not_connected.png', {
// maxDiffPixels: 500
// });
// });

test('WMS GetFeatureInfo JSON', async ({ page }) => {

const getFeatureInfo = await page.evaluate(async () => {
return await fetch("/index.php/lizmap/service?repository=testsrepository&project=filter_layer_by_user&SERVICE=WMS&REQUEST=GetFeatureInfo&VERSION=1.3.0&CRS=EPSG%3A2154&INFO_FORMAT=application%2Fjson&QUERY_LAYERS=green_filter_layer_by_user_edition_only%2Cblue_filter_layer_by_user%2Cred_layer_with_no_filter&LAYERS=green_filter_layer_by_user_edition_only%2Cblue_filter_layer_by_user%2Cred_layer_with_no_filter&STYLE=default%2Cdefault%2Cdefault&FEATURE_COUNT=10&FILTER=green_filter_layer_by_user_edition_only:\"gid\" > 0")
.then(r => r.ok ? r.json() : Promise.reject(r))
})

// check features
expect(getFeatureInfo.features).toHaveLength(4)
// check a specific feature
const feature = getFeatureInfo.features[0]
expect(feature.id).not.toBeUndefined()

expect(await page.locator('#newOlMap').screenshot()).toMatchSnapshot('map_not_connected.png', {
maxDiffPixels: 500
});
});

test('Popup', async ({ page }) => {
test('Popup with map click', async ({ page }) => {
let getFeatureInfoRequestPromise = page.waitForRequest(request => request.method() === 'POST' && request.postData()?.includes('GetFeatureInfo') === true);

// blue_filter_layer_by_user
Expand Down Expand Up @@ -133,17 +150,34 @@ test.describe('Filter layer data by user - user in group a', () => {
await page.locator('#dock-close').click();
});

test('GetMap', async ({ page }) => {
// Hide all elements but #newOlMap and its children
await page.$eval("*", el => el.style.visibility = 'hidden');
await page.$eval("#newOlMap, #newOlMap *", el => el.style.visibility = 'visible');
// DISABLED BECAUSE IT IS NOT RELIABLE, MAINTAINABLE AND CAUSES HEADACHE ;-)
// Instead, we use a WMS GetFeatureInfo in JSON format below
// test('GetMap', async ({ page }) => {
// // Hide all elements but #newOlMap and its children
// await page.$eval("*", el => el.style.visibility = 'hidden');
// await page.$eval("#newOlMap, #newOlMap *", el => el.style.visibility = 'visible');

// expect(await page.locator('#newOlMap').screenshot()).toMatchSnapshot('map_connected_as_user_in_group_a.png', {
// maxDiffPixels: 500
// });
// });

test('WMS GetFeatureInfo JSON', async ({ page }) => {

const getFeatureInfo = await page.evaluate(async () => {
return await fetch("/index.php/lizmap/service?repository=testsrepository&project=filter_layer_by_user&SERVICE=WMS&REQUEST=GetFeatureInfo&VERSION=1.3.0&CRS=EPSG%3A2154&INFO_FORMAT=application%2Fjson&QUERY_LAYERS=green_filter_layer_by_user_edition_only%2Cblue_filter_layer_by_user%2Cred_layer_with_no_filter&LAYERS=green_filter_layer_by_user_edition_only%2Cblue_filter_layer_by_user%2Cred_layer_with_no_filter&STYLE=default%2Cdefault%2Cdefault&FEATURE_COUNT=10&FILTER=green_filter_layer_by_user_edition_only:\"gid\" > 0")
.then(r => r.ok ? r.json() : Promise.reject(r))
})

// check features
expect(getFeatureInfo.features).toHaveLength(5)
// check a specific feature
const feature = getFeatureInfo.features[0]
expect(feature.id).not.toBeUndefined()

expect(await page.locator('#newOlMap').screenshot()).toMatchSnapshot('map_connected_as_user_in_group_a.png', {
maxDiffPixels: 500
});
});

test('Popup', async ({ page }) => {
test('Popup with map click', async ({ page }) => {
let getFeatureInfoRequestPromise = page.waitForRequest(request => request.method() === 'POST' && request.postData()?.includes('GetFeatureInfo') === true);

// blue_filter_layer_by_user
Expand Down Expand Up @@ -258,17 +292,34 @@ test.describe('Filter layer data by user - admin', () => {
await page.locator('#dock-close').click();
});

test('GetMap', async ({ page }) => {
// Hide all elements but #newOlMap and its children
await page.$eval("*", el => el.style.visibility = 'hidden');
await page.$eval("#newOlMap, #newOlMap *", el => el.style.visibility = 'visible');
// DISABLED BECAUSE IT IS NOT RELIABLE, MAINTAINABLE AND CAUSES HEADACHE ;-)
// Instead, we use a WMS GetFeatureInfo in JSON format below
// test('GetMap', async ({ page }) => {
// // Hide all elements but #newOlMap and its children
// await page.$eval("*", el => el.style.visibility = 'hidden');
// await page.$eval("#newOlMap, #newOlMap *", el => el.style.visibility = 'visible');

// expect(await page.locator('#newOlMap').screenshot()).toMatchSnapshot('map_connected_as_admin.png', {
// maxDiffPixels: 500
// });
// });

test('WMS GetFeatureInfo JSON', async ({ page }) => {

const getFeatureInfo = await page.evaluate(async () => {
return await fetch("/index.php/lizmap/service?repository=testsrepository&project=filter_layer_by_user&SERVICE=WMS&REQUEST=GetFeatureInfo&VERSION=1.3.0&CRS=EPSG%3A2154&INFO_FORMAT=application%2Fjson&QUERY_LAYERS=green_filter_layer_by_user_edition_only%2Cblue_filter_layer_by_user%2Cred_layer_with_no_filter&LAYERS=green_filter_layer_by_user_edition_only%2Cblue_filter_layer_by_user%2Cred_layer_with_no_filter&STYLE=default%2Cdefault%2Cdefault&FEATURE_COUNT=10&FILTER=green_filter_layer_by_user_edition_only:\"gid\" > 0")
.then(r => r.ok ? r.json() : Promise.reject(r))
})

// check features
expect(getFeatureInfo.features).toHaveLength(7)
// check a specific feature
const feature = getFeatureInfo.features[0]
expect(feature.id).not.toBeUndefined()

expect(await page.locator('#newOlMap').screenshot()).toMatchSnapshot('map_connected_as_admin.png', {
maxDiffPixels: 500
});
});

test('Popup', async ({ page }) => {
test('Popup with map click', async ({ page }) => {
let getFeatureInfoRequestPromise = page.waitForRequest(request => request.method() === 'POST' && request.postData()?.includes('GetFeatureInfo') === true);

// blue_filter_layer_by_user
Expand Down
Loading
Loading