Merge branch 'master' into blender-tweaks

This commit is contained in:
2019-02-04 14:17:46 +01:00
75 changed files with 2969 additions and 388 deletions

View File

@@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_project.project
ADD subtype VARCHAR(64) COLLATE {$COLLATE_TEXT} NOT NULL;

View File

@@ -0,0 +1,2 @@
UPDATE {$NAMESPACE}_project.project
SET subtype = 'default' WHERE subtype = '';

View File

@@ -0,0 +1,18 @@
<?php
// See PHI1046. The "spacePHID" column for milestones may have fallen out of
// sync; correct all existing values.
$table = new PhabricatorProject();
$conn = $table->establishConnection('w');
$table_name = $table->getTableName();
foreach (new LiskRawMigrationIterator($conn, $table_name) as $project_row) {
queryfx(
$conn,
'UPDATE %R SET spacePHID = %ns
WHERE parentProjectPHID = %s AND milestoneNumber IS NOT NULL',
$table,
$project_row['spacePHID'],
$project_row['phid']);
}

View File

@@ -55,8 +55,8 @@ foreach (array('text', 'html') as $part) {
} }
$headers = $parser->getHeaders(); $headers = $parser->getHeaders();
$headers['subject'] = iconv_mime_decode($headers['subject'], 0, 'UTF-8'); $headers['subject'] = phutil_decode_mime_header($headers['subject']);
$headers['from'] = iconv_mime_decode($headers['from'], 0, 'UTF-8'); $headers['from'] = phutil_decode_mime_header($headers['from']);
if ($args->getArg('process-duplicates')) { if ($args->getArg('process-duplicates')) {
$headers['message-id'] = Filesystem::readRandomCharacters(64); $headers['message-id'] = Filesystem::readRandomCharacters(64);

View File

@@ -183,7 +183,6 @@ phutil_register_library_map(array(
'AphrontCalendarEventView' => 'applications/calendar/view/AphrontCalendarEventView.php', 'AphrontCalendarEventView' => 'applications/calendar/view/AphrontCalendarEventView.php',
'AphrontController' => 'aphront/AphrontController.php', 'AphrontController' => 'aphront/AphrontController.php',
'AphrontCursorPagerView' => 'view/control/AphrontCursorPagerView.php', 'AphrontCursorPagerView' => 'view/control/AphrontCursorPagerView.php',
'AphrontDefaultApplicationConfiguration' => 'aphront/configuration/AphrontDefaultApplicationConfiguration.php',
'AphrontDialogResponse' => 'aphront/response/AphrontDialogResponse.php', 'AphrontDialogResponse' => 'aphront/response/AphrontDialogResponse.php',
'AphrontDialogView' => 'view/AphrontDialogView.php', 'AphrontDialogView' => 'view/AphrontDialogView.php',
'AphrontEpochHTTPParameterType' => 'aphront/httpparametertype/AphrontEpochHTTPParameterType.php', 'AphrontEpochHTTPParameterType' => 'aphront/httpparametertype/AphrontEpochHTTPParameterType.php',
@@ -2231,18 +2230,26 @@ phutil_register_library_map(array(
'PhabricatorAuthFactorConfigQuery' => 'applications/auth/query/PhabricatorAuthFactorConfigQuery.php', 'PhabricatorAuthFactorConfigQuery' => 'applications/auth/query/PhabricatorAuthFactorConfigQuery.php',
'PhabricatorAuthFactorProvider' => 'applications/auth/storage/PhabricatorAuthFactorProvider.php', 'PhabricatorAuthFactorProvider' => 'applications/auth/storage/PhabricatorAuthFactorProvider.php',
'PhabricatorAuthFactorProviderController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php', 'PhabricatorAuthFactorProviderController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php',
'PhabricatorAuthFactorProviderDuoCredentialTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoCredentialTransaction.php',
'PhabricatorAuthFactorProviderDuoEnrollTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoEnrollTransaction.php',
'PhabricatorAuthFactorProviderDuoHostnameTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoHostnameTransaction.php',
'PhabricatorAuthFactorProviderDuoUsernamesTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderDuoUsernamesTransaction.php',
'PhabricatorAuthFactorProviderEditController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php', 'PhabricatorAuthFactorProviderEditController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php',
'PhabricatorAuthFactorProviderEditEngine' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php', 'PhabricatorAuthFactorProviderEditEngine' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php',
'PhabricatorAuthFactorProviderEditor' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditor.php', 'PhabricatorAuthFactorProviderEditor' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditor.php',
'PhabricatorAuthFactorProviderEnrollMessageTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderEnrollMessageTransaction.php',
'PhabricatorAuthFactorProviderListController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php', 'PhabricatorAuthFactorProviderListController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php',
'PhabricatorAuthFactorProviderMFAEngine' => 'applications/auth/engine/PhabricatorAuthFactorProviderMFAEngine.php',
'PhabricatorAuthFactorProviderMessageController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderMessageController.php',
'PhabricatorAuthFactorProviderNameTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderNameTransaction.php', 'PhabricatorAuthFactorProviderNameTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderNameTransaction.php',
'PhabricatorAuthFactorProviderQuery' => 'applications/auth/query/PhabricatorAuthFactorProviderQuery.php', 'PhabricatorAuthFactorProviderQuery' => 'applications/auth/query/PhabricatorAuthFactorProviderQuery.php',
'PhabricatorAuthFactorProviderStatus' => 'applications/auth/constants/PhabricatorAuthFactorProviderStatus.php',
'PhabricatorAuthFactorProviderStatusTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderStatusTransaction.php',
'PhabricatorAuthFactorProviderTransaction' => 'applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php', 'PhabricatorAuthFactorProviderTransaction' => 'applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php',
'PhabricatorAuthFactorProviderTransactionQuery' => 'applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php', 'PhabricatorAuthFactorProviderTransactionQuery' => 'applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php',
'PhabricatorAuthFactorProviderTransactionType' => 'applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php', 'PhabricatorAuthFactorProviderTransactionType' => 'applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php',
'PhabricatorAuthFactorProviderViewController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php', 'PhabricatorAuthFactorProviderViewController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php',
'PhabricatorAuthFactorResult' => 'applications/auth/factor/PhabricatorAuthFactorResult.php', 'PhabricatorAuthFactorResult' => 'applications/auth/factor/PhabricatorAuthFactorResult.php',
'PhabricatorAuthFactorResultException' => 'applications/auth/exception/PhabricatorAuthFactorResultException.php',
'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php', 'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php',
'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php', 'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php',
'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php', 'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php',
@@ -2802,6 +2809,7 @@ phutil_register_library_map(array(
'PhabricatorCountdownTransactionType' => 'applications/countdown/xaction/PhabricatorCountdownTransactionType.php', 'PhabricatorCountdownTransactionType' => 'applications/countdown/xaction/PhabricatorCountdownTransactionType.php',
'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php', 'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php',
'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php', 'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php',
'PhabricatorCredentialEditField' => 'applications/transactions/editfield/PhabricatorCredentialEditField.php',
'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php', 'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php', 'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php',
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php', 'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php',
@@ -2988,6 +2996,7 @@ phutil_register_library_map(array(
'PhabricatorDraftEngine' => 'applications/transactions/draft/PhabricatorDraftEngine.php', 'PhabricatorDraftEngine' => 'applications/transactions/draft/PhabricatorDraftEngine.php',
'PhabricatorDraftInterface' => 'applications/transactions/draft/PhabricatorDraftInterface.php', 'PhabricatorDraftInterface' => 'applications/transactions/draft/PhabricatorDraftInterface.php',
'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.php', 'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.php',
'PhabricatorDuoAuthFactor' => 'applications/auth/factor/PhabricatorDuoAuthFactor.php',
'PhabricatorDuoFuture' => 'applications/auth/future/PhabricatorDuoFuture.php', 'PhabricatorDuoFuture' => 'applications/auth/future/PhabricatorDuoFuture.php',
'PhabricatorEdgeChangeRecord' => 'infrastructure/edges/util/PhabricatorEdgeChangeRecord.php', 'PhabricatorEdgeChangeRecord' => 'infrastructure/edges/util/PhabricatorEdgeChangeRecord.php',
'PhabricatorEdgeChangeRecordTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeChangeRecordTestCase.php', 'PhabricatorEdgeChangeRecordTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeChangeRecordTestCase.php',
@@ -3057,6 +3066,8 @@ phutil_register_library_map(array(
'PhabricatorEditPage' => 'applications/transactions/editengine/PhabricatorEditPage.php', 'PhabricatorEditPage' => 'applications/transactions/editengine/PhabricatorEditPage.php',
'PhabricatorEditType' => 'applications/transactions/edittype/PhabricatorEditType.php', 'PhabricatorEditType' => 'applications/transactions/edittype/PhabricatorEditType.php',
'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php', 'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php',
'PhabricatorEditorExtension' => 'applications/transactions/engineextension/PhabricatorEditorExtension.php',
'PhabricatorEditorExtensionModule' => 'applications/transactions/engineextension/PhabricatorEditorExtensionModule.php',
'PhabricatorEditorMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php', 'PhabricatorEditorMailEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditorMailEngineExtension.php',
'PhabricatorEditorMultipleSetting' => 'applications/settings/setting/PhabricatorEditorMultipleSetting.php', 'PhabricatorEditorMultipleSetting' => 'applications/settings/setting/PhabricatorEditorMultipleSetting.php',
'PhabricatorEditorSetting' => 'applications/settings/setting/PhabricatorEditorSetting.php', 'PhabricatorEditorSetting' => 'applications/settings/setting/PhabricatorEditorSetting.php',
@@ -4110,6 +4121,8 @@ phutil_register_library_map(array(
'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php', 'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php',
'PhabricatorProjectSubprojectsController' => 'applications/project/controller/PhabricatorProjectSubprojectsController.php', 'PhabricatorProjectSubprojectsController' => 'applications/project/controller/PhabricatorProjectSubprojectsController.php',
'PhabricatorProjectSubprojectsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php', 'PhabricatorProjectSubprojectsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php',
'PhabricatorProjectSubtypeDatasource' => 'applications/project/typeahead/PhabricatorProjectSubtypeDatasource.php',
'PhabricatorProjectSubtypesConfigType' => 'applications/project/config/PhabricatorProjectSubtypesConfigType.php',
'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php', 'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php',
'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php', 'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php',
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php', 'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
@@ -5624,7 +5637,6 @@ phutil_register_library_map(array(
'AphrontCalendarEventView' => 'AphrontView', 'AphrontCalendarEventView' => 'AphrontView',
'AphrontController' => 'Phobject', 'AphrontController' => 'Phobject',
'AphrontCursorPagerView' => 'AphrontView', 'AphrontCursorPagerView' => 'AphrontView',
'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration',
'AphrontDialogResponse' => 'AphrontResponse', 'AphrontDialogResponse' => 'AphrontResponse',
'AphrontDialogView' => array( 'AphrontDialogView' => array(
'AphrontView', 'AphrontView',
@@ -7963,20 +7975,29 @@ phutil_register_library_map(array(
'PhabricatorApplicationTransactionInterface', 'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface', 'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface', 'PhabricatorExtendedPolicyInterface',
'PhabricatorEditEngineMFAInterface',
), ),
'PhabricatorAuthFactorProviderController' => 'PhabricatorAuthProviderController', 'PhabricatorAuthFactorProviderController' => 'PhabricatorAuthProviderController',
'PhabricatorAuthFactorProviderDuoCredentialTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderDuoEnrollTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderDuoHostnameTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderDuoUsernamesTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderEditController' => 'PhabricatorAuthFactorProviderController', 'PhabricatorAuthFactorProviderEditController' => 'PhabricatorAuthFactorProviderController',
'PhabricatorAuthFactorProviderEditEngine' => 'PhabricatorEditEngine', 'PhabricatorAuthFactorProviderEditEngine' => 'PhabricatorEditEngine',
'PhabricatorAuthFactorProviderEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorAuthFactorProviderEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthFactorProviderEnrollMessageTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderListController' => 'PhabricatorAuthProviderController', 'PhabricatorAuthFactorProviderListController' => 'PhabricatorAuthProviderController',
'PhabricatorAuthFactorProviderMFAEngine' => 'PhabricatorEditEngineMFAEngine',
'PhabricatorAuthFactorProviderMessageController' => 'PhabricatorAuthFactorProviderController',
'PhabricatorAuthFactorProviderNameTransaction' => 'PhabricatorAuthFactorProviderTransactionType', 'PhabricatorAuthFactorProviderNameTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthFactorProviderQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthFactorProviderStatus' => 'Phobject',
'PhabricatorAuthFactorProviderStatusTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderTransaction' => 'PhabricatorModularTransaction', 'PhabricatorAuthFactorProviderTransaction' => 'PhabricatorModularTransaction',
'PhabricatorAuthFactorProviderTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuthFactorProviderTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthFactorProviderTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorAuthFactorProviderTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorAuthFactorProviderViewController' => 'PhabricatorAuthFactorProviderController', 'PhabricatorAuthFactorProviderViewController' => 'PhabricatorAuthFactorProviderController',
'PhabricatorAuthFactorResult' => 'Phobject', 'PhabricatorAuthFactorResult' => 'Phobject',
'PhabricatorAuthFactorResultException' => 'Exception',
'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase', 'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase',
'PhabricatorAuthFinishController' => 'PhabricatorAuthController', 'PhabricatorAuthFinishController' => 'PhabricatorAuthController',
'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO', 'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO',
@@ -8639,6 +8660,7 @@ phutil_register_library_map(array(
'PhabricatorCountdownTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorCountdownTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCountdownView' => 'AphrontView', 'PhabricatorCountdownView' => 'AphrontView',
'PhabricatorCountdownViewController' => 'PhabricatorCountdownController', 'PhabricatorCountdownViewController' => 'PhabricatorCountdownController',
'PhabricatorCredentialEditField' => 'PhabricatorEditField',
'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery', 'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorCustomField' => 'Phobject', 'PhabricatorCustomField' => 'Phobject',
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
@@ -8843,6 +8865,7 @@ phutil_register_library_map(array(
'PhabricatorDraftDAO' => 'PhabricatorLiskDAO', 'PhabricatorDraftDAO' => 'PhabricatorLiskDAO',
'PhabricatorDraftEngine' => 'Phobject', 'PhabricatorDraftEngine' => 'Phobject',
'PhabricatorDrydockApplication' => 'PhabricatorApplication', 'PhabricatorDrydockApplication' => 'PhabricatorApplication',
'PhabricatorDuoAuthFactor' => 'PhabricatorAuthFactor',
'PhabricatorDuoFuture' => 'FutureProxy', 'PhabricatorDuoFuture' => 'FutureProxy',
'PhabricatorEdgeChangeRecord' => 'Phobject', 'PhabricatorEdgeChangeRecord' => 'Phobject',
'PhabricatorEdgeChangeRecordTestCase' => 'PhabricatorTestCase', 'PhabricatorEdgeChangeRecordTestCase' => 'PhabricatorTestCase',
@@ -8919,6 +8942,8 @@ phutil_register_library_map(array(
'PhabricatorEditPage' => 'Phobject', 'PhabricatorEditPage' => 'Phobject',
'PhabricatorEditType' => 'Phobject', 'PhabricatorEditType' => 'Phobject',
'PhabricatorEditor' => 'Phobject', 'PhabricatorEditor' => 'Phobject',
'PhabricatorEditorExtension' => 'Phobject',
'PhabricatorEditorExtensionModule' => 'PhabricatorConfigModule',
'PhabricatorEditorMailEngineExtension' => 'PhabricatorMailEngineExtension', 'PhabricatorEditorMailEngineExtension' => 'PhabricatorMailEngineExtension',
'PhabricatorEditorMultipleSetting' => 'PhabricatorSelectSetting', 'PhabricatorEditorMultipleSetting' => 'PhabricatorSelectSetting',
'PhabricatorEditorSetting' => 'PhabricatorStringSetting', 'PhabricatorEditorSetting' => 'PhabricatorStringSetting',
@@ -10023,6 +10048,7 @@ phutil_register_library_map(array(
'PhabricatorConduitResultInterface', 'PhabricatorConduitResultInterface',
'PhabricatorColumnProxyInterface', 'PhabricatorColumnProxyInterface',
'PhabricatorSpacesInterface', 'PhabricatorSpacesInterface',
'PhabricatorEditEngineSubtypeInterface',
), ),
'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction', 'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction',
'PhabricatorProjectApplication' => 'PhabricatorApplication', 'PhabricatorProjectApplication' => 'PhabricatorApplication',
@@ -10150,6 +10176,8 @@ phutil_register_library_map(array(
'PhabricatorProjectSubprojectWarningController' => 'PhabricatorProjectController', 'PhabricatorProjectSubprojectWarningController' => 'PhabricatorProjectController',
'PhabricatorProjectSubprojectsController' => 'PhabricatorProjectController', 'PhabricatorProjectSubprojectsController' => 'PhabricatorProjectController',
'PhabricatorProjectSubprojectsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorProjectSubprojectsProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorProjectSubtypeDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectSubtypesConfigType' => 'PhabricatorJSONConfigType',
'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorProjectTransaction' => 'PhabricatorModularTransaction', 'PhabricatorProjectTransaction' => 'PhabricatorModularTransaction',
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',

View File

@@ -5,55 +5,81 @@
* @task response Response Handling * @task response Response Handling
* @task exception Exception Handling * @task exception Exception Handling
*/ */
abstract class AphrontApplicationConfiguration extends Phobject { final class AphrontApplicationConfiguration
extends Phobject {
private $request; private $request;
private $host; private $host;
private $path; private $path;
private $console; private $console;
abstract public function buildRequest(); public function buildRequest() {
abstract public function build404Controller(); $parser = new PhutilQueryStringParser();
abstract public function buildRedirectController($uri, $external);
final public function setRequest(AphrontRequest $request) { $data = array();
$data += $_POST;
$data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', ''));
$cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix');
$request = new AphrontRequest($this->getHost(), $this->getPath());
$request->setRequestData($data);
$request->setApplicationConfiguration($this);
$request->setCookiePrefix($cookie_prefix);
return $request;
}
public function build404Controller() {
return array(new Phabricator404Controller(), array());
}
public function buildRedirectController($uri, $external) {
return array(
new PhabricatorRedirectController(),
array(
'uri' => $uri,
'external' => $external,
),
);
}
public function setRequest(AphrontRequest $request) {
$this->request = $request; $this->request = $request;
return $this; return $this;
} }
final public function getRequest() { public function getRequest() {
return $this->request; return $this->request;
} }
final public function getConsole() { public function getConsole() {
return $this->console; return $this->console;
} }
final public function setConsole($console) { public function setConsole($console) {
$this->console = $console; $this->console = $console;
return $this; return $this;
} }
final public function setHost($host) { public function setHost($host) {
$this->host = $host; $this->host = $host;
return $this; return $this;
} }
final public function getHost() { public function getHost() {
return $this->host; return $this->host;
} }
final public function setPath($path) { public function setPath($path) {
$this->path = $path; $this->path = $path;
return $this; return $this;
} }
final public function getPath() { public function getPath() {
return $this->path; return $this->path;
} }
public function willBuildRequest() {}
/** /**
* @phutil-external-symbol class PhabricatorStartup * @phutil-external-symbol class PhabricatorStartup
@@ -83,6 +109,8 @@ abstract class AphrontApplicationConfiguration extends Phobject {
PhabricatorStartup::beginStartupPhase('env.init'); PhabricatorStartup::beginStartupPhase('env.init');
self::readHTTPPOSTData();
try { try {
PhabricatorEnv::initializeWebEnvironment(); PhabricatorEnv::initializeWebEnvironment();
$database_exception = null; $database_exception = null;
@@ -142,16 +170,10 @@ abstract class AphrontApplicationConfiguration extends Phobject {
$host = AphrontRequest::getHTTPHeader('Host'); $host = AphrontRequest::getHTTPHeader('Host');
$path = $_REQUEST['__path__']; $path = $_REQUEST['__path__'];
switch ($host) { $application = new self();
default:
$config_key = 'aphront.default-application-configuration-class';
$application = PhabricatorEnv::newObjectFromConfig($config_key);
break;
}
$application->setHost($host); $application->setHost($host);
$application->setPath($path); $application->setPath($path);
$application->willBuildRequest();
$request = $application->buildRequest(); $request = $application->buildRequest();
// Now that we have a request, convert the write guard into one which // Now that we have a request, convert the write guard into one which
@@ -313,7 +335,7 @@ abstract class AphrontApplicationConfiguration extends Phobject {
* parameters. * parameters.
* @task routing * @task routing
*/ */
final private function buildController() { private function buildController() {
$request = $this->getRequest(); $request = $this->getRequest();
// If we're configured to operate in cluster mode, reject requests which // If we're configured to operate in cluster mode, reject requests which
@@ -708,4 +730,88 @@ abstract class AphrontApplicationConfiguration extends Phobject {
->setContent($result); ->setContent($result);
} }
private static function readHTTPPOSTData() {
$request_method = idx($_SERVER, 'REQUEST_METHOD');
if ($request_method === 'PUT') {
// For PUT requests, do nothing: in particular, do NOT read input. This
// allows us to stream input later and process very large PUT requests,
// like those coming from Git LFS.
return;
}
// For POST requests, we're going to read the raw input ourselves here
// if we can. Among other things, this corrects variable names with
// the "." character in them, which PHP normally converts into "_".
// There are two major considerations here: whether the
// `enable_post_data_reading` option is set, and whether the content
// type is "multipart/form-data" or not.
// If `enable_post_data_reading` is off, we're free to read the entire
// raw request body and parse it -- and we must, because $_POST and
// $_FILES are not built for us. If `enable_post_data_reading` is on,
// which is the default, we may not be able to read the body (the
// documentation says we can't, but empirically we can at least some
// of the time).
// If the content type is "multipart/form-data", we need to build both
// $_POST and $_FILES, which is involved. The body itself is also more
// difficult to parse than other requests.
$raw_input = PhabricatorStartup::getRawInput();
$parser = new PhutilQueryStringParser();
if (strlen($raw_input)) {
$content_type = idx($_SERVER, 'CONTENT_TYPE');
$is_multipart = preg_match('@^multipart/form-data@i', $content_type);
if ($is_multipart && !ini_get('enable_post_data_reading')) {
$multipart_parser = id(new AphrontMultipartParser())
->setContentType($content_type);
$multipart_parser->beginParse();
$multipart_parser->continueParse($raw_input);
$parts = $multipart_parser->endParse();
// We're building and then parsing a query string so that requests
// with arrays (like "x[]=apple&x[]=banana") work correctly. This also
// means we can't use "phutil_build_http_querystring()", since it
// can't build a query string with duplicate names.
$query_string = array();
foreach ($parts as $part) {
if (!$part->isVariable()) {
continue;
}
$name = $part->getName();
$value = $part->getVariableValue();
$query_string[] = rawurlencode($name).'='.rawurlencode($value);
}
$query_string = implode('&', $query_string);
$post = $parser->parseQueryString($query_string);
$files = array();
foreach ($parts as $part) {
if ($part->isVariable()) {
continue;
}
$files[$part->getName()] = $part->getPHPFileDictionary();
}
$_FILES = $files;
} else {
$post = $parser->parseQueryString($raw_input);
}
$_POST = $post;
PhabricatorStartup::rebuildRequest();
} else if ($_POST) {
$post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW);
if (is_array($post)) {
$_POST = $post;
PhabricatorStartup::rebuildRequest();
}
}
}
} }

View File

@@ -1,121 +0,0 @@
<?php
/**
* NOTE: Do not extend this!
*
* @concrete-extensible
*/
class AphrontDefaultApplicationConfiguration
extends AphrontApplicationConfiguration {
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public function buildRequest() {
$parser = new PhutilQueryStringParser();
$data = array();
$request_method = idx($_SERVER, 'REQUEST_METHOD');
if ($request_method === 'PUT') {
// For PUT requests, do nothing: in particular, do NOT read input. This
// allows us to stream input later and process very large PUT requests,
// like those coming from Git LFS.
} else {
// For POST requests, we're going to read the raw input ourselves here
// if we can. Among other things, this corrects variable names with
// the "." character in them, which PHP normally converts into "_".
// There are two major considerations here: whether the
// `enable_post_data_reading` option is set, and whether the content
// type is "multipart/form-data" or not.
// If `enable_post_data_reading` is off, we're free to read the entire
// raw request body and parse it -- and we must, because $_POST and
// $_FILES are not built for us. If `enable_post_data_reading` is on,
// which is the default, we may not be able to read the body (the
// documentation says we can't, but empirically we can at least some
// of the time).
// If the content type is "multipart/form-data", we need to build both
// $_POST and $_FILES, which is involved. The body itself is also more
// difficult to parse than other requests.
$raw_input = PhabricatorStartup::getRawInput();
if (strlen($raw_input)) {
$content_type = idx($_SERVER, 'CONTENT_TYPE');
$is_multipart = preg_match('@^multipart/form-data@i', $content_type);
if ($is_multipart && !ini_get('enable_post_data_reading')) {
$multipart_parser = id(new AphrontMultipartParser())
->setContentType($content_type);
$multipart_parser->beginParse();
$multipart_parser->continueParse($raw_input);
$parts = $multipart_parser->endParse();
$query_string = array();
foreach ($parts as $part) {
if (!$part->isVariable()) {
continue;
}
$name = $part->getName();
$value = $part->getVariableValue();
$query_string[] = urlencode($name).'='.urlencode($value);
}
$query_string = implode('&', $query_string);
$post = $parser->parseQueryString($query_string);
$files = array();
foreach ($parts as $part) {
if ($part->isVariable()) {
continue;
}
$files[$part->getName()] = $part->getPHPFileDictionary();
}
$_FILES = $files;
} else {
$post = $parser->parseQueryString($raw_input);
}
$_POST = $post;
PhabricatorStartup::rebuildRequest();
$data += $post;
} else if ($_POST) {
$post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW);
if (is_array($post)) {
$_POST = $post;
PhabricatorStartup::rebuildRequest();
}
$data += $_POST;
}
}
$data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', ''));
$cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix');
$request = new AphrontRequest($this->getHost(), $this->getPath());
$request->setRequestData($data);
$request->setApplicationConfiguration($this);
$request->setCookiePrefix($cookie_prefix);
return $request;
}
public function build404Controller() {
return array(new Phabricator404Controller(), array());
}
public function buildRedirectController($uri, $external) {
return array(
new PhabricatorRedirectController(),
array(
'uri' => $uri,
'external' => $external,
),
);
}
}

View File

@@ -78,15 +78,13 @@ final class PhabricatorHighSecurityRequestExceptionHandler
$form_layout = $form->buildLayoutView(); $form_layout = $form->buildLayoutView();
if ($is_upgrade) { if ($is_upgrade) {
$messages = array( $message = pht(
pht(
'You are taking an action which requires you to enter '. 'You are taking an action which requires you to enter '.
'high security.'), 'high security.');
);
$info_view = id(new PHUIInfoView()) $info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA) ->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors($messages); ->setErrors(array($message));
$dialog $dialog
->appendChild($info_view) ->appendChild($info_view)
@@ -100,12 +98,18 @@ final class PhabricatorHighSecurityRequestExceptionHandler
'period of time. When you are finished taking sensitive '. 'period of time. When you are finished taking sensitive '.
'actions, you should leave high security.')); 'actions, you should leave high security.'));
} else { } else {
$message = pht(
'You are taking an action which requires you to provide '.
'multi-factor credentials.');
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors(array($message));
$dialog $dialog
->appendChild($info_view)
->setErrors( ->setErrors(
array( array(
pht(
'You are taking an action which requires you to provide '.
'multi-factor credentials.'),
)) ))
->appendChild($form_layout); ->appendChild($form_layout);
} }

View File

@@ -95,6 +95,8 @@ final class PhabricatorAuthApplication extends PhabricatorApplication {
'PhabricatorAuthFactorProviderEditController', 'PhabricatorAuthFactorProviderEditController',
'(?P<id>[1-9]\d*)/' => '(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthFactorProviderViewController', 'PhabricatorAuthFactorProviderViewController',
'message/(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthFactorProviderMessageController',
), ),
'message/' => array( 'message/' => array(

View File

@@ -0,0 +1,103 @@
<?php
final class PhabricatorAuthFactorProviderStatus
extends Phobject {
private $key;
private $spec = array();
const STATUS_ACTIVE = 'active';
const STATUS_DEPRECATED = 'deprecated';
const STATUS_DISABLED = 'disabled';
public static function newForStatus($status) {
$result = new self();
$result->key = $status;
$result->spec = self::newSpecification($status);
return $result;
}
public function getName() {
return idx($this->spec, 'name', $this->key);
}
public function getStatusHeaderIcon() {
return idx($this->spec, 'header.icon');
}
public function getStatusHeaderColor() {
return idx($this->spec, 'header.color');
}
public function isActive() {
return ($this->key === self::STATUS_ACTIVE);
}
public function getListIcon() {
return idx($this->spec, 'list.icon');
}
public function getListColor() {
return idx($this->spec, 'list.color');
}
public function getFactorIcon() {
return idx($this->spec, 'factor.icon');
}
public function getFactorColor() {
return idx($this->spec, 'factor.color');
}
public function getOrder() {
return idx($this->spec, 'order', 0);
}
public static function getMap() {
$specs = self::newSpecifications();
return ipull($specs, 'name');
}
private static function newSpecification($key) {
$specs = self::newSpecifications();
return idx($specs, $key, array());
}
private static function newSpecifications() {
return array(
self::STATUS_ACTIVE => array(
'name' => pht('Active'),
'header.icon' => 'fa-check',
'header.color' => null,
'list.icon' => null,
'list.color' => null,
'factor.icon' => 'fa-check',
'factor.color' => 'green',
'order' => 1,
),
self::STATUS_DEPRECATED => array(
'name' => pht('Deprecated'),
'header.icon' => 'fa-ban',
'header.color' => 'indigo',
'list.icon' => 'fa-ban',
'list.color' => 'indigo',
'factor.icon' => 'fa-ban',
'factor.color' => 'indigo',
'order' => 2,
),
self::STATUS_DISABLED => array(
'name' => pht('Disabled'),
'header.icon' => 'fa-times',
'header.color' => 'red',
'list.icon' => 'fa-times',
'list.color' => 'red',
'factor.icon' => 'fa-times',
'factor.color' => 'grey',
'order' => 3,
),
);
}
}

View File

@@ -197,6 +197,8 @@ final class PhabricatorAuthNeedsMultiFactorController
->addCancelButton('/', pht('Continue')); ->addCancelButton('/', pht('Continue'));
} }
$views = array();
$messages = array(); $messages = array();
$messages[] = pht( $messages[] = pht(
@@ -210,7 +212,39 @@ final class PhabricatorAuthNeedsMultiFactorController
->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors($messages); ->setErrors($messages);
return $view; $views[] = $view;
$providers = id(new PhabricatorAuthFactorProviderQuery())
->setViewer($viewer)
->withStatuses(
array(
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
))
->execute();
if (!$providers) {
$messages = array();
$required_key = 'security.require-multi-factor-auth';
$messages[] = pht(
'This install has the configuration option "%s" enabled, but does '.
'not have any active multifactor providers configured. This means '.
'you are required to add MFA, but are also prevented from doing so. '.
'An administrator must disable "%s" or enable an MFA provider to '.
'allow you to continue.',
$required_key,
$required_key);
$view = id(new PHUIInfoView())
->setTitle(pht('Multi-Factor Authentication is Misconfigured'))
->setSeverity(PHUIInfoView::SEVERITY_ERROR)
->setErrors($messages);
$views[] = $view;
}
return $views;
} }
} }

View File

@@ -20,6 +20,16 @@ final class PhabricatorAuthFactorProviderListController
->setHeader($provider->getDisplayName()) ->setHeader($provider->getDisplayName())
->setHref($provider->getURI()); ->setHref($provider->getURI());
$status = $provider->newStatus();
$icon = $status->getListIcon();
$color = $status->getListColor();
if ($icon !== null) {
$item->setStatusIcon("{$icon} {$color}", $status->getName());
}
$item->setDisabled(!$status->isActive());
$list->addItem($item); $list->addItem($item);
} }

View File

@@ -0,0 +1,84 @@
<?php
final class PhabricatorAuthFactorProviderMessageController
extends PhabricatorAuthFactorProviderController {
public function handleRequest(AphrontRequest $request) {
$this->requireApplicationCapability(
AuthManageProvidersCapability::CAPABILITY);
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$provider = id(new PhabricatorAuthFactorProviderQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$provider) {
return new Aphront404Response();
}
$cancel_uri = $provider->getURI();
$enroll_key =
PhabricatorAuthFactorProviderEnrollMessageTransaction::TRANSACTIONTYPE;
$message = $provider->getEnrollMessage();
if ($request->isFormOrHisecPost()) {
$message = $request->getStr('message');
$xactions = array();
$xactions[] = id(new PhabricatorAuthFactorProviderTransaction())
->setTransactionType($enroll_key)
->setNewValue($message);
$editor = id(new PhabricatorAuthFactorProviderEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setCancelURI($cancel_uri);
$editor->applyTransactions($provider, $xactions);
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
$default_message = $provider->getEnrollDescription($viewer);
$default_message = new PHUIRemarkupView($viewer, $default_message);
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendRemarkupInstructions(
pht(
'When users add a factor for this provider, they are given this '.
'enrollment guidance by default:'))
->appendControl(
id(new AphrontFormMarkupControl())
->setLabel(pht('Default Message'))
->setValue($default_message))
->appendRemarkupInstructions(
pht(
'You may optionally customize the enrollment message users are '.
'presented with by providing a replacement message below:'))
->appendControl(
id(new PhabricatorRemarkupControl())
->setLabel(pht('Custom Message'))
->setName('message')
->setValue($message));
return $this->newDialog()
->setTitle(pht('Change Enroll Message'))
->setWidth(AphrontDialogView::WIDTH_FORM)
->appendForm($form)
->addCancelButton($cancel_uri)
->addSubmitButton(pht('Save'));
}
}

View File

@@ -58,6 +58,15 @@ final class PhabricatorAuthFactorProviderViewController
->setHeader($provider->getDisplayName()) ->setHeader($provider->getDisplayName())
->setPolicyObject($provider); ->setPolicyObject($provider);
$status = $provider->newStatus();
$header_icon = $status->getStatusHeaderIcon();
$header_color = $status->getStatusHeaderColor();
$header_name = $status->getName();
if ($header_icon !== null) {
$view->setStatus($header_icon, $header_color, $header_name);
}
return $view; return $view;
} }
@@ -72,6 +81,16 @@ final class PhabricatorAuthFactorProviderViewController
pht('Factor Type'), pht('Factor Type'),
$provider->getFactor()->getFactorName()); $provider->getFactor()->getFactorName());
$custom_enroll = $provider->getEnrollMessage();
if (strlen($custom_enroll)) {
$view->addSectionHeader(
pht('Custom Enroll Message'),
PHUIPropertyListView::ICON_SUMMARY);
$view->addTextContent(
new PHUIRemarkupView($viewer, $custom_enroll));
}
return $view; return $view;
} }
@@ -94,6 +113,14 @@ final class PhabricatorAuthFactorProviderViewController
->setDisabled(!$can_edit) ->setDisabled(!$can_edit)
->setWorkflow(!$can_edit)); ->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Customize Enroll Message'))
->setIcon('fa-commenting-o')
->setHref($this->getApplicationURI("mfa/message/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow(true));
return $curtain; return $curtain;
} }

View File

@@ -93,9 +93,12 @@ final class PhabricatorAuthFactorProviderEditEngine
} }
protected function buildCustomEditFields($object) { protected function buildCustomEditFields($object) {
$factor_name = $object->getFactor()->getFactorName(); $factor = $object->getFactor();
$factor_name = $factor->getFactorName();
return array( $status_map = PhabricatorAuthFactorProviderStatus::getMap();
$fields = array(
id(new PhabricatorStaticEditField()) id(new PhabricatorStaticEditField())
->setKey('displayType') ->setKey('displayType')
->setLabel(pht('Factor Type')) ->setLabel(pht('Factor Type'))
@@ -109,7 +112,22 @@ final class PhabricatorAuthFactorProviderEditEngine
->setDescription(pht('Display name for the MFA provider.')) ->setDescription(pht('Display name for the MFA provider.'))
->setValue($object->getName()) ->setValue($object->getName())
->setPlaceholder($factor_name), ->setPlaceholder($factor_name),
id(new PhabricatorSelectEditField())
->setKey('status')
->setTransactionType(
PhabricatorAuthFactorProviderStatusTransaction::TRANSACTIONTYPE)
->setLabel(pht('Status'))
->setDescription(pht('Status of the MFA provider.'))
->setValue($object->getStatus())
->setOptions($status_map),
); );
$factor_fields = $factor->newEditEngineFields($this, $object);
foreach ($factor_fields as $field) {
$fields[] = $field;
}
return $fields;
} }
} }

View File

@@ -0,0 +1,10 @@
<?php
final class PhabricatorAuthFactorProviderMFAEngine
extends PhabricatorEditEngineMFAEngine {
public function shouldTryMFA() {
return true;
}
}

View File

@@ -473,9 +473,20 @@ final class PhabricatorAuthSessionEngine extends Phobject {
$factors = id(new PhabricatorAuthFactorConfigQuery()) $factors = id(new PhabricatorAuthFactorConfigQuery())
->setViewer($viewer) ->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID())) ->withUserPHIDs(array($viewer->getPHID()))
->setOrderVector(array('-id')) ->withFactorProviderStatuses(
array(
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
))
->execute(); ->execute();
// Sort factors in the same order that they appear in on the Settings
// panel. This means that administrators changing provider statuses may
// change the order of prompts for users, but the alternative is that the
// Settings panel order disagrees with the prompt order, which seems more
// disruptive.
$factors = msort($factors, 'newSortVector');
// If the account has no associated multi-factor auth, just issue a token // If the account has no associated multi-factor auth, just issue a token
// without putting the session into high security mode. This is generally // without putting the session into high security mode. This is generally
// easier for users. A minor but desirable side effect is that when a user // easier for users. A minor but desirable side effect is that when a user
@@ -529,14 +540,22 @@ final class PhabricatorAuthSessionEngine extends Phobject {
$provider = $factor->getFactorProvider(); $provider = $factor->getFactorProvider();
$impl = $provider->getFactor(); $impl = $provider->getFactor();
try {
$new_challenges = $impl->getNewIssuedChallenges( $new_challenges = $impl->getNewIssuedChallenges(
$factor, $factor,
$viewer, $viewer,
$issued_challenges); $issued_challenges);
} catch (PhabricatorAuthFactorResultException $ex) {
// NOTE: We may get a list of challenges back, or may just get an early
// result. For example, this can happen on an SMS factor if all SMS
// mailers have been disabled.
if ($new_challenges instanceof PhabricatorAuthFactorResult) {
$result = $new_challenges;
if (!$result->getIsValid()) {
$ok = false; $ok = false;
$validation_results[$factor_phid] = $ex->getResult(); }
$validation_results[$factor_phid] = $result;
$challenge_map[$factor_phid] = $issued_challenges; $challenge_map[$factor_phid] = $issued_challenges;
continue; continue;
} }

View File

@@ -1,17 +0,0 @@
<?php
final class PhabricatorAuthFactorResultException
extends Exception {
private $result;
public function __construct(PhabricatorAuthFactorResult $result) {
$this->result = $result;
parent::__construct();
}
public function getResult() {
return $this->result;
}
}

View File

@@ -3,6 +3,7 @@
abstract class PhabricatorAuthFactor extends Phobject { abstract class PhabricatorAuthFactor extends Phobject {
abstract public function getFactorName(); abstract public function getFactorName();
abstract public function getFactorShortName();
abstract public function getFactorKey(); abstract public function getFactorKey();
abstract public function getFactorCreateHelp(); abstract public function getFactorCreateHelp();
abstract public function getFactorDescription(); abstract public function getFactorDescription();
@@ -54,14 +55,31 @@ abstract class PhabricatorAuthFactor extends Phobject {
return null; return null;
} }
public function canCreateNewConfiguration(PhabricatorUser $user) { public function canCreateNewConfiguration(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return true; return true;
} }
public function getConfigurationCreateDescription(PhabricatorUser $user) { public function getConfigurationCreateDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return null; return null;
} }
public function getConfigurationListDetails(
PhabricatorAuthFactorConfig $config,
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $viewer) {
return null;
}
public function newEditEngineFields(
PhabricatorEditEngine $engine,
PhabricatorAuthFactorProvider $provider) {
return array();
}
/** /**
* Is this a factor which depends on the user's contact number? * Is this a factor which depends on the user's contact number?
* *
@@ -129,6 +147,11 @@ abstract class PhabricatorAuthFactor extends Phobject {
$viewer, $viewer,
$challenges); $challenges);
if ($this->isAuthResult($new_challenges)) {
unset($unguarded);
return $new_challenges;
}
assert_instances_of($new_challenges, 'PhabricatorAuthChallenge'); assert_instances_of($new_challenges, 'PhabricatorAuthChallenge');
foreach ($new_challenges as $new_challenge) { foreach ($new_challenges as $new_challenge) {
@@ -177,7 +200,7 @@ abstract class PhabricatorAuthFactor extends Phobject {
return $result; return $result;
} }
if (!($result instanceof PhabricatorAuthFactorResult)) { if (!$this->isAuthResult($result)) {
throw new Exception( throw new Exception(
pht( pht(
'Expected "newResultFromIssuedChallenges()" to return null or '. 'Expected "newResultFromIssuedChallenges()" to return null or '.
@@ -209,7 +232,7 @@ abstract class PhabricatorAuthFactor extends Phobject {
$request, $request,
$challenges); $challenges);
if (!($result instanceof PhabricatorAuthFactorResult)) { if (!$this->isAuthResult($result)) {
throw new Exception( throw new Exception(
pht( pht(
'Expected "newResultFromChallengeResponse()" to return an object '. 'Expected "newResultFromChallengeResponse()" to return an object '.
@@ -314,6 +337,7 @@ abstract class PhabricatorAuthFactor extends Phobject {
final protected function loadMFASyncToken( final protected function loadMFASyncToken(
PhabricatorAuthFactorProvider $provider,
AphrontRequest $request, AphrontRequest $request,
AphrontFormView $form, AphrontFormView $form,
PhabricatorUser $user) { PhabricatorUser $user) {
@@ -380,7 +404,13 @@ abstract class PhabricatorAuthFactor extends Phobject {
->setTokenCode($sync_key_digest) ->setTokenCode($sync_key_digest)
->setTokenExpires($now + $sync_ttl); ->setTokenExpires($now + $sync_ttl);
$properties = $this->newMFASyncTokenProperties($user); $properties = $this->newMFASyncTokenProperties(
$provider,
$user);
if ($this->isAuthResult($properties)) {
return $properties;
}
foreach ($properties as $key => $value) { foreach ($properties as $key => $value) {
$sync_token->setTemporaryTokenProperty($key, $value); $sync_token->setTemporaryTokenProperty($key, $value);
@@ -394,7 +424,9 @@ abstract class PhabricatorAuthFactor extends Phobject {
return $sync_token; return $sync_token;
} }
protected function newMFASyncTokenProperties(PhabricatorUser $user) { protected function newMFASyncTokenProperties(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return array(); return array();
} }
@@ -481,10 +513,6 @@ abstract class PhabricatorAuthFactor extends Phobject {
$rows); $rows);
} }
final protected function throwResult(PhabricatorAuthFactorResult $result) {
throw new PhabricatorAuthFactorResultException($result);
}
final protected function getInstallDisplayName() { final protected function getInstallDisplayName() {
$uri = PhabricatorEnv::getURI('/'); $uri = PhabricatorEnv::getURI('/');
$uri = new PhutilURI($uri); $uri = new PhutilURI($uri);
@@ -520,4 +548,19 @@ abstract class PhabricatorAuthFactor extends Phobject {
return $request->validateCSRF(); return $request->validateCSRF();
} }
final protected function loadConfigurationsForProvider(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return id(new PhabricatorAuthFactorConfigQuery())
->setViewer($user)
->withUserPHIDs(array($user->getPHID()))
->withFactorProviderPHIDs(array($provider->getPHID()))
->execute();
}
final protected function isAuthResult($object) {
return ($object instanceof PhabricatorAuthFactorResult);
}
} }

View File

@@ -0,0 +1,813 @@
<?php
final class PhabricatorDuoAuthFactor
extends PhabricatorAuthFactor {
const PROP_CREDENTIAL = 'duo.credentialPHID';
const PROP_ENROLL = 'duo.enroll';
const PROP_USERNAMES = 'duo.usernames';
const PROP_HOSTNAME = 'duo.hostname';
public function getFactorKey() {
return 'duo';
}
public function getFactorName() {
return pht('Duo Security');
}
public function getFactorShortName() {
return pht('Duo');
}
public function getFactorCreateHelp() {
return pht('Support for Duo push authentication.');
}
public function getFactorDescription() {
return pht(
'When you need to authenticate, a request will be pushed to the '.
'Duo application on your phone.');
}
public function getEnrollDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return pht(
'To add a Duo factor, first download and install the Duo application '.
'on your phone. Once you have launched the application and are ready '.
'to perform setup, click continue.');
}
public function canCreateNewConfiguration(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
if ($this->loadConfigurationsForProvider($provider, $user)) {
return false;
}
return true;
}
public function getConfigurationCreateDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
$messages = array();
if ($this->loadConfigurationsForProvider($provider, $user)) {
$messages[] = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
pht(
'You already have Duo authentication attached to your account '.
'for this provider.'),
));
}
return $messages;
}
public function getConfigurationListDetails(
PhabricatorAuthFactorConfig $config,
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $viewer) {
$duo_user = $config->getAuthFactorConfigProperty('duo.username');
return pht('Duo Username: %s', $duo_user);
}
public function newEditEngineFields(
PhabricatorEditEngine $engine,
PhabricatorAuthFactorProvider $provider) {
$viewer = $engine->getViewer();
$credential_phid = $provider->getAuthFactorProviderProperty(
self::PROP_CREDENTIAL);
$hostname = $provider->getAuthFactorProviderProperty(self::PROP_HOSTNAME);
$usernames = $provider->getAuthFactorProviderProperty(self::PROP_USERNAMES);
$enroll = $provider->getAuthFactorProviderProperty(self::PROP_ENROLL);
$credential_type = PassphrasePasswordCredentialType::CREDENTIAL_TYPE;
$provides_type = PassphrasePasswordCredentialType::PROVIDES_TYPE;
$credentials = id(new PassphraseCredentialQuery())
->setViewer($viewer)
->withIsDestroyed(false)
->withProvidesTypes(array($provides_type))
->execute();
$xaction_hostname =
PhabricatorAuthFactorProviderDuoHostnameTransaction::TRANSACTIONTYPE;
$xaction_credential =
PhabricatorAuthFactorProviderDuoCredentialTransaction::TRANSACTIONTYPE;
$xaction_usernames =
PhabricatorAuthFactorProviderDuoUsernamesTransaction::TRANSACTIONTYPE;
$xaction_enroll =
PhabricatorAuthFactorProviderDuoEnrollTransaction::TRANSACTIONTYPE;
return array(
id(new PhabricatorTextEditField())
->setLabel(pht('Duo API Hostname'))
->setKey('duo.hostname')
->setValue($hostname)
->setTransactionType($xaction_hostname)
->setIsRequired(true),
id(new PhabricatorCredentialEditField())
->setLabel(pht('Duo API Credential'))
->setKey('duo.credential')
->setValue($credential_phid)
->setTransactionType($xaction_credential)
->setCredentialType($credential_type)
->setCredentials($credentials),
id(new PhabricatorSelectEditField())
->setLabel(pht('Duo Username'))
->setKey('duo.usernames')
->setValue($usernames)
->setTransactionType($xaction_usernames)
->setOptions(
array(
'username' => pht('Use Phabricator Username'),
'email' => pht('Use Primary Email Address'),
)),
id(new PhabricatorSelectEditField())
->setLabel(pht('Create Accounts'))
->setKey('duo.enroll')
->setValue($enroll)
->setTransactionType($xaction_enroll)
->setOptions(
array(
'deny' => pht('Require Existing Duo Account'),
'allow' => pht('Create New Duo Account'),
)),
);
}
public function processAddFactorForm(
PhabricatorAuthFactorProvider $provider,
AphrontFormView $form,
AphrontRequest $request,
PhabricatorUser $user) {
$token = $this->loadMFASyncToken($provider, $request, $form, $user);
if ($this->isAuthResult($token)) {
$form->appendChild($this->newAutomaticControl($token));
return;
}
$enroll = $token->getTemporaryTokenProperty('duo.enroll');
$duo_id = $token->getTemporaryTokenProperty('duo.user-id');
$duo_uri = $token->getTemporaryTokenProperty('duo.uri');
$duo_user = $token->getTemporaryTokenProperty('duo.username');
$is_external = ($enroll === 'external');
$is_auto = ($enroll === 'auto');
$is_blocked = ($enroll === 'blocked');
if (!$token->getIsNewTemporaryToken()) {
if ($is_auto) {
return $this->newDuoConfig($user, $duo_user);
} else if ($is_external || $is_blocked) {
$parameters = array(
'username' => $duo_user,
);
$result = $this->newDuoFuture($provider)
->setMethod('preauth', $parameters)
->resolve();
$result_code = $result['response']['result'];
switch ($result_code) {
case 'auth':
case 'allow':
return $this->newDuoConfig($user, $duo_user);
case 'enroll':
if ($is_blocked) {
// We'll render an equivalent static control below, so skip
// rendering here. We explicitly don't want to give the user
// an enroll workflow.
break;
}
$duo_uri = $result['response']['enroll_portal_url'];
$waiting_icon = id(new PHUIIconView())
->setIcon('fa-mobile', 'red');
$waiting_control = id(new PHUIFormTimerControl())
->setIcon($waiting_icon)
->setError(pht('Not Complete'))
->appendChild(
pht(
'You have not completed Duo enrollment yet. '.
'Complete enrollment, then click continue.'));
$form->appendControl($waiting_control);
break;
default:
case 'deny':
break;
}
} else {
$parameters = array(
'user_id' => $duo_id,
'activation_code' => $duo_uri,
);
$future = $this->newDuoFuture($provider)
->setMethod('enroll_status', $parameters);
$result = $future->resolve();
$response = $result['response'];
switch ($response) {
case 'success':
return $this->newDuoConfig($user, $duo_user);
case 'waiting':
$waiting_icon = id(new PHUIIconView())
->setIcon('fa-mobile', 'red');
$waiting_control = id(new PHUIFormTimerControl())
->setIcon($waiting_icon)
->setError(pht('Not Complete'))
->appendChild(
pht(
'You have not activated this enrollment in the Duo '.
'application on your phone yet. Complete activation, then '.
'click continue.'));
$form->appendControl($waiting_control);
break;
case 'invalid':
default:
throw new Exception(
pht(
'This Duo enrollment attempt is invalid or has '.
'expired ("%s"). Cancel the workflow and try again.',
$response));
}
}
}
if ($is_blocked) {
$blocked_icon = id(new PHUIIconView())
->setIcon('fa-times', 'red');
$blocked_control = id(new PHUIFormTimerControl())
->setIcon($blocked_icon)
->appendChild(
pht(
'Your Duo account ("%s") has not completed Duo enrollment. '.
'Check your email and complete enrollment to continue.',
phutil_tag('strong', array(), $duo_user)));
$form->appendControl($blocked_control);
} else if ($is_auto) {
$auto_icon = id(new PHUIIconView())
->setIcon('fa-check', 'green');
$auto_control = id(new PHUIFormTimerControl())
->setIcon($auto_icon)
->appendChild(
pht(
'Duo account ("%s") is fully enrolled.',
phutil_tag('strong', array(), $duo_user)));
$form->appendControl($auto_control);
} else {
$duo_button = phutil_tag(
'a',
array(
'href' => $duo_uri,
'class' => 'button button-grey',
'target' => ($is_external ? '_blank' : null),
),
pht('Enroll Duo Account: %s', $duo_user));
$duo_button = phutil_tag(
'div',
array(
'class' => 'mfa-form-enroll-button',
),
$duo_button);
if ($is_external) {
$form->appendRemarkupInstructions(
pht(
'Complete enrolling your phone with Duo:'));
$form->appendControl(
id(new AphrontFormMarkupControl())
->setValue($duo_button));
} else {
$form->appendRemarkupInstructions(
pht(
'Scan this QR code with the Duo application on your mobile '.
'phone:'));
$qr_code = $this->newQRCode($duo_uri);
$form->appendChild($qr_code);
$form->appendRemarkupInstructions(
pht(
'If you are currently using your phone to view this page, '.
'click this button to open the Duo application:'));
$form->appendControl(
id(new AphrontFormMarkupControl())
->setValue($duo_button));
}
$form->appendRemarkupInstructions(
pht(
'Once you have completed setup on your phone, click continue.'));
}
}
protected function newMFASyncTokenProperties(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
$duo_user = $this->getDuoUsername($provider, $user);
// Duo automatically normalizes usernames to lowercase. Just do that here
// so that our value agrees more closely with Duo.
$duo_user = phutil_utf8_strtolower($duo_user);
$parameters = array(
'username' => $duo_user,
);
$result = $this->newDuoFuture($provider)
->setMethod('preauth', $parameters)
->resolve();
$external_uri = null;
$result_code = $result['response']['result'];
$status_message = $result['response']['status_msg'];
switch ($result_code) {
case 'auth':
case 'allow':
// If the user already has a Duo account, they don't need to do
// anything.
return array(
'duo.enroll' => 'auto',
'duo.username' => $duo_user,
);
case 'enroll':
if (!$this->shouldAllowDuoEnrollment($provider)) {
return array(
'duo.enroll' => 'blocked',
'duo.username' => $duo_user,
);
}
$external_uri = $result['response']['enroll_portal_url'];
// Otherwise, enrollment is permitted so we're going to continue.
break;
default:
case 'deny':
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'Your Duo account ("%s") is not permitted to access this '.
'system. Contact your Duo administrator for help. '.
'The Duo preauth API responded with status message ("%s"): %s',
$duo_user,
$result_code,
$status_message));
}
// Duo's "/enroll" API isn't repeatable for the same username. If we're
// the first call, great: we can do inline enrollment, which is way more
// user friendly. Otherwise, we have to send the user on an adventure.
$parameters = array(
'username' => $duo_user,
'valid_secs' => phutil_units('1 hour in seconds'),
);
try {
$result = $this->newDuoFuture($provider)
->setMethod('enroll', $parameters)
->resolve();
} catch (HTTPFutureHTTPResponseStatus $ex) {
return array(
'duo.enroll' => 'external',
'duo.username' => $duo_user,
'duo.uri' => $external_uri,
);
}
return array(
'duo.enroll' => 'inline',
'duo.uri' => $result['response']['activation_code'],
'duo.username' => $duo_user,
'duo.user-id' => $result['response']['user_id'],
);
}
protected function newIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
// If we already issued a valid challenge for this workflow and session,
// don't issue a new one.
$challenge = $this->getChallengeForCurrentContext(
$config,
$viewer,
$challenges);
if ($challenge) {
return array();
}
if (!$this->hasCSRF($config)) {
return $this->newResult()
->setIsContinue(true)
->setErrorMessage(
pht(
'An authorization request will be pushed to the Duo '.
'application on your phone.'));
}
$provider = $config->getFactorProvider();
// Otherwise, issue a new challenge.
$duo_user = (string)$config->getAuthFactorConfigProperty('duo.username');
$parameters = array(
'username' => $duo_user,
);
$response = $this->newDuoFuture($provider)
->setMethod('preauth', $parameters)
->resolve();
$response = $response['response'];
$next_step = $response['result'];
$status_message = $response['status_msg'];
switch ($next_step) {
case 'auth':
// We're good to go.
break;
case 'allow':
// Duo is telling us to bypass MFA. For now, refuse.
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'Duo is not requiring a challenge, which defeats the '.
'purpose of MFA. Duo must be configured to challenge you.'));
case 'enroll':
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'Your Duo account ("%s") requires enrollment. Contact your '.
'Duo administrator for help. Duo status message: %s',
$duo_user,
$status_message));
case 'deny':
default:
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'Your Duo account ("%s") is not permitted to access this '.
'system. Contact your Duo administrator for help. The Duo '.
'preauth API responded with status message ("%s"): %s',
$duo_user,
$next_step,
$status_message));
}
$has_push = false;
$devices = $response['devices'];
foreach ($devices as $device) {
$capabilities = array_fuse($device['capabilities']);
if (isset($capabilities['push'])) {
$has_push = true;
break;
}
}
if (!$has_push) {
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'This factor has been removed from your device, so Phabricator '.
'can not send you a challenge. To continue, an administrator '.
'must strip this factor from your account.'));
}
$push_info = array(
pht('Domain') => $this->getInstallDisplayName(),
);
$push_info = phutil_build_http_querystring($push_info);
$parameters = array(
'username' => $duo_user,
'factor' => 'push',
'async' => '1',
// Duo allows us to specify a device, or to pass "auto" to have it pick
// the first one. For now, just let it pick.
'device' => 'auto',
// This is a hard-coded prefix for the word "... request" in the Duo UI,
// which defaults to "Login". We could pass richer information from
// workflows here, but it's not very flexible anyway.
'type' => 'Authentication',
'display_username' => $viewer->getUsername(),
'pushinfo' => $push_info,
);
$result = $this->newDuoFuture($provider)
->setMethod('auth', $parameters)
->resolve();
$duo_xaction = $result['response']['txid'];
// The Duo push timeout is 60 seconds. Set our challenge to expire slightly
// more quickly so that we'll re-issue a new challenge before Duo times out.
// This should keep users away from a dead-end where they can't respond to
// Duo but Phabricator won't issue a new challenge yet.
$ttl_seconds = 55;
return array(
$this->newChallenge($config, $viewer)
->setChallengeKey($duo_xaction)
->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),
);
}
protected function newResultFromIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
$challenge = $this->getChallengeForCurrentContext(
$config,
$viewer,
$challenges);
if ($challenge->getIsAnsweredChallenge()) {
return $this->newResult()
->setAnsweredChallenge($challenge);
}
$provider = $config->getFactorProvider();
$duo_xaction = $challenge->getChallengeKey();
$parameters = array(
'txid' => $duo_xaction,
);
// This endpoint always long-polls, so use a timeout to force it to act
// more asynchronously.
try {
$result = $this->newDuoFuture($provider)
->setHTTPMethod('GET')
->setMethod('auth_status', $parameters)
->setTimeout(5)
->resolve();
$state = $result['response']['result'];
$status = $result['response']['status'];
} catch (HTTPFutureCURLResponseStatus $exception) {
if ($exception->isTimeout()) {
$state = 'waiting';
$status = 'poll';
} else {
throw $exception;
}
}
$now = PhabricatorTime::getNow();
switch ($state) {
case 'allow':
$ttl = PhabricatorTime::getNow()
+ phutil_units('15 minutes in seconds');
$challenge
->markChallengeAsAnswered($ttl);
return $this->newResult()
->setAnsweredChallenge($challenge);
case 'waiting':
// No result yet, we'll render a default state later on.
break;
default:
case 'deny':
if ($status === 'timeout') {
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'This request has timed out because you took too long to '.
'respond.'));
} else {
$wait_duration = ($challenge->getChallengeTTL() - $now) + 1;
return $this->newResult()
->setIsWait(true)
->setErrorMessage(
pht(
'You denied this request. Wait %s second(s) to try again.',
new PhutilNumber($wait_duration)));
}
break;
}
return null;
}
public function renderValidateFactorForm(
PhabricatorAuthFactorConfig $config,
AphrontFormView $form,
PhabricatorUser $viewer,
PhabricatorAuthFactorResult $result) {
$control = $this->newAutomaticControl($result);
if (!$control) {
$result = $this->newResult()
->setIsContinue(true)
->setErrorMessage(
pht(
'A challenge has been sent to your phone. Open the Duo '.
'application and confirm the challenge, then continue.'));
$control = $this->newAutomaticControl($result);
}
$control
->setLabel(pht('Duo'))
->setCaption(pht('Factor Name: %s', $config->getFactorName()));
$form->appendChild($control);
}
public function getRequestHasChallengeResponse(
PhabricatorAuthFactorConfig $config,
AphrontRequest $request) {
$value = $this->getChallengeResponseFromRequest($config, $request);
return (bool)strlen($value);
}
protected function newResultFromChallengeResponse(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request,
array $challenges) {
$challenge = $this->getChallengeForCurrentContext(
$config,
$viewer,
$challenges);
$code = $this->getChallengeResponseFromRequest(
$config,
$request);
$result = $this->newResult()
->setValue($code);
if ($challenge->getIsAnsweredChallenge()) {
return $result->setAnsweredChallenge($challenge);
}
if (phutil_hashes_are_identical($code, $challenge->getChallengeKey())) {
$ttl = PhabricatorTime::getNow() + phutil_units('15 minutes in seconds');
$challenge
->markChallengeAsAnswered($ttl);
return $result->setAnsweredChallenge($challenge);
}
if (strlen($code)) {
$error_message = pht('Invalid');
} else {
$error_message = pht('Required');
}
$result->setErrorMessage($error_message);
return $result;
}
private function newDuoFuture(PhabricatorAuthFactorProvider $provider) {
$credential_phid = $provider->getAuthFactorProviderProperty(
self::PROP_CREDENTIAL);
$omnipotent = PhabricatorUser::getOmnipotentUser();
$credential = id(new PassphraseCredentialQuery())
->setViewer($omnipotent)
->withPHIDs(array($credential_phid))
->needSecrets(true)
->executeOne();
if (!$credential) {
throw new Exception(
pht(
'Unable to load Duo API credential ("%s").',
$credential_phid));
}
$duo_key = $credential->getUsername();
$duo_secret = $credential->getSecret();
if (!$duo_secret) {
throw new Exception(
pht(
'Duo API credential ("%s") has no secret key.',
$credential_phid));
}
$duo_host = $provider->getAuthFactorProviderProperty(
self::PROP_HOSTNAME);
self::requireDuoAPIHostname($duo_host);
return id(new PhabricatorDuoFuture())
->setIntegrationKey($duo_key)
->setSecretKey($duo_secret)
->setAPIHostname($duo_host)
->setTimeout(10)
->setHTTPMethod('POST');
}
private function getDuoUsername(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
$mode = $provider->getAuthFactorProviderProperty(self::PROP_USERNAMES);
switch ($mode) {
case 'username':
return $user->getUsername();
case 'email':
return $user->loadPrimaryEmailAddress();
default:
throw new Exception(
pht(
'Duo username pairing mode ("%s") is not supported.',
$mode));
}
}
private function shouldAllowDuoEnrollment(
PhabricatorAuthFactorProvider $provider) {
$mode = $provider->getAuthFactorProviderProperty(self::PROP_ENROLL);
switch ($mode) {
case 'deny':
return false;
case 'allow':
return true;
default:
throw new Exception(
pht(
'Duo enrollment mode ("%s") is not supported.',
$mode));
}
}
private function newDuoConfig(PhabricatorUser $user, $duo_user) {
$config_properties = array(
'duo.username' => $duo_user,
);
$config = $this->newConfigForUser($user)
->setFactorName(pht('Duo (%s)', $duo_user))
->setProperties($config_properties);
return $config;
}
public static function requireDuoAPIHostname($hostname) {
if (preg_match('/\.duosecurity\.com\z/', $hostname)) {
return;
}
throw new Exception(
pht(
'Duo API hostname ("%s") is invalid, hostname must be '.
'"*.duosecurity.com".',
$hostname));
}
}

View File

@@ -8,6 +8,10 @@ final class PhabricatorSMSAuthFactor
} }
public function getFactorName() { public function getFactorName() {
return pht('Text Message (SMS)');
}
public function getFactorShortName() {
return pht('SMS'); return pht('SMS');
} }
@@ -67,15 +71,24 @@ final class PhabricatorSMSAuthFactor
return $messages; return $messages;
} }
public function canCreateNewConfiguration(PhabricatorUser $user) { public function canCreateNewConfiguration(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
if (!$this->loadUserContactNumber($user)) { if (!$this->loadUserContactNumber($user)) {
return false; return false;
} }
if ($this->loadConfigurationsForProvider($provider, $user)) {
return false;
}
return true; return true;
} }
public function getConfigurationCreateDescription(PhabricatorUser $user) { public function getConfigurationCreateDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
$messages = array(); $messages = array();
@@ -91,6 +104,16 @@ final class PhabricatorSMSAuthFactor
)); ));
} }
if ($this->loadConfigurationsForProvider($provider, $user)) {
$messages[] = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
pht(
'You already have SMS authentication attached to your account.'),
));
}
return $messages; return $messages;
} }
@@ -117,7 +140,7 @@ final class PhabricatorSMSAuthFactor
AphrontRequest $request, AphrontRequest $request,
PhabricatorUser $user) { PhabricatorUser $user) {
$token = $this->loadMFASyncToken($request, $form, $user); $token = $this->loadMFASyncToken($provider, $request, $form, $user);
$code = $request->getStr('sms.code'); $code = $request->getStr('sms.code');
$e_code = true; $e_code = true;
@@ -172,35 +195,29 @@ final class PhabricatorSMSAuthFactor
} }
if (!$this->loadUserContactNumber($viewer)) { if (!$this->loadUserContactNumber($viewer)) {
$result = $this->newResult() return $this->newResult()
->setIsError(true) ->setIsError(true)
->setErrorMessage( ->setErrorMessage(
pht( pht(
'Your account has no primary contact number.')); 'Your account has no primary contact number.'));
$this->throwResult($result);
} }
if (!$this->isSMSMailerConfigured()) { if (!$this->isSMSMailerConfigured()) {
$result = $this->newResult() return $this->newResult()
->setIsError(true) ->setIsError(true)
->setErrorMessage( ->setErrorMessage(
pht( pht(
'No outbound mailer which can deliver SMS messages is '. 'No outbound mailer which can deliver SMS messages is '.
'configured.')); 'configured.'));
$this->throwResult($result);
} }
if (!$this->hasCSRF($config)) { if (!$this->hasCSRF($config)) {
$result = $this->newResult() return $this->newResult()
->setIsContinue(true) ->setIsContinue(true)
->setErrorMessage( ->setErrorMessage(
pht( pht(
'A text message with an authorization code will be sent to your '. 'A text message with an authorization code will be sent to your '.
'primary contact number.')); 'primary contact number.'));
$this->throwResult($result);
} }
// Otherwise, issue a new challenge. // Otherwise, issue a new challenge.
@@ -347,7 +364,10 @@ final class PhabricatorSMSAuthFactor
return head($contact_numbers); return head($contact_numbers);
} }
protected function newMFASyncTokenProperties(PhabricatorUser $user) { protected function newMFASyncTokenProperties(
PhabricatorAuthFactorProvider $providerr,
PhabricatorUser $user) {
$sms_code = $this->newSMSChallengeCode(); $sms_code = $this->newSMSChallengeCode();
$envelope = new PhutilOpaqueEnvelope($sms_code); $envelope = new PhutilOpaqueEnvelope($sms_code);

View File

@@ -10,6 +10,10 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
return pht('Mobile Phone App (TOTP)'); return pht('Mobile Phone App (TOTP)');
} }
public function getFactorShortName() {
return pht('TOTP');
}
public function getFactorCreateHelp() { public function getFactorCreateHelp() {
return pht( return pht(
'Allow users to attach a mobile authenticator application (like '. 'Allow users to attach a mobile authenticator application (like '.
@@ -38,6 +42,15 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
'to add a new TOTP code, continue to the next step.'); 'to add a new TOTP code, continue to the next step.');
} }
public function getConfigurationListDetails(
PhabricatorAuthFactorConfig $config,
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $viewer) {
$bits = strlen($config->getFactorSecret()) * 8;
return pht('%d-Bit Secret', $bits);
}
public function processAddFactorForm( public function processAddFactorForm(
PhabricatorAuthFactorProvider $provider, PhabricatorAuthFactorProvider $provider,
AphrontFormView $form, AphrontFormView $form,
@@ -45,6 +58,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
PhabricatorUser $user) { PhabricatorUser $user) {
$sync_token = $this->loadMFASyncToken( $sync_token = $this->loadMFASyncToken(
$provider,
$request, $request,
$form, $form,
$user); $user);
@@ -427,7 +441,9 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
return null; return null;
} }
protected function newMFASyncTokenProperties(PhabricatorUser $user) { protected function newMFASyncTokenProperties(
PhabricatorAuthFactorProvider $providerr,
PhabricatorUser $user) {
return array( return array(
'secret' => self::generateNewTOTPKey(), 'secret' => self::generateNewTOTPKey(),
); );

View File

@@ -91,11 +91,7 @@ final class PhabricatorDuoFuture
$http_method = $this->getHTTPMethod(); $http_method = $this->getHTTPMethod();
ksort($data); ksort($data);
$data_parts = array(); $data_parts = phutil_build_http_querystring($data);
foreach ($data as $key => $value) {
$data_parts[] = rawurlencode($key).'='.rawurlencode($value);
}
$data_parts = implode('&', $data_parts);
$corpus = array( $corpus = array(
$date, $date,

View File

@@ -7,6 +7,7 @@ final class PhabricatorAuthFactorConfigQuery
private $phids; private $phids;
private $userPHIDs; private $userPHIDs;
private $factorProviderPHIDs; private $factorProviderPHIDs;
private $factorProviderStatuses;
public function withIDs(array $ids) { public function withIDs(array $ids) {
$this->ids = $ids; $this->ids = $ids;
@@ -28,6 +29,11 @@ final class PhabricatorAuthFactorConfigQuery
return $this; return $this;
} }
public function withFactorProviderStatuses(array $statuses) {
$this->factorProviderStatuses = $statuses;
return $this;
}
public function newResultObject() { public function newResultObject() {
return new PhabricatorAuthFactorConfig(); return new PhabricatorAuthFactorConfig();
} }
@@ -42,34 +48,54 @@ final class PhabricatorAuthFactorConfigQuery
if ($this->ids !== null) { if ($this->ids !== null) {
$where[] = qsprintf( $where[] = qsprintf(
$conn, $conn,
'id IN (%Ld)', 'config.id IN (%Ld)',
$this->ids); $this->ids);
} }
if ($this->phids !== null) { if ($this->phids !== null) {
$where[] = qsprintf( $where[] = qsprintf(
$conn, $conn,
'phid IN (%Ls)', 'config.phid IN (%Ls)',
$this->phids); $this->phids);
} }
if ($this->userPHIDs !== null) { if ($this->userPHIDs !== null) {
$where[] = qsprintf( $where[] = qsprintf(
$conn, $conn,
'userPHID IN (%Ls)', 'config.userPHID IN (%Ls)',
$this->userPHIDs); $this->userPHIDs);
} }
if ($this->factorProviderPHIDs !== null) { if ($this->factorProviderPHIDs !== null) {
$where[] = qsprintf( $where[] = qsprintf(
$conn, $conn,
'factorProviderPHID IN (%Ls)', 'config.factorProviderPHID IN (%Ls)',
$this->factorProviderPHIDs); $this->factorProviderPHIDs);
} }
if ($this->factorProviderStatuses !== null) {
$where[] = qsprintf(
$conn,
'provider.status IN (%Ls)',
$this->factorProviderStatuses);
}
return $where; return $where;
} }
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->factorProviderStatuses !== null) {
$joins[] = qsprintf(
$conn,
'JOIN %R provider ON config.factorProviderPHID = provider.phid',
new PhabricatorAuthFactorProvider());
}
return $joins;
}
protected function willFilterPage(array $configs) { protected function willFilterPage(array $configs) {
$provider_phids = mpull($configs, 'getFactorProviderPHID'); $provider_phids = mpull($configs, 'getFactorProviderPHID');
@@ -94,6 +120,10 @@ final class PhabricatorAuthFactorConfigQuery
return $configs; return $configs;
} }
protected function getPrimaryTableAlias() {
return 'config';
}
public function getQueryApplicationClass() { public function getQueryApplicationClass() {
return 'PhabricatorAuthApplication'; return 'PhabricatorAuthApplication';
} }

View File

@@ -80,6 +80,12 @@ final class PhabricatorAuthFactorConfig
return $this; return $this;
} }
public function newSortVector() {
return id(new PhutilSortVector())
->addInt($this->getFactorProvider()->newStatus()->getOrder())
->addInt($this->getID());
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */ /* -( PhabricatorPolicyInterface )----------------------------------------- */

View File

@@ -5,7 +5,8 @@ final class PhabricatorAuthFactorProvider
implements implements
PhabricatorApplicationTransactionInterface, PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface, PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface { PhabricatorExtendedPolicyInterface,
PhabricatorEditEngineMFAInterface {
protected $providerFactorKey; protected $providerFactorKey;
protected $name; protected $name;
@@ -14,15 +15,11 @@ final class PhabricatorAuthFactorProvider
private $factor = self::ATTACHABLE; private $factor = self::ATTACHABLE;
const STATUS_ACTIVE = 'active';
const STATUS_DEPRECATED = 'deprecated';
const STATUS_DISABLED = 'disabled';
public static function initializeNewProvider(PhabricatorAuthFactor $factor) { public static function initializeNewProvider(PhabricatorAuthFactor $factor) {
return id(new self()) return id(new self())
->setProviderFactorKey($factor->getFactorKey()) ->setProviderFactorKey($factor->getFactorKey())
->attachFactor($factor) ->attachFactor($factor)
->setStatus(self::STATUS_ACTIVE); ->setStatus(PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE);
} }
protected function getConfiguration() { protected function getConfiguration() {
@@ -60,6 +57,14 @@ final class PhabricatorAuthFactorProvider
return $this; return $this;
} }
public function getEnrollMessage() {
return $this->getAuthFactorProviderProperty('enroll-message');
}
public function setEnrollMessage($message) {
return $this->setAuthFactorProviderProperty('enroll-message', $message);
}
public function attachFactor(PhabricatorAuthFactor $factor) { public function attachFactor(PhabricatorAuthFactor $factor) {
$this->factor = $factor; $this->factor = $factor;
return $this; return $this;
@@ -117,6 +122,29 @@ final class PhabricatorAuthFactorProvider
return $this->getFactor()->getEnrollButtonText($this, $user); return $this->getFactor()->getEnrollButtonText($this, $user);
} }
public function newStatus() {
$status_key = $this->getStatus();
return PhabricatorAuthFactorProviderStatus::newForStatus($status_key);
}
public function canCreateNewConfiguration(PhabricatorUser $user) {
return $this->getFactor()->canCreateNewConfiguration($this, $user);
}
public function getConfigurationCreateDescription(PhabricatorUser $user) {
return $this->getFactor()->getConfigurationCreateDescription($this, $user);
}
public function getConfigurationListDetails(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer) {
return $this->getFactor()->getConfigurationListDetails(
$config,
$this,
$viewer);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */ /* -( PhabricatorApplicationTransactionInterface )------------------------- */
@@ -169,4 +197,11 @@ final class PhabricatorAuthFactorProvider
} }
/* -( PhabricatorEditEngineMFAInterface )---------------------------------- */
public function newEditEngineMFAEngine() {
return new PhabricatorAuthFactorProviderMFAEngine();
}
} }

View File

@@ -0,0 +1,69 @@
<?php
final class PhabricatorAuthFactorProviderDuoCredentialTransaction
extends PhabricatorAuthFactorProviderTransactionType {
const TRANSACTIONTYPE = 'duo.credential';
public function generateOldValue($object) {
$key = PhabricatorDuoAuthFactor::PROP_CREDENTIAL;
return $object->getAuthFactorProviderProperty($key);
}
public function applyInternalEffects($object, $value) {
$key = PhabricatorDuoAuthFactor::PROP_CREDENTIAL;
$object->setAuthFactorProviderProperty($key, $value);
}
public function getTitle() {
return pht(
'%s changed the credential for this provider from %s to %s.',
$this->renderAuthor(),
$this->renderOldHandle(),
$this->renderNewHandle());
}
public function validateTransactions($object, array $xactions) {
$actor = $this->getActor();
$errors = array();
if (!$this->isDuoProvider($object)) {
return $errors;
}
$old_value = $this->generateOldValue($object);
if ($this->isEmptyTextTransaction($old_value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('Duo providers must have an API credential.'));
}
foreach ($xactions as $xaction) {
$new_value = $xaction->getNewValue();
if (!strlen($new_value)) {
continue;
}
if ($new_value === $old_value) {
continue;
}
$credential = id(new PassphraseCredentialQuery())
->setViewer($actor)
->withIsDestroyed(false)
->withPHIDs(array($new_value))
->executeOne();
if (!$credential) {
$errors[] = $this->newInvalidError(
pht(
'Credential ("%s") is not valid.',
$new_value),
$xaction);
continue;
}
}
return $errors;
}
}

View File

@@ -0,0 +1,26 @@
<?php
final class PhabricatorAuthFactorProviderDuoEnrollTransaction
extends PhabricatorAuthFactorProviderTransactionType {
const TRANSACTIONTYPE = 'duo.enroll';
public function generateOldValue($object) {
$key = PhabricatorDuoAuthFactor::PROP_ENROLL;
return $object->getAuthFactorProviderProperty($key);
}
public function applyInternalEffects($object, $value) {
$key = PhabricatorDuoAuthFactor::PROP_ENROLL;
$object->setAuthFactorProviderProperty($key, $value);
}
public function getTitle() {
return pht(
'%s changed the enrollment policy for this provider from %s to %s.',
$this->renderAuthor(),
$this->renderOldValue(),
$this->renderNewValue());
}
}

View File

@@ -0,0 +1,63 @@
<?php
final class PhabricatorAuthFactorProviderDuoHostnameTransaction
extends PhabricatorAuthFactorProviderTransactionType {
const TRANSACTIONTYPE = 'duo.hostname';
public function generateOldValue($object) {
$key = PhabricatorDuoAuthFactor::PROP_HOSTNAME;
return $object->getAuthFactorProviderProperty($key);
}
public function applyInternalEffects($object, $value) {
$key = PhabricatorDuoAuthFactor::PROP_HOSTNAME;
$object->setAuthFactorProviderProperty($key, $value);
}
public function getTitle() {
return pht(
'%s changed the hostname for this provider from %s to %s.',
$this->renderAuthor(),
$this->renderOldValue(),
$this->renderNewValue());
}
public function validateTransactions($object, array $xactions) {
$errors = array();
if (!$this->isDuoProvider($object)) {
return $errors;
}
$old_value = $this->generateOldValue($object);
if ($this->isEmptyTextTransaction($old_value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('Duo providers must have an API hostname.'));
}
foreach ($xactions as $xaction) {
$new_value = $xaction->getNewValue();
if (!strlen($new_value)) {
continue;
}
if ($new_value === $old_value) {
continue;
}
try {
PhabricatorDuoAuthFactor::requireDuoAPIHostname($new_value);
} catch (Exception $ex) {
$errors[] = $this->newInvalidError(
$ex->getMessage(),
$xaction);
continue;
}
}
return $errors;
}
}

View File

@@ -0,0 +1,26 @@
<?php
final class PhabricatorAuthFactorProviderDuoUsernamesTransaction
extends PhabricatorAuthFactorProviderTransactionType {
const TRANSACTIONTYPE = 'duo.usernames';
public function generateOldValue($object) {
$key = PhabricatorDuoAuthFactor::PROP_USERNAMES;
return $object->getAuthFactorProviderProperty($key);
}
public function applyInternalEffects($object, $value) {
$key = PhabricatorDuoAuthFactor::PROP_USERNAMES;
$object->setAuthFactorProviderProperty($key, $value);
}
public function getTitle() {
return pht(
'%s changed the username policy for this provider from %s to %s.',
$this->renderAuthor(),
$this->renderOldValue(),
$this->renderNewValue());
}
}

View File

@@ -0,0 +1,39 @@
<?php
final class PhabricatorAuthFactorProviderEnrollMessageTransaction
extends PhabricatorAuthFactorProviderTransactionType {
const TRANSACTIONTYPE = 'enroll-message';
public function generateOldValue($object) {
return $object->getEnrollMessage();
}
public function applyInternalEffects($object, $value) {
$object->setEnrollMessage($value);
}
public function getTitle() {
return pht(
'%s updated the enroll message.',
$this->renderAuthor());
}
public function hasChangeDetailView() {
return true;
}
public function getMailDiffSectionHeader() {
return pht('CHANGES TO ENROLL MESSAGE');
}
public function newChangeDetailView() {
$viewer = $this->getViewer();
return id(new PhabricatorApplicationTransactionTextDiffDetailView())
->setViewer($viewer)
->setOldText($this->getOldValue())
->setNewText($this->getNewValue());
}
}

View File

@@ -0,0 +1,103 @@
<?php
final class PhabricatorAuthFactorProviderStatusTransaction
extends PhabricatorAuthFactorProviderTransactionType {
const TRANSACTIONTYPE = 'status';
public function generateOldValue($object) {
return $object->getStatus();
}
public function applyInternalEffects($object, $value) {
$object->setStatus($value);
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
$old_display = PhabricatorAuthFactorProviderStatus::newForStatus($old)
->getName();
$new_display = PhabricatorAuthFactorProviderStatus::newForStatus($new)
->getName();
return pht(
'%s changed the status of this provider from %s to %s.',
$this->renderAuthor(),
$this->renderValue($old_display),
$this->renderValue($new_display));
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$actor = $this->getActor();
$map = PhabricatorAuthFactorProviderStatus::getMap();
foreach ($xactions as $xaction) {
$new_value = $xaction->getNewValue();
if (!isset($map[$new_value])) {
$errors[] = $this->newInvalidError(
pht(
'Status "%s" is invalid. Valid statuses are: %s.',
$new_value,
implode(', ', array_keys($map))),
$xaction);
continue;
}
$require_key = 'security.require-multi-factor-auth';
$require_mfa = PhabricatorEnv::getEnvConfig($require_key);
if ($require_mfa) {
$status_active = PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE;
if ($new_value !== $status_active) {
$active_providers = id(new PhabricatorAuthFactorProviderQuery())
->setViewer($actor)
->withStatuses(
array(
$status_active,
))
->execute();
$active_providers = mpull($active_providers, null, 'getID');
unset($active_providers[$object->getID()]);
if (!$active_providers) {
$errors[] = $this->newInvalidError(
pht(
'You can not deprecate or disable the last active MFA '.
'provider while "%s" is enabled, because new users would '.
'be unable to enroll in MFA. Disable the MFA requirement '.
'in Config, or create or enable another MFA provider first.',
$require_key));
continue;
}
}
}
}
return $errors;
}
public function didCommitTransaction($object, $value) {
$status = PhabricatorAuthFactorProviderStatus::newForStatus($value);
// If a provider has undergone a status change, reset the MFA enrollment
// cache for all users. This may immediately force a lot of users to redo
// MFA enrollment.
// We could be more surgical about this: we only really need to affect
// users who had a factor under the provider, and only really need to
// do anything if a provider was disabled. This is just a little simpler.
$table = new PhabricatorUser();
$conn = $table->establishConnection('w');
queryfx(
$conn,
'UPDATE %R SET isEnrolledInMultiFactor = 0',
$table);
}
}

View File

@@ -1,4 +1,12 @@
<?php <?php
abstract class PhabricatorAuthFactorProviderTransactionType abstract class PhabricatorAuthFactorProviderTransactionType
extends PhabricatorModularTransactionType {} extends PhabricatorModularTransactionType {
final protected function isDuoProvider(
PhabricatorAuthFactorProvider $provider) {
$duo_key = id(new PhabricatorDuoAuthFactor())->getFactorKey();
return ($provider->getProviderFactorKey() === $duo_key);
}
}

View File

@@ -12,7 +12,7 @@ final class PhabricatorAccessControlTestCase extends PhabricatorTestCase {
$root = dirname(phutil_get_library_root('phabricator')); $root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/support/startup/PhabricatorStartup.php'; require_once $root.'/support/startup/PhabricatorStartup.php';
$application_configuration = new AphrontDefaultApplicationConfiguration(); $application_configuration = new AphrontApplicationConfiguration();
$host = 'meow.example.com'; $host = 'meow.example.com';

View File

@@ -11,14 +11,13 @@ final class PhabricatorExtensionsSetupCheck extends PhabricatorSetupCheck {
} }
protected function executeChecks() { protected function executeChecks() {
// TODO: Make 'mbstring' and 'iconv' soft requirements. // TODO: Make 'mbstring' a soft requirement.
$required = array( $required = array(
'hash', 'hash',
'json', 'json',
'openssl', 'openssl',
'mbstring', 'mbstring',
'iconv',
'ctype', 'ctype',
// There is a tiny chance we might not need this, but a significant // There is a tiny chance we might not need this, but a significant

View File

@@ -416,6 +416,10 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck {
'metamta.pholio.subject-prefix' => $prefix_reason, 'metamta.pholio.subject-prefix' => $prefix_reason,
'metamta.phriction.subject-prefix' => $prefix_reason, 'metamta.phriction.subject-prefix' => $prefix_reason,
'aphront.default-application-configuration-class' => pht(
'This ancient extension point has been replaced with other '.
'mechanisms, including "AphrontSite".'),
); );
return $ancient_config; return $ancient_config;

View File

@@ -63,6 +63,8 @@ final class PhabricatorConfigVersionController
$version_from_file); $version_from_file);
} }
$version_property_list->addProperty('php', phpversion());
$binaries = PhutilBinaryAnalyzer::getAllBinaries(); $binaries = PhutilBinaryAnalyzer::getAllBinaries();
foreach ($binaries as $binary) { foreach ($binaries as $binary) {
if (!$binary->isBinaryAvailable()) { if (!$binary->isBinaryAvailable()) {

View File

@@ -36,14 +36,6 @@ final class PhabricatorExtendingPhabricatorConfigOptions
'occur. Specify a list of classes which extend '. 'occur. Specify a list of classes which extend '.
'PhabricatorEventListener here.')) 'PhabricatorEventListener here.'))
->addExample('MyEventListener', pht('Valid Setting')), ->addExample('MyEventListener', pht('Valid Setting')),
$this->newOption(
'aphront.default-application-configuration-class',
'class',
'AphrontDefaultApplicationConfiguration')
->setLocked(true)
->setBaseClass('AphrontApplicationConfiguration')
// TODO: This could probably use some better documentation.
->setDescription(pht('Application configuration class.')),
); );
} }

