Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 132
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ConsumerAcceptanceSubmitControl
0.00% covered (danger)
0.00%
0 / 132
0.00% covered (danger)
0.00%
0 / 6
756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getRequiredFields
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
12
 checkBasePermissions
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 processAction
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 1
306
 isOAuth2
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 removeOAuth2AccessTokens
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
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
22namespace MediaWiki\Extension\OAuth\Control;
23
24use FormatJson;
25use IContextSource;
26use MediaWiki\Extension\OAuth\Backend\Consumer;
27use MediaWiki\Extension\OAuth\Backend\ConsumerAcceptance;
28use MediaWiki\Extension\OAuth\Backend\MWOAuthException;
29use MediaWiki\Extension\OAuth\Backend\Utils;
30use MediaWiki\Extension\OAuth\Lib\OAuthException;
31use MediaWiki\Extension\OAuth\Repository\AccessTokenRepository;
32use MediaWiki\Logger\LoggerFactory;
33use MediaWiki\MediaWikiServices;
34use 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 */
44class 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}