Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 132 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
ConsumerAcceptanceSubmitControl | |
0.00% |
0 / 132 |
|
0.00% |
0 / 6 |
756 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getRequiredFields | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
12 | |||
checkBasePermissions | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
processAction | |
0.00% |
0 / 92 |
|
0.00% |
0 / 1 |
306 | |||
isOAuth2 | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
removeOAuth2AccessTokens | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * (c) Aaron Schulz 2013, GPL |
5 | * |
6 | * This program is free software; you can redistribute it and/or modify |
7 | * it under the terms of the GNU General Public License as published by |
8 | * the Free Software Foundation; either version 2 of the License, or |
9 | * (at your option) any later version. |
10 | * |
11 | * This program is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | * GNU General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU General Public License along |
17 | * with this program; if not, write to the Free Software Foundation, Inc., |
18 | * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
19 | * http://www.gnu.org/copyleft/gpl.html |
20 | */ |
21 | |
22 | namespace MediaWiki\Extension\OAuth\Control; |
23 | |
24 | use FormatJson; |
25 | use IContextSource; |
26 | use MediaWiki\Extension\OAuth\Backend\Consumer; |
27 | use MediaWiki\Extension\OAuth\Backend\ConsumerAcceptance; |
28 | use MediaWiki\Extension\OAuth\Backend\MWOAuthException; |
29 | use MediaWiki\Extension\OAuth\Backend\Utils; |
30 | use MediaWiki\Extension\OAuth\Lib\OAuthException; |
31 | use MediaWiki\Extension\OAuth\Repository\AccessTokenRepository; |
32 | use MediaWiki\Logger\LoggerFactory; |
33 | use MediaWiki\MediaWikiServices; |
34 | use Wikimedia\Rdbms\IDatabase; |
35 | |
36 | /** |
37 | * This handles the core logic of submitting/approving application |
38 | * consumer requests and the logic of managing approved consumers |
39 | * |
40 | * This control can be used on any wiki, not just the management one |
41 | * |
42 | * @TODO: improve error messages |
43 | */ |
44 | class ConsumerAcceptanceSubmitControl extends SubmitControl { |
45 | /** @var IDatabase */ |
46 | protected $dbw; |
47 | |
48 | /** @var int */ |
49 | protected $oauthVersion; |
50 | |
51 | /** |
52 | * @param IContextSource $context |
53 | * @param array $params |
54 | * @param IDatabase $dbw Result of Utils::getCentralDB( DB_PRIMARY ) |
55 | * @param int $oauthVersion |
56 | */ |
57 | public function __construct( |
58 | IContextSource $context, array $params, IDatabase $dbw, $oauthVersion |
59 | ) { |
60 | parent::__construct( $context, $params ); |
61 | $this->dbw = $dbw; |
62 | $this->oauthVersion = (int)$oauthVersion; |
63 | } |
64 | |
65 | protected function getRequiredFields() { |
66 | $required = [ |
67 | 'update' => [ |
68 | 'acceptanceId' => '/^\d+$/', |
69 | 'grants' => static function ( $s ) { |
70 | $grants = FormatJson::decode( $s, true ); |
71 | return is_array( $grants ) && Utils::grantsAreValid( $grants ); |
72 | } |
73 | ], |
74 | 'renounce' => [ |
75 | 'acceptanceId' => '/^\d+$/', |
76 | ], |
77 | ]; |
78 | if ( $this->isOAuth2() ) { |
79 | $required['accept'] = [ |
80 | 'client_id' => '/^[0-9a-f]{32}$/', |
81 | 'confirmUpdate' => '/^[01]$/', |
82 | ]; |
83 | } else { |
84 | $required['accept'] = [ |
85 | 'consumerKey' => '/^[0-9a-f]{32}$/', |
86 | 'requestToken' => '/^[0-9a-f]{32}$/', |
87 | 'confirmUpdate' => '/^[01]$/', |
88 | ]; |
89 | } |
90 | |
91 | return $required; |
92 | } |
93 | |
94 | protected function checkBasePermissions() { |
95 | $user = $this->getUser(); |
96 | $services = MediaWikiServices::getInstance(); |
97 | $permissionManager = $services->getPermissionManager(); |
98 | $readOnlyMode = $services->getReadOnlyMode(); |
99 | |
100 | if ( !$user->getID() ) { |
101 | return $this->failure( 'not_logged_in', 'badaccess-group0' ); |
102 | } elseif ( !$permissionManager->userHasRight( $user, 'mwoauthmanagemygrants' ) ) { |
103 | return $this->failure( 'permission_denied', 'badaccess-group0' ); |
104 | } elseif ( $readOnlyMode->isReadOnly() ) { |
105 | return $this->failure( 'readonly', 'readonlytext', $readOnlyMode->getReason() ); |
106 | } |
107 | return $this->success(); |
108 | } |
109 | |
110 | protected function processAction( $action ) { |
111 | // proposer or admin |
112 | $user = $this->getUser(); |
113 | $dbw = $this->dbw; |
114 | |
115 | $centralUserId = Utils::getCentralIdFromLocalUser( $user ); |
116 | if ( !$centralUserId ) { |
117 | return $this->failure( 'permission_denied', 'badaccess-group0' ); |
118 | } |
119 | |
120 | switch ( $action ) { |
121 | case 'accept': |
122 | $payload = []; |
123 | $identifier = $this->isOAuth2() ? 'client_id' : 'consumerKey'; |
124 | $cmr = Consumer::newFromKey( $this->dbw, $this->vals[$identifier] ); |
125 | if ( !$cmr ) { |
126 | return $this->failure( 'invalid_consumer_key', 'mwoauth-invalid-consumer-key' ); |
127 | } elseif ( !$cmr->isUsableBy( $user ) ) { |
128 | return $this->failure( 'permission_denied', 'badaccess-group0' ); |
129 | } |
130 | |
131 | try { |
132 | if ( $this->isOAuth2() ) { |
133 | $scopes = isset( $this->vals['scope'] ) ? explode( ' ', $this->vals['scope'] ) : []; |
134 | $payload = $cmr->authorize( $this->getUser(), (bool)$this->vals['confirmUpdate'], $scopes ); |
135 | } else { |
136 | $callback = $cmr->authorize( |
137 | $this->getUser(), |
138 | (bool)$this->vals[ 'confirmUpdate' ], |
139 | $cmr->getGrants(), |
140 | $this->vals[ 'requestToken' ] |
141 | ); |
142 | $payload = [ 'callbackUrl' => $callback ]; |
143 | } |
144 | } catch ( MWOAuthException $exception ) { |
145 | return $this->failure( 'oauth_exception', $exception->getMessageObject() ); |
146 | } catch ( OAuthException $exception ) { |
147 | return $this->failure( 'oauth_exception', |
148 | 'mwoauth-oauth-exception', $exception->getMessage() ); |
149 | } |
150 | |
151 | LoggerFactory::getInstance( 'OAuth' )->info( |
152 | '{user} performed action {action} on consumer {consumer}', [ |
153 | 'action' => 'accept', |
154 | 'user' => $user->getName(), |
155 | 'consumer' => $cmr->getConsumerKey(), |
156 | 'target' => Utils::getCentralUserNameFromId( $cmr->getUserId(), 'raw' ), |
157 | 'comment' => '', |
158 | 'clientip' => $this->getContext()->getRequest()->getIP(), |
159 | ] |
160 | ); |
161 | |
162 | return $this->success( $payload ); |
163 | case 'update': |
164 | $cmra = ConsumerAcceptance::newFromId( $dbw, $this->vals['acceptanceId'] ); |
165 | if ( !$cmra ) { |
166 | return $this->failure( 'invalid_access_token', 'mwoauth-invalid-access-token' ); |
167 | } elseif ( $cmra->getUserId() !== $centralUserId ) { |
168 | return $this->failure( 'invalid_access_token', 'mwoauth-invalid-access-token' ); |
169 | } |
170 | $cmr = Consumer::newFromId( $dbw, $cmra->getConsumerId() ); |
171 | |
172 | // requested grants |
173 | $grants = FormatJson::decode( $this->vals['grants'], true ); |
174 | $grants = array_unique( array_intersect( |
175 | array_merge( |
176 | // implied grants |
177 | MediaWikiServices::getInstance() |
178 | ->getGrantsInfo() |
179 | ->getHiddenGrants(), |
180 | $grants |
181 | ), |
182 | // Only keep the applicable ones |
183 | $cmr->getGrants() |
184 | ) ); |
185 | |
186 | LoggerFactory::getInstance( 'OAuth' )->info( |
187 | '{user} performed action {action} on consumer {consumer}', [ |
188 | 'action' => 'update-acceptance', |
189 | 'user' => $user->getName(), |
190 | 'consumer' => $cmr->getConsumerKey(), |
191 | 'target' => Utils::getCentralUserNameFromId( $cmr->getUserId(), 'raw' ), |
192 | 'comment' => '', |
193 | 'clientip' => $this->getContext()->getRequest()->getIP(), |
194 | ] |
195 | ); |
196 | $cmra->setFields( [ |
197 | 'grants' => array_intersect( $grants, $cmr->getGrants() ) |
198 | ] ); |
199 | $cmra->save( $dbw ); |
200 | |
201 | return $this->success( $cmra ); |
202 | case 'renounce': |
203 | $cmra = ConsumerAcceptance::newFromId( $dbw, $this->vals['acceptanceId'] ); |
204 | if ( !$cmra ) { |
205 | return $this->failure( 'invalid_access_token', 'mwoauth-invalid-access-token' ); |
206 | } elseif ( $cmra->getUserId() !== $centralUserId ) { |
207 | return $this->failure( 'invalid_access_token', 'mwoauth-invalid-access-token' ); |
208 | } |
209 | |
210 | $cmr = Consumer::newFromId( $dbw, $cmra->get( 'consumerId' ) ); |
211 | LoggerFactory::getInstance( 'OAuth' )->info( |
212 | '{user} performed action {action} on consumer {consumer}', [ |
213 | 'action' => 'renounce', |
214 | 'user' => $user->getName(), |
215 | 'consumer' => $cmr->getConsumerKey(), |
216 | 'target' => Utils::getCentralUserNameFromId( $cmr->getUserId(), 'raw' ), |
217 | 'comment' => '', |
218 | 'clientip' => $this->getContext()->getRequest()->getIP(), |
219 | ] |
220 | ); |
221 | |
222 | if ( $cmr->getOAuthVersion() === Consumer::OAUTH_VERSION_2 ) { |
223 | $this->removeOAuth2AccessTokens( $cmra->getId() ); |
224 | } |
225 | $cmra->delete( $dbw ); |
226 | |
227 | return $this->success( $cmra ); |
228 | } |
229 | } |
230 | |
231 | /** |
232 | * Convenience function |
233 | * |
234 | * @return bool |
235 | */ |
236 | private function isOAuth2() { |
237 | return $this->oauthVersion === Consumer::OAUTH_VERSION_2; |
238 | } |
239 | |
240 | /** |
241 | * @param int $approvalId |
242 | */ |
243 | private function removeOAuth2AccessTokens( $approvalId ) { |
244 | $accessTokenRepository = new AccessTokenRepository(); |
245 | $accessTokenRepository->deleteForApprovalId( $approvalId ); |
246 | } |
247 | } |