View File

@@ -235,6 +235,32 @@ final class DifferentialRevisionEditEngine
->setConduitTypeDescription(pht('List of tasks.')) ->setConduitTypeDescription(pht('List of tasks.'))
->setValue(array()); ->setValue(array());
$fields[] = id(new PhabricatorHandlesEditField())
->setKey('parents')
->setUseEdgeTransactions(true)
->setIsFormField(false)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST)
->setDescription(pht('Parent revisions of this revision.'))
->setConduitDescription(pht('Change associated parent revisions.'))
->setConduitTypeDescription(pht('List of revisions.'))
->setValue(array());
$fields[] = id(new PhabricatorHandlesEditField())
->setKey('children')
->setUseEdgeTransactions(true)
->setIsFormField(false)
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue(
'edge:type',
DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST)
->setDescription(pht('Child revisions of this revision.'))
->setConduitDescription(pht('Change associated child revisions.'))
->setConduitTypeDescription(pht('List of revisions.'))
->setValue(array());
$actions = DifferentialRevisionActionTransaction::loadAllActions(); $actions = DifferentialRevisionActionTransaction::loadAllActions();
$actions = msortv($actions, 'getRevisionActionOrderVector'); $actions = msortv($actions, 'getRevisionActionOrderVector');

View File

