diff --git a/resources/sql/autopatches/20190115.mfa.01.provider.sql b/resources/sql/autopatches/20190115.mfa.01.provider.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20190115.mfa.01.provider.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_factorconfig + ADD factorProviderPHID VARBINARY(64) NOT NULL; diff --git a/resources/sql/autopatches/20190115.mfa.02.migrate.php b/resources/sql/autopatches/20190115.mfa.02.migrate.php new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20190115.mfa.02.migrate.php @@ -0,0 +1,72 @@ +establishConnection('w'); + +$provider_table = new PhabricatorAuthFactorProvider(); +$provider_phid = null; +$iterator = new LiskRawMigrationIterator($conn, $table->getTableName()); +$totp_key = 'totp'; +foreach ($iterator as $row) { + + // This wasn't a TOTP factor, so skip it. + if ($row['factorKey'] !== $totp_key) { + continue; + } + + // This factor already has an associated provider. + if (strlen($row['factorProviderPHID'])) { + continue; + } + + // Find (or create) a suitable TOTP provider. Note that we can't "save()" + // an object or this migration will break if the object ever gets new + // columns; just INSERT the raw fields instead. + + if ($provider_phid === null) { + $provider_row = queryfx_one( + $conn, + 'SELECT phid FROM %R WHERE providerFactorKey = %s LIMIT 1', + $provider_table, + $totp_key); + + if ($provider_row) { + $provider_phid = $provider_row['phid']; + } else { + $provider_phid = $provider_table->generatePHID(); + queryfx( + $conn, + 'INSERT INTO %R + (phid, providerFactorKey, name, status, properties, + dateCreated, dateModified) + VALUES (%s, %s, %s, %s, %s, %d, %d)', + $provider_table, + $provider_phid, + $totp_key, + '', + 'active', + '{}', + PhabricatorTime::getNow(), + PhabricatorTime::getNow()); + } + } + + queryfx( + $conn, + 'UPDATE %R SET factorProviderPHID = %s WHERE id = %d', + $table, + $provider_phid, + $row['id']); +} diff --git a/resources/sql/autopatches/20190115.mfa.03.factorkey.sql b/resources/sql/autopatches/20190115.mfa.03.factorkey.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20190115.mfa.03.factorkey.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_factorconfig + DROP factorKey; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2208,6 +2208,7 @@ 'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php', 'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php', 'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php', + 'PhabricatorAuthFactorConfigQuery' => 'applications/auth/query/PhabricatorAuthFactorConfigQuery.php', 'PhabricatorAuthFactorProvider' => 'applications/auth/storage/PhabricatorAuthFactorProvider.php', 'PhabricatorAuthFactorProviderController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php', 'PhabricatorAuthFactorProviderEditController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php', @@ -7874,7 +7875,11 @@ 'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController', 'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthFactor' => 'Phobject', - 'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO', + 'PhabricatorAuthFactorConfig' => array( + 'PhabricatorAuthDAO', + 'PhabricatorPolicyInterface', + ), + 'PhabricatorAuthFactorConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthFactorProvider' => array( 'PhabricatorAuthDAO', 'PhabricatorApplicationTransactionInterface', diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -462,10 +462,14 @@ return $token; } - // Load the multi-factor auth sources attached to this account. - $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( - 'userPHID = %s', - $viewer->getPHID()); + // Load the multi-factor auth sources attached to this account. Note that + // we order factors from oldest to newest, which is not the default query + // ordering but makes the greatest sense in context. + $factors = id(new PhabricatorAuthFactorConfigQuery()) + ->setViewer($viewer) + ->withUserPHIDs(array($viewer->getPHID())) + ->setOrderVector(array('-id')) + ->execute(); // If the account has no associated multi-factor auth, just issue a token // without putting the session into high security mode. This is generally @@ -516,7 +520,8 @@ foreach ($factors as $factor) { $factor_phid = $factor->getPHID(); $issued_challenges = idx($challenge_map, $factor_phid, array()); - $impl = $factor->requireImplementation(); + $provider = $factor->getFactorProvider(); + $impl = $provider->getFactor(); $new_challenges = $impl->getNewIssuedChallenges( $factor, @@ -552,7 +557,9 @@ // Limit factor verification rates to prevent brute force attacks. $any_attempt = false; foreach ($factors as $factor) { - $impl = $factor->requireImplementation(); + $provider = $factor->getFactorProvider(); + $impl = $provider->getFactor(); + if ($impl->getRequestHasChallengeResponse($factor, $request)) { $any_attempt = true; break; @@ -577,7 +584,8 @@ $issued_challenges = idx($challenge_map, $factor_phid, array()); - $impl = $factor->requireImplementation(); + $provider = $factor->getFactorProvider(); + $impl = $provider->getFactor(); $validation_result = $impl->getResultFromChallengeResponse( $factor, @@ -716,7 +724,10 @@ foreach ($factors as $factor) { $result = $validation_results[$factor->getPHID()]; - $factor->requireImplementation()->renderValidateFactorForm( + $provider = $factor->getFactorProvider(); + $impl = $provider->getFactor(); + + $impl->renderValidateFactorForm( $factor, $form, $viewer, diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -7,6 +7,7 @@ abstract public function getFactorCreateHelp(); abstract public function getFactorDescription(); abstract public function processAddFactorForm( + PhabricatorAuthFactorProvider $provider, AphrontFormView $form, AphrontRequest $request, PhabricatorUser $user); @@ -32,8 +33,7 @@ protected function newConfigForUser(PhabricatorUser $user) { return id(new PhabricatorAuthFactorConfig()) - ->setUserPHID($user->getPHID()) - ->setFactorKey($this->getFactorKey()); + ->setUserPHID($user->getPHID()); } protected function newResult() { diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -26,6 +26,7 @@ } public function processAddFactorForm( + PhabricatorAuthFactorProvider $provider, AphrontFormView $form, AphrontRequest $request, PhabricatorUser $user) { diff --git a/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php b/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php @@ -0,0 +1,88 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withUserPHIDs(array $user_phids) { + $this->userPHIDs = $user_phids; + return $this; + } + + public function newResultObject() { + return new PhabricatorAuthFactorConfig(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->userPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'userPHID IN (%Ls)', + $this->userPHIDs); + } + + return $where; + } + + protected function willFilterPage(array $configs) { + $provider_phids = mpull($configs, 'getFactorProviderPHID'); + + $providers = id(new PhabricatorAuthFactorProviderQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($provider_phids) + ->execute(); + $providers = mpull($providers, null, 'getPHID'); + + foreach ($configs as $key => $config) { + $provider = idx($providers, $config->getFactorProviderPHID()); + + if (!$provider) { + unset($configs[$key]); + $this->didRejectResult($config); + continue; + } + + $config->attachFactorProvider($provider); + } + + return $configs; + } + + public function getQueryApplicationClass() { + return 'PhabricatorAuthApplication'; + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php b/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php --- a/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php +++ b/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php @@ -5,6 +5,8 @@ private $ids; private $phids; + private $statuses; + private $providerFactorKeys; public function withIDs(array $ids) { $this->ids = $ids; @@ -15,6 +17,17 @@ $this->phids = $phids; return $this; } + + public function withProviderFactorKeys(array $keys) { + $this->providerFactorKeys = $keys; + return $this; + } + + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + public function newResultObject() { return new PhabricatorAuthFactorProvider(); } @@ -40,6 +53,20 @@ $this->phids); } + if ($this->statuses !== null) { + $where[] = qsprintf( + $conn, + 'status IN (%Ls)', + $this->statuses); + } + + if ($this->providerFactorKeys !== null) { + $where[] = qsprintf( + $conn, + 'providerFactorKey IN (%Ls)', + $this->providerFactorKeys); + } + return $where; } diff --git a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php --- a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php @@ -1,14 +1,17 @@ true, self::CONFIG_COLUMN_SCHEMA => array( - 'factorKey' => 'text64', 'factorName' => 'text', 'factorSecret' => 'text', ), @@ -29,26 +31,18 @@ ) + parent::getConfiguration(); } - public function generatePHID() { - return PhabricatorPHID::generateNewPHID( - PhabricatorAuthAuthFactorPHIDType::TYPECONST); + public function getPHIDType() { + return PhabricatorAuthAuthFactorPHIDType::TYPECONST; } - public function getImplementation() { - return idx(PhabricatorAuthFactor::getAllFactors(), $this->getFactorKey()); + public function attachFactorProvider( + PhabricatorAuthFactorProvider $provider) { + $this->factorProvider = $provider; + return $this; } - public function requireImplementation() { - $impl = $this->getImplementation(); - if (!$impl) { - throw new Exception( - pht( - 'Attempting to operate on multi-factor auth which has no '. - 'corresponding implementation (factor key is "%s").', - $this->getFactorKey())); - } - - return $impl; + public function getFactorProvider() { + return $this->assertAttached($this->factorProvider); } public function setSessionEngine(PhabricatorAuthSessionEngine $engine) { @@ -64,4 +58,23 @@ return $this->sessionEngine; } + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return $this->getUserPHID(); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + } diff --git a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php --- a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php @@ -78,6 +78,29 @@ return $this->getFactor()->getFactorName(); } + public function newIconView() { + return $this->getFactor()->newIconView(); + } + + public function getDisplayDescription() { + return $this->getFactor()->getFactorDescription(); + } + + public function processAddFactorForm( + AphrontFormView $form, + AphrontRequest $request, + PhabricatorUser $user) { + + $factor = $this->getFactor(); + + $config = $factor->processAddFactorForm($this, $form, $request, $user); + if ($config) { + $config->setFactorProviderPHID($this->getPHID()); + } + + return $config; + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -16,7 +16,7 @@ } public function processRequest(AphrontRequest $request) { - if ($request->getExists('new')) { + if ($request->getExists('new') || $request->getExists('providerPHID')) { return $this->processNew($request); } @@ -31,22 +31,18 @@ $user = $this->getUser(); $viewer = $request->getUser(); - $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( - 'userPHID = %s', - $user->getPHID()); + $factors = id(new PhabricatorAuthFactorConfigQuery()) + ->setViewer($viewer) + ->withUserPHIDs(array($user->getPHID())) + ->setOrderVector(array('-id')) + ->execute(); $rows = array(); $rowc = array(); $highlight_id = $request->getInt('id'); foreach ($factors as $factor) { - - $impl = $factor->getImplementation(); - if ($impl) { - $type = $impl->getFactorName(); - } else { - $type = $factor->getFactorKey(); - } + $provider = $factor->getFactorProvider(); if ($factor->getID() == $highlight_id) { $rowc[] = 'highlighted'; @@ -62,7 +58,7 @@ 'sigil' => 'workflow', ), $factor->getFactorName()), - $type, + $provider->getDisplayName(), phabricator_datetime($factor->getDateCreated(), $viewer), javelin_tag( 'a', @@ -128,88 +124,101 @@ $viewer = $request->getUser(); $user = $this->getUser(); + $cancel_uri = $this->getPanelURI(); + + // Check that we have providers before we send the user through the MFA + // gate, so you don't authenticate and then immediately get roadblocked. + $providers = id(new PhabricatorAuthFactorProviderQuery()) + ->setViewer($viewer) + ->withStatuses(array(PhabricatorAuthFactorProvider::STATUS_ACTIVE)) + ->execute(); + if (!$providers) { + return $this->newDialog() + ->setTitle(pht('No MFA Providers')) + ->appendParagraph( + pht( + 'There are no active MFA providers. At least one active provider '. + 'must be available to add new MFA factors.')) + ->addCancelButton($cancel_uri); + } + $providers = mpull($providers, null, 'getPHID'); + $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, - $this->getPanelURI()); + $cancel_uri); - $factors = PhabricatorAuthFactor::getAllFactors(); - - $form = id(new AphrontFormView()) - ->setUser($viewer); - - $type = $request->getStr('type'); - if (empty($factors[$type]) || !$request->isFormPost()) { - $factor = null; + $selected_phid = $request->getStr('providerPHID'); + if (empty($providers[$selected_phid])) { + $selected_provider = null; } else { - $factor = $factors[$type]; + $selected_provider = $providers[$selected_phid]; } - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->addHiddenInput('new', true); - - if ($factor === null) { - $choice_control = id(new AphrontFormRadioButtonControl()) - ->setName('type') - ->setValue(key($factors)); - - foreach ($factors as $available_factor) { - $choice_control->addButton( - $available_factor->getFactorKey(), - $available_factor->getFactorName(), - $available_factor->getFactorDescription()); - } + if (!$selected_provider) { + $menu = id(new PHUIObjectItemListView()) + ->setViewer($viewer) + ->setBig(true) + ->setFlush(true); - $dialog->appendParagraph( - pht( - 'Adding an additional authentication factor improves the security '. - 'of your account. Choose the type of factor to add:')); + foreach ($providers as $provider_phid => $provider) { + $provider_uri = id(new PhutilURI($this->getPanelURI())) + ->setQueryParam('providerPHID', $provider_phid); - $form - ->appendChild($choice_control); + $item = id(new PHUIObjectItemView()) + ->setHeader($provider->getDisplayName()) + ->setHref($provider_uri) + ->setClickable(true) + ->setImageIcon($provider->newIconView()) + ->addAttribute($provider->getDisplayDescription()); - } else { - $dialog->addHiddenInput('type', $type); + $menu->addItem($item); + } + + return $this->newDialog() + ->setTitle(pht('Choose Factor Type')) + ->appendChild($menu) + ->addCancelButton($cancel_uri); + } - $config = $factor->processAddFactorForm( - $form, - $request, - $user); + $form = id(new AphrontFormView()) + ->setViewer($viewer); - if ($config) { - $config->save(); + $config = $selected_provider->processAddFactorForm( + $form, + $request, + $user); - $log = PhabricatorUserLog::initializeNewLog( - $viewer, - $user->getPHID(), - PhabricatorUserLog::ACTION_MULTI_ADD); - $log->save(); + if ($config) { + $config->save(); - $user->updateMultiFactorEnrollment(); + $log = PhabricatorUserLog::initializeNewLog( + $viewer, + $user->getPHID(), + PhabricatorUserLog::ACTION_MULTI_ADD); + $log->save(); - // Terminate other sessions so they must log in and survive the - // multi-factor auth check. + $user->updateMultiFactorEnrollment(); - id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( - $user, - new PhutilOpaqueEnvelope( - $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); + // Terminate other sessions so they must log in and survive the + // multi-factor auth check. - return id(new AphrontRedirectResponse()) - ->setURI($this->getPanelURI('?id='.$config->getID())); - } + id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( + $user, + new PhutilOpaqueEnvelope( + $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); + + return id(new AphrontRedirectResponse()) + ->setURI($this->getPanelURI('?id='.$config->getID())); } - $dialog + return $this->newDialog() + ->addHiddenInput('providerPHID', $selected_provider->getPHID()) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle(pht('Add Authentication Factor')) ->appendChild($form->buildLayoutView()) ->addSubmitButton(pht('Continue')) - ->addCancelButton($this->getPanelURI()); - - return id(new AphrontDialogResponse()) - ->setDialog($dialog); + ->addCancelButton($cancel_uri); } private function processEdit(AphrontRequest $request) {