Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / settings / panel / PhabricatorMultiFactorSettingsPanel.php
blob0054610c28a7c051bef84c873d623b53aee21e68
1 <?php
3 final class PhabricatorMultiFactorSettingsPanel
4 extends PhabricatorSettingsPanel {
6 private $isEnrollment;
8 public function getPanelKey() {
9 return 'multifactor';
12 public function getPanelName() {
13 return pht('Multi-Factor Auth');
16 public function getPanelMenuIcon() {
17 return 'fa-lock';
20 public function getPanelGroupKey() {
21 return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY;
24 public function isMultiFactorEnrollmentPanel() {
25 return true;
28 public function setIsEnrollment($is_enrollment) {
29 $this->isEnrollment = $is_enrollment;
30 return $this;
33 public function getIsEnrollment() {
34 return $this->isEnrollment;
37 public function processRequest(AphrontRequest $request) {
38 if ($request->getExists('new') || $request->getExists('providerPHID')) {
39 return $this->processNew($request);
42 if ($request->getExists('edit')) {
43 return $this->processEdit($request);
46 if ($request->getExists('delete')) {
47 return $this->processDelete($request);
50 $user = $this->getUser();
51 $viewer = $request->getUser();
53 $factors = id(new PhabricatorAuthFactorConfigQuery())
54 ->setViewer($viewer)
55 ->withUserPHIDs(array($user->getPHID()))
56 ->execute();
57 $factors = msortv($factors, 'newSortVector');
59 $rows = array();
60 $rowc = array();
62 $highlight_id = $request->getInt('id');
63 foreach ($factors as $factor) {
64 $provider = $factor->getFactorProvider();
66 if ($factor->getID() == $highlight_id) {
67 $rowc[] = 'highlighted';
68 } else {
69 $rowc[] = null;
72 $status = $provider->newStatus();
73 $status_icon = $status->getFactorIcon();
74 $status_color = $status->getFactorColor();
76 $icon = id(new PHUIIconView())
77 ->setIcon("{$status_icon} {$status_color}")
78 ->setTooltip(pht('Provider: %s', $status->getName()));
80 $details = $provider->getConfigurationListDetails($factor, $viewer);
82 $rows[] = array(
83 $icon,
84 javelin_tag(
85 'a',
86 array(
87 'href' => $this->getPanelURI('?edit='.$factor->getID()),
88 'sigil' => 'workflow',
90 $factor->getFactorName()),
91 $provider->getFactor()->getFactorShortName(),
92 $provider->getDisplayName(),
93 $details,
94 phabricator_datetime($factor->getDateCreated(), $viewer),
95 javelin_tag(
96 'a',
97 array(
98 'href' => $this->getPanelURI('?delete='.$factor->getID()),
99 'sigil' => 'workflow',
100 'class' => 'small button button-grey',
102 pht('Remove')),
106 $table = new AphrontTableView($rows);
107 $table->setNoDataString(
108 pht("You haven't added any authentication factors to your account yet."));
109 $table->setHeaders(
110 array(
111 null,
112 pht('Name'),
113 pht('Type'),
114 pht('Provider'),
115 pht('Details'),
116 pht('Created'),
117 null,
119 $table->setColumnClasses(
120 array(
121 null,
122 'wide pri',
123 null,
124 null,
125 null,
126 'right',
127 'action',
129 $table->setRowClasses($rowc);
130 $table->setDeviceVisibility(
131 array(
132 true,
133 true,
134 false,
135 false,
136 false,
137 false,
138 true,
141 $help_uri = PhabricatorEnv::getDoclink(
142 'User Guide: Multi-Factor Authentication');
144 $buttons = array();
146 // If we're enrolling a new account in MFA, provide a small visual hint
147 // that this is the button they want to click.
148 if ($this->getIsEnrollment()) {
149 $add_color = PHUIButtonView::BLUE;
150 } else {
151 $add_color = PHUIButtonView::GREY;
154 $can_add = (bool)$this->loadActiveMFAProviders();
156 $buttons[] = id(new PHUIButtonView())
157 ->setTag('a')
158 ->setIcon('fa-plus')
159 ->setText(pht('Add Auth Factor'))
160 ->setHref($this->getPanelURI('?new=true'))
161 ->setWorkflow(true)
162 ->setDisabled(!$can_add)
163 ->setColor($add_color);
165 $buttons[] = id(new PHUIButtonView())
166 ->setTag('a')
167 ->setIcon('fa-book')
168 ->setText(pht('Help'))
169 ->setHref($help_uri)
170 ->setColor(PHUIButtonView::GREY);
172 return $this->newBox(pht('Authentication Factors'), $table, $buttons);
175 private function processNew(AphrontRequest $request) {
176 $viewer = $request->getUser();
177 $user = $this->getUser();
179 $cancel_uri = $this->getPanelURI();
181 // Check that we have providers before we send the user through the MFA
182 // gate, so you don't authenticate and then immediately get roadblocked.
183 $providers = $this->loadActiveMFAProviders();
185 if (!$providers) {
186 return $this->newDialog()
187 ->setTitle(pht('No MFA Providers'))
188 ->appendParagraph(
189 pht(
190 'This install does not have any active MFA providers configured. '.
191 'At least one provider must be configured and active before you '.
192 'can add new MFA factors.'))
193 ->addCancelButton($cancel_uri);
196 $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
197 $viewer,
198 $request,
199 $cancel_uri);
201 $selected_phid = $request->getStr('providerPHID');
202 if (empty($providers[$selected_phid])) {
203 $selected_provider = null;
204 } else {
205 $selected_provider = $providers[$selected_phid];
207 // Only let the user continue creating a factor for a given provider if
208 // they actually pass the provider's checks.
209 if (!$selected_provider->canCreateNewConfiguration($viewer)) {
210 $selected_provider = null;
214 if (!$selected_provider) {
215 $menu = id(new PHUIObjectItemListView())
216 ->setViewer($viewer)
217 ->setBig(true)
218 ->setFlush(true);
220 foreach ($providers as $provider_phid => $provider) {
221 $provider_uri = id(new PhutilURI($this->getPanelURI()))
222 ->replaceQueryParam('providerPHID', $provider_phid);
224 $is_enabled = $provider->canCreateNewConfiguration($viewer);
226 $item = id(new PHUIObjectItemView())
227 ->setHeader($provider->getDisplayName())
228 ->setImageIcon($provider->newIconView())
229 ->addAttribute($provider->getDisplayDescription());
231 if ($is_enabled) {
232 $item
233 ->setHref($provider_uri)
234 ->setClickable(true);
235 } else {
236 $item->setDisabled(true);
239 $create_description = $provider->getConfigurationCreateDescription(
240 $viewer);
241 if ($create_description) {
242 $item->appendChild($create_description);
245 $menu->addItem($item);
248 return $this->newDialog()
249 ->setTitle(pht('Choose Factor Type'))
250 ->appendChild($menu)
251 ->addCancelButton($cancel_uri);
254 // NOTE: Beyond providing guidance, this step is also providing a CSRF gate
255 // on this endpoint, since prompting the user to respond to a challenge
256 // sometimes requires us to push a challenge to them as a side effect (for
257 // example, with SMS).
258 if (!$request->isFormPost() || !$request->getBool('mfa.start')) {
259 $enroll = $selected_provider->getEnrollMessage();
260 if (!strlen($enroll)) {
261 $enroll = $selected_provider->getEnrollDescription($viewer);
264 return $this->newDialog()
265 ->addHiddenInput('providerPHID', $selected_provider->getPHID())
266 ->addHiddenInput('mfa.start', 1)
267 ->setTitle(pht('Add Authentication Factor'))
268 ->appendChild(new PHUIRemarkupView($viewer, $enroll))
269 ->addCancelButton($cancel_uri)
270 ->addSubmitButton($selected_provider->getEnrollButtonText($viewer));
273 $form = id(new AphrontFormView())
274 ->setViewer($viewer);
276 if ($request->getBool('mfa.enroll')) {
277 // Subject users to rate limiting so that it's difficult to add factors
278 // by pure brute force. This is normally not much of an attack, but push
279 // factor types may have side effects.
280 PhabricatorSystemActionEngine::willTakeAction(
281 array($viewer->getPHID()),
282 new PhabricatorAuthNewFactorAction(),
284 } else {
285 // Test the limit before showing the user a form, so we don't give them
286 // a form which can never possibly work because it will always hit rate
287 // limiting.
288 PhabricatorSystemActionEngine::willTakeAction(
289 array($viewer->getPHID()),
290 new PhabricatorAuthNewFactorAction(),
294 $config = $selected_provider->processAddFactorForm(
295 $form,
296 $request,
297 $user);
299 if ($config) {
300 // If the user added a factor, give them a rate limiting point back.
301 PhabricatorSystemActionEngine::willTakeAction(
302 array($viewer->getPHID()),
303 new PhabricatorAuthNewFactorAction(),
304 -1);
306 $config->save();
308 // If we used a temporary token to handle synchronizing the factor,
309 // revoke it now.
310 $sync_token = $config->getMFASyncToken();
311 if ($sync_token) {
312 $sync_token->revokeToken();
315 $log = PhabricatorUserLog::initializeNewLog(
316 $viewer,
317 $user->getPHID(),
318 PhabricatorAddMultifactorUserLogType::LOGTYPE);
319 $log->save();
321 $user->updateMultiFactorEnrollment();
323 // Terminate other sessions so they must log in and survive the
324 // multi-factor auth check.
326 id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
327 $user,
328 new PhutilOpaqueEnvelope(
329 $request->getCookie(PhabricatorCookies::COOKIE_SESSION)));
331 return id(new AphrontRedirectResponse())
332 ->setURI($this->getPanelURI('?id='.$config->getID()));
335 return $this->newDialog()
336 ->addHiddenInput('providerPHID', $selected_provider->getPHID())
337 ->addHiddenInput('mfa.start', 1)
338 ->addHiddenInput('mfa.enroll', 1)
339 ->setWidth(AphrontDialogView::WIDTH_FORM)
340 ->setTitle(pht('Add Authentication Factor'))
341 ->appendChild($form->buildLayoutView())
342 ->addSubmitButton(pht('Continue'))
343 ->addCancelButton($cancel_uri);
346 private function processEdit(AphrontRequest $request) {
347 $viewer = $request->getUser();
348 $user = $this->getUser();
350 $factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere(
351 'id = %d AND userPHID = %s',
352 $request->getInt('edit'),
353 $user->getPHID());
354 if (!$factor) {
355 return new Aphront404Response();
358 $e_name = true;
359 $errors = array();
360 if ($request->isFormPost()) {
361 $name = $request->getStr('name');
362 if (!strlen($name)) {
363 $e_name = pht('Required');
364 $errors[] = pht(
365 'Authentication factors must have a name to identify them.');
368 if (!$errors) {
369 $factor->setFactorName($name);
370 $factor->save();
372 $user->updateMultiFactorEnrollment();
374 return id(new AphrontRedirectResponse())
375 ->setURI($this->getPanelURI('?id='.$factor->getID()));
377 } else {
378 $name = $factor->getFactorName();
381 $form = id(new AphrontFormView())
382 ->setUser($viewer)
383 ->appendChild(
384 id(new AphrontFormTextControl())
385 ->setName('name')
386 ->setLabel(pht('Name'))
387 ->setValue($name)
388 ->setError($e_name));
390 $dialog = id(new AphrontDialogView())
391 ->setUser($viewer)
392 ->addHiddenInput('edit', $factor->getID())
393 ->setTitle(pht('Edit Authentication Factor'))
394 ->setErrors($errors)
395 ->appendChild($form->buildLayoutView())
396 ->addSubmitButton(pht('Save'))
397 ->addCancelButton($this->getPanelURI());
399 return id(new AphrontDialogResponse())
400 ->setDialog($dialog);
403 private function processDelete(AphrontRequest $request) {
404 $viewer = $request->getUser();
405 $user = $this->getUser();
407 $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
408 $viewer,
409 $request,
410 $this->getPanelURI());
412 $factor = id(new PhabricatorAuthFactorConfig())->loadOneWhere(
413 'id = %d AND userPHID = %s',
414 $request->getInt('delete'),
415 $user->getPHID());
416 if (!$factor) {
417 return new Aphront404Response();
420 if ($request->isFormPost()) {
421 $factor->delete();
423 $log = PhabricatorUserLog::initializeNewLog(
424 $viewer,
425 $user->getPHID(),
426 PhabricatorRemoveMultifactorUserLogType::LOGTYPE);
427 $log->save();
429 $user->updateMultiFactorEnrollment();
431 return id(new AphrontRedirectResponse())
432 ->setURI($this->getPanelURI());
435 $dialog = id(new AphrontDialogView())
436 ->setUser($viewer)
437 ->addHiddenInput('delete', $factor->getID())
438 ->setTitle(pht('Delete Authentication Factor'))
439 ->appendParagraph(
440 pht(
441 'Really remove the authentication factor %s from your account?',
442 phutil_tag('strong', array(), $factor->getFactorName())))
443 ->addSubmitButton(pht('Remove Factor'))
444 ->addCancelButton($this->getPanelURI());
446 return id(new AphrontDialogResponse())
447 ->setDialog($dialog);
450 private function loadActiveMFAProviders() {
451 $viewer = $this->getViewer();
453 $providers = id(new PhabricatorAuthFactorProviderQuery())
454 ->setViewer($viewer)
455 ->withStatuses(
456 array(
457 PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
459 ->execute();
461 $providers = mpull($providers, null, 'getPHID');
462 $providers = msortv($providers, 'newSortVector');
464 return $providers;