1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-23 21:18:19 +01:00

(stable) Promote 2019 Week 4

This commit is contained in:
epriestley 2019-01-28 18:34:23 -08:00
commit 6bca2e0773
129 changed files with 5809 additions and 573 deletions

View file

@ -9,7 +9,7 @@ return array(
'names' => array(
'conpherence.pkg.css' => '3c8a0668',
'conpherence.pkg.js' => '020aebcf',
'core.pkg.css' => 'e94cc920',
'core.pkg.css' => 'e0cb8094',
'core.pkg.js' => '5c737607',
'differential.pkg.css' => 'b8df73d4',
'differential.pkg.js' => '67c9ea4c',
@ -127,7 +127,7 @@ return array(
'rsrc/css/phui/calendar/phui-calendar-list.css' => 'ccd7e4e2',
'rsrc/css/phui/calendar/phui-calendar-month.css' => 'cb758c42',
'rsrc/css/phui/calendar/phui-calendar.css' => 'f11073aa',
'rsrc/css/phui/object-item/phui-oi-big-ui.css' => 'e5b1fb04',
'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '9e037c7a',
'rsrc/css/phui/object-item/phui-oi-color.css' => 'b517bfa0',
'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => 'da15d3dc',
'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '490e2e2e',
@ -151,7 +151,7 @@ return array(
'rsrc/css/phui/phui-document.css' => '52b748a5',
'rsrc/css/phui/phui-feed-story.css' => 'a0c05029',
'rsrc/css/phui/phui-fontkit.css' => '9b714a5e',
'rsrc/css/phui/phui-form-view.css' => '9508671e',
'rsrc/css/phui/phui-form-view.css' => '0807e7ac',
'rsrc/css/phui/phui-form.css' => '159e2d9c',
'rsrc/css/phui/phui-head-thing.css' => 'd7f293df',
'rsrc/css/phui/phui-header-view.css' => '93cea4ec',
@ -159,7 +159,7 @@ return array(
'rsrc/css/phui/phui-icon-set-selector.css' => '7aa5f3ec',
'rsrc/css/phui/phui-icon.css' => '281f964d',
'rsrc/css/phui/phui-image-mask.css' => '62c7f4d2',
'rsrc/css/phui/phui-info-view.css' => 'f9464caf',
'rsrc/css/phui/phui-info-view.css' => '37b8d9ce',
'rsrc/css/phui/phui-invisible-character-view.css' => 'c694c4a4',
'rsrc/css/phui/phui-left-right.css' => '68513c34',
'rsrc/css/phui/phui-lightbox.css' => '4ebf22da',
@ -817,7 +817,7 @@ return array(
'phui-font-icon-base-css' => 'd7994e06',
'phui-fontkit-css' => '9b714a5e',
'phui-form-css' => '159e2d9c',
'phui-form-view-css' => '9508671e',
'phui-form-view-css' => '0807e7ac',
'phui-head-thing-view-css' => 'd7f293df',
'phui-header-view-css' => '93cea4ec',
'phui-hovercard' => '074f0783',
@ -825,14 +825,14 @@ return array(
'phui-icon-set-selector-css' => '7aa5f3ec',
'phui-icon-view-css' => '281f964d',
'phui-image-mask-css' => '62c7f4d2',
'phui-info-view-css' => 'f9464caf',
'phui-info-view-css' => '37b8d9ce',
'phui-inline-comment-view-css' => '48acce5b',
'phui-invisible-character-view-css' => 'c694c4a4',
'phui-left-right-css' => '68513c34',
'phui-lightbox-css' => '4ebf22da',
'phui-list-view-css' => '470b1adb',
'phui-object-box-css' => '9b58483d',
'phui-oi-big-ui-css' => 'e5b1fb04',
'phui-oi-big-ui-css' => '9e037c7a',
'phui-oi-color-css' => 'b517bfa0',
'phui-oi-drag-ui-css' => 'da15d3dc',
'phui-oi-flush-ui-css' => '490e2e2e',
@ -1710,6 +1710,9 @@ return array(
'javelin-uri',
'phabricator-textareautils',
),
'9e037c7a' => array(
'phui-oi-list-view-css',
),
'9f081f05' => array(
'javelin-behavior',
'javelin-dom',
@ -2024,9 +2027,6 @@ return array(
'e562708c' => array(
'javelin-install',
),
'e5b1fb04' => array(
'phui-oi-list-view-css',
),
'e5bdb730' => array(
'javelin-behavior',
'javelin-stratcom',

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_auth.auth_factorconfig
ADD factorProviderPHID VARBINARY(64) NOT NULL;

View file

@ -0,0 +1,72 @@
<?php
// Previously, MFA factors for individual users were bound to raw factor types.
// The only factor type ever implemented in the upstream was "totp".
// Going forward, individual factors are bound to a provider instead. This
// allows factor types to have some configuration, like API keys for
// service-based MFA. It also allows installs to select which types of factors
// they want users to be able to set up.
// Migrate all existing TOTP factors to the first available TOTP provider,
// creating one if none exists. This migration is a little bit messy, but
// gives us a clean slate going forward with no "builtin" providers.
$table = new PhabricatorAuthFactorConfig();
$conn = $table->establishConnection('w');
$provider_table = new PhabricatorAuthFactorProvider();
$provider_phid = null;
$iterator = new LiskRawMigrationIterator($conn, $table->getTableName());
$totp_key = 'totp';
foreach ($iterator as $row) {
// This wasn't a TOTP factor, so skip it.
if ($row['factorKey'] !== $totp_key) {
continue;
}
// This factor already has an associated provider.
if (strlen($row['factorProviderPHID'])) {
continue;
}
// Find (or create) a suitable TOTP provider. Note that we can't "save()"
// an object or this migration will break if the object ever gets new
// columns; just INSERT the raw fields instead.
if ($provider_phid === null) {
$provider_row = queryfx_one(
$conn,
'SELECT phid FROM %R WHERE providerFactorKey = %s LIMIT 1',
$provider_table,
$totp_key);
if ($provider_row) {
$provider_phid = $provider_row['phid'];
} else {
$provider_phid = $provider_table->generatePHID();
queryfx(
$conn,
'INSERT INTO %R
(phid, providerFactorKey, name, status, properties,
dateCreated, dateModified)
VALUES (%s, %s, %s, %s, %s, %d, %d)',
$provider_table,
$provider_phid,
$totp_key,
'',
'active',
'{}',
PhabricatorTime::getNow(),
PhabricatorTime::getNow());
}
}
queryfx(
$conn,
'UPDATE %R SET factorProviderPHID = %s WHERE id = %d',
$table,
$provider_phid,
$row['id']);
}

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_auth.auth_factorconfig
DROP factorKey;

View file

@ -0,0 +1,11 @@
CREATE TABLE {$NAMESPACE}_auth.auth_contactnumber (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
objectPHID VARBINARY(64) NOT NULL,
contactNumber VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT},
status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
uniqueKey BINARY(12),
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,19 @@
CREATE TABLE {$NAMESPACE}_auth.auth_contactnumbertransaction (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
phid VARBINARY(64) NOT NULL,
authorPHID VARBINARY(64) NOT NULL,
objectPHID VARBINARY(64) NOT NULL,
viewPolicy VARBINARY(64) NOT NULL,
editPolicy VARBINARY(64) NOT NULL,
commentPHID VARBINARY(64) DEFAULT NULL,
commentVersion INT UNSIGNED NOT NULL,
transactionType VARCHAR(32) NOT NULL,
oldValue LONGTEXT NOT NULL,
newValue LONGTEXT NOT NULL,
contentSource LONGTEXT NOT NULL,
metadata LONGTEXT NOT NULL,
dateCreated INT UNSIGNED NOT NULL,
dateModified INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (`phid`),
KEY `key_object` (`objectPHID`)
) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_auth.auth_contactnumber
ADD isPrimary BOOL NOT NULL;

View file

@ -218,6 +218,7 @@ $user->openTransaction();
->setActor($actor)
->setActingAsPHID($people_application_phid)
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$transaction_editor->applyTransactions($user, $xactions);

View file

@ -2076,7 +2076,6 @@ phutil_register_library_map(array(
'PhabricatorAccessLog' => 'infrastructure/log/PhabricatorAccessLog.php',
'PhabricatorAccessLogConfigOptions' => 'applications/config/option/PhabricatorAccessLogConfigOptions.php',
'PhabricatorAccessibilitySetting' => 'applications/settings/setting/PhabricatorAccessibilitySetting.php',
'PhabricatorAccountSettingsPanel' => 'applications/settings/panel/PhabricatorAccountSettingsPanel.php',
'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php',
'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php',
'PhabricatorActivitySettingsPanel' => 'applications/settings/panel/PhabricatorActivitySettingsPanel.php',
@ -2084,6 +2083,7 @@ phutil_register_library_map(array(
'PhabricatorAjaxRequestExceptionHandler' => 'aphront/handler/PhabricatorAjaxRequestExceptionHandler.php',
'PhabricatorAlmanacApplication' => 'applications/almanac/application/PhabricatorAlmanacApplication.php',
'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php',
'PhabricatorAmazonSNSFuture' => 'applications/metamta/future/PhabricatorAmazonSNSFuture.php',
'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php',
'PhabricatorAphlictManagementDebugWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php',
'PhabricatorAphlictManagementNotifyWorkflow' => 'applications/aphlict/management/PhabricatorAphlictManagementNotifyWorkflow.php',
@ -2200,6 +2200,24 @@ phutil_register_library_map(array(
'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php',
'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php',
'PhabricatorAuthConfirmLinkController' => 'applications/auth/controller/PhabricatorAuthConfirmLinkController.php',
'PhabricatorAuthContactNumber' => 'applications/auth/storage/PhabricatorAuthContactNumber.php',
'PhabricatorAuthContactNumberController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberController.php',
'PhabricatorAuthContactNumberDisableController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberDisableController.php',
'PhabricatorAuthContactNumberEditController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php',
'PhabricatorAuthContactNumberEditEngine' => 'applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php',
'PhabricatorAuthContactNumberEditor' => 'applications/auth/editor/PhabricatorAuthContactNumberEditor.php',
'PhabricatorAuthContactNumberMFAEngine' => 'applications/auth/engine/PhabricatorAuthContactNumberMFAEngine.php',
'PhabricatorAuthContactNumberNumberTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php',
'PhabricatorAuthContactNumberPHIDType' => 'applications/auth/phid/PhabricatorAuthContactNumberPHIDType.php',
'PhabricatorAuthContactNumberPrimaryController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php',
'PhabricatorAuthContactNumberPrimaryTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php',
'PhabricatorAuthContactNumberQuery' => 'applications/auth/query/PhabricatorAuthContactNumberQuery.php',
'PhabricatorAuthContactNumberStatusTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php',
'PhabricatorAuthContactNumberTestController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberTestController.php',
'PhabricatorAuthContactNumberTransaction' => 'applications/auth/storage/PhabricatorAuthContactNumberTransaction.php',
'PhabricatorAuthContactNumberTransactionQuery' => 'applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php',
'PhabricatorAuthContactNumberTransactionType' => 'applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php',
'PhabricatorAuthContactNumberViewController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php',
'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php',
'PhabricatorAuthDAO' => 'applications/auth/storage/PhabricatorAuthDAO.php',
'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php',
@ -2207,14 +2225,22 @@ phutil_register_library_map(array(
'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php',
'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php',
'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php',
'PhabricatorAuthFactorConfigQuery' => 'applications/auth/query/PhabricatorAuthFactorConfigQuery.php',
'PhabricatorAuthFactorProvider' => 'applications/auth/storage/PhabricatorAuthFactorProvider.php',
'PhabricatorAuthFactorProviderController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php',
'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',
'PhabricatorAuthFactorProviderEditEngine' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php',
'PhabricatorAuthFactorProviderEditor' => 'applications/auth/editor/PhabricatorAuthFactorProviderEditor.php',
'PhabricatorAuthFactorProviderListController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php',
'PhabricatorAuthFactorProviderMFAEngine' => 'applications/auth/engine/PhabricatorAuthFactorProviderMFAEngine.php',
'PhabricatorAuthFactorProviderNameTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderNameTransaction.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',
'PhabricatorAuthFactorProviderTransactionQuery' => 'applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php',
'PhabricatorAuthFactorProviderTransactionType' => 'applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php',
@ -2249,10 +2275,12 @@ phutil_register_library_map(array(
'PhabricatorAuthLoginMessageType' => 'applications/auth/message/PhabricatorAuthLoginMessageType.php',
'PhabricatorAuthLogoutConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthLogoutConduitAPIMethod.php',
'PhabricatorAuthMFAEditEngineExtension' => 'applications/auth/engineextension/PhabricatorAuthMFAEditEngineExtension.php',
'PhabricatorAuthMFASyncTemporaryTokenType' => 'applications/auth/factor/PhabricatorAuthMFASyncTemporaryTokenType.php',
'PhabricatorAuthMainMenuBarExtension' => 'applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php',
'PhabricatorAuthManagementCachePKCS8Workflow' => 'applications/auth/management/PhabricatorAuthManagementCachePKCS8Workflow.php',
'PhabricatorAuthManagementLDAPWorkflow' => 'applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php',
'PhabricatorAuthManagementListFactorsWorkflow' => 'applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php',
'PhabricatorAuthManagementListMFAProvidersWorkflow' => 'applications/auth/management/PhabricatorAuthManagementListMFAProvidersWorkflow.php',
'PhabricatorAuthManagementRecoverWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php',
'PhabricatorAuthManagementRefreshWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRefreshWorkflow.php',
'PhabricatorAuthManagementRevokeWorkflow' => 'applications/auth/management/PhabricatorAuthManagementRevokeWorkflow.php',
@ -2279,6 +2307,7 @@ phutil_register_library_map(array(
'PhabricatorAuthNeedsApprovalController' => 'applications/auth/controller/PhabricatorAuthNeedsApprovalController.php',
'PhabricatorAuthNeedsMultiFactorController' => 'applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php',
'PhabricatorAuthNewController' => 'applications/auth/controller/config/PhabricatorAuthNewController.php',
'PhabricatorAuthNewFactorAction' => 'applications/auth/action/PhabricatorAuthNewFactorAction.php',
'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php',
'PhabricatorAuthOneTimeLoginController' => 'applications/auth/controller/PhabricatorAuthOneTimeLoginController.php',
'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthOneTimeLoginTemporaryTokenType.php',
@ -2341,7 +2370,6 @@ phutil_register_library_map(array(
'PhabricatorAuthSetPasswordController' => 'applications/auth/controller/PhabricatorAuthSetPasswordController.php',
'PhabricatorAuthSetupCheck' => 'applications/config/check/PhabricatorAuthSetupCheck.php',
'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php',
'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'applications/auth/factor/PhabricatorAuthTOTPKeyTemporaryTokenType.php',
'PhabricatorAuthTemporaryToken' => 'applications/auth/storage/PhabricatorAuthTemporaryToken.php',
'PhabricatorAuthTemporaryTokenGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthTemporaryTokenGarbageCollector.php',
'PhabricatorAuthTemporaryTokenQuery' => 'applications/auth/query/PhabricatorAuthTemporaryTokenQuery.php',
@ -2349,6 +2377,7 @@ phutil_register_library_map(array(
'PhabricatorAuthTemporaryTokenType' => 'applications/auth/tokentype/PhabricatorAuthTemporaryTokenType.php',
'PhabricatorAuthTemporaryTokenTypeModule' => 'applications/auth/tokentype/PhabricatorAuthTemporaryTokenTypeModule.php',
'PhabricatorAuthTerminateSessionController' => 'applications/auth/controller/PhabricatorAuthTerminateSessionController.php',
'PhabricatorAuthTestSMSAction' => 'applications/auth/action/PhabricatorAuthTestSMSAction.php',
'PhabricatorAuthTryFactorAction' => 'applications/auth/action/PhabricatorAuthTryFactorAction.php',
'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php',
'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php',
@ -2737,6 +2766,7 @@ phutil_register_library_map(array(
'PhabricatorConpherenceWidgetVisibleSetting' => 'applications/settings/setting/PhabricatorConpherenceWidgetVisibleSetting.php',
'PhabricatorConsoleApplication' => 'applications/console/application/PhabricatorConsoleApplication.php',
'PhabricatorConsoleContentSource' => 'infrastructure/contentsource/PhabricatorConsoleContentSource.php',
'PhabricatorContactNumbersSettingsPanel' => 'applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php',
'PhabricatorContentSource' => 'infrastructure/contentsource/PhabricatorContentSource.php',
'PhabricatorContentSourceModule' => 'infrastructure/contentsource/PhabricatorContentSourceModule.php',
'PhabricatorContentSourceView' => 'infrastructure/contentsource/PhabricatorContentSourceView.php',
@ -2774,6 +2804,7 @@ phutil_register_library_map(array(
'PhabricatorCountdownTransactionType' => 'applications/countdown/xaction/PhabricatorCountdownTransactionType.php',
'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php',
'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php',
'PhabricatorCredentialEditField' => 'applications/transactions/editfield/PhabricatorCredentialEditField.php',
'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php',
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'infrastructure/customfield/datasource/PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource.php',
@ -2960,6 +2991,8 @@ phutil_register_library_map(array(
'PhabricatorDraftEngine' => 'applications/transactions/draft/PhabricatorDraftEngine.php',
'PhabricatorDraftInterface' => 'applications/transactions/draft/PhabricatorDraftInterface.php',
'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.php',
'PhabricatorDuoAuthFactor' => 'applications/auth/factor/PhabricatorDuoAuthFactor.php',
'PhabricatorDuoFuture' => 'applications/auth/future/PhabricatorDuoFuture.php',
'PhabricatorEdgeChangeRecord' => 'infrastructure/edges/util/PhabricatorEdgeChangeRecord.php',
'PhabricatorEdgeChangeRecordTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeChangeRecordTestCase.php',
'PhabricatorEdgeConfig' => 'infrastructure/edges/constants/PhabricatorEdgeConfig.php',
@ -3347,6 +3380,7 @@ phutil_register_library_map(array(
'PhabricatorKeyringConfigOptionType' => 'applications/files/keyring/PhabricatorKeyringConfigOptionType.php',
'PhabricatorLDAPAuthProvider' => 'applications/auth/provider/PhabricatorLDAPAuthProvider.php',
'PhabricatorLabelProfileMenuItem' => 'applications/search/menuitem/PhabricatorLabelProfileMenuItem.php',
'PhabricatorLanguageSettingsPanel' => 'applications/settings/panel/PhabricatorLanguageSettingsPanel.php',
'PhabricatorLegalpadApplication' => 'applications/legalpad/application/PhabricatorLegalpadApplication.php',
'PhabricatorLegalpadDocumentPHIDType' => 'applications/legalpad/phid/PhabricatorLegalpadDocumentPHIDType.php',
'PhabricatorLegalpadSignaturePolicyRule' => 'applications/legalpad/policyrule/PhabricatorLegalpadSignaturePolicyRule.php',
@ -3402,6 +3436,7 @@ phutil_register_library_map(array(
'PhabricatorMacroViewController' => 'applications/macro/controller/PhabricatorMacroViewController.php',
'PhabricatorMailAdapter' => 'applications/metamta/adapter/PhabricatorMailAdapter.php',
'PhabricatorMailAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php',
'PhabricatorMailAmazonSNSAdapter' => 'applications/metamta/adapter/PhabricatorMailAmazonSNSAdapter.php',
'PhabricatorMailAttachment' => 'applications/metamta/message/PhabricatorMailAttachment.php',
'PhabricatorMailConfigTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMailConfigTestCase.php',
'PhabricatorMailEmailEngine' => 'applications/metamta/engine/PhabricatorMailEmailEngine.php',
@ -3436,6 +3471,7 @@ phutil_register_library_map(array(
'PhabricatorMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorMailReceiverTestCase.php',
'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php',
'PhabricatorMailRoutingRule' => 'applications/metamta/constants/PhabricatorMailRoutingRule.php',
'PhabricatorMailSMSEngine' => 'applications/metamta/engine/PhabricatorMailSMSEngine.php',
'PhabricatorMailSMSMessage' => 'applications/metamta/message/PhabricatorMailSMSMessage.php',
'PhabricatorMailSMTPAdapter' => 'applications/metamta/adapter/PhabricatorMailSMTPAdapter.php',
'PhabricatorMailSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailSendGridAdapter.php',
@ -3868,6 +3904,7 @@ phutil_register_library_map(array(
'PhabricatorPholioApplication' => 'applications/pholio/application/PhabricatorPholioApplication.php',
'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php',
'PhabricatorPhoneNumber' => 'applications/metamta/message/PhabricatorPhoneNumber.php',
'PhabricatorPhoneNumberTestCase' => 'applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php',
'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php',
'PhabricatorPhortuneContentSource' => 'applications/phortune/contentsource/PhabricatorPhortuneContentSource.php',
'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php',
@ -4275,6 +4312,7 @@ phutil_register_library_map(array(
'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php',
'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php',
'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php',
'PhabricatorSMSAuthFactor' => 'applications/auth/factor/PhabricatorSMSAuthFactor.php',
'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php',
'PhabricatorSSHKeyGenerator' => 'infrastructure/util/PhabricatorSSHKeyGenerator.php',
'PhabricatorSSHKeysSettingsPanel' => 'applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php',
@ -7740,7 +7778,6 @@ phutil_register_library_map(array(
'PhabricatorAccessLog' => 'Phobject',
'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting',
'PhabricatorAccountSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorActionListView' => 'AphrontTagView',
'PhabricatorActionView' => 'AphrontView',
'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel',
@ -7748,6 +7785,7 @@ phutil_register_library_map(array(
'PhabricatorAjaxRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorAlmanacApplication' => 'PhabricatorApplication',
'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorAmazonSNSFuture' => 'PhutilAWSFuture',
'PhabricatorAnchorView' => 'AphrontView',
'PhabricatorAphlictManagementDebugWorkflow' => 'PhabricatorAphlictManagementWorkflow',
'PhabricatorAphlictManagementNotifyWorkflow' => 'PhabricatorAphlictManagementWorkflow',
@ -7882,26 +7920,63 @@ phutil_register_library_map(array(
'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod',
'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker',
'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController',
'PhabricatorAuthContactNumber' => array(
'PhabricatorAuthDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
'PhabricatorEditEngineMFAInterface',
),
'PhabricatorAuthContactNumberController' => 'PhabricatorAuthController',
'PhabricatorAuthContactNumberDisableController' => 'PhabricatorAuthContactNumberController',
'PhabricatorAuthContactNumberEditController' => 'PhabricatorAuthContactNumberController',
'PhabricatorAuthContactNumberEditEngine' => 'PhabricatorEditEngine',
'PhabricatorAuthContactNumberEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthContactNumberMFAEngine' => 'PhabricatorEditEngineMFAEngine',
'PhabricatorAuthContactNumberNumberTransaction' => 'PhabricatorAuthContactNumberTransactionType',
'PhabricatorAuthContactNumberPHIDType' => 'PhabricatorPHIDType',
'PhabricatorAuthContactNumberPrimaryController' => 'PhabricatorAuthContactNumberController',
'PhabricatorAuthContactNumberPrimaryTransaction' => 'PhabricatorAuthContactNumberTransactionType',
'PhabricatorAuthContactNumberQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthContactNumberStatusTransaction' => 'PhabricatorAuthContactNumberTransactionType',
'PhabricatorAuthContactNumberTestController' => 'PhabricatorAuthContactNumberController',
'PhabricatorAuthContactNumberTransaction' => 'PhabricatorModularTransaction',
'PhabricatorAuthContactNumberTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthContactNumberTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorAuthContactNumberViewController' => 'PhabricatorAuthContactNumberController',
'PhabricatorAuthController' => 'PhabricatorController',
'PhabricatorAuthDAO' => 'PhabricatorLiskDAO',
'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController',
'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthFactor' => 'Phobject',
'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO',
'PhabricatorAuthFactorConfig' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
'PhabricatorDestructibleInterface',
),
'PhabricatorAuthFactorConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthFactorProvider' => array(
'PhabricatorAuthDAO',
'PhabricatorApplicationTransactionInterface',
'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface',
'PhabricatorEditEngineMFAInterface',
),
'PhabricatorAuthFactorProviderController' => 'PhabricatorAuthProviderController',
'PhabricatorAuthFactorProviderDuoCredentialTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderDuoEnrollTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderDuoHostnameTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderDuoUsernamesTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderEditController' => 'PhabricatorAuthFactorProviderController',
'PhabricatorAuthFactorProviderEditEngine' => 'PhabricatorEditEngine',
'PhabricatorAuthFactorProviderEditor' => 'PhabricatorApplicationTransactionEditor',
'PhabricatorAuthFactorProviderListController' => 'PhabricatorAuthProviderController',
'PhabricatorAuthFactorProviderMFAEngine' => 'PhabricatorEditEngineMFAEngine',
'PhabricatorAuthFactorProviderNameTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthFactorProviderStatus' => 'Phobject',
'PhabricatorAuthFactorProviderStatusTransaction' => 'PhabricatorAuthFactorProviderTransactionType',
'PhabricatorAuthFactorProviderTransaction' => 'PhabricatorModularTransaction',
'PhabricatorAuthFactorProviderTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthFactorProviderTransactionType' => 'PhabricatorModularTransactionType',
@ -7939,10 +8014,12 @@ phutil_register_library_map(array(
'PhabricatorAuthLoginMessageType' => 'PhabricatorAuthMessageType',
'PhabricatorAuthLogoutConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod',
'PhabricatorAuthMFAEditEngineExtension' => 'PhabricatorEditEngineExtension',
'PhabricatorAuthMFASyncTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorAuthMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension',
'PhabricatorAuthManagementCachePKCS8Workflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementLDAPWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementListFactorsWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementListMFAProvidersWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementRecoverWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementRefreshWorkflow' => 'PhabricatorAuthManagementWorkflow',
'PhabricatorAuthManagementRevokeWorkflow' => 'PhabricatorAuthManagementWorkflow',
@ -7974,6 +8051,7 @@ phutil_register_library_map(array(
'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController',
'PhabricatorAuthNeedsMultiFactorController' => 'PhabricatorAuthController',
'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController',
'PhabricatorAuthNewFactorAction' => 'PhabricatorSystemAction',
'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController',
'PhabricatorAuthOneTimeLoginController' => 'PhabricatorAuthController',
'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
@ -8052,7 +8130,6 @@ phutil_register_library_map(array(
'PhabricatorAuthSetPasswordController' => 'PhabricatorAuthController',
'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorAuthStartController' => 'PhabricatorAuthController',
'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
'PhabricatorAuthTemporaryToken' => array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
@ -8063,6 +8140,7 @@ phutil_register_library_map(array(
'PhabricatorAuthTemporaryTokenType' => 'Phobject',
'PhabricatorAuthTemporaryTokenTypeModule' => 'PhabricatorConfigModule',
'PhabricatorAuthTerminateSessionController' => 'PhabricatorAuthController',
'PhabricatorAuthTestSMSAction' => 'PhabricatorSystemAction',
'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction',
'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController',
'PhabricatorAuthValidateController' => 'PhabricatorAuthController',
@ -8516,6 +8594,7 @@ phutil_register_library_map(array(
'PhabricatorConpherenceWidgetVisibleSetting' => 'PhabricatorInternalSetting',
'PhabricatorConsoleApplication' => 'PhabricatorApplication',
'PhabricatorConsoleContentSource' => 'PhabricatorContentSource',
'PhabricatorContactNumbersSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorContentSource' => 'Phobject',
'PhabricatorContentSourceModule' => 'PhabricatorConfigModule',
'PhabricatorContentSourceView' => 'AphrontView',
@ -8564,6 +8643,7 @@ phutil_register_library_map(array(
'PhabricatorCountdownTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCountdownView' => 'AphrontView',
'PhabricatorCountdownViewController' => 'PhabricatorCountdownController',
'PhabricatorCredentialEditField' => 'PhabricatorEditField',
'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
'PhabricatorCustomField' => 'Phobject',
'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'PhabricatorTypeaheadDatasource',
@ -8768,6 +8848,8 @@ phutil_register_library_map(array(
'PhabricatorDraftDAO' => 'PhabricatorLiskDAO',
'PhabricatorDraftEngine' => 'Phobject',
'PhabricatorDrydockApplication' => 'PhabricatorApplication',
'PhabricatorDuoAuthFactor' => 'PhabricatorAuthFactor',
'PhabricatorDuoFuture' => 'FutureProxy',
'PhabricatorEdgeChangeRecord' => 'Phobject',
'PhabricatorEdgeChangeRecordTestCase' => 'PhabricatorTestCase',
'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants',
@ -9204,6 +9286,7 @@ phutil_register_library_map(array(
'PhabricatorKeyringConfigOptionType' => 'PhabricatorConfigJSONOptionType',
'PhabricatorLDAPAuthProvider' => 'PhabricatorAuthProvider',
'PhabricatorLabelProfileMenuItem' => 'PhabricatorProfileMenuItem',
'PhabricatorLanguageSettingsPanel' => 'PhabricatorEditEngineSettingsPanel',
'PhabricatorLegalpadApplication' => 'PhabricatorApplication',
'PhabricatorLegalpadDocumentPHIDType' => 'PhabricatorPHIDType',
'PhabricatorLegalpadSignaturePolicyRule' => 'PhabricatorPolicyRule',
@ -9259,6 +9342,7 @@ phutil_register_library_map(array(
'PhabricatorMacroViewController' => 'PhabricatorMacroController',
'PhabricatorMailAdapter' => 'Phobject',
'PhabricatorMailAmazonSESAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailAmazonSNSAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailAttachment' => 'Phobject',
'PhabricatorMailConfigTestCase' => 'PhabricatorTestCase',
'PhabricatorMailEmailEngine' => 'PhabricatorMailMessageEngine',
@ -9293,6 +9377,7 @@ phutil_register_library_map(array(
'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase',
'PhabricatorMailReplyHandler' => 'Phobject',
'PhabricatorMailRoutingRule' => 'Phobject',
'PhabricatorMailSMSEngine' => 'PhabricatorMailMessageEngine',
'PhabricatorMailSMSMessage' => 'PhabricatorMailExternalMessage',
'PhabricatorMailSMTPAdapter' => 'PhabricatorMailAdapter',
'PhabricatorMailSendGridAdapter' => 'PhabricatorMailAdapter',
@ -9808,6 +9893,7 @@ phutil_register_library_map(array(
'PhabricatorPholioApplication' => 'PhabricatorApplication',
'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorPhoneNumber' => 'Phobject',
'PhabricatorPhoneNumberTestCase' => 'PhabricatorTestCase',
'PhabricatorPhortuneApplication' => 'PhabricatorApplication',
'PhabricatorPhortuneContentSource' => 'PhabricatorContentSource',
'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow',
@ -10340,6 +10426,7 @@ phutil_register_library_map(array(
'PhabricatorResourceSite' => 'PhabricatorSite',
'PhabricatorRobotsController' => 'PhabricatorController',
'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorSMSAuthFactor' => 'PhabricatorAuthFactor',
'PhabricatorSQLPatchList' => 'Phobject',
'PhabricatorSSHKeyGenerator' => 'Phobject',
'PhabricatorSSHKeysSettingsPanel' => 'PhabricatorSettingsPanel',

View file

@ -38,10 +38,14 @@ final class PhabricatorHighSecurityRequestExceptionHandler
$request);
$is_wait = false;
$is_continue = false;
foreach ($results as $result) {
if ($result->getIsWait()) {
$is_wait = true;
break;
}
if ($result->getIsContinue()) {
$is_continue = true;
}
}
@ -55,7 +59,7 @@ final class PhabricatorHighSecurityRequestExceptionHandler
if ($is_wait) {
$submit = pht('Wait Patiently');
} else if ($is_upgrade) {
} else if ($is_upgrade && !$is_continue) {
$submit = pht('Enter High Security');
} else {
$submit = pht('Continue');
@ -74,19 +78,21 @@ final class PhabricatorHighSecurityRequestExceptionHandler
$form_layout = $form->buildLayoutView();
if ($is_upgrade) {
$messages = array(
pht(
'You are taking an action which requires you to enter '.
'high security.'),
);
$info_view = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_MFA)
->setErrors($messages);
$dialog
->setErrors(
array(
pht(
'You are taking an action which requires you to enter '.
'high security.'),
))
->appendChild($info_view)
->appendParagraph(
pht(
'High security mode helps protect your account from security '.
'threats, like session theft or someone messing with your stuff '.
'while you\'re grabbing a coffee. To enter high security mode, '.
'confirm your credentials.'))
'To enter high security mode, confirm your credentials:'))
->appendChild($form_layout)
->appendParagraph(
pht(

View file

@ -30,6 +30,14 @@ final class AlmanacCacheEngineExtension
foreach ($interfaces as $interface) {
$results[] = $interface;
}
$bindings = id(new AlmanacBindingQuery())
->setViewer($viewer)
->withDevicePHIDs(mpull($devices, 'getPHID'))
->execute();
foreach ($bindings as $binding) {
$results[] = $binding;
}
}
foreach ($this->selectObjects($objects, 'AlmanacInterface') as $iface) {

View file

@ -0,0 +1,21 @@
<?php
final class PhabricatorAuthNewFactorAction extends PhabricatorSystemAction {
const TYPECONST = 'auth.factor.new';
public function getActionConstant() {
return self::TYPECONST;
}
public function getScoreThreshold() {
return 60 / phutil_units('1 hour in seconds');
}
public function getLimitExplanation() {
return pht(
'You have failed too many attempts to synchronize new multi-factor '.
'authentication methods in a short period of time.');
}
}

View file

@ -0,0 +1,22 @@
<?php
final class PhabricatorAuthTestSMSAction extends PhabricatorSystemAction {
const TYPECONST = 'auth.sms.test';
public function getActionConstant() {
return self::TYPECONST;
}
public function getScoreThreshold() {
return 60 / phutil_units('1 hour in seconds');
}
public function getLimitExplanation() {
return pht(
'You and other users on this install are collectively sending too '.
'many test text messages too quickly. Wait a few minutes to continue '.
'texting tests.');
}
}

View file

@ -72,8 +72,10 @@ final class PhabricatorAuthApplication extends PhabricatorApplication {
=> 'PhabricatorAuthRevokeTokenController',
'session/downgrade/'
=> 'PhabricatorAuthDowngradeSessionController',
'multifactor/'
=> 'PhabricatorAuthNeedsMultiFactorController',
'enroll/' => array(
'(?:(?P<pageKey>[^/]+)/)?(?:(?P<formSaved>saved)/)?'
=> 'PhabricatorAuthNeedsMultiFactorController',
),
'sshkey/' => array(
$this->getQueryRoutePattern('for/(?P<forPHID>[^/]+)/')
=> 'PhabricatorAuthSSHKeyListController',
@ -104,6 +106,18 @@ final class PhabricatorAuthApplication extends PhabricatorApplication {
'PhabricatorAuthMessageViewController',
),
'contact/' => array(
$this->getEditRoutePattern('edit/') =>
'PhabricatorAuthContactNumberEditController',
'(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthContactNumberViewController',
'(?P<action>disable|enable)/(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthContactNumberDisableController',
'primary/(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthContactNumberPrimaryController',
'test/(?P<id>[1-9]\d*)/' =>
'PhabricatorAuthContactNumberTestController',
),
),
'/oauth/(?P<provider>\w+)/login/'

View file

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

View file

@ -30,80 +30,221 @@ final class PhabricatorAuthNeedsMultiFactorController
return new Aphront400Response();
}
$panel = id(new PhabricatorMultiFactorSettingsPanel())
->setUser($viewer)
->setViewer($viewer)
->setOverrideURI($this->getApplicationURI('/multifactor/'))
->processRequest($request);
$panels = $this->loadPanels();
if ($panel instanceof AphrontResponse) {
return $panel;
$multifactor_key = id(new PhabricatorMultiFactorSettingsPanel())
->getPanelKey();
$panel_key = $request->getURIData('pageKey');
if (!strlen($panel_key)) {
$panel_key = $multifactor_key;
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Add Multi-Factor Auth'));
if (!isset($panels[$panel_key])) {
return new Aphront404Response();
}
$nav = $this->newNavigation();
$nav->selectFilter($panel_key);
$panel = $panels[$panel_key];
$viewer->updateMultiFactorEnrollment();
if (!$viewer->getIsEnrolledInMultiFactor()) {
$help = id(new PHUIInfoView())
->setTitle(pht('Add Multi-Factor Authentication To Your Account'))
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
pht(
'Before you can use Phabricator, you need to add multi-factor '.
'authentication to your account.'),
pht(
'Multi-factor authentication helps secure your account by '.
'making it more difficult for attackers to gain access or '.
'take sensitive actions.'),
pht(
'To learn more about multi-factor authentication, click the '.
'%s button below.',
phutil_tag('strong', array(), pht('Help'))),
pht(
'To add an authentication factor, click the %s button below.',
phutil_tag('strong', array(), pht('Add Authentication Factor'))),
pht(
'To continue, add at least one authentication factor to your '.
'account.'),
));
if ($panel_key === $multifactor_key) {
$header_text = pht('Add Multi-Factor Auth');
$help = $this->newGuidance();
$panel->setIsEnrollment(true);
} else {
$help = id(new PHUIInfoView())
->setTitle(pht('Multi-Factor Authentication Configured'))
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->setErrors(
array(
pht(
'You have successfully configured multi-factor authentication '.
'for your account.'),
pht(
'You can make adjustments from the Settings panel later.'),
pht(
'When you are ready, %s.',
phutil_tag(
'strong',
array(),
phutil_tag(
'a',
array(
'href' => '/',
),
pht('continue to Phabricator')))),
));
$header_text = $panel->getPanelName();
$help = null;
}
$view = array(
$help,
$panel,
);
$response = $panel
->setController($this)
->setNavigation($nav)
->processRequest($request);
if (($response instanceof AphrontResponse) ||
($response instanceof AphrontResponseProducerInterface)) {
return $response;
}
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb(pht('Add Multi-Factor Auth'))
->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader($header_text);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter(
array(
$help,
$response,
));
return $this->newPage()
->setTitle(pht('Add Multi-Factor Authentication'))
->setCrumbs($crumbs)
->setNavigation($nav)
->appendChild($view);
}
private function loadPanels() {
$viewer = $this->getViewer();
$preferences = PhabricatorUserPreferences::loadUserPreferences($viewer);
$panels = PhabricatorSettingsPanel::getAllDisplayPanels();
$base_uri = $this->newEnrollBaseURI();
$result = array();
foreach ($panels as $key => $panel) {
$panel
->setPreferences($preferences)
->setViewer($viewer)
->setUser($viewer)
->setOverrideURI(urisprintf('%s%s/', $base_uri, $key));
if (!$panel->isEnabled()) {
continue;
}
if (!$panel->isUserPanel()) {
continue;
}
if (!$panel->isMultiFactorEnrollmentPanel()) {
continue;
}
if (!empty($result[$key])) {
throw new Exception(pht(
"Two settings panels share the same panel key ('%s'): %s, %s.",
$key,
get_class($panel),
get_class($result[$key])));
}
$result[$key] = $panel;
}
return $result;
}
private function newNavigation() {
$viewer = $this->getViewer();
$enroll_uri = $this->newEnrollBaseURI();
$nav = id(new AphrontSideNavFilterView())
->setBaseURI(new PhutilURI($enroll_uri));
$multifactor_key = id(new PhabricatorMultiFactorSettingsPanel())
->getPanelKey();
$nav->addFilter(
$multifactor_key,
pht('Enroll in MFA'),
null,
'fa-exclamation-triangle blue');
$panels = $this->loadPanels();
if ($panels) {
$nav->addLabel(pht('Settings'));
}
foreach ($panels as $panel_key => $panel) {
if ($panel_key === $multifactor_key) {
continue;
}
$nav->addFilter(
$panel->getPanelKey(),
$panel->getPanelName(),
null,
$panel->getPanelMenuIcon());
}
return $nav;
}
private function newEnrollBaseURI() {
return $this->getApplicationURI('enroll/');
}
private function newGuidance() {
$viewer = $this->getViewer();
if ($viewer->getIsEnrolledInMultiFactor()) {
$guidance = pht(
'{icon check, color="green"} **Setup Complete!**'.
"\n\n".
'You have successfully configured multi-factor authentication '.
'for your account.'.
"\n\n".
'You can make adjustments from the [[ /settings/ | Settings ]] panel '.
'later.');
return $this->newDialog()
->setTitle(pht('Multi-Factor Authentication Setup Complete'))
->setWidth(AphrontDialogView::WIDTH_FULL)
->appendChild(new PHUIRemarkupView($viewer, $guidance))
->addCancelButton('/', pht('Continue'));
}
$views = array();
$messages = array();
$messages[] = pht(
'Before you can use Phabricator, you need to add multi-factor '.
'authentication to your account. Multi-factor authentication helps '.
'secure your account by making it more difficult for attackers to '.
'gain access or take sensitive actions.');
$view = id(new PHUIInfoView())
->setTitle(pht('Add Multi-Factor Authentication To Your Account'))
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors($messages);
$views[] = $view;
$providers = id(new PhabricatorAuthFactorProviderQuery())
->setViewer($viewer)
->withStatuses(
array(
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
))
->execute();
if (!$providers) {
$messages = array();
$required_key = 'security.require-multi-factor-auth';
$messages[] = pht(
'This install has the configuration option "%s" enabled, but does '.
'not have any active multifactor providers configured. This means '.
'you are required to add MFA, but are also prevented from doing so. '.
'An administrator must disable "%s" or enable an MFA provider to '.
'allow you to continue.',
$required_key,
$required_key);
$view = id(new PHUIInfoView())
->setTitle(pht('Multi-Factor Authentication is Misconfigured'))
->setSeverity(PHUIInfoView::SEVERITY_ERROR)
->setErrors($messages);
$views[] = $view;
}
return $views;
}
}

View file

@ -88,7 +88,7 @@ final class PhabricatorAuthStartController
'This Phabricator install is not configured with any enabled '.
'authentication providers which can be used to log in. If you '.
'have accidentally locked yourself out by disabling all providers, '.
'you can use `%s` to recover access to an administrative account.',
'you can use `%s` to recover access to an account.',
'phabricator/bin/auth recover <username>'));
}

View file

@ -0,0 +1,31 @@
<?php
abstract class PhabricatorAuthContactNumberController
extends PhabricatorAuthController {
// Users may need to access these controllers to enroll in SMS MFA during
// account setup.
public function shouldRequireMultiFactorEnrollment() {
return false;
}
public function shouldRequireEnabledUser() {
return false;
}
public function shouldRequireEmailVerification() {
return false;
}
protected function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
$crumbs->addTextCrumb(
pht('Contact Numbers'),
pht('/settings/panel/contact/'));
return $crumbs;
}
}

View file

@ -0,0 +1,88 @@
<?php
final class PhabricatorAuthContactNumberDisableController
extends PhabricatorAuthContactNumberController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$number = id(new PhabricatorAuthContactNumberQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$number) {
return new Aphront404Response();
}
$is_disable = ($request->getURIData('action') == 'disable');
$id = $number->getID();
$cancel_uri = $number->getURI();
if ($request->isFormOrHisecPost()) {
$xactions = array();
if ($is_disable) {
$new_status = PhabricatorAuthContactNumber::STATUS_DISABLED;
} else {
$new_status = PhabricatorAuthContactNumber::STATUS_ACTIVE;
}
$xactions[] = id(new PhabricatorAuthContactNumberTransaction())
->setTransactionType(
PhabricatorAuthContactNumberStatusTransaction::TRANSACTIONTYPE)
->setNewValue($new_status);
$editor = id(new PhabricatorAuthContactNumberEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setCancelURI($cancel_uri);
try {
$editor->applyTransactions($number, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
// This happens when you enable a number which collides with another
// number.
return $this->newDialog()
->setTitle(pht('Changing Status Failed'))
->setValidationException($ex)
->addCancelButton($cancel_uri);
}
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
$number_display = phutil_tag(
'strong',
array(),
$number->getDisplayName());
if ($is_disable) {
$title = pht('Disable Contact Number');
$body = pht(
'Disable the contact number %s?',
$number_display);
$button = pht('Disable Number');
} else {
$title = pht('Enable Contact Number');
$body = pht(
'Enable the contact number %s?',
$number_display);
$button = pht('Enable Number');
}
return $this->newDialog()
->setTitle($title)
->appendParagraph($body)
->addSubmitButton($button)
->addCancelButton($cancel_uri);
}
}

View file

@ -0,0 +1,12 @@
<?php
final class PhabricatorAuthContactNumberEditController
extends PhabricatorAuthContactNumberController {
public function handleRequest(AphrontRequest $request) {
return id(new PhabricatorAuthContactNumberEditEngine())
->setController($this)
->buildResponse();
}
}

View file

@ -0,0 +1,88 @@
<?php
final class PhabricatorAuthContactNumberPrimaryController
extends PhabricatorAuthContactNumberController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$number = id(new PhabricatorAuthContactNumberQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$number) {
return new Aphront404Response();
}
$id = $number->getID();
$cancel_uri = $number->getURI();
if ($number->isDisabled()) {
return $this->newDialog()
->setTitle(pht('Number Disabled'))
->appendParagraph(
pht(
'You can not make a disabled number your primary contact number.'))
->addCancelButton($cancel_uri);
}
if ($number->getIsPrimary()) {
return $this->newDialog()
->setTitle(pht('Number Already Primary'))
->appendParagraph(
pht(
'This contact number is already your primary contact number.'))
->addCancelButton($cancel_uri);
}
if ($request->isFormOrHisecPost()) {
$xactions = array();
$xactions[] = id(new PhabricatorAuthContactNumberTransaction())
->setTransactionType(
PhabricatorAuthContactNumberPrimaryTransaction::TRANSACTIONTYPE)
->setNewValue(true);
$editor = id(new PhabricatorAuthContactNumberEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setCancelURI($cancel_uri);
try {
$editor->applyTransactions($number, $xactions);
} catch (PhabricatorApplicationTransactionValidationException $ex) {
// This happens when you try to make a number into your primary
// number, but you have contact number MFA on your account.
return $this->newDialog()
->setTitle(pht('Unable to Make Primary'))
->setValidationException($ex)
->addCancelButton($cancel_uri);
}
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
$number_display = phutil_tag(
'strong',
array(),
$number->getDisplayName());
return $this->newDialog()
->setTitle(pht('Set Primary Contact Number'))
->appendParagraph(
pht(
'Designate %s as your primary contact number?',
$number_display))
->addSubmitButton(pht('Make Primary'))
->addCancelButton($cancel_uri);
}
}

View file

@ -0,0 +1,64 @@
<?php
final class PhabricatorAuthContactNumberTestController
extends PhabricatorAuthContactNumberController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$number = id(new PhabricatorAuthContactNumberQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$number) {
return new Aphront404Response();
}
$id = $number->getID();
$cancel_uri = $number->getURI();
// NOTE: This is a global limit shared by all users.
PhabricatorSystemActionEngine::willTakeAction(
array(id(new PhabricatorAuthApplication())->getPHID()),
new PhabricatorAuthTestSMSAction(),
1);
if ($request->isFormPost()) {
$uri = PhabricatorEnv::getURI('/');
$uri = new PhutilURI($uri);
$mail = id(new PhabricatorMetaMTAMail())
->setMessageType(PhabricatorMailSMSMessage::MESSAGETYPE)
->addTos(array($viewer->getPHID()))
->setSensitiveContent(false)
->setBody(
pht(
'This is a terse test text message from Phabricator (%s).',
$uri->getDomain()))
->save();
return id(new AphrontRedirectResponse())->setURI($mail->getURI());
}
$number_display = phutil_tag(
'strong',
array(),
$number->getDisplayName());
return $this->newDialog()
->setTitle(pht('Set Test Message'))
->appendParagraph(
pht(
'Send a test message to %s?',
$number_display))
->addSubmitButton(pht('Send SMS'))
->addCancelButton($cancel_uri);
}
}

View file

@ -0,0 +1,139 @@
<?php
final class PhabricatorAuthContactNumberViewController
extends PhabricatorAuthContactNumberController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$number = id(new PhabricatorAuthContactNumberQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->executeOne();
if (!$number) {
return new Aphront404Response();
}
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($number->getObjectName())
->setBorder(true);
$header = $this->buildHeaderView($number);
$properties = $this->buildPropertiesView($number);
$curtain = $this->buildCurtain($number);
$timeline = $this->buildTransactionTimeline(
$number,
new PhabricatorAuthContactNumberTransactionQuery());
$timeline->setShouldTerminate(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(
array(
$timeline,
))
->addPropertySection(pht('Details'), $properties);
return $this->newPage()
->setTitle($number->getDisplayName())
->setCrumbs($crumbs)
->setPageObjectPHIDs(
array(
$number->getPHID(),
))
->appendChild($view);
}
private function buildHeaderView(PhabricatorAuthContactNumber $number) {
$viewer = $this->getViewer();
$view = id(new PHUIHeaderView())
->setViewer($viewer)
->setHeader($number->getObjectName())
->setPolicyObject($number);
if ($number->isDisabled()) {
$view->setStatus('fa-ban', 'red', pht('Disabled'));
} else if ($number->getIsPrimary()) {
$view->setStatus('fa-certificate', 'blue', pht('Primary'));
}
return $view;
}
private function buildPropertiesView(
PhabricatorAuthContactNumber $number) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView())
->setViewer($viewer);
$view->addProperty(
pht('Owner'),
$viewer->renderHandle($number->getObjectPHID()));
$view->addProperty(pht('Contact Number'), $number->getDisplayName());
return $view;
}
private function buildCurtain(PhabricatorAuthContactNumber $number) {
$viewer = $this->getViewer();
$id = $number->getID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$number,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain = $this->newCurtainView($number);
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Contact Number'))
->setIcon('fa-pencil')
->setHref($this->getApplicationURI("contact/edit/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Send Test Message'))
->setIcon('fa-envelope-o')
->setHref($this->getApplicationURI("contact/test/{$id}/"))
->setDisabled(!$can_edit)
->setWorkflow(true));
if ($number->isDisabled()) {
$disable_uri = $this->getApplicationURI("contact/enable/{$id}/");
$disable_name = pht('Enable Contact Number');
$disable_icon = 'fa-check';
$can_primary = false;
} else {
$disable_uri = $this->getApplicationURI("contact/disable/{$id}/");
$disable_name = pht('Disable Contact Number');
$disable_icon = 'fa-ban';
$can_primary = !$number->getIsPrimary();
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Make Primary Number'))
->setIcon('fa-certificate')
->setHref($this->getApplicationURI("contact/primary/{$id}/"))
->setDisabled(!$can_primary)
->setWorkflow(true));
$curtain->addAction(
id(new PhabricatorActionView())
->setName($disable_name)
->setIcon($disable_icon)
->setHref($disable_uri)
->setWorkflow(true));
return $curtain;
}
}

View file

@ -41,18 +41,33 @@ final class PhabricatorAuthFactorProviderEditController
->setBig(true)
->setFlush(true);
$factors = msortv($factors, 'newSortVector');
foreach ($factors as $factor_key => $factor) {
$factor_uri = id(new PhutilURI('/mfa/edit/'))
->setQueryParam('providerFactorKey', $factor_key);
$factor_uri = $this->getApplicationURI($factor_uri);
$is_enabled = $factor->canCreateNewProvider();
$item = id(new PHUIObjectItemView())
->setHeader($factor->getFactorName())
->setHref($factor_uri)
->setClickable(true)
->setImageIcon($factor->newIconView())
->addAttribute($factor->getFactorCreateHelp());
if ($is_enabled) {
$item
->setHref($factor_uri)
->setClickable(true);
} else {
$item->setDisabled(true);
}
$create_description = $factor->getProviderCreateDescription();
if ($create_description) {
$item->appendChild($create_description);
}
$menu->addItem($item);
}

View file

@ -20,6 +20,16 @@ final class PhabricatorAuthFactorProviderListController
->setHeader($provider->getDisplayName())
->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);
}

View file

@ -58,6 +58,15 @@ final class PhabricatorAuthFactorProviderViewController
->setHeader($provider->getDisplayName())
->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;
}

View file

@ -0,0 +1,86 @@
<?php
final class PhabricatorAuthContactNumberEditEngine
extends PhabricatorEditEngine {
const ENGINECONST = 'auth.contact';
public function isEngineConfigurable() {
return false;
}
public function getEngineName() {
return pht('Contact Numbers');
}
public function getSummaryHeader() {
return pht('Edit Contact Numbers');
}
public function getSummaryText() {
return pht('This engine is used to edit contact numbers.');
}
public function getEngineApplicationClass() {
return 'PhabricatorAuthApplication';
}
protected function newEditableObject() {
$viewer = $this->getViewer();
return PhabricatorAuthContactNumber::initializeNewContactNumber($viewer);
}
protected function newObjectQuery() {
return new PhabricatorAuthContactNumberQuery();
}
protected function getObjectCreateTitleText($object) {
return pht('Create Contact Number');
}
protected function getObjectCreateButtonText($object) {
return pht('Create Contact Number');
}
protected function getObjectEditTitleText($object) {
return pht('Edit Contact Number');
}
protected function getObjectEditShortText($object) {
return $object->getObjectName();
}
protected function getObjectCreateShortText() {
return pht('Create Contact Number');
}
protected function getObjectName() {
return pht('Contact Number');
}
protected function getEditorURI() {
return '/auth/contact/edit/';
}
protected function getObjectCreateCancelURI($object) {
return '/settings/panel/contact/';
}
protected function getObjectViewURI($object) {
return $object->getURI();
}
protected function buildCustomEditFields($object) {
return array(
id(new PhabricatorTextEditField())
->setKey('contactNumber')
->setTransactionType(
PhabricatorAuthContactNumberNumberTransaction::TRANSACTIONTYPE)
->setLabel(pht('Contact Number'))
->setDescription(pht('The contact number.'))
->setValue($object->getContactNumber())
->setIsRequired(true),
);
}
}

View file

@ -0,0 +1,38 @@
<?php
final class PhabricatorAuthContactNumberEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorAuthApplication';
}
public function getEditorObjectsDescription() {
return pht('Contact Numbers');
}
public function getCreateObjectTitle($author, $object) {
return pht('%s created this contact number.', $author);
}
public function getCreateObjectTitleForFeed($author, $object) {
return pht('%s created %s.', $author, $object);
}
protected function didCatchDuplicateKeyException(
PhabricatorLiskDAO $object,
array $xactions,
Exception $ex) {
$errors = array();
$errors[] = new PhabricatorApplicationTransactionValidationError(
PhabricatorAuthContactNumberNumberTransaction::TRANSACTIONTYPE,
pht('Duplicate'),
pht('This contact number is already in use.'),
null);
throw new PhabricatorApplicationTransactionValidationException($errors);
}
}

View file

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

View file

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

View file

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

View file

@ -47,6 +47,7 @@ final class PhabricatorAuthSessionEngine extends Phobject {
private $workflowKey;
private $request;
public function setWorkflowKey($workflow_key) {
$this->workflowKey = $workflow_key;
@ -65,6 +66,10 @@ final class PhabricatorAuthSessionEngine extends Phobject {
return $this->workflowKey;
}
public function getRequest() {
return $this->request;
}
/**
* Get the session kind (e.g., anonymous, user, external account) from a
@ -462,10 +467,25 @@ final class PhabricatorAuthSessionEngine extends Phobject {
return $token;
}
// Load the multi-factor auth sources attached to this account.
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID = %s',
$viewer->getPHID());
// Load the multi-factor auth sources attached to this account. Note that
// we order factors from oldest to newest, which is not the default query
// ordering but makes the greatest sense in context.
$factors = id(new PhabricatorAuthFactorConfigQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->withFactorProviderStatuses(
array(
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
))
->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
// without putting the session into high security mode. This is generally
@ -476,6 +496,7 @@ final class PhabricatorAuthSessionEngine extends Phobject {
return $this->issueHighSecurityToken($session, true);
}
$this->request = $request;
foreach ($factors as $factor) {
$factor->setSessionEngine($this);
}
@ -516,13 +537,29 @@ final class PhabricatorAuthSessionEngine extends Phobject {
foreach ($factors as $factor) {
$factor_phid = $factor->getPHID();
$issued_challenges = idx($challenge_map, $factor_phid, array());
$impl = $factor->requireImplementation();
$provider = $factor->getFactorProvider();
$impl = $provider->getFactor();
$new_challenges = $impl->getNewIssuedChallenges(
$factor,
$viewer,
$issued_challenges);
// 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;
}
$validation_results[$factor_phid] = $result;
$challenge_map[$factor_phid] = $issued_challenges;
continue;
}
foreach ($new_challenges as $new_challenge) {
$issued_challenges[] = $new_challenge;
}
@ -541,7 +578,10 @@ final class PhabricatorAuthSessionEngine extends Phobject {
continue;
}
$ok = false;
if (!$result->getIsValid()) {
$ok = false;
}
$validation_results[$factor_phid] = $result;
}
@ -552,7 +592,18 @@ final class PhabricatorAuthSessionEngine extends Phobject {
// Limit factor verification rates to prevent brute force attacks.
$any_attempt = false;
foreach ($factors as $factor) {
$impl = $factor->requireImplementation();
$factor_phid = $factor->getPHID();
$provider = $factor->getFactorProvider();
$impl = $provider->getFactor();
// If we already have a result (normally "wait..."), we won't try
// to validate whatever the user submitted, so this doesn't count as
// an attempt for rate limiting purposes.
if (isset($validation_results[$factor_phid])) {
continue;
}
if ($impl->getRequestHasChallengeResponse($factor, $request)) {
$any_attempt = true;
break;
@ -577,7 +628,8 @@ final class PhabricatorAuthSessionEngine extends Phobject {
$issued_challenges = idx($challenge_map, $factor_phid, array());
$impl = $factor->requireImplementation();
$provider = $factor->getFactorProvider();
$impl = $provider->getFactor();
$validation_result = $impl->getResultFromChallengeResponse(
$factor,
@ -716,7 +768,10 @@ final class PhabricatorAuthSessionEngine extends Phobject {
foreach ($factors as $factor) {
$result = $validation_results[$factor->getPHID()];
$factor->requireImplementation()->renderValidateFactorForm(
$provider = $factor->getFactorProvider();
$impl = $provider->getFactor();
$impl->renderValidateFactorForm(
$factor,
$form,
$viewer,

View file

@ -3,10 +3,12 @@
abstract class PhabricatorAuthFactor extends Phobject {
abstract public function getFactorName();
abstract public function getFactorShortName();
abstract public function getFactorKey();
abstract public function getFactorCreateHelp();
abstract public function getFactorDescription();
abstract public function processAddFactorForm(
PhabricatorAuthFactorProvider $provider,
AphrontFormView $form,
AphrontRequest $request,
PhabricatorUser $user);
@ -33,7 +35,7 @@ abstract class PhabricatorAuthFactor extends Phobject {
protected function newConfigForUser(PhabricatorUser $user) {
return id(new PhabricatorAuthFactorConfig())
->setUserPHID($user->getPHID())
->setFactorKey($this->getFactorKey());
->setFactorSecret('');
}
protected function newResult() {
@ -45,6 +47,72 @@ abstract class PhabricatorAuthFactor extends Phobject {
->setIcon('fa-mobile');
}
public function canCreateNewProvider() {
return true;
}
public function getProviderCreateDescription() {
return null;
}
public function canCreateNewConfiguration(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return true;
}
public function getConfigurationCreateDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
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?
*
* If a user has a "contact number" factor configured, they can not modify
* or switch their primary contact number.
*
* @return bool True if this factor should lock contact numbers.
*/
public function isContactNumberFactor() {
return false;
}
abstract public function getEnrollDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user);
public function getEnrollButtonText(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return pht('Continue');
}
public function getFactorOrder() {
return 1000;
}
final public function newSortVector() {
return id(new PhutilSortVector())
->addInt($this->canCreateNewProvider() ? 0 : 1)
->addInt($this->getFactorOrder())
->addString($this->getFactorName());
}
protected function newChallenge(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer) {
@ -70,11 +138,20 @@ abstract class PhabricatorAuthFactor extends Phobject {
$now = PhabricatorTime::getNow();
// Factor implementations may need to perform writes in order to issue
// challenges, particularly push factors like SMS.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$new_challenges = $this->newIssuedChallenges(
$config,
$viewer,
$challenges);
if ($new_challenges instanceof PhabricatorAuthFactorResult) {
unset($unguarded);
return $new_challenges;
}
assert_instances_of($new_challenges, 'PhabricatorAuthChallenge');
foreach ($new_challenges as $new_challenge) {
@ -94,10 +171,10 @@ abstract class PhabricatorAuthFactor extends Phobject {
}
}
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
foreach ($new_challenges as $challenge) {
$challenge->save();
}
foreach ($new_challenges as $challenge) {
$challenge->save();
}
unset($unguarded);
return $new_challenges;
@ -178,6 +255,16 @@ abstract class PhabricatorAuthFactor extends Phobject {
final protected function newAutomaticControl(
PhabricatorAuthFactorResult $result) {
$is_error = $result->getIsError();
if ($is_error) {
return $this->newErrorControl($result);
}
$is_continue = $result->getIsContinue();
if ($is_continue) {
return $this->newContinueControl($result);
}
$is_answered = (bool)$result->getAnsweredChallenge();
if ($is_answered) {
return $this->newAnsweredControl($result);
@ -217,5 +304,255 @@ abstract class PhabricatorAuthFactor extends Phobject {
pht('You responded to this challenge correctly.'));
}
private function newErrorControl(
PhabricatorAuthFactorResult $result) {
$error = $result->getErrorMessage();
$icon = id(new PHUIIconView())
->setIcon('fa-times', 'red');
return id(new PHUIFormTimerControl())
->setIcon($icon)
->appendChild($error)
->setError(pht('Error'));
}
private function newContinueControl(
PhabricatorAuthFactorResult $result) {
$error = $result->getErrorMessage();
$icon = id(new PHUIIconView())
->setIcon('fa-commenting', 'green');
return id(new PHUIFormTimerControl())
->setIcon($icon)
->appendChild($error);
}
/* -( Synchronizing New Factors )------------------------------------------ */
final protected function loadMFASyncToken(
PhabricatorAuthFactorProvider $provider,
AphrontRequest $request,
AphrontFormView $form,
PhabricatorUser $user) {
// If the form included a synchronization key, load the corresponding
// token. The user must synchronize to a key we generated because this
// raises the barrier to theoretical attacks where an attacker might
// provide a known key for factors like TOTP.
// (We store and verify the hash of the key, not the key itself, to limit
// how useful the data in the table is to an attacker.)
$sync_type = PhabricatorAuthMFASyncTemporaryTokenType::TOKENTYPE;
$sync_token = null;
$sync_key = $request->getStr($this->getMFASyncTokenFormKey());
if (strlen($sync_key)) {
$sync_key_digest = PhabricatorHash::digestWithNamedKey(
$sync_key,
PhabricatorAuthMFASyncTemporaryTokenType::DIGEST_KEY);
$sync_token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($user)
->withTokenResources(array($user->getPHID()))
->withTokenTypes(array($sync_type))
->withExpired(false)
->withTokenCodes(array($sync_key_digest))
->executeOne();
}
if (!$sync_token) {
// Don't generate a new sync token if there are too many outstanding
// tokens already. This is mostly relevant for push factors like SMS,
// where generating a token has the side effect of sending a user a
// message.
$outstanding_limit = 10;
$outstanding_tokens = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($user)
->withTokenResources(array($user->getPHID()))
->withTokenTypes(array($sync_type))
->withExpired(false)
->execute();
if (count($outstanding_tokens) > $outstanding_limit) {
throw new Exception(
pht(
'Your account has too many outstanding, incomplete MFA '.
'synchronization attempts. Wait an hour and try again.'));
}
$now = PhabricatorTime::getNow();
$sync_key = Filesystem::readRandomCharacters(32);
$sync_key_digest = PhabricatorHash::digestWithNamedKey(
$sync_key,
PhabricatorAuthMFASyncTemporaryTokenType::DIGEST_KEY);
$sync_ttl = $this->getMFASyncTokenTTL();
$sync_token = id(new PhabricatorAuthTemporaryToken())
->setIsNewTemporaryToken(true)
->setTokenResource($user->getPHID())
->setTokenType($sync_type)
->setTokenCode($sync_key_digest)
->setTokenExpires($now + $sync_ttl);
$properties = $this->newMFASyncTokenProperties(
$provider,
$user);
foreach ($properties as $key => $value) {
$sync_token->setTemporaryTokenProperty($key, $value);
}
$sync_token->save();
}
$form->addHiddenInput($this->getMFASyncTokenFormKey(), $sync_key);
return $sync_token;
}
protected function newMFASyncTokenProperties(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return array();
}
private function getMFASyncTokenFormKey() {
return 'sync.key';
}
private function getMFASyncTokenTTL() {
return phutil_units('1 hour in seconds');
}
final protected function getChallengeForCurrentContext(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
$session_phid = $viewer->getSession()->getPHID();
$engine = $config->getSessionEngine();
$workflow_key = $engine->getWorkflowKey();
foreach ($challenges as $challenge) {
if ($challenge->getSessionPHID() !== $session_phid) {
continue;
}
if ($challenge->getWorkflowKey() !== $workflow_key) {
continue;
}
if ($challenge->getIsCompleted()) {
continue;
}
if ($challenge->getIsReusedChallenge()) {
continue;
}
return $challenge;
}
return null;
}
/**
* @phutil-external-symbol class QRcode
*/
final protected function newQRCode($uri) {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/phpqrcode/phpqrcode.php';
$lines = QRcode::text($uri);
$total_width = 240;
$cell_size = floor($total_width / count($lines));
$rows = array();
foreach ($lines as $line) {
$cells = array();
for ($ii = 0; $ii < strlen($line); $ii++) {
if ($line[$ii] == '1') {
$color = '#000';
} else {
$color = '#fff';
}
$cells[] = phutil_tag(
'td',
array(
'width' => $cell_size,
'height' => $cell_size,
'style' => 'background: '.$color,
),
'');
}
$rows[] = phutil_tag('tr', array(), $cells);
}
return phutil_tag(
'table',
array(
'style' => 'margin: 24px auto;',
),
$rows);
}
final protected function getInstallDisplayName() {
$uri = PhabricatorEnv::getURI('/');
$uri = new PhutilURI($uri);
return $uri->getDomain();
}
final protected function getChallengeResponseParameterName(
PhabricatorAuthFactorConfig $config) {
return $this->getParameterName($config, 'mfa.response');
}
final protected function getChallengeResponseFromRequest(
PhabricatorAuthFactorConfig $config,
AphrontRequest $request) {
$name = $this->getChallengeResponseParameterName($config);
$value = $request->getStr($name);
$value = (string)$value;
$value = trim($value);
return $value;
}
final protected function hasCSRF(PhabricatorAuthFactorConfig $config) {
$engine = $config->getSessionEngine();
$request = $engine->getRequest();
if (!$request->isHTTPPost()) {
return false;
}
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();
}
}

View file

@ -5,6 +5,8 @@ final class PhabricatorAuthFactorResult
private $answeredChallenge;
private $isWait = false;
private $isError = false;
private $isContinue = false;
private $errorMessage;
private $value;
private $issuedChallenges = array();
@ -44,6 +46,24 @@ final class PhabricatorAuthFactorResult
return $this->isWait;
}
public function setIsError($is_error) {
$this->isError = $is_error;
return $this;
}
public function getIsError() {
return $this->isError;
}
public function setIsContinue($is_continue) {
$this->isContinue = $is_continue;
return $this;
}
public function getIsContinue() {
return $this->isContinue;
}
public function setErrorMessage($error_message) {
$this->errorMessage = $error_message;
return $this;

View file

@ -1,17 +1,18 @@
<?php
final class PhabricatorAuthTOTPKeyTemporaryTokenType
final class PhabricatorAuthMFASyncTemporaryTokenType
extends PhabricatorAuthTemporaryTokenType {
const TOKENTYPE = 'mfa:totp:key';
const TOKENTYPE = 'mfa.sync';
const DIGEST_KEY = 'mfa.sync';
public function getTokenTypeDisplayName() {
return pht('TOTP Synchronization');
return pht('MFA Sync');
}
public function getTokenReadableTypeName(
PhabricatorAuthTemporaryToken $token) {
return pht('TOTP Sync Token');
return pht('MFA Sync Token');
}
}

View file

@ -0,0 +1,802 @@
<?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);
$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'];
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 account is not permitted to access this system.'));
}
// 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(
'Duo has denied you access. Duo status message ("%s"): %s',
$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(),
);
foreach ($push_info as $k => $v) {
$push_info[$k] = rawurlencode($k).'='.rawurlencode($v);
}
$push_info = implode('&', $push_info);
$parameters = array(
'username' => $duo_user,
'factor' => 'push',
'async' => '1',
// Duo allows us to specify a device, or to pass "auto" to have it pick
// the first one. For now, just let it pick.
'device' => 'auto',
// This is a hard-coded prefix for the word "... request" in the Duo UI,
// which defaults to "Login". We could pass richer information from
// workflows here, but it's not very flexible anyway.
'type' => 'Authentication',
'display_username' => $viewer->getUsername(),
'pushinfo' => $push_info,
);
$result = $this->newDuoFuture($provider)
->setMethod('auth', $parameters)
->resolve();
$duo_xaction = $result['response']['txid'];
// The Duo push timeout is 60 seconds. Set our challenge to expire slightly
// more quickly so that we'll re-issue a new challenge before Duo times out.
// This should keep users away from a dead-end where they can't respond to
// Duo but Phabricator won't issue a new challenge yet.
$ttl_seconds = 55;
return array(
$this->newChallenge($config, $viewer)
->setChallengeKey($duo_xaction)
->setChallengeTTL(PhabricatorTime::getNow() + $ttl_seconds),
);
}
protected function newResultFromIssuedChallenges(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
array $challenges) {
$challenge = $this->getChallengeForCurrentContext(
$config,
$viewer,
$challenges);
if ($challenge->getIsAnsweredChallenge()) {
return $this->newResult()
->setAnsweredChallenge($challenge);
}
$provider = $config->getFactorProvider();
$duo_xaction = $challenge->getChallengeKey();
$parameters = array(
'txid' => $duo_xaction,
);
// This endpoint always long-polls, so use a timeout to force it to act
// more asynchronously.
try {
$result = $this->newDuoFuture($provider)
->setHTTPMethod('GET')
->setMethod('auth_status', $parameters)
->setTimeout(5)
->resolve();
$state = $result['response']['result'];
$status = $result['response']['status'];
} catch (HTTPFutureCURLResponseStatus $exception) {
if ($exception->isTimeout()) {
$state = 'waiting';
$status = 'poll';
} else {
throw $exception;
}
}
$now = PhabricatorTime::getNow();
switch ($state) {
case 'allow':
$ttl = PhabricatorTime::getNow()
+ phutil_units('15 minutes in seconds');
$challenge
->markChallengeAsAnswered($ttl);
return $this->newResult()
->setAnsweredChallenge($challenge);
case 'waiting':
// No result yet, we'll render a default state later on.
break;
default:
case 'deny':
if ($status === 'timeout') {
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'This request has timed out because you took too long to '.
'respond.'));
} else {
$wait_duration = ($challenge->getChallengeTTL() - $now) + 1;
return $this->newResult()
->setIsWait(true)
->setErrorMessage(
pht(
'You denied this request. Wait %s second(s) to try again.',
new PhutilNumber($wait_duration)));
}
break;
}
return null;
}
public function renderValidateFactorForm(
PhabricatorAuthFactorConfig $config,
AphrontFormView $form,
PhabricatorUser $viewer,
PhabricatorAuthFactorResult $result) {
$control = $this->newAutomaticControl($result);
if (!$control) {
$result = $this->newResult()
->setIsContinue(true)
->setErrorMessage(
pht(
'A challenge has been sent to your phone. Open the Duo '.
'application and confirm the challenge, then continue.'));
$control = $this->newAutomaticControl($result);
}
$control
->setLabel(pht('Duo'))
->setCaption(pht('Factor Name: %s', $config->getFactorName()));
$form->appendChild($control);
}
public function getRequestHasChallengeResponse(
PhabricatorAuthFactorConfig $config,
AphrontRequest $request) {
$value = $this->getChallengeResponseFromRequest($config, $request);
return (bool)strlen($value);
}
protected function newResultFromChallengeResponse(
PhabricatorAuthFactorConfig $config,
PhabricatorUser $viewer,
AphrontRequest $request,
array $challenges) {
$challenge = $this->getChallengeForCurrentContext(
$config,
$viewer,
$challenges);
$code = $this->getChallengeResponseFromRequest(
$config,
$request);
$result = $this->newResult()
->setValue($code);
if ($challenge->getIsAnsweredChallenge()) {
return $result->setAnsweredChallenge($challenge);
}
if (phutil_hashes_are_identical($code, $challenge->getChallengeKey())) {
$ttl = PhabricatorTime::getNow() + phutil_units('15 minutes in seconds');
$challenge
->markChallengeAsAnswered($ttl);
return $result->setAnsweredChallenge($challenge);
}
if (strlen($code)) {
$error_message = pht('Invalid');
} else {
$error_message = pht('Required');
}
$result->setErrorMessage($error_message);
return $result;
}
private function newDuoFuture(PhabricatorAuthFactorProvider $provider) {
$credential_phid = $provider->getAuthFactorProviderProperty(
self::PROP_CREDENTIAL);
$omnipotent = PhabricatorUser::getOmnipotentUser();
$credential = id(new PassphraseCredentialQuery())
->setViewer($omnipotent)
->withPHIDs(array($credential_phid))
->needSecrets(true)
->executeOne();
if (!$credential) {
throw new Exception(
pht(
'Unable to load Duo API credential ("%s").',
$credential_phid));
}
$duo_key = $credential->getUsername();
$duo_secret = $credential->getSecret();
if (!$duo_secret) {
throw new Exception(
pht(
'Duo API credential ("%s") has no secret key.',
$credential_phid));
}
$duo_host = $provider->getAuthFactorProviderProperty(
self::PROP_HOSTNAME);
self::requireDuoAPIHostname($duo_host);
return id(new PhabricatorDuoFuture())
->setIntegrationKey($duo_key)
->setSecretKey($duo_secret)
->setAPIHostname($duo_host)
->setTimeout(10)
->setHTTPMethod('POST');
}
private function getDuoUsername(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
$mode = $provider->getAuthFactorProviderProperty(self::PROP_USERNAMES);
switch ($mode) {
case 'username':
return $user->getUsername();
case 'email':
return $user->loadPrimaryEmailAddress();
default:
throw new Exception(
pht(
'Duo username pairing mode ("%s") is not supported.',
$mode));
}
}
private function shouldAllowDuoEnrollment(
PhabricatorAuthFactorProvider $provider) {
$mode = $provider->getAuthFactorProviderProperty(self::PROP_ENROLL);
switch ($mode) {
case 'deny':
return false;
case 'allow':
return true;
default:
throw new Exception(
pht(
'Duo enrollment mode ("%s") is not supported.',
$mode));
}
}
private function newDuoConfig(PhabricatorUser $user, $duo_user) {
$config_properties = array(
'duo.username' => $duo_user,
);
$config = $this->newConfigForUser($user)
->setFactorName(pht('Duo (%s)', $duo_user))
->setProperties($config_properties);
return $config;
}
public static function requireDuoAPIHostname($hostname) {
if (preg_match('/\.duosecurity\.com\z/', $hostname)) {
return;
}
throw new Exception(
pht(
'Duo API hostname ("%s") is invalid, hostname must be '.
'"*.duosecurity.com".',
$hostname));
}
}

View file

@ -0,0 +1,401 @@
<?php
final class PhabricatorSMSAuthFactor
extends PhabricatorAuthFactor {
public function getFactorKey() {
return 'sms';
}
public function getFactorName() {
return pht('Text Message (SMS)');
}
public function getFactorShortName() {
return pht('SMS');
}
public function getFactorCreateHelp() {
return pht(
'Allow users to receive a code via SMS.');
}
public function getFactorDescription() {
return pht(
'When you need to authenticate, a text message with a code will '.
'be sent to your phone.');
}
public function getFactorOrder() {
// Sort this factor toward the end of the list because SMS is relatively
// weak.
return 2000;
}
public function isContactNumberFactor() {
return true;
}
public function canCreateNewProvider() {
return $this->isSMSMailerConfigured();
}
public function getProviderCreateDescription() {
$messages = array();
if (!$this->isSMSMailerConfigured()) {
$messages[] = id(new PHUIInfoView())
->setErrors(
array(
pht(
'You have not configured an outbound SMS mailer. You must '.
'configure one before you can set up SMS. See: %s',
phutil_tag(
'a',
array(
'href' => '/config/edit/cluster.mailers/',
),
'cluster.mailers')),
));
}
$messages[] = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
pht(
'SMS is weak, and relatively easy for attackers to compromise. '.
'Strongly consider using a different MFA provider.'),
));
return $messages;
}
public function canCreateNewConfiguration(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
if (!$this->loadUserContactNumber($user)) {
return false;
}
if ($this->loadConfigurationsForProvider($provider, $user)) {
return false;
}
return true;
}
public function getConfigurationCreateDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
$messages = array();
if (!$this->loadUserContactNumber($user)) {
$messages[] = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setErrors(
array(
pht(
'You have not configured a primary contact number. Configure '.
'a contact number before adding SMS as an authentication '.
'factor.'),
));
}
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;
}
public function getEnrollDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return pht(
'To verify your phone as an authentication factor, a text message with '.
'a secret code will be sent to the phone number you have listed as '.
'your primary contact number.');
}
public function getEnrollButtonText(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
$contact_number = $this->loadUserContactNumber($user);
return pht('Send SMS: %s', $contact_number->getDisplayName());
}
public function processAddFactorForm(
PhabricatorAuthFactorProvider $provider,
AphrontFormView $form,
AphrontRequest $request,
PhabricatorUser $user) {
$token = $this->loadMFASyncToken($provider, $request, $form, $user);
$code = $request->getStr('sms.code');
$e_code = true;
if (!$token->getIsNewTemporaryToken()) {
$expect_code = $token->getTemporaryTokenProperty('code');
$okay = phutil_hashes_are_identical(
$this->normalizeSMSCode($code),
$this->normalizeSMSCode($expect_code));
if ($okay) {
$config = $this->newConfigForUser($user)
->setFactorName(pht('SMS'));
return $config;
} else {
if (!strlen($code)) {
$e_code = pht('Required');
} else {
$e_code = pht('Invalid');
}
}
}
$form->appendRemarkupInstructions(
pht(
'Enter the code from the text message which was sent to your '.
'primary contact number.'));
$form->appendChild(
id(new PHUIFormNumberControl())
->setLabel(pht('SMS Code'))
->setName('sms.code')
->setValue($code)
->setError($e_code));
}
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->loadUserContactNumber($viewer)) {
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'Your account has no primary contact number.'));
}
if (!$this->isSMSMailerConfigured()) {
return $this->newResult()
->setIsError(true)
->setErrorMessage(
pht(
'No outbound mailer which can deliver SMS messages is '.
'configured.'));
}
if (!$this->hasCSRF($config)) {
return $this->newResult()
->setIsContinue(true)
->setErrorMessage(
pht(
'A text message with an authorization code will be sent to your '.
'primary contact number.'));
}
// Otherwise, issue a new challenge.
$challenge_code = $this->newSMSChallengeCode();
$envelope = new PhutilOpaqueEnvelope($challenge_code);
$this->sendSMSCodeToUser($envelope, $viewer);
$ttl_seconds = phutil_units('15 minutes in seconds');
return array(
$this->newChallenge($config, $viewer)
->setChallengeKey($challenge_code)
->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);
}
return null;
}
public function renderValidateFactorForm(
PhabricatorAuthFactorConfig $config,
AphrontFormView $form,
PhabricatorUser $viewer,
PhabricatorAuthFactorResult $result) {
$control = $this->newAutomaticControl($result);
if (!$control) {
$value = $result->getValue();
$error = $result->getErrorMessage();
$name = $this->getChallengeResponseParameterName($config);
$control = id(new PHUIFormNumberControl())
->setName($name)
->setDisableAutocomplete(true)
->setValue($value)
->setError($error);
}
$control
->setLabel(pht('SMS Code'))
->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 newSMSChallengeCode() {
$value = Filesystem::readRandomInteger(0, 99999999);
$value = sprintf('%08d', $value);
return $value;
}
private function isSMSMailerConfigured() {
$mailers = PhabricatorMetaMTAMail::newMailers(
array(
'outbound' => true,
'media' => array(
PhabricatorMailSMSMessage::MESSAGETYPE,
),
));
return (bool)$mailers;
}
private function loadUserContactNumber(PhabricatorUser $user) {
$contact_numbers = id(new PhabricatorAuthContactNumberQuery())
->setViewer($user)
->withObjectPHIDs(array($user->getPHID()))
->withStatuses(
array(
PhabricatorAuthContactNumber::STATUS_ACTIVE,
))
->withIsPrimary(true)
->execute();
if (count($contact_numbers) !== 1) {
return null;
}
return head($contact_numbers);
}
protected function newMFASyncTokenProperties(
PhabricatorAuthFactorProvider $providerr,
PhabricatorUser $user) {
$sms_code = $this->newSMSChallengeCode();
$envelope = new PhutilOpaqueEnvelope($sms_code);
$this->sendSMSCodeToUser($envelope, $user);
return array(
'code' => $sms_code,
);
}
private function sendSMSCodeToUser(
PhutilOpaqueEnvelope $envelope,
PhabricatorUser $user) {
return id(new PhabricatorMetaMTAMail())
->setMessageType(PhabricatorMailSMSMessage::MESSAGETYPE)
->addTos(array($user->getPHID()))
->setForceDelivery(true)
->setSensitiveContent(true)
->setBody(
pht(
'Phabricator (%s) MFA Code: %s',
$this->getInstallDisplayName(),
$envelope->openEnvelope()))
->save();
}
private function normalizeSMSCode($code) {
return trim($code);
}
}

View file

@ -2,8 +2,6 @@
final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
const DIGEST_TEMPORARY_KEY = 'mfa.totp.sync';
public function getFactorKey() {
return 'totp';
}
@ -12,6 +10,10 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
return pht('Mobile Phone App (TOTP)');
}
public function getFactorShortName() {
return pht('TOTP');
}
public function getFactorCreateHelp() {
return pht(
'Allow users to attach a mobile authenticator application (like '.
@ -25,73 +27,57 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
'authenticate, you will enter a code shown on your phone.');
}
public function getEnrollDescription(
PhabricatorAuthFactorProvider $provider,
PhabricatorUser $user) {
return pht(
'To add a TOTP factor to your account, you will first need to install '.
'a mobile authenticator application on your phone. Two applications '.
'which work well are **Google Authenticator** and **Authy**, but any '.
'other TOTP application should also work.'.
"\n\n".
'If you haven\'t already, download and install a TOTP application on '.
'your phone now. Once you\'ve launched the application and are ready '.
'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(
PhabricatorAuthFactorProvider $provider,
AphrontFormView $form,
AphrontRequest $request,
PhabricatorUser $user) {
$totp_token_type = PhabricatorAuthTOTPKeyTemporaryTokenType::TOKENTYPE;
$key = $request->getStr('totpkey');
if (strlen($key)) {
// If the user is providing a key, make sure it's a key we generated.
// This raises the barrier to theoretical attacks where an attacker might
// provide a known key (such attacks are already prevented by CSRF, but
// this is a second barrier to overcome).
// (We store and verify the hash of the key, not the key itself, to limit
// how useful the data in the table is to an attacker.)
$token_code = PhabricatorHash::digestWithNamedKey(
$key,
self::DIGEST_TEMPORARY_KEY);
$temporary_token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($user)
->withTokenResources(array($user->getPHID()))
->withTokenTypes(array($totp_token_type))
->withExpired(false)
->withTokenCodes(array($token_code))
->executeOne();
if (!$temporary_token) {
// If we don't have a matching token, regenerate the key below.
$key = null;
}
}
if (!strlen($key)) {
$key = self::generateNewTOTPKey();
// Mark this key as one we generated, so the user is allowed to submit
// a response for it.
$token_code = PhabricatorHash::digestWithNamedKey(
$key,
self::DIGEST_TEMPORARY_KEY);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
id(new PhabricatorAuthTemporaryToken())
->setTokenResource($user->getPHID())
->setTokenType($totp_token_type)
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
->setTokenCode($token_code)
->save();
unset($unguarded);
}
$sync_token = $this->loadMFASyncToken(
$provider,
$request,
$form,
$user);
$secret = $sync_token->getTemporaryTokenProperty('secret');
$code = $request->getStr('totpcode');
$e_code = true;
if ($request->getExists('totp')) {
if (!$sync_token->getIsNewTemporaryToken()) {
$okay = (bool)$this->getTimestepAtWhichResponseIsValid(
$this->getAllowedTimesteps($this->getCurrentTimestep()),
new PhutilOpaqueEnvelope($key),
new PhutilOpaqueEnvelope($secret),
$code);
if ($okay) {
$config = $this->newConfigForUser($user)
->setFactorName(pht('Mobile App (TOTP)'))
->setFactorSecret($key);
->setFactorSecret($secret)
->setMFASyncToken($sync_token);
return $config;
} else {
@ -103,20 +89,10 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
}
}
$form->addHiddenInput('totp', true);
$form->addHiddenInput('totpkey', $key);
$form->appendRemarkupInstructions(
pht(
'First, download an authenticator application on your phone. Two '.
'applications which work well are **Authy** and **Google '.
'Authenticator**, but any other TOTP application should also work.'));
$form->appendInstructions(
pht(
'Launch the application on your phone, and add a new entry for '.
'this Phabricator install. When prompted, scan the QR code or '.
'manually enter the key shown below into the application.'));
'Scan the QR code or manually enter the key shown below into the '.
'application.'));
$prod_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/'));
$issuer = $prod_uri->getDomain();
@ -125,16 +101,16 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
'otpauth://totp/%s:%s?secret=%s&issuer=%s',
$issuer,
$user->getUsername(),
$key,
$secret,
$issuer);
$qrcode = $this->renderQRCode($uri);
$qrcode = $this->newQRCode($uri);
$form->appendChild($qrcode);
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Key'))
->setValue(phutil_tag('strong', array(), $key)));
->setValue(phutil_tag('strong', array(), $secret)));
$form->appendInstructions(
pht(
@ -428,49 +404,6 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
return $code;
}
/**
* @phutil-external-symbol class QRcode
*/
private function renderQRCode($uri) {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/externals/phpqrcode/phpqrcode.php';
$lines = QRcode::text($uri);
$total_width = 240;
$cell_size = floor($total_width / count($lines));
$rows = array();
foreach ($lines as $line) {
$cells = array();
for ($ii = 0; $ii < strlen($line); $ii++) {
if ($line[$ii] == '1') {
$color = '#000';
} else {
$color = '#fff';
}
$cells[] = phutil_tag(
'td',
array(
'width' => $cell_size,
'height' => $cell_size,
'style' => 'background: '.$color,
),
'');
}
$rows[] = phutil_tag('tr', array(), $cells);
}
return phutil_tag(
'table',
array(
'style' => 'margin: 24px auto;',
),
$rows);
}
private function getTimestepDuration() {
return 30;
}
@ -508,21 +441,12 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor {
return null;
}
private function getChallengeResponseParameterName(
PhabricatorAuthFactorConfig $config) {
return $this->getParameterName($config, 'totpcode');
protected function newMFASyncTokenProperties(
PhabricatorAuthFactorProvider $providerr,
PhabricatorUser $user) {
return array(
'secret' => self::generateNewTOTPKey(),
);
}
private function getChallengeResponseFromRequest(
PhabricatorAuthFactorConfig $config,
AphrontRequest $request) {
$name = $this->getChallengeResponseParameterName($config);
$value = $request->getStr($name);
$value = (string)$value;
$value = trim($value);
return $value;
}
}

View file

@ -0,0 +1,155 @@
<?php
final class PhabricatorDuoFuture
extends FutureProxy {
private $future;
private $integrationKey;
private $secretKey;
private $apiHostname;
private $httpMethod = 'POST';
private $method;
private $parameters;
private $timeout;
public function __construct() {
parent::__construct(null);
}
public function setIntegrationKey($integration_key) {
$this->integrationKey = $integration_key;
return $this;
}
public function setSecretKey(PhutilOpaqueEnvelope $key) {
$this->secretKey = $key;
return $this;
}
public function setAPIHostname($hostname) {
$this->apiHostname = $hostname;
return $this;
}
public function setMethod($method, array $parameters) {
$this->method = $method;
$this->parameters = $parameters;
return $this;
}
public function setTimeout($timeout) {
$this->timeout = $timeout;
return $this;
}
public function getTimeout() {
return $this->timeout;
}
public function setHTTPMethod($method) {
$this->httpMethod = $method;
return $this;
}
public function getHTTPMethod() {
return $this->httpMethod;
}
protected function getProxiedFuture() {
if (!$this->future) {
if ($this->integrationKey === null) {
throw new PhutilInvalidStateException('setIntegrationKey');
}
if ($this->secretKey === null) {
throw new PhutilInvalidStateException('setSecretKey');
}
if ($this->apiHostname === null) {
throw new PhutilInvalidStateException('setAPIHostname');
}
if ($this->method === null || $this->parameters === null) {
throw new PhutilInvalidStateException('setMethod');
}
$path = (string)urisprintf('/auth/v2/%s', $this->method);
$host = $this->apiHostname;
$host = phutil_utf8_strtolower($host);
$uri = id(new PhutilURI(''))
->setProtocol('https')
->setDomain($host)
->setPath($path);
$data = $this->parameters;
$date = date('r');
$http_method = $this->getHTTPMethod();
ksort($data);
$data_parts = array();
foreach ($data as $key => $value) {
$data_parts[] = rawurlencode($key).'='.rawurlencode($value);
}
$data_parts = implode('&', $data_parts);
$corpus = array(
$date,
$http_method,
$host,
$path,
$data_parts,
);
$corpus = implode("\n", $corpus);
$signature = hash_hmac(
'sha1',
$corpus,
$this->secretKey->openEnvelope());
$signature = new PhutilOpaqueEnvelope($signature);
if ($http_method === 'GET') {
$uri->setQueryParams($data);
$data = array();
}
$future = id(new HTTPSFuture($uri, $data))
->setHTTPBasicAuthCredentials($this->integrationKey, $signature)
->setMethod($http_method)
->addHeader('Accept', 'application/json')
->addHeader('Date', $date);
$timeout = $this->getTimeout();
if ($timeout) {
$future->setTimeout($timeout);
}
$this->future = $future;
}
return $this->future;
}
protected function didReceiveResult($result) {
list($status, $body, $headers) = $result;
if ($status->isError()) {
throw $status;
}
try {
$data = phutil_json_decode($body);
} catch (PhutilJSONParserException $ex) {
throw new PhutilProxyException(
pht('Expected JSON response from Duo.'),
$ex);
}
return $data;
}
}

View file

@ -14,9 +14,8 @@ final class PhabricatorAuthManagementListFactorsWorkflow
public function execute(PhutilArgumentParser $args) {
$factors = PhabricatorAuthFactor::getAllFactors();
$console = PhutilConsole::getConsole();
foreach ($factors as $factor) {
$console->writeOut(
echo tsprintf(
"%s\t%s\n",
$factor->getFactorKey(),
$factor->getFactorName());

View file

@ -0,0 +1,33 @@
<?php
final class PhabricatorAuthManagementListMFAProvidersWorkflow
extends PhabricatorAuthManagementWorkflow {
protected function didConstruct() {
$this
->setName('list-mfa-providers')
->setExamples('**list-mfa-providerrs**')
->setSynopsis(
pht(
'List available multi-factor authentication providers.'))
->setArguments(array());
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$providers = id(new PhabricatorAuthFactorProviderQuery())
->setViewer($viewer)
->execute();
foreach ($providers as $provider) {
echo tsprintf(
"%s\t%s\n",
$provider->getPHID(),
$provider->getDisplayName());
}
return 0;
}
}

View file

@ -24,12 +24,22 @@ final class PhabricatorAuthManagementStripWorkflow
'name' => 'type',
'param' => 'factortype',
'repeat' => true,
'help' => pht('Strip a specific factor type.'),
'help' => pht(
'Strip a specific factor type. Use `bin/auth list-factors` for '.
'a list of factor types.'),
),
array(
'name' => 'all-types',
'help' => pht('Strip all factors, regardless of type.'),
),
array(
'name' => 'provider',
'param' => 'phid',
'repeat' => true,
'help' => pht(
'Strip factors for a specific provider. Use '.
'`bin/auth list-mfa-providers` for a list of providers.'),
),
array(
'name' => 'force',
'help' => pht('Strip factors without prompting.'),
@ -42,6 +52,8 @@ final class PhabricatorAuthManagementStripWorkflow
}
public function execute(PhutilArgumentParser $args) {
$viewer = $this->getViewer();
$usernames = $args->getArg('user');
$all_users = $args->getArg('all-users');
@ -55,10 +67,8 @@ final class PhabricatorAuthManagementStripWorkflow
} else if (!$usernames && !$all_users) {
throw new PhutilArgumentUsageException(
pht(
'Use %s to specify which user to strip factors from, or '.
'%s to strip factors from all users.',
'--user',
'--all-users'));
'Use "--user <username>" to specify which user to strip factors '.
'from, or "--all-users" to strip factors from all users.'));
} else if ($usernames) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
@ -79,37 +89,83 @@ final class PhabricatorAuthManagementStripWorkflow
}
$types = $args->getArg('type');
$provider_phids = $args->getArg('provider');
$all_types = $args->getArg('all-types');
if ($types && $all_types) {
throw new PhutilArgumentUsageException(
pht(
'Specify either specific factors with --type, or all factors with '.
'--all-types, but not both.'));
} else if (!$types && !$all_types) {
'Specify either specific factors with "--type", or all factors with '.
'"--all-types", but not both.'));
} else if ($provider_phids && $all_types) {
throw new PhutilArgumentUsageException(
pht(
'Use --type to specify which factor to strip, or --all-types to '.
'strip all factors. Use `auth list-factors` to show the available '.
'factor types.'));
'Specify either specific factors with "--provider", or all factors '.
'with "--all-types", but not both.'));
} else if (!$types && !$all_types && !$provider_phids) {
throw new PhutilArgumentUsageException(
pht(
'Use "--type <type>" or "--provider <phid>" to specify which '.
'factors to strip, or "--all-types" to strip all factors. '.
'Use `bin/auth list-factors` to show the available factor types '.
'or `bin/auth list-mfa-providers` to show available providers.'));
}
if ($users && $types) {
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID IN (%Ls) AND factorKey IN (%Ls)',
mpull($users, 'getPHID'),
$types);
} else if ($users) {
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'userPHID IN (%Ls)',
mpull($users, 'getPHID'));
} else if ($types) {
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
'factorKey IN (%Ls)',
$types);
} else {
$factors = id(new PhabricatorAuthFactorConfig())->loadAll();
$type_map = PhabricatorAuthFactor::getAllFactors();
if ($types) {
foreach ($types as $type) {
if (!isset($type_map[$type])) {
throw new PhutilArgumentUsageException(
pht(
'Factor type "%s" is unknown. Use `bin/auth list-factors` to '.
'get a list of known factor types.',
$type));
}
}
}
$provider_query = id(new PhabricatorAuthFactorProviderQuery())
->setViewer($viewer);
if ($provider_phids) {
$provider_query->withPHIDs($provider_phids);
}
if ($types) {
$provider_query->withProviderFactorKeys($types);
}
$providers = $provider_query->execute();
$providers = mpull($providers, null, 'getPHID');
if ($provider_phids) {
foreach ($provider_phids as $provider_phid) {
if (!isset($providers[$provider_phid])) {
throw new PhutilArgumentUsageException(
pht(
'No provider with PHID "%s" exists. '.
'Use `bin/auth list-mfa-providers` to list providers.',
$provider_phid));
}
}
} else {
if (!$providers) {
throw new PhutilArgumentUsageException(
pht(
'There are no configured multi-factor providers.'));
}
}
$factor_query = id(new PhabricatorAuthFactorConfigQuery())
->setViewer($viewer)
->withFactorProviderPHIDs(array_keys($providers));
if ($users) {
$factor_query->withUserPHIDs(mpull($users, 'getPHID'));
}
$factors = $factor_query->execute();
if (!$factors) {
throw new PhutilArgumentUsageException(
pht('There are no matching factors to strip.'));
@ -125,14 +181,13 @@ final class PhabricatorAuthManagementStripWorkflow
$console->writeOut("%s\n\n", pht('These auth factors will be stripped:'));
foreach ($factors as $factor) {
$impl = $factor->getImplementation();
$console->writeOut(
$provider = $factor->getFactorProvider();
echo tsprintf(
" %s\t%s\t%s\n",
$handles[$factor->getUserPHID()]->getName(),
$factor->getFactorKey(),
($impl
? $impl->getFactorName()
: '?'));
$provider->getProviderFactorKey(),
$provider->getDisplayName());
}
$is_dry_run = $args->getArg('dry-run');
@ -154,17 +209,9 @@ final class PhabricatorAuthManagementStripWorkflow
$console->writeOut("%s\n", pht('Stripping authentication factors...'));
$engine = new PhabricatorDestructionEngine();
foreach ($factors as $factor) {
$user = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withPHIDs(array($factor->getUserPHID()))
->executeOne();
$factor->delete();
if ($user) {
$user->updateMultiFactorEnrollment();
}
$engine->destroyObject($factor);
}
$console->writeOut("%s\n", pht('Done.'));

View file

@ -0,0 +1,38 @@
<?php
final class PhabricatorAuthContactNumberPHIDType
extends PhabricatorPHIDType {
const TYPECONST = 'CTNM';
public function getTypeName() {
return pht('Contact Number');
}
public function newObject() {
return new PhabricatorAuthContactNumber();
}
public function getPHIDTypeApplicationClass() {
return 'PhabricatorAuthApplication';
}
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return id(new PhabricatorAuthContactNumberQuery())
->withPHIDs($phids);
}
public function loadHandles(
PhabricatorHandleQuery $query,
array $handles,
array $objects) {
foreach ($handles as $phid => $handle) {
$contact_number = $objects[$phid];
}
}
}

View file

@ -19,7 +19,9 @@ final class PhabricatorAuthMessagePHIDType extends PhabricatorPHIDType {
protected function buildQueryForObjects(
PhabricatorObjectQuery $query,
array $phids) {
return new PhabricatorAuthMessageQuery();
return id(new PhabricatorAuthMessageQuery())
->withPHIDs($phids);
}
public function loadHandles(

View file

@ -0,0 +1,103 @@
<?php
final class PhabricatorAuthContactNumberQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $objectPHIDs;
private $statuses;
private $uniqueKeys;
private $isPrimary;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withUniqueKeys(array $unique_keys) {
$this->uniqueKeys = $unique_keys;
return $this;
}
public function withIsPrimary($is_primary) {
$this->isPrimary = $is_primary;
return $this;
}
public function newResultObject() {
return new PhabricatorAuthContactNumber();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'phid IN (%Ls)',
$this->phids);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn,
'objectPHID IN (%Ls)',
$this->objectPHIDs);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'status IN (%Ls)',
$this->statuses);
}
if ($this->uniqueKeys !== null) {
$where[] = qsprintf(
$conn,
'uniqueKey IN (%Ls)',
$this->uniqueKeys);
}
if ($this->isPrimary !== null) {
$where[] = qsprintf(
$conn,
'isPrimary = %d',
(int)$this->isPrimary);
}
return $where;
}
public function getQueryApplicationClass() {
return 'PhabricatorAuthApplication';
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhabricatorAuthContactNumberTransactionQuery
extends PhabricatorApplicationTransactionQuery {
public function getTemplateApplicationTransaction() {
return new PhabricatorAuthContactNumberTransaction();
}
}

View file

@ -0,0 +1,131 @@
<?php
final class PhabricatorAuthFactorConfigQuery
extends PhabricatorCursorPagedPolicyAwareQuery {
private $ids;
private $phids;
private $userPHIDs;
private $factorProviderPHIDs;
private $factorProviderStatuses;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->phids = $phids;
return $this;
}
public function withUserPHIDs(array $user_phids) {
$this->userPHIDs = $user_phids;
return $this;
}
public function withFactorProviderPHIDs(array $provider_phids) {
$this->factorProviderPHIDs = $provider_phids;
return $this;
}
public function withFactorProviderStatuses(array $statuses) {
$this->factorProviderStatuses = $statuses;
return $this;
}
public function newResultObject() {
return new PhabricatorAuthFactorConfig();
}
protected function loadPage() {
return $this->loadStandardPage($this->newResultObject());
}
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn,
'config.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn,
'config.phid IN (%Ls)',
$this->phids);
}
if ($this->userPHIDs !== null) {
$where[] = qsprintf(
$conn,
'config.userPHID IN (%Ls)',
$this->userPHIDs);
}
if ($this->factorProviderPHIDs !== null) {
$where[] = qsprintf(
$conn,
'config.factorProviderPHID IN (%Ls)',
$this->factorProviderPHIDs);
}
if ($this->factorProviderStatuses !== null) {
$where[] = qsprintf(
$conn,
'provider.status IN (%Ls)',
$this->factorProviderStatuses);
}
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) {
$provider_phids = mpull($configs, 'getFactorProviderPHID');
$providers = id(new PhabricatorAuthFactorProviderQuery())
->setViewer($this->getViewer())
->withPHIDs($provider_phids)
->execute();
$providers = mpull($providers, null, 'getPHID');
foreach ($configs as $key => $config) {
$provider = idx($providers, $config->getFactorProviderPHID());
if (!$provider) {
unset($configs[$key]);
$this->didRejectResult($config);
continue;
}
$config->attachFactorProvider($provider);
}
return $configs;
}
protected function getPrimaryTableAlias() {
return 'config';
}
public function getQueryApplicationClass() {
return 'PhabricatorAuthApplication';
}
}

View file

@ -5,6 +5,8 @@ final class PhabricatorAuthFactorProviderQuery
private $ids;
private $phids;
private $statuses;
private $providerFactorKeys;
public function withIDs(array $ids) {
$this->ids = $ids;
@ -15,6 +17,17 @@ final class PhabricatorAuthFactorProviderQuery
$this->phids = $phids;
return $this;
}
public function withProviderFactorKeys(array $keys) {
$this->providerFactorKeys = $keys;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function newResultObject() {
return new PhabricatorAuthFactorProvider();
}
@ -40,6 +53,20 @@ final class PhabricatorAuthFactorProviderQuery
$this->phids);
}
if ($this->statuses !== null) {
$where[] = qsprintf(
$conn,
'status IN (%Ls)',
$this->statuses);
}
if ($this->providerFactorKeys !== null) {
$where[] = qsprintf(
$conn,
'providerFactorKey IN (%Ls)',
$this->providerFactorKeys);
}
return $where;
}

View file

@ -163,10 +163,16 @@ final class PhabricatorAuthChallenge
$token = Filesystem::readRandomCharacters(32);
$token = new PhutilOpaqueEnvelope($token);
return $this
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$this
->setResponseToken($token)
->setResponseTTL($ttl)
->save();
unset($unguarded);
return $this;
}
public function markChallengeAsCompleted() {

View file

@ -0,0 +1,243 @@
<?php
final class PhabricatorAuthContactNumber
extends PhabricatorAuthDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface,
PhabricatorEditEngineMFAInterface {
protected $objectPHID;
protected $contactNumber;
protected $uniqueKey;
protected $status;
protected $isPrimary;
protected $properties = array();
const STATUS_ACTIVE = 'active';
const STATUS_DISABLED = 'disabled';
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'contactNumber' => 'text255',
'status' => 'text32',
'uniqueKey' => 'bytes12?',
'isPrimary' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_object' => array(
'columns' => array('objectPHID'),
),
'key_unique' => array(
'columns' => array('uniqueKey'),
'unique' => true,
),
),
) + parent::getConfiguration();
}
public static function initializeNewContactNumber($object) {
return id(new self())
->setStatus(self::STATUS_ACTIVE)
->setObjectPHID($object->getPHID())
->setIsPrimary(0);
}
public function getPHIDType() {
return PhabricatorAuthContactNumberPHIDType::TYPECONST;
}
public function getURI() {
return urisprintf('/auth/contact/%s/', $this->getID());
}
public function getObjectName() {
return pht('Contact Number %d', $this->getID());
}
public function getDisplayName() {
return $this->getContactNumber();
}
public function isDisabled() {
return ($this->getStatus() === self::STATUS_DISABLED);
}
public function newIconView() {
if ($this->isDisabled()) {
return id(new PHUIIconView())
->setIcon('fa-ban', 'grey')
->setTooltip(pht('Disabled'));
}
if ($this->getIsPrimary()) {
return id(new PHUIIconView())
->setIcon('fa-certificate', 'blue')
->setTooltip(pht('Primary Number'));
}
return id(new PHUIIconView())
->setIcon('fa-hashtag', 'bluegrey')
->setTooltip(pht('Active Phone Number'));
}
public function newUniqueKey() {
$parts = array(
// This is future-proofing for a world where we have multiple types
// of contact numbers, so we might be able to avoid re-hashing
// everything.
'phone',
$this->getContactNumber(),
);
$parts = implode("\0", $parts);
return PhabricatorHash::digestForIndex($parts);
}
public function save() {
// We require that active contact numbers be unique, but it's okay to
// disable a number and then reuse it somewhere else.
if ($this->isDisabled()) {
$this->uniqueKey = null;
} else {
$this->uniqueKey = $this->newUniqueKey();
}
parent::save();
return $this->updatePrimaryContactNumber();
}
private function updatePrimaryContactNumber() {
// Update the "isPrimary" column so that at most one number is primary for
// each user, and no disabled number is primary.
$conn = $this->establishConnection('w');
$this_id = (int)$this->getID();
if ($this->getIsPrimary() && !$this->isDisabled()) {
// If we're trying to make this number primary and it's active, great:
// make this number the primary number.
$primary_id = $this_id;
} else {
// If we aren't trying to make this number primary or it is disabled,
// pick another number to make primary if we can. A number must be active
// to become primary.
// If there are multiple active numbers, pick the oldest one currently
// marked primary (usually, this should mean that we just keep the
// current primary number as primary).
// If none are marked primary, just pick the oldest one.
$primary_row = queryfx_one(
$conn,
'SELECT id FROM %R
WHERE objectPHID = %s AND status = %s
ORDER BY isPrimary DESC, id ASC
LIMIT 1',
$this,
$this->getObjectPHID(),
self::STATUS_ACTIVE);
if ($primary_row) {
$primary_id = (int)$primary_row['id'];
} else {
$primary_id = -1;
}
}
// Set the chosen number to primary, and all other numbers to nonprimary.
queryfx(
$conn,
'UPDATE %R SET isPrimary = IF(id = %d, 1, 0)
WHERE objectPHID = %s',
$this,
$primary_id,
$this->getObjectPHID());
$this->setIsPrimary((int)($primary_id === $this_id));
return $this;
}
public static function getStatusNameMap() {
return ipull(self::getStatusPropertyMap(), 'name');
}
private static function getStatusPropertyMap() {
return array(
self::STATUS_ACTIVE => array(
'name' => pht('Active'),
),
self::STATUS_DISABLED => array(
'name' => pht('Disabled'),
),
);
}
public function getSortVector() {
// Sort the primary number first, then active numbers, then disabled
// numbers. In each group, sort from oldest to newest.
return id(new PhutilSortVector())
->addInt($this->getIsPrimary() ? 0 : 1)
->addInt($this->isDisabled() ? 1 : 0)
->addInt($this->getID());
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getObjectPHID();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->delete();
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorAuthContactNumberEditor();
}
public function getApplicationTransactionTemplate() {
return new PhabricatorAuthContactNumberTransaction();
}
/* -( PhabricatorEditEngineMFAInterface )---------------------------------- */
public function newEditEngineMFAEngine() {
return new PhabricatorAuthContactNumberMFAEngine();
}
}

View file

@ -0,0 +1,18 @@
<?php
final class PhabricatorAuthContactNumberTransaction
extends PhabricatorModularTransaction {
public function getApplicationName() {
return 'auth';
}
public function getApplicationTransactionType() {
return PhabricatorAuthContactNumberPHIDType::TYPECONST;
}
public function getBaseTransactionClass() {
return 'PhabricatorAuthContactNumberTransactionType';
}
}

View file

@ -1,14 +1,21 @@
<?php
final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO {
final class PhabricatorAuthFactorConfig
extends PhabricatorAuthDAO
implements
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
protected $userPHID;
protected $factorKey;
protected $factorProviderPHID;
protected $factorName;
protected $factorSecret;
protected $properties = array();
private $sessionEngine;
private $factorProvider = self::ATTACHABLE;
private $mfaSyncToken;
protected function getConfiguration() {
return array(
@ -17,7 +24,6 @@ final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO {
),
self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'factorKey' => 'text64',
'factorName' => 'text',
'factorSecret' => 'text',
),
@ -29,26 +35,18 @@ final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO {
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorAuthAuthFactorPHIDType::TYPECONST);
public function getPHIDType() {
return PhabricatorAuthAuthFactorPHIDType::TYPECONST;
}
public function getImplementation() {
return idx(PhabricatorAuthFactor::getAllFactors(), $this->getFactorKey());
public function attachFactorProvider(
PhabricatorAuthFactorProvider $provider) {
$this->factorProvider = $provider;
return $this;
}
public function requireImplementation() {
$impl = $this->getImplementation();
if (!$impl) {
throw new Exception(
pht(
'Attempting to operate on multi-factor auth which has no '.
'corresponding implementation (factor key is "%s").',
$this->getFactorKey()));
}
return $impl;
public function getFactorProvider() {
return $this->assertAttached($this->factorProvider);
}
public function setSessionEngine(PhabricatorAuthSessionEngine $engine) {
@ -64,4 +62,66 @@ final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO {
return $this->sessionEngine;
}
public function setMFASyncToken(PhabricatorAuthTemporaryToken $token) {
$this->mfaSyncToken = $token;
return $this;
}
public function getMFASyncToken() {
return $this->mfaSyncToken;
}
public function getAuthFactorConfigProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setAuthFactorConfigProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function newSortVector() {
return id(new PhutilSortVector())
->addInt($this->getFactorProvider()->newStatus()->getOrder())
->addInt($this->getID());
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
return $this->getUserPHID();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$user = id(new PhabricatorPeopleQuery())
->setViewer($engine->getViewer())
->withPHIDs(array($this->getUserPHID()))
->executeOne();
$this->delete();
if ($user) {
$user->updateMultiFactorEnrollment();
}
}
}

View file

@ -5,7 +5,8 @@ final class PhabricatorAuthFactorProvider
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorExtendedPolicyInterface {
PhabricatorExtendedPolicyInterface,
PhabricatorEditEngineMFAInterface {
protected $providerFactorKey;
protected $name;
@ -14,15 +15,11 @@ final class PhabricatorAuthFactorProvider
private $factor = self::ATTACHABLE;
const STATUS_ACTIVE = 'active';
const STATUS_DEPRECATED = 'deprecated';
const STATUS_DISABLED = 'disabled';
public static function initializeNewProvider(PhabricatorAuthFactor $factor) {
return id(new self())
->setProviderFactorKey($factor->getFactorKey())
->attachFactor($factor)
->setStatus(self::STATUS_ACTIVE);
->setStatus(PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE);
}
protected function getConfiguration() {
@ -78,6 +75,67 @@ final class PhabricatorAuthFactorProvider
return $this->getFactor()->getFactorName();
}
public function newIconView() {
return $this->getFactor()->newIconView();
}
public function getDisplayDescription() {
return $this->getFactor()->getFactorDescription();
}
public function processAddFactorForm(
AphrontFormView $form,
AphrontRequest $request,
PhabricatorUser $user) {
$factor = $this->getFactor();
$config = $factor->processAddFactorForm($this, $form, $request, $user);
if ($config) {
$config->setFactorProviderPHID($this->getPHID());
}
return $config;
}
public function newSortVector() {
$factor = $this->getFactor();
return id(new PhutilSortVector())
->addInt($factor->getFactorOrder())
->addInt($this->getID());
}
public function getEnrollDescription(PhabricatorUser $user) {
return $this->getFactor()->getEnrollDescription($this, $user);
}
public function getEnrollButtonText(PhabricatorUser $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 )------------------------- */
@ -131,4 +189,11 @@ final class PhabricatorAuthFactorProvider
}
/* -( PhabricatorEditEngineMFAInterface )---------------------------------- */
public function newEditEngineMFAEngine() {
return new PhabricatorAuthFactorProviderMFAEngine();
}
}

View file

@ -10,7 +10,9 @@ final class PhabricatorAuthTemporaryToken extends PhabricatorAuthDAO
protected $tokenExpires;
protected $tokenCode;
protected $userPHID;
protected $properties;
protected $properties = array();
private $isNew = false;
protected function getConfiguration() {
return array(
@ -114,6 +116,14 @@ final class PhabricatorAuthTemporaryToken extends PhabricatorAuthDAO
return $this->getTemporaryTokenProperty('force-full-session', false);
}
public function setIsNewTemporaryToken($is_new) {
$this->isNew = $is_new;
return $this;
}
public function getIsNewTemporaryToken() {
return $this->isNew;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -0,0 +1,96 @@
<?php
final class PhabricatorAuthContactNumberNumberTransaction
extends PhabricatorAuthContactNumberTransactionType {
const TRANSACTIONTYPE = 'number';
public function generateOldValue($object) {
return $object->getContactNumber();
}
public function generateNewValue($object, $value) {
$number = new PhabricatorPhoneNumber($value);
return $number->toE164();
}
public function applyInternalEffects($object, $value) {
$object->setContactNumber($value);
}
public function getTitle() {
$old = $this->getOldValue();
$new = $this->getNewValue();
return pht(
'%s changed this contact number from %s to %s.',
$this->renderAuthor(),
$this->renderOldValue(),
$this->renderNewValue());
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$current_value = $object->getContactNumber();
if ($this->isEmptyTextTransaction($current_value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('Contact numbers must have a contact number.'));
return $errors;
}
$max_length = $object->getColumnMaximumByteLength('contactNumber');
foreach ($xactions as $xaction) {
$new_value = $xaction->getNewValue();
$new_length = strlen($new_value);
if ($new_length > $max_length) {
$errors[] = $this->newInvalidError(
pht(
'Contact numbers can not be longer than %s characters.',
new PhutilNumber($max_length)),
$xaction);
continue;
}
try {
new PhabricatorPhoneNumber($new_value);
} catch (Exception $ex) {
$errors[] = $this->newInvalidError(
pht(
'Contact number is invalid: %s',
$ex->getMessage()),
$xaction);
continue;
}
$new_value = $this->generateNewValue($object, $new_value);
$unique_key = id(clone $object)
->setContactNumber($new_value)
->newUniqueKey();
$other = id(new PhabricatorAuthContactNumberQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUniqueKeys(array($unique_key))
->executeOne();
if ($other) {
if ($other->getID() !== $object->getID()) {
$errors[] = $this->newInvalidError(
pht('Contact number is already in use.'),
$xaction);
continue;
}
}
$mfa_error = $this->newContactNumberMFAError($object, $xaction);
if ($mfa_error) {
$errors[] = $mfa_error;
continue;
}
}
return $errors;
}
}

View file

@ -0,0 +1,55 @@
<?php
final class PhabricatorAuthContactNumberPrimaryTransaction
extends PhabricatorAuthContactNumberTransactionType {
const TRANSACTIONTYPE = 'primary';
public function generateOldValue($object) {
return (bool)$object->getIsPrimary();
}
public function applyInternalEffects($object, $value) {
$object->setIsPrimary((int)$value);
}
public function getTitle() {
return pht(
'%s made this the primary contact number.',
$this->renderAuthor());
}
public function validateTransactions($object, array $xactions) {
$errors = array();
foreach ($xactions as $xaction) {
$new_value = $xaction->getNewValue();
if (!$new_value) {
$errors[] = $this->newInvalidError(
pht(
'To choose a different primary contact number, make that '.
'number primary (instead of trying to demote this one).'),
$xaction);
continue;
}
if ($object->isDisabled()) {
$errors[] = $this->newInvalidError(
pht(
'You can not make a disabled number a primary contact number.'),
$xaction);
continue;
}
$mfa_error = $this->newContactNumberMFAError($object, $xaction);
if ($mfa_error) {
$errors[] = $mfa_error;
continue;
}
}
return $errors;
}
}

View file

@ -0,0 +1,65 @@
<?php
final class PhabricatorAuthContactNumberStatusTransaction
extends PhabricatorAuthContactNumberTransactionType {
const TRANSACTIONTYPE = 'status';
public function generateOldValue($object) {
return $object->getStatus();
}
public function applyInternalEffects($object, $value) {
$object->setStatus($value);
}
public function getTitle() {
$new = $this->getNewValue();
if ($new === PhabricatorAuthContactNumber::STATUS_DISABLED) {
return pht(
'%s disabled this contact number.',
$this->renderAuthor());
} else {
return pht(
'%s enabled this contact number.',
$this->renderAuthor());
}
}
public function validateTransactions($object, array $xactions) {
$errors = array();
$map = PhabricatorAuthContactNumber::getStatusNameMap();
foreach ($xactions as $xaction) {
$new_value = $xaction->getNewValue();
if (!isset($map[$new_value])) {
$errors[] = $this->newInvalidError(
pht(
'Status ("%s") is not a valid contact number status. Valid '.
'status constants are: %s.',
$new_value,
implode(', ', array_keys($map))),
$xaction);
continue;
}
$mfa_error = $this->newContactNumberMFAError($object, $xaction);
if ($mfa_error) {
$errors[] = $mfa_error;
continue;
}
// NOTE: Enabling a contact number may cause us to collide with another
// active contact number. However, there might also be a transaction in
// this group that changes the number itself. Since we can't easily
// predict if we'll collide or not, just let the duplicate key logic
// handle it when we do.
}
return $errors;
}
}

View file

@ -0,0 +1,72 @@
<?php
abstract class PhabricatorAuthContactNumberTransactionType
extends PhabricatorModularTransactionType {
protected function newContactNumberMFAError($object, $xaction) {
// If a contact number is attached to a user and that user has SMS MFA
// configured, don't let the user modify their primary contact number or
// make another contact number into their primary number.
$primary_type =
PhabricatorAuthContactNumberPrimaryTransaction::TRANSACTIONTYPE;
if ($xaction->getTransactionType() === $primary_type) {
// We're trying to make a non-primary number into the primary number,
// so do MFA checks.
$is_primary = false;
} else if ($object->getIsPrimary()) {
// We're editing the primary number, so do MFA checks.
$is_primary = true;
} else {
// Editing a non-primary number and not making it primary, so this is
// fine.
return null;
}
$target_phid = $object->getObjectPHID();
$omnipotent = PhabricatorUser::getOmnipotentUser();
$user_configs = id(new PhabricatorAuthFactorConfigQuery())
->setViewer($omnipotent)
->withUserPHIDs(array($target_phid))
->execute();
$problem_configs = array();
foreach ($user_configs as $config) {
$provider = $config->getFactorProvider();
$factor = $provider->getFactor();
if ($factor->isContactNumberFactor()) {
$problem_configs[] = $config;
}
}
if (!$problem_configs) {
return null;
}
$problem_config = head($problem_configs);
if ($is_primary) {
return $this->newInvalidError(
pht(
'You currently have multi-factor authentication ("%s") which '.
'depends on your primary contact number. You must remove this '.
'authentication factor before you can modify or disable your '.
'primary contact number.',
$problem_config->getFactorName()),
$xaction);
} else {
return $this->newInvalidError(
pht(
'You currently have multi-factor authentication ("%s") which '.
'depends on your primary contact number. You must remove this '.
'authentication factor before you can designate a new primary '.
'contact number.',
$problem_config->getFactorName()),
$xaction);
}
}
}

View file

@ -0,0 +1,65 @@
<?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();
$old_value = $this->generateOldValue($object);
if ($this->isEmptyTextTransaction($old_value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('Duo providers must have an API credential.'));
}
foreach ($xactions as $xaction) {
$new_value = $xaction->getNewValue();
if (!strlen($new_value)) {
continue;
}
if ($new_value === $old_value) {
continue;
}
$credential = id(new PassphraseCredentialQuery())
->setViewer($actor)
->withIsDestroyed(false)
->withPHIDs(array($new_value))
->executeOne();
if (!$credential) {
$errors[] = $this->newInvalidError(
pht(
'Credential ("%s") is not valid.',
$new_value),
$xaction);
continue;
}
}
return $errors;
}
}

View file

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

View file

@ -0,0 +1,59 @@
<?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();
$old_value = $this->generateOldValue($object);
if ($this->isEmptyTextTransaction($old_value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('Duo providers must have an API hostname.'));
}
foreach ($xactions as $xaction) {
$new_value = $xaction->getNewValue();
if (!strlen($new_value)) {
continue;
}
if ($new_value === $old_value) {
continue;
}
try {
PhabricatorDuoAuthFactor::requireDuoAPIHostname($new_value);
} catch (Exception $ex) {
$errors[] = $this->newInvalidError(
$ex->getMessage(),
$xaction);
continue;
}
}
return $errors;
}
}

View file

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

View file

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

View file

@ -19,6 +19,10 @@ final class PhabricatorConduitTokensSettingsPanel
return pht('Conduit API Tokens');
}
public function getPanelMenuIcon() {
return id(new PhabricatorConduitApplication())->getIcon();
}
public function getPanelGroupKey() {
return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY;
}

View file

@ -11,16 +11,23 @@ final class PhabricatorPHPPreflightSetupCheck extends PhabricatorSetupCheck {
}
protected function executeChecks() {
if (version_compare(phpversion(), 7, '>=') &&
version_compare(phpversion(), 7.1, '<')) {
$version = phpversion();
if (version_compare($version, 7, '>=') &&
version_compare($version, 7.1, '<')) {
$message = pht(
'This version of Phabricator does not support PHP 7.0. You '.
'are running PHP %s. Upgrade to PHP 7.1 or newer.',
phpversion());
'You are running PHP version %s. Phabricator does not support PHP '.
'versions between 7.0 and 7.1.'.
"\n\n".
'PHP removed signal handling features that Phabricator requires in '.
'PHP 7.0, and did not restore them until PHP 7.1.'.
"\n\n".
'Upgrade to PHP 7.1 or newer (recommended) or downgrade to an older '.
'version of PHP 5 (discouraged).',
$version);
$this->newIssue('php.version7')
->setIsFatal(true)
->setName(pht('PHP 7.0 Not Supported'))
->setName(pht('PHP 7.0-7.1 Not Supported'))
->setMessage($message)
->addLink(
'https://phurl.io/u/php7',

View file

@ -18,6 +18,10 @@ final class DiffusionSetPasswordSettingsPanel extends PhabricatorSettingsPanel {
return pht('VCS Password');
}
public function getPanelMenuIcon() {
return 'fa-code';
}
public function getPanelGroupKey() {
return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY;
}

View file

@ -0,0 +1,63 @@
<?php
final class PhabricatorMailAmazonSNSAdapter
extends PhabricatorMailAdapter {
const ADAPTERTYPE = 'sns';
public function getSupportedMessageTypes() {
return array(
PhabricatorMailSMSMessage::MESSAGETYPE,
);
}
protected function validateOptions(array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
'access-key' => 'string',
'secret-key' => 'string',
'endpoint' => 'string',
'region' => 'string',
));
}
public function newDefaultOptions() {
return array(
'access-key' => null,
'secret-key' => null,
'endpoint' => null,
'region' => null,
);
}
public function sendMessage(PhabricatorMailExternalMessage $message) {
$access_key = $this->getOption('access-key');
$secret_key = $this->getOption('secret-key');
$secret_key = new PhutilOpaqueEnvelope($secret_key);
$endpoint = $this->getOption('endpoint');
$region = $this->getOption('region');
$to_number = $message->getToNumber();
$text_body = $message->getTextBody();
$params = array(
'Version' => '2010-03-31',
'Action' => 'Publish',
'PhoneNumber' => $to_number->toE164(),
'Message' => $text_body,
);
return id(new PhabricatorAmazonSNSFuture())
->setParameters($params)
->setEndpoint($endpoint)
->setAccessKey($access_key)
->setSecretKey($secret_key)
->setRegion($region)
->setTimeout(60)
->resolve();
}
}

View file

@ -33,6 +33,7 @@ final class PhabricatorMailTestAdapter
public function getSupportedMessageTypes() {
return array(
PhabricatorMailEmailMessage::MESSAGETYPE,
PhabricatorMailSMSMessage::MESSAGETYPE,
);
}
@ -63,6 +64,28 @@ final class PhabricatorMailTestAdapter
pht('Unit Test (Temporary)'));
}
switch ($message->getMessageType()) {
case PhabricatorMailEmailMessage::MESSAGETYPE:
$guts = $this->newEmailGuts($message);
break;
case PhabricatorMailSMSMessage::MESSAGETYPE:
$guts = $this->newSMSGuts($message);
break;
}
$guts['did-send'] = true;
$this->guts = $guts;
}
public function getBody() {
return idx($this->guts, 'body');
}
public function getHTMLBody() {
return idx($this->guts, 'html-body');
}
private function newEmailGuts(PhabricatorMailExternalMessage $message) {
$guts = array();
$from = $message->getFromAddress();
@ -123,19 +146,16 @@ final class PhabricatorMailTestAdapter
}
$guts['attachments'] = $file_list;
$guts['did-send'] = true;
$this->guts = $guts;
return $guts;
}
private function newSMSGuts(PhabricatorMailExternalMessage $message) {
$guts = array();
public function getBody() {
return idx($this->guts, 'body');
$guts['to'] = $message->getToNumber();
$guts['body'] = $message->getTextBody();
return $guts;
}
public function getHTMLBody() {
return idx($this->guts, 'html-body');
}
}

View file

@ -187,6 +187,9 @@ final class PhabricatorMetaMTAMailViewController
->setStacked(true);
$headers = $mail->getDeliveredHeaders();
if (!$headers) {
$headers = array();
}
// Sort headers by name.
$headers = isort($headers, 0);

View file

@ -0,0 +1,75 @@
<?php
final class PhabricatorMailSMSEngine
extends PhabricatorMailMessageEngine {
public function newMessage() {
$mailer = $this->getMailer();
$mail = $this->getMail();
$message = new PhabricatorMailSMSMessage();
$phids = $mail->getToPHIDs();
if (!$phids) {
$mail->setMessage(pht('Message has no "To" recipient.'));
return null;
}
if (count($phids) > 1) {
$mail->setMessage(pht('Message has more than one "To" recipient.'));
return null;
}
$phid = head($phids);
$actor = $this->getActor($phid);
if (!$actor) {
$mail->setMessage(pht('Message recipient has no mailable actor.'));
return null;
}
if (!$actor->isDeliverable()) {
$mail->setMessage(pht('Message recipient is not deliverable.'));
return null;
}
$omnipotent = PhabricatorUser::getOmnipotentUser();
$contact_numbers = id(new PhabricatorAuthContactNumberQuery())
->setViewer($omnipotent)
->withObjectPHIDs(array($phid))
->withStatuses(
array(
PhabricatorAuthContactNumber::STATUS_ACTIVE,
))
->withIsPrimary(true)
->execute();
if (!$contact_numbers) {
$mail->setMessage(
pht('Message recipient has no primary contact number.'));
return null;
}
// The database does not strictly guarantee that only one number is
// primary, so make sure no one has monkeyed with stuff.
if (count($contact_numbers) > 1) {
$mail->setMessage(
pht('Message recipient has more than one primary contact number.'));
return null;
}
$contact_number = head($contact_numbers);
$contact_number = $contact_number->getContactNumber();
$to_number = new PhabricatorPhoneNumber($contact_number);
$message->setToNumber($to_number);
$body = $mail->getBody();
if ($body !== null) {
$message->setTextBody($body);
}
return $message;
}
}

View file

@ -0,0 +1,41 @@
<?php
final class PhabricatorAmazonSNSFuture extends PhutilAWSFuture {
private $parameters = array();
private $timeout;
public function setParameters($parameters) {
$this->parameters = $parameters;
return $this;
}
protected function getParameters() {
return $this->parameters;
}
public function getServiceName() {
return 'sns';
}
public function setTimeout($timeout) {
$this->timeout = $timeout;
return $this;
}
public function getTimeout() {
return $this->timeout;
}
protected function getProxiedFuture() {
$future = parent::getProxiedFuture();
$timeout = $this->getTimeout();
if ($timeout) {
$future->setTimeout($timeout);
}
return $future;
}
}

View file

@ -58,7 +58,7 @@ final class PhabricatorTwilioFuture extends FutureProxy {
$this->accountSID,
$this->method);
$uri = id(new PhutilURI('https://api.twilio.com/2010-04-01/accounts/'))
$uri = id(new PhutilURI('https://api.twilio.com/'))
->setPath($path);
$data = $this->parameters;

View file

@ -7,8 +7,7 @@ final class PhabricatorMailManagementListOutboundWorkflow
$this
->setName('list-outbound')
->setSynopsis(pht('List outbound messages sent by Phabricator.'))
->setExamples(
'**list-outbound**')
->setExamples('**list-outbound**')
->setArguments(
array(
array(
@ -39,6 +38,7 @@ final class PhabricatorMailManagementListOutboundWorkflow
->addColumn('id', array('title' => pht('ID')))
->addColumn('encrypt', array('title' => pht('#')))
->addColumn('status', array('title' => pht('Status')))
->addColumn('type', array('title' => pht('Type')))
->addColumn('subject', array('title' => pht('Subject')));
foreach (array_reverse($mails) as $mail) {
@ -48,6 +48,7 @@ final class PhabricatorMailManagementListOutboundWorkflow
'id' => $mail->getID(),
'encrypt' => ($mail->getMustEncrypt() ? '#' : ' '),
'status' => PhabricatorMailOutboundStatus::getStatusName($status),
'type' => $mail->getMessageType(),
'subject' => $mail->getSubject(),
));
}

View file

@ -60,6 +60,12 @@ final class PhabricatorMailManagementSendTestWorkflow
'name' => 'bulk',
'help' => pht('Send with bulk headers.'),
),
array(
'name' => 'type',
'param' => 'message-type',
'help' => pht(
'Send the specified type of message (email, sms, ...).'),
),
));
}
@ -67,6 +73,20 @@ final class PhabricatorMailManagementSendTestWorkflow
$console = PhutilConsole::getConsole();
$viewer = $this->getViewer();
$type = $args->getArg('type');
if (!strlen($type)) {
$type = PhabricatorMailEmailMessage::MESSAGETYPE;
}
$type_map = PhabricatorMailExternalMessage::getAllMessageTypes();
if (!isset($type_map[$type])) {
throw new PhutilArgumentUsageException(
pht(
'Message type "%s" is unknown, supported message types are: %s.',
$type,
implode(', ', array_keys($type_map))));
}
$from = $args->getArg('from');
if ($from) {
$user = id(new PhabricatorPeopleQuery())
@ -86,9 +106,8 @@ final class PhabricatorMailManagementSendTestWorkflow
if (!$tos && !$ccs) {
throw new PhutilArgumentUsageException(
pht(
'Specify one or more users to send mail to with `%s` and `%s`.',
'--to',
'--cc'));
'Specify one or more users to send a message to with "--to" and/or '.
'"--cc".'));
}
$names = array_merge($tos, $ccs);
@ -166,26 +185,32 @@ final class PhabricatorMailManagementSendTestWorkflow
$mail->setFrom($from->getPHID());
}
$mailers = PhabricatorMetaMTAMail::newMailers(
array(
'media' => array($type),
'outbound' => true,
));
$mailers = mpull($mailers, null, 'getKey');
if (!$mailers) {
throw new PhutilArgumentUsageException(
pht(
'No configured mailers support outbound messages of type "%s".',
$type));
}
$mailer_key = $args->getArg('mailer');
if ($mailer_key !== null) {
$mailers = PhabricatorMetaMTAMail::newMailers(array());
$mailers = mpull($mailers, null, 'getKey');
if (!isset($mailers[$mailer_key])) {
throw new PhutilArgumentUsageException(
pht(
'Mailer key ("%s") is not configured. Available keys are: %s.',
'Mailer key ("%s") is not configured, or does not support '.
'outbound messages of type "%s". Available mailers are: %s.',
$mailer_key,
$type,
implode(', ', array_keys($mailers))));
}
if (!$mailers[$mailer_key]->getSupportsOutbound()) {
throw new PhutilArgumentUsageException(
pht(
'Mailer ("%s") is not configured to support outbound mail.',
$mailer_key));
}
$mail->setTryMailers(array($mailer_key));
}
@ -197,6 +222,8 @@ final class PhabricatorMailManagementSendTestWorkflow
$mail->addAttachment($file);
}
$mail->setMessageType($type);
PhabricatorWorker::setRunAllTasksInProcess(true);
$mail->save();

View file

@ -15,6 +15,10 @@ final class PhabricatorMailEmailMessage
private $textBody;
private $htmlBody;
public function newMailMessageEngine() {
return new PhabricatorMailEmailEngine();
}
public function setFromAddress(PhutilEmailAddress $from_address) {
$this->fromAddress = $from_address;
return $this;

View file

@ -7,4 +7,11 @@ abstract class PhabricatorMailExternalMessage
return $this->getPhobjectClassConstant('MESSAGETYPE');
}
final public static function getAllMessageTypes() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getMessageType')
->execute();
}
}

View file

@ -8,6 +8,10 @@ final class PhabricatorMailSMSMessage
private $toNumber;
private $textBody;
public function newMailMessageEngine() {
return new PhabricatorMailSMSEngine();
}
public function setToNumber(PhabricatorPhoneNumber $to_number) {
$this->toNumber = $to_number;
return $this;

View file

@ -8,13 +8,23 @@ final class PhabricatorPhoneNumber
public function __construct($raw_number) {
$number = preg_replace('/[^\d]+/', '', $raw_number);
if (!preg_match('/^[1-9]\d{1,14}\z/', $number)) {
if (!preg_match('/^[1-9]\d{9,14}\z/', $number)) {
throw new Exception(
pht(
'Phone number ("%s") is not in a recognized format.',
'Phone number ("%s") is not in a recognized format: expected a '.
'US number like "(555) 555-5555", or an international number '.
'like "+55 5555 555555".',
$raw_number));
}
// If the number didn't start with "+" and has has 10 digits, assume it is
// a US number with no country code prefix, like "(555) 555-5555".
if (!preg_match('/^[+]/', $raw_number)) {
if (strlen($number) === 10) {
$number = '1'.$number;
}
}
$this->number = $number;
}

View file

@ -0,0 +1,37 @@
<?php
final class PhabricatorPhoneNumberTestCase
extends PhabricatorTestCase {
public function testNumberNormalization() {
$map = array(
'+15555555555' => '+15555555555',
'+1 (555) 555-5555' => '+15555555555',
'(555) 555-5555' => '+15555555555',
'' => false,
'1-800-CALL-SAUL' => false,
);
foreach ($map as $input => $expect) {
$caught = null;
try {
$actual = id(new PhabricatorPhoneNumber($input))
->toE164();
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertEqual(
(bool)$caught,
($expect === false),
pht('Exception raised by: %s', $input));
if ($expect !== false) {
$this->assertEqual($expect, $actual, pht('E164 of: %s', $input));
}
}
}
}

View file

@ -400,6 +400,18 @@ final class PhabricatorMetaMTAMail
return $this->getParam('cc', array());
}
public function setMessageType($message_type) {
return $this->setParam('message.type', $message_type);
}
public function getMessageType() {
return $this->getParam(
'message.type',
PhabricatorMailEmailMessage::MESSAGETYPE);
}
/**
* Force delivery of a message, even if recipients have preferences which
* would otherwise drop the message.
@ -529,6 +541,9 @@ final class PhabricatorMetaMTAMail
$mailers = self::newMailers(
array(
'outbound' => true,
'media' => array(
$this->getMessageType(),
),
));
$try_mailers = $this->getParam('mailers.try');
@ -699,10 +714,19 @@ final class PhabricatorMetaMTAMail
$file->attachToObject($this->getPHID());
}
$type_map = PhabricatorMailExternalMessage::getAllMessageTypes();
$type = idx($type_map, $this->getMessageType());
if (!$type) {
throw new Exception(
pht(
'Unable to send message with unknown message type "%s".',
$type));
}
$exceptions = array();
foreach ($mailers as $mailer) {
try {
$message = id(new PhabricatorMailEmailEngine())
$message = $type->newMailMessageEngine()
->setMailer($mailer)
->setMail($this)
->setActors($actors)

View file

@ -11,6 +11,10 @@ final class PhabricatorOAuthServerAuthorizationsSettingsPanel
return pht('OAuth Authorizations');
}
public function getPanelMenuIcon() {
return 'fa-exchange';
}
public function getPanelGroupKey() {
return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY;
}

View file

@ -21,12 +21,8 @@ final class PassphraseCredentialRevealController
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();
if ($is_locked) {
@ -39,7 +35,7 @@ final class PassphraseCredentialRevealController
->addCancelButton($view_uri);
}
if ($request->isFormPost()) {
if ($request->isFormOrHisecPost()) {
$secret = $credential->getSecret();
if (!$secret) {
$body = pht('This credential has no associated secret.');
@ -76,6 +72,7 @@ final class PassphraseCredentialRevealController
$editor = id(new PassphraseCredentialTransactionEditor())
->setActor($viewer)
->setCancelURI($view_uri)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->applyTransactions($credential, $xactions);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2024,10 +2024,35 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
}
if ($never_proxy) {
// See PHI1030. This error can arise from various device name/address
// mismatches which are hard to detect, so try to provide as much
// information as we can.
if ($writable) {
$request_type = pht('(This is a write request.)');
} else {
$request_type = pht('(This is a read request.)');
}
throw new Exception(
pht(
'Refusing to proxy a repository request from a cluster host. '.
'Cluster hosts must correctly route their intracluster requests.'));
'This repository request (for repository "%s") has been '.
'incorrectly routed to a cluster host (with device name "%s", '.
'and hostname "%s") which can not serve the request.'.
"\n\n".
'The Almanac device address for the correct device may improperly '.
'point at this host, or the "device.id" configuration file on '.
'this host may be incorrect.'.
"\n\n".
'Requests routed within the cluster by Phabricator are always '.
'expected to be sent to a node which can serve the request. To '.
'prevent loops, this request will not be proxied again.'.
"\n\n".
"%s",
$this->getDisplayName(),
$local_device,
php_uname('n'),
$request_type));
}
if (count($results) > 1) {
@ -2147,7 +2172,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
$parts = array(
"repo({$repository_phid})",
"serv({$service_phid})",
'v3',
'v4',
);
return implode('.', $parts);

View file

@ -126,7 +126,7 @@ final class PhabricatorSearchResultView extends AphrontView {
}
// Go through the string one display glyph at a time. If a glyph starts
// on a highlighted byte position, turn on highlighting for the nubmer
// on a highlighted byte position, turn on highlighting for the number
// of matching bytes. If a query searches for "e" and the document contains
// an "e" followed by a bunch of combining marks, this will correctly
// highlight the entire glyph.

View file

@ -115,7 +115,7 @@ final class PhabricatorSettingsMainController
$crumbs->setBorder(true);
if ($this->user) {
$header_text = pht('Edit Settings (%s)', $user->getUserName());
$header_text = pht('Edit Settings: %s', $user->getUserName());
} else {
$header_text = pht('Edit Global Settings');
}
@ -127,15 +127,13 @@ final class PhabricatorSettingsMainController
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFixed(true)
->setNavigation($nav)
->setMainColumn($response);
->setFooter($response);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setNavigation($nav)
->appendChild($view);
}
private function buildPanels(PhabricatorUserPreferences $preferences) {
@ -211,7 +209,11 @@ final class PhabricatorSettingsMainController
}
}
$nav->addFilter($panel->getPanelKey(), $panel->getPanelName());
$nav->addFilter(
$panel->getPanelKey(),
$panel->getPanelName(),
null,
$panel->getPanelMenuIcon());
}
return $nav;

View file

@ -101,7 +101,7 @@ final class PhabricatorSettingsEditEngine
protected function getPageHeader($object) {
$user = $object->getUser();
if ($user) {
$text = pht('Edit Settings (%s)', $user->getUserName());
$text = pht('Edit Settings: %s', $user->getUserName());
} else {
$text = pht('Edit Global Settings');
}
@ -152,7 +152,7 @@ final class PhabricatorSettingsEditEngine
$viewer = $this->getViewer();
$user = $object->getUser();
$panels = PhabricatorSettingsPanel::getAllPanels();
$panels = PhabricatorSettingsPanel::getAllDisplayPanels();
foreach ($panels as $key => $panel) {
if (!($panel instanceof PhabricatorEditEngineSettingsPanel)) {

View file

@ -10,6 +10,10 @@ final class PhabricatorActivitySettingsPanel extends PhabricatorSettingsPanel {
return pht('Activity Logs');
}
public function getPanelMenuIcon() {
return 'fa-list';
}
public function getPanelGroupKey() {
return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY;
}

View file

@ -9,6 +9,10 @@ final class PhabricatorConpherencePreferencesSettingsPanel
return pht('Conpherence');
}
public function getPanelMenuIcon() {
return 'fa-comment-o';
}
public function getPanelGroupKey() {
return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY;
}

View file

@ -0,0 +1,91 @@
<?php
final class PhabricatorContactNumbersSettingsPanel
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'contact';
}
public function getPanelName() {
return pht('Contact Numbers');
}
public function getPanelMenuIcon() {
return 'fa-hashtag';
}
public function getPanelGroupKey() {
return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY;
}
public function isMultiFactorEnrollmentPanel() {
return true;
}
public function processRequest(AphrontRequest $request) {
$user = $this->getUser();
$viewer = $request->getUser();
$numbers = id(new PhabricatorAuthContactNumberQuery())
->setViewer($viewer)
->withObjectPHIDs(array($user->getPHID()))
->execute();
$numbers = msortv($numbers, 'getSortVector');
$rows = array();
$row_classes = array();
foreach ($numbers as $number) {
if ($number->getIsPrimary()) {
$primary_display = pht('Primary');
$row_classes[] = 'highlighted';
} else {
$primary_display = null;
$row_classes[] = null;
}
$rows[] = array(
$number->newIconView(),
phutil_tag(
'a',
array(
'href' => $number->getURI(),
),
$number->getDisplayName()),
$primary_display,
phabricator_datetime($number->getDateCreated(), $viewer),
);
}
$table = id(new AphrontTableView($rows))
->setNoDataString(
pht("You haven't added any contact numbers to your account."))
->setRowClasses($row_classes)
->setHeaders(
array(
null,
pht('Number'),
pht('Status'),
pht('Created'),
))
->setColumnClasses(
array(
null,
'wide pri',
null,
'right',
));
$buttons = array();
$buttons[] = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-plus')
->setText(pht('Add Contact Number'))
->setHref('/auth/contact/edit/')
->setColor(PHUIButtonView::GREY);
return $this->newBox(pht('Contact Numbers'), $table, $buttons);
}
}

View file

@ -9,6 +9,10 @@ final class PhabricatorDateTimeSettingsPanel
return pht('Date and Time');
}
public function getPanelMenuIcon() {
return 'fa-calendar';
}
public function getPanelGroupKey() {
return PhabricatorSettingsAccountPanelGroup::PANELGROUPKEY;
}

View file

@ -9,6 +9,10 @@ final class PhabricatorDeveloperPreferencesSettingsPanel
return pht('Developer Settings');
}
public function getPanelMenuIcon() {
return 'fa-magic';
}
public function getPanelGroupKey() {
return PhabricatorSettingsDeveloperPanelGroup::PANELGROUPKEY;
}

View file

@ -9,6 +9,10 @@ final class PhabricatorDiffPreferencesSettingsPanel
return pht('Diff Preferences');
}
public function getPanelMenuIcon() {
return 'fa-cog';
}
public function getPanelGroupKey() {
return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY;
}

View file

@ -9,6 +9,10 @@ final class PhabricatorDisplayPreferencesSettingsPanel
return pht('Display Preferences');
}
public function getPanelMenuIcon() {
return 'fa-desktop';
}
public function getPanelGroupKey() {
return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY;
}

View file

@ -11,6 +11,10 @@ final class PhabricatorEmailAddressesSettingsPanel
return pht('Email Addresses');
}
public function getPanelMenuIcon() {
return 'fa-at';
}
public function getPanelGroupKey() {
return PhabricatorSettingsEmailPanelGroup::PANELGROUPKEY;
}

View file

@ -9,6 +9,10 @@ final class PhabricatorEmailDeliverySettingsPanel
return pht('Email Delivery');
}
public function getPanelMenuIcon() {
return 'fa-envelope-o';
}
public function getPanelGroupKey() {
return PhabricatorSettingsEmailPanelGroup::PANELGROUPKEY;
}

Some files were not shown because too many files have changed in this diff Show more