Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
87.67% |
64 / 73 |
|
66.67% |
4 / 6 |
CRAP | |
0.00% |
0 / 1 |
ProtectedVarsAccessLogger | |
87.67% |
64 / 73 |
|
66.67% |
4 / 6 |
14.37 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
logAccessEnabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
logAccessDisabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
logViewProtectedVariableValue | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
2.00 | |||
log | |
84.62% |
44 / 52 |
|
0.00% |
0 / 1 |
8.23 | |||
createManualLogEntry | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\AbuseFilter; |
4 | |
5 | use ManualLogEntry; |
6 | use MediaWiki\Deferred\DeferredUpdates; |
7 | use MediaWiki\MediaWikiServices; |
8 | use MediaWiki\Title\Title; |
9 | use MediaWiki\User\ActorStore; |
10 | use MediaWiki\User\UserIdentity; |
11 | use Profiler; |
12 | use Psr\Log\LoggerInterface; |
13 | use Wikimedia\Assert\Assert; |
14 | use Wikimedia\Rdbms\DBError; |
15 | use Wikimedia\Rdbms\IConnectionProvider; |
16 | |
17 | /** |
18 | * Defines the API for the component responsible for logging the following interactions: |
19 | * |
20 | * - A user enables protected variable viewing |
21 | * - A user disables protected variable viewing |
22 | */ |
23 | class ProtectedVarsAccessLogger { |
24 | /** |
25 | * Represents a user enabling their own access to view protected variables |
26 | * |
27 | * @var string |
28 | */ |
29 | public const ACTION_CHANGE_ACCESS_ENABLED = 'change-access-enable'; |
30 | |
31 | /** |
32 | * Represents a user disabling their own access to view protected variables |
33 | * |
34 | * @var string |
35 | */ |
36 | public const ACTION_CHANGE_ACCESS_DISABLED = 'change-access-disable'; |
37 | |
38 | /** |
39 | * Represents a user viewing the value of a protected variable |
40 | * |
41 | * @var string |
42 | */ |
43 | public const ACTION_VIEW_PROTECTED_VARIABLE_VALUE = 'view-protected-var-value'; |
44 | |
45 | /** |
46 | * @var string |
47 | */ |
48 | public const LOG_TYPE = 'abusefilter-protected-vars'; |
49 | |
50 | private LoggerInterface $logger; |
51 | private IConnectionProvider $lbFactory; |
52 | private ActorStore $actorStore; |
53 | private int $delay; |
54 | |
55 | /** |
56 | * @param LoggerInterface $logger |
57 | * @param IConnectionProvider $lbFactory |
58 | * @param ActorStore $actorStore |
59 | * @param int $delay The number of seconds after which a duplicate log entry can be |
60 | * created for a debounced log |
61 | */ |
62 | public function __construct( |
63 | LoggerInterface $logger, |
64 | IConnectionProvider $lbFactory, |
65 | ActorStore $actorStore, |
66 | int $delay |
67 | ) { |
68 | Assert::parameter( $delay > 0, 'delay', 'delay must be positive' ); |
69 | |
70 | $this->logger = $logger; |
71 | $this->lbFactory = $lbFactory; |
72 | $this->actorStore = $actorStore; |
73 | $this->delay = $delay; |
74 | } |
75 | |
76 | /** |
77 | * Log when the user enables their own access |
78 | * |
79 | * @param UserIdentity $performer |
80 | */ |
81 | public function logAccessEnabled( UserIdentity $performer ): void { |
82 | $this->log( $performer, $performer->getName(), self::ACTION_CHANGE_ACCESS_ENABLED, false ); |
83 | } |
84 | |
85 | /** |
86 | * Log when the user disables their own access |
87 | * |
88 | * @param UserIdentity $performer |
89 | */ |
90 | public function logAccessDisabled( UserIdentity $performer ): void { |
91 | $this->log( $performer, $performer->getName(), self::ACTION_CHANGE_ACCESS_DISABLED, false ); |
92 | } |
93 | |
94 | /** |
95 | * Log when the user views the values of protected variables |
96 | * |
97 | * @param UserIdentity $performer |
98 | * @param string $target |
99 | * @param int|null $timestamp |
100 | */ |
101 | public function logViewProtectedVariableValue( |
102 | UserIdentity $performer, |
103 | string $target, |
104 | ?int $timestamp = null |
105 | ): void { |
106 | if ( !$timestamp ) { |
107 | $timestamp = (int)wfTimestamp(); |
108 | } |
109 | // Create the log on POSTSEND, as this can be called in a context of a GET request through the |
110 | // QueryAbuseLog API (T379083). |
111 | DeferredUpdates::addCallableUpdate( function () use ( $performer, $target, $timestamp ) { |
112 | // We need to create a log entry and PostSend-GET expects no writes are performed, so we need to |
113 | // silence the warnings created by this. |
114 | $trxProfiler = Profiler::instance()->getTransactionProfiler(); |
115 | $scope = $trxProfiler->silenceForScope( $trxProfiler::EXPECTATION_REPLICAS_ONLY ); |
116 | $this->log( |
117 | $performer, |
118 | $target, |
119 | self::ACTION_VIEW_PROTECTED_VARIABLE_VALUE, |
120 | true, |
121 | $timestamp |
122 | ); |
123 | } ); |
124 | } |
125 | |
126 | /** |
127 | * @param UserIdentity $performer |
128 | * @param string $target |
129 | * @param string $action |
130 | * @param bool $shouldDebounce |
131 | * @param int|null $timestamp |
132 | * @param array|null $params |
133 | */ |
134 | private function log( |
135 | UserIdentity $performer, |
136 | string $target, |
137 | string $action, |
138 | bool $shouldDebounce, |
139 | ?int $timestamp = null, |
140 | ?array $params = [] |
141 | ): void { |
142 | if ( !$timestamp ) { |
143 | $timestamp = (int)wfTimestamp(); |
144 | } |
145 | |
146 | // Log to CheckUser's temporary accounts log if CU is installed |
147 | if ( MediaWikiServices::getInstance()->getExtensionRegistry()->isLoaded( 'CheckUser' ) ) { |
148 | // Add the extension name to the action so that CheckUser has a clearer |
149 | // reference to the source in the message key |
150 | $action = 'af-' . $action; |
151 | |
152 | $logger = MediaWikiServices::getInstance() |
153 | ->getService( 'CheckUserTemporaryAccountLoggerFactory' ) |
154 | ->getLogger(); |
155 | $logger->logFromExternal( |
156 | $performer, |
157 | $target, |
158 | $action, |
159 | $params, |
160 | $shouldDebounce, |
161 | $timestamp |
162 | ); |
163 | } else { |
164 | $dbw = $this->lbFactory->getPrimaryDatabase(); |
165 | $shouldLog = false; |
166 | |
167 | // If the log is debounced, check against the logging table before logging |
168 | if ( $shouldDebounce ) { |
169 | $timestampMinusDelay = $timestamp - $this->delay; |
170 | $actorId = $this->actorStore->findActorId( $performer, $dbw ); |
171 | if ( !$actorId ) { |
172 | $shouldLog = true; |
173 | } else { |
174 | $logline = $dbw->newSelectQueryBuilder() |
175 | ->select( '*' ) |
176 | ->from( 'logging' ) |
177 | ->where( [ |
178 | 'log_type' => self::LOG_TYPE, |
179 | 'log_action' => $action, |
180 | 'log_actor' => $actorId, |
181 | 'log_namespace' => NS_USER, |
182 | 'log_title' => $target, |
183 | $dbw->expr( 'log_timestamp', '>', $dbw->timestamp( $timestampMinusDelay ) ), |
184 | ] ) |
185 | ->caller( __METHOD__ ) |
186 | ->fetchRow(); |
187 | |
188 | if ( !$logline ) { |
189 | $shouldLog = true; |
190 | } |
191 | } |
192 | } else { |
193 | // If the log isn't debounced then it should always be logged |
194 | $shouldLog = true; |
195 | } |
196 | |
197 | // Actually write to logging table |
198 | if ( $shouldLog ) { |
199 | $logEntry = $this->createManualLogEntry( $action ); |
200 | $logEntry->setPerformer( $performer ); |
201 | $logEntry->setTarget( Title::makeTitle( NS_USER, $target ) ); |
202 | $logEntry->setParameters( $params ); |
203 | $logEntry->setTimestamp( wfTimestamp( TS_MW, $timestamp ) ); |
204 | |
205 | try { |
206 | $logEntry->insert( $dbw ); |
207 | } catch ( DBError $e ) { |
208 | $this->logger->critical( |
209 | 'AbuseFilter proctected variable log entry was not recorded. ' . |
210 | 'This means access to IPs can occur without being auditable. ' . |
211 | 'Immediate fix required.' |
212 | ); |
213 | |
214 | throw $e; |
215 | } |
216 | } |
217 | } |
218 | } |
219 | |
220 | /** |
221 | * @internal |
222 | * |
223 | * @param string $subtype |
224 | * @return ManualLogEntry |
225 | */ |
226 | protected function createManualLogEntry( string $subtype ): ManualLogEntry { |
227 | return new ManualLogEntry( self::LOG_TYPE, $subtype ); |
228 | } |
229 | } |