@@ -177,6 +177,7 @@ final class DifferentialDiffExtractionEngine extends Phobject {
'repository' => $repository, 'repository' => $repository,
)); ));
try {
$response = DiffusionQuery::callConduitWithDiffusionRequest( $response = DiffusionQuery::callConduitWithDiffusionRequest(
$viewer, $viewer,
$drequest, $drequest,
@@ -185,6 +186,12 @@ final class DifferentialDiffExtractionEngine extends Phobject {
'commit' => $identifier, 'commit' => $identifier,
'path' => $path, 'path' => $path,
)); ));
} catch (Exception $ex) {
// TODO: See PHI1044. This call may fail if the diff deleted the
// file. If the call fails, just detect a change for now. This should
// generally be made cleaner in the future.
return true;
}
$new_file_phid = $response['filePHID']; $new_file_phid = $response['filePHID'];
if (!$new_file_phid) { if (!$new_file_phid) {

View File

@@ -528,7 +528,7 @@ final class DiffusionServeController extends DiffusionController {
unset($query_data[$key]); unset($query_data[$key]);
} }
} }
$query_string = http_build_query($query_data, '', '&'); $query_string = phutil_build_http_querystring($query_data);
// We're about to wipe out PATH with the rest of the environment, so // We're about to wipe out PATH with the rest of the environment, so
// resolve the binary first. // resolve the binary first.

