From: Gergő Tisza Date: Sun, 10 Jun 2018 23:05:59 +0000 (+0200) Subject: Add maintenance script for deleting local passwords X-Git-Tag: 1.34.0-rc.0~4576 X-Git-Url: https://git.heureux-cyclage.org/?p=lhc%2Fweb%2Fwiklou.git;a=commitdiff_plain;h=d7886920762d1ec5a3be9d286a8684288fc8e3dc;hp=fb73286fba73c399e119ef50ff036255dd6a1096 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 --- 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() ); + } +}