Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
21.60% |
35 / 162 |
|
11.11% |
1 / 9 |
CRAP | |
0.00% |
0 / 1 |
OATHUserRepository | |
21.60% |
35 / 162 |
|
11.11% |
1 / 9 |
277.87 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
setLogger | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
findByUser | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
persist | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
20 | |||
createKey | |
90.00% |
27 / 30 |
|
0.00% |
0 / 1 |
4.02 | |||
removeKey | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
12 | |||
remove | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
removeAll | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
2 | |||
loadKeysFromDatabase | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | */ |
18 | |
19 | namespace MediaWiki\Extension\OATHAuth; |
20 | |
21 | use BagOStuff; |
22 | use FormatJson; |
23 | use InvalidArgumentException; |
24 | use MediaWiki\Config\ConfigException; |
25 | use MediaWiki\Extension\OATHAuth\Notifications\Manager; |
26 | use MediaWiki\User\CentralId\CentralIdLookupFactory; |
27 | use MediaWiki\User\User; |
28 | use MWException; |
29 | use Psr\Log\LoggerAwareInterface; |
30 | use Psr\Log\LoggerInterface; |
31 | use RequestContext; |
32 | use RuntimeException; |
33 | use Wikimedia\Rdbms\IConnectionProvider; |
34 | |
35 | class OATHUserRepository implements LoggerAwareInterface { |
36 | private IConnectionProvider $dbProvider; |
37 | |
38 | private BagOStuff $cache; |
39 | |
40 | private OATHAuthModuleRegistry $moduleRegistry; |
41 | |
42 | private CentralIdLookupFactory $centralIdLookupFactory; |
43 | |
44 | private LoggerInterface $logger; |
45 | |
46 | public function __construct( |
47 | IConnectionProvider $dbProvider, |
48 | BagOStuff $cache, |
49 | OATHAuthModuleRegistry $moduleRegistry, |
50 | CentralIdLookupFactory $centralIdLookupFactory, |
51 | LoggerInterface $logger |
52 | ) { |
53 | $this->dbProvider = $dbProvider; |
54 | $this->cache = $cache; |
55 | $this->moduleRegistry = $moduleRegistry; |
56 | $this->centralIdLookupFactory = $centralIdLookupFactory; |
57 | $this->setLogger( $logger ); |
58 | } |
59 | |
60 | /** |
61 | * @param LoggerInterface $logger |
62 | */ |
63 | public function setLogger( LoggerInterface $logger ) { |
64 | $this->logger = $logger; |
65 | } |
66 | |
67 | /** |
68 | * @param User $user |
69 | * @return OATHUser |
70 | * @throws ConfigException |
71 | * @throws MWException |
72 | */ |
73 | public function findByUser( User $user ) { |
74 | $oathUser = $this->cache->get( $user->getName() ); |
75 | if ( !$oathUser ) { |
76 | $uid = $this->centralIdLookupFactory->getLookup() |
77 | ->centralIdFromLocalUser( $user ); |
78 | $oathUser = new OATHUser( $user, $uid ); |
79 | $this->loadKeysFromDatabase( $oathUser ); |
80 | $this->cache->set( $user->getName(), $oathUser ); |
81 | } |
82 | return $oathUser; |
83 | } |
84 | |
85 | /** |
86 | * @param OATHUser $user |
87 | * @param string|null $clientInfo |
88 | * @throws ConfigException |
89 | * @throws MWException |
90 | */ |
91 | public function persist( OATHUser $user, $clientInfo = null ) { |
92 | if ( !$clientInfo ) { |
93 | $clientInfo = RequestContext::getMain()->getRequest()->getIP(); |
94 | } |
95 | $prevUser = $this->findByUser( $user->getUser() ); |
96 | $userId = $this->centralIdLookupFactory->getLookup()->centralIdFromLocalUser( $user->getUser() ); |
97 | $moduleId = $this->moduleRegistry->getModuleId( $user->getModule()->getName() ); |
98 | |
99 | $dbw = $this->dbProvider->getPrimaryDatabase( 'virtual-oathauth' ); |
100 | $dbw->startAtomic( __METHOD__ ); |
101 | |
102 | // TODO: only update changed rows |
103 | $dbw->newDeleteQueryBuilder() |
104 | ->deleteFrom( 'oathauth_devices' ) |
105 | ->where( [ 'oad_user' => $userId ] ) |
106 | ->caller( __METHOD__ ) |
107 | ->execute(); |
108 | |
109 | foreach ( $user->getKeys() as $key ) { |
110 | $dbw->newInsertQueryBuilder() |
111 | ->insertInto( 'oathauth_devices' ) |
112 | ->row( [ |
113 | 'oad_user' => $userId, |
114 | 'oad_type' => $moduleId, |
115 | 'oad_data' => FormatJson::encode( $key->jsonSerialize() ) |
116 | ] ) |
117 | ->caller( __METHOD__ ) |
118 | ->execute(); |
119 | } |
120 | |
121 | $dbw->endAtomic( __METHOD__ ); |
122 | |
123 | $this->loadKeysFromDatabase( $user ); |
124 | |
125 | $userName = $user->getUser()->getName(); |
126 | $this->cache->set( $userName, $user ); |
127 | |
128 | if ( $prevUser !== false ) { |
129 | $this->logger->info( 'OATHAuth updated for {user} from {clientip}', [ |
130 | 'user' => $userName, |
131 | 'clientip' => $clientInfo, |
132 | 'oldoathtype' => $prevUser->getModule()->getName(), |
133 | 'newoathtype' => $user->getModule()->getName(), |
134 | ] ); |
135 | } else { |
136 | // If findByUser() has returned false, there was no user row or cache entry |
137 | $this->logger->info( 'OATHAuth enabled for {user} from {clientip}', [ |
138 | 'user' => $userName, |
139 | 'clientip' => $clientInfo, |
140 | 'oathtype' => $user->getModule()->getName(), |
141 | ] ); |
142 | Manager::notifyEnabled( $user ); |
143 | } |
144 | } |
145 | |
146 | /** |
147 | * Persists the given OAuth key in the database. |
148 | * |
149 | * @param OATHUser $user |
150 | * @param IModule $module |
151 | * @param array $keyData |
152 | * @param string $clientInfo |
153 | * @return IAuthKey |
154 | */ |
155 | public function createKey( OATHUser $user, IModule $module, array $keyData, string $clientInfo ): IAuthKey { |
156 | if ( $user->getModule() && $user->getModule()->getName() !== $module->getName() ) { |
157 | throw new InvalidArgumentException( |
158 | "User already has a key from a different module enabled ({$user->getModule()->getName()})" |
159 | ); |
160 | } |
161 | |
162 | $userId = $this->centralIdLookupFactory->getLookup()->centralIdFromLocalUser( $user->getUser() ); |
163 | $moduleId = $this->moduleRegistry->getModuleId( $module->getName() ); |
164 | |
165 | $dbw = $this->dbProvider->getPrimaryDatabase( 'virtual-oathauth' ); |
166 | $dbw->newInsertQueryBuilder() |
167 | ->insertInto( 'oathauth_devices' ) |
168 | ->row( [ |
169 | 'oad_user' => $userId, |
170 | 'oad_type' => $moduleId, |
171 | 'oad_data' => FormatJson::encode( $keyData ), |
172 | ] ) |
173 | ->caller( __METHOD__ ) |
174 | ->execute(); |
175 | $id = $dbw->insertId(); |
176 | |
177 | $hasExistingKey = $user->isTwoFactorAuthEnabled(); |
178 | |
179 | $key = $module->newKey( $keyData + [ 'id' => $id ] ); |
180 | $user->addKey( $key ); |
181 | |
182 | $this->logger->info( 'OATHAuth {oathtype} key {key} added for {user} from {clientip}', [ |
183 | 'key' => $id, |
184 | 'user' => $user->getUser()->getName(), |
185 | 'clientip' => $clientInfo, |
186 | 'oathtype' => $module->getName(), |
187 | ] ); |
188 | |
189 | if ( !$hasExistingKey ) { |
190 | $user->setModule( $module ); |
191 | Manager::notifyEnabled( $user ); |
192 | } |
193 | |
194 | return $key; |
195 | } |
196 | |
197 | /** |
198 | * @param OATHUser $user |
199 | * @param IAuthKey $key |
200 | * @param string $clientInfo |
201 | * @param bool $self Whether they disabled it themselves |
202 | */ |
203 | public function removeKey( OATHUser $user, IAuthKey $key, string $clientInfo, bool $self ) { |
204 | $keyId = $key->getId(); |
205 | if ( !$keyId ) { |
206 | throw new InvalidArgumentException( 'A non-persisted key cannot be removed' ); |
207 | } |
208 | |
209 | $userId = $this->centralIdLookupFactory->getLookup() |
210 | ->centralIdFromLocalUser( $user->getUser() ); |
211 | $this->dbProvider->getPrimaryDatabase( 'virtual-oathauth' ) |
212 | ->newDeleteQueryBuilder() |
213 | ->deleteFrom( 'oathauth_devices' ) |
214 | ->where( [ 'oad_user' => $userId, 'oad_id' => $keyId ] ) |
215 | ->caller( __METHOD__ ) |
216 | ->execute(); |
217 | |
218 | // TODO: figure this out from the key itself |
219 | // After calling ->disable(), getModule() will return null so this |
220 | // has to be done before. |
221 | $keyType = $user->getModule()->getName(); |
222 | |
223 | // Remove the key from the user object |
224 | $user->setKeys( |
225 | array_values( |
226 | array_filter( |
227 | $user->getKeys(), |
228 | static function ( IAuthKey $key ) use ( $keyId ) { |
229 | return $key->getId() !== $keyId; |
230 | } |
231 | ) |
232 | ) |
233 | ); |
234 | |
235 | if ( !$user->getKeys() ) { |
236 | $user->setModule( null ); |
237 | } |
238 | |
239 | $userName = $user->getUser()->getName(); |
240 | $this->cache->delete( $userName ); |
241 | |
242 | $this->logger->info( 'OATHAuth removed {oathtype} key {key} for {user} from {clientip}', [ |
243 | 'key' => $keyId, |
244 | 'user' => $userName, |
245 | 'clientip' => $clientInfo, |
246 | 'oathtype' => $keyType, |
247 | ] ); |
248 | |
249 | Manager::notifyDisabled( $user, $self ); |
250 | } |
251 | |
252 | /** |
253 | * @param OATHUser $user |
254 | * @param string $clientInfo |
255 | * @param bool $self Whether the user disabled the 2FA themselves |
256 | * |
257 | * @deprecated since 1.41, use removeAll() instead |
258 | */ |
259 | public function remove( OATHUser $user, $clientInfo, bool $self ) { |
260 | $this->removeAll( $user, $clientInfo, $self ); |
261 | } |
262 | |
263 | /** |
264 | * @param OATHUser $user |
265 | * @param string $clientInfo |
266 | * @param bool $self Whether they disabled it themselves |
267 | */ |
268 | public function removeAll( OATHUser $user, $clientInfo, bool $self ) { |
269 | $userId = $this->centralIdLookupFactory->getLookup() |
270 | ->centralIdFromLocalUser( $user->getUser() ); |
271 | $this->dbProvider->getPrimaryDatabase( 'virtual-oathauth' ) |
272 | ->newDeleteQueryBuilder() |
273 | ->deleteFrom( 'oathauth_devices' ) |
274 | ->where( [ 'oad_user' => $userId ] ) |
275 | ->caller( __METHOD__ ) |
276 | ->execute(); |
277 | |
278 | // TODO: figure this out from the key itself |
279 | // After calling ->disable(), getModule() will return null so this |
280 | // has to be done before. |
281 | $keyType = $user->getModule()->getName(); |
282 | |
283 | $user->disable(); |
284 | |
285 | $userName = $user->getUser()->getName(); |
286 | $this->cache->delete( $userName ); |
287 | |
288 | $this->logger->info( 'OATHAuth disabled for {user} from {clientip}', [ |
289 | 'user' => $userName, |
290 | 'clientip' => $clientInfo, |
291 | 'oathtype' => $keyType, |
292 | ] ); |
293 | |
294 | Manager::notifyDisabled( $user, $self ); |
295 | } |
296 | |
297 | private function loadKeysFromDatabase( OATHUser $user ): void { |
298 | $uid = $this->centralIdLookupFactory->getLookup() |
299 | ->centralIdFromLocalUser( $user->getUser() ); |
300 | |
301 | $res = $this->dbProvider |
302 | ->getReplicaDatabase( 'virtual-oathauth' ) |
303 | ->newSelectQueryBuilder() |
304 | ->select( [ |
305 | 'oad_id', |
306 | 'oad_data', |
307 | 'oat_name', |
308 | ] ) |
309 | ->from( 'oathauth_devices' ) |
310 | ->join( 'oathauth_types', null, [ 'oat_id = oad_type' ] ) |
311 | ->where( [ 'oad_user' => $uid ] ) |
312 | ->caller( __METHOD__ ) |
313 | ->fetchResultSet(); |
314 | |
315 | $module = null; |
316 | |
317 | // Clear stored key list before loading keys |
318 | $user->disable(); |
319 | |
320 | foreach ( $res as $row ) { |
321 | if ( $module && $row->oat_name !== $module->getName() ) { |
322 | // Not supported by current application-layer code. |
323 | throw new RuntimeException( "User {$uid} has multiple different two-factor modules defined" ); |
324 | } |
325 | |
326 | if ( !$module ) { |
327 | $module = $this->moduleRegistry->getModuleByKey( $row->oat_name ); |
328 | $user->setModule( $module ); |
329 | |
330 | if ( !$module ) { |
331 | throw new MWException( 'oathauth-module-invalid' ); |
332 | } |
333 | } |
334 | |
335 | $keyData = FormatJson::decode( $row->oad_data, true ); |
336 | $user->addKey( $module->newKey( $keyData + [ 'id' => (int)$row->oad_id ] ) ); |
337 | } |
338 | } |
339 | } |