View File

@@ -188,7 +188,7 @@ final class DiffusionRepositoryClusterEngine extends Phobject {
if ($this_version) { if ($this_version) {
$this_version = (int)$this_version->getRepositoryVersion(); $this_version = (int)$this_version->getRepositoryVersion();
} else { } else {
$this_version = -1; $this_version = null;
} }
if ($versions) { if ($versions) {
@@ -197,7 +197,7 @@ final class DiffusionRepositoryClusterEngine extends Phobject {
// leader, we want to fetch from a leader and then update our version. // leader, we want to fetch from a leader and then update our version.
$max_version = (int)max(mpull($versions, 'getRepositoryVersion')); $max_version = (int)max(mpull($versions, 'getRepositoryVersion'));
if ($max_version > $this_version) { if (($this_version === null) || ($max_version > $this_version)) {
if ($repository->isHosted()) { if ($repository->isHosted()) {
$fetchable = array(); $fetchable = array();
foreach ($versions as $version) { foreach ($versions as $version) {
@@ -206,6 +206,7 @@ final class DiffusionRepositoryClusterEngine extends Phobject {
} }
} }
$this->synchronizeWorkingCopyFromDevices( $this->synchronizeWorkingCopyFromDevices(
$fetchable, $fetchable,
$this_version, $this_version,
@@ -445,10 +446,10 @@ final class DiffusionRepositoryClusterEngine extends Phobject {
if ($this_version) { if ($this_version) {
$this_version = (int)$this_version->getRepositoryVersion(); $this_version = (int)$this_version->getRepositoryVersion();
} else { } else {
$this_version = -1; $this_version = null;
} }
if ($new_version > $this_version) { if (($this_version === null) || ($new_version > $this_version)) {
PhabricatorRepositoryWorkingCopyVersion::updateVersion( PhabricatorRepositoryWorkingCopyVersion::updateVersion(
$repository_phid, $repository_phid,
$device_phid, $device_phid,

View File

@@ -222,8 +222,10 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
pht('No repository "%s" exists!', $identifier)); pht('No repository "%s" exists!', $identifier));
} }
$is_cluster = $this->getIsClusterRequest();
$protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH; $protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH;
if (!$repository->canServeProtocol($protocol, false)) { if (!$repository->canServeProtocol($protocol, false, $is_cluster)) {
throw new Exception( throw new Exception(
pht( pht(
'This repository ("%s") is not available over SSH.', 'This repository ("%s") is not available over SSH.',

View File

@@ -21,12 +21,8 @@ final class PassphraseCredentialRevealController
return new Aphront404Response(); return new Aphront404Response();
} }
$view_uri = '/K'.$credential->getID(); $view_uri = $credential->getURI();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$view_uri);
$is_locked = $credential->getIsLocked(); $is_locked = $credential->getIsLocked();
if ($is_locked) { if ($is_locked) {
@@ -39,7 +35,7 @@ final class PassphraseCredentialRevealController
->addCancelButton($view_uri); ->addCancelButton($view_uri);
} }
if ($request->isFormPost()) { if ($request->isFormOrHisecPost()) {
$secret = $credential->getSecret(); $secret = $credential->getSecret();
if (!$secret) { if (!$secret) {
$body = pht('This credential has no associated secret.'); $body = pht('This credential has no associated secret.');
@@ -76,6 +72,7 @@ final class PassphraseCredentialRevealController
$editor = id(new PassphraseCredentialTransactionEditor()) $editor = id(new PassphraseCredentialTransactionEditor())
->setActor($viewer) ->setActor($viewer)
->setCancelURI($view_uri)
->setContinueOnNoEffect(true) ->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request) ->setContentSourceFromRequest($request)
->applyTransactions($credential, $xactions); ->applyTransactions($credential, $xactions);

View File

@@ -52,6 +52,10 @@ final class PassphraseCredential extends PassphraseDAO
return 'K'.$this->getID(); return 'K'.$this->getID();
} }
public function getURI() {
return '/'.$this->getMonogram();
}
protected function getConfiguration() { protected function getConfiguration() {
return array( return array(
self::CONFIG_AUX_PHID => true, self::CONFIG_AUX_PHID => true,

View File

@@ -30,4 +30,10 @@ final class PassphraseCredentialLookedAtTransaction
return 'blue'; return 'blue';
} }
public function shouldTryMFA(
$object,
PhabricatorApplicationTransaction $xaction) {
return true;
}
} }

View File

@@ -17,14 +17,9 @@ final class PhabricatorPeopleRenameController
$done_uri = $this->getApplicationURI("manage/{$id}/"); $done_uri = $this->getApplicationURI("manage/{$id}/");
id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$done_uri);
$validation_exception = null; $validation_exception = null;
$username = $user->getUsername(); $username = $user->getUsername();
if ($request->isFormPost()) { if ($request->isFormOrHisecPost()) {
$username = $request->getStr('username'); $username = $request->getStr('username');
$xactions = array(); $xactions = array();
@@ -36,6 +31,7 @@ final class PhabricatorPeopleRenameController
$editor = id(new PhabricatorUserTransactionEditor()) $editor = id(new PhabricatorUserTransactionEditor())
->setActor($viewer) ->setActor($viewer)
->setContentSourceFromRequest($request) ->setContentSourceFromRequest($request)
->setCancelURI($done_uri)
->setContinueOnMissingFields(true); ->setContinueOnMissingFields(true);
try { try {

View File

@@ -978,9 +978,15 @@ final class PhabricatorUser
* @task factors * @task factors
*/ */
public function updateMultiFactorEnrollment() { public function updateMultiFactorEnrollment() {
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( $factors = id(new PhabricatorAuthFactorConfigQuery())
'userPHID = %s', ->setViewer($this)
$this->getPHID()); ->withUserPHIDs(array($this->getPHID()))
->withFactorProviderStatuses(
array(
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
))
->execute();
$enrolled = count($factors) ? 1 : 0; $enrolled = count($factors) ? 1 : 0;
if ($enrolled !== $this->isEnrolledInMultiFactor) { if ($enrolled !== $this->isEnrolledInMultiFactor) {

View File

@@ -89,4 +89,11 @@ final class PhabricatorUserUsernameTransaction
return null; return null;
} }
public function shouldTryMFA(
$object,
PhabricatorApplicationTransaction $xaction) {
return true;
}
} }

View File

@@ -83,6 +83,34 @@ EOTEXT
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType'; $custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
$subtype_type = 'projects.subtypes';
$subtype_default_key = PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT;
$subtype_example = array(
array(
'key' => $subtype_default_key,
'name' => pht('Project'),
),
array(
'key' => 'team',
'name' => pht('Team'),
),
);
$subtype_example = id(new PhutilJSON())->encodeAsList($subtype_example);
$subtype_default = array(
array(
'key' => $subtype_default_key,
'name' => pht('Project'),
),
);
$subtype_description = $this->deformat(pht(<<<EOTEXT
Allows you to define project subtypes. For a more detailed description of
subtype configuration, see @{config:maniphest.subtypes}.
EOTEXT
));
return array( return array(
$this->newOption('projects.custom-field-definitions', 'wild', array()) $this->newOption('projects.custom-field-definitions', 'wild', array())
->setSummary(pht('Custom Projects fields.')) ->setSummary(pht('Custom Projects fields.'))
@@ -102,6 +130,11 @@ EOTEXT
$this->newOption('projects.colors', $colors_type, $default_colors) $this->newOption('projects.colors', $colors_type, $default_colors)
->setSummary(pht('Adjust project colors.')) ->setSummary(pht('Adjust project colors.'))
->setDescription($colors_description), ->setDescription($colors_description),
$this->newOption('projects.subtypes', $subtype_type, $subtype_default)
->setSummary(pht('Define project subtypes.'))
->setDescription($subtype_description)
->addExample($subtype_example, pht('Simple Subtypes')),
); );
} }

View File

@@ -0,0 +1,14 @@
<?php
final class PhabricatorProjectSubtypesConfigType
extends PhabricatorJSONConfigType {
const TYPEKEY = 'projects.subtypes';
public function validateStoredValue(
PhabricatorConfigOption $option,
$value) {
PhabricatorEditEngineSubtype::validateConfiguration($value);
}
}

View File

@@ -51,6 +51,12 @@ final class PhabricatorProjectProfileController
$watch_action = $this->renderWatchAction($project); $watch_action = $this->renderWatchAction($project);
$header->addActionLink($watch_action); $header->addActionLink($watch_action);
$subtype = $project->newSubtypeObject();
if ($subtype && $subtype->hasTagView()) {
$subtype_tag = $subtype->newTagView();
$header->addTag($subtype_tag);
}
$milestone_list = $this->buildMilestoneList($project); $milestone_list = $this->buildMilestoneList($project);
$subproject_list = $this->buildSubprojectList($project); $subproject_list = $this->buildSubprojectList($project);

View File

@@ -249,6 +249,17 @@ final class PhabricatorProjectTransactionEditor
->rematerialize($new_parent); ->rematerialize($new_parent);
} }
// See PHI1046. Milestones are always in the Space of their parent project.
// Synchronize the database values to match the application values.
$conn = $object->establishConnection('w');
queryfx(
$conn,
'UPDATE %R SET spacePHID = %ns
WHERE parentProjectPHID = %s AND milestoneNumber IS NOT NULL',
$object,
$object->getSpacePHID(),
$object->getPHID());
return parent::applyFinalEffects($object, $xactions); return parent::applyFinalEffects($object, $xactions);
} }

View File

@@ -24,6 +24,7 @@ final class PhabricatorProjectQuery
private $maxDepth; private $maxDepth;
private $minMilestoneNumber; private $minMilestoneNumber;
private $maxMilestoneNumber; private $maxMilestoneNumber;
private $subtypes;
private $status = 'status-any'; private $status = 'status-any';
const STATUS_ANY = 'status-any'; const STATUS_ANY = 'status-any';
@@ -131,6 +132,11 @@ final class PhabricatorProjectQuery
return $this; return $this;
} }
public function withSubtypes(array $subtypes) {
$this->subtypes = $subtypes;
return $this;
}
public function needMembers($need_members) { public function needMembers($need_members) {
$this->needMembers = $need_members; $this->needMembers = $need_members;
return $this; return $this;
@@ -618,6 +624,13 @@ final class PhabricatorProjectQuery
$this->maxMilestoneNumber); $this->maxMilestoneNumber);
} }
if ($this->subtypes !== null) {
$where[] = qsprintf(
$conn,
'subtype IN (%Ls)',
$this->subtypes);
}
return $where; return $where;
} }

View File

@@ -19,6 +19,9 @@ final class PhabricatorProjectSearchEngine
} }
protected function buildCustomSearchFields() { protected function buildCustomSearchFields() {
$subtype_map = id(new PhabricatorProject())->newEditEngineSubtypeMap();
$hide_subtypes = ($subtype_map->getCount() == 1);
return array( return array(
id(new PhabricatorSearchTextField()) id(new PhabricatorSearchTextField())
->setLabel(pht('Name')) ->setLabel(pht('Name'))
@@ -62,6 +65,14 @@ final class PhabricatorProjectSearchEngine
pht( pht(
'Pass true to find only milestones, or false to omit '. 'Pass true to find only milestones, or false to omit '.
'milestones.')), 'milestones.')),
id(new PhabricatorSearchDatasourceField())
->setLabel(pht('Subtypes'))
->setKey('subtypes')
->setAliases(array('subtype'))
->setDescription(
pht('Search for projects with given subtypes.'))
->setDatasource(new PhabricatorProjectSubtypeDatasource())
->setIsHidden($hide_subtypes),
id(new PhabricatorSearchCheckboxesField()) id(new PhabricatorSearchCheckboxesField())
->setLabel(pht('Icons')) ->setLabel(pht('Icons'))
->setKey('icons') ->setKey('icons')
@@ -134,6 +145,10 @@ final class PhabricatorProjectSearchEngine
$query->withAncestorProjectPHIDs($map['ancestorPHIDs']); $query->withAncestorProjectPHIDs($map['ancestorPHIDs']);
} }
if ($map['subtypes']) {
$query->withSubtypes($map['subtypes']);
}
return $query; return $query;
} }

View File

@@ -12,7 +12,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO
PhabricatorFerretInterface, PhabricatorFerretInterface,
PhabricatorConduitResultInterface, PhabricatorConduitResultInterface,
PhabricatorColumnProxyInterface, PhabricatorColumnProxyInterface,
PhabricatorSpacesInterface { PhabricatorSpacesInterface,
PhabricatorEditEngineSubtypeInterface {
protected $name; protected $name;
protected $status = PhabricatorProjectStatus::STATUS_ACTIVE; protected $status = PhabricatorProjectStatus::STATUS_ACTIVE;
@@ -40,6 +41,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
protected $properties = array(); protected $properties = array();
protected $spacePHID; protected $spacePHID;
protected $subtype;
private $memberPHIDs = self::ATTACHABLE; private $memberPHIDs = self::ATTACHABLE;
private $watcherPHIDs = self::ATTACHABLE; private $watcherPHIDs = self::ATTACHABLE;
@@ -102,6 +104,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
->setHasWorkboard(0) ->setHasWorkboard(0)
->setHasMilestones(0) ->setHasMilestones(0)
->setHasSubprojects(0) ->setHasSubprojects(0)
->setSubtype(PhabricatorEditEngineSubtype::SUBTYPE_DEFAULT)
->attachParentProject(null); ->attachParentProject(null);
} }
@@ -237,6 +240,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
'projectPath' => 'hashpath64', 'projectPath' => 'hashpath64',
'projectDepth' => 'uint32', 'projectDepth' => 'uint32',
'projectPathKey' => 'bytes4', 'projectPathKey' => 'bytes4',
'subtype' => 'text64',
), ),
self::CONFIG_KEY_SCHEMA => array( self::CONFIG_KEY_SCHEMA => array(
'key_icon' => array( 'key_icon' => array(
@@ -765,6 +769,10 @@ final class PhabricatorProject extends PhabricatorProjectDAO
->setKey('slug') ->setKey('slug')
->setType('string') ->setType('string')
->setDescription(pht('Primary slug/hashtag.')), ->setDescription(pht('Primary slug/hashtag.')),
id(new PhabricatorConduitSearchFieldSpecification())
->setKey('subtype')
->setType('string')
->setDescription(pht('Subtype of the project.')),
id(new PhabricatorConduitSearchFieldSpecification()) id(new PhabricatorConduitSearchFieldSpecification())
->setKey('milestone') ->setKey('milestone')
->setType('int?') ->setType('int?')
@@ -814,6 +822,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
return array( return array(
'name' => $this->getName(), 'name' => $this->getName(),
'slug' => $this->getPrimarySlug(), 'slug' => $this->getPrimarySlug(),
'subtype' => $this->getSubtype(),
'milestone' => $milestone, 'milestone' => $milestone,
'depth' => (int)$this->getProjectDepth(), 'depth' => (int)$this->getProjectDepth(),
'parent' => $parent_ref, 'parent' => $parent_ref,
@@ -873,4 +882,26 @@ final class PhabricatorProject extends PhabricatorProjectDAO
} }
/* -( PhabricatorEditEngineSubtypeInterface )------------------------------ */
public function getEditEngineSubtype() {
return $this->getSubtype();
}
public function setEditEngineSubtype($value) {
return $this->setSubtype($value);
}
public function newEditEngineSubtypeMap() {
$config = PhabricatorEnv::getEnvConfig('projects.subtypes');
return PhabricatorEditEngineSubtype::newSubtypeMap($config);
}
public function newSubtypeObject() {
$subtype_key = $this->getEditEngineSubtype();
$subtype_map = $this->newEditEngineSubtypeMap();
return $subtype_map->getSubtype($subtype_key);
}
} }

View File

@@ -0,0 +1,45 @@
<?php
final class PhabricatorProjectSubtypeDatasource
extends PhabricatorTypeaheadDatasource {
public function getBrowseTitle() {
return pht('Browse Subtypes');
}
public function getPlaceholderText() {
return pht('Type a project subtype name...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorProjectApplication';
}
public function loadResults() {
$results = $this->buildResults();
return $this->filterResultsAgainstTokens($results);
}
protected function renderSpecialTokens(array $values) {
return $this->renderTokensFromResults($this->buildResults(), $values);
}
private function buildResults() {
$results = array();
$subtype_map = id(new PhabricatorProject())->newEditEngineSubtypeMap();
foreach ($subtype_map->getSubtypes() as $key => $subtype) {
$result = id(new PhabricatorTypeaheadResult())
->setIcon($subtype->getIcon())
->setColor($subtype->getColor())
->setPHID($key)
->setName($subtype->getName());
$results[$key] = $result;
}
return $results;
}
}

View File

@@ -87,6 +87,13 @@ final class PhabricatorProjectListView extends AphrontView {
} }
} }
$subtype = $project->newSubtypeObject();
if ($subtype && $subtype->hasTagView()) {
$subtype_tag = $subtype->newTagView()
->setSlimShady(true);
$item->addAttribute($subtype_tag);
}
$list->addItem($item); $list->addItem($item);
} }

View File

@@ -73,6 +73,9 @@ final class PhabricatorRepositoryPullLocalDaemon
$futures = array(); $futures = array();
$queue = array(); $queue = array();
$sync_wait = phutil_units('2 minutes in seconds');
$last_sync = array();
while (!$this->shouldExit()) { while (!$this->shouldExit()) {
PhabricatorCaches::destroyRequestCache(); PhabricatorCaches::destroyRequestCache();
$device = AlmanacKeys::getLiveDevice(); $device = AlmanacKeys::getLiveDevice();
@@ -96,6 +99,37 @@ final class PhabricatorRepositoryPullLocalDaemon
$retry_after[$message->getRepositoryID()] = time(); $retry_after[$message->getRepositoryID()] = time();
} }
if ($device) {
$unsynchronized = $this->loadUnsynchronizedRepositories($device);
$now = PhabricatorTime::getNow();
foreach ($unsynchronized as $repository) {
$id = $repository->getID();
$this->log(
pht(
'Cluster repository ("%s") is out of sync on this node ("%s").',
$repository->getDisplayName(),
$device->getName()));
// Don't let out-of-sync conditions trigger updates too frequently,
// since we don't want to get trapped in a death spiral if sync is
// failing.
$sync_at = idx($last_sync, $id, 0);
$wait_duration = ($now - $sync_at);
if ($wait_duration < $sync_wait) {
$this->log(
pht(
'Skipping forced out-of-sync update because the last update '.
'was too recent (%s seconds ago).',
$wait_duration));
continue;
}
$last_sync[$id] = $now;
$retry_after[$id] = $now;
}
}
// If any repositories were deleted, remove them from the retry timer map // If any repositories were deleted, remove them from the retry timer map
// so we don't end up with a retry timer that never gets updated and // so we don't end up with a retry timer that never gets updated and
// causes us to sleep for the minimum amount of time. // causes us to sleep for the minimum amount of time.
@@ -521,4 +555,41 @@ final class PhabricatorRepositoryPullLocalDaemon
return false; return false;
} }
private function loadUnsynchronizedRepositories(AlmanacDevice $device) {
$viewer = $this->getViewer();
$table = new PhabricatorRepositoryWorkingCopyVersion();
$conn = $table->establishConnection('r');
$our_versions = queryfx_all(
$conn,
'SELECT repositoryPHID, repositoryVersion FROM %R WHERE devicePHID = %s',
$table,
$device->getPHID());
$our_versions = ipull($our_versions, 'repositoryVersion', 'repositoryPHID');
$max_versions = queryfx_all(
$conn,
'SELECT repositoryPHID, MAX(repositoryVersion) maxVersion FROM %R
GROUP BY repositoryPHID',
$table);
$max_versions = ipull($max_versions, 'maxVersion', 'repositoryPHID');
$unsynchronized_phids = array();
foreach ($max_versions as $repository_phid => $max_version) {
$our_version = idx($our_versions, $repository_phid);
if (($our_version === null) || ($our_version < $max_version)) {
$unsynchronized_phids[] = $repository_phid;
}
}
if (!$unsynchronized_phids) {
return array();
}
return id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->withPHIDs($unsynchronized_phids)
->execute();
}
} }

