From a62f334d95030f73bd9c6a926a4fefd6d41fc2a7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 28 Dec 2018 16:52:00 -0800 Subject: [PATCH] Add a skeleton for configurable MFA provider types Summary: Ref T13222. Ref T13231. See PHI912. I'm planning to turn MFA providers into concrete objects, so you can disable and configure them. Currently, we only support TOTP, which doesn't require any configuration, but other provider types (like Duo or Yubikey OTP) do require some configuration (server URIs, API keys, etc). TOTP //could// also have some configuration, like "bits of entropy" or "allowed window size" or whatever, if we want. Add concrete objects for this and standard transaction / policy / query support. These objects don't do anything interesting yet and don't actually interact with MFA, this is just skeleton code for now. Test Plan: {F6090444} {F6090445} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13231, T13222 Differential Revision: https://secure.phabricator.com/D19935 --- .../autopatches/20181228.auth.01.provider.sql | 9 ++ .../autopatches/20181228.auth.02.xaction.sql | 19 +++ .../sql/autopatches/20181228.auth.03.name.sql | 2 + src/__phutil_library_map__.php | 35 ++++- .../PhabricatorAuthApplication.php | 9 ++ .../config/PhabricatorAuthListController.php | 25 ++-- ...habricatorAuthProviderConfigController.php | 30 +--- .../PhabricatorAuthProviderController.php | 43 ++++++ ...habricatorAuthFactorProviderController.php | 11 ++ ...icatorAuthFactorProviderEditController.php | 65 +++++++++ ...icatorAuthFactorProviderListController.php | 72 ++++++++++ ...icatorAuthFactorProviderViewController.php | 100 +++++++++++++ ...habricatorAuthFactorProviderEditEngine.php | 115 +++++++++++++++ .../PhabricatorAuthFactorProviderEditor.php | 22 +++ .../auth/factor/PhabricatorAuthFactor.php | 6 + .../auth/factor/PhabricatorTOTPAuthFactor.php | 6 + ...bricatorAuthAuthFactorProviderPHIDType.php | 40 ++++++ .../PhabricatorAuthFactorProviderQuery.php | 67 +++++++++ ...atorAuthFactorProviderTransactionQuery.php | 10 ++ .../storage/PhabricatorAuthFactorProvider.php | 134 ++++++++++++++++++ ...abricatorAuthFactorProviderTransaction.php | 18 +++ ...catorAuthFactorProviderNameTransaction.php | 69 +++++++++ ...catorAuthFactorProviderTransactionType.php | 4 + 23 files changed, 871 insertions(+), 40 deletions(-) create mode 100644 resources/sql/autopatches/20181228.auth.01.provider.sql create mode 100644 resources/sql/autopatches/20181228.auth.02.xaction.sql create mode 100644 resources/sql/autopatches/20181228.auth.03.name.sql create mode 100644 src/applications/auth/controller/config/PhabricatorAuthProviderController.php create mode 100644 src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php create mode 100644 src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php create mode 100644 src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php create mode 100644 src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php create mode 100644 src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php create mode 100644 src/applications/auth/editor/PhabricatorAuthFactorProviderEditor.php create mode 100644 src/applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php create mode 100644 src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php create mode 100644 src/applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php create mode 100644 src/applications/auth/storage/PhabricatorAuthFactorProvider.php create mode 100644 src/applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthFactorProviderNameTransaction.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php diff --git a/resources/sql/autopatches/20181228.auth.01.provider.sql b/resources/sql/autopatches/20181228.auth.01.provider.sql new file mode 100644 index 0000000000..4ffd23c846 --- /dev/null +++ b/resources/sql/autopatches/20181228.auth.01.provider.sql @@ -0,0 +1,9 @@ +CREATE TABLE {$NAMESPACE}_auth.auth_factorprovider ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + providerFactorKey VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}, + status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20181228.auth.02.xaction.sql b/resources/sql/autopatches/20181228.auth.02.xaction.sql new file mode 100644 index 0000000000..c595cdd8fc --- /dev/null +++ b/resources/sql/autopatches/20181228.auth.02.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_auth.auth_factorprovidertransaction ( + 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}; diff --git a/resources/sql/autopatches/20181228.auth.03.name.sql b/resources/sql/autopatches/20181228.auth.03.name.sql new file mode 100644 index 0000000000..856c10287d --- /dev/null +++ b/resources/sql/autopatches/20181228.auth.03.name.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_factorprovider + ADD name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index d2a7dcb3d8..e30f3258d3 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2190,6 +2190,7 @@ phutil_register_library_map(array( 'PhabricatorAuthAccountView' => 'applications/auth/view/PhabricatorAuthAccountView.php', 'PhabricatorAuthApplication' => 'applications/auth/application/PhabricatorAuthApplication.php', 'PhabricatorAuthAuthFactorPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php', + 'PhabricatorAuthAuthFactorProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php', 'PhabricatorAuthAuthProviderPHIDType' => 'applications/auth/phid/PhabricatorAuthAuthProviderPHIDType.php', 'PhabricatorAuthCSRFEngine' => 'applications/auth/engine/PhabricatorAuthCSRFEngine.php', 'PhabricatorAuthChallenge' => 'applications/auth/storage/PhabricatorAuthChallenge.php', @@ -2207,6 +2208,18 @@ phutil_register_library_map(array( 'PhabricatorAuthEditController' => 'applications/auth/controller/config/PhabricatorAuthEditController.php', 'PhabricatorAuthFactor' => 'applications/auth/factor/PhabricatorAuthFactor.php', 'PhabricatorAuthFactorConfig' => 'applications/auth/storage/PhabricatorAuthFactorConfig.php', + 'PhabricatorAuthFactorProvider' => 'applications/auth/storage/PhabricatorAuthFactorProvider.php', + 'PhabricatorAuthFactorProviderController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.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', + 'PhabricatorAuthFactorProviderNameTransaction' => 'applications/auth/xaction/PhabricatorAuthFactorProviderNameTransaction.php', + 'PhabricatorAuthFactorProviderQuery' => 'applications/auth/query/PhabricatorAuthFactorProviderQuery.php', + 'PhabricatorAuthFactorProviderTransaction' => 'applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php', + 'PhabricatorAuthFactorProviderTransactionQuery' => 'applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php', + 'PhabricatorAuthFactorProviderTransactionType' => 'applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php', + 'PhabricatorAuthFactorProviderViewController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php', 'PhabricatorAuthFactorResult' => 'applications/auth/factor/PhabricatorAuthFactorResult.php', 'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php', 'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php', @@ -2277,6 +2290,7 @@ phutil_register_library_map(array( 'PhabricatorAuthProviderConfigQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigQuery.php', 'PhabricatorAuthProviderConfigTransaction' => 'applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php', 'PhabricatorAuthProviderConfigTransactionQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigTransactionQuery.php', + 'PhabricatorAuthProviderController' => 'applications/auth/controller/config/PhabricatorAuthProviderController.php', 'PhabricatorAuthProvidersGuidanceContext' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceContext.php', 'PhabricatorAuthProvidersGuidanceEngineExtension' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php', 'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthQueryPublicKeysConduitAPIMethod.php', @@ -7835,6 +7849,7 @@ phutil_register_library_map(array( 'PhabricatorAuthAccountView' => 'AphrontView', 'PhabricatorAuthApplication' => 'PhabricatorApplication', 'PhabricatorAuthAuthFactorPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorAuthAuthFactorProviderPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthAuthProviderPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthCSRFEngine' => 'Phobject', 'PhabricatorAuthChallenge' => array( @@ -7855,6 +7870,23 @@ phutil_register_library_map(array( 'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthFactor' => 'Phobject', 'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO', + 'PhabricatorAuthFactorProvider' => array( + 'PhabricatorAuthDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', + ), + 'PhabricatorAuthFactorProviderController' => 'PhabricatorAuthProviderController', + 'PhabricatorAuthFactorProviderEditController' => 'PhabricatorAuthFactorProviderController', + 'PhabricatorAuthFactorProviderEditEngine' => 'PhabricatorEditEngine', + 'PhabricatorAuthFactorProviderEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorAuthFactorProviderListController' => 'PhabricatorAuthProviderController', + 'PhabricatorAuthFactorProviderNameTransaction' => 'PhabricatorAuthFactorProviderTransactionType', + 'PhabricatorAuthFactorProviderQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorAuthFactorProviderTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorAuthFactorProviderTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorAuthFactorProviderTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorAuthFactorProviderViewController' => 'PhabricatorAuthFactorProviderController', 'PhabricatorAuthFactorResult' => 'Phobject', 'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase', 'PhabricatorAuthFinishController' => 'PhabricatorAuthController', @@ -7931,11 +7963,12 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', ), - 'PhabricatorAuthProviderConfigController' => 'PhabricatorAuthController', + 'PhabricatorAuthProviderConfigController' => 'PhabricatorAuthProviderController', 'PhabricatorAuthProviderConfigEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorAuthProviderConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthProviderConfigTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorAuthProviderController' => 'PhabricatorAuthController', 'PhabricatorAuthProvidersGuidanceContext' => 'PhabricatorGuidanceContext', 'PhabricatorAuthProvidersGuidanceEngineExtension' => 'PhabricatorGuidanceEngineExtension', 'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod', diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index ff4ed1f136..62f86a00f8 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -85,6 +85,15 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { 'view/(?P\d+)/' => 'PhabricatorAuthSSHKeyViewController', ), 'password/' => 'PhabricatorAuthSetPasswordController', + + 'mfa/' => array( + $this->getQueryRoutePattern() => + 'PhabricatorAuthFactorProviderListController', + $this->getEditRoutePattern('edit/') => + 'PhabricatorAuthFactorProviderEditController', + '(?P[1-9]\d*)/' => + 'PhabricatorAuthFactorProviderViewController', + ), ), '/oauth/(?P\w+)/login/' diff --git a/src/applications/auth/controller/config/PhabricatorAuthListController.php b/src/applications/auth/controller/config/PhabricatorAuthListController.php index c5d4f7ad77..bb118d798e 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthListController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthListController.php @@ -91,7 +91,7 @@ final class PhabricatorAuthListController pht('Add Authentication Provider')))); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Auth Providers')); + $crumbs->addTextCrumb(pht('Login and Registration')); $crumbs->setBorder(true); $guidance_context = new PhabricatorAuthProvidersGuidanceContext(); @@ -102,12 +102,12 @@ final class PhabricatorAuthListController ->newInfoView(); $button = id(new PHUIButtonView()) - ->setTag('a') - ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE) - ->setHref($this->getApplicationURI('config/new/')) - ->setIcon('fa-plus') - ->setDisabled(!$can_manage) - ->setText(pht('Add Provider')); + ->setTag('a') + ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE) + ->setHref($this->getApplicationURI('config/new/')) + ->setIcon('fa-plus') + ->setDisabled(!$can_manage) + ->setText(pht('Add Provider')); $list->setFlush(true); $list = id(new PHUIObjectBoxView()) @@ -115,7 +115,7 @@ final class PhabricatorAuthListController ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($list); - $title = pht('Auth Providers'); + $title = pht('Login and Registration Providers'); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setHeaderIcon('fa-key') @@ -128,10 +128,15 @@ final class PhabricatorAuthListController $list, )); - return $this->newPage() - ->setTitle($title) + $nav = $this->newNavigation() ->setCrumbs($crumbs) ->appendChild($view); + + $nav->selectFilter('login'); + + return $this->newPage() + ->setTitle($title) + ->appendChild($nav); } } diff --git a/src/applications/auth/controller/config/PhabricatorAuthProviderConfigController.php b/src/applications/auth/controller/config/PhabricatorAuthProviderConfigController.php index db9ec06799..c2d20dd1b3 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthProviderConfigController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthProviderConfigController.php @@ -1,32 +1,4 @@ setBaseURI(new PhutilURI($this->getApplicationURI())); - - if ($for_app) { - $nav->addLabel(pht('Create')); - $nav->addFilter('', - pht('Add Authentication Provider'), - $this->getApplicationURI('/config/new/')); - } - return $nav; - } - - public function buildApplicationMenu() { - return $this->buildSideNavView($for_app = true)->getMenu(); - } - - protected function buildApplicationCrumbs() { - $crumbs = parent::buildApplicationCrumbs(); - - $can_create = $this->hasApplicationCapability( - AuthManageProvidersCapability::CAPABILITY); - - return $crumbs; - } - -} + extends PhabricatorAuthProviderController {} diff --git a/src/applications/auth/controller/config/PhabricatorAuthProviderController.php b/src/applications/auth/controller/config/PhabricatorAuthProviderController.php new file mode 100644 index 0000000000..2fb4386ef4 --- /dev/null +++ b/src/applications/auth/controller/config/PhabricatorAuthProviderController.php @@ -0,0 +1,43 @@ +getViewer(); + + $nav = id(new AphrontSideNavFilterView()) + ->setBaseURI(new PhutilURI($this->getApplicationURI())) + ->setViewer($viewer); + + $nav->addMenuItem( + id(new PHUIListItemView()) + ->setName(pht('Authentication')) + ->setType(PHUIListItemView::TYPE_LABEL)); + + $nav->addMenuItem( + id(new PHUIListItemView()) + ->setKey('login') + ->setName(pht('Login and Registration')) + ->setType(PHUIListItemView::TYPE_LINK) + ->setHref($this->getApplicationURI('/')) + ->setIcon('fa-key')); + + $nav->addMenuItem( + id(new PHUIListItemView()) + ->setKey('mfa') + ->setName(pht('Multi-Factor')) + ->setType(PHUIListItemView::TYPE_LINK) + ->setHref($this->getApplicationURI('mfa/')) + ->setIcon('fa-mobile')); + + $nav->selectFilter(null); + + return $nav; + } + + public function buildApplicationMenu() { + return $this->newNavigation()->getMenu(); + } + +} diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php new file mode 100644 index 0000000000..53a8f10be3 --- /dev/null +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderController.php @@ -0,0 +1,11 @@ +addTextCrumb(pht('Multi-Factor'), $this->getApplicationURI('mfa/')); + } + +} diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php new file mode 100644 index 0000000000..5108eaaefd --- /dev/null +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php @@ -0,0 +1,65 @@ +requireApplicationCapability( + AuthManageProvidersCapability::CAPABILITY); + + $engine = id(new PhabricatorAuthFactorProviderEditEngine()) + ->setController($this); + + $id = $request->getURIData('id'); + if (!$id) { + $factor_key = $request->getStr('providerFactorKey'); + + $map = PhabricatorAuthFactor::getAllFactors(); + $factor = idx($map, $factor_key); + if (!$factor) { + return $this->buildFactorSelectionResponse(); + } + + $engine + ->addContextParameter('providerFactorKey', $factor_key) + ->setProviderFactor($factor); + } + + return $engine->buildResponse(); + } + + private function buildFactorSelectionResponse() { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $cancel_uri = $this->getApplicationURI('mfa/'); + + $factors = PhabricatorAuthFactor::getAllFactors(); + + $menu = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setBig(true) + ->setFlush(true); + + foreach ($factors as $factor_key => $factor) { + $factor_uri = id(new PhutilURI('/mfa/edit/')) + ->setQueryParam('providerFactorKey', $factor_key); + $factor_uri = $this->getApplicationURI($factor_uri); + + $item = id(new PHUIObjectItemView()) + ->setHeader($factor->getFactorName()) + ->setHref($factor_uri) + ->setClickable(true) + ->setImageIcon($factor->newIconView()) + ->addAttribute($factor->getFactorCreateHelp()); + + $menu->addItem($item); + } + + return $this->newDialog() + ->setTitle(pht('Choose Provider Type')) + ->appendChild($menu) + ->addCancelButton($cancel_uri); + } + +} diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php new file mode 100644 index 0000000000..293728cf36 --- /dev/null +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php @@ -0,0 +1,72 @@ +getViewer(); + + $can_manage = $this->hasApplicationCapability( + AuthManageProvidersCapability::CAPABILITY); + + $providers = id(new PhabricatorAuthFactorProviderQuery()) + ->setViewer($viewer) + ->execute(); + + $list = new PHUIObjectItemListView(); + foreach ($providers as $provider) { + $item = id(new PHUIObjectItemView()) + ->setObjectName($provider->getObjectName()) + ->setHeader($provider->getDisplayName()) + ->setHref($provider->getURI()); + + $list->addItem($item); + } + + $list->setNoDataString( + pht('You have not configured any multi-factor providers yet.')); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Multi-Factor')) + ->setBorder(true); + + $button = id(new PHUIButtonView()) + ->setTag('a') + ->setButtonType(PHUIButtonView::BUTTONTYPE_SIMPLE) + ->setHref($this->getApplicationURI('mfa/edit/')) + ->setIcon('fa-plus') + ->setDisabled(!$can_manage) + ->setWorkflow(true) + ->setText(pht('Add MFA Provider')); + + $list->setFlush(true); + $list = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('MFA Providers')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($list); + + $title = pht('MFA Providers'); + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setHeaderIcon('fa-mobile') + ->addActionLink($button); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $list, + )); + + $nav = $this->newNavigation() + ->setCrumbs($crumbs) + ->appendChild($view); + + $nav->selectFilter('mfa'); + + return $this->newPage() + ->setTitle($title) + ->appendChild($nav); + } + +} diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php new file mode 100644 index 0000000000..67edf2f81b --- /dev/null +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php @@ -0,0 +1,100 @@ +getViewer(); + + $this->requireApplicationCapability( + AuthManageProvidersCapability::CAPABILITY); + + $provider = id(new PhabricatorAuthFactorProviderQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->executeOne(); + if (!$provider) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($provider->getObjectName()) + ->setBorder(true); + + $header = $this->buildHeaderView($provider); + $properties = $this->buildPropertiesView($provider); + $curtain = $this->buildCurtain($provider); + + + $timeline = $this->buildTransactionTimeline( + $provider, + new PhabricatorAuthFactorProviderTransactionQuery()); + $timeline->setShouldTerminate(true); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $timeline, + )) + ->addPropertySection(pht('Details'), $properties); + + return $this->newPage() + ->setTitle($provider->getDisplayName()) + ->setCrumbs($crumbs) + ->setPageObjectPHIDs( + array( + $provider->getPHID(), + )) + ->appendChild($view); + } + + private function buildHeaderView(PhabricatorAuthFactorProvider $provider) { + $viewer = $this->getViewer(); + + $view = id(new PHUIHeaderView()) + ->setViewer($viewer) + ->setHeader($provider->getDisplayName()) + ->setPolicyObject($provider); + + return $view; + } + + private function buildPropertiesView( + PhabricatorAuthFactorProvider $provider) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setViewer($viewer); + + $view->addProperty( + pht('Factor Type'), + $provider->getFactor()->getFactorName()); + + return $view; + } + + private function buildCurtain(PhabricatorAuthFactorProvider $provider) { + $viewer = $this->getViewer(); + $id = $provider->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $provider, + PhabricatorPolicyCapability::CAN_EDIT); + + $curtain = $this->newCurtainView($provider); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit MFA Provider')) + ->setIcon('fa-pencil') + ->setHref($this->getApplicationURI("mfa/edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $curtain; + } + +} diff --git a/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php new file mode 100644 index 0000000000..a0b5988589 --- /dev/null +++ b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php @@ -0,0 +1,115 @@ +providerFactor = $factor; + return $this; + } + + public function getProviderFactor() { + return $this->providerFactor; + } + + protected function newEditableObject() { + $factor = $this->getProviderFactor(); + if ($factor) { + $provider = PhabricatorAuthFactorProvider::initializeNewProvider($factor); + } else { + $provider = new PhabricatorAuthFactorProvider(); + } + + return $provider; + } + + protected function newObjectQuery() { + return new PhabricatorAuthFactorProviderQuery(); + } + + protected function getObjectCreateTitleText($object) { + return pht('Create MFA Provider'); + } + + protected function getObjectCreateButtonText($object) { + return pht('Create MFA Provider'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit MFA Provider'); + } + + protected function getObjectEditShortText($object) { + return $object->getObjectName(); + } + + protected function getObjectCreateShortText() { + return pht('Create MFA Provider'); + } + + protected function getObjectName() { + return pht('MFA Provider'); + } + + protected function getEditorURI() { + return '/auth/mfa/edit/'; + } + + protected function getObjectCreateCancelURI($object) { + return '/auth/mfa/'; + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function getCreateNewObjectPolicy() { + return $this->getApplication()->getPolicy( + AuthManageProvidersCapability::CAPABILITY); + } + + protected function buildCustomEditFields($object) { + $factor_name = $object->getFactor()->getFactorName(); + + return array( + id(new PhabricatorStaticEditField()) + ->setKey('displayType') + ->setLabel(pht('Factor Type')) + ->setDescription(pht('Type of the MFA provider.')) + ->setValue($factor_name), + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setTransactionType( + PhabricatorAuthFactorProviderNameTransaction::TRANSACTIONTYPE) + ->setLabel(pht('Name')) + ->setDescription(pht('Display name for the MFA provider.')) + ->setValue($object->getName()) + ->setPlaceholder($factor_name), + ); + } + +} diff --git a/src/applications/auth/editor/PhabricatorAuthFactorProviderEditor.php b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditor.php new file mode 100644 index 0000000000..144f275391 --- /dev/null +++ b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditor.php @@ -0,0 +1,22 @@ +setIcon('fa-mobile'); + } + protected function newChallenge( PhabricatorAuthFactorConfig $config, PhabricatorUser $viewer) { diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index 33bb961691..3632ca5c45 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -12,6 +12,12 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { return pht('Mobile Phone App (TOTP)'); } + public function getFactorCreateHelp() { + return pht( + 'Allow users to attach a mobile authenticator application (like '. + 'Google Authenticator) to their account.'); + } + public function getFactorDescription() { return pht( 'Attach a mobile authenticator application (like Authy '. diff --git a/src/applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php b/src/applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php new file mode 100644 index 0000000000..f0f9f572e8 --- /dev/null +++ b/src/applications/auth/phid/PhabricatorAuthAuthFactorProviderPHIDType.php @@ -0,0 +1,40 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $provider = $objects[$phid]; + + $handle->setURI($provider->getURI()); + } + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php b/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php new file mode 100644 index 0000000000..f4ce60773b --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php @@ -0,0 +1,67 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + public function newResultObject() { + return new PhabricatorAuthFactorProvider(); + } + + 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); + } + + return $where; + } + + protected function willFilterPage(array $providers) { + $map = PhabricatorAuthFactor::getAllFactors(); + foreach ($providers as $key => $provider) { + $factor_key = $provider->getProviderFactorKey(); + $factor = idx($map, $factor_key); + + if (!$factor) { + unset($providers[$key]); + continue; + } + + $provider->attachFactor($factor); + } + + return $providers; + } + + public function getQueryApplicationClass() { + return 'PhabricatorAuthApplication'; + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php b/src/applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php new file mode 100644 index 0000000000..5add1345c4 --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthFactorProviderTransactionQuery.php @@ -0,0 +1,10 @@ +setProviderFactorKey($factor->getFactorKey()) + ->attachFactor($factor) + ->setStatus(self::STATUS_ACTIVE); + } + + protected function getConfiguration() { + return array( + self::CONFIG_SERIALIZATION => array( + 'properties' => self::SERIALIZATION_JSON, + ), + self::CONFIG_AUX_PHID => true, + self::CONFIG_COLUMN_SCHEMA => array( + 'providerFactorKey' => 'text64', + 'name' => 'text255', + 'status' => 'text32', + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorAuthAuthFactorProviderPHIDType::TYPECONST; + } + + public function getURI() { + return '/auth/mfa/'.$this->getID().'/'; + } + + public function getObjectName() { + return pht('MFA Provider %d', $this->getID()); + } + + public function getAuthFactorProviderProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setAuthFactorProviderProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function attachFactor(PhabricatorAuthFactor $factor) { + $this->factor = $factor; + return $this; + } + + public function getFactor() { + return $this->assertAttached($this->factor); + } + + public function getDisplayName() { + $name = $this->getName(); + if (strlen($name)) { + return $name; + } + + return $this->getFactor()->getFactorName(); + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorAuthFactorProviderEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorAuthFactorProviderTransaction(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::getMostOpenPolicy(); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + $extended = array(); + + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + break; + case PhabricatorPolicyCapability::CAN_EDIT: + $extended[] = array( + new PhabricatorAuthApplication(), + AuthManageProvidersCapability::CAPABILITY, + ); + break; + } + + return $extended; + } + + +} diff --git a/src/applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php b/src/applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php new file mode 100644 index 0000000000..0b7b7fc6a7 --- /dev/null +++ b/src/applications/auth/storage/PhabricatorAuthFactorProviderTransaction.php @@ -0,0 +1,18 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!strlen($old)) { + return pht( + '%s named this provider %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else if (!strlen($new)) { + return pht( + '%s removed the name (%s) of this provider.', + $this->renderAuthor(), + $this->renderOldValue()); + } else { + return pht( + '%s renamed this provider from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $max_length = $object->getColumnMaximumByteLength('name'); + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + $new_length = strlen($new_value); + if ($new_length > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'Provider names can not be longer than %s characters.', + new PhutilNumber($max_length)), + $xaction); + } + } + + return $errors; + } + + public function getTransactionTypeForConduit($xaction) { + return 'name'; + } + + public function getFieldValuesForConduit($xaction, $data) { + return array( + 'old' => $xaction->getOldValue(), + 'new' => $xaction->getNewValue(), + ); + } + +} diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php new file mode 100644 index 0000000000..fe17eee545 --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php @@ -0,0 +1,4 @@ +