Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.47% covered (warning)
89.47%
51 / 57
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
AntiSpoofPreAuthenticationProvider
89.47% covered (warning)
89.47%
51 / 57
66.67% covered (warning)
66.67%
4 / 6
24.67
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAuthenticationRequests
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 testForAccountCreation
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 testUserInternal
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
1 / 1
9
 testUserForCreation
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 getSpoofUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types=1 );
3/**
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
18 */
19
20namespace MediaWiki\Extension\AntiSpoof;
21
22use MediaWiki\Auth\AbstractPreAuthenticationProvider;
23use MediaWiki\Auth\AuthenticationRequest;
24use MediaWiki\Auth\AuthManager;
25use MediaWiki\Context\RequestContext;
26use MediaWiki\Language\RawMessage;
27use MediaWiki\Message\Message;
28use MediaWiki\Permissions\PermissionManager;
29use MediaWiki\User\User;
30use MediaWiki\User\UserIdentity;
31use Psr\Log\LoggerInterface;
32use Psr\Log\NullLogger;
33use StatusValue;
34
35class AntiSpoofPreAuthenticationProvider extends AbstractPreAuthenticationProvider {
36    /** @var bool False effectively disables this provider, but spoofed names will still be logged. */
37    protected bool $antiSpoofAccounts;
38
39    /**
40     * @param PermissionManager $permissionManager
41     * @param array $params Options:
42     * - antiSpoofAccounts: (bool) stop spoofed accounts from being created. When false, only log.
43     */
44    public function __construct(
45        private readonly PermissionManager $permissionManager,
46        array $params = [],
47    ) {
48        global $wgAntiSpoofAccounts;
49
50        $params += [ 'antiSpoofAccounts' => $wgAntiSpoofAccounts ];
51
52        $this->antiSpoofAccounts = $params['antiSpoofAccounts'];
53    }
54
55    /** @inheritDoc */
56    public function getAuthenticationRequests( $action, array $options ) {
57        $needed = false;
58        if ( $action == AuthManager::ACTION_CREATE ) {
59            $user = User::newFromName( $options['username'] ) ?: new User();
60            $needed = $this->antiSpoofAccounts &&
61                $this->permissionManager->userHasAnyRight( $user, 'override-antispoof' );
62        }
63
64        return $needed ? [ new AntiSpoofAuthenticationRequest() ] : [];
65    }
66
67    /** @inheritDoc */
68    public function testForAccountCreation( $user, $creator, array $reqs ) {
69        /** @var AntiSpoofAuthenticationRequest $req */
70        $req = AuthenticationRequest::getRequestByClass( $reqs, AntiSpoofAuthenticationRequest::class );
71        $override = $req && $req->ignoreAntiSpoof && $creator->isAllowed( 'override-antispoof' );
72
73        return $this->testUserInternal( $user, $override, $this->logger );
74    }
75
76    private function testUserInternal( UserIdentity $user, bool $override, LoggerInterface $logger ): StatusValue {
77        $name = $user->getName();
78        $spoofUser = $this->getSpoofUser( $user );
79        $mode = !$this->antiSpoofAccounts ? 'LOGGING ' : ( $override ? 'OVERRIDE ' : '' );
80        $active = $this->antiSpoofAccounts && !$override;
81
82        if ( $spoofUser->isLegal() ) {
83            $normalized = $spoofUser->getNormalized();
84            $conflicts = $spoofUser->getConflicts();
85            if ( !$conflicts ) {
86                $logger->debug( "{mode}PASS new account '{name}' [{normalized}]", [
87                    'mode' => $mode,
88                    'name' => $name,
89                    'normalized' => $normalized,
90                ] );
91            } else {
92                $logger->info( "{mode}CONFLICT new account '{name}' [{normalized}] spoofs {spoofs}", [
93                    'mode' => $mode,
94                    'name' => $name,
95                    'normalized' => $normalized,
96                    'spoofs' => $conflicts,
97                ] );
98
99                if ( $active ) {
100                    $list = [];
101                    foreach ( $conflicts as $simUser ) {
102                        $list[] = "* " . wfMessage( 'antispoof-conflict-item', $simUser )->plain();
103                    }
104                    $list = implode( "\n", $list );
105
106                    return StatusValue::newFatal(
107                        'antispoof-conflict',
108                        $name,
109                        Message::numParam( count( $conflicts ) ),
110                        // Avoid forced wikitext escaping for params in the Status class
111                        new RawMessage( $list )
112                    );
113                }
114            }
115        } else {
116            $error = $spoofUser->getErrorStatus();
117            $logger->info( "{mode}ILLEGAL new account '{name}' {error}", [
118                'mode' => $mode,
119                'name' => $name,
120                'error' => $error->getMessage( false, false, 'en' )->text(),
121            ] );
122            if ( $active ) {
123                return StatusValue::newFatal( 'antispoof-name-illegal', $name,
124                    $error->getMessage() );
125            }
126        }
127        return StatusValue::newGood();
128    }
129
130    /** @inheritDoc */
131    public function testUserForCreation( $user, $autocreate, array $options = [] ) {
132        $sv = StatusValue::newGood();
133
134        // For "cancreate" checks via the API, test if the current user could
135        // create the username.
136        if ( $this->antiSpoofAccounts && !$autocreate && empty( $options['creating'] ) &&
137            !$this->permissionManager->userHasAnyRight( RequestContext::getMain()->getUser(), 'override-antispoof' )
138        ) {
139            $sv->merge( $this->testUserInternal( $user, false, new NullLogger ) );
140        }
141
142        return $sv;
143    }
144
145    protected function getSpoofUser( UserIdentity $user ): SpoofUser {
146        return new SpoofUser( $user->getName() );
147    }
148}