Commit c1bb7574 authored by Benni Mack's avatar Benni Mack
Browse files

[!!!][FEATURE] Move BE Group Resolving into separate functionality

In order to re-use the group resolving, this logic
is now extract from AbstractUserAuthentication,
allowing to further optimize this code.

The hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroups_postProcessing']
is removed and replaced by a PSR-14 event to modify the Group Data.

Resolves: #93056
Releases: master
Change-Id: If0fc7939e2617fae899474ba47ba786405e87a3c
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/67034

Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent ba0ea875
......@@ -1239,13 +1239,40 @@ class BackendUserAuthentication extends AbstractUserAuthentication
// Fileoperation permissions
$this->groupData['file_permissions'] = $this->user['file_permissions'];
// BE_GROUPS:
// Get the groups...
if (!empty($this->user[$this->usergroup_column])) {
// Fetch groups will add a lot of information to the internal arrays: modules, accesslists, TSconfig etc.
// Refer to fetchGroups() function.
$this->fetchGroups($this->user[$this->usergroup_column]);
// Get the groups and accumulate their permission settings
$mountOptions = new BackendGroupMountOption($this->user['options']);
$groupResolver = GeneralUtility::makeInstance(GroupResolver::class);
$resolvedGroups = $groupResolver->resolveGroupsForUser($this->user, $this->usergroup_table);
foreach ($resolvedGroups as $groupInfo) {
// Add the group uid to internal arrays.
$this->userGroupsUID[] = (int)$groupInfo['uid'];
$this->userGroups[(int)$groupInfo['uid']] = $groupInfo;
// Mount group database-mounts
if ($mountOptions->shouldUserIncludePageMountsFromAssociatedGroups()) {
$this->groupData['webmounts'] .= ',' . $groupInfo['db_mountpoints'];
}
// Mount group file-mounts
if ($mountOptions->shouldUserIncludePageMountsFromAssociatedGroups()) {
$this->groupData['filemounts'] .= ',' . $groupInfo['file_mountpoints'];
}
// Gather permission detail fields
$this->groupData['modules'] .= ',' . $groupInfo['groupMods'];
$this->groupData['available_widgets'] .= ',' . $groupInfo['availableWidgets'];
$this->groupData['tables_select'] .= ',' . $groupInfo['tables_select'];
$this->groupData['tables_modify'] .= ',' . $groupInfo['tables_modify'];
$this->groupData['pagetypes_select'] .= ',' . $groupInfo['pagetypes_select'];
$this->groupData['non_exclude_fields'] .= ',' . $groupInfo['non_exclude_fields'];
$this->groupData['explicit_allowdeny'] .= ',' . $groupInfo['explicit_allowdeny'];
$this->groupData['allowed_languages'] .= ',' . $groupInfo['allowed_languages'];
$this->groupData['custom_options'] .= ',' . $groupInfo['custom_options'];
$this->groupData['file_permissions'] .= ',' . $groupInfo['file_permissions'];
// Setting workspace permissions:
$this->groupData['workspace_perms'] |= $groupInfo['workspace_perms'];
if (!$this->firstMainGroup) {
$this->firstMainGroup = (int)$groupInfo['uid'];
}
}
// Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.!!
$this->userGroupsUID = array_reverse(array_unique(array_reverse($this->userGroupsUID)));
// Finally this is the list of group_uid's in the order they are parsed (including subgroups!)
......@@ -1368,99 +1395,6 @@ TCAdefaults.sys_note.email = ' . $this->user['email'];
}
}
/**
* Fetches the group records, subgroups and fills internal arrays.
* Function is called recursively to fetch subgroups
*
* @param string $grList Commalist of be_groups uid numbers
* @param string $idList List of already processed be_groups-uids so the function will not fall into an eternal recursion.
* @internal
*/
public function fetchGroups($grList, $idList = '')
{
// Fetching records of the groups in $grList:
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->usergroup_table);
$expressionBuilder = $queryBuilder->expr();
$constraints = $expressionBuilder->andX(
$expressionBuilder->eq(
'pid',
$queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
),
$expressionBuilder->in(
'uid',
$queryBuilder->createNamedParameter(
GeneralUtility::intExplode(',', $grList),
Connection::PARAM_INT_ARRAY
)
)
);
// Hook for manipulation of the WHERE sql sentence which controls which BE-groups are included
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroupQuery'] ?? [] as $className) {
$hookObj = GeneralUtility::makeInstance($className);
if (method_exists($hookObj, 'fetchGroupQuery_processQuery')) {
$constraints = $hookObj->fetchGroupQuery_processQuery($this, $grList, $idList, (string)$constraints);
}
}
$res = $queryBuilder->select('*')
->from($this->usergroup_table)
->where($constraints)
->execute();
// The userGroups array is filled
while ($row = $res->fetch(\PDO::FETCH_ASSOC)) {
$this->userGroups[$row['uid']] = $row;
}
$mountOptions = new BackendGroupMountOption((int)$this->user['options']);
// Traversing records in the correct order
foreach (explode(',', $grList) as $uid) {
// Get row:
$row = $this->userGroups[$uid];
// Must be an array and $uid should not be in the idList, because then it is somewhere previously in the grouplist
if (is_array($row) && !GeneralUtility::inList($idList, $uid)) {
// Include sub groups
if (trim($row['subgroup'])) {
// Make integer list
$theList = implode(',', GeneralUtility::intExplode(',', $row['subgroup']));
// Call recursively, pass along list of already processed groups so they are not recursed again.
$this->fetchGroups($theList, $idList . ',' . $uid);
}
// Add the group uid, current list to the internal arrays.
$this->userGroupsUID[] = (int)$uid;
// Mount group database-mounts
if ($mountOptions->shouldUserIncludePageMountsFromAssociatedGroups()) {
$this->groupData['webmounts'] .= ',' . $row['db_mountpoints'];
}
// Mount group file-mounts
if ($mountOptions->shouldUserIncludeFileMountsFromAssociatedGroups()) {
$this->groupData['filemounts'] .= ',' . $row['file_mountpoints'];
}
// The lists are made: groupMods, tables_select, tables_modify, pagetypes_select, non_exclude_fields, explicit_allowdeny, allowed_languages, custom_options
$this->groupData['modules'] .= ',' . $row['groupMods'];
$this->groupData['available_widgets'] .= ',' . $row['availableWidgets'];
$this->groupData['tables_select'] .= ',' . $row['tables_select'];
$this->groupData['tables_modify'] .= ',' . $row['tables_modify'];
$this->groupData['pagetypes_select'] .= ',' . $row['pagetypes_select'];
$this->groupData['non_exclude_fields'] .= ',' . $row['non_exclude_fields'];
$this->groupData['explicit_allowdeny'] .= ',' . $row['explicit_allowdeny'];
$this->groupData['allowed_languages'] .= ',' . $row['allowed_languages'];
$this->groupData['custom_options'] .= ',' . $row['custom_options'];
$this->groupData['file_permissions'] .= ',' . $row['file_permissions'];
// Setting workspace permissions:
$this->groupData['workspace_perms'] |= $row['workspace_perms'];
// If this function is processing the users OWN group-list (not subgroups) AND
// if the ->firstMainGroup is not set, then the ->firstMainGroup will be set.
if ($idList === '' && !$this->firstMainGroup) {
$this->firstMainGroup = $uid;
}
}
}
// HOOK: fetchGroups_postProcessing
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroups_postProcessing'] ?? [] as $_funcRef) {
$_params = [];
GeneralUtility::callUserFunction($_funcRef, $_params, $this);
}
}
/**
* Updates the field be_users.usergroup_cached_list if the groupList of the user
* has changed/is different from the current list.
......
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Authentication\Event;
/**
* Event fired after user groups have been resolved for a specific user
*/
final class AfterGroupsResolvedEvent
{
private string $sourceDatabaseTable;
private array $groups;
private array $originalGroupIds;
private array $userData;
public function __construct(string $sourceDatabaseTable, array $groups, array $originalGroupIds, array $userData)
{
$this->sourceDatabaseTable = $sourceDatabaseTable;
$this->groups = $groups;
$this->originalGroupIds = $originalGroupIds;
$this->userData = $userData;
}
/**
* @return string 'be_groups' or 'fe_groups' depending on context.
*/
public function getSourceDatabaseTable(): string
{
return $this->sourceDatabaseTable;
}
/**
* List of group records including sub groups as resolved by core.
*
* Note order is important: A user with main groups "1,2", where 1 has sub group 3,
* results in "3,1,2" as record list array - sub groups are listed before the group
* that includes the sub group.
*/
public function getGroups(): array
{
return $this->groups;
}
/**
* List of group records as manipulated by the event.
*/
public function setGroups(array $groups): void
{
$this->groups = $groups;
}
/**
* List of group uids directly attached to the user
*/
public function getOriginalGroupIds(): array
{
return $this->originalGroupIds;
}
/**
* Full user record with all fields
*/
public function getUserData(): array
{
return $this->userData;
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Authentication;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Authentication\Event\AfterGroupsResolvedEvent;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* A provider for resolving fe_groups / be_groups, including nested sub groups.
*
* When fetching subgroups, the current group (parent group) is handed in recursive.
* Duplicates are suppressed: If a sub group is including in multiple parent groups,
* it will be resolved only once.
*
* @internal this is not part of TYPO3 Core API.
*/
class GroupResolver
{
protected EventDispatcherInterface $eventDispatcher;
protected string $sourceTable = '';
protected string $sourceField = 'usergroup';
protected string $recursiveSourceField = 'subgroup';
public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
/**
* Fetch all group records for a given user recursive.
*
* Note order is important: A user with main groups "1,2", where 1 has sub group 3,
* results in "3,1,2" as record list array - sub groups are listed before the group
* that includes the sub group.
*
* @param array $userRecord Used for context in PSR-14 event
* @param string $sourceTable The database table to look up: be_groups / fe_groups depending on context
* @return array List of group records. Note the ordering note above.
*/
public function resolveGroupsForUser(array $userRecord, string $sourceTable): array
{
$this->sourceTable = $sourceTable;
$originalGroupIds = GeneralUtility::intExplode(',', $userRecord[$this->sourceField] ?? '', true);
$resolvedGroups = $this->fetchGroupsRecursive($originalGroupIds);
$event = $this->eventDispatcher->dispatch(new AfterGroupsResolvedEvent($sourceTable, $resolvedGroups, $originalGroupIds, $userRecord));
return $event->getGroups();
}
/**
* Load a list of group uids, and take into account if groups have been loaded before.
*
* @param int[] $groupIds
* @param array $processedGroupIds
* @return array
*/
protected function fetchGroupsRecursive(array $groupIds, array $processedGroupIds = []): array
{
if (empty($groupIds)) {
return [];
}
$foundGroups = $this->fetchRowsFromDatabase($groupIds);
$validGroups = [];
foreach ($groupIds as $groupId) {
// Database did not find the record
if (!is_array($foundGroups[$groupId])) {
continue;
}
// Record was already processed, continue to avoid adding this group again
if (in_array($groupId, $processedGroupIds, true)) {
continue;
}
// Add sub groups first
$subgroupIds = GeneralUtility::intExplode(',', $foundGroups[$groupId][$this->recursiveSourceField] ?? '', true);
if (!empty($subgroupIds)) {
$subgroups = $this->fetchGroupsRecursive($subgroupIds, array_merge($processedGroupIds, [$groupId]));
$validGroups = array_merge($validGroups, $subgroups);
}
// Add main group after sub groups have been added
$validGroups[] = $foundGroups[$groupId];
}
return $validGroups;
}
/**
* Does the database query. Does not care about ordering, this is done by caller.
*
* @param array $groupIds
* @return array Full records with record uid as key
*/
protected function fetchRowsFromDatabase(array $groupIds): array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->sourceTable);
$result = $queryBuilder
->select('*')
->from($this->sourceTable)
->where(
$queryBuilder->expr()->in(
'uid',
$queryBuilder->createNamedParameter(
$groupIds,
Connection::PARAM_INT_ARRAY
)
)
)
->execute();
$groups = [];
while ($row = $result->fetch()) {
$groups[(int)$row['uid']] = $row;
}
return $groups;
}
}
......@@ -93,6 +93,10 @@ services:
shared: false
public: true
TYPO3\CMS\Core\Authentication\GroupResolver:
shared: false
public: true
# FAL security checks for backend users
TYPO3\CMS\Core\Resource\Security\StoragePermissionsAspect:
tags:
......
.. include:: ../../Includes.txt
====================================================================
Breaking: #93056 - Removed hooks when retrieving Backend user groups
====================================================================
See :issue:`93056`
Description
===========
When the user groups of a backend user are loaded, two hooks
(before fetching and after fetching) were in place to modify the
list of groups.
:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroupQuery']`
:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroups_postProcessing']`
This functionality is replaced by a new PHP "GroupResolver" class,
the hooks have been removed, and a new Event has been added instead.
Impact
======
Using this hook has no effect anymore, as the hooks are never called in TYPO3 v11.0.
Affected Installations
======================
TYPO3 installations with custom extensions using these hooks,
which is usually around enhancing the permission system or custom
group resolving.
Migration
=========
When user groups are loaded, for example when a backend editors' groups and permissions
are calculated, a new PSR-14 event `AfterGroupsResolvedEvent` is fired.
The hooks have been removed without deprecation in order to allow
extensions to make their extension compatible with TYPO3 v10 (using the hooks),
and TYPO3 v11 (use the PSR-14 instead).
.. index:: PHP-API, FullyScanned, ext:backend
.. include:: ../../Includes.txt
====================================================================
Feature: #93056 - New Event after retrieving user groups recursively
====================================================================
See :issue:`93056`
Description
===========
When user groups are loaded, for example when a backend editors' groups and permissions
are calculated, a new PSR-14 event `AfterGroupsResolvedEvent` is fired.
Impact
======
This Event contains a list of retrieved groups from the database, which can
be modified (e.g. adding more groups when a particular user or a user from a
given location is logged in) via Event listeners.
This event acts as a substitution for the removed TYPO3 Hook
:php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroups_postProcessing']`.
.. index:: PHP-API, ext:backend
......@@ -136,7 +136,7 @@ class BackendUserAuthenticationTest extends FunctionalTestCase
$subject = $this->setUpBackendUser(3);
$subject->fetchGroupData();
self::assertEquals('web_info,web_layout,web_list,file_filelist', $subject->groupData['modules']);
self::assertEquals(['1', '4', '5', '3', '2', '6'], $subject->userGroupsUID);
self::assertEquals([1, 4, 5, 3, 2, 6], $subject->userGroupsUID);
self::assertEquals(['groupValue' => 'from_group_6', 'userValue' => 'from_user_3'], $subject->getTSConfig()['test.']['default.']);
}
}
......@@ -465,4 +465,16 @@ return [
'Breaking-92941-LockToIPUserTsConfigOptionRemoved.rst',
],
],
'$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'t3lib/class.t3lib_userauthgroup.php\'][\'fetchGroupQuery\']' => [
'restFiles' => [
'Breaking-93056-RemovedHooksWhenRetrievingBackendUserGroups.rst',
'Feature-93056-NewEventAfterRetrievingUserGroupsRecursively.rst',
],
],
'$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'t3lib/class.t3lib_userauthgroup.php\'][\'fetchGroups_postProcessing\']' => [
'restFiles' => [
'Breaking-93056-RemovedHooksWhenRetrievingBackendUserGroups.rst',
'Feature-93056-NewEventAfterRetrievingUserGroupsRecursively.rst',
],
],
];
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment