Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14846789
D19975.id47678.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
22 KB
Referenced Files
None
Subscribers
None
D19975.id47678.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D19975: Convert user MFA factors to point at configurable "MFA Providers", not raw "MFA Factors"
Attached
Detach File
Event Timeline
Log In to Comment