Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 90
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
HookHandler
0.00% covered (danger)
0.00%
0 / 90
0.00% covered (danger)
0.00%
0 / 7
552
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 onAuthChangeFormFields
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 onGetPreferences
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
56
 getDisabledGroups
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 onUserEffectiveGroups
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onGetUserPermissionsErrors
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 onUserGetRights
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace MediaWiki\Extension\OATHAuth\Hook;
4
5use MediaWiki\Auth\AuthenticationRequest;
6use MediaWiki\Config\Config;
7use MediaWiki\Context\RequestContext;
8use MediaWiki\Extension\OATHAuth\OATHAuth;
9use MediaWiki\Extension\OATHAuth\OATHUserRepository;
10use MediaWiki\Message\Message;
11use MediaWiki\Permissions\Hook\GetUserPermissionsErrorsHook;
12use MediaWiki\Permissions\Hook\UserGetRightsHook;
13use MediaWiki\Permissions\PermissionManager;
14use MediaWiki\Preferences\Hook\GetPreferencesHook;
15use MediaWiki\SpecialPage\Hook\AuthChangeFormFieldsHook;
16use MediaWiki\SpecialPage\SpecialPage;
17use MediaWiki\Title\Title;
18use MediaWiki\User\Hook\UserEffectiveGroupsHook;
19use MediaWiki\User\User;
20use MediaWiki\User\UserGroupManager;
21use MediaWiki\User\UserGroupMembership;
22use OOUI\ButtonWidget;
23use OOUI\HorizontalLayout;
24use OOUI\LabelWidget;
25
26class HookHandler implements
27    AuthChangeFormFieldsHook,
28    GetPreferencesHook,
29    getUserPermissionsErrorsHook,
30    UserEffectiveGroupsHook,
31    UserGetRightsHook
32{
33    /**
34     * @var OATHUserRepository
35     */
36    private $userRepo;
37
38    /**
39     * @var PermissionManager
40     */
41    private $permissionManager;
42
43    /**
44     * @var UserGroupManager
45     */
46    private $userGroupManager;
47
48    /**
49     * @var Config
50     */
51    private $config;
52
53    /**
54     * @param OATHUserRepository $userRepo
55     * @param PermissionManager $permissionManager
56     * @param Config $config
57     * @param UserGroupManager $userGroupManager
58     */
59    public function __construct( $userRepo, $permissionManager, $config, $userGroupManager ) {
60        $this->userRepo = $userRepo;
61        $this->permissionManager = $permissionManager;
62        $this->config = $config;
63        $this->userGroupManager = $userGroupManager;
64    }
65
66    /**
67     * @param AuthenticationRequest[] $requests
68     * @param array $fieldInfo
69     * @param array &$formDescriptor
70     * @param string $action
71     *
72     * @return bool
73     */
74    public function onAuthChangeFormFields( $requests, $fieldInfo, &$formDescriptor, $action ) {
75        if ( !isset( $fieldInfo['OATHToken'] ) ) {
76            return true;
77        }
78
79        $formDescriptor['OATHToken'] += [
80            'cssClass' => 'loginText',
81            'id' => 'wpOATHToken',
82            'size' => 20,
83            'dir' => 'ltr',
84            'autofocus' => true,
85            'persistent' => false,
86            'autocomplete' => 'one-time-code',
87            'spellcheck' => false,
88            'help-message' => 'oathauth-auth-token-help-ui',
89        ];
90        return true;
91    }
92
93    /**
94     * @param User $user
95     * @param array &$preferences
96     *
97     * @return bool
98     */
99    public function onGetPreferences( $user, &$preferences ) {
100        $oathUser = $this->userRepo->findByUser( $user );
101
102        // If there is no existing module for the user, and the user is not allowed to enable it,
103        // we have nothing to show.
104        if (
105            !$oathUser->isTwoFactorAuthEnabled() &&
106            !$this->permissionManager->userHasRight( $user, 'oathauth-enable' )
107        ) {
108            return true;
109        }
110
111        $module = $oathUser->getModule();
112
113        $moduleLabel = $module === null ?
114            wfMessage( 'oathauth-ui-no-module' ) :
115            $module->getDisplayName();
116
117        $manageButton = new ButtonWidget( [
118            'href' => SpecialPage::getTitleFor( 'OATHManage' )->getLocalURL(),
119            'label' => wfMessage( 'oathauth-ui-manage' )->text()
120        ] );
121
122        $currentModuleLabel = new LabelWidget( [
123            'label' => $moduleLabel->text()
124        ] );
125
126        $control = new HorizontalLayout( [
127            'items' => [
128                $currentModuleLabel,
129                $manageButton
130            ]
131        ] );
132
133        $preferences['oathauth-module'] = [
134            'type' => 'info',
135            'raw' => true,
136            'default' => (string)$control,
137            'label-message' => 'oathauth-prefs-label',
138            'section' => 'personal/info',
139        ];
140
141        $dbGroups = $this->userGroupManager->getUserGroups( $user );
142        $disabledGroups = $this->getDisabledGroups( $user, $dbGroups );
143        if ( !$oathUser->isTwoFactorAuthEnabled() && $disabledGroups ) {
144            $context = RequestContext::getMain();
145            $list = [];
146            foreach ( $disabledGroups as $disabledGroup ) {
147                $list[] = UserGroupMembership::getLinkHTML( $disabledGroup, $context );
148            }
149            $info = $context->getLanguage()->commaList( $list );
150            $disabledInfo = [ 'oathauth-disabledgroups' => [
151                'type' => 'info',
152                'label-message' => [ 'oathauth-prefs-disabledgroups',
153                    Message::numParam( count( $disabledGroups ) ) ],
154                'help-message' => [ 'oathauth-prefs-disabledgroups-help',
155                    Message::numParam( count( $disabledGroups ) ), $user->getName() ],
156                'default' => $info,
157                'raw' => true,
158                'section' => 'personal/info',
159            ] ];
160            // Insert right after "Member of groups"
161            $preferences = wfArrayInsertAfter( $preferences, $disabledInfo, 'usergroups' );
162        }
163
164        return true;
165    }
166
167    /**
168     * Return the groups that this user is supposed to be in, but are disabled
169     * because 2FA isn't enabled
170     *
171     * @param User $user
172     * @param string[] $groups All groups the user is supposed to be in
173     * @return string[] Groups the user should be disabled in
174     */
175    private function getDisabledGroups( User $user, array $groups ): array {
176        $requiredGroups = $this->config->get( 'OATHRequiredForGroups' );
177        // Bail early if:
178        // * No configured restricted groups
179        // * The user is not in any of the restricted groups
180        $intersect = array_intersect( $groups, $requiredGroups );
181        if ( !$requiredGroups || !$intersect ) {
182            return [];
183        }
184
185        $oathUser = $this->userRepo->findByUser( $user );
186        if ( !$oathUser->isTwoFactorAuthEnabled() ) {
187            // Not enabled, strip the groups
188            return $intersect;
189        }
190
191        return [];
192    }
193
194    /**
195     * Remove groups if 2FA is required for them and it's not enabled
196     *
197     * @param User $user User to get groups for
198     * @param string[] &$groups Current effective groups
199     */
200    public function onUserEffectiveGroups( $user, &$groups ) {
201        $disabledGroups = $this->getDisabledGroups( $user, $groups );
202        if ( $disabledGroups ) {
203            $groups = array_diff( $groups, $disabledGroups );
204        }
205    }
206
207    /**
208     * @param Title $title
209     * @param User $user
210     * @param string $action
211     * @param string &$result
212     *
213     * @return bool
214     */
215    public function onGetUserPermissionsErrors( $title, $user, $action, &$result ) {
216        if ( !$this->config->has( 'OATHExclusiveRights' ) ) {
217            return true;
218        }
219
220        // TODO: Get the session from somewhere more... sane?
221        $session = $user->getRequest()->getSession();
222        if (
223            !(bool)$session->get( OATHAuth::AUTHENTICATED_OVER_2FA, false ) &&
224            in_array( $action, $this->config->get( 'OATHExclusiveRights' ) )
225        ) {
226            $result = 'oathauth-action-exclusive-to-2fa';
227            return false;
228        }
229        return true;
230    }
231
232    /**
233     * If a user has groups disabled for not having 2FA enabled, make sure they
234     * have "oathauth-enable" so they can turn it on
235     *
236     * @param User $user User to get rights for
237     * @param string[] &$rights Current rights
238     */
239    public function onUserGetRights( $user, &$rights ) {
240        if ( in_array( 'oathauth-enable', $rights ) ) {
241            return;
242        }
243
244        $dbGroups = $this->userGroupManager->getUserGroups( $user );
245        if ( $this->getDisabledGroups( $user, $dbGroups ) ) {
246            // User has some disabled groups, add oathauth-enable
247            $rights[] = 'oathauth-enable';
248        }
249    }
250}