Page MenuHomePhabricator

D19975.id47678.diff
No OneTemporary

D19975.id47678.diff

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 @@
+<?php
+
+// Previously, MFA factors for individual users were bound to raw factor types.
+// The only factor type ever implemented in the upstream was "totp".
+
+// Going forward, individual factors are bound to a provider instead. This
+// allows factors types to have some configuration, like API keys for
+// service-based MFA. It also allows installs to select which types of factors
+// they want users to be able to set up.
+
+// Migrate all existing TOTP factors to the first available TOTP provider,
+// creating one if none exists. This migration is a little bit messy, but
+// gives us a clean slate going forward with no "builtin" providers.
+
+$table = new PhabricatorAuthFactorConfig();
+$conn = $table->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',
@@ -7873,7 +7874,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 @@
+<?php
+
+final class PhabricatorAuthFactorConfigQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+ private $userPHIDs;
+
+ public function withIDs(array $ids) {
+ $this->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 @@
<?php
-final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO {
+final class PhabricatorAuthFactorConfig
+ extends PhabricatorAuthDAO
+ implements PhabricatorPolicyInterface {
protected $userPHID;
- protected $factorKey;
+ protected $factorProviderPHID;
protected $factorName;
protected $factorSecret;
protected $properties = array();
private $sessionEngine;
+ private $factorProvider = self::ATTACHABLE;
protected function getConfiguration() {
return array(
@@ -17,7 +20,6 @@
),
self::CONFIG_AUX_PHID => 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
@@ -74,6 +74,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) {

File Metadata

Mime Type
text/plain
Expires
Tue, Feb 4, 6:59 AM (13 h, 48 m)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7089055
Default Alt Text
D19975.id47678.diff (22 KB)

Event Timeline