View File

@@ -7,7 +7,7 @@ abstract class PhabricatorRepositoryManagementWorkflow
$identifiers = $args->getArg($param); $identifiers = $args->getArg($param);
if (!$identifiers) { if (!$identifiers) {
return null; return array();
} }
$query = id(new PhabricatorRepositoryQuery()) $query = id(new PhabricatorRepositoryQuery())

View File

@@ -1506,10 +1506,19 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
return $this->setDetail('hosting-enabled', $enabled); return $this->setDetail('hosting-enabled', $enabled);
} }
public function canServeProtocol($protocol, $write) { public function canServeProtocol(
$protocol,
$write,
$is_intracluster = false) {
// See T13192. If a repository is inactive, don't serve it to users. We
// still synchronize it within the cluster and serve it to other repository
// nodes.
if (!$is_intracluster) {
if (!$this->isTracked()) { if (!$this->isTracked()) {
return false; return false;
} }
}
$clone_uris = $this->getCloneURIs(); $clone_uris = $this->getCloneURIs();
foreach ($clone_uris as $uri) { foreach ($clone_uris as $uri) {

View File

@@ -53,8 +53,8 @@ final class PhabricatorMultiFactorSettingsPanel
$factors = id(new PhabricatorAuthFactorConfigQuery()) $factors = id(new PhabricatorAuthFactorConfigQuery())
->setViewer($viewer) ->setViewer($viewer)
->withUserPHIDs(array($user->getPHID())) ->withUserPHIDs(array($user->getPHID()))
->setOrderVector(array('-id'))
->execute(); ->execute();
$factors = msort($factors, 'newSortVector');
$rows = array(); $rows = array();
$rowc = array(); $rowc = array();
@@ -69,7 +69,18 @@ final class PhabricatorMultiFactorSettingsPanel
$rowc[] = null; $rowc[] = null;
} }
$status = $provider->newStatus();
$status_icon = $status->getFactorIcon();
$status_color = $status->getFactorColor();
$icon = id(new PHUIIconView())
->setIcon("{$status_icon} {$status_color}")
->setTooltip(pht('Provider: %s', $status->getName()));
$details = $provider->getConfigurationListDetails($factor, $viewer);
$rows[] = array( $rows[] = array(
$icon,
javelin_tag( javelin_tag(
'a', 'a',
array( array(
@@ -77,7 +88,9 @@ final class PhabricatorMultiFactorSettingsPanel
'sigil' => 'workflow', 'sigil' => 'workflow',
), ),
$factor->getFactorName()), $factor->getFactorName()),
$provider->getFactor()->getFactorShortName(),
$provider->getDisplayName(), $provider->getDisplayName(),
$details,
phabricator_datetime($factor->getDateCreated(), $viewer), phabricator_datetime($factor->getDateCreated(), $viewer),
javelin_tag( javelin_tag(
'a', 'a',
@@ -95,15 +108,21 @@ final class PhabricatorMultiFactorSettingsPanel
pht("You haven't added any authentication factors to your account yet.")); pht("You haven't added any authentication factors to your account yet."));
$table->setHeaders( $table->setHeaders(
array( array(
null,
pht('Name'), pht('Name'),
pht('Type'), pht('Type'),
pht('Provider'),
pht('Details'),
pht('Created'), pht('Created'),
'', null,
)); ));
$table->setColumnClasses( $table->setColumnClasses(
array( array(
null,
'wide pri', 'wide pri',
'', null,
null,
null,
'right', 'right',
'action', 'action',
)); ));
@@ -111,6 +130,9 @@ final class PhabricatorMultiFactorSettingsPanel
$table->setDeviceVisibility( $table->setDeviceVisibility(
array( array(
true, true,
true,
false,
false,
false, false,
false, false,
true, true,
@@ -129,12 +151,15 @@ final class PhabricatorMultiFactorSettingsPanel
$add_color = PHUIButtonView::GREY; $add_color = PHUIButtonView::GREY;
} }
$can_add = (bool)$this->loadActiveMFAProviders();
$buttons[] = id(new PHUIButtonView()) $buttons[] = id(new PHUIButtonView())
->setTag('a') ->setTag('a')
->setIcon('fa-plus') ->setIcon('fa-plus')
->setText(pht('Add Auth Factor')) ->setText(pht('Add Auth Factor'))
->setHref($this->getPanelURI('?new=true')) ->setHref($this->getPanelURI('?new=true'))
->setWorkflow(true) ->setWorkflow(true)
->setDisabled(!$can_add)
->setColor($add_color); ->setColor($add_color);
$buttons[] = id(new PHUIButtonView()) $buttons[] = id(new PHUIButtonView())
@@ -155,21 +180,18 @@ final class PhabricatorMultiFactorSettingsPanel
// Check that we have providers before we send the user through the MFA // Check that we have providers before we send the user through the MFA
// gate, so you don't authenticate and then immediately get roadblocked. // gate, so you don't authenticate and then immediately get roadblocked.
$providers = id(new PhabricatorAuthFactorProviderQuery()) $providers = $this->loadActiveMFAProviders();
->setViewer($viewer)
->withStatuses(array(PhabricatorAuthFactorProvider::STATUS_ACTIVE))
->execute();
if (!$providers) { if (!$providers) {
return $this->newDialog() return $this->newDialog()
->setTitle(pht('No MFA Providers')) ->setTitle(pht('No MFA Providers'))
->appendParagraph( ->appendParagraph(
pht( pht(
'There are no active MFA providers. At least one active provider '. 'This install does not have any active MFA providers configured. '.
'must be available to add new MFA factors.')) 'At least one provider must be configured and active before you '.
'can add new MFA factors.'))
->addCancelButton($cancel_uri); ->addCancelButton($cancel_uri);
} }
$providers = mpull($providers, null, 'getPHID');
$proivders = msortv($providers, 'newSortVector');
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer, $viewer,
@@ -184,8 +206,7 @@ final class PhabricatorMultiFactorSettingsPanel
// Only let the user continue creating a factor for a given provider if // Only let the user continue creating a factor for a given provider if
// they actually pass the provider's checks. // they actually pass the provider's checks.
$selected_factor = $selected_provider->getFactor(); if (!$selected_provider->canCreateNewConfiguration($viewer)) {
if (!$selected_factor->canCreateNewConfiguration($viewer)) {
$selected_provider = null; $selected_provider = null;
} }
} }
@@ -200,8 +221,7 @@ final class PhabricatorMultiFactorSettingsPanel
$provider_uri = id(new PhutilURI($this->getPanelURI())) $provider_uri = id(new PhutilURI($this->getPanelURI()))
->setQueryParam('providerPHID', $provider_phid); ->setQueryParam('providerPHID', $provider_phid);
$factor = $provider->getFactor(); $is_enabled = $provider->canCreateNewConfiguration($viewer);
$is_enabled = $factor->canCreateNewConfiguration($viewer);
$item = id(new PHUIObjectItemView()) $item = id(new PHUIObjectItemView())
->setHeader($provider->getDisplayName()) ->setHeader($provider->getDisplayName())
@@ -216,7 +236,7 @@ final class PhabricatorMultiFactorSettingsPanel
$item->setDisabled(true); $item->setDisabled(true);
} }
$create_description = $factor->getConfigurationCreateDescription( $create_description = $provider->getConfigurationCreateDescription(
$viewer); $viewer);
if ($create_description) { if ($create_description) {
$item->appendChild($create_description); $item->appendChild($create_description);
@@ -236,13 +256,16 @@ final class PhabricatorMultiFactorSettingsPanel
// sometimes requires us to push a challenge to them as a side effect (for // sometimes requires us to push a challenge to them as a side effect (for
// example, with SMS). // example, with SMS).
if (!$request->isFormPost() || !$request->getBool('mfa.start')) { if (!$request->isFormPost() || !$request->getBool('mfa.start')) {
$description = $selected_provider->getEnrollDescription($viewer); $enroll = $selected_provider->getEnrollMessage();
if (!strlen($enroll)) {
$enroll = $selected_provider->getEnrollDescription($viewer);
}
return $this->newDialog() return $this->newDialog()
->addHiddenInput('providerPHID', $selected_provider->getPHID()) ->addHiddenInput('providerPHID', $selected_provider->getPHID())
->addHiddenInput('mfa.start', 1) ->addHiddenInput('mfa.start', 1)
->setTitle(pht('Add Authentication Factor')) ->setTitle(pht('Add Authentication Factor'))
->appendChild(new PHUIRemarkupView($viewer, $description)) ->appendChild(new PHUIRemarkupView($viewer, $enroll))
->addCancelButton($cancel_uri) ->addCancelButton($cancel_uri)
->addSubmitButton($selected_provider->getEnrollButtonText($viewer)); ->addSubmitButton($selected_provider->getEnrollButtonText($viewer));
} }
@@ -424,5 +447,22 @@ final class PhabricatorMultiFactorSettingsPanel
->setDialog($dialog); ->setDialog($dialog);
} }
private function loadActiveMFAProviders() {
$viewer = $this->getViewer();
$providers = id(new PhabricatorAuthFactorProviderQuery())
->setViewer($viewer)
->withStatuses(
array(
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
))
->execute();
$providers = mpull($providers, null, 'getPHID');
$providers = msortv($providers, 'newSortVector');
return $providers;
}
} }

View File

@@ -68,6 +68,7 @@ final class TransactionSearchConduitAPIMethod
$object); $object);
$xaction_query $xaction_query
->needHandles(false)
->withObjectPHIDs(array($object->getPHID())) ->withObjectPHIDs(array($object->getPHID()))
->setViewer($viewer); ->setViewer($viewer);

View File

@@ -1279,14 +1279,41 @@ abstract class PhabricatorEditEngine
$fields = $this->willBuildEditForm($object, $fields); $fields = $this->willBuildEditForm($object, $fields);
$request_path = $request->getRequestURI()
->setQueryParams(array());
$form = id(new AphrontFormView()) $form = id(new AphrontFormView())
->setUser($viewer) ->setUser($viewer)
->setAction($request_path)
->addHiddenInput('editEngine', 'true'); ->addHiddenInput('editEngine', 'true');
foreach ($this->contextParameters as $param) { foreach ($this->contextParameters as $param) {
$form->addHiddenInput($param, $request->getStr($param)); $form->addHiddenInput($param, $request->getStr($param));
} }
$requires_mfa = false;
if ($object instanceof PhabricatorEditEngineMFAInterface) {
$mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
->setViewer($viewer);
$requires_mfa = $mfa_engine->shouldRequireMFA();
}
if ($requires_mfa) {
$message = pht(
'You will be required to provide multi-factor credentials to make '.
'changes.');
$form->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors(array($message)));
// TODO: This should also set workflow on the form, so the user doesn't
// lose any form data if they "Cancel". However, Maniphest currently
// overrides "newEditResponse()" if the request is Ajax and returns a
// bag of view data. This can reasonably be cleaned up when workboards
// get their next iteration.
}
foreach ($fields as $field) { foreach ($fields as $field) {
if (!$field->getIsFormField()) { if (!$field->getIsFormField()) {
continue; continue;
@@ -1565,11 +1592,19 @@ abstract class PhabricatorEditEngine
$comment_uri = $this->getEditURI($object, 'comment/'); $comment_uri = $this->getEditURI($object, 'comment/');
$requires_mfa = false;
if ($object instanceof PhabricatorEditEngineMFAInterface) {
$mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
->setViewer($viewer);
$requires_mfa = $mfa_engine->shouldRequireMFA();
}
$view = id(new PhabricatorApplicationTransactionCommentView()) $view = id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer) ->setUser($viewer)
->setObjectPHID($object_phid) ->setObjectPHID($object_phid)
->setHeaderText($header_text) ->setHeaderText($header_text)
->setAction($comment_uri) ->setAction($comment_uri)
->setRequiresMFA($requires_mfa)
->setSubmitButtonName($button_text); ->setSubmitButtonName($button_text);
$draft = PhabricatorVersionedDraft::loadDraft( $draft = PhabricatorVersionedDraft::loadDraft(

View File

@@ -0,0 +1,43 @@
<?php
final class PhabricatorCredentialEditField
extends PhabricatorEditField {
private $credentialType;
private $credentials;
public function setCredentialType($credential_type) {
$this->credentialType = $credential_type;
return $this;
}
public function getCredentialType() {
return $this->credentialType;
}
public function setCredentials(array $credentials) {
$this->credentials = $credentials;
return $this;
}
public function getCredentials() {
return $this->credentials;
}
protected function newControl() {
$control = id(new PassphraseCredentialControl())
->setCredentialType($this->getCredentialType())
->setOptions($this->getCredentials());
return $control;
}
protected function newHTTPParameterType() {
return new AphrontPHIDHTTPParameterType();
}
protected function newConduitParameterType() {
return new ConduitPHIDParameterType();
}
}

View File

@@ -28,7 +28,6 @@ final class PhabricatorSpaceEditField
return new ConduitPHIDParameterType(); return new ConduitPHIDParameterType();
} }
public function shouldReadValueFromRequest() { public function shouldReadValueFromRequest() {
return $this->getPolicyField()->shouldReadValueFromRequest(); return $this->getPolicyField()->shouldReadValueFromRequest();
} }

View File

@@ -88,6 +88,7 @@ abstract class PhabricatorApplicationTransactionEditor
private $hasRequiredMFA = false; private $hasRequiredMFA = false;
private $request; private $request;
private $cancelURI; private $cancelURI;
private $extensions;
const STORAGE_ENCODING_BINARY = 'binary'; const STORAGE_ENCODING_BINARY = 'binary';
@@ -1013,6 +1014,7 @@ abstract class PhabricatorApplicationTransactionEditor
} }
$errors[] = $this->validateAllTransactions($object, $xactions); $errors[] = $this->validateAllTransactions($object, $xactions);
$errors[] = $this->validateTransactionsWithExtensions($object, $xactions);
$errors = array_mergev($errors); $errors = array_mergev($errors);
$continue_on_missing = $this->getContinueOnMissingFields(); $continue_on_missing = $this->getContinueOnMissingFields();
@@ -2670,9 +2672,15 @@ abstract class PhabricatorApplicationTransactionEditor
$transaction_type) { $transaction_type) {
$errors = array(); $errors = array();
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( $factors = id(new PhabricatorAuthFactorConfigQuery())
'userPHID = %s', ->setViewer($this->getActor())
$this->getActingAsPHID()); ->withUserPHIDs(array($this->getActingAsPHID()))
->withFactorProviderStatuses(
array(
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
))
->execute();
foreach ($xactions as $xaction) { foreach ($xactions as $xaction) {
if (!$factors) { if (!$factors) {
@@ -3289,7 +3297,7 @@ abstract class PhabricatorApplicationTransactionEditor
// move the other transactions down so they provide context above the // move the other transactions down so they provide context above the
// actual comment. // actual comment.
$comment = $xaction->getBodyForMail(); $comment = $this->getBodyForTextMail($xaction);
if ($comment !== null) { if ($comment !== null) {
$is_comment = true; $is_comment = true;
$comments[] = array( $comments[] = array(
@@ -3302,12 +3310,12 @@ abstract class PhabricatorApplicationTransactionEditor
} }
if (!$is_comment || !$seen_comment) { if (!$is_comment || !$seen_comment) {
$header = $xaction->getTitleForTextMail(); $header = $this->getTitleForTextMail($xaction);
if ($header !== null) { if ($header !== null) {
$headers[] = $header; $headers[] = $header;
} }
$header_html = $xaction->getTitleForHTMLMail(); $header_html = $this->getTitleForHTMLMail($xaction);
if ($header_html !== null) { if ($header_html !== null) {
$headers_html[] = $header_html; $headers_html[] = $header_html;
} }
@@ -3387,12 +3395,12 @@ abstract class PhabricatorApplicationTransactionEditor
// If this is not the first comment in the mail, add the header showing // If this is not the first comment in the mail, add the header showing
// who wrote the comment immediately above the comment. // who wrote the comment immediately above the comment.
if (!$is_initial) { if (!$is_initial) {
$header = $xaction->getTitleForTextMail(); $header = $this->getTitleForTextMail($xaction);
if ($header !== null) { if ($header !== null) {
$body->addRawPlaintextSection($header); $body->addRawPlaintextSection($header);
} }
$header_html = $xaction->getTitleForHTMLMail(); $header_html = $this->getTitleForHTMLMail($xaction);
if ($header_html !== null) { if ($header_html !== null) {
$body->addRawHTMLSection($header_html); $body->addRawHTMLSection($header_html);
} }
@@ -4851,6 +4859,13 @@ abstract class PhabricatorApplicationTransactionEditor
} }
private function requireMFA(PhabricatorLiskDAO $object, array $xactions) { private function requireMFA(PhabricatorLiskDAO $object, array $xactions) {
$actor = $this->getActor();
// Let omnipotent editors skip MFA. This is mostly aimed at scripts.
if ($actor->isOmnipotent()) {
return;
}
$editor_class = get_class($this); $editor_class = get_class($this);
$object_phid = $object->getPHID(); $object_phid = $object->getPHID();
@@ -4865,8 +4880,6 @@ abstract class PhabricatorApplicationTransactionEditor
$editor_class); $editor_class);
} }
$actor = $this->getActor();
$request = $this->getRequest(); $request = $this->getRequest();
if ($request === null) { if ($request === null) {
$source_type = $this->getContentSource()->getSourceTypeConstant(); $source_type = $this->getContentSource()->getSourceTypeConstant();
@@ -4909,20 +4922,47 @@ abstract class PhabricatorApplicationTransactionEditor
PhabricatorLiskDAO $object, PhabricatorLiskDAO $object,
array $xactions) { array $xactions) {
$is_mfa = ($object instanceof PhabricatorEditEngineMFAInterface); $has_engine = ($object instanceof PhabricatorEditEngineMFAInterface);
if (!$is_mfa) { if ($has_engine) {
return $xactions;
}
$engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object) $engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
->setViewer($this->getActor()); ->setViewer($this->getActor());
$require_mfa = $engine->shouldRequireMFA(); $require_mfa = $engine->shouldRequireMFA();
$try_mfa = $engine->shouldTryMFA();
} else {
$require_mfa = false;
$try_mfa = false;
}
// If the user is mentioning an MFA object on another object or creating
// a relationship like "parent" or "child" to this object, we always
// allow the edit to move forward without requiring MFA.
if ($this->getIsInverseEdgeEditor()) {
return $xactions;
}
if (!$require_mfa) { if (!$require_mfa) {
$try_mfa = $engine->shouldTryMFA(); // If the object hasn't already opted into MFA, see if any of the
// transactions want it.
if (!$try_mfa) {
foreach ($xactions as $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
if ($xtype->shouldTryMFA($object, $xaction)) {
$try_mfa = true;
break;
}
}
}
}
if ($try_mfa) { if ($try_mfa) {
$this->setShouldRequireMFA(true); $this->setShouldRequireMFA(true);
} }
return $xactions; return $xactions;
} }
@@ -4940,13 +4980,6 @@ abstract class PhabricatorApplicationTransactionEditor
return $xactions; return $xactions;
} }
// If the user is mentioning an MFA object on another object or creating
// a relationship like "parent" or "child" to this object, we allow the
// edit to move forward without requiring MFA.
if ($this->getIsInverseEdgeEditor()) {
return $xactions;
}
$template = $object->getApplicationTransactionTemplate(); $template = $object->getApplicationTransactionTemplate();
$mfa_xaction = id(clone $template) $mfa_xaction = id(clone $template)
@@ -4958,4 +4991,112 @@ abstract class PhabricatorApplicationTransactionEditor
return $xactions; return $xactions;
} }
private function getTitleForTextMail(
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
$comment = $xtype->getTitleForTextMail();
if ($comment !== false) {
return $comment;
}
}
return $xaction->getTitleForTextMail();
}
private function getTitleForHTMLMail(
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
$comment = $xtype->getTitleForHTMLMail();
if ($comment !== false) {
return $comment;
}
}
return $xaction->getTitleForHTMLMail();
}
private function getBodyForTextMail(
PhabricatorApplicationTransaction $xaction) {
$type = $xaction->getTransactionType();
$xtype = $this->getModularTransactionType($type);
if ($xtype) {
$xtype = clone $xtype;
$xtype->setStorage($xaction);
$comment = $xtype->getBodyForTextMail();
if ($comment !== false) {
return $comment;
}
}
return $xaction->getBodyForMail();
}
/* -( Extensions )--------------------------------------------------------- */
private function validateTransactionsWithExtensions(
PhabricatorLiskDAO $object,
array $xactions) {
$errors = array();
$extensions = $this->getEditorExtensions();
foreach ($extensions as $extension) {
$extension_errors = $extension
->setObject($object)
->validateTransactions($object, $xactions);
assert_instances_of(
$extension_errors,
'PhabricatorApplicationTransactionValidationError');
$errors[] = $extension_errors;
}
return array_mergev($errors);
}
private function getEditorExtensions() {
if ($this->extensions === null) {
$this->extensions = $this->newEditorExtensions();
}
return $this->extensions;
}
private function newEditorExtensions() {
$extensions = PhabricatorEditorExtension::getAllExtensions();
$actor = $this->getActor();
$object = $this->object;
foreach ($extensions as $key => $extension) {
$extension = id(clone $extension)
->setViewer($actor)
->setEditor($this)
->setObject($object);
if (!$extension->supportsObject($this, $object)) {
unset($extensions[$key]);
continue;
}
$extensions[$key] = $extension;
}
return $extensions;
}
} }

View File

@@ -0,0 +1,83 @@
<?php
abstract class PhabricatorEditorExtension
extends Phobject {
private $viewer;
private $editor;
private $object;
final public function getExtensionKey() {
return $this->getPhobjectClassConstant('EXTENSIONKEY');
}
final public function setEditor(
PhabricatorApplicationTransactionEditor $editor) {
$this->editor = $editor;
return $this;
}
final public function getEditor() {
return $this->editor;
}
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function setObject(
PhabricatorApplicationTransactionInterface $object) {
$this->object = $object;
return $this;
}
final public static function getAllExtensions() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getExtensionKey')
->execute();
}
abstract public function getExtensionName();
public function supportsObject(
PhabricatorApplicationTransactionEditor $editor,
PhabricatorApplicationTransactionInterface $object) {
return true;
}
public function validateTransactions($object, array $xactions) {
return array();
}
final protected function newTransactionError(
PhabricatorApplicationTransaction $xaction,
$title,
$message) {
return new PhabricatorApplicationTransactionValidationError(
$xaction->getTransactionType(),
$title,
$message,
$xaction);
}
final protected function newRequiredTransasctionError(
PhabricatorApplicationTransaction $xaction,
$message) {
return $this->newError($xaction, pht('Required'), $message)
->setIsMissingFieldError(true);
}
final protected function newInvalidTransactionError(
PhabricatorApplicationTransaction $xaction,
$message) {
return $this->newTransactionError($xaction, pht('Invalid'), $message);
}
}

View File

@@ -0,0 +1,40 @@
<?php
final class PhabricatorEditorExtensionModule
extends PhabricatorConfigModule {
public function getModuleKey() {
return 'editor';
}
public function getModuleName() {
return pht('Engine: Editor');
}
public function renderModuleStatus(AphrontRequest $request) {
$viewer = $request->getViewer();
$extensions = PhabricatorEditorExtension::getAllExtensions();
$rows = array();
foreach ($extensions as $extension) {
$rows[] = array(
get_class($extension),
$extension->getExtensionName(),
);
}
return id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Class'),
pht('Name'),
))
->setColumnClasses(
array(
null,
'wide pri',
));
}
}

View File

@@ -740,6 +740,7 @@ abstract class PhabricatorApplicationTransaction
switch ($this->getTransactionType()) { switch ($this->getTransactionType()) {
case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_TOKEN:
case PhabricatorTransactions::TYPE_MFA:
return true; return true;
case PhabricatorTransactions::TYPE_EDGE: case PhabricatorTransactions::TYPE_EDGE:
$edge_type = $this->getMetadataValue('edge:type'); $edge_type = $this->getMetadataValue('edge:type');

View File

@@ -425,4 +425,74 @@ abstract class PhabricatorModularTransactionType
return PhabricatorPolicyCapability::CAN_EDIT; return PhabricatorPolicyCapability::CAN_EDIT;
} }
public function shouldTryMFA(
$object,
PhabricatorApplicationTransaction $xaction) {
return false;
}
// NOTE: See T12921. These APIs are somewhat aspirational. For now, all of
// these use "TARGET_TEXT" (even the HTML methods!) and the body methods
// actually return Remarkup, not text or HTML.
final public function getTitleForTextMail() {
return $this->getTitleForMailWithRenderingTarget(
PhabricatorApplicationTransaction::TARGET_TEXT);
}
final public function getTitleForHTMLMail() {
return $this->getTitleForMailWithRenderingTarget(
PhabricatorApplicationTransaction::TARGET_TEXT);
}
final public function getBodyForTextMail() {
return $this->getBodyForMailWithRenderingTarget(
PhabricatorApplicationTransaction::TARGET_TEXT);
}
final public function getBodyForHTMLMail() {
return $this->getBodyForMailWithRenderingTarget(
PhabricatorApplicationTransaction::TARGET_TEXT);
}
private function getTitleForMailWithRenderingTarget($target) {
$storage = $this->getStorage();
$old_target = $storage->getRenderingTarget();
try {
$storage->setRenderingTarget($target);
$result = $this->getTitleForMail();
} catch (Exception $ex) {
$storage->setRenderingTarget($old_target);
throw $ex;
}
$storage->setRenderingTarget($old_target);
return $result;
}
private function getBodyForMailWithRenderingTarget($target) {
$storage = $this->getStorage();
$old_target = $storage->getRenderingTarget();
try {
$storage->setRenderingTarget($target);
$result = $this->getBodyForMail();
} catch (Exception $ex) {
$storage->setRenderingTarget($old_target);
throw $ex;
}
$storage->setRenderingTarget($old_target);
return $result;
}
protected function getTitleForMail() {
return false;
}
protected function getBodyForMail() {
return false;
}
} }

View File

