Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
65 / 65 |
|
100.00% |
4 / 4 |
CRAP | |
100.00% |
1 / 1 |
ProtectedVarsAccessLogger | |
100.00% |
65 / 65 |
|
100.00% |
4 / 4 |
10 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
logViewProtectedVariableValue | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
createProtectedVariableValueAccessLog | |
100.00% |
45 / 45 |
|
100.00% |
1 / 1 |
6 | |||
createManualLogEntry | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\AbuseFilter; |
4 | |
5 | use MediaWiki\Deferred\DeferredUpdates; |
6 | use MediaWiki\Extension\AbuseFilter\Hooks\AbuseFilterHookRunner; |
7 | use MediaWiki\Logging\ManualLogEntry; |
8 | use MediaWiki\Title\TitleFactory; |
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 when a user views the value of protected variables. |
19 | */ |
20 | class ProtectedVarsAccessLogger { |
21 | /** |
22 | * Represents a user viewing the value of a protected variable |
23 | * |
24 | * @var string |
25 | */ |
26 | public const ACTION_VIEW_PROTECTED_VARIABLE_VALUE = 'view-protected-var-value'; |
27 | |
28 | /** |
29 | * @var string |
30 | */ |
31 | public const LOG_TYPE = 'abusefilter-protected-vars'; |
32 | |
33 | private LoggerInterface $logger; |
34 | private IConnectionProvider $lbFactory; |
35 | private ActorStore $actorStore; |
36 | private AbuseFilterHookRunner $hookRunner; |
37 | private TitleFactory $titleFactory; |
38 | private int $delay; |
39 | |
40 | /** |
41 | * @param LoggerInterface $logger |
42 | * @param IConnectionProvider $lbFactory |
43 | * @param ActorStore $actorStore |
44 | * @param AbuseFilterHookRunner $hookRunner |
45 | * @param TitleFactory $titleFactory |
46 | * @param int $delay The number of seconds after which a duplicate log entry can be |
47 | * created for a debounced log |
48 | * @internal Use {@link AbuseLoggerFactory::getProtectedVarsAccessLogger} instead |
49 | */ |
50 | public function __construct( |
51 | LoggerInterface $logger, |
52 | IConnectionProvider $lbFactory, |
53 | ActorStore $actorStore, |
54 | AbuseFilterHookRunner $hookRunner, |
55 | TitleFactory $titleFactory, |
56 | int $delay |
57 | ) { |
58 | Assert::parameter( $delay > 0, 'delay', 'delay must be positive' ); |
59 | |
60 | $this->logger = $logger; |
61 | $this->lbFactory = $lbFactory; |
62 | $this->actorStore = $actorStore; |
63 | $this->hookRunner = $hookRunner; |
64 | $this->titleFactory = $titleFactory; |
65 | $this->delay = $delay; |
66 | } |
67 | |
68 | /** |
69 | * Log when the user views the values of protected variables |
70 | * |
71 | * @param UserIdentity $performer |
72 | * @param string $target |
73 | * @param string[] $viewedVariables The variables associated with the values the user saw. |
74 | * @param int|null $timestamp |
75 | */ |
76 | public function logViewProtectedVariableValue( |
77 | UserIdentity $performer, |
78 | string $target, |
79 | array $viewedVariables, |
80 | ?int $timestamp = null |
81 | ): void { |
82 | if ( !$timestamp ) { |
83 | $timestamp = (int)wfTimestamp(); |
84 | } |
85 | // Create the log on POSTSEND, as this can be called in a context of a GET request through the |
86 | // QueryAbuseLog API (T379083). |
87 | DeferredUpdates::addCallableUpdate( function () use ( $performer, $target, $timestamp, $viewedVariables ) { |
88 | // We need to create a log entry and PostSend-GET expects no writes are performed, so we need to |
89 | // silence the warnings created by this. |
90 | $trxProfiler = Profiler::instance()->getTransactionProfiler(); |
91 | $scope = $trxProfiler->silenceForScope( $trxProfiler::EXPECTATION_REPLICAS_ONLY ); |
92 | $this->createProtectedVariableValueAccessLog( |
93 | $performer, |
94 | $target, |
95 | $timestamp, |
96 | [ 'variables' => $viewedVariables ] |
97 | ); |
98 | } ); |
99 | } |
100 | |
101 | /** |
102 | * Actually creates the log for when a user views the value of protected variables |
103 | * |
104 | * @param UserIdentity $performer |
105 | * @param string $target |
106 | * @param int $timestamp |
107 | * @param array $params |
108 | */ |
109 | private function createProtectedVariableValueAccessLog( |
110 | UserIdentity $performer, |
111 | string $target, |
112 | int $timestamp, |
113 | array $params |
114 | ): void { |
115 | // Allow external extensions to hook into this logger and pass along all known |
116 | // values. External extensions can abort this hook to stop additional logging |
117 | if ( !$this->hookRunner->onAbuseFilterLogProtectedVariableValueAccess( |
118 | $performer, |
119 | $target, |
120 | self::ACTION_VIEW_PROTECTED_VARIABLE_VALUE, |
121 | true, |
122 | $timestamp, |
123 | $params |
124 | ) ) { |
125 | // Don't continue if the hook returns false |
126 | return; |
127 | } |
128 | |
129 | $dbw = $this->lbFactory->getPrimaryDatabase(); |
130 | $shouldLog = false; |
131 | |
132 | // Don't log more than one protected variable access log if the same log was created |
133 | // within the delay period. |
134 | $timestampMinusDelay = $timestamp - $this->delay; |
135 | $actorId = $this->actorStore->findActorId( $performer, $dbw ); |
136 | $targetAsTitle = $this->titleFactory->makeTitle( NS_USER, $target ); |
137 | if ( !$actorId ) { |
138 | $shouldLog = true; |
139 | } else { |
140 | $logline = $dbw->newSelectQueryBuilder() |
141 | ->select( '*' ) |
142 | ->from( 'logging' ) |
143 | ->where( [ |
144 | 'log_type' => self::LOG_TYPE, |
145 | 'log_action' => self::ACTION_VIEW_PROTECTED_VARIABLE_VALUE, |
146 | 'log_actor' => $actorId, |
147 | 'log_namespace' => $targetAsTitle->getNamespace(), |
148 | 'log_title' => $targetAsTitle->getDBkey(), |
149 | $dbw->expr( 'log_timestamp', '>', $dbw->timestamp( $timestampMinusDelay ) ), |
150 | ] ) |
151 | ->caller( __METHOD__ ) |
152 | ->fetchRow(); |
153 | |
154 | if ( !$logline ) { |
155 | $shouldLog = true; |
156 | } |
157 | } |
158 | |
159 | // Actually write to logging table |
160 | if ( $shouldLog ) { |
161 | $logEntry = $this->createManualLogEntry( self::ACTION_VIEW_PROTECTED_VARIABLE_VALUE ); |
162 | $logEntry->setPerformer( $performer ); |
163 | $logEntry->setTarget( $targetAsTitle ); |
164 | $logEntry->setParameters( $params ); |
165 | $logEntry->setTimestamp( wfTimestamp( TS_MW, $timestamp ) ); |
166 | |
167 | try { |
168 | $logEntry->insert( $dbw ); |
169 | } catch ( DBError $e ) { |
170 | $this->logger->critical( |
171 | 'AbuseFilter protected variable log entry was not recorded. ' . |
172 | 'This means access to private data can occur without this being auditable. ' . |
173 | 'Immediate fix required.' |
174 | ); |
175 | |
176 | throw $e; |
177 | } |
178 | } |
179 | } |
180 | |
181 | /** |
182 | * @internal |
183 | * |
184 | * @param string $subtype |
185 | * @return ManualLogEntry |
186 | */ |
187 | protected function createManualLogEntry( string $subtype ): ManualLogEntry { |
188 | return new ManualLogEntry( self::LOG_TYPE, $subtype ); |
189 | } |
190 | } |