From d7886920762d1ec5a3be9d286a8684288fc8e3dc Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gerg=C5=91=20Tisza?= Date: Mon, 11 Jun 2018 01:05:59 +0200 Subject: [PATCH] Add maintenance script for deleting local passwords This is mainly for the benefit of authentication extensions which all need similar functionality for removing local passwords on a wiki where local authentication was used for a while but has been disabled, but can be used directly to just indiscriminately remove the passwords of all users. To test the change without irreversibly locking out users, an option is provided to make the password invalid in an easy-to-reverse way. The immediate use case is I974184899c33. This patch also introduces the maintenance/includes directory to hold PHP files which are not executable scripts themselves. (Previously such files had a .inc extension, but that is so PHP4.) Bug: T57420 Change-Id: If7207b80a2c8374e90182e0b09d8f76ee94264b0 --- autoload.php | 1 + maintenance/deleteLocalPasswords.php | 27 +++ maintenance/includes/DeleteLocalPasswords.php | 186 ++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100644 maintenance/deleteLocalPasswords.php create mode 100644 maintenance/includes/DeleteLocalPasswords.php diff --git a/autoload.php b/autoload.php index b5b5c52060..38f6ba9cd8 100644 --- a/autoload.php +++ b/autoload.php @@ -378,6 +378,7 @@ $wgAutoloadLocalClasses = [ 'DeleteEqualMessages' => __DIR__ . '/maintenance/deleteEqualMessages.php', 'DeleteFileOp' => __DIR__ . '/includes/libs/filebackend/fileop/DeleteFileOp.php', 'DeleteLinksJob' => __DIR__ . '/includes/jobqueue/jobs/DeleteLinksJob.php', + 'DeleteLocalPasswords' => __DIR__ . '/maintenance/includes/DeleteLocalPasswords.php', 'DeleteLogFormatter' => __DIR__ . '/includes/logging/DeleteLogFormatter.php', 'DeleteOldRevisions' => __DIR__ . '/maintenance/deleteOldRevisions.php', 'DeleteOrphanedRevisions' => __DIR__ . '/maintenance/deleteOrphanedRevisions.php', diff --git a/maintenance/deleteLocalPasswords.php b/maintenance/deleteLocalPasswords.php new file mode 100644 index 0000000000..ecee23948c --- /dev/null +++ b/maintenance/deleteLocalPasswords.php @@ -0,0 +1,27 @@ + true or disable it + * completely before running this, otherwise it might recreate passwords. + * + * This class can also be used directly to just delete all local passwords, or those for a specific + * user. Deleting all passwords is useful when the wiki has used local password login in the past + * but it has been disabled. + */ +class DeleteLocalPasswords extends Maintenance { + /** @var string|null User to run on, or null for all. */ + protected $user; + + /** @var int Number of deleted passwords. */ + protected $total; + + public function __construct() { + parent::__construct(); + $this->mDescription = "Deletes local password for users."; + $this->setBatchSize( 1000 ); + + $this->addOption( 'user', 'If specified, only checks the given user', false, true ); + $this->addOption( 'delete', 'Really delete. To prevent accidents, you must provide this flag.' ); + $this->addOption( 'prefix', "Instead of deleting, make passwords invalid by prefixing with " + . "':null:'. Make sure PasswordConfig has a 'null' entry. This is meant for testing before " + . "hard delete." ); + $this->addOption( 'unprefix', 'Instead of deleting, undo the effect of --prefix.' ); + } + + protected function initialize() { + if ( + $this->hasOption( 'delete' ) + $this->hasOption( 'prefix' ) + + $this->hasOption( 'unprefix' ) !== 1 + ) { + $this->fatalError( "Exactly one of the 'delete', 'prefix', 'unprefix' options must be used\n" ); + } + if ( $this->hasOption( 'prefix' ) || $this->hasOption( 'unprefix' ) ) { + $passwordHashTypes = MediaWikiServices::getInstance()->getPasswordFactory()->getTypes(); + if ( + !isset( $passwordHashTypes['null'] ) + || $passwordHashTypes['null']['class'] !== InvalidPassword::class + ) { + $this->fatalError( +<<<'ERROR' +'null' password entry missing. To use password prefixing, add + $wgPasswordConfig['null'] = [ 'class' => InvalidPassword::class ]; +to your configuration (and remove once the passwords were deleted). +ERROR + ); + } + } + + $user = $this->getOption( 'user', false ); + if ( $user !== false ) { + $this->user = User::getCanonicalName( $user ); + if ( $this->user === false ) { + $this->fatalError( "Invalid user name\n" ); + } + } + } + + public function execute() { + $this->initialize(); + + foreach ( $this->getUserBatches() as $userBatch ) { + $this->processUsers( $userBatch, $this->getUserDB() ); + } + + $this->output( "done. (wrote $this->total rows)\n" ); + } + + /** + * Get the master DB handle for the current user batch. This is provided for the benefit + * of authentication extensions which subclass this and work with wiki farms. + */ + protected function getUserDB() { + return $this->getDB( DB_MASTER ); + } + + protected function processUsers( array $userBatch, IDatabase $dbw ) { + if ( !$userBatch ) { + return; + } + if ( $this->getOption( 'delete' ) ) { + $dbw->update( 'user', + [ 'user_password' => PasswordFactory::newInvalidPassword()->toString() ], + [ 'user_name' => $userBatch ], + __METHOD__ + ); + } elseif ( $this->getOption( 'prefix' ) ) { + $dbw->update( 'user', + [ 'user_password = ' . $dbw->buildConcat( [ $dbw->addQuotes( ':null:' ), + 'user_password' ] ) ], + [ + 'NOT (user_password ' . $dbw->buildLike( ':null:', $dbw->anyString() ) . ')', + "user_password != " . $dbw->addQuotes( PasswordFactory::newInvalidPassword()->toString() ), + 'user_password IS NOT NULL', + 'user_name' => $userBatch, + ], + __METHOD__ + ); + } elseif ( $this->getOption( 'unprefix' ) ) { + $dbw->update( 'user', + [ 'user_password = ' . $dbw->buildSubString( 'user_password', strlen( ':null:' ) + 1 ) ], + [ + 'user_password ' . $dbw->buildLike( ':null:', $dbw->anyString() ), + 'user_name' => $userBatch, + ], + __METHOD__ + ); + } + $this->total += $dbw->affectedRows(); + } + + /** + * This method iterates through the requested users and returns their names in batches of + * self::$mBatchSize. + * + * Subclasses should reimplement this and locate users who use the specific authentication + * method. The default implementation just iterates through all users. Extensions that work + * with wikifarm should also update self::getUserDB() as necessary. + * @return Generator + */ + protected function getUserBatches() { + if ( !is_null( $this->user ) ) { + $this->output( "\t ... querying '$this->user'\n" ); + yield [ $this->user ]; + return; + } + + $lastUsername = ''; + $dbw = $this->getDB( DB_MASTER ); + do { + $this->output( "\t ... querying from '$lastUsername'\n" ); + $users = $dbw->selectFieldValues( + 'user', + 'user_name', + [ + 'user_name > ' .$dbw->addQuotes( $lastUsername ), + ], + __METHOD__, + [ + 'LIMIT' => $this->getBatchSize(), + 'ORDER BY' => 'user_name ASC', + ] + ); + if ( $users ) { + yield $users; + $lastUsername = end( $users ); + } + } while ( count( $users ) === $this->getBatchSize() ); + } +} -- 2.20.1