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:
commit
6bca2e0773
129 changed files with 5809 additions and 573 deletions
|
@ -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',
|
||||
|
|
2
resources/sql/autopatches/20190115.mfa.01.provider.sql
Normal file
2
resources/sql/autopatches/20190115.mfa.01.provider.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_auth.auth_factorconfig
|
||||
ADD factorProviderPHID VARBINARY(64) NOT NULL;
|
72
resources/sql/autopatches/20190115.mfa.02.migrate.php
Normal file
72
resources/sql/autopatches/20190115.mfa.02.migrate.php
Normal 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']);
|
||||
}
|
2
resources/sql/autopatches/20190115.mfa.03.factorkey.sql
Normal file
2
resources/sql/autopatches/20190115.mfa.03.factorkey.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_auth.auth_factorconfig
|
||||
DROP factorKey;
|
11
resources/sql/autopatches/20190116.contact.01.number.sql
Normal file
11
resources/sql/autopatches/20190116.contact.01.number.sql
Normal 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};
|
19
resources/sql/autopatches/20190116.contact.02.xaction.sql
Normal file
19
resources/sql/autopatches/20190116.contact.02.xaction.sql
Normal 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};
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_auth.auth_contactnumber
|
||||
ADD isPrimary BOOL NOT NULL;
|
|
@ -218,6 +218,7 @@ $user->openTransaction();
|
|||
->setActor($actor)
|
||||
->setActingAsPHID($people_application_phid)
|
||||
->setContentSource($content_source)
|
||||
->setContinueOnNoEffect(true)
|
||||
->setContinueOnMissingFields(true);
|
||||
|
||||
$transaction_editor->applyTransactions($user, $xactions);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
||||
}
|
|
@ -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.');
|
||||
}
|
||||
|
||||
}
|
|
@ -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/'
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>'));
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAuthContactNumberEditController
|
||||
extends PhabricatorAuthContactNumberController {
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
return id(new PhabricatorAuthContactNumberEditEngine())
|
||||
->setController($this)
|
||||
->buildResponse();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAuthContactNumberMFAEngine
|
||||
extends PhabricatorEditEngineMFAEngine {
|
||||
|
||||
public function shouldTryMFA() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAuthFactorProviderMFAEngine
|
||||
extends PhabricatorEditEngineMFAEngine {
|
||||
|
||||
public function shouldTryMFA() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
802
src/applications/auth/factor/PhabricatorDuoAuthFactor.php
Normal file
802
src/applications/auth/factor/PhabricatorDuoAuthFactor.php
Normal 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));
|
||||
}
|
||||
|
||||
}
|
401
src/applications/auth/factor/PhabricatorSMSAuthFactor.php
Normal file
401
src/applications/auth/factor/PhabricatorSMSAuthFactor.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
155
src/applications/auth/future/PhabricatorDuoFuture.php
Normal file
155
src/applications/auth/future/PhabricatorDuoFuture.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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.'));
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAuthContactNumberTransactionQuery
|
||||
extends PhabricatorApplicationTransactionQuery {
|
||||
|
||||
public function getTemplateApplicationTransaction() {
|
||||
return new PhabricatorAuthContactNumberTransaction();
|
||||
}
|
||||
|
||||
}
|
131
src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php
Normal file
131
src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php
Normal 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';
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
243
src/applications/auth/storage/PhabricatorAuthContactNumber.php
Normal file
243
src/applications/auth/storage/PhabricatorAuthContactNumber.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 )----------------------------------------- */
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAuthFactorProviderDuoEnrollTransaction
|
||||
extends PhabricatorAuthFactorProviderTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'duo.enroll';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
$key = PhabricatorDuoAuthFactor::PROP_ENROLL;
|
||||
return $object->getAuthFactorProviderProperty($key);
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$key = PhabricatorDuoAuthFactor::PROP_ENROLL;
|
||||
$object->setAuthFactorProviderProperty($key, $value);
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
return pht(
|
||||
'%s changed the enrollment policy for this provider from %s to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderOldValue(),
|
||||
$this->renderNewValue());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAuthFactorProviderDuoUsernamesTransaction
|
||||
extends PhabricatorAuthFactorProviderTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'duo.usernames';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
$key = PhabricatorDuoAuthFactor::PROP_USERNAMES;
|
||||
return $object->getAuthFactorProviderProperty($key);
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$key = PhabricatorDuoAuthFactor::PROP_USERNAMES;
|
||||
$object->setAuthFactorProviderProperty($key, $value);
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
return pht(
|
||||
'%s changed the username policy for this provider from %s to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderOldValue(),
|
||||
$this->renderNewValue());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -187,6 +187,9 @@ final class PhabricatorMetaMTAMailViewController
|
|||
->setStacked(true);
|
||||
|
||||
$headers = $mail->getDeliveredHeaders();
|
||||
if (!$headers) {
|
||||
$headers = array();
|
||||
}
|
||||
|
||||
// Sort headers by name.
|
||||
$headers = isort($headers, 0);
|
||||
|
|
75
src/applications/metamta/engine/PhabricatorMailSMSEngine.php
Normal file
75
src/applications/metamta/engine/PhabricatorMailSMSEngine.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
));
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -11,6 +11,10 @@ final class PhabricatorOAuthServerAuthorizationsSettingsPanel
|
|||
return pht('OAuth Authorizations');
|
||||
}
|
||||
|
||||
public function getPanelMenuIcon() {
|
||||
return 'fa-exchange';
|
||||
}
|
||||
|
||||
public function getPanelGroupKey() {
|
||||
return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -30,4 +30,10 @@ final class PassphraseCredentialLookedAtTransaction
|
|||
return 'blue';
|
||||
}
|
||||
|
||||
public function shouldTryMFA(
|
||||
$object,
|
||||
PhabricatorApplicationTransaction $xaction) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -89,4 +89,11 @@ final class PhabricatorUserUsernameTransaction
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function shouldTryMFA(
|
||||
$object,
|
||||
PhabricatorApplicationTransaction $xaction) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,10 @@ final class PhabricatorConpherencePreferencesSettingsPanel
|
|||
return pht('Conpherence');
|
||||
}
|
||||
|
||||
public function getPanelMenuIcon() {
|
||||
return 'fa-comment-o';
|
||||
}
|
||||
|
||||
public function getPanelGroupKey() {
|
||||
return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -9,6 +9,10 @@ final class PhabricatorDateTimeSettingsPanel
|
|||
return pht('Date and Time');
|
||||
}
|
||||
|
||||
public function getPanelMenuIcon() {
|
||||
return 'fa-calendar';
|
||||
}
|
||||
|
||||
public function getPanelGroupKey() {
|
||||
return PhabricatorSettingsAccountPanelGroup::PANELGROUPKEY;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,10 @@ final class PhabricatorDeveloperPreferencesSettingsPanel
|
|||
return pht('Developer Settings');
|
||||
}
|
||||
|
||||
public function getPanelMenuIcon() {
|
||||
return 'fa-magic';
|
||||
}
|
||||
|
||||
public function getPanelGroupKey() {
|
||||
return PhabricatorSettingsDeveloperPanelGroup::PANELGROUPKEY;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,10 @@ final class PhabricatorDiffPreferencesSettingsPanel
|
|||
return pht('Diff Preferences');
|
||||
}
|
||||
|
||||
public function getPanelMenuIcon() {
|
||||
return 'fa-cog';
|
||||
}
|
||||
|
||||
public function getPanelGroupKey() {
|
||||
return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,10 @@ final class PhabricatorDisplayPreferencesSettingsPanel
|
|||
return pht('Display Preferences');
|
||||
}
|
||||
|
||||
public function getPanelMenuIcon() {
|
||||
return 'fa-desktop';
|
||||
}
|
||||
|
||||
public function getPanelGroupKey() {
|
||||
return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,10 @@ final class PhabricatorEmailAddressesSettingsPanel
|
|||
return pht('Email Addresses');
|
||||
}
|
||||
|
||||
public function getPanelMenuIcon() {
|
||||
return 'fa-at';
|
||||
}
|
||||
|
||||
public function getPanelGroupKey() {
|
||||
return PhabricatorSettingsEmailPanelGroup::PANELGROUPKEY;
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue