Add UserGroupManager::getUserPrivilegedGroups()
This moves the core part of wfGetPrivilegedGroups() out of Wikimedia config and makes it possible to move functionality built on it into core. Bug: T208477 Change-Id: I6536ef2909caeed047447e8b6a25831d6f00d827
This commit is contained in:
parent
b71cf7de9e
commit
7a21b9a032
11 changed files with 226 additions and 1 deletions
|
|
@ -25,6 +25,8 @@ For notes on 1.40.x and older releases, see HISTORY.
|
|||
* …
|
||||
|
||||
==== New configuration ====
|
||||
* $wgPrivilegedGroups – Users belonging in some of the listed groups will be
|
||||
audited more aggressively.
|
||||
* …
|
||||
|
||||
==== Changed configuration ====
|
||||
|
|
|
|||
|
|
@ -2115,6 +2115,7 @@ $wgAutoloadLocalClasses = [
|
|||
'MediaWiki\\User\\Hook\\UserLoadAfterLoadFromSessionHook' => __DIR__ . '/includes/user/Hook/UserLoadAfterLoadFromSessionHook.php',
|
||||
'MediaWiki\\User\\Hook\\UserLoadDefaultsHook' => __DIR__ . '/includes/user/Hook/UserLoadDefaultsHook.php',
|
||||
'MediaWiki\\User\\Hook\\UserLogoutHook' => __DIR__ . '/includes/user/Hook/UserLogoutHook.php',
|
||||
'MediaWiki\\User\\Hook\\UserPrivilegedGroupsHook' => __DIR__ . '/includes/user/Hook/UserPrivilegedGroupsHook.php',
|
||||
'MediaWiki\\User\\Hook\\UserRemoveGroupHook' => __DIR__ . '/includes/user/Hook/UserRemoveGroupHook.php',
|
||||
'MediaWiki\\User\\Hook\\UserSaveSettingsHook' => __DIR__ . '/includes/user/Hook/UserSaveSettingsHook.php',
|
||||
'MediaWiki\\User\\Hook\\UserSendConfirmationMailHook' => __DIR__ . '/includes/user/Hook/UserSendConfirmationMailHook.php',
|
||||
|
|
|
|||
|
|
@ -4960,6 +4960,19 @@ config-schema:
|
|||
Functionality to make pages inaccessible has not been extensively tested
|
||||
for security. Use at your own risk!
|
||||
This replaces $wgWhitelistAccount and $wgWhitelistEdit
|
||||
PrivilegedGroups:
|
||||
default:
|
||||
- bureaucrat
|
||||
- interface-admin
|
||||
- suppress
|
||||
- sysop
|
||||
type: array
|
||||
description: |-
|
||||
List of groups which should be considered privileged (user accounts
|
||||
belonging in these groups can be abused in dangerous ways).
|
||||
This is used for some security checks, mainly logging.
|
||||
@since 1.41
|
||||
@see UserGroupManager::getUserPrivilegedGroups()
|
||||
RevokePermissions:
|
||||
default: { }
|
||||
type: object
|
||||
|
|
|
|||
|
|
@ -2729,6 +2729,12 @@ $wgHideIdentifiableRedirects = null;
|
|||
*/
|
||||
$wgGroupPermissions = null;
|
||||
|
||||
/**
|
||||
* Config variable stub for the PrivilegedGroups setting, for use by phpdoc and IDEs.
|
||||
* @see MediaWiki\MainConfigSchema::PrivilegedGroups
|
||||
*/
|
||||
$wgPrivilegedGroups = null;
|
||||
|
||||
/**
|
||||
* Config variable stub for the RevokePermissions setting, for use by phpdoc and IDEs.
|
||||
* @see MediaWiki\MainConfigSchema::RevokePermissions
|
||||
|
|
|
|||
|
|
@ -557,6 +557,7 @@ class HookRunner implements
|
|||
\MediaWiki\User\Hook\UserLoadAfterLoadFromSessionHook,
|
||||
\MediaWiki\User\Hook\UserLoadDefaultsHook,
|
||||
\MediaWiki\User\Hook\UserLogoutHook,
|
||||
\MediaWiki\User\Hook\UserPrivilegedGroupsHook,
|
||||
\MediaWiki\User\Hook\UserRemoveGroupHook,
|
||||
\MediaWiki\User\Hook\UserSaveSettingsHook,
|
||||
\MediaWiki\User\Hook\UserSendConfirmationMailHook,
|
||||
|
|
@ -4234,6 +4235,13 @@ class HookRunner implements
|
|||
);
|
||||
}
|
||||
|
||||
public function onUserPrivilegedGroups( $userIdentity, &$groups ) {
|
||||
return $this->container->run(
|
||||
'UserPrivilegedGroups',
|
||||
[ $userIdentity, &$groups ]
|
||||
);
|
||||
}
|
||||
|
||||
public function onUserGetReservedNames( &$reservedUsernames ) {
|
||||
return $this->container->run(
|
||||
'UserGetReservedNames',
|
||||
|
|
|
|||
|
|
@ -2744,6 +2744,12 @@ class MainConfigNames {
|
|||
*/
|
||||
public const GroupPermissions = 'GroupPermissions';
|
||||
|
||||
/**
|
||||
* Name constant for the PrivilegedGroups setting, for use with Config::get()
|
||||
* @see MainConfigSchema::PrivilegedGroups
|
||||
*/
|
||||
public const PrivilegedGroups = 'PrivilegedGroups';
|
||||
|
||||
/**
|
||||
* Name constant for the RevokePermissions setting, for use with Config::get()
|
||||
* @see MainConfigSchema::RevokePermissions
|
||||
|
|
|
|||
|
|
@ -7879,6 +7879,23 @@ class MainConfigSchema {
|
|||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* List of groups which should be considered privileged (user accounts
|
||||
* belonging in these groups can be abused in dangerous ways).
|
||||
* This is used for some security checks, mainly logging.
|
||||
* @since 1.41
|
||||
* @see UserGroupManager::getUserPrivilegedGroups()
|
||||
*/
|
||||
public const PrivilegedGroups = [
|
||||
'default' => [
|
||||
'bureaucrat',
|
||||
'interface-admin',
|
||||
'suppress',
|
||||
'sysop',
|
||||
],
|
||||
'type' => 'list',
|
||||
];
|
||||
|
||||
/**
|
||||
* Permission keys revoked from users in each group.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1270,6 +1270,12 @@ return [
|
|||
'deletelogentry' => true,
|
||||
],
|
||||
],
|
||||
'PrivilegedGroups' => [
|
||||
0 => 'bureaucrat',
|
||||
1 => 'interface-admin',
|
||||
2 => 'suppress',
|
||||
3 => 'sysop',
|
||||
],
|
||||
'RevokePermissions' => [
|
||||
],
|
||||
'GroupInheritsPermissions' => [
|
||||
|
|
@ -2742,6 +2748,7 @@ return [
|
|||
'BlockCIDRLimit' => 'object',
|
||||
'EnablePartialActionBlocks' => 'boolean',
|
||||
'GroupPermissions' => 'object',
|
||||
'PrivilegedGroups' => 'array',
|
||||
'RevokePermissions' => 'object',
|
||||
'GroupInheritsPermissions' => 'object',
|
||||
'ImplicitGroups' => 'array',
|
||||
|
|
|
|||
25
includes/user/Hook/UserPrivilegedGroupsHook.php
Normal file
25
includes/user/Hook/UserPrivilegedGroupsHook.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\User\Hook;
|
||||
|
||||
use MediaWiki\User\UserIdentity;
|
||||
|
||||
/**
|
||||
* This is a hook handler interface, see docs/Hooks.md.
|
||||
* Use the hook name "UserPrivilegedGroups" to register handlers implementing this interface.
|
||||
*
|
||||
* @stable to implement
|
||||
* @ingroup Hooks
|
||||
*/
|
||||
interface UserPrivilegedGroupsHook {
|
||||
/**
|
||||
* This hook is called in UserGroupManager::getUserPrivilegedGroups().
|
||||
*
|
||||
* @since 1.41
|
||||
*
|
||||
* @param UserIdentity $userIdentity User identity to get groups for
|
||||
* @param string[] &$groups Current privileged groups
|
||||
* @return bool|void True or no return value to continue or false to abort
|
||||
*/
|
||||
public function onUserPrivilegedGroups( UserIdentity $userIdentity, array &$groups );
|
||||
}
|
||||
|
|
@ -72,6 +72,7 @@ class UserGroupManager implements IDBAccessObject {
|
|||
MainConfigNames::GroupsRemoveFromSelf,
|
||||
MainConfigNames::RevokePermissions,
|
||||
MainConfigNames::RemoveGroups,
|
||||
MainConfigNames::PrivilegedGroups,
|
||||
];
|
||||
|
||||
/** @var ServiceOptions */
|
||||
|
|
@ -122,6 +123,9 @@ class UserGroupManager implements IDBAccessObject {
|
|||
/** string key for former groups cache */
|
||||
private const CACHE_FORMER = 'former';
|
||||
|
||||
/** string key for former groups cache */
|
||||
private const CACHE_PRIVILEGED = 'privileged';
|
||||
|
||||
/**
|
||||
* @var array Service caches, an assoc. array keyed after the user-keys generated
|
||||
* by the getCacheKey method and storing values in the following format:
|
||||
|
|
@ -131,6 +135,7 @@ class UserGroupManager implements IDBAccessObject {
|
|||
* self::CACHE_EFFECTIVE => effective groups cache
|
||||
* self::CACHE_MEMBERSHIP => [ ] // Array of UserGroupMembership objects
|
||||
* self::CACHE_FORMER => former groups cache
|
||||
* self::CACHE_PRIVILEGED => privileged groups cache
|
||||
* ]
|
||||
*/
|
||||
private $userGroupCache = [];
|
||||
|
|
@ -142,8 +147,9 @@ class UserGroupManager implements IDBAccessObject {
|
|||
* userKey => [
|
||||
* self::CACHE_IMPLICIT => implicit groups query flag
|
||||
* self::CACHE_EFFECTIVE => effective groups query flag
|
||||
* self::CACHE_MEMBERSHIP => membership groups query flag
|
||||
* self::CACHE_MEMBERSHIP => membership groups query flag
|
||||
* self::CACHE_FORMER => former groups query flag
|
||||
* self::CACHE_PRIVILEGED => privileged groups query flag
|
||||
* ]
|
||||
*/
|
||||
private $queryFlagsUsedForCaching = [];
|
||||
|
|
@ -470,6 +476,58 @@ class UserGroupManager implements IDBAccessObject {
|
|||
return $promote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of privileged groups $user belongs to.
|
||||
* Privileged groups are ones that can be abused in dangerous way.
|
||||
*
|
||||
* Depending on how extensions extend this method, it might return values
|
||||
* that are not strictly user groups (ACL list names, etc). It is meant
|
||||
* for logging/auditing, not for passing to methods that expect group names.
|
||||
*
|
||||
* @param UserIdentity $user
|
||||
* @param int $queryFlags
|
||||
* @param bool $recache Whether to avoid the cache
|
||||
* @return string[]
|
||||
* @since 1.36
|
||||
* @see User::isPrivileged()
|
||||
* @see $wgPrivilegedGroups
|
||||
* @see https://www.mediawiki.org/wiki/Manual:Hooks/UserGetPrivilegedGroups
|
||||
*/
|
||||
public function getUserPrivilegedGroups(
|
||||
UserIdentity $user,
|
||||
int $queryFlags = self::READ_NORMAL,
|
||||
bool $recache = false
|
||||
): array {
|
||||
$userKey = $this->getCacheKey( $user );
|
||||
|
||||
if ( !$recache &&
|
||||
$this->canUseCachedValues( $user, self::CACHE_PRIVILEGED, $queryFlags ) &&
|
||||
isset( $this->userGroupCache[$userKey][self::CACHE_PRIVILEGED] )
|
||||
) {
|
||||
return $this->userGroupCache[$userKey][self::CACHE_PRIVILEGED];
|
||||
}
|
||||
|
||||
if ( !$user->isRegistered() ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$groups = array_intersect(
|
||||
$this->getUserEffectiveGroups( $user, $queryFlags, $recache ),
|
||||
$this->options->get( 'PrivilegedGroups' )
|
||||
);
|
||||
|
||||
$this->hookRunner->onUserPrivilegedGroups( $user, $groups );
|
||||
|
||||
$this->setCache(
|
||||
$this->getCacheKey( $user ),
|
||||
self::CACHE_PRIVILEGED,
|
||||
array_values( array_unique( $groups ) ),
|
||||
$queryFlags
|
||||
);
|
||||
|
||||
return $this->userGroupCache[$userKey][self::CACHE_PRIVILEGED];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively check a condition. Conditions are in the form
|
||||
* [ '&' or '|' or '^' or '!', cond1, cond2, ... ]
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ use MediaWiki\User\UserIdentity;
|
|||
use MediaWiki\User\UserIdentityValue;
|
||||
use MediaWiki\Utils\MWTimestamp;
|
||||
use MediaWikiIntegrationTestCase;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\MockObject\Rule\InvokedCount;
|
||||
use RequestContext;
|
||||
use SiteConfiguration;
|
||||
use TestLogger;
|
||||
|
|
@ -119,6 +121,20 @@ class UserGroupManagerTest extends MediaWikiIntegrationTestCase {
|
|||
$this->clearHooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a callable that must be called exactly $invokedCount times.
|
||||
* @param InvokedCount $invokedCount
|
||||
* @return callable|MockObject
|
||||
*/
|
||||
private function countPromise( $invokedCount ) {
|
||||
$mockHandler = $this->getMockBuilder( \stdClass::class )
|
||||
->addMethods( [ '__invoke' ] )
|
||||
->getMock();
|
||||
$mockHandler->expects( $invokedCount )
|
||||
->method( '__invoke' );
|
||||
return $mockHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserGroupManager $manager
|
||||
* @param UserIdentity $user
|
||||
|
|
@ -1223,4 +1239,70 @@ class UserGroupManagerTest extends MediaWikiIntegrationTestCase {
|
|||
$manager = $this->getManager( self::CHANGEABLE_GROUPS_TEST_CONFIG );
|
||||
$this->assertGroupsEquals( $expected, $manager->getGroupsChangeableByGroup( $group ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\User\UserGroupManager::getUserPrivilegedGroups()
|
||||
*/
|
||||
public function testGetUserPrivilegedGroups() {
|
||||
$this->setMwGlobals( 'wgPrivilegedGroups', [ 'sysop', 'interface-admin', 'bar', 'baz' ] );
|
||||
$makeHook = function ( $invocationCount, User $userToMatch, array $groupsToAdd ) {
|
||||
return function ( $u, &$groups ) use ( $userToMatch, $invocationCount, $groupsToAdd ) {
|
||||
$invocationCount();
|
||||
$this->assertTrue( $userToMatch->equals( $u ) );
|
||||
$groups = array_merge( $groups, $groupsToAdd );
|
||||
};
|
||||
};
|
||||
|
||||
$manager = $this->getManager();
|
||||
|
||||
$user = new User;
|
||||
$user->setName( '*Unregistered 1234' );
|
||||
|
||||
$this->assertArrayEquals(
|
||||
[],
|
||||
$manager->getUserPrivilegedGroups( $user )
|
||||
);
|
||||
|
||||
$user = $this->getTestUser( [ 'sysop', 'bot', 'interface-admin' ] )->getUser();
|
||||
|
||||
$this->setTemporaryHook( 'UserPrivilegedGroups',
|
||||
$makeHook( $this->countPromise( $this->once() ), $user, [ 'foo' ] ) );
|
||||
$this->setTemporaryHook( 'UserEffectiveGroups',
|
||||
$makeHook( $this->countPromise( $this->once() ), $user, [ 'bar', 'boom' ] ) );
|
||||
$this->assertArrayEquals(
|
||||
[ 'sysop', 'interface-admin', 'foo', 'bar' ],
|
||||
$manager->getUserPrivilegedGroups( $user )
|
||||
);
|
||||
$this->assertArrayEquals(
|
||||
[ 'sysop', 'interface-admin', 'foo', 'bar' ],
|
||||
$manager->getUserPrivilegedGroups( $user )
|
||||
);
|
||||
|
||||
$this->setTemporaryHook( 'UserPrivilegedGroups',
|
||||
$makeHook( $this->countPromise( $this->once() ), $user, [ 'baz' ] ) );
|
||||
$this->setTemporaryHook( 'UserEffectiveGroups',
|
||||
$makeHook( $this->countPromise( $this->once() ), $user, [ 'baz' ] ) );
|
||||
$this->assertArrayEquals(
|
||||
[ 'sysop', 'interface-admin', 'foo', 'bar' ],
|
||||
$manager->getUserPrivilegedGroups( $user )
|
||||
);
|
||||
$this->assertArrayEquals(
|
||||
[ 'sysop', 'interface-admin', 'baz' ],
|
||||
$manager->getUserPrivilegedGroups( $user, UserGroupManager::READ_NORMAL, true )
|
||||
);
|
||||
$this->assertArrayEquals(
|
||||
[ 'sysop', 'interface-admin', 'baz' ],
|
||||
$manager->getUserPrivilegedGroups( $user )
|
||||
);
|
||||
|
||||
$this->setTemporaryHook( 'UserPrivilegedGroups', static function () {
|
||||
} );
|
||||
$this->setTemporaryHook( 'UserEffectiveGroups', static function () {
|
||||
} );
|
||||
$user = $this->getTestUser( [] )->getUser();
|
||||
$this->assertArrayEquals(
|
||||
[],
|
||||
$manager->getUserPrivilegedGroups( $user, UserGroupManager::READ_NORMAL, true )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue