Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 104
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 / 104
0.00% covered (danger)
0.00%
0 / 6
600
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 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 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 / 70
0.00% covered (danger)
0.00%
0 / 1
240
 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 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * (c) Aaron Schulz 2013, GPL
5 *
6 * @license GPL-2.0-or-later
7 */
8
9namespace MediaWiki\Extension\OAuth\Control;
10
11use MediaWiki\Context\IContextSource;
12use MediaWiki\Exception\ILocalizedException;
13use MediaWiki\Extension\OAuth\Backend\Consumer;
14use MediaWiki\Extension\OAuth\Entity\UserEntity;
15use MediaWiki\Extension\OAuth\Lib\OAuthException;
16use MediaWiki\Extension\OAuth\OAuthServices;
17use MediaWiki\Logger\LoggerFactory;
18use MediaWiki\MediaWikiServices;
19use MediaWiki\Status\Status;
20use Wikimedia\NormalizedException\INormalizedException;
21use Wikimedia\Rdbms\IDatabase;
22use Wikimedia\Rdbms\IDBAccessObject;
23
24/**
25 * This handles the core logic of submitting/approving application
26 * consumer requests and the logic of managing approved consumers
27 *
28 * This control can be used on any wiki, not just the management one
29 *
30 * @todo improve error messages
31 */
32class ConsumerAcceptanceSubmitControl extends SubmitControl {
33    /** @var IDatabase */
34    protected $dbw;
35
36    /** @var int */
37    protected $oauthVersion;
38
39    /**
40     * @param IContextSource $context
41     * @param array $params
42     * @param IDatabase $dbw Result of Utils::getOAuthDB( DB_PRIMARY )
43     * @param int $oauthVersion
44     */
45    public function __construct(
46        IContextSource $context, array $params, IDatabase $dbw, $oauthVersion
47    ) {
48        parent::__construct( $context, $params );
49        $this->dbw = $dbw;
50        $this->oauthVersion = (int)$oauthVersion;
51    }
52
53    /** @inheritDoc */
54    protected function getRequiredFields() {
55        $required = [
56            'renounce' => [
57                'acceptanceId' => '/^\d+$/',
58            ],
59        ];
60        if ( $this->isOAuth2() ) {
61            $required['accept'] = [
62                'client_id' => '/^[0-9a-f]{32}$/',
63                'confirmUpdate' => '/^[01]$/',
64            ];
65        } else {
66            $required['accept'] = [
67                'consumerKey'   => '/^[0-9a-f]{32}$/',
68                'requestToken'  => '/^[0-9a-f]{32}$/',
69                'confirmUpdate' => '/^[01]$/',
70            ];
71        }
72
73        return $required;
74    }
75
76    /** @inheritDoc */
77    protected function checkBasePermissions() {
78        $user = $this->getUser();
79        $services = MediaWikiServices::getInstance();
80        $permissionManager = $services->getPermissionManager();
81        $readOnlyMode = $services->getReadOnlyMode();
82
83        if ( !$user->isRegistered() ) {
84            return $this->failure( 'not_logged_in', 'badaccess-group0' );
85        } elseif ( !$permissionManager->userHasRight( $user, 'mwoauthmanagemygrants' ) ) {
86            return $this->failure( 'permission_denied', 'badaccess-group0' );
87        } elseif ( $readOnlyMode->isReadOnly() ) {
88            return $this->failure( 'readonly', 'readonlytext', $readOnlyMode->getReason() );
89        }
90        return $this->success();
91    }
92
93    /** @inheritDoc */
94    protected function processAction( $action ): Status {
95        // proposer or admin
96        $user = $this->getUser();
97        $userEntity = UserEntity::newFromMWUser( $user );
98        if ( !$userEntity ) {
99            return $this->failure( 'permission_denied', 'badaccess-group0' );
100        }
101
102        switch ( $action ) {
103            case 'accept':
104                $payload = [];
105                $identifier = $this->isOAuth2() ? 'client_id' : 'consumerKey';
106                $consumerRepository = OAuthServices::wrap( MediaWikiServices::getInstance() )->getConsumerRepository();
107                $cmr = $consumerRepository->getByKey( $this->vals[$identifier], IDBAccessObject::READ_LATEST );
108                if ( !$cmr ) {
109                    return $this->failure( 'invalid_consumer_key', 'mwoauth-invalid-consumer-key' );
110                } elseif ( !$cmr->isUsableBy( $userEntity ) ) {
111                    return $this->failure( 'permission_denied', 'badaccess-group0' );
112                }
113
114                try {
115                    if ( $this->isOAuth2() ) {
116                        $scopes = isset( $this->vals['scope'] ) ? explode( ' ', $this->vals['scope'] ) : [];
117
118                        // T413947: Ensure that the 'basic'/'useoauth' scope can't be removed, e.g. by setting
119                        // the 'scope' parameter to the /oauth2/authorize endpoint incorrectly
120                        $scopes = $this->getAcceptedConsumerGrants( $scopes, $cmr );
121
122                        $payload = $cmr->authorize( $userEntity, (bool)$this->vals['confirmUpdate'], $scopes );
123                    } else {
124                        $callback = $cmr->authorize(
125                            $userEntity,
126                            (bool)$this->vals[ 'confirmUpdate' ],
127                            $cmr->getGrants(),
128                            $this->vals[ 'requestToken' ]
129                        );
130                        $payload = [ 'callbackUrl' => $callback ];
131                    }
132                } catch ( OAuthException $exception ) {
133                    if ( $exception instanceof INormalizedException ) {
134                        LoggerFactory::getInstance( 'OAuth' )->warning(
135                            __METHOD__ . ": Exception " . $exception->getNormalizedMessage(),
136                            [ 'exception' => $exception ] + $exception->getMessageContext()
137                        );
138                    } else {
139                        LoggerFactory::getInstance( 'OAuth' )->warning(
140                            __METHOD__ . ": Exception " . $exception->getMessage(),
141                            [ 'exception' => $exception ]
142                        );
143                    }
144                    if ( $exception instanceof ILocalizedException ) {
145                        return $this->failure( 'oauth_exception', $exception->getMessageObject() );
146                    } else {
147                        return $this->failure( 'oauth_exception',
148                            'mwoauth-oauth-exception', $exception->getMessage() );
149                    }
150                }
151
152                LoggerFactory::getInstance( 'OAuth' )->info(
153                    '{user} performed action {action} on consumer {consumer_key}', [
154                        'action' => 'accept',
155                        'user' => $user->getName(),
156                        'comment' => '',
157                        'clientip' => $this->getContext()->getRequest()->getIP(),
158                    ] + $cmr->getLogContext()
159                );
160
161                return $this->success( $payload );
162
163            case 'renounce':
164                $consumerAcceptanceRepository = OAuthServices::wrap( MediaWikiServices::getInstance() )
165                    ->getConsumerAcceptanceRepository();
166                $cmra = $consumerAcceptanceRepository->getById(
167                    $this->vals['acceptanceId'], IDBAccessObject::READ_LATEST );
168                if ( !$cmra ) {
169                    return $this->failure( 'invalid_access_token', 'mwoauth-invalid-access-token' );
170                } elseif ( !$cmra->isAuthorizedBy( $userEntity ) ) {
171                    return $this->failure( 'invalid_access_token', 'mwoauth-invalid-access-token' );
172                }
173
174                $consumerRepository = OAuthServices::wrap( MediaWikiServices::getInstance() )->getConsumerRepository();
175                $cmr = $consumerRepository->getById( $cmra->getConsumerId(), IDBAccessObject::READ_LATEST );
176                LoggerFactory::getInstance( 'OAuth' )->info(
177                    '{user} performed action {action} on consumer {consumer_key}', [
178                        'action' => 'renounce',
179                        'user' => $user->getName(),
180                        'comment' => '',
181                        'clientip' => $this->getContext()->getRequest()->getIP(),
182                    ] + $cmr->getLogContext(),
183                );
184
185                if ( $cmr->getOAuthVersion() === Consumer::OAUTH_VERSION_2 ) {
186                    $this->removeOAuth2AccessTokens( $cmra->getId() );
187                }
188                $consumerAcceptanceRepository->delete( $cmra );
189
190                return $this->success( $cmra );
191        }
192    }
193
194    /**
195     * Convenience function
196     *
197     * @return bool
198     */
199    private function isOAuth2() {
200        return $this->oauthVersion === Consumer::OAUTH_VERSION_2;
201    }
202
203    /**
204     * @param int $approvalId
205     */
206    private function removeOAuth2AccessTokens( $approvalId ) {
207        $accessTokenRepository = OAuthServices::wrap( MediaWikiServices::getInstance() )
208            ->getAccessTokenRepository();
209        $accessTokenRepository->deleteForApprovalId( $approvalId );
210    }
211}