Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.15% covered (success)
96.15%
50 / 52
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
OATHAuthLogger
96.15% covered (success)
96.15%
50 / 52
87.50% covered (warning)
87.50%
7 / 8
13
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 logImplicitVerification
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 logOATHRecovery
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 logFailedVerification
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 logSuccessfulVerification
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 insertLogEntry
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 updateCheckUserData
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getClientIP
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
1<?php
2/*
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Extension\OATHAuth;
8
9use Exception;
10use MediaWiki\CheckUser\Services\CheckUserInsert;
11use MediaWiki\Context\IContextSource;
12use MediaWiki\Logging\ManualLogEntry;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\Message\Message;
15use MediaWiki\Page\PageReferenceValue;
16use MediaWiki\Registration\ExtensionRegistry;
17use MediaWiki\User\UserIdentity;
18use Psr\Log\LoggerInterface;
19
20/**
21 * A helper class to facilitate writing to the logs. It primarily sends information to the 'oath' on-wiki log,
22 * but it also records CheckUser data (if present) and ordinary (off-wiki) logs.
23 */
24class OATHAuthLogger {
25
26    public function __construct(
27        private readonly ExtensionRegistry $extensionRegistry,
28        private readonly IContextSource $context,
29        private readonly LoggerInterface $logger,
30    ) {
31    }
32
33    /**
34     * Creates a new log entry to resemble an implicit verification of a user's 2FA enrollment,
35     * when checking if user is eligible for being a member of some groups.
36     */
37    public function logImplicitVerification( UserIdentity $performer, UserIdentity $target ): void {
38        $comment = Message::newFromKey( 'oathauth-verify-automatic-comment' )
39            ->inContentLanguage()
40            ->text();
41
42        // messages used: logentry-oath-verify, log-action-oath-verify
43        $this->insertLogEntry( 'verify', $performer, $target, $comment );
44
45        $this->logger->info(
46            'OATHAuth status implicitly checked for {usertarget} by {user} from {clientip}', [
47                'user' => $performer->getName(),
48                'usertarget' => $target,
49                'clientip' => $this->getClientIP(),
50            ]
51        );
52    }
53
54    /**
55     * Creates a new log entry for when a user generates additional recovery keys for another user.
56     */
57    public function logOATHRecovery(
58        UserIdentity $performer,
59        UserIdentity $target,
60        string $reason,
61        int $codesCount
62    ): void {
63        // messages used: logentry-oath-recover, log-action-oath-recover
64        $this->insertLogEntry( 'recover', $performer, $target, $reason, [ '4::count' => $codesCount ] );
65
66        $this->logger->info(
67            '{user} generated additional OATHAuth recovery keys for {usertarget} from {clientip}', [
68                'user' => $performer->getName(),
69                'usertarget' => $target,
70                'clientip' => $this->getClientIP(),
71            ]
72        );
73    }
74
75    /**
76     * Creates a CheckUser-only log entry for a failed 2FA verification attempt.
77     */
78    public function logFailedVerification( UserIdentity $user ): void {
79        if ( !$this->extensionRegistry->isLoaded( 'CheckUser' ) ) {
80            // @codeCoverageIgnoreStart
81            return;
82            // @codeCoverageIgnoreEnd
83        }
84
85        $logEntry = new ManualLogEntry( 'oath', 'verify-failed' );
86        $logEntry->setPerformer( $user );
87        $logEntry->setTarget(
88            PageReferenceValue::localReference( NS_USER, $user->getName() )
89        );
90
91        $this->updateCheckUserData( $logEntry );
92    }
93
94    /**
95     * Creates a CheckUser-only log entry for a successful 2FA verification attempt.
96     */
97    public function logSuccessfulVerification( UserIdentity $user ): void {
98        if ( !$this->extensionRegistry->isLoaded( 'CheckUser' ) ) {
99            // @codeCoverageIgnoreStart
100            return;
101            // @codeCoverageIgnoreEnd
102        }
103
104        $logEntry = new ManualLogEntry( 'oath', 'verify-success' );
105        $logEntry->setPerformer( $user );
106        $logEntry->setTarget(
107            PageReferenceValue::localReference( NS_USER, $user->getName() )
108        );
109
110        $this->updateCheckUserData( $logEntry );
111    }
112
113    private function insertLogEntry(
114        string $subtype,
115        UserIdentity $performer,
116        UserIdentity $target,
117        string $comment,
118        array $params = []
119    ): void {
120        $targetPage = PageReferenceValue::localReference( NS_USER, $target->getName() );
121
122        $logEntry = new ManualLogEntry( 'oath', $subtype );
123        $logEntry->setPerformer( $performer );
124        $logEntry->setTarget( $targetPage );
125        $logEntry->setComment( $comment );
126        $logEntry->setParameters( $params );
127        $logId = $logEntry->insert();
128
129        $this->updateCheckUserData( $logEntry, $logId );
130    }
131
132    private function updateCheckUserData( ManualLogEntry $logEntry, ?int $logId = null ): void {
133        if ( !$this->extensionRegistry->isLoaded( 'CheckUser' ) ) {
134            // @codeCoverageIgnoreStart
135            return;
136            // @codeCoverageIgnoreEnd
137        }
138
139        /** @var CheckUserInsert $checkUserInsert */
140        $checkUserInsert = MediaWikiServices::getInstance()->get( 'CheckUserInsert' );
141        $recentChange = $logId === null
142            ? $logEntry->getRecentChange()
143            : $logEntry->getRecentChange( $logId );
144        $checkUserInsert->updateCheckUserData( $recentChange );
145    }
146
147    /**
148     * Returns the IP address from which the current request originated or 'unknown IP' if it cannot be determined.
149     */
150    private function getClientIP(): string {
151        try {
152            $request = $this->context->getRequest();
153            return $request->getIP();
154        } catch ( Exception ) {
155            // Let's log with unknown IP, it's not a serious condition, and it's better to have any
156            // logs around 2FA than not
157            return 'unknown IP';
158        }
159    }
160}