Gergő Tisza has uploaded a new change for review. https://gerrit.wikimedia.org/r/287500
Change subject: Update for AuthManager ...................................................................... Update for AuthManager This is a breaking change for the API as the capitalization of the parameter has changed from ignoreantispoof to ignoreAntiSpoof. Bug: T110449 Change-Id: If71135d1cae428abb7e6375ccb8c371cba53d147 --- M AntiSpoof.php A AntiSpoofAuthenticationRequest.php M AntiSpoofHooks.php A AntiSpoofPreAuthenticationProvider.php M i18n/en.json M i18n/qqq.json A tests/AntiSpoofAuthenticationRequestTest.php A tests/AntiSpoofPreAuthenticationProviderTest.php 8 files changed, 272 insertions(+), 5 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/AntiSpoof refs/changes/00/287500/1 diff --git a/AntiSpoof.php b/AntiSpoof.php index 566b145..f5f5239 100644 --- a/AntiSpoof.php +++ b/AntiSpoof.php @@ -1,4 +1,5 @@ <?php + if ( !defined( 'MEDIAWIKI' ) ) { exit( 1 ); } @@ -54,17 +55,17 @@ $wgAutoloadClasses['AntiSpoof'] = "$dir/AntiSpoof_body.php"; $wgAutoloadClasses['AntiSpoofHooks'] = "$dir/AntiSpoofHooks.php"; $wgAutoloadClasses['SpoofUser'] = "$dir/SpoofUser.php"; +$wgAutoloadClasses['AntiSpoofAuthenticationRequest'] = "$dir/AntiSpoofAuthenticationRequest.php"; +$wgAutoloadClasses['AntiSpoofPreAuthenticationProvider'] + = "$dir/AntiSpoofPreAuthenticationProvider.php"; // Register the API method $wgAutoloadClasses['ApiAntiSpoof'] = "$dir/api/ApiAntiSpoof.php"; $wgAPIModules['antispoof'] = 'ApiAntiSpoof'; $wgHooks['LoadExtensionSchemaUpdates'][] = 'AntiSpoofHooks::asUpdateSchema'; -$wgHooks['AbortNewAccount'][] = 'AntiSpoofHooks::asAbortNewAccountHook'; -$wgHooks['UserCreateForm'][] = 'AntiSpoofHooks::asUserCreateFormHook'; -$wgHooks['AddNewAccount'][] = 'AntiSpoofHooks::asAddNewAccountHook'; $wgHooks['RenameUserComplete'][] = 'AntiSpoofHooks::asAddRenameUserHook'; $wgHooks['DeleteAccount'][] = 'AntiSpoofHooks::asDeleteAccount'; $wgHooks['UnitTestsList'][] = 'AntiSpoofHooks::asUnitTestsList'; -$wgHooks['APIGetAllowedParams'][] = 'AntiSpoofHooks::onAPIGetAllowedParams'; -$wgHooks['AddNewAccountApiForm'][] = 'AntiSpoofHooks::addNewAccountApiForm'; + +$wgExtensionFunctions[] = 'AntiSpoofHooks::onRegistration'; diff --git a/AntiSpoofAuthenticationRequest.php b/AntiSpoofAuthenticationRequest.php new file mode 100644 index 0000000..cafd872 --- /dev/null +++ b/AntiSpoofAuthenticationRequest.php @@ -0,0 +1,18 @@ +<?php + +use MediaWiki\Auth\AuthenticationRequest; + +class AntiSpoofAuthenticationRequest extends AuthenticationRequest { + public $ignoreAntiSpoof; + + public function getFieldInfo() { + return [ + 'ignoreAntiSpoof' => [ + 'type' => 'checkbox', + 'label' => wfMessage( 'antispoof-ignore' ), + 'help' => wfMessage( 'antispoof-ignore-help' ), + 'optional' => true, + ], + ]; + } +} diff --git a/AntiSpoofHooks.php b/AntiSpoofHooks.php index a28a902..1ca126a 100644 --- a/AntiSpoofHooks.php +++ b/AntiSpoofHooks.php @@ -1,6 +1,27 @@ <?php +use MediaWiki\Auth\AuthManager; + class AntiSpoofHooks { + /** + * Called after global variables have been set up + */ + public static function onRegistration() { + global $wgDisableAuthManager, $wgAuthManagerConfig; + + if ( class_exists( AuthManager::class ) && !$wgDisableAuthManager ) { + $wgAuthManagerConfig['preauth'][AntiSpoofPreAuthenticationProvider::class] = + [ 'class' => AntiSpoofPreAuthenticationProvider::class ]; + Hooks::register( 'LocalUserCreated', 'AntiSpoofHooks::asLocalUserCreated' ); + } else { + Hooks::register( 'AbortNewAccount', 'AntiSpoofHooks::asAbortNewAccountHook' ); + Hooks::register( 'UserCreateForm', 'AntiSpoofHooks::asUserCreateFormHook' ); + Hooks::register( 'AddNewAccount', 'AntiSpoofHooks::asAddNewAccountHook' ); + Hooks::register( 'APIGetAllowedParams', 'AntiSpoofHooks::onAPIGetAllowedParams' ); + Hooks::register( 'AddNewAccountApiForm', 'AntiSpoofHooks::addNewAccountApiForm' ); + } + } + /** * @param $updater DatabaseUpdater * @return bool @@ -97,6 +118,19 @@ } /** + * On new account creation, record the username's thing-bob. + * Replaces AddNewAccountHook for more modern MediaWiki versions- + * + * @param $user User + * @return bool + */ + public static function asLocalUserCreated( $user ) { + $spoof = new SpoofUser( $user->getName() ); + $spoof->record(); + return true; + } + + /** * On rename, remove the old entry and add the new * (After a sucessful user rename) * diff --git a/AntiSpoofPreAuthenticationProvider.php b/AntiSpoofPreAuthenticationProvider.php new file mode 100644 index 0000000..e93d360 --- /dev/null +++ b/AntiSpoofPreAuthenticationProvider.php @@ -0,0 +1,98 @@ +<?php + +use MediaWiki\Auth\AbstractPreAuthenticationProvider; +use MediaWiki\Auth\AuthenticationRequest; +use MediaWiki\Auth\AuthManager; + +class AntiSpoofPreAuthenticationProvider extends AbstractPreAuthenticationProvider { + /** @var bool False effectively disables this provider, but spoofed names will still be logged. */ + protected $antiSpoofAccounts; + + /** + * Options: + * - antiSpoofAccounts: (bool) stop spoofed accounts from being created. When false, only log. + * @param array $params + */ + public function __construct( array $params = [] ) { + global $wgAntiSpoofAccounts; + + $params += [ 'antiSpoofAccounts' => $wgAntiSpoofAccounts ]; + + $this->antiSpoofAccounts = $params['antiSpoofAccounts']; + } + + public function getAuthenticationRequests( $action, array $options ) { + $needed = false; + switch ( $action ) { + case AuthManager::ACTION_CREATE: + $user = User::newFromName( $options['username'] ) ?: new User(); + $needed = $this->antiSpoofAccounts && $user->isAllowed( 'override-antispoof' ); + break; + } + + return $needed ? [ new AntiSpoofAuthenticationRequest() ] : []; + } + + public function testForAccountCreation( $user, $creator, array $reqs ) { + /** @var AntiSpoofAuthenticationRequest $req */ + $req = AuthenticationRequest::getRequestByClass( $reqs, AntiSpoofAuthenticationRequest::class ); + $spoofUser = $this->getSpoofUser( $user ); + $override = $req && $req->ignoreAntiSpoof && $creator->isAllowed( 'override-antispoof' ); + $active = $this->antiSpoofAccounts && !$override; + $mode = !$this->antiSpoofAccounts ? 'LOGGING ' : $override ? 'OVERRIDE ' : ''; + + if ( $spoofUser->isLegal() ) { + $normalized = $spoofUser->getNormalized(); + $conflicts = $spoofUser->getConflicts(); + if ( empty( $conflicts ) ) { + $this->logger->debug( "{mode}PASS new account '{name}' [{normalized}]", [ + 'mode' => $mode, + 'name' => $user->getName(), + 'normalized' => $normalized, + ] ); + } else { + $this->logger->info( "{mode}CONFLICT new account '{name}' [{normalized}] spoofs {spoofs}", [ + 'mode' => $mode, + 'name' => $user->getName(), + 'normalized' => $normalized, + 'spoofs' => $conflicts, + ] ); + if ( $active ) { + $cnt = count( $conflicts ); + $list = ''; + foreach ( $conflicts as $simUser ) { + $list .= Html::element( 'li', [], + wfMessage( 'antispoof-conflict-item', $simUser ) ); + } + $list = Html::rawElement( 'ul', [], $list ); + + $message = new RawMessage( '$1$2$3', [ + wfMessage( 'antispoof-conflict-top', $user->getName() )->numParams( $cnt ), + $list, + wfMessage( 'antispoof-conflict-bottom' ) + ] ); + return StatusValue::newFatal( $message ); + } + } + } else { + $error = $spoofUser->getError(); + $this->logger->info( "{mode}ILLEGAL new account '{name}' {error}", [ + 'mode' => $mode, + 'name' => $user->getName(), + 'error' => $error, + ] ); + if ( $active ) { + return StatusValue::newFatal( 'antispoof-name-illegal', $user->getName(), $error ); + } + } + return StatusValue::newGood(); + } + + /** + * @param User $user + * @return SpoofUser + */ + protected function getSpoofUser( User $user ) { + return new SpoofUser( $user->getName() ); + } +} diff --git a/i18n/en.json b/i18n/en.json index c382244..a49d48e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -19,6 +19,7 @@ "antispoof-mixedscripts": "Contains incompatible mixed scripts", "antispoof-tooshort": "Canonicalized name too short", "antispoof-ignore": "Ignore spoofing checks", + "antispoof-ignore-help": "Allows users with sufficient privileges to create an account even if its name is similar to that of an existing account.", "right-override-antispoof": "Override the spoofing checks", "apihelp-antispoof-description": "Check a username against AntiSpoof's normalisation checks.", "apihelp-antispoof-param-username": "The username to check against AntiSpoof.", diff --git a/i18n/qqq.json b/i18n/qqq.json index dce6734..9665dbd 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -27,6 +27,7 @@ "antispoof-mixedscripts": "Reason for failed account creation.", "antispoof-tooshort": "Reason for failed account creation.", "antispoof-ignore": "This is a checkbox shown on [[Special:UserLogin|a signup page]] when a user with both [[MediaWiki:Right-createaccount/qqq|createaccount]] and [[MediaWiki:Right-override-antispoof/qqq|override-antispoof]] rights tries to register a new user account. It allows to register a username that would otherwise be blocked by the [[mw:Extension:AntiSpoof|AntiSpoof extension]].", + "antispoof-ignore-help": "API help message describing {{msg-mw|antispoof-ignore}}.", "right-override-antispoof": "{{doc-right|override-antispoof}}", "apihelp-antispoof-description": "{{doc-apihelp-description|antispoof}}", "apihelp-antispoof-param-username": "{{doc-apihelp-param|antispoof|username}}", diff --git a/tests/AntiSpoofAuthenticationRequestTest.php b/tests/AntiSpoofAuthenticationRequestTest.php new file mode 100644 index 0000000..a485538 --- /dev/null +++ b/tests/AntiSpoofAuthenticationRequestTest.php @@ -0,0 +1,17 @@ +<?php + +use MediaWiki\Auth\AuthenticationRequestTestCase; + +class AntiSpoofAuthenticationRequestTest extends AuthenticationRequestTestCase { + + protected function getInstance( array $args = [] ) { + return new AntiSpoofAuthenticationRequest(); + } + + public function provideLoadFromSubmission() { + return [ + 'empty' => [ [], [], [ 'ignoreAntiSpoof' => false ] ], + 'true' => [ [], [ 'ignoreAntiSpoof' => '1' ], [ 'ignoreAntiSpoof' => true ] ], + ]; + } +} diff --git a/tests/AntiSpoofPreAuthenticationProviderTest.php b/tests/AntiSpoofPreAuthenticationProviderTest.php new file mode 100644 index 0000000..abac19a --- /dev/null +++ b/tests/AntiSpoofPreAuthenticationProviderTest.php @@ -0,0 +1,97 @@ +<?php + +use MediaWiki\Auth\AuthManager; + +/** + * @group Database + */ +class AntiSpoofPreAuthenticationProviderTest extends MediaWikiTestCase { + public function setUp() { + global $wgDisableAuthManager; + if ( !class_exists( AuthManager::class ) || $wgDisableAuthManager ) { + $this->markTestSkipped( 'AuthManager is disabled' ); + } + + parent::setUp(); + } + + /** + * @dataProvider provideGetAuthenticationRequests + */ + public function testGetAuthenticationRequests( $action, $params, $username, $expectedReqs ) { + $this->setMwGlobals( 'wgAntiSpoofAccounts', false ); + $provider = new AntiSpoofPreAuthenticationProvider( $params ); + $provider->setManager( AuthManager::singleton() ); + $reqs = $provider->getAuthenticationRequests( $action, [ 'username' => $username ] ); + $this->assertEquals( $expectedReqs, $reqs ); + } + + public function provideGetAuthenticationRequests() { + return [ + [ AuthManager::ACTION_LOGIN, [], null, [] ], + [ AuthManager::ACTION_CREATE, [], null, [] ], + [ AuthManager::ACTION_CREATE, [ 'antiSpoofAccounts' => true ], null, [] ], + [ AuthManager::ACTION_CREATE, [], 'UTSysop', [] ], + [ AuthManager::ACTION_CREATE, [ 'antiSpoofAccounts' => true ], 'UTSysop', + [ new AntiSpoofAuthenticationRequest() ] ], + [ AuthManager::ACTION_CHANGE, [], null, [] ], + [ AuthManager::ACTION_REMOVE, [], null, [] ], + ]; + } + + /** + * @dataProvider provideTestForAccountCreation + */ + public function testTestForAccountCreation( + $enabled, $isLegal, $conflicts, $creator, $reqs, $error + ) { + $provider = $this->getMockBuilder( AntiSpoofPreAuthenticationProvider::class ) + ->setConstructorArgs( [ [ 'antiSpoofAccounts' => $enabled ] ] ) + ->setMethods( [ 'getSpoofUser' ] )->getMock(); + $spoofUser = $this->getMockBuilder( SpoofUser::class ) + ->disableOriginalConstructor()->getMock(); + $provider->expects( $this->any() )->method( 'getSpoofUser' )->willReturn( $spoofUser ); + /** @var $provider \MediaWiki\Auth\PreAuthenticationProvider */ + $provider->setManager( AuthManager::singleton() ); + $provider->setLogger( new \Psr\Log\NullLogger() ); + + $spoofUser->expects( $this->any() )->method( 'isLegal' )->willReturn( $isLegal ); + $spoofUser->expects( $this->any() )->method( 'getConflicts' )->willReturn( $conflicts ); + + /** @var StatusValue $status */ + $status = $provider->testForAccountCreation( new User(), $creator, $reqs ); + + if ( $error ) { + $this->assertFalse( $status->isGood() ); + $this->assertEquals( $error, Status::wrap( $status )->getMessage()->getKey() ); + } else { + $this->assertTrue( $status->isGood() ); + } + } + + public function provideTestForAccountCreation() { + $user = new User(); + $sysop = User::newFromName( 'UTSysop' ); + $noSkip = new AntiSpoofAuthenticationRequest(); + $skip = new AntiSpoofAuthenticationRequest(); + $skip->ignoreAntiSpoof = true; + + return [ + // enabled, isLegal, conflicts, creator, reqs, error + 'no spoofing' => [ true, true, [], $user, [], null ], + 'illegal' => [ true, false, [], $user, [], 'antispoof-name-illegal' ], + 'illegal, inactve' => [ false, false, [], $user, [], null ], + 'illegal, sysop w/o skipping' => [ true, false, [], $sysop, [], + 'antispoof-name-illegal' ], + 'illegal, sysop w/o skipping #2' => [ true, false, [], $sysop, [ $noSkip ], + 'antispoof-name-illegal' ], + 'illegal, sysop skipping' => [ true, false, [], $sysop, [ $skip ], null ], + // this should never happen but is good for layered defense + 'fake skipping' => [ true, false, [], $user, [ $skip ], 'antispoof-name-illegal' ], + 'conflicts' => [ true, true, [ 'x' ], $user, [], '$1$2$3' ], + 'conflicts w/ skipping' => [ true, true, [ 'x' ], $sysop, [ $skip ], null ], + 'conflicts w/ fake skipping' => [ true, true, [ 'x' ], $user, [ $skip ], '$1$2$3' ], + 'illegal takes priority' => [ true, false, [ 'x' ], $user, [], 'antispoof-name-illegal' ], + ]; + } +} -- To view, visit https://gerrit.wikimedia.org/r/287500 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: If71135d1cae428abb7e6375ccb8c371cba53d147 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/AntiSpoof Gerrit-Branch: master Gerrit-Owner: Gergő Tisza <gti...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits