Merge branch 'master' into blender-tweaks
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE {$NAMESPACE}_project.project
|
||||||
|
ADD subtype VARCHAR(64) COLLATE {$COLLATE_TEXT} NOT NULL;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
UPDATE {$NAMESPACE}_project.project
|
||||||
|
SET subtype = 'default' WHERE subtype = '';
|
||||||
18
resources/sql/autopatches/20190129.project.01.spaces.php
Normal file
18
resources/sql/autopatches/20190129.project.01.spaces.php
Normal 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']);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorAuthFactorProviderMFAEngine
|
||||||
|
extends PhabricatorEditEngineMFAEngine {
|
||||||
|
|
||||||
|
public function shouldTryMFA() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
813
src/applications/auth/factor/PhabricatorDuoAuthFactor.php
Normal file
813
src/applications/auth/factor/PhabricatorDuoAuthFactor.php
Normal 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 )----------------------------------------- */
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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.')),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.',
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -30,4 +30,10 @@ final class PassphraseCredentialLookedAtTransaction
|
|||||||
return 'blue';
|
return 'blue';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldTryMFA(
|
||||||
|
$object,
|
||||||
|
PhabricatorApplicationTransaction $xaction) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -89,4 +89,11 @@ final class PhabricatorUserUsernameTransaction
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function shouldTryMFA(
|
||||||
|
$object,
|
||||||
|
PhabricatorApplicationTransaction $xaction) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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')),
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorProjectSubtypesConfigType
|
||||||
|
extends PhabricatorJSONConfigType {
|
||||||
|
|
||||||
|
const TYPEKEY = 'projects.subtypes';
|
||||||
|
|
||||||
|
public function validateStoredValue(
|
||||||
|
PhabricatorConfigOption $option,
|
||||||
|
$value) {
|
||||||
|
PhabricatorEditEngineSubtype::validateConfiguration($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user