3 * Helper for deleting unused local passwords.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
21 * @ingroup Maintenance
24 use MediaWiki\MediaWikiServices
;
25 use Wikimedia\Rdbms\IDatabase
;
26 use Wikimedia\Rdbms\IMaintainableDatabase
;
28 require_once __DIR__
. '/../Maintenance.php';
31 * Delete unused local passwords.
33 * Mainly intended to be used as a base class by authentication extensions to provide maintenance
34 * scripts which allow deleting local passwords for users who have another way of logging in.
35 * Such scripts would customize how to locate users who have other login methods and don't need
36 * local login anymore.
37 * Make sure to set LocalPasswordPrimaryAuthenticationProvider to loginOnly => true or disable it
38 * completely before running this, otherwise it might recreate passwords.
40 * This class can also be used directly to just delete all local passwords, or those for a specific
41 * user. Deleting all passwords is useful when the wiki has used local password login in the past
42 * but it has been disabled.
44 class DeleteLocalPasswords
extends Maintenance
{
45 /** @var string|null User to run on, or null for all. */
48 /** @var int Number of deleted passwords. */
51 public function __construct() {
52 parent
::__construct();
53 $this->addDescription( "Deletes local password for users." );
54 $this->setBatchSize( 1000 );
56 $this->addOption( 'user', 'If specified, only checks the given user', false, true );
57 $this->addOption( 'delete', 'Really delete. To prevent accidents, you must provide this flag.' );
58 $this->addOption( 'prefix', "Instead of deleting, make passwords invalid by prefixing with "
59 . "':null:'. Make sure PasswordConfig has a 'null' entry. This is meant for testing before "
61 $this->addOption( 'unprefix', 'Instead of deleting, undo the effect of --prefix.' );
64 protected function initialize() {
66 $this->hasOption( 'delete' ) +
$this->hasOption( 'prefix' )
67 +
$this->hasOption( 'unprefix' ) !== 1
69 $this->fatalError( "Exactly one of the 'delete', 'prefix', 'unprefix' options must be used\n" );
71 if ( $this->hasOption( 'prefix' ) ||
$this->hasOption( 'unprefix' ) ) {
72 $passwordHashTypes = MediaWikiServices
::getInstance()->getPasswordFactory()->getTypes();
74 !isset( $passwordHashTypes['null'] )
75 ||
$passwordHashTypes['null']['class'] !== InvalidPassword
::class
79 'null' password entry missing. To use password prefixing, add
80 $wgPasswordConfig['null'] = [ 'class' => InvalidPassword::class ];
81 to your configuration (and remove once the passwords were deleted).
87 $user = $this->getOption( 'user', false );
88 if ( $user !== false ) {
89 $this->user
= User
::getCanonicalName( $user );
90 if ( $this->user
=== false ) {
91 $this->fatalError( "Invalid user name\n" );
96 public function execute() {
99 foreach ( $this->getUserBatches() as $userBatch ) {
100 $this->processUsers( $userBatch, $this->getUserDB() );
103 $this->output( "done. (wrote $this->total rows)\n" );
107 * Get the master DB handle for the current user batch. This is provided for the benefit
108 * of authentication extensions which subclass this and work with wiki farms.
109 * @return IMaintainableDatabase
111 protected function getUserDB() {
112 return $this->getDB( DB_MASTER
);
115 protected function processUsers( array $userBatch, IDatabase
$dbw ) {
119 if ( $this->getOption( 'delete' ) ) {
120 $dbw->update( 'user',
121 [ 'user_password' => PasswordFactory
::newInvalidPassword()->toString() ],
122 [ 'user_name' => $userBatch ],
125 } elseif ( $this->getOption( 'prefix' ) ) {
126 $dbw->update( 'user',
127 [ 'user_password = ' . $dbw->buildConcat( [ $dbw->addQuotes( ':null:' ),
128 'user_password' ] ) ],
130 'NOT (user_password ' . $dbw->buildLike( ':null:', $dbw->anyString() ) . ')',
131 "user_password != " . $dbw->addQuotes( PasswordFactory
::newInvalidPassword()->toString() ),
132 'user_password IS NOT NULL',
133 'user_name' => $userBatch,
137 } elseif ( $this->getOption( 'unprefix' ) ) {
138 $dbw->update( 'user',
139 [ 'user_password = ' . $dbw->buildSubString( 'user_password', strlen( ':null:' ) +
1 ) ],
141 'user_password ' . $dbw->buildLike( ':null:', $dbw->anyString() ),
142 'user_name' => $userBatch,
147 $this->total +
= $dbw->affectedRows();
148 MediaWikiServices
::getInstance()->getDBLoadBalancerFactory()->waitForReplication();
152 * This method iterates through the requested users and returns their names in batches of
155 * Subclasses should reimplement this and locate users who use the specific authentication
156 * method. The default implementation just iterates through all users. Extensions that work
157 * with wikifarm should also update self::getUserDB() as necessary.
160 protected function getUserBatches() {
161 if ( !is_null( $this->user
) ) {
162 $this->output( "\t ... querying '$this->user'\n" );
163 yield
[ [ $this->user
] ];
168 $dbw = $this->getDB( DB_MASTER
);
170 $this->output( "\t ... querying from '$lastUsername'\n" );
171 $users = $dbw->selectFieldValues(
175 'user_name > ' . $dbw->addQuotes( $lastUsername ),
179 'LIMIT' => $this->getBatchSize(),
180 'ORDER BY' => 'user_name ASC',
185 $lastUsername = end( $users );
187 } while ( count( $users ) === $this->getBatchSize() );