@@ -1,9 +1,7 @@
<?php <?php
/** final class PhabricatorApplicationTransactionCommentView
* @concrete-extensible extends AphrontView {
*/
class PhabricatorApplicationTransactionCommentView extends AphrontView {
private $submitButtonName; private $submitButtonName;
private $action; private $action;
@@ -24,6 +22,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
private $infoView; private $infoView;
private $editEngineLock; private $editEngineLock;
private $noBorder; private $noBorder;
private $requiresMFA;
private $currentVersion; private $currentVersion;
private $versionedDraft; private $versionedDraft;
@@ -160,6 +159,15 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
return $this->editEngineLock; return $this->editEngineLock;
} }
public function setRequiresMFA($requires_mfa) {
$this->requiresMFA = $requires_mfa;
return $this;
}
public function getRequiresMFA() {
return $this->requiresMFA;
}
public function setTransactionTimeline( public function setTransactionTimeline(
PhabricatorApplicationTransactionView $timeline) { PhabricatorApplicationTransactionView $timeline) {
@@ -187,8 +195,8 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
)); ));
} }
$user = $this->getUser(); $viewer = $this->getViewer();
if (!$user->isLoggedIn()) { if (!$viewer->isLoggedIn()) {
$uri = id(new PhutilURI('/login/')) $uri = id(new PhutilURI('/login/'))
->setQueryParam('next', (string)$this->getRequestURI()); ->setQueryParam('next', (string)$this->getRequestURI());
return id(new PHUIObjectBoxView()) return id(new PHUIObjectBoxView())
@@ -203,6 +211,25 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
pht('Log In to Comment'))); pht('Log In to Comment')));
} }
if ($this->getRequiresMFA()) {
if (!$viewer->getIsEnrolledInMultiFactor()) {
$viewer->updateMultiFactorEnrollment();
if (!$viewer->getIsEnrolledInMultiFactor()) {
$messages = array();
$messages[] = pht(
'You must provide multi-factor credentials to comment or make '.
'changes, but you do not have multi-factor authentication '.
'configured on your account.');
$messages[] = pht(
'To continue, configure multi-factor authentication in Settings.');
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors($messages);
}
}
}
$data = array(); $data = array();
$comment = $this->renderCommentPanel(); $comment = $this->renderCommentPanel();
@@ -226,7 +253,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
} }
require_celerity_resource('phui-comment-form-css'); require_celerity_resource('phui-comment-form-css');
$image_uri = $user->getProfileImageURI(); $image_uri = $viewer->getProfileImageURI();
$image = phutil_tag( $image = phutil_tag(
'div', 'div',
array( array(
@@ -388,6 +415,17 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView {
$form->appendChild($info_view); $form->appendChild($info_view);
} }
if ($this->getRequiresMFA()) {
$message = pht(
'You will be required to provide multi-factor credentials to '.
'comment or make changes.');
$form->appendChild(
id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors(array($message)));
}
$form->appendChild($invisi_bar); $form->appendChild($invisi_bar);
$form->addClass('phui-comment-has-actions'); $form->addClass('phui-comment-has-actions');

View File

@@ -1,33 +1,45 @@
@title Configuring Outbound Email @title Configuring Outbound Email
@group config @group config
Instructions for configuring Phabricator to send mail. Instructions for configuring Phabricator to send email and other types of
messages, like text messages.
Overview Overview
======== ========
Phabricator can send outbound email through several different mail services, Phabricator sends outbound messages through "mailers". Most mailers send
email and most messages are email messages, but mailers may also send other
types of messages (like text messages).
Phabricator can send outbound messages through multiple different mailers,
including a local mailer or various third-party services. Options include: including a local mailer or various third-party services. Options include:
| Send Mail With | Setup | Cost | Inbound | Notes | | Send Mail With | Setup | Cost | Inbound | Media | Notes |
|---------|-------|------|---------|-------| |----------------|-------|------|---------|-------|-------|
| Postmark | Easy | Cheap | Yes | Recommended | | Postmark | Easy | Cheap | Yes | Email | Recommended |
| Mailgun | Easy | Cheap | Yes | Recommended | | Mailgun | Easy | Cheap | Yes | Email | Recommended |
| Amazon SES | Easy | Cheap | No | Recommended | | Amazon SES | Easy | Cheap | No | Email | |
| SendGrid | Medium | Cheap | Yes | Discouraged | | SendGrid | Medium | Cheap | Yes | Email | |
| External SMTP | Medium | Varies | No | Gmail, etc. | | Twilio | Easy | Cheap | No | SMS | Recommended |
| Local SMTP | Hard | Free | No | sendmail, postfix, etc | | Amazon SNS | Easy | Cheap | No | SMS | Recommended |
| Custom | Hard | Free | No | Write a custom mailer for some other service. | | External SMTP | Medium | Varies | No | Email | Gmail, etc. |
| Drop in a Hole | Easy | Free | No | Drops mail in a deep, dark hole. | | Local SMTP | Hard | Free | No | Email | sendmail, postfix, etc |
| Custom | Hard | Free | No | All | Write a custom mailer. |
| Drop in a Hole | Easy | Free | No | All | Drops mail in a deep, dark hole. |
See below for details on how to select and configure mail delivery for each See below for details on how to select and configure mail delivery for each
mailer. mailer.
Overall, Postmark and Mailgun are much easier to set up, and using one of them For email, Postmark or Mailgun are recommended because they make it easy to
is recommended. Both will also let you set up inbound email easily. set up inbound and outbound mail and have good track records in our production
services. Other services will also generally work well, but they may be more
difficult to set up.
If you have some internal mail service you'd like to use you can also write a For SMS, Twilio or SNS are recommended. They're also your only upstream
custom mailer, but this requires digging into the code. options.
If you have some internal mail or messaging service you'd like to use you can
also write a custom mailer, but this requires digging into the code.
Phabricator sends mail in the background, so the daemons need to be running for Phabricator sends mail in the background, so the daemons need to be running for
it to be able to deliver mail. You should receive setup warnings if they are it to be able to deliver mail. You should receive setup warnings if they are
@@ -91,13 +103,14 @@ The supported keys for each mailer are:
types. Normally, you do not need to configure this. See below for a list types. Normally, you do not need to configure this. See below for a list
of media types. of media types.
The `type` field can be used to select these third-party mailers: The `type` field can be used to select these mailer services:
- `mailgun`: Use Mailgun. - `mailgun`: Use Mailgun.
- `ses`: Use Amazon SES. - `ses`: Use Amazon SES.
- `sendgrid`: Use SendGrid. - `sendgrid`: Use SendGrid.
- `postmark`: Use Postmark. - `postmark`: Use Postmark.
- `sns`: Use Amazon SNS (only for sending SMS messages). - `twilio`: Use Twilio.
- `sns`: Use Amazon SNS.
It also supports these local mailers: It also supports these local mailers:
@@ -153,6 +166,12 @@ For alternatives and more information on configuration, see
Mailer: Postmark Mailer: Postmark
================ ================
| Media | Email
|---------|
| Inbound | Yes
|---------|
Postmark is a third-party email delivery service. You can learn more at Postmark is a third-party email delivery service. You can learn more at
<https://www.postmarkapp.com/>. <https://www.postmarkapp.com/>.
@@ -183,8 +202,13 @@ documented at: <https://postmarkapp.com/support/article/800-ips-for-firewalls>
Mailer: Mailgun Mailer: Mailgun
=============== ===============
| Media | Email
|---------|
| Inbound | Yes
|---------|
Mailgun is a third-party email delivery service. You can learn more at Mailgun is a third-party email delivery service. You can learn more at
<http://www.mailgun.com>. Mailgun is easy to configure and works well. <https://www.mailgun.com>. Mailgun is easy to configure and works well.
To use this mailer, set `type` to `mailgun`, then configure these `options`: To use this mailer, set `type` to `mailgun`, then configure these `options`:
@@ -195,8 +219,13 @@ To use this mailer, set `type` to `mailgun`, then configure these `options`:
Mailer: Amazon SES Mailer: Amazon SES
================== ==================
| Media | Email
|---------|
| Inbound | No
|---------|
Amazon SES is Amazon's cloud email service. You can learn more at Amazon SES is Amazon's cloud email service. You can learn more at
<http://aws.amazon.com/ses/>. <https://aws.amazon.com/ses/>.
To use this mailer, set `type` to `ses`, then configure these `options`: To use this mailer, set `type` to `ses`, then configure these `options`:
@@ -209,21 +238,58 @@ which "From" address to use by setting `metamta.default-address` in your
config, then follow the Amazon SES verification process to verify it. You config, then follow the Amazon SES verification process to verify it. You
won't be able to send email until you do this! won't be able to send email until you do this!
Mailer: Twilio
==================
| Media | SMS
|---------|
| Inbound | No
|---------|
Twilio is a third-party notification service. You can learn more at
<https://www.twilio.com/>.
To use this mailer, set `type` to `twilio`, then configure these options:
- `account-sid`: Your Twilio Account SID.
- `auth-token`: Your Twilio Auth Token.
- `from-number`: Number to send text messages from, in E.164 format
(like `+15551237890`).
Mailer: Amazon SNS Mailer: Amazon SNS
================== ==================
| Media | SMS
|---------|
| Inbound | No
|---------|
Amazon SNS is Amazon's cloud notification service. You can learn more at Amazon SNS is Amazon's cloud notification service. You can learn more at
<http://aws.amazon.com/sns/>. Note that this mailer is only able to send <https://aws.amazon.com/sns/>. Note that this mailer is only able to send
SMS messages, not emails. SMS messages, not emails.
To use this mailer, set `type` to `sns`, then configure the options similarly To use this mailer, set `type` to `sns`, then configure these options:
to the SES configuration above.
- `access-key`: Required string. Your Amazon SNS access key.
- `secret-key`: Required string. Your Amazon SNS secret key.
- `endpoint`: Required string. Your Amazon SNS endpoint.
- `region`: Required string. Your Amazon SNS region.
You can find the correct `region` value for your endpoint in the SNS
documentation.
Mailer: SendGrid Mailer: SendGrid
================ ================
| Media | Email
|---------|
| Inbound | Yes
|---------|
SendGrid is a third-party email delivery service. You can learn more at SendGrid is a third-party email delivery service. You can learn more at
<http://sendgrid.com/>. <https://sendgrid.com/>.
You can configure SendGrid in two ways: you can send via SMTP or via the REST You can configure SendGrid in two ways: you can send via SMTP or via the REST
API. To use SMTP, configure Phabricator to use an `smtp` mailer. API. To use SMTP, configure Phabricator to use an `smtp` mailer.
@@ -240,10 +306,16 @@ including an "API User". Make sure you're configuring your "API Key".
Mailer: Sendmail Mailer: Sendmail
================ ================
| Media | Email
|---------|
| Inbound | Requires Configuration
|---------|
This requires a `sendmail` binary to be installed on the system. Most MTAs This requires a `sendmail` binary to be installed on the system. Most MTAs
(e.g., sendmail, qmail, postfix) should do this, but your machine may not have (e.g., sendmail, qmail, postfix) should install one for you, but your machine
one installed by default. For install instructions, consult the documentation may not have one installed by default. For install instructions, consult the
for your favorite MTA. documentation for your favorite MTA.
Since you'll be sending the mail yourself, you are subject to things like SPF Since you'll be sending the mail yourself, you are subject to things like SPF
rules, blackholes, and MTA configuration which are beyond the scope of this rules, blackholes, and MTA configuration which are beyond the scope of this
@@ -258,6 +330,11 @@ configure.
Mailer: SMTP Mailer: SMTP
============ ============
| Media | Email
|---------|
| Inbound | Requires Configuration
|---------|
You can use this adapter to send mail via an external SMTP server, like Gmail. You can use this adapter to send mail via an external SMTP server, like Gmail.
To use this mailer, set `type` to `smtp`, then configure these `options`: To use this mailer, set `type` to `smtp`, then configure these `options`:
@@ -273,7 +350,15 @@ To use this mailer, set `type` to `smtp`, then configure these `options`:
Disable Mail Disable Mail
============ ============
To disable mail, just don't configure any mailers. | Media | All
|---------|
| Inbound | No
|---------|
To disable mail, just don't configure any mailers. (You can safely ignore the
setup warning reminding you to set up mailers if you don't plan to configure
any.)
Testing and Debugging Outbound Email Testing and Debugging Outbound Email
@@ -288,6 +373,9 @@ particular:
Run `bin/mail help <command>` for more help on using these commands. Run `bin/mail help <command>` for more help on using these commands.
By default, `bin/mail send-test` sends email messages, but you can use
the `--type` flag to send different types of messages.
You can monitor daemons using the Daemon Console (`/daemon/`, or click You can monitor daemons using the Daemon Console (`/daemon/`, or click
**Daemon Console** from the homepage). **Daemon Console** from the homepage).

View File

@@ -9,40 +9,39 @@ Overview
Multi-factor authentication allows you to add additional credentials to your Multi-factor authentication allows you to add additional credentials to your
account to make it more secure. account to make it more secure.
This sounds complicated, but in most cases it just means that Phabricator will Once multi-factor authentication is configured on your account, you'll usually
make sure you have your mobile phone (by sending you a text message or having use your mobile phone to provide an authorization code or an extra confirmation
you enter a code from a mobile application) before allowing you to log in or when you try to log in to a new session or take certain actions (like changing
take certain "high security" actions (like changing your password). your password).
Requiring you to prove you're really you by asking for something you know (your Requiring you to prove you're really you by asking for something you know (your
password) //and// something you have (your mobile phone) makes it much harder password) //and// something you have (your mobile phone) makes it much harder
for attackers to access your account. The phone is an additional "factor" which for attackers to access your account. The phone is an additional "factor" which
protects your account from attacks. protects your account from attacks.
Requiring re-authentication before performing high security actions further
limits the damage an attacker can do even if they manage to compromise a
login session.
How Multi-Factor Authentication Works How Multi-Factor Authentication Works
===================================== =====================================
If you've configured multi-factor authentication and try to log in to your If you've configured multi-factor authentication and try to log in to your
account or take certain high security actions (like changing your password), account or take certain sensitive actions (like changing your password),
you'll be stopped and asked to enter additional credentials. you'll be stopped and asked to enter additional credentials.
Usually, this means you'll receive an SMS with a security code on your phone, or Usually, this means you'll receive an SMS with a authorization code on your
you'll open an app on your phone which will show you a security code. phone, or you'll open an app on your phone which will show you a authorization
In both cases, you'll enter the security code into Phabricator. code or ask you to confirm the action. If you're given a authorization code,
you'll enter it into Phabricator.
If you're logging in, Phabricator will log you in after you enter the code. If you're logging in, Phabricator will log you in after you enter the code.
If you're taking a high security action, Phabricator will put your account in If you're taking a sensitive action, Phabricator will sometimes put your
"high security" mode for a few minutes. In this mode, you can take high security account in "high security" mode for a few minutes. In this mode, you can take
actions like changing passwords or SSH keys freely without entering any more sensitive actions like changing passwords or SSH keys freely, without
credentials. You can explicitly leave high security once you're done performing entering any more credentials.
account management, or your account will naturally return to normal security
after a short period of time. You can explicitly leave high security once you're done performing account
management, or your account will naturally return to normal security after a
short period of time.
While your account is in high security, you'll see a notification on screen While your account is in high security, you'll see a notification on screen
with instructions for returning to normal security. with instructions for returning to normal security.
@@ -52,8 +51,8 @@ Configuring Multi-Factor Authentication
======================================= =======================================
To manage authentication factors for your account, go to To manage authentication factors for your account, go to
Settings > Multi-Factor Auth. You can use this control panel to add or remove {nav Settings > Multi-Factor Auth}. You can use this control panel to add
authentication factors from your account. or remove authentication factors from your account.
You can also rename a factor by clicking the name. This can help you identify You can also rename a factor by clicking the name. This can help you identify
factors if you have several similar factors attached to your account. factors if you have several similar factors attached to your account.
@@ -65,7 +64,7 @@ Factor: Mobile Phone App (TOTP)
=============================== ===============================
TOTP stands for "Time-based One-Time Password". This factor operates by having TOTP stands for "Time-based One-Time Password". This factor operates by having
you enter security codes from your mobile phone into Phabricator. The codes you enter authorization codes from your mobile phone into Phabricator. The codes
change every 30 seconds, so you will need to have your phone with you in order change every 30 seconds, so you will need to have your phone with you in order
to enter them. to enter them.
@@ -79,23 +78,91 @@ application, so check any in-house documentation for details. In general, any
TOTP application should work properly. TOTP application should work properly.
After you've downloaded the application onto your phone, use the Phabricator After you've downloaded the application onto your phone, use the Phabricator
settings panel to add a factor to your account. You'll be prompted to enter a settings panel to add a factor to your account. You'll be prompted to scan a
master key into your phone, and then read a security code from your phone and QR code, and then read an authorization code from your phone and type it into
type it into Phabricator. Phabricator.
Later, when you need to authenticate, you'll follow this same process: launch Later, when you need to authenticate, you'll follow this same process: launch
the application, read the security code, and type it into Phabricator. This will the application, read the authorization code, and type it into Phabricator.
prove you have your phone. This will prove you have your phone.
Don't lose your phone! You'll need it to log into Phabricator in the future. Don't lose your phone! You'll need it to log into Phabricator in the future.
Recovering from Lost Factors Factor: SMS
============================ ===========
If you've lost a factor associated with your account (for example, your phone This factor operates by texting you a short authorization code when you try to
has been lost or damaged), an administrator can strip the factor off your log in or perform a sensitive action.
account so that you can log in without it.
To use SMS, first add your phone number in {nav Settings > Contact Numbers}.
Once a primary contact number is configured on your account, you'll be able
to add an SMS factor.
To enroll in SMS, you'll be sent a confirmation code to make sure your contact
number is correct and SMS is being delivered properly. Enter it when prompted.
When you're asked to confirm your identity in the future, you'll be texted
an authorization code to enter into the prompt.
(WARNING) SMS is a very weak factor and can be compromised or intercepted. For
details, see: <https://phurl.io/u/sms>.
Factor: Duo
===========
This factor supports integration with [[ https://duo.com/ | Duo Security ]], a
third-party authentication service popular with enterprises that have a lot of
policies to enforce.
To use Duo, you'll install the Duo application on your phone. When you try
to take a sensitive action, you'll be asked to confirm it in the application.
Administration: Configuration
=============================
New Phabricator installs start without any multi-factor providers enabled.
Users won't be able to add new factors until you set up multi-factor
authentication by configuring at least one provider.
Configure new providers in {nav Auth > Multi-Factor}.
Providers may be in these states:
- **Active**: Users may add new factors. Users will be prompted to respond
to challenges from these providers when they take a sensitive action.
- **Deprecated**: Users may not add new factors, but they will still be
asked to respond to challenges from exising factors.
- **Disabled**: Users may not add new factors, and existing factors will
not be used. If MFA is required and a user only has disabled factors,
they will be forced to add a new factor.
If you want to change factor types for your organization, the process will
normally look something like this:
- Configure and test a new provider.
- Deprecate the old provider.
- Notify users that the old provider is deprecated and that they should move
to the new provider at their convenience, but before some upcoming
deadline.
- Once the deadline arrives, disable the old provider.
Administration: Requiring MFA
=============================
As an administrator, you can require all users to add MFA to their accounts by
setting the `security.require-multi-factor-auth` option in Config.
Administration: Recovering from Lost Factors
============================================
If a user has lost a factor associated with their account (for example, their
phone has been lost or damaged), an administrator with host access can strip
the factor off their account so that they can log in without it.
IMPORTANT: Before stripping factors from a user account, be absolutely certain IMPORTANT: Before stripping factors from a user account, be absolutely certain
that the user is who they claim to be! that the user is who they claim to be!
@@ -113,9 +180,10 @@ advance and require them to perform it. But no matter what you do, be certain
the user (not an attacker //pretending// to be the user) is really the one the user (not an attacker //pretending// to be the user) is really the one
making the request before stripping factors. making the request before stripping factors.
After verifying identity, administrators can strip authentication factors from After verifying identity, administrators with host access can strip
user accounts using the `bin/auth strip` command. For example, to strip all authentication factors from user accounts using the `bin/auth strip` command.
factors from the account of a user who has lost their phone, run this command: For example, to strip all factors from the account of a user who has lost
their phone, run this command:
```lang=console ```lang=console
# Strip all factors from a given user account. # Strip all factors from a given user account.
@@ -125,7 +193,7 @@ phabricator/ $ ./bin/auth strip --user <username> --all-types
You can run `bin/auth help strip` for more detail and all available flags and You can run `bin/auth help strip` for more detail and all available flags and
arguments. arguments.
This command can selectively strip types of factors. You can use This command can selectively strip factors by factor type. You can use
`bin/auth list-factors` to get a list of available factor types. `bin/auth list-factors` to get a list of available factor types.
```lang=console ```lang=console
@@ -133,8 +201,9 @@ This command can selectively strip types of factors. You can use
phabricator/ $ ./bin/auth list-factors phabricator/ $ ./bin/auth list-factors
``` ```
Once you've identified the factor types you want to strip, you can strip them Once you've identified the factor types you want to strip, you can strip
using the `--type` flag to specify one or more factor types: matching factors by using the `--type` flag to specify one or more factor
types:
```lang=console ```lang=console
# Strip all SMS and TOTP factors for a user. # Strip all SMS and TOTP factors for a user.

View File

@@ -2855,6 +2855,13 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
} }
} }
// See T13240. If this query raises policy exceptions, don't filter objects
// in the MySQL layer. We want them to reach the application layer so we
// can reject them and raise an exception.
if ($this->shouldRaisePolicyExceptions()) {
return null;
}
$space_phids = array(); $space_phids = array();
$include_null = false; $include_null = false;