diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 59e2feb7d0..cfd11d630f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3763,6 +3763,7 @@ phutil_register_library_map(array( 'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php', 'PhabricatorSearchCheckboxesField' => 'applications/search/field/PhabricatorSearchCheckboxesField.php', 'PhabricatorSearchConfigOptions' => 'applications/search/config/PhabricatorSearchConfigOptions.php', + 'PhabricatorSearchConstraintException' => 'applications/search/exception/PhabricatorSearchConstraintException.php', 'PhabricatorSearchController' => 'applications/search/controller/PhabricatorSearchController.php', 'PhabricatorSearchCustomFieldProxyField' => 'applications/search/field/PhabricatorSearchCustomFieldProxyField.php', 'PhabricatorSearchDAO' => 'applications/search/storage/PhabricatorSearchDAO.php', @@ -9072,6 +9073,7 @@ phutil_register_library_map(array( 'PhabricatorSearchBaseController' => 'PhabricatorController', 'PhabricatorSearchCheckboxesField' => 'PhabricatorSearchField', 'PhabricatorSearchConfigOptions' => 'PhabricatorApplicationConfigOptions', + 'PhabricatorSearchConstraintException' => 'Exception', 'PhabricatorSearchController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchCustomFieldProxyField' => 'PhabricatorSearchField', 'PhabricatorSearchDAO' => 'PhabricatorLiskDAO', diff --git a/src/applications/config/option/PhabricatorSecurityConfigOptions.php b/src/applications/config/option/PhabricatorSecurityConfigOptions.php index 4d4da4bd02..47fd1bb6f2 100644 --- a/src/applications/config/option/PhabricatorSecurityConfigOptions.php +++ b/src/applications/config/option/PhabricatorSecurityConfigOptions.php @@ -66,6 +66,21 @@ EOTEXT , PhabricatorEnv::getDoclink('Configuring Encryption'))); + $require_mfa_description = $this->deformat(pht(<<newOption('security.alternate-file-domain', 'string', null) ->setLocked(true) @@ -132,13 +147,7 @@ EOTEXT ->setLocked(true) ->setSummary( pht('Require all users to configure multi-factor authentication.')) - ->setDescription( - pht( - 'By default, Phabricator allows users to add multi-factor '. - 'authentication to their accounts, but does not require it. '. - 'By enabling this option, you can force all users to add '. - 'at least one authentication factor before they can use their '. - 'accounts.')) + ->setDescription($require_mfa_description) ->setBoolOptions( array( pht('Multi-Factor Required'), diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php index b6791a7be2..6cb2922083 100644 --- a/src/applications/people/query/PhabricatorPeopleQuery.php +++ b/src/applications/people/query/PhabricatorPeopleQuery.php @@ -18,6 +18,7 @@ final class PhabricatorPeopleQuery private $nameLike; private $nameTokens; private $namePrefixes; + private $isEnrolledInMultiFactor; private $needPrimaryEmail; private $needProfile; @@ -100,6 +101,11 @@ final class PhabricatorPeopleQuery return $this; } + public function withIsEnrolledInMultiFactor($enrolled) { + $this->isEnrolledInMultiFactor = $enrolled; + return $this; + } + public function needPrimaryEmail($need) { $this->needPrimaryEmail = $need; return $this; @@ -337,6 +343,13 @@ final class PhabricatorPeopleQuery $this->nameLike); } + if ($this->isEnrolledInMultiFactor !== null) { + $where[] = qsprintf( + $conn, + 'user.isEnrolledInMultiFactor = %d', + (int)$this->isEnrolledInMultiFactor); + } + return $where; } diff --git a/src/applications/people/query/PhabricatorPeopleSearchEngine.php b/src/applications/people/query/PhabricatorPeopleSearchEngine.php index ce448b0bc9..e800a8ee1a 100644 --- a/src/applications/people/query/PhabricatorPeopleSearchEngine.php +++ b/src/applications/people/query/PhabricatorPeopleSearchEngine.php @@ -18,7 +18,7 @@ final class PhabricatorPeopleSearchEngine } protected function buildCustomSearchFields() { - return array( + $fields = array( id(new PhabricatorSearchStringListField()) ->setLabel(pht('Usernames')) ->setKey('usernames') @@ -84,18 +84,36 @@ final class PhabricatorPeopleSearchEngine pht( 'Pass true to find only users awaiting administrative approval, '. 'or false to omit these users.')), - id(new PhabricatorSearchDateField()) - ->setKey('createdStart') - ->setLabel(pht('Joined After')) - ->setDescription( - pht('Find user accounts created after a given time.')), - id(new PhabricatorSearchDateField()) - ->setKey('createdEnd') - ->setLabel(pht('Joined Before')) - ->setDescription( - pht('Find user accounts created before a given time.')), - ); + + $viewer = $this->requireViewer(); + if ($viewer->getIsAdmin()) { + $fields[] = id(new PhabricatorSearchThreeStateField()) + ->setLabel(pht('Has MFA')) + ->setKey('mfa') + ->setOptions( + pht('(Show All)'), + pht('Show Only Users With MFA'), + pht('Hide Users With MFA')) + ->setDescription( + pht( + 'Pass true to find only users who are enrolled in MFA, or false '. + 'to omit these users.')); + } + + $fields[] = id(new PhabricatorSearchDateField()) + ->setKey('createdStart') + ->setLabel(pht('Joined After')) + ->setDescription( + pht('Find user accounts created after a given time.')); + + $fields[] = id(new PhabricatorSearchDateField()) + ->setKey('createdEnd') + ->setLabel(pht('Joined Before')) + ->setDescription( + pht('Find user accounts created before a given time.')); + + return $fields; } protected function getDefaultFieldOrder() { @@ -151,6 +169,19 @@ final class PhabricatorPeopleSearchEngine $query->withIsApproved(!$map['needsApproval']); } + if (idx($map, 'mfa') !== null) { + $viewer = $this->requireViewer(); + if (!$viewer->getIsAdmin()) { + throw new PhabricatorSearchConstraintException( + pht( + 'The "Has MFA" query constraint may only be used by '. + 'administrators, to prevent attackers from using it to target '. + 'weak accounts.')); + } + + $query->withIsEnrolledInMultiFactor($map['mfa']); + } + if ($map['createdStart']) { $query->withDateCreatedAfter($map['createdStart']); } @@ -254,6 +285,12 @@ final class PhabricatorPeopleSearchEngine $item->addIcon('fa-envelope-o', pht('Mailing List')); } + if ($viewer->getIsAdmin()) { + if ($user->getIsEnrolledInMultiFactor()) { + $item->addIcon('fa-lock', pht('Has MFA')); + } + } + if ($viewer->getIsAdmin()) { $user_id = $user->getID(); if ($is_approval) { diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index c7c2b432b4..faca9991ea 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -331,6 +331,8 @@ final class PhabricatorApplicationSearchController 'query parameters and correct errors.'); } catch (PhutilSearchQueryCompilerSyntaxException $ex) { $exec_errors[] = $ex->getMessage(); + } catch (PhabricatorSearchConstraintException $ex) { + $exec_errors[] = $ex->getMessage(); } // The engine may have encountered additional errors during rendering; diff --git a/src/applications/search/exception/PhabricatorSearchConstraintException.php b/src/applications/search/exception/PhabricatorSearchConstraintException.php new file mode 100644 index 0000000000..7d674518e1 --- /dev/null +++ b/src/applications/search/exception/PhabricatorSearchConstraintException.php @@ -0,0 +1,4 @@ +