Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 89
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 / 89
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 / 13
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\Extension\OATHAuth\OATHAuth;
8use MediaWiki\Extension\OATHAuth\OATHUserRepository;
9use MediaWiki\Permissions\Hook\GetUserPermissionsErrorsHook;
10use MediaWiki\Permissions\Hook\UserGetRightsHook;
11use MediaWiki\Permissions\PermissionManager;
12use MediaWiki\Preferences\Hook\GetPreferencesHook;
13use MediaWiki\SpecialPage\Hook\AuthChangeFormFieldsHook;
14use MediaWiki\SpecialPage\SpecialPage;
15use MediaWiki\Title\Title;
16use MediaWiki\User\Hook\UserEffectiveGroupsHook;
17use MediaWiki\User\User;
18use MediaWiki\User\UserGroupManager;
19use MediaWiki\User\UserGroupMembership;
20use Message;
21use OOUI\ButtonWidget;
22use OOUI\HorizontalLayout;
23use OOUI\LabelWidget;
24use RequestContext;
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        ];
89        return true;
90    }
91
92    /**
93     * @param User $user
94     * @param array &$preferences
95     *
96     * @return bool
97     */
98    public function onGetPreferences( $user, &$preferences ) {
99        $oathUser = $this->userRepo->findByUser( $user );
100
101        // If there is no existing module for the user, and the user is not allowed to enable it,
102        // we have nothing to show.
103        if (
104            !$oathUser->isTwoFactorAuthEnabled() &&
105            !$this->permissionManager->userHasRight( $user, 'oathauth-enable' )
106        ) {
107            return true;
108        }
109
110        $module = $oathUser->getModule();
111
112        $moduleLabel = $module === null ?
113            wfMessage( 'oathauth-ui-no-module' ) :
114            $module->getDisplayName();
115
116        $manageButton = new ButtonWidget( [
117            'href' => SpecialPage::getTitleFor( 'OATHManage' )->getLocalURL(),
118            'label' => wfMessage( 'oathauth-ui-manage' )->text()
119        ] );
120
121        $currentModuleLabel = new LabelWidget( [
122            'label' => $moduleLabel->text()
123        ] );
124
125        $control = new HorizontalLayout( [
126            'items' => [
127                $currentModuleLabel,
128                $manageButton
129            ]
130        ] );
131
132        $preferences['oathauth-module'] = [
133            'type' => 'info',
134            'raw' => true,
135            'default' => (string)$control,
136            'label-message' => 'oathauth-prefs-label',
137            'section' => 'personal/info',
138        ];
139
140        $dbGroups = $this->userGroupManager->getUserGroups( $user );
141        $disabledGroups = $this->getDisabledGroups( $user, $dbGroups );
142        if ( !$oathUser->isTwoFactorAuthEnabled() && $disabledGroups ) {
143            $context = RequestContext::getMain();
144            $list = [];
145            foreach ( $disabledGroups as $disabledGroup ) {
146                $list[] = UserGroupMembership::getLinkHTML( $disabledGroup, $context );
147            }
148            $info = $context->getLanguage()->commaList( $list );
149            $disabledInfo = [ 'oathauth-disabledgroups' => [
150                'type' => 'info',
151                'label-message' => [ 'oathauth-prefs-disabledgroups',
152                    Message::numParam( count( $disabledGroups ) ) ],
153                'help-message' => [ 'oathauth-prefs-disabledgroups-help',
154                    Message::numParam( count( $disabledGroups ) ), $user->getName() ],
155                'default' => $info,
156                'raw' => true,
157                'section' => 'personal/info',
158            ] ];
159            // Insert right after "Member of groups"
160            $preferences = wfArrayInsertAfter( $preferences, $disabledInfo, 'usergroups' );
161        }
162
163        return true;
164    }
165
166    /**
167     * Return the groups that this user is supposed to be in, but are disabled
168     * because 2FA isn't enabled
169     *
170     * @param User $user
171     * @param string[] $groups All groups the user is supposed to be in
172     * @return string[] Groups the user should be disabled in
173     */
174    private function getDisabledGroups( User $user, array $groups ): array {
175        $requiredGroups = $this->config->get( 'OATHRequiredForGroups' );
176        // Bail early if:
177        // * No configured restricted groups
178        // * The user is not in any of the restricted groups
179        $intersect = array_intersect( $groups, $requiredGroups );
180        if ( !$requiredGroups || !$intersect ) {
181            return [];
182        }
183
184        $oathUser = $this->userRepo->findByUser( $user );
185        if ( !$oathUser->isTwoFactorAuthEnabled() ) {
186            // Not enabled, strip the groups
187            return $intersect;
188        }
189
190        return [];
191    }
192
193    /**
194     * Remove groups if 2FA is required for them and it's not enabled
195     *
196     * @param User $user User to get groups for
197     * @param string[] &$groups Current effective groups
198     */
199    public function onUserEffectiveGroups( $user, &$groups ) {
200        $disabledGroups = $this->getDisabledGroups( $user, $groups );
201        if ( $disabledGroups ) {
202            $groups = array_diff( $groups, $disabledGroups );
203        }
204    }
205
206    /**
207     * @param Title $title
208     * @param User $user
209     * @param string $action
210     * @param string &$result
211     *
212     * @return bool
213     */
214    public function onGetUserPermissionsErrors( $title, $user, $action, &$result ) {
215        if ( !$this->config->has( 'OATHExclusiveRights' ) ) {
216            return true;
217        }
218
219        // TODO: Get the session from somewhere more... sane?
220        $session = $user->getRequest()->getSession();
221        if (
222            !(bool)$session->get( OATHAuth::AUTHENTICATED_OVER_2FA, false ) &&
223            in_array( $action, $this->config->get( 'OATHExclusiveRights' ) )
224        ) {
225            $result = 'oathauth-action-exclusive-to-2fa';
226            return false;
227        }
228        return true;
229    }
230
231    /**
232     * If a user has groups disabled for not having 2FA enabled, make sure they
233     * have "oathauth-enable" so they can turn it on
234     *
235     * @param User $user User to get rights for
236     * @param string[] &$rights Current rights
237     */
238    public function onUserGetRights( $user, &$rights ) {
239        if ( in_array( 'oathauth-enable', $rights ) ) {
240            return;
241        }
242
243        $dbGroups = $this->userGroupManager->getUserGroups( $user );
244        if ( $this->getDisabledGroups( $user, $dbGroups ) ) {
245            // User has some disabled groups, add oathauth-enable
246            $rights[] = 'oathauth-enable';
247        }
248    }
249}