From 755c40221d5fc58f6d29457d77bc9653c1431fc1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 19 Jan 2019 05:10:02 -0800 Subject: [PATCH 01/42] Temporarily disable transaction story links in HTML mail for the deploy Ref T12921. See that task for discussion. Behavioral revert of D19968. --- .../storage/PhabricatorApplicationTransaction.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 515fd87394..efbbdb4a09 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -785,7 +785,11 @@ abstract class PhabricatorApplicationTransaction } public function getTitleForHTMLMail() { - $title = $this->getTitleForMailWithRenderingTarget(self::TARGET_HTML); + // TODO: For now, rendering this with TARGET_HTML generates links with + // bad targets ("/x/y/" instead of "https://dev.example.com/x/y/"). Throw + // a rug over the issue for the moment. See T12921. + + $title = $this->getTitleForMailWithRenderingTarget(self::TARGET_TEXT); if ($title === null) { return null; } From be8b7c9ebac76daa1475546821fc7dbf01f9fa30 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 20 Jan 2019 06:56:23 -0800 Subject: [PATCH 02/42] Fix "Welcome Mail" check for a message when no message exists Summary: Fixes T13239. See that task for discussion. Test Plan: Tried to send welcome mail with no "Welcome" message. Maniphest Tasks: T13239 Differential Revision: https://secure.phabricator.com/D20001 --- .../people/controller/PhabricatorPeopleWelcomeController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/people/controller/PhabricatorPeopleWelcomeController.php b/src/applications/people/controller/PhabricatorPeopleWelcomeController.php index 3fb75265ff..94d5e0bb03 100644 --- a/src/applications/people/controller/PhabricatorPeopleWelcomeController.php +++ b/src/applications/people/controller/PhabricatorPeopleWelcomeController.php @@ -51,7 +51,7 @@ final class PhabricatorPeopleWelcomeController $default_message = PhabricatorAuthMessage::loadMessage( $admin, PhabricatorAuthWelcomeMailMessageType::MESSAGEKEY); - if (strlen($default_message->getMessageText())) { + if ($default_message && strlen($default_message->getMessageText())) { $message_instructions = pht( 'The email will identify you as the sender. You may optionally '. 'replace the [[ %s | default custom mail body ]] with different text '. From 0db29e624cc2918205e68640f3d25c904c9c0d7e Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 20 Jan 2019 09:27:48 -0800 Subject: [PATCH 03/42] Provide a richer error when an intracluster request can not be satisfied by the target node Summary: See PHI1030. When installs hit this error, provide more details about which node we ended up on and what's going on. Test Plan: ``` $ git pull phabricator-ssh-exec: This repository request (for repository "spellbook") has been incorrectly routed to a cluster host (with device name "local.phacility.net", and hostname "orbital-3.local") which can not serve the request. 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. 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. (This is a read request.) fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. ``` Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20002 --- .../storage/PhabricatorRepository.php | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index e1febe7ac8..9dfafbcf59 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -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) { From afd2ace0dc9cd5d35635f5582fcbc0810c8af3ba Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 14 Jan 2019 12:33:28 -0800 Subject: [PATCH 04/42] Apply inverse edge edits after committing primary object edits Summary: Fixes T13082. When you create a revision (say, `D111`) with `Ref T222` in the body, we write a `D111 -> T222` edge ("revision 111 references task 222") and an inverse `T222 -> D111` edge ("task 222 is referenced by revision 111"). We also apply a transaction to `D111` ("alice added a task: Txxx.") and an inverse transaction to `T222` ("alice added a revision: Dxxx"). Currently, it appears that the inverse transaction can sometimes generate mail faster than `D111` actually commits its (database) transactions, so the mail says "alice added a revision: Unknown Object (Differential Revision)". See T13082 for evidence that this is true, and a reproduction case. To fix this, apply the inverse transaction (to `T222`) after we commit the main object (here, `D111`). This is tricky because when we apply transactions, the transaction editor automatically "fixes" them to be consistent with the database state. For example, if a task already has title "XYZ" and you set the title to "XYZ" (same title), we just no-op the transaction. It also fixes edge edits. The old sequence was: - Open (database) transaction. - Apply our transaction ("alice added a task"). - Apply the inverse transaction ("alice added a revision"). - Write the edges to the database. - Commit (database) transaction. Under this sequence, the inverse transaction was "correct" and didn't need to be fixed, so the fixing step didn't touch it. The new sequence is: - Open (database) transaction. - Apply our transaction ("alice added a task"). - Write the edges. - Commit (database) transaction. - Apply the inverse transaction ("alice added a revision"). Since the inverse transaction now happens after the database edge write, the fixing step detects that it's a no-op and throws it away if we do this naively. Instead, add some special cases around inverse edits to skip the correction/fixing logic, and just pass the "right" values in the first place. Test Plan: Added and removed related tasks from revisions, saw appropriate transactions render on both objects. (It's hard to be certain this completely fixes the issue since it only happened occasionally in the first place, but we can see if it happens any more on `secure`.) Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13082, T222 Differential Revision: https://secure.phabricator.com/D19969 --- ...habricatorApplicationTransactionEditor.php | 82 +++++++++++++++---- 1 file changed, 67 insertions(+), 15 deletions(-) diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 64f375fd88..e77c2c7ba1 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -447,6 +447,12 @@ abstract class PhabricatorApplicationTransactionEditor 'edge:type')); } + // See T13082. If this is an inverse edit, the parent editor has + // already populated the transaction values correctly. + if ($this->getIsInverseEdgeEditor()) { + return $xaction->getOldValue(); + } + $old_edges = array(); if ($object->getPHID()) { $edge_src = $object->getPHID(); @@ -513,6 +519,12 @@ abstract class PhabricatorApplicationTransactionEditor return $space_phid; } case PhabricatorTransactions::TYPE_EDGE: + // See T13082. If this is an inverse edit, the parent editor has + // already populated appropriate transaction values. + if ($this->getIsInverseEdgeEditor()) { + return $xaction->getNewValue(); + } + $new_value = $this->getEdgeTransactionNewValue($xaction); $edge_type = $xaction->getMetadataValue('edge:type'); @@ -790,14 +802,6 @@ abstract class PhabricatorApplicationTransactionEditor $src = $object->getPHID(); $const = $xaction->getMetadataValue('edge:type'); - $type = PhabricatorEdgeType::getByConstant($const); - if ($type->shouldWriteInverseTransactions()) { - $this->applyInverseEdgeTransactions( - $object, - $xaction, - $type->getInverseEdgeConstant()); - } - foreach ($new as $dst_phid => $edge) { $new[$dst_phid]['src'] = $src; } @@ -900,6 +904,30 @@ abstract class PhabricatorApplicationTransactionEditor foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); + // See T13082. When we're writing edges that imply corresponding inverse + // transactions, apply those inverse transactions now. We have to wait + // until the object we're editing (with this editor) has committed its + // transactions to do this. If we don't, the inverse editor may race, + // build a mail before we actually commit this object, and render "alice + // added an edge: Unknown Object". + + if ($type === PhabricatorTransactions::TYPE_EDGE) { + // Don't do anything if we're already an inverse edge editor. + if ($this->getIsInverseEdgeEditor()) { + continue; + } + + $edge_const = $xaction->getMetadataValue('edge:type'); + $edge_type = PhabricatorEdgeType::getByConstant($edge_const); + if ($edge_type->shouldWriteInverseTransactions()) { + $this->applyInverseEdgeTransactions( + $object, + $xaction, + $edge_type->getInverseEdgeConstant()); + } + continue; + } + $xtype = $this->getModularTransactionType($type); if (!$xtype) { continue; @@ -1504,6 +1532,12 @@ abstract class PhabricatorApplicationTransactionEditor $expect_value = !$xaction->shouldGenerateOldValue(); $has_value = $xaction->hasOldValue(); + // See T13082. In the narrow case of applying inverse edge edits, we + // expect the old value to be populated. + if ($this->getIsInverseEdgeEditor()) { + $expect_value = true; + } + if ($expect_value && !$has_value) { throw new PhabricatorApplicationTransactionStructureException( $xaction, @@ -3853,6 +3887,8 @@ abstract class PhabricatorApplicationTransactionEditor ->withPHIDs($all) ->execute(); + $object_phid = $object->getPHID(); + foreach ($nodes as $node) { if (!($node instanceof PhabricatorApplicationTransactionInterface)) { continue; @@ -3865,22 +3901,38 @@ abstract class PhabricatorApplicationTransactionEditor continue; } + $node_phid = $node->getPHID(); $editor = $node->getApplicationTransactionEditor(); $template = $node->getApplicationTransactionTemplate(); - if (isset($add[$node->getPHID()])) { - $edge_edit_type = '+'; + // See T13082. We have to build these transactions with synthetic values + // because we've already applied the actual edit to the edge database + // table. If we try to apply this transaction naturally, it will no-op + // itself because it doesn't have any effect. + + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array($node_phid)) + ->withEdgeTypes(array($inverse_type)); + + $edge_query->execute(); + + $edge_phids = $edge_query->getDestinationPHIDs(); + $edge_phids = array_fuse($edge_phids); + + $new_phids = $edge_phids; + $old_phids = $edge_phids; + + if (isset($add[$node_phid])) { + unset($old_phids[$object_phid]); } else { - $edge_edit_type = '-'; + $old_phids[$object_phid] = $object_phid; } $template ->setTransactionType($xaction->getTransactionType()) ->setMetadataValue('edge:type', $inverse_type) - ->setNewValue( - array( - $edge_edit_type => array($object->getPHID() => $object->getPHID()), - )); + ->setOldValue($old_phids) + ->setNewValue($new_phids); $editor ->setContinueOnNoEffect(true) From 881d79c1eab3a7a55911d72f1e0e0fb5bb39c771 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 20 Jan 2019 20:27:32 -0800 Subject: [PATCH 05/42] When dirtying repository cluster routing caches after an Almanac edit, discover linked bindings from devices Summary: See PHI1030. When you edit an Almanac object, we attempt to discover all the related objects so we can dirty the repository cluster routing cache: if you modify a Device or Service that's part of a clustered repository, we need to blow away our cached view of the layout. Currently, we don't correctly find linked Bindings when editing a Device, so we may miss Services which have keys that need to be disabled. Instead, discover these linked objects. See D17000 for the original implementation and more context. Test Plan: - Used `var_dump()` to dump out the discovered objects and dirtied cache keys. - Before change: editing a Service dirties repository routing keys (this is correct), but editing a Device does not. - After change: editing a Device now correctly dirties repository routing keys. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20003 --- .../engineextension/AlmanacCacheEngineExtension.php | 8 ++++++++ .../repository/storage/PhabricatorRepository.php | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/applications/almanac/engineextension/AlmanacCacheEngineExtension.php b/src/applications/almanac/engineextension/AlmanacCacheEngineExtension.php index 20c6bbcd71..d00926232d 100644 --- a/src/applications/almanac/engineextension/AlmanacCacheEngineExtension.php +++ b/src/applications/almanac/engineextension/AlmanacCacheEngineExtension.php @@ -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) { diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 9dfafbcf59..ed90f47f13 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -2172,7 +2172,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO $parts = array( "repo({$repository_phid})", "serv({$service_phid})", - 'v3', + 'v4', ); return implode('.', $parts); From 6138d5885d236071a595eda30fe8e8eef34b5c93 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Mon, 21 Jan 2019 11:56:56 -0800 Subject: [PATCH 06/42] Update documentation to reflect bin/auth changes Summary: See https://secure.phabricator.com/D18901#249481. Update the docs and a warning string to reflect the new reality that `bin/auth recover` is now able to recover any account, not just administrators. Test Plan: Mk 1 eyeball Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D20007 --- .../PhabricatorAuthStartController.php | 2 +- ...figuring_accounts_and_registration.diviner | 20 +++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php index 3dc8c61a51..29fa7e0b9f 100644 --- a/src/applications/auth/controller/PhabricatorAuthStartController.php +++ b/src/applications/auth/controller/PhabricatorAuthStartController.php @@ -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 ')); } diff --git a/src/docs/user/configuration/configuring_accounts_and_registration.diviner b/src/docs/user/configuration/configuring_accounts_and_registration.diviner index 8a4c59b193..05d11b11f3 100644 --- a/src/docs/user/configuration/configuring_accounts_and_registration.diviner +++ b/src/docs/user/configuration/configuring_accounts_and_registration.diviner @@ -14,8 +14,6 @@ there is a "Username/Password" authentication provider available, which allows users to log in with a traditional username and password. Other providers support logging in with other credentials. For example: - - **Username/Password:** Users use a username and password to log in or - register. - **LDAP:** Users use LDAP credentials to log in or register. - **OAuth:** Users use accounts on a supported OAuth2 provider (like GitHub, Facebook, or Google) to log in or register. @@ -30,16 +28,16 @@ After you add a provider, you can link it to existing accounts (for example, associate an existing Phabricator account with a GitHub OAuth account) or users can use it to register new accounts (assuming you enable these options). -= Recovering Administrator Accounts = += Recovering Inaccessible Accounts = -If you accidentally lock yourself out of Phabricator, you can use the `bin/auth` -script to recover access to an administrator account. To recover access, run: +If you accidentally lock yourself out of Phabricator (for example, by disabling +all authentication providers), you can use the `bin/auth` +script to recover access to an account. To recover access, run: phabricator/ $ ./bin/auth recover -...where `` is the admin account username you want to recover access -to. This will give you a link which will log you in as the specified -administrative user. +...where `` is the account username you want to recover access +to. This will generate a link which will log you in as the specified user. = Managing Accounts with the Web Console = @@ -57,9 +55,9 @@ To use the CLI script, run: phabricator/ $ ./bin/accountadmin -Some options (like setting passwords and changing certain account flags) are -only available from the CLI. You can also use this script to make a user -an administrator (if you accidentally remove your admin flag) or create an +Some options (like changing certain account flags) are only available from +the CLI. You can also use this script to make a user +an administrator (if you accidentally remove your admin flag) or to create an administrative account. = Next Steps = From c756bf3476175f4835490a2ee95a19d0c0662665 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Mon, 21 Jan 2019 12:34:16 -0800 Subject: [PATCH 07/42] Fix bin/accountadmin when not making changes Summary: If you go through the `accountadmin` flow and change nothing, you get an exception about the transaction not having any effect. Instead, let the `applyTransactions` call continue even on no effect. Test Plan: Ran `accountadmin` without changing anything for an existing user. No longer got an exception about no-effect transactions. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D20009 --- scripts/user/account_admin.php | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/user/account_admin.php b/scripts/user/account_admin.php index 4ad722e125..4e4500a2f7 100755 --- a/scripts/user/account_admin.php +++ b/scripts/user/account_admin.php @@ -218,6 +218,7 @@ $user->openTransaction(); ->setActor($actor) ->setActingAsPHID($people_application_phid) ->setContentSource($content_source) + ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $transaction_editor->applyTransactions($user, $xactions); From 0fcff782533aa446dedf6de2b074852a03c40d9a Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 15 Jan 2019 07:57:32 -0800 Subject: [PATCH 08/42] Convert user MFA factors to point at configurable "MFA Providers", not raw "MFA Factors" Summary: Ref T13222. Users configure "Factor Configs", which say "I have an entry on my phone for TOTP secret key XYZ". Currently, these point at raw implementations -- always "TOTP" in practice. To support configuring available MFA types (like "no MFA") and adding MFA types that need some options set (like "Duo", which needs API keys), bind "Factor Configs" to a "Factor Provider" instead. In the future, several "Factors" will be available (TOTP, SMS, Duo, Postal Mail, ...). Administrators configure zero or more "MFA Providers" they want to use (e.g., "Duo" + here's my API key). Then users can add configs for these providers (e.g., "here's my Duo account"). Upshot: - Factor: a PHP subclass, implements the technical details of a type of MFA factor (TOTP, SMS, Duo, etc). - FactorProvider: a storage object, owned by administrators, configuration of a Factor that says "this should be available on this install", plus provides API keys, a human-readable name, etc. - FactorConfig: a storage object, owned by a user, says "I have a factor for provider X on my phone/whatever with secret key Q / my duo account is X / my address is Y". Couple of things not covered here: - Statuses for providers ("Disabled", "Deprecated") don't do anything yet, but you can't edit them anyway. - Some `bin/auth` tools need to be updated. - When no providers are configured, the MFA panel should probably vanish. - Documentation. Test Plan: - Ran migration with providers, saw configs point at the first provider. - Ran migration without providers, saw a provider created and configs pointed at it. - Added/removed factors and providers. Passed MFA gates. Spot-checked database for general sanity. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19975 --- .../autopatches/20190115.mfa.01.provider.sql | 2 + .../autopatches/20190115.mfa.02.migrate.php | 72 ++++++++ .../autopatches/20190115.mfa.03.factorkey.sql | 2 + src/__phutil_library_map__.php | 7 +- .../engine/PhabricatorAuthSessionEngine.php | 27 ++- .../auth/factor/PhabricatorAuthFactor.php | 4 +- .../auth/factor/PhabricatorTOTPAuthFactor.php | 1 + .../PhabricatorAuthFactorConfigQuery.php | 88 +++++++++ .../PhabricatorAuthFactorProviderQuery.php | 27 +++ .../storage/PhabricatorAuthFactorConfig.php | 51 ++++-- .../storage/PhabricatorAuthFactorProvider.php | 23 +++ .../PhabricatorMultiFactorSettingsPanel.php | 171 +++++++++--------- 12 files changed, 364 insertions(+), 111 deletions(-) create mode 100644 resources/sql/autopatches/20190115.mfa.01.provider.sql create mode 100644 resources/sql/autopatches/20190115.mfa.02.migrate.php create mode 100644 resources/sql/autopatches/20190115.mfa.03.factorkey.sql create mode 100644 src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php diff --git a/resources/sql/autopatches/20190115.mfa.01.provider.sql b/resources/sql/autopatches/20190115.mfa.01.provider.sql new file mode 100644 index 0000000000..52e818f8d8 --- /dev/null +++ b/resources/sql/autopatches/20190115.mfa.01.provider.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_factorconfig + ADD factorProviderPHID VARBINARY(64) NOT NULL; diff --git a/resources/sql/autopatches/20190115.mfa.02.migrate.php b/resources/sql/autopatches/20190115.mfa.02.migrate.php new file mode 100644 index 0000000000..95a60789c3 --- /dev/null +++ b/resources/sql/autopatches/20190115.mfa.02.migrate.php @@ -0,0 +1,72 @@ +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']); +} diff --git a/resources/sql/autopatches/20190115.mfa.03.factorkey.sql b/resources/sql/autopatches/20190115.mfa.03.factorkey.sql new file mode 100644 index 0000000000..619787a838 --- /dev/null +++ b/resources/sql/autopatches/20190115.mfa.03.factorkey.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_factorconfig + DROP factorKey; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 8ee28d39a7..0800434676 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2207,6 +2207,7 @@ 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', 'PhabricatorAuthFactorProviderEditController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php', @@ -7888,7 +7889,11 @@ phutil_register_library_map(array( 'PhabricatorAuthDowngradeSessionController' => 'PhabricatorAuthController', 'PhabricatorAuthEditController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthFactor' => 'Phobject', - 'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO', + 'PhabricatorAuthFactorConfig' => array( + 'PhabricatorAuthDAO', + 'PhabricatorPolicyInterface', + ), + 'PhabricatorAuthFactorConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthFactorProvider' => array( 'PhabricatorAuthDAO', 'PhabricatorApplicationTransactionInterface', diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index 66a3e9e8fb..cef2323209 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -462,10 +462,14 @@ 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())) + ->setOrderVector(array('-id')) + ->execute(); // If the account has no associated multi-factor auth, just issue a token // without putting the session into high security mode. This is generally @@ -516,7 +520,8 @@ 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, @@ -552,7 +557,9 @@ final class PhabricatorAuthSessionEngine extends Phobject { // Limit factor verification rates to prevent brute force attacks. $any_attempt = false; foreach ($factors as $factor) { - $impl = $factor->requireImplementation(); + $provider = $factor->getFactorProvider(); + $impl = $provider->getFactor(); + if ($impl->getRequestHasChallengeResponse($factor, $request)) { $any_attempt = true; break; @@ -577,7 +584,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 +724,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, diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index ef6ab5d04b..f797ce3a15 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -7,6 +7,7 @@ abstract class PhabricatorAuthFactor extends Phobject { abstract public function getFactorCreateHelp(); abstract public function getFactorDescription(); abstract public function processAddFactorForm( + PhabricatorAuthFactorProvider $provider, AphrontFormView $form, AphrontRequest $request, PhabricatorUser $user); @@ -32,8 +33,7 @@ abstract class PhabricatorAuthFactor extends Phobject { protected function newConfigForUser(PhabricatorUser $user) { return id(new PhabricatorAuthFactorConfig()) - ->setUserPHID($user->getPHID()) - ->setFactorKey($this->getFactorKey()); + ->setUserPHID($user->getPHID()); } protected function newResult() { diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index 3632ca5c45..7f48455f4d 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -26,6 +26,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { } public function processAddFactorForm( + PhabricatorAuthFactorProvider $provider, AphrontFormView $form, AphrontRequest $request, PhabricatorUser $user) { diff --git a/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php b/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php new file mode 100644 index 0000000000..f40c12e48a --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php @@ -0,0 +1,88 @@ +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 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, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->userPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'userPHID IN (%Ls)', + $this->userPHIDs); + } + + return $where; + } + + 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; + } + + public function getQueryApplicationClass() { + return 'PhabricatorAuthApplication'; + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php b/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php index f4ce60773b..57b554885c 100644 --- a/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php +++ b/src/applications/auth/query/PhabricatorAuthFactorProviderQuery.php @@ -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; } diff --git a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php index 2bed939402..6b04b9eeab 100644 --- a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php @@ -1,14 +1,17 @@ true, self::CONFIG_COLUMN_SCHEMA => array( - 'factorKey' => 'text64', 'factorName' => 'text', 'factorSecret' => 'text', ), @@ -29,26 +31,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 +58,23 @@ final class PhabricatorAuthFactorConfig extends PhabricatorAuthDAO { return $this->sessionEngine; } + +/* -( 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; + } + } diff --git a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php index 13140395df..e61740b4d7 100644 --- a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php @@ -78,6 +78,29 @@ 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; + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php index 5fada0bbed..06f1547afa 100644 --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -16,7 +16,7 @@ final class PhabricatorMultiFactorSettingsPanel } public function processRequest(AphrontRequest $request) { - if ($request->getExists('new')) { + if ($request->getExists('new') || $request->getExists('providerPHID')) { return $this->processNew($request); } @@ -31,22 +31,18 @@ final class PhabricatorMultiFactorSettingsPanel $user = $this->getUser(); $viewer = $request->getUser(); - $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( - 'userPHID = %s', - $user->getPHID()); + $factors = id(new PhabricatorAuthFactorConfigQuery()) + ->setViewer($viewer) + ->withUserPHIDs(array($user->getPHID())) + ->setOrderVector(array('-id')) + ->execute(); $rows = array(); $rowc = array(); $highlight_id = $request->getInt('id'); foreach ($factors as $factor) { - - $impl = $factor->getImplementation(); - if ($impl) { - $type = $impl->getFactorName(); - } else { - $type = $factor->getFactorKey(); - } + $provider = $factor->getFactorProvider(); if ($factor->getID() == $highlight_id) { $rowc[] = 'highlighted'; @@ -62,7 +58,7 @@ final class PhabricatorMultiFactorSettingsPanel 'sigil' => 'workflow', ), $factor->getFactorName()), - $type, + $provider->getDisplayName(), phabricator_datetime($factor->getDateCreated(), $viewer), javelin_tag( 'a', @@ -128,88 +124,101 @@ final class PhabricatorMultiFactorSettingsPanel $viewer = $request->getUser(); $user = $this->getUser(); + $cancel_uri = $this->getPanelURI(); + + // Check that we have providers before we send the user through the MFA + // gate, so you don't authenticate and then immediately get roadblocked. + $providers = id(new PhabricatorAuthFactorProviderQuery()) + ->setViewer($viewer) + ->withStatuses(array(PhabricatorAuthFactorProvider::STATUS_ACTIVE)) + ->execute(); + if (!$providers) { + return $this->newDialog() + ->setTitle(pht('No MFA Providers')) + ->appendParagraph( + pht( + 'There are no active MFA providers. At least one active provider '. + 'must be available to add new MFA factors.')) + ->addCancelButton($cancel_uri); + } + $providers = mpull($providers, null, 'getPHID'); + $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, - $this->getPanelURI()); + $cancel_uri); - $factors = PhabricatorAuthFactor::getAllFactors(); + $selected_phid = $request->getStr('providerPHID'); + if (empty($providers[$selected_phid])) { + $selected_provider = null; + } else { + $selected_provider = $providers[$selected_phid]; + } + + if (!$selected_provider) { + $menu = id(new PHUIObjectItemListView()) + ->setViewer($viewer) + ->setBig(true) + ->setFlush(true); + + foreach ($providers as $provider_phid => $provider) { + $provider_uri = id(new PhutilURI($this->getPanelURI())) + ->setQueryParam('providerPHID', $provider_phid); + + $item = id(new PHUIObjectItemView()) + ->setHeader($provider->getDisplayName()) + ->setHref($provider_uri) + ->setClickable(true) + ->setImageIcon($provider->newIconView()) + ->addAttribute($provider->getDisplayDescription()); + + $menu->addItem($item); + } + + return $this->newDialog() + ->setTitle(pht('Choose Factor Type')) + ->appendChild($menu) + ->addCancelButton($cancel_uri); + } $form = id(new AphrontFormView()) - ->setUser($viewer); + ->setViewer($viewer); - $type = $request->getStr('type'); - if (empty($factors[$type]) || !$request->isFormPost()) { - $factor = null; - } else { - $factor = $factors[$type]; + $config = $selected_provider->processAddFactorForm( + $form, + $request, + $user); + + if ($config) { + $config->save(); + + $log = PhabricatorUserLog::initializeNewLog( + $viewer, + $user->getPHID(), + PhabricatorUserLog::ACTION_MULTI_ADD); + $log->save(); + + $user->updateMultiFactorEnrollment(); + + // Terminate other sessions so they must log in and survive the + // multi-factor auth check. + + id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( + $user, + new PhutilOpaqueEnvelope( + $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); + + return id(new AphrontRedirectResponse()) + ->setURI($this->getPanelURI('?id='.$config->getID())); } - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->addHiddenInput('new', true); - - if ($factor === null) { - $choice_control = id(new AphrontFormRadioButtonControl()) - ->setName('type') - ->setValue(key($factors)); - - foreach ($factors as $available_factor) { - $choice_control->addButton( - $available_factor->getFactorKey(), - $available_factor->getFactorName(), - $available_factor->getFactorDescription()); - } - - $dialog->appendParagraph( - pht( - 'Adding an additional authentication factor improves the security '. - 'of your account. Choose the type of factor to add:')); - - $form - ->appendChild($choice_control); - - } else { - $dialog->addHiddenInput('type', $type); - - $config = $factor->processAddFactorForm( - $form, - $request, - $user); - - if ($config) { - $config->save(); - - $log = PhabricatorUserLog::initializeNewLog( - $viewer, - $user->getPHID(), - PhabricatorUserLog::ACTION_MULTI_ADD); - $log->save(); - - $user->updateMultiFactorEnrollment(); - - // Terminate other sessions so they must log in and survive the - // multi-factor auth check. - - id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( - $user, - new PhutilOpaqueEnvelope( - $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); - - return id(new AphrontRedirectResponse()) - ->setURI($this->getPanelURI('?id='.$config->getID())); - } - } - - $dialog + return $this->newDialog() + ->addHiddenInput('providerPHID', $selected_provider->getPHID()) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle(pht('Add Authentication Factor')) ->appendChild($form->buildLayoutView()) ->addSubmitButton(pht('Continue')) - ->addCancelButton($this->getPanelURI()); - - return id(new AphrontDialogResponse()) - ->setDialog($dialog); + ->addCancelButton($cancel_uri); } private function processEdit(AphrontRequest $request) { From aa483738899d1c30cde3cf7f96d4fd8cd0055265 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 15 Jan 2019 13:34:31 -0800 Subject: [PATCH 09/42] Update `bin/auth` MFA commands for the new "MFA Provider" indirection layer Summary: Ref T13222. This updates the CLI tools and documentation for the changes in D19975. The flags `--type` and `--all-types` retain their current meaning. In most cases, `bin/auth strip --type totp` is sufficient and you don't need to bother looking up the relevant provider PHID. The existing `bin/auth list-factors` is also unchanged. The new `--provider` flag allows you to select configs from a particular provider in a more granular way. The new `bin/auth list-mfa-providers` provides an easy way to get PHIDs. (In the Phacility cluster, the "Strip MFA" action just reaches into the database and deletes rows manually, so this isn't terribly important. I verified that the code should still work properly.) Test Plan: - Ran `bin/auth list-mfa-providers`. - Stripped by user / type / provider. - Grepped for `list-factors` and `auth strip`. - Hit all (?) of the various possible error cases. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D19976 --- src/__phutil_library_map__.php | 3 + ...catorAuthManagementListFactorsWorkflow.php | 3 +- ...AuthManagementListMFAProvidersWorkflow.php | 33 +++++ ...PhabricatorAuthManagementStripWorkflow.php | 131 ++++++++++++------ .../PhabricatorAuthFactorConfigQuery.php | 13 ++ .../storage/PhabricatorAuthFactorConfig.php | 24 +++- .../user/userguide/multi_factor_auth.diviner | 21 ++- 7 files changed, 182 insertions(+), 46 deletions(-) create mode 100644 src/applications/auth/management/PhabricatorAuthManagementListMFAProvidersWorkflow.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0800434676..9a5341c0ba 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2254,6 +2254,7 @@ phutil_register_library_map(array( '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', @@ -7892,6 +7893,7 @@ phutil_register_library_map(array( 'PhabricatorAuthFactorConfig' => array( 'PhabricatorAuthDAO', 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', ), 'PhabricatorAuthFactorConfigQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthFactorProvider' => array( @@ -7948,6 +7950,7 @@ phutil_register_library_map(array( 'PhabricatorAuthManagementCachePKCS8Workflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementLDAPWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementListFactorsWorkflow' => 'PhabricatorAuthManagementWorkflow', + 'PhabricatorAuthManagementListMFAProvidersWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementRecoverWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementRefreshWorkflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementRevokeWorkflow' => 'PhabricatorAuthManagementWorkflow', diff --git a/src/applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php index 1367335cd2..1e4ab7d8df 100644 --- a/src/applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php +++ b/src/applications/auth/management/PhabricatorAuthManagementListFactorsWorkflow.php @@ -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()); diff --git a/src/applications/auth/management/PhabricatorAuthManagementListMFAProvidersWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementListMFAProvidersWorkflow.php new file mode 100644 index 0000000000..8121bf955f --- /dev/null +++ b/src/applications/auth/management/PhabricatorAuthManagementListMFAProvidersWorkflow.php @@ -0,0 +1,33 @@ +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; + } + +} diff --git a/src/applications/auth/management/PhabricatorAuthManagementStripWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementStripWorkflow.php index f25d05301b..22bfacb6f4 100644 --- a/src/applications/auth/management/PhabricatorAuthManagementStripWorkflow.php +++ b/src/applications/auth/management/PhabricatorAuthManagementStripWorkflow.php @@ -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 " 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 " or "--provider " 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.')); diff --git a/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php b/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php index f40c12e48a..1674573b68 100644 --- a/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php +++ b/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php @@ -6,6 +6,7 @@ final class PhabricatorAuthFactorConfigQuery private $ids; private $phids; private $userPHIDs; + private $factorProviderPHIDs; public function withIDs(array $ids) { $this->ids = $ids; @@ -22,6 +23,11 @@ final class PhabricatorAuthFactorConfigQuery return $this; } + public function withFactorProviderPHIDs(array $provider_phids) { + $this->factorProviderPHIDs = $provider_phids; + return $this; + } + public function newResultObject() { return new PhabricatorAuthFactorConfig(); } @@ -54,6 +60,13 @@ final class PhabricatorAuthFactorConfigQuery $this->userPHIDs); } + if ($this->factorProviderPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'factorProviderPHID IN (%Ls)', + $this->factorProviderPHIDs); + } + return $where; } diff --git a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php index 6b04b9eeab..9fbf3fbfb1 100644 --- a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php @@ -1,8 +1,11 @@ setViewer($engine->getViewer()) + ->withPHIDs(array($this->getUserPHID())) + ->executeOne(); + + $this->delete(); + + if ($user) { + $user->updateMultiFactorEnrollment(); + } + } + } diff --git a/src/docs/user/userguide/multi_factor_auth.diviner b/src/docs/user/userguide/multi_factor_auth.diviner index c17c80d296..44811e16f2 100644 --- a/src/docs/user/userguide/multi_factor_auth.diviner +++ b/src/docs/user/userguide/multi_factor_auth.diviner @@ -126,9 +126,28 @@ You can run `bin/auth help strip` for more detail and all available flags and arguments. This command can selectively strip types of factors. You can use -`bin/auth list-factors` for a list of available factor types. +`bin/auth list-factors` to get a list of available factor types. ```lang=console # Show supported factor types. phabricator/ $ ./bin/auth list-factors ``` + +Once you've identified the factor types you want to strip, you can strip them +using the `--type` flag to specify one or more factor types: + +```lang=console +# Strip all SMS and TOTP factors for a user. +phabricator/ $ ./bin/auth strip --user --type sms --type totp +``` + +The `bin/auth strip` command can also selectively strip factors for certain +providers. This is more granular than stripping all factors of a given type. +You can use `bin/auth list-mfa-providers` to get a list of providers. + +Once you have a provider PHID, use `--provider` to select factors to strip: + +```lang=console +# Strip all factors for a particular provider. +phabricator/ $ ./bin/auth strip --user --provider +``` From f0c6ee48233a199ab35ae64293fbed99305a1316 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 16 Jan 2019 09:56:44 -0800 Subject: [PATCH 10/42] Add "Contact Numbers" so we can send users SMS mesages Summary: Ref T920. To send you SMS messages, we need to know your phone number. This adds bare-bone basics (transactions, storage, editor, etc). From here: **Disabling Numbers**: I'll let you disable numbers in an upcoming diff. **Primary Number**: I think I'm just going to let you pick a number as "primary", similar to how email works. We could imagine a world where you have one "MFA" number and one "notifications" number, but this seems unlikely-ish? **Publishing Numbers (Profile / API)**: At some point, we could let you say that a number is public / "show on my profile" and provide API access / directory features. Not planning to touch this for now. **Non-Phone Numbers**: Eventually this could be a list of other similar contact mechanisms (APNS/GCM devices, Whatsapp numbers, ICQ number, twitter handle so MFA can slide into your DM's?). Not planning to touch this for now, but the path should be straightforward when we get there. This is why it's called "Contact Number", not "Phone Number". **MFA-Required + SMS**: Right now, if the only MFA provider is SMS and MFA is required on the install, you can't actually get into Settings to add a contact number to configure SMS. I'll look at the best way to deal with this in an upcoming diff -- likely, giving you partial access to more of Setings before you get thorugh the MFA gate. Conceptually, it seems reasonable to let you adjust some other settings, like "Language" and "Accessibility", before you set up MFA, so if the "you need to add MFA" portal was more like a partial Settings screen, maybe that's pretty reasonable. **Verifying Numbers**: We'll probably need to tackle this eventually, but I'm not planning to worry about it for now. Test Plan: {F6137174} Reviewers: amckinley Reviewed By: amckinley Subscribers: avivey, PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T920 Differential Revision: https://secure.phabricator.com/D19988 --- .../20190116.contact.01.number.sql | 11 ++ .../20190116.contact.02.xaction.sql | 19 +++ src/__phutil_library_map__.php | 33 ++++ .../PhabricatorAuthApplication.php | 6 + ...PhabricatorAuthContactNumberController.php | 16 ++ ...ricatorAuthContactNumberEditController.php | 12 ++ ...ricatorAuthContactNumberViewController.php | 98 ++++++++++++ ...PhabricatorAuthContactNumberEditEngine.php | 86 +++++++++++ .../PhabricatorAuthContactNumberEditor.php | 38 +++++ .../PhabricatorAuthContactNumberPHIDType.php | 38 +++++ .../PhabricatorAuthContactNumberQuery.php | 90 +++++++++++ ...catorAuthContactNumberTransactionQuery.php | 10 ++ .../storage/PhabricatorAuthContactNumber.php | 141 ++++++++++++++++++ ...habricatorAuthContactNumberTransaction.php | 18 +++ ...atorAuthContactNumberNumberTransaction.php | 91 +++++++++++ ...icatorAuthContactNumberTransactionType.php | 4 + .../message/PhabricatorPhoneNumber.php | 14 +- .../PhabricatorPhoneNumberTestCase.php | 37 +++++ .../view/PhabricatorSearchResultView.php | 2 +- ...PhabricatorContactNumbersSettingsPanel.php | 69 +++++++++ 20 files changed, 830 insertions(+), 3 deletions(-) create mode 100644 resources/sql/autopatches/20190116.contact.01.number.sql create mode 100644 resources/sql/autopatches/20190116.contact.02.xaction.sql create mode 100644 src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php create mode 100644 src/applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php create mode 100644 src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php create mode 100644 src/applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php create mode 100644 src/applications/auth/editor/PhabricatorAuthContactNumberEditor.php create mode 100644 src/applications/auth/phid/PhabricatorAuthContactNumberPHIDType.php create mode 100644 src/applications/auth/query/PhabricatorAuthContactNumberQuery.php create mode 100644 src/applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php create mode 100644 src/applications/auth/storage/PhabricatorAuthContactNumber.php create mode 100644 src/applications/auth/storage/PhabricatorAuthContactNumberTransaction.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php create mode 100644 src/applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php create mode 100644 src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php diff --git a/resources/sql/autopatches/20190116.contact.01.number.sql b/resources/sql/autopatches/20190116.contact.01.number.sql new file mode 100644 index 0000000000..14e2b78d1d --- /dev/null +++ b/resources/sql/autopatches/20190116.contact.01.number.sql @@ -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}; diff --git a/resources/sql/autopatches/20190116.contact.02.xaction.sql b/resources/sql/autopatches/20190116.contact.02.xaction.sql new file mode 100644 index 0000000000..bd0d361bc5 --- /dev/null +++ b/resources/sql/autopatches/20190116.contact.02.xaction.sql @@ -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}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9a5341c0ba..132ea75367 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2200,6 +2200,18 @@ 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', + 'PhabricatorAuthContactNumberEditController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php', + 'PhabricatorAuthContactNumberEditEngine' => 'applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php', + 'PhabricatorAuthContactNumberEditor' => 'applications/auth/editor/PhabricatorAuthContactNumberEditor.php', + 'PhabricatorAuthContactNumberNumberTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php', + 'PhabricatorAuthContactNumberPHIDType' => 'applications/auth/phid/PhabricatorAuthContactNumberPHIDType.php', + 'PhabricatorAuthContactNumberQuery' => 'applications/auth/query/PhabricatorAuthContactNumberQuery.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', @@ -2739,6 +2751,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', @@ -3870,6 +3883,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', @@ -7884,6 +7898,23 @@ phutil_register_library_map(array( 'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod', 'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker', 'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController', + 'PhabricatorAuthContactNumber' => array( + 'PhabricatorAuthDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', + ), + 'PhabricatorAuthContactNumberController' => 'PhabricatorAuthController', + 'PhabricatorAuthContactNumberEditController' => 'PhabricatorAuthContactNumberController', + 'PhabricatorAuthContactNumberEditEngine' => 'PhabricatorEditEngine', + 'PhabricatorAuthContactNumberEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorAuthContactNumberNumberTransaction' => 'PhabricatorAuthContactNumberTransactionType', + 'PhabricatorAuthContactNumberPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorAuthContactNumberQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorAuthContactNumberTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorAuthContactNumberTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorAuthContactNumberTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorAuthContactNumberViewController' => 'PhabricatorAuthContactNumberController', 'PhabricatorAuthController' => 'PhabricatorController', 'PhabricatorAuthDAO' => 'PhabricatorLiskDAO', 'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController', @@ -8524,6 +8555,7 @@ phutil_register_library_map(array( 'PhabricatorConpherenceWidgetVisibleSetting' => 'PhabricatorInternalSetting', 'PhabricatorConsoleApplication' => 'PhabricatorApplication', 'PhabricatorConsoleContentSource' => 'PhabricatorContentSource', + 'PhabricatorContactNumbersSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorContentSource' => 'Phobject', 'PhabricatorContentSourceModule' => 'PhabricatorConfigModule', 'PhabricatorContentSourceView' => 'AphrontView', @@ -9816,6 +9848,7 @@ phutil_register_library_map(array( 'PhabricatorPholioApplication' => 'PhabricatorApplication', 'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorPhoneNumber' => 'Phobject', + 'PhabricatorPhoneNumberTestCase' => 'PhabricatorTestCase', 'PhabricatorPhortuneApplication' => 'PhabricatorApplication', 'PhabricatorPhortuneContentSource' => 'PhabricatorContentSource', 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow', diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index 2c36e935ee..20547d8ca3 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -104,6 +104,12 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { 'PhabricatorAuthMessageViewController', ), + 'contact/' => array( + $this->getEditRoutePattern('edit/') => + 'PhabricatorAuthContactNumberEditController', + '(?P[1-9]\d*)/' => + 'PhabricatorAuthContactNumberViewController', + ), ), '/oauth/(?P\w+)/login/' diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php new file mode 100644 index 0000000000..a713f48a3b --- /dev/null +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php @@ -0,0 +1,16 @@ +addTextCrumb( + pht('Contact Numbers'), + pht('/settings/panel/contact/')); + + return $crumbs; + } + +} diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php new file mode 100644 index 0000000000..95764496da --- /dev/null +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php @@ -0,0 +1,12 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php new file mode 100644 index 0000000000..5423d93dcd --- /dev/null +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php @@ -0,0 +1,98 @@ +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); + + 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)); + + return $curtain; + } + +} diff --git a/src/applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php b/src/applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php new file mode 100644 index 0000000000..5b1a059b2f --- /dev/null +++ b/src/applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php @@ -0,0 +1,86 @@ +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), + ); + } + +} diff --git a/src/applications/auth/editor/PhabricatorAuthContactNumberEditor.php b/src/applications/auth/editor/PhabricatorAuthContactNumberEditor.php new file mode 100644 index 0000000000..9dfb569e89 --- /dev/null +++ b/src/applications/auth/editor/PhabricatorAuthContactNumberEditor.php @@ -0,0 +1,38 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $contact_number = $objects[$phid]; + } + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthContactNumberQuery.php b/src/applications/auth/query/PhabricatorAuthContactNumberQuery.php new file mode 100644 index 0000000000..10cfba7a65 --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthContactNumberQuery.php @@ -0,0 +1,90 @@ +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 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); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorAuthApplication'; + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php b/src/applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php new file mode 100644 index 0000000000..a443cbab42 --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php @@ -0,0 +1,10 @@ + array( + 'properties' => self::SERIALIZATION_JSON, + ), + self::CONFIG_AUX_PHID => true, + self::CONFIG_COLUMN_SCHEMA => array( + 'contactNumber' => 'text255', + 'status' => 'text32', + 'uniqueKey' => 'bytes12?', + ), + 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()); + } + + 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')); + } + + return id(new PHUIIconView()) + ->setIcon('fa-mobile', 'green') + ->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() { + $this->uniqueKey = $this->newUniqueKey(); + return parent::save(); + } + + +/* -( 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(); + } + + +} diff --git a/src/applications/auth/storage/PhabricatorAuthContactNumberTransaction.php b/src/applications/auth/storage/PhabricatorAuthContactNumberTransaction.php new file mode 100644 index 0000000000..d6faccf497 --- /dev/null +++ b/src/applications/auth/storage/PhabricatorAuthContactNumberTransaction.php @@ -0,0 +1,18 @@ +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; + } + } + + } + + return $errors; + } + +} diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php new file mode 100644 index 0000000000..c32fbe6a30 --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php @@ -0,0 +1,4 @@ +number = $number; } diff --git a/src/applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php b/src/applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php new file mode 100644 index 0000000000..4a5da3bcc5 --- /dev/null +++ b/src/applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php @@ -0,0 +1,37 @@ + '+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)); + } + } + + } + +} diff --git a/src/applications/search/view/PhabricatorSearchResultView.php b/src/applications/search/view/PhabricatorSearchResultView.php index 6c527733e8..b209b4422a 100644 --- a/src/applications/search/view/PhabricatorSearchResultView.php +++ b/src/applications/search/view/PhabricatorSearchResultView.php @@ -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. diff --git a/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php b/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php new file mode 100644 index 0000000000..0bfe747ec3 --- /dev/null +++ b/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php @@ -0,0 +1,69 @@ +getUser(); + $viewer = $request->getUser(); + + $numbers = id(new PhabricatorAuthContactNumberQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($user->getPHID())) + ->execute(); + + $rows = array(); + foreach ($numbers as $number) { + $rows[] = array( + $number->newIconView(), + phutil_tag( + 'a', + array( + 'href' => $number->getURI(), + ), + $number->getDisplayName()), + phabricator_datetime($number->getDateCreated(), $viewer), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setNoDataString( + pht("You haven't added any contact numbers to your account.")) + ->setHeaders( + array( + null, + pht('Number'), + pht('Created'), + )) + ->setColumnClasses( + array( + null, + 'wide pri', + '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); + } + +} From f713fa1fd7ec0096c3ca2245c782d4aae03a593d Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 21 Jan 2019 09:50:54 -0800 Subject: [PATCH 11/42] Expand "Settings" UI to full-width Summary: Depends on D19988. See D19826 for the last UI expansion. I don't have an especially strong product rationale for un-fixed-width'ing Settings since it doesn't suffer from the "mystery meat actions" issues that other fixed-width UIs do, but I like the full-width UI better and the other other fixed-width UIs all (?) have some actual rationale (e.g., large tables, multiple actions on subpanels), so "consistency" is an argument here. Also rename "account" to "language" since both settings are language-related. This moves away from the direction in D18436. Test Plan: Clicked each Settings panel, saw sensible rendering at full-width. {F6145944} Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20005 --- src/__phutil_library_map__.php | 4 ++-- .../controller/PhabricatorSettingsMainController.php | 8 +++----- .../settings/editor/PhabricatorSettingsEditEngine.php | 2 +- ...ingsPanel.php => PhabricatorLanguageSettingsPanel.php} | 6 +++--- .../panelgroup/PhabricatorSettingsAccountPanelGroup.php | 2 +- .../settings/setting/PhabricatorPronounSetting.php | 2 +- .../settings/setting/PhabricatorTranslationSetting.php | 2 +- .../transactions/editengine/PhabricatorEditEngine.php | 8 +++----- 8 files changed, 15 insertions(+), 19 deletions(-) rename src/applications/settings/panel/{PhabricatorAccountSettingsPanel.php => PhabricatorLanguageSettingsPanel.php} (75%) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 132ea75367..7932a7e79e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -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', @@ -3362,6 +3361,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', @@ -7756,7 +7756,6 @@ phutil_register_library_map(array( 'PhabricatorAccessLog' => 'Phobject', 'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting', - 'PhabricatorAccountSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorActionListView' => 'AphrontTagView', 'PhabricatorActionView' => 'AphrontView', 'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel', @@ -9244,6 +9243,7 @@ phutil_register_library_map(array( 'PhabricatorKeyringConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'PhabricatorLDAPAuthProvider' => 'PhabricatorAuthProvider', 'PhabricatorLabelProfileMenuItem' => 'PhabricatorProfileMenuItem', + 'PhabricatorLanguageSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorLegalpadApplication' => 'PhabricatorApplication', 'PhabricatorLegalpadDocumentPHIDType' => 'PhabricatorPHIDType', 'PhabricatorLegalpadSignaturePolicyRule' => 'PhabricatorPolicyRule', diff --git a/src/applications/settings/controller/PhabricatorSettingsMainController.php b/src/applications/settings/controller/PhabricatorSettingsMainController.php index 9dc84a9bd0..46246c3ce5 100644 --- a/src/applications/settings/controller/PhabricatorSettingsMainController.php +++ b/src/applications/settings/controller/PhabricatorSettingsMainController.php @@ -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) { diff --git a/src/applications/settings/editor/PhabricatorSettingsEditEngine.php b/src/applications/settings/editor/PhabricatorSettingsEditEngine.php index 30e831543d..4f16d0338b 100644 --- a/src/applications/settings/editor/PhabricatorSettingsEditEngine.php +++ b/src/applications/settings/editor/PhabricatorSettingsEditEngine.php @@ -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'); } diff --git a/src/applications/settings/panel/PhabricatorAccountSettingsPanel.php b/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php similarity index 75% rename from src/applications/settings/panel/PhabricatorAccountSettingsPanel.php rename to src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php index a7eb3ad099..9b846bd4b6 100644 --- a/src/applications/settings/panel/PhabricatorAccountSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php @@ -1,12 +1,12 @@ setHeader($page_header); } + $view->setFooter($content); + $page = $controller->newPage() ->setTitle($header_text) ->setCrumbs($crumbs) @@ -1256,11 +1258,7 @@ abstract class PhabricatorEditEngine $navigation = $this->getNavigation(); if ($navigation) { - $view->setFixed(true); - $view->setNavigation($navigation); - $view->setMainColumn($content); - } else { - $view->setFooter($content); + $page->setNavigation($navigation); } return $page; From d6d93dd6582222142db818e4fbb98023a881a393 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 21 Jan 2019 10:31:23 -0800 Subject: [PATCH 12/42] Add icons to Settings Summary: Depends on D20005. I love icons. Test Plan: {F6145996} Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20006 --- .../PhabricatorConduitTokensSettingsPanel.php | 4 ++++ .../panel/DiffusionSetPasswordSettingsPanel.php | 4 ++++ ...atorOAuthServerAuthorizationsSettingsPanel.php | 4 ++++ .../PhabricatorSettingsMainController.php | 6 +++++- .../panel/PhabricatorActivitySettingsPanel.php | 4 ++++ ...ricatorConpherencePreferencesSettingsPanel.php | 4 ++++ .../PhabricatorContactNumbersSettingsPanel.php | 4 ++++ .../panel/PhabricatorDateTimeSettingsPanel.php | 4 ++++ ...abricatorDeveloperPreferencesSettingsPanel.php | 4 ++++ .../PhabricatorDiffPreferencesSettingsPanel.php | 4 ++++ ...PhabricatorDisplayPreferencesSettingsPanel.php | 4 ++++ .../PhabricatorEmailAddressesSettingsPanel.php | 4 ++++ .../PhabricatorEmailDeliverySettingsPanel.php | 4 ++++ .../panel/PhabricatorEmailFormatSettingsPanel.php | 15 ++++----------- .../PhabricatorEmailPreferencesSettingsPanel.php | 4 ++++ .../PhabricatorExternalAccountsSettingsPanel.php | 4 ++++ .../panel/PhabricatorLanguageSettingsPanel.php | 4 ++++ .../panel/PhabricatorMultiFactorSettingsPanel.php | 4 ++++ .../PhabricatorNotificationsSettingsPanel.php | 4 ++++ .../panel/PhabricatorPasswordSettingsPanel.php | 4 ++++ .../panel/PhabricatorSSHKeysSettingsPanel.php | 4 ++++ .../panel/PhabricatorSessionsSettingsPanel.php | 4 ++++ .../settings/panel/PhabricatorSettingsPanel.php | 10 ++++++++++ .../panel/PhabricatorTokensSettingsPanel.php | 4 ++++ 24 files changed, 103 insertions(+), 12 deletions(-) diff --git a/src/applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php b/src/applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php index 2075582386..cd97e2fd7f 100644 --- a/src/applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php +++ b/src/applications/conduit/settings/PhabricatorConduitTokensSettingsPanel.php @@ -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; } diff --git a/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php b/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php index 1899302223..789adfbf57 100644 --- a/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php +++ b/src/applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php @@ -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; } diff --git a/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php b/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php index 37e85ab53b..89a1cc0281 100644 --- a/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php +++ b/src/applications/oauthserver/panel/PhabricatorOAuthServerAuthorizationsSettingsPanel.php @@ -11,6 +11,10 @@ final class PhabricatorOAuthServerAuthorizationsSettingsPanel return pht('OAuth Authorizations'); } + public function getPanelMenuIcon() { + return 'fa-exchange'; + } + public function getPanelGroupKey() { return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/controller/PhabricatorSettingsMainController.php b/src/applications/settings/controller/PhabricatorSettingsMainController.php index 46246c3ce5..ded20a8e96 100644 --- a/src/applications/settings/controller/PhabricatorSettingsMainController.php +++ b/src/applications/settings/controller/PhabricatorSettingsMainController.php @@ -209,7 +209,11 @@ final class PhabricatorSettingsMainController } } - $nav->addFilter($panel->getPanelKey(), $panel->getPanelName()); + $nav->addFilter( + $panel->getPanelKey(), + $panel->getPanelName(), + null, + $panel->getPanelMenuIcon()); } return $nav; diff --git a/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php b/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php index 2759f3a26c..a3654a4388 100644 --- a/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php @@ -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; } diff --git a/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php index 6ed6325d67..3ce72af2f8 100644 --- a/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorConpherencePreferencesSettingsPanel.php @@ -9,6 +9,10 @@ final class PhabricatorConpherencePreferencesSettingsPanel return pht('Conpherence'); } + public function getPanelMenuIcon() { + return 'fa-comment-o'; + } + public function getPanelGroupKey() { return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php b/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php index 0bfe747ec3..7834bff593 100644 --- a/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php @@ -11,6 +11,10 @@ final class PhabricatorContactNumbersSettingsPanel return pht('Contact Numbers'); } + public function getPanelMenuIcon() { + return 'fa-mobile'; + } + public function getPanelGroupKey() { return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php index e5ca46510e..285bc6989f 100644 --- a/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorDateTimeSettingsPanel.php @@ -9,6 +9,10 @@ final class PhabricatorDateTimeSettingsPanel return pht('Date and Time'); } + public function getPanelMenuIcon() { + return 'fa-calendar'; + } + public function getPanelGroupKey() { return PhabricatorSettingsAccountPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php index 384f7e3be9..e6ed8e7564 100644 --- a/src/applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php @@ -9,6 +9,10 @@ final class PhabricatorDeveloperPreferencesSettingsPanel return pht('Developer Settings'); } + public function getPanelMenuIcon() { + return 'fa-magic'; + } + public function getPanelGroupKey() { return PhabricatorSettingsDeveloperPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php index 2e055c3408..acb7f50541 100644 --- a/src/applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php @@ -9,6 +9,10 @@ final class PhabricatorDiffPreferencesSettingsPanel return pht('Diff Preferences'); } + public function getPanelMenuIcon() { + return 'fa-cog'; + } + public function getPanelGroupKey() { return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php index 6033ef79e9..7c17a9fea5 100644 --- a/src/applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorDisplayPreferencesSettingsPanel.php @@ -9,6 +9,10 @@ final class PhabricatorDisplayPreferencesSettingsPanel return pht('Display Preferences'); } + public function getPanelMenuIcon() { + return 'fa-desktop'; + } + public function getPanelGroupKey() { return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php index cd1bfba540..1b69adcd62 100644 --- a/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php @@ -11,6 +11,10 @@ final class PhabricatorEmailAddressesSettingsPanel return pht('Email Addresses'); } + public function getPanelMenuIcon() { + return 'fa-at'; + } + public function getPanelGroupKey() { return PhabricatorSettingsEmailPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php index 86260c1b5a..55932aa49b 100644 --- a/src/applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorEmailDeliverySettingsPanel.php @@ -9,6 +9,10 @@ final class PhabricatorEmailDeliverySettingsPanel return pht('Email Delivery'); } + public function getPanelMenuIcon() { + return 'fa-envelope-o'; + } + public function getPanelGroupKey() { return PhabricatorSettingsEmailPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php index 51ff40ed9d..5a4a707a05 100644 --- a/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorEmailFormatSettingsPanel.php @@ -9,6 +9,10 @@ final class PhabricatorEmailFormatSettingsPanel return pht('Email Format'); } + public function getPanelMenuIcon() { + return 'fa-font'; + } + public function getPanelGroupKey() { return PhabricatorSettingsEmailPanelGroup::PANELGROUPKEY; } @@ -19,17 +23,6 @@ final class PhabricatorEmailFormatSettingsPanel public function isManagementPanel() { return false; -/* - if (!$this->isUserPanel()) { - return false; - } - - if ($this->getUser()->getIsMailingList()) { - return true; - } - - return false; -*/ } public function isTemplatePanel() { diff --git a/src/applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php index faa79889ed..defee73393 100644 --- a/src/applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorEmailPreferencesSettingsPanel.php @@ -11,6 +11,10 @@ final class PhabricatorEmailPreferencesSettingsPanel return pht('Email Preferences'); } + public function getPanelMenuIcon() { + return 'fa-envelope-open-o'; + } + public function getPanelGroupKey() { return PhabricatorSettingsEmailPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php index e380248a83..1215487208 100644 --- a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php @@ -11,6 +11,10 @@ final class PhabricatorExternalAccountsSettingsPanel return pht('External Accounts'); } + public function getPanelMenuIcon() { + return 'fa-users'; + } + public function getPanelGroupKey() { return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php b/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php index 9b846bd4b6..65a0be4e79 100644 --- a/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php @@ -9,6 +9,10 @@ final class PhabricatorLanguageSettingsPanel return pht('Language'); } + public function getPanelMenuIcon() { + return 'fa-globe'; + } + public function getPanelGroupKey() { return PhabricatorSettingsAccountPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php index 06f1547afa..93a95254a9 100644 --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -11,6 +11,10 @@ final class PhabricatorMultiFactorSettingsPanel return pht('Multi-Factor Auth'); } + public function getPanelMenuIcon() { + return 'fa-lock'; + } + public function getPanelGroupKey() { return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php b/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php index 797bcafcb3..d0165dc3f1 100644 --- a/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorNotificationsSettingsPanel.php @@ -21,6 +21,10 @@ final class PhabricatorNotificationsSettingsPanel return pht('Notifications'); } + public function getPanelMenuIcon() { + return 'fa-bell-o'; + } + public function getPanelGroupKey() { return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php index 79d7610f2f..37393d5d4f 100644 --- a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php @@ -10,6 +10,10 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { return pht('Password'); } + public function getPanelMenuIcon() { + return 'fa-key'; + } + public function getPanelGroupKey() { return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php b/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php index 13944411ed..131f602974 100644 --- a/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php @@ -18,6 +18,10 @@ final class PhabricatorSSHKeysSettingsPanel extends PhabricatorSettingsPanel { return pht('SSH Public Keys'); } + public function getPanelMenuIcon() { + return 'fa-file-text-o'; + } + public function getPanelGroupKey() { return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php b/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php index 314d68f69d..fb10572e11 100644 --- a/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorSessionsSettingsPanel.php @@ -10,6 +10,10 @@ final class PhabricatorSessionsSettingsPanel extends PhabricatorSettingsPanel { return pht('Sessions'); } + public function getPanelMenuIcon() { + return 'fa-user'; + } + public function getPanelGroupKey() { return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY; } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanel.php b/src/applications/settings/panel/PhabricatorSettingsPanel.php index 19ac6fec62..8250418812 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanel.php @@ -131,6 +131,16 @@ abstract class PhabricatorSettingsPanel extends Phobject { abstract public function getPanelName(); + /** + * Return an icon for the panel in the menu. + * + * @return string Icon identifier. + * @task config + */ + public function getPanelMenuIcon() { + return 'fa-wrench'; + } + /** * Return a panel group key constant for this panel. * diff --git a/src/applications/settings/panel/PhabricatorTokensSettingsPanel.php b/src/applications/settings/panel/PhabricatorTokensSettingsPanel.php index f2021bafa5..91064a432f 100644 --- a/src/applications/settings/panel/PhabricatorTokensSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorTokensSettingsPanel.php @@ -10,6 +10,10 @@ final class PhabricatorTokensSettingsPanel extends PhabricatorSettingsPanel { return pht('Temporary Tokens'); } + public function getPanelMenuIcon() { + return 'fa-ticket'; + } + public function getPanelGroupKey() { return PhabricatorSettingsLogsPanelGroup::PANELGROUPKEY; } From c4244aa177b8aecf52f57fce697baa1d5cd0bd73 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 21 Jan 2019 11:04:49 -0800 Subject: [PATCH 13/42] Allow users to access some settings at the "Add MFA" account setup roadblock Summary: Depends on D20006. Ref T13222. Currently, the "MFA Is Required" gate doesn't let you do anything else, but you'll need to be able to access "Contact Numbers" if an install provides SMS MFA. Tweak this UI to give users limited access to settings, so they can set up contact numbers and change their language. (This is a little bit fiddly, and I'm doing it early on partly so it can get more testing as these changes move forward.) Test Plan: {F6146136} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20008 --- .../PhabricatorAuthApplication.php | 6 +- ...bricatorAuthNeedsMultiFactorController.php | 225 +++++++++++++----- ...PhabricatorAuthContactNumberController.php | 15 ++ ...PhabricatorContactNumbersSettingsPanel.php | 4 + .../PhabricatorLanguageSettingsPanel.php | 4 + .../PhabricatorMultiFactorSettingsPanel.php | 25 +- .../panel/PhabricatorSettingsPanel.php | 11 + 7 files changed, 228 insertions(+), 62 deletions(-) diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index 20547d8ca3..4c0003e806 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -72,8 +72,10 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { => 'PhabricatorAuthRevokeTokenController', 'session/downgrade/' => 'PhabricatorAuthDowngradeSessionController', - 'multifactor/' - => 'PhabricatorAuthNeedsMultiFactorController', + 'enroll/' => array( + '(?:(?P[^/]+)/)?(?:(?Psaved)/)?' + => 'PhabricatorAuthNeedsMultiFactorController', + ), 'sshkey/' => array( $this->getQueryRoutePattern('for/(?P[^/]+)/') => 'PhabricatorAuthSSHKeyListController', diff --git a/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php b/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php index 27e03485ca..92328b2000 100644 --- a/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php +++ b/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php @@ -30,80 +30,187 @@ 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')); + } + + $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); + + return $view; + } + } diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php index a713f48a3b..3ae923fbbc 100644 --- a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php @@ -3,6 +3,21 @@ 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(); diff --git a/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php b/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php index 7834bff593..3e4ed8880e 100644 --- a/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php @@ -19,6 +19,10 @@ final class PhabricatorContactNumbersSettingsPanel return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY; } + public function isMultiFactorEnrollmentPanel() { + return true; + } + public function processRequest(AphrontRequest $request) { $user = $this->getUser(); $viewer = $request->getUser(); diff --git a/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php b/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php index 65a0be4e79..39bd5deac9 100644 --- a/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorLanguageSettingsPanel.php @@ -25,4 +25,8 @@ final class PhabricatorLanguageSettingsPanel return true; } + public function isMultiFactorEnrollmentPanel() { + return true; + } + } diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php index 93a95254a9..d2e9c34247 100644 --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -3,6 +3,8 @@ final class PhabricatorMultiFactorSettingsPanel extends PhabricatorSettingsPanel { + private $isEnrollment; + public function getPanelKey() { return 'multifactor'; } @@ -19,6 +21,19 @@ final class PhabricatorMultiFactorSettingsPanel return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY; } + public function isMultiFactorEnrollmentPanel() { + return true; + } + + public function setIsEnrollment($is_enrollment) { + $this->isEnrollment = $is_enrollment; + return $this; + } + + public function getIsEnrollment() { + return $this->isEnrollment; + } + public function processRequest(AphrontRequest $request) { if ($request->getExists('new') || $request->getExists('providerPHID')) { return $this->processNew($request); @@ -106,13 +121,21 @@ final class PhabricatorMultiFactorSettingsPanel $buttons = array(); + // If we're enrolling a new account in MFA, provide a small visual hint + // that this is the button they want to click. + if ($this->getIsEnrollment()) { + $add_color = PHUIButtonView::BLUE; + } else { + $add_color = PHUIButtonView::GREY; + } + $buttons[] = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-plus') ->setText(pht('Add Auth Factor')) ->setHref($this->getPanelURI('?new=true')) ->setWorkflow(true) - ->setColor(PHUIButtonView::GREY); + ->setColor($add_color); $buttons[] = id(new PHUIButtonView()) ->setTag('a') diff --git a/src/applications/settings/panel/PhabricatorSettingsPanel.php b/src/applications/settings/panel/PhabricatorSettingsPanel.php index 8250418812..e2efd92093 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanel.php @@ -198,6 +198,17 @@ abstract class PhabricatorSettingsPanel extends Phobject { return false; } + /** + * Return true if this panel should be available when enrolling in MFA on + * a new account with MFA requiredd. + * + * @return bool True to allow configuration during MFA enrollment. + * @task config + */ + public function isMultiFactorEnrollmentPanel() { + return false; + } + /* -( Panel Implementation )----------------------------------------------- */ From 12203762b7f0c1cc3a38ca00320dbd0cba9ba394 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 21 Jan 2019 12:30:10 -0800 Subject: [PATCH 14/42] Allow contact numbers to be enabled and disabled Summary: Depends on D20008. Ref T920. Continue fleshing out contact number behaviors. Test Plan: - Enabled and disabled a contact number. - Saw list, detail views reflect change. - Added number X, disabled it, added it again (allowed), enabled the disabled one ("already in use" exception). Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T920 Differential Revision: https://secure.phabricator.com/D20010 --- src/__phutil_library_map__.php | 4 + .../PhabricatorAuthApplication.php | 2 + ...atorAuthContactNumberDisableController.php | 87 +++++++++++++++++++ ...ricatorAuthContactNumberViewController.php | 22 +++++ .../storage/PhabricatorAuthContactNumber.php | 24 ++++- ...atorAuthContactNumberStatusTransaction.php | 59 +++++++++++++ 6 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 src/applications/auth/controller/contact/PhabricatorAuthContactNumberDisableController.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 7932a7e79e..ca96225ce8 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2201,12 +2201,14 @@ phutil_register_library_map(array( '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', 'PhabricatorAuthContactNumberNumberTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php', 'PhabricatorAuthContactNumberPHIDType' => 'applications/auth/phid/PhabricatorAuthContactNumberPHIDType.php', 'PhabricatorAuthContactNumberQuery' => 'applications/auth/query/PhabricatorAuthContactNumberQuery.php', + 'PhabricatorAuthContactNumberStatusTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php', 'PhabricatorAuthContactNumberTransaction' => 'applications/auth/storage/PhabricatorAuthContactNumberTransaction.php', 'PhabricatorAuthContactNumberTransactionQuery' => 'applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php', 'PhabricatorAuthContactNumberTransactionType' => 'applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php', @@ -7904,12 +7906,14 @@ phutil_register_library_map(array( 'PhabricatorDestructibleInterface', ), 'PhabricatorAuthContactNumberController' => 'PhabricatorAuthController', + 'PhabricatorAuthContactNumberDisableController' => 'PhabricatorAuthContactNumberController', 'PhabricatorAuthContactNumberEditController' => 'PhabricatorAuthContactNumberController', 'PhabricatorAuthContactNumberEditEngine' => 'PhabricatorEditEngine', 'PhabricatorAuthContactNumberEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorAuthContactNumberNumberTransaction' => 'PhabricatorAuthContactNumberTransactionType', 'PhabricatorAuthContactNumberPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthContactNumberQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorAuthContactNumberStatusTransaction' => 'PhabricatorAuthContactNumberTransactionType', 'PhabricatorAuthContactNumberTransaction' => 'PhabricatorModularTransaction', 'PhabricatorAuthContactNumberTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuthContactNumberTransactionType' => 'PhabricatorModularTransactionType', diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index 4c0003e806..1a186cbed4 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -111,6 +111,8 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { 'PhabricatorAuthContactNumberEditController', '(?P[1-9]\d*)/' => 'PhabricatorAuthContactNumberViewController', + '(?Pdisable|enable)/(?P[1-9]\d*)/' => + 'PhabricatorAuthContactNumberDisableController', ), ), diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberDisableController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberDisableController.php new file mode 100644 index 0000000000..f860205458 --- /dev/null +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberDisableController.php @@ -0,0 +1,87 @@ +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->isFormPost()) { + $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); + + 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); + } + +} diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php index 5423d93dcd..d04d73d13c 100644 --- a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php @@ -54,6 +54,10 @@ final class PhabricatorAuthContactNumberViewController ->setHeader($number->getObjectName()) ->setPolicyObject($number); + if ($number->isDisabled()) { + $view->setStatus('fa-ban', 'red', pht('Disabled')); + } + return $view; } @@ -92,6 +96,24 @@ final class PhabricatorAuthContactNumberViewController ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); + + if ($number->isDisabled()) { + $disable_uri = $this->getApplicationURI("contact/enable/{$id}/"); + $disable_name = pht('Enable Contact Number'); + $disable_icon = 'fa-check'; + } else { + $disable_uri = $this->getApplicationURI("contact/disable/{$id}/"); + $disable_name = pht('Disable Contact Number'); + $disable_icon = 'fa-ban'; + } + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName($disable_name) + ->setIcon($disable_icon) + ->setHref($disable_uri) + ->setWorkflow(true)); + return $curtain; } diff --git a/src/applications/auth/storage/PhabricatorAuthContactNumber.php b/src/applications/auth/storage/PhabricatorAuthContactNumber.php index f22b90962c..d104bddb00 100644 --- a/src/applications/auth/storage/PhabricatorAuthContactNumber.php +++ b/src/applications/auth/storage/PhabricatorAuthContactNumber.php @@ -93,10 +93,32 @@ final class PhabricatorAuthContactNumber } public function save() { - $this->uniqueKey = $this->newUniqueKey(); + // 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(); + } + return parent::save(); } + 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'), + ), + ); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php new file mode 100644 index 0000000000..305243ae15 --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php @@ -0,0 +1,59 @@ +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; + } + + // 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; + } + +} From 596435b35e30504988502cca249efd5cf004a123 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 21 Jan 2019 13:02:37 -0800 Subject: [PATCH 15/42] Support designating a contact number as "primary" Summary: Depends on D20010. Ref T920. Allow users to designate which contact number is "primary": the number we'll actually send stuff to. Since this interacts in weird ways with "disable", just do a "when any number is touched, put all of the user's rows into the right state" sort of thing. Test Plan: - Added numbers, made numbers primary, disabled a primary number, un-disabled a number with no primaries. Got sensible behavior in all cases. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T920 Differential Revision: https://secure.phabricator.com/D20011 --- .../20190121.contact.01.primary.sql | 2 + src/__phutil_library_map__.php | 4 + .../PhabricatorAuthApplication.php | 2 + ...atorAuthContactNumberPrimaryController.php | 78 +++++++++++++++++++ ...ricatorAuthContactNumberViewController.php | 13 +++- .../storage/PhabricatorAuthContactNumber.php | 78 ++++++++++++++++++- ...torAuthContactNumberPrimaryTransaction.php | 49 ++++++++++++ ...PhabricatorContactNumbersSettingsPanel.php | 16 +++- 8 files changed, 237 insertions(+), 5 deletions(-) create mode 100644 resources/sql/autopatches/20190121.contact.01.primary.sql create mode 100644 src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php diff --git a/resources/sql/autopatches/20190121.contact.01.primary.sql b/resources/sql/autopatches/20190121.contact.01.primary.sql new file mode 100644 index 0000000000..84a7570679 --- /dev/null +++ b/resources/sql/autopatches/20190121.contact.01.primary.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_auth.auth_contactnumber + ADD isPrimary BOOL NOT NULL; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ca96225ce8..04fa6cf167 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2207,6 +2207,8 @@ phutil_register_library_map(array( 'PhabricatorAuthContactNumberEditor' => 'applications/auth/editor/PhabricatorAuthContactNumberEditor.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', 'PhabricatorAuthContactNumberTransaction' => 'applications/auth/storage/PhabricatorAuthContactNumberTransaction.php', @@ -7912,6 +7914,8 @@ phutil_register_library_map(array( 'PhabricatorAuthContactNumberEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorAuthContactNumberNumberTransaction' => 'PhabricatorAuthContactNumberTransactionType', 'PhabricatorAuthContactNumberPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorAuthContactNumberPrimaryController' => 'PhabricatorAuthContactNumberController', + 'PhabricatorAuthContactNumberPrimaryTransaction' => 'PhabricatorAuthContactNumberTransactionType', 'PhabricatorAuthContactNumberQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthContactNumberStatusTransaction' => 'PhabricatorAuthContactNumberTransactionType', 'PhabricatorAuthContactNumberTransaction' => 'PhabricatorModularTransaction', diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index 1a186cbed4..a4f3cae9ed 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -113,6 +113,8 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { 'PhabricatorAuthContactNumberViewController', '(?Pdisable|enable)/(?P[1-9]\d*)/' => 'PhabricatorAuthContactNumberDisableController', + 'primary/(?P[1-9]\d*)/' => + 'PhabricatorAuthContactNumberPrimaryController', ), ), diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php new file mode 100644 index 0000000000..afcc065559 --- /dev/null +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php @@ -0,0 +1,78 @@ +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->isFormPost()) { + $xactions = array(); + + $xactions[] = id(new PhabricatorAuthContactNumberTransaction()) + ->setTransactionType( + PhabricatorAuthContactNumberPrimaryTransaction::TRANSACTIONTYPE) + ->setNewValue(true); + + $editor = id(new PhabricatorAuthContactNumberEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($number, $xactions); + + 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); + } + +} diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php index d04d73d13c..eaf84ea08a 100644 --- a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php @@ -56,6 +56,8 @@ final class PhabricatorAuthContactNumberViewController if ($number->isDisabled()) { $view->setStatus('fa-ban', 'red', pht('Disabled')); + } else if ($number->getIsPrimary()) { + $view->setStatus('fa-certificate', 'blue', pht('Primary')); } return $view; @@ -96,17 +98,26 @@ final class PhabricatorAuthContactNumberViewController ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); - 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) diff --git a/src/applications/auth/storage/PhabricatorAuthContactNumber.php b/src/applications/auth/storage/PhabricatorAuthContactNumber.php index d104bddb00..e3dfd77bb2 100644 --- a/src/applications/auth/storage/PhabricatorAuthContactNumber.php +++ b/src/applications/auth/storage/PhabricatorAuthContactNumber.php @@ -12,6 +12,7 @@ final class PhabricatorAuthContactNumber protected $contactNumber; protected $uniqueKey; protected $status; + protected $isPrimary; protected $properties = array(); const STATUS_ACTIVE = 'active'; @@ -27,6 +28,7 @@ final class PhabricatorAuthContactNumber 'contactNumber' => 'text255', 'status' => 'text32', 'uniqueKey' => 'bytes12?', + 'isPrimary' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_object' => array( @@ -43,7 +45,8 @@ final class PhabricatorAuthContactNumber public static function initializeNewContactNumber($object) { return id(new self()) ->setStatus(self::STATUS_ACTIVE) - ->setObjectPHID($object->getPHID()); + ->setObjectPHID($object->getPHID()) + ->setIsPrimary(0); } public function getPHIDType() { @@ -73,8 +76,14 @@ final class PhabricatorAuthContactNumber ->setTooltip(pht('Disabled')); } + if ($this->getIsPrimary()) { + return id(new PHUIIconView()) + ->setIcon('fa-certificate', 'blue') + ->setTooltip(pht('Primary Number')); + } + return id(new PHUIIconView()) - ->setIcon('fa-mobile', 'green') + ->setIcon('fa-hashtag', 'bluegrey') ->setTooltip(pht('Active Phone Number')); } @@ -101,7 +110,61 @@ final class PhabricatorAuthContactNumber $this->uniqueKey = $this->newUniqueKey(); } - return parent::save(); + 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() { @@ -119,6 +182,15 @@ final class PhabricatorAuthContactNumber ); } + 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 )----------------------------------------- */ diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php new file mode 100644 index 0000000000..2e4b6ff55c --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php @@ -0,0 +1,49 @@ +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; + } + } + + return $errors; + } + +} diff --git a/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php b/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php index 3e4ed8880e..7056fd02de 100644 --- a/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php @@ -12,7 +12,7 @@ final class PhabricatorContactNumbersSettingsPanel } public function getPanelMenuIcon() { - return 'fa-mobile'; + return 'fa-hashtag'; } public function getPanelGroupKey() { @@ -31,9 +31,19 @@ final class PhabricatorContactNumbersSettingsPanel ->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( @@ -42,6 +52,7 @@ final class PhabricatorContactNumbersSettingsPanel 'href' => $number->getURI(), ), $number->getDisplayName()), + $primary_display, phabricator_datetime($number->getDateCreated(), $viewer), ); } @@ -49,16 +60,19 @@ final class PhabricatorContactNumbersSettingsPanel $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', )); From af71c51f0a0b04d90446a033e197a06e71d9c0f5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 21 Jan 2019 15:03:45 -0800 Subject: [PATCH 16/42] Give "MetaMTAMail" a "message type" and support SMS Summary: Depends on D20011. Ref T920. This change lets a "MetaMTAMail" storage object represent various different types of messages, and makes "all" the `bin/mail` stuff "totally work" with messages of non-email types. In practice, a lot of the related tooling needs some polish/refinement, but the basics work. Test Plan: Used `echo beep boop | bin/mail send-test --to epriestley --type sms` to send myself SMS. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T920 Differential Revision: https://secure.phabricator.com/D20012 --- src/__phutil_library_map__.php | 2 + .../PhabricatorAuthContactNumberQuery.php | 13 ++++ .../engine/PhabricatorMailSMSEngine.php | 75 +++++++++++++++++++ ...atorMailManagementListOutboundWorkflow.php | 5 +- ...bricatorMailManagementSendTestWorkflow.php | 55 ++++++++++---- .../message/PhabricatorMailEmailMessage.php | 4 + .../PhabricatorMailExternalMessage.php | 7 ++ .../message/PhabricatorMailSMSMessage.php | 4 + .../storage/PhabricatorMetaMTAMail.php | 26 ++++++- 9 files changed, 174 insertions(+), 17 deletions(-) create mode 100644 src/applications/metamta/engine/PhabricatorMailSMSEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 04fa6cf167..9e7769f093 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3455,6 +3455,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', @@ -9341,6 +9342,7 @@ phutil_register_library_map(array( 'PhabricatorMailReceiverTestCase' => 'PhabricatorTestCase', 'PhabricatorMailReplyHandler' => 'Phobject', 'PhabricatorMailRoutingRule' => 'Phobject', + 'PhabricatorMailSMSEngine' => 'PhabricatorMailMessageEngine', 'PhabricatorMailSMSMessage' => 'PhabricatorMailExternalMessage', 'PhabricatorMailSMTPAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailSendGridAdapter' => 'PhabricatorMailAdapter', diff --git a/src/applications/auth/query/PhabricatorAuthContactNumberQuery.php b/src/applications/auth/query/PhabricatorAuthContactNumberQuery.php index 10cfba7a65..77b3b559dd 100644 --- a/src/applications/auth/query/PhabricatorAuthContactNumberQuery.php +++ b/src/applications/auth/query/PhabricatorAuthContactNumberQuery.php @@ -8,6 +8,7 @@ final class PhabricatorAuthContactNumberQuery private $objectPHIDs; private $statuses; private $uniqueKeys; + private $isPrimary; public function withIDs(array $ids) { $this->ids = $ids; @@ -34,6 +35,11 @@ final class PhabricatorAuthContactNumberQuery return $this; } + public function withIsPrimary($is_primary) { + $this->isPrimary = $is_primary; + return $this; + } + public function newResultObject() { return new PhabricatorAuthContactNumber(); } @@ -80,6 +86,13 @@ final class PhabricatorAuthContactNumberQuery $this->uniqueKeys); } + if ($this->isPrimary !== null) { + $where[] = qsprintf( + $conn, + 'isPrimary = %d', + (int)$this->isPrimary); + } + return $where; } diff --git a/src/applications/metamta/engine/PhabricatorMailSMSEngine.php b/src/applications/metamta/engine/PhabricatorMailSMSEngine.php new file mode 100644 index 0000000000..9f5c2fef36 --- /dev/null +++ b/src/applications/metamta/engine/PhabricatorMailSMSEngine.php @@ -0,0 +1,75 @@ +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; + } + +} diff --git a/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php index a83dafb0a8..30939dd436 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementListOutboundWorkflow.php @@ -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(), )); } diff --git a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php index ab5bd7f336..f390ff27df 100644 --- a/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php +++ b/src/applications/metamta/management/PhabricatorMailManagementSendTestWorkflow.php @@ -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(); diff --git a/src/applications/metamta/message/PhabricatorMailEmailMessage.php b/src/applications/metamta/message/PhabricatorMailEmailMessage.php index 577b6052ea..c98cdc2e33 100644 --- a/src/applications/metamta/message/PhabricatorMailEmailMessage.php +++ b/src/applications/metamta/message/PhabricatorMailEmailMessage.php @@ -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; diff --git a/src/applications/metamta/message/PhabricatorMailExternalMessage.php b/src/applications/metamta/message/PhabricatorMailExternalMessage.php index f691090d18..048b20ab54 100644 --- a/src/applications/metamta/message/PhabricatorMailExternalMessage.php +++ b/src/applications/metamta/message/PhabricatorMailExternalMessage.php @@ -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(); + } + } diff --git a/src/applications/metamta/message/PhabricatorMailSMSMessage.php b/src/applications/metamta/message/PhabricatorMailSMSMessage.php index a7e1d10923..ae7cd7122d 100644 --- a/src/applications/metamta/message/PhabricatorMailSMSMessage.php +++ b/src/applications/metamta/message/PhabricatorMailSMSMessage.php @@ -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; diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php index 70d94ccb0c..cc3ae82bef 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -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) From f69fbf5ea6d02d3fa7d1bbfb9089b2fb060845be Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 21 Jan 2019 15:15:52 -0800 Subject: [PATCH 17/42] Make the "Test" adapter support both SMS and email Summary: Depends on D20012. Ref T920. If you have a test adapter configured, it should swallow messages and prevent them from ever hitting a lower-priority adapter. Make the test adapter support SMS so this actually happens. Test Plan: Ran `bin/mail send-test --type sms ...` with a test adapter (first) and a Twilio adapter (second). Got SMS swallowed by test adapter instead of live SMS messages. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T920 Differential Revision: https://secure.phabricator.com/D20013 --- .../adapter/PhabricatorMailTestAdapter.php | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php b/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php index f0840ba7bf..a6258a8874 100644 --- a/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailTestAdapter.php @@ -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'); - } - - } From ee7b03bdf7cf8dbac2fc7d17c6a0bf99937a46a4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 23 Jan 2019 06:39:09 -0800 Subject: [PATCH 18/42] Correct an issue where the default "Settings" screen could show the wrong settings Summary: Depends on D20013. Recently, I renamed the "Account" panel to "Language". When you land on "Settings" and the first panel is an "EditEngine" panel ("Account/Langauge", "Date and Time", and "Conpherence" are all "EditEngine" panels), the engine shows the controls for the first panel. However, the "first panel" according to EditEngine and the "first panel" in the menu are currently different: the menu groups panels into topics. When I renamed "Account" to "Language", it went from conicidentally being the first panel in both lists to being the second panel in the grouped menu list and the, uh, like 12th panel in the ungrouped raw list. This made landing on "Settings" show you the right chrome, but show you a different panel's controls ("Conpherence", now alphabetically first). Instead, use the same order in both places. (This was also a pre-existing bug if you use a language which translates the panel names such that "Account" is not alphabetically first.) Test Plan: Visited "Settings", saw "Date & Time" form controls instead of "Conpherence" form controls on the default screen with "Date & Time" selected in the menu. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20016 --- .../settings/editor/PhabricatorSettingsEditEngine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/settings/editor/PhabricatorSettingsEditEngine.php b/src/applications/settings/editor/PhabricatorSettingsEditEngine.php index 4f16d0338b..34a6132d80 100644 --- a/src/applications/settings/editor/PhabricatorSettingsEditEngine.php +++ b/src/applications/settings/editor/PhabricatorSettingsEditEngine.php @@ -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)) { From bb20c136513c7cf16ed82abeed931e8483a206a6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 23 Jan 2019 06:49:55 -0800 Subject: [PATCH 19/42] Allow MFA factors to provide more guidance text on create workflows Summary: Depends on D20016. Ref T920. This does nothing interesting on its own since the TOTP provider has no guidance/warnings, but landing it separately helps to simplify an upcoming SMS diff. SMS will have these guidance messages: - "Administrator: you haven't configured any mailer which can send SMS, like Twilio." - "Administrator: SMS is weak." - "User: you haven't configured a contact number." Test Plan: {F6151283} {F6151284} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T920 Differential Revision: https://secure.phabricator.com/D20017 --- resources/celerity/map.php | 12 ++++----- ...icatorAuthFactorProviderEditController.php | 19 +++++++++++-- .../auth/factor/PhabricatorAuthFactor.php | 27 +++++++++++++++++++ .../storage/PhabricatorAuthFactorProvider.php | 8 ++++++ .../PhabricatorMultiFactorSettingsPanel.php | 27 +++++++++++++++++-- .../css/phui/object-item/phui-oi-big-ui.css | 5 ++++ 6 files changed, 88 insertions(+), 10 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 5764623ad5..651e95bf15 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'e94cc920', + 'core.pkg.css' => 'a66ea2e7', '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', @@ -832,7 +832,7 @@ return array( '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', diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php index 0dde1b3c6f..a8d87e2ead 100644 --- a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php @@ -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); } diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index f797ce3a15..c7fc9891af 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -45,6 +45,33 @@ abstract class PhabricatorAuthFactor extends Phobject { ->setIcon('fa-mobile'); } + public function canCreateNewProvider() { + return true; + } + + public function getProviderCreateDescription() { + return null; + } + + public function canCreateNewConfiguration(PhabricatorUser $user) { + return true; + } + + public function getConfigurationCreateDescription(PhabricatorUser $user) { + return null; + } + + 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) { diff --git a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php index e61740b4d7..b1abb02ba5 100644 --- a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php @@ -101,6 +101,14 @@ final class PhabricatorAuthFactorProvider return $config; } + public function newSortVector() { + $factor = $this->getFactor(); + + return id(new PhutilSortVector()) + ->addInt($factor->getFactorOrder()) + ->addInt($this->getID()); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php index d2e9c34247..60fca07ff9 100644 --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -169,6 +169,7 @@ final class PhabricatorMultiFactorSettingsPanel ->addCancelButton($cancel_uri); } $providers = mpull($providers, null, 'getPHID'); + $proivders = msortv($providers, 'newSortVector'); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, @@ -180,6 +181,13 @@ final class PhabricatorMultiFactorSettingsPanel $selected_provider = null; } else { $selected_provider = $providers[$selected_phid]; + + // Only let the user continue creating a factor for a given provider if + // they actually pass the provider's checks. + $selected_factor = $selected_provider->getFactor(); + if (!$selected_factor->canCreateNewConfiguration($viewer)) { + $selected_provider = null; + } } if (!$selected_provider) { @@ -192,13 +200,28 @@ final class PhabricatorMultiFactorSettingsPanel $provider_uri = id(new PhutilURI($this->getPanelURI())) ->setQueryParam('providerPHID', $provider_phid); + $factor = $provider->getFactor(); + $is_enabled = $factor->canCreateNewConfiguration($viewer); + $item = id(new PHUIObjectItemView()) ->setHeader($provider->getDisplayName()) - ->setHref($provider_uri) - ->setClickable(true) ->setImageIcon($provider->newIconView()) ->addAttribute($provider->getDisplayDescription()); + if ($is_enabled) { + $item + ->setHref($provider_uri) + ->setClickable(true); + } else { + $item->setDisabled(true); + } + + $create_description = $factor->getConfigurationCreateDescription( + $viewer); + if ($create_description) { + $item->appendChild($create_description); + } + $menu->addItem($item); } diff --git a/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css b/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css index 6f60560a2e..a793c018c3 100644 --- a/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css +++ b/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css @@ -72,3 +72,8 @@ .device-desktop .phui-oi-linked-container a:hover { text-decoration: none; } + +/* Spacing for InfoView inside an object item list, like MFA setup. */ +.phui-oi .phui-info-view { + margin: 0 4px 4px; +} From e91bc26da685c280f46f1b9136eeab26d3edc8cc Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 23 Jan 2019 07:09:20 -0800 Subject: [PATCH 20/42] Don't rate limit users clicking "Wait Patiently" at an MFA gate even if they typed some text earlier Summary: Depends on D20017. Ref T13222. Currently, if you: - type some text at a TOTP gate; - wait ~60 seconds for the challenge to expire; - submit the form into a "Wait patiently" message; and - mash that wait button over and over again very patiently ...you still rack up rate limiting points, because the hidden text from your original request is preserved and triggers the "is the user responding to a challenge" test. Only perform this test if we haven't already decided that we're going to make them wait. Test Plan: - Did the above; before patch: rate limited; after patch: not rate limited. - Intentionally typed a bunch of bad answers which were actually evaluated: rate limited properly. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20018 --- .../auth/engine/PhabricatorAuthSessionEngine.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index cef2323209..1fa2dadfd6 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -557,9 +557,18 @@ final class PhabricatorAuthSessionEngine extends Phobject { // Limit factor verification rates to prevent brute force attacks. $any_attempt = false; foreach ($factors as $factor) { + $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; From 7c1d1c13f4a308244f30ef7658d447f9347ee133 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 23 Jan 2019 07:22:10 -0800 Subject: [PATCH 21/42] Add a rate limit for enroll attempts when adding new MFA configurations Summary: Depends on D20018. Ref T13222. When you add a new MFA configuration, you can technically (?) guess your way through it with brute force. It's not clear why this would ever really be useful (if an attacker can get here and wants to add TOTP, they can just add TOTP!) but it's probably bad, so don't let users do it. This limit is fairly generous because I don't think this actually part of any real attack, at least today with factors we're considering. Test Plan: - Added TOTP, guessed wrong a ton of times, got rate limited. - Added TOTP, guessed right, got a TOTP factor configuration added to my account. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20019 --- src/__phutil_library_map__.php | 2 ++ .../action/PhabricatorAuthNewFactorAction.php | 21 ++++++++++++++++ .../PhabricatorMultiFactorSettingsPanel.php | 24 +++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 src/applications/auth/action/PhabricatorAuthNewFactorAction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9e7769f093..7b4055d1b0 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2296,6 +2296,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', @@ -8021,6 +8022,7 @@ phutil_register_library_map(array( 'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController', 'PhabricatorAuthNeedsMultiFactorController' => 'PhabricatorAuthController', 'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController', + 'PhabricatorAuthNewFactorAction' => 'PhabricatorSystemAction', 'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController', 'PhabricatorAuthOneTimeLoginController' => 'PhabricatorAuthController', 'PhabricatorAuthOneTimeLoginTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', diff --git a/src/applications/auth/action/PhabricatorAuthNewFactorAction.php b/src/applications/auth/action/PhabricatorAuthNewFactorAction.php new file mode 100644 index 0000000000..c1244587f1 --- /dev/null +++ b/src/applications/auth/action/PhabricatorAuthNewFactorAction.php @@ -0,0 +1,21 @@ +setViewer($viewer); + if ($request->isFormPost()) { + // Subject users to rate limiting so that it's difficult to add factors + // by pure brute force. This is normally not much of an attack, but push + // factor types may have side effects. + PhabricatorSystemActionEngine::willTakeAction( + array($viewer->getPHID()), + new PhabricatorAuthNewFactorAction(), + 1); + } else { + // Test the limit before showing the user a form, so we don't give them + // a form which can never possibly work because it will always hit rate + // limiting. + PhabricatorSystemActionEngine::willTakeAction( + array($viewer->getPHID()), + new PhabricatorAuthNewFactorAction(), + 0); + } + $config = $selected_provider->processAddFactorForm( $form, $request, $user); if ($config) { + // If the user added a factor, give them a rate limiting point back. + PhabricatorSystemActionEngine::willTakeAction( + array($viewer->getPHID()), + new PhabricatorAuthNewFactorAction(), + -1); + $config->save(); $log = PhabricatorUserLog::initializeNewLog( From f3340c633562218b7d37cd4f9e61abe3fc9de154 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 23 Jan 2019 08:17:05 -0800 Subject: [PATCH 22/42] Allow different MFA factor types (SMS, TOTP, Duo, ...) to share "sync" tokens when enrolling new factors Summary: Depends on D20019. Ref T13222. Currently, TOTP uses a temporary token to make sure you've set up the app on your phone properly and that you're providing an answer to a secret which we generated (not an attacker-generated secret). However, most factor types need some kind of sync token. SMS needs to send you a code; Duo needs to store a transaction ID. Turn this "TOTP" token into an "MFA Sync" token and lift the implementation up to the base class. Also, slightly simplify some of the HTTP form gymnastics. Test Plan: - Hit the TOTP enroll screen. - Reloaded it, got new secrets. - Reloaded it more than 10 times, got told to stop generating new challenges. - Answered a challenge properly, got a new TOTP factor. - Grepped for removed class name. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20020 --- src/__phutil_library_map__.php | 4 +- .../auth/factor/PhabricatorAuthFactor.php | 100 ++++++++++++++++++ ...bricatorAuthMFASyncTemporaryTokenType.php} | 9 +- .../auth/factor/PhabricatorTOTPAuthFactor.php | 76 ++++--------- .../storage/PhabricatorAuthFactorConfig.php | 10 ++ .../storage/PhabricatorAuthTemporaryToken.php | 12 ++- .../PhabricatorMultiFactorSettingsPanel.php | 7 ++ 7 files changed, 153 insertions(+), 65 deletions(-) rename src/applications/auth/factor/{PhabricatorAuthTOTPKeyTemporaryTokenType.php => PhabricatorAuthMFASyncTemporaryTokenType.php} (52%) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 7b4055d1b0..624bba384a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2265,6 +2265,7 @@ 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', @@ -2359,7 +2360,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', @@ -7986,6 +7986,7 @@ phutil_register_library_map(array( 'PhabricatorAuthLoginMessageType' => 'PhabricatorAuthMessageType', 'PhabricatorAuthLogoutConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod', 'PhabricatorAuthMFAEditEngineExtension' => 'PhabricatorEditEngineExtension', + 'PhabricatorAuthMFASyncTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', 'PhabricatorAuthMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension', 'PhabricatorAuthManagementCachePKCS8Workflow' => 'PhabricatorAuthManagementWorkflow', 'PhabricatorAuthManagementLDAPWorkflow' => 'PhabricatorAuthManagementWorkflow', @@ -8101,7 +8102,6 @@ phutil_register_library_map(array( 'PhabricatorAuthSetPasswordController' => 'PhabricatorAuthController', 'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorAuthStartController' => 'PhabricatorAuthController', - 'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', 'PhabricatorAuthTemporaryToken' => array( 'PhabricatorAuthDAO', 'PhabricatorPolicyInterface', diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index c7fc9891af..71c0fba836 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -245,4 +245,104 @@ abstract class PhabricatorAuthFactor extends Phobject { } +/* -( Synchronizing New Factors )------------------------------------------ */ + + + final protected function loadMFASyncToken( + 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); + + // Note that property generation is unguarded, since factors that push + // a challenge generally need to perform a write there. + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $properties = $this->newMFASyncTokenProperties($user); + + foreach ($properties as $key => $value) { + $sync_token->setTemporaryTokenProperty($key, $value); + } + + $sync_token->save(); + unset($unguarded); + } + + $form->addHiddenInput($this->getMFASyncTokenFormKey(), $sync_key); + + return $sync_token; + } + + protected function newMFASyncTokenProperties(PhabricatorUser $user) { + return array(); + } + + private function getMFASyncTokenFormKey() { + return 'sync.key'; + } + + private function getMFASyncTokenTTL() { + return phutil_units('1 hour in seconds'); + } + } diff --git a/src/applications/auth/factor/PhabricatorAuthTOTPKeyTemporaryTokenType.php b/src/applications/auth/factor/PhabricatorAuthMFASyncTemporaryTokenType.php similarity index 52% rename from src/applications/auth/factor/PhabricatorAuthTOTPKeyTemporaryTokenType.php rename to src/applications/auth/factor/PhabricatorAuthMFASyncTemporaryTokenType.php index 02f62e76be..e44da0b00c 100644 --- a/src/applications/auth/factor/PhabricatorAuthTOTPKeyTemporaryTokenType.php +++ b/src/applications/auth/factor/PhabricatorAuthMFASyncTemporaryTokenType.php @@ -1,17 +1,18 @@ 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( + $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 { @@ -104,9 +60,6 @@ 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 '. @@ -126,7 +79,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { 'otpauth://totp/%s:%s?secret=%s&issuer=%s', $issuer, $user->getUsername(), - $key, + $secret, $issuer); $qrcode = $this->renderQRCode($uri); @@ -135,7 +88,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Key')) - ->setValue(phutil_tag('strong', array(), $key))); + ->setValue(phutil_tag('strong', array(), $secret))); $form->appendInstructions( pht( @@ -526,4 +479,11 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { return $value; } + + protected function newMFASyncTokenProperties(PhabricatorUser $user) { + return array( + 'secret' => self::generateNewTOTPKey(), + ); + } + } diff --git a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php index 9fbf3fbfb1..1ade16f681 100644 --- a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php @@ -15,6 +15,7 @@ final class PhabricatorAuthFactorConfig private $sessionEngine; private $factorProvider = self::ATTACHABLE; + private $mfaSyncToken; protected function getConfiguration() { return array( @@ -61,6 +62,15 @@ final class PhabricatorAuthFactorConfig return $this->sessionEngine; } + public function setMFASyncToken(PhabricatorAuthTemporaryToken $token) { + $this->mfaSyncToken = $token; + return $this; + } + + public function getMFASyncToken() { + return $this->mfaSyncToken; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php b/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php index 8ffd603a47..2b96c7815f 100644 --- a/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php +++ b/src/applications/auth/storage/PhabricatorAuthTemporaryToken.php @@ -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 )----------------------------------------- */ diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php index e5d1ba17c2..c2b1a0b090 100644 --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -266,6 +266,13 @@ final class PhabricatorMultiFactorSettingsPanel $config->save(); + // If we used a temporary token to handle synchronizing the factor, + // revoke it now. + $sync_token = $config->getMFASyncToken(); + if ($sync_token) { + $sync_token->revokeToken(); + } + $log = PhabricatorUserLog::initializeNewLog( $viewer, $user->getPHID(), From 6c11f373965cd6bb6919b95e6f24d873173f083b Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 23 Jan 2019 09:26:32 -0800 Subject: [PATCH 23/42] Add a pre-enroll step for MFA, primarily as a CSRF gate Summary: Depends on D20020. Ref T13222. This puts another step in the MFA enrollment flow: pick a provider; read text and click "Continue"; actually enroll. This is primarily to stop CSRF attacks, since otherwise an attacker can put `` on `cute-cat-pix.com` and get you to send yourself some SMS enrollment text messages, which would be mildly annoying. We could skip this step if we already have a valid CSRF token (and we often will), but I think there's some value in doing it anyway. In particular: - For SMS/Duo, it seems nice to have an explicit "we're about to hit your phone" button. - We could let installs customize this text and give users a smoother onboard. - It allows the relatively wordy enroll form to be a little less wordy. - For tokens which can expire (SMS, Duo) it might save you from answering too slowly if you have to go dig your phone out of your bag downstairs or something. Test Plan: Added factors, read text. Tried to CSRF the endpoint, got a dialog instead of a live challenge generation. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20021 --- .../auth/factor/PhabricatorAuthFactor.php | 24 ++++++++++------- .../auth/factor/PhabricatorTOTPAuthFactor.php | 26 ++++++++++++------- .../storage/PhabricatorAuthFactorProvider.php | 7 +++++ .../PhabricatorMultiFactorSettingsPanel.php | 20 +++++++++++++- 4 files changed, 58 insertions(+), 19 deletions(-) diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index 71c0fba836..af55ac3c8c 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -61,6 +61,16 @@ abstract class PhabricatorAuthFactor extends Phobject { return null; } + abstract public function getEnrollDescription( + PhabricatorAuthFactorProvider $provider, + PhabricatorUser $user); + + public function getEnrollButtonText( + PhabricatorAuthFactorProvider $provider, + PhabricatorUser $user) { + return pht('Continue'); + } + public function getFactorOrder() { return 1000; } @@ -315,17 +325,13 @@ abstract class PhabricatorAuthFactor extends Phobject { ->setTokenCode($sync_key_digest) ->setTokenExpires($now + $sync_ttl); - // Note that property generation is unguarded, since factors that push - // a challenge generally need to perform a write there. - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - $properties = $this->newMFASyncTokenProperties($user); + $properties = $this->newMFASyncTokenProperties($user); - foreach ($properties as $key => $value) { - $sync_token->setTemporaryTokenProperty($key, $value); - } + foreach ($properties as $key => $value) { + $sync_token->setTemporaryTokenProperty($key, $value); + } - $sync_token->save(); - unset($unguarded); + $sync_token->save(); } $form->addHiddenInput($this->getMFASyncTokenFormKey(), $sync_key); diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index 0edf6fcb74..91799950e7 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -23,6 +23,21 @@ 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 processAddFactorForm( PhabricatorAuthFactorProvider $provider, AphrontFormView $form, @@ -60,17 +75,10 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { } } - $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(); diff --git a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php index b1abb02ba5..40a51e741e 100644 --- a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php @@ -109,6 +109,13 @@ final class PhabricatorAuthFactorProvider ->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); + } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php index c2b1a0b090..d5cbf75951 100644 --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -231,10 +231,26 @@ final class PhabricatorMultiFactorSettingsPanel ->addCancelButton($cancel_uri); } + // NOTE: Beyond providing guidance, this step is also providing a CSRF gate + // on this endpoint, since prompting the user to respond to a challenge + // sometimes requires us to push a challenge to them as a side effect (for + // example, with SMS). + if (!$request->isFormPost() || !$request->getBool('mfa.start')) { + $description = $selected_provider->getEnrollDescription($viewer); + + return $this->newDialog() + ->addHiddenInput('providerPHID', $selected_provider->getPHID()) + ->addHiddenInput('mfa.start', 1) + ->setTitle(pht('Add Authentication Factor')) + ->appendChild(new PHUIRemarkupView($viewer, $description)) + ->addCancelButton($cancel_uri) + ->addSubmitButton($selected_provider->getEnrollButtonText($viewer)); + } + $form = id(new AphrontFormView()) ->setViewer($viewer); - if ($request->isFormPost()) { + if ($request->getBool('mfa.enroll')) { // Subject users to rate limiting so that it's difficult to add factors // by pure brute force. This is normally not much of an attack, but push // factor types may have side effects. @@ -295,6 +311,8 @@ final class PhabricatorMultiFactorSettingsPanel return $this->newDialog() ->addHiddenInput('providerPHID', $selected_provider->getPHID()) + ->addHiddenInput('mfa.start', 1) + ->addHiddenInput('mfa.enroll', 1) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle(pht('Add Authentication Factor')) ->appendChild($form->buildLayoutView()) From ada8a56bb7dbf028bd125f26401c2fd4135c8940 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 16 Jan 2019 08:38:45 -0800 Subject: [PATCH 24/42] Implement SMS MFA Summary: Depends on D20021. Ref T13222. This has a few rough edges, including: - The challenges theselves are CSRF-able. - You can go disable/edit your contact number after setting up SMS MFA and lock yourself out of your account. - SMS doesn't require MFA so an attacker can just swap your number to their number. ...but mostly works. Test Plan: - Added SMS MFA to my account. - Typed in the number I was texted. - Typed in some other different numbers (didn't work). - Cancelled/resumed the workflow, used SMS in conjunction with other factors, tried old codes, etc. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20022 --- src/__phutil_library_map__.php | 2 + .../auth/factor/PhabricatorAuthFactor.php | 47 ++- .../auth/factor/PhabricatorSMSAuthFactor.php | 367 ++++++++++++++++++ .../phid/PhabricatorAuthMessagePHIDType.php | 4 +- 4 files changed, 414 insertions(+), 6 deletions(-) create mode 100644 src/applications/auth/factor/PhabricatorSMSAuthFactor.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 624bba384a..84b99531ac 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4297,6 +4297,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', @@ -10393,6 +10394,7 @@ phutil_register_library_map(array( 'PhabricatorResourceSite' => 'PhabricatorSite', 'PhabricatorRobotsController' => 'PhabricatorController', 'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine', + 'PhabricatorSMSAuthFactor' => 'PhabricatorAuthFactor', 'PhabricatorSQLPatchList' => 'Phobject', 'PhabricatorSSHKeyGenerator' => 'Phobject', 'PhabricatorSSHKeysSettingsPanel' => 'PhabricatorSettingsPanel', diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index af55ac3c8c..768ad34e18 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -33,7 +33,8 @@ abstract class PhabricatorAuthFactor extends Phobject { protected function newConfigForUser(PhabricatorUser $user) { return id(new PhabricatorAuthFactorConfig()) - ->setUserPHID($user->getPHID()); + ->setUserPHID($user->getPHID()) + ->setFactorSecret(''); } protected function newResult() { @@ -107,6 +108,10 @@ 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, @@ -131,10 +136,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; @@ -351,4 +356,36 @@ abstract class PhabricatorAuthFactor extends Phobject { 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; + } + } diff --git a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php new file mode 100644 index 0000000000..03558f7333 --- /dev/null +++ b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php @@ -0,0 +1,367 @@ +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(PhabricatorUser $user) { + if (!$this->loadUserContactNumber($user)) { + return false; + } + + return true; + } + + public function getConfigurationCreateDescription(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.'), + )); + } + + 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($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(); + } + + // 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(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) { + + $uri = PhabricatorEnv::getURI('/'); + $uri = new PhutilURI($uri); + + return id(new PhabricatorMetaMTAMail()) + ->setMessageType(PhabricatorMailSMSMessage::MESSAGETYPE) + ->addTos(array($user->getPHID())) + ->setForceDelivery(true) + ->setSensitiveContent(true) + ->setBody( + pht( + 'Phabricator (%s) MFA Code: %s', + $uri->getDomain(), + $envelope->openEnvelope())) + ->save(); + } + + private function normalizeSMSCode($code) { + return trim($code); + } + + private function getChallengeResponseParameterName( + PhabricatorAuthFactorConfig $config) { + return $this->getParameterName($config, 'sms.code'); + } + + private function getChallengeResponseFromRequest( + PhabricatorAuthFactorConfig $config, + AphrontRequest $request) { + + $name = $this->getChallengeResponseParameterName($config); + + $value = $request->getStr($name); + $value = (string)$value; + $value = trim($value); + + return $value; + } + +} diff --git a/src/applications/auth/phid/PhabricatorAuthMessagePHIDType.php b/src/applications/auth/phid/PhabricatorAuthMessagePHIDType.php index cc37880ac8..7a023c4a4c 100644 --- a/src/applications/auth/phid/PhabricatorAuthMessagePHIDType.php +++ b/src/applications/auth/phid/PhabricatorAuthMessagePHIDType.php @@ -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( From 7805b217ad8ba29ef4b98202d7059f1663aa1092 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 23 Jan 2019 11:08:02 -0800 Subject: [PATCH 25/42] Prevent users from editing, disabling, or swapping their primary contact number while they have SMS MFA Summary: Depends on D20022. Ref T13222. Since you can easily lock yourself out of your account by swapping to a bad number, prevent contact number edits while "contact number" MFA (today, always SMS) is enabled. (Another approach would be to bind factors to specific contact numbers, and then prevent that number from being edited or disabled while SMS MFA was attached to it. However, I think that's a bit more complicated and a little more unwieldy, and ends up in about the same place as this. I'd consider it more strongly in the future if we had like 20 users say "I have 9 phones" but I doubt this is a real use case.) Test Plan: - With SMS MFA, tried to edit my primary contact number, disable it, and promote another number to become primary. Got a sensible error message in all cases. - After removing SMS MFA, did all that stuff with no issues. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20023 --- ...atorAuthContactNumberPrimaryController.php | 11 ++- .../auth/factor/PhabricatorAuthFactor.php | 12 ++++ .../auth/factor/PhabricatorSMSAuthFactor.php | 4 ++ ...atorAuthContactNumberNumberTransaction.php | 5 ++ ...torAuthContactNumberPrimaryTransaction.php | 6 ++ ...atorAuthContactNumberStatusTransaction.php | 6 ++ ...icatorAuthContactNumberTransactionType.php | 70 ++++++++++++++++++- 7 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php index afcc065559..ee76094a68 100644 --- a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php @@ -55,7 +55,16 @@ final class PhabricatorAuthContactNumberPrimaryController ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); - $editor->applyTransactions($number, $xactions); + 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); } diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index 768ad34e18..f11b81549f 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -62,6 +62,18 @@ abstract class PhabricatorAuthFactor extends Phobject { return null; } + /** + * 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); diff --git a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php index 03558f7333..baa714c21d 100644 --- a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php @@ -28,6 +28,10 @@ final class PhabricatorSMSAuthFactor return 2000; } + public function isContactNumberFactor() { + return true; + } + public function canCreateNewProvider() { return $this->isSMSMailerConfigured(); } diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php index 00499959ec..88d9d4bffc 100644 --- a/src/applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php +++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php @@ -83,6 +83,11 @@ final class PhabricatorAuthContactNumberNumberTransaction } } + $mfa_error = $this->newContactNumberMFAError($object, $xaction); + if ($mfa_error) { + $errors[] = $mfa_error; + continue; + } } return $errors; diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php index 2e4b6ff55c..42788029b5 100644 --- a/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php +++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberPrimaryTransaction.php @@ -41,6 +41,12 @@ final class PhabricatorAuthContactNumberPrimaryTransaction $xaction); continue; } + + $mfa_error = $this->newContactNumberMFAError($object, $xaction); + if ($mfa_error) { + $errors[] = $mfa_error; + continue; + } } return $errors; diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php index 305243ae15..5dab6fe8c0 100644 --- a/src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php +++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberStatusTransaction.php @@ -46,6 +46,12 @@ final class PhabricatorAuthContactNumberStatusTransaction 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 diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php index c32fbe6a30..a74c78d4c4 100644 --- a/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php +++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php @@ -1,4 +1,72 @@ 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); + } + } + +} From 587e9cea19ac6cf1e10842e97df25f6cd894537a Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 23 Jan 2019 11:27:11 -0800 Subject: [PATCH 26/42] Always require MFA to edit contact numbers Summary: Depends on D20023. Ref T13222. Although I think this isn't strictly necessary from a pure security perspective (since you can't modify the primary number while you have MFA SMS), it seems like a generally good idea. This adds a slightly new MFA mode, where we want MFA if it's available but don't strictly require it. Test Plan: Disabled, enabled, primaried, unprimaried, and edited contact numbers. With MFA enabled, got prompted for MFA. With no MFA, no prompts. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20024 --- src/__phutil_library_map__.php | 3 +++ ...atorAuthContactNumberDisableController.php | 5 ++-- ...atorAuthContactNumberPrimaryController.php | 5 ++-- .../PhabricatorAuthContactNumberMFAEngine.php | 10 ++++++++ .../storage/PhabricatorAuthContactNumber.php | 10 +++++++- .../PhabricatorEditEngineMFAEngine.php | 24 ++++++++++++++++++- ...habricatorApplicationTransactionEditor.php | 4 ++++ 7 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 src/applications/auth/engine/PhabricatorAuthContactNumberMFAEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 84b99531ac..2045dd217b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2205,6 +2205,7 @@ phutil_register_library_map(array( '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', @@ -7909,12 +7910,14 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorEditEngineMFAInterface', ), 'PhabricatorAuthContactNumberController' => 'PhabricatorAuthController', 'PhabricatorAuthContactNumberDisableController' => 'PhabricatorAuthContactNumberController', 'PhabricatorAuthContactNumberEditController' => 'PhabricatorAuthContactNumberController', 'PhabricatorAuthContactNumberEditEngine' => 'PhabricatorEditEngine', 'PhabricatorAuthContactNumberEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorAuthContactNumberMFAEngine' => 'PhabricatorEditEngineMFAEngine', 'PhabricatorAuthContactNumberNumberTransaction' => 'PhabricatorAuthContactNumberTransactionType', 'PhabricatorAuthContactNumberPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthContactNumberPrimaryController' => 'PhabricatorAuthContactNumberController', diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberDisableController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberDisableController.php index f860205458..a525e7b930 100644 --- a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberDisableController.php +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberDisableController.php @@ -24,7 +24,7 @@ final class PhabricatorAuthContactNumberDisableController $id = $number->getID(); $cancel_uri = $number->getURI(); - if ($request->isFormPost()) { + if ($request->isFormOrHisecPost()) { $xactions = array(); if ($is_disable) { @@ -42,7 +42,8 @@ final class PhabricatorAuthContactNumberDisableController ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true); + ->setContinueOnMissingFields(true) + ->setCancelURI($cancel_uri); try { $editor->applyTransactions($number, $xactions); diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php index ee76094a68..cad1bbf3fc 100644 --- a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberPrimaryController.php @@ -41,7 +41,7 @@ final class PhabricatorAuthContactNumberPrimaryController ->addCancelButton($cancel_uri); } - if ($request->isFormPost()) { + if ($request->isFormOrHisecPost()) { $xactions = array(); $xactions[] = id(new PhabricatorAuthContactNumberTransaction()) @@ -53,7 +53,8 @@ final class PhabricatorAuthContactNumberPrimaryController ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true); + ->setContinueOnMissingFields(true) + ->setCancelURI($cancel_uri); try { $editor->applyTransactions($number, $xactions); diff --git a/src/applications/auth/engine/PhabricatorAuthContactNumberMFAEngine.php b/src/applications/auth/engine/PhabricatorAuthContactNumberMFAEngine.php new file mode 100644 index 0000000000..969ca320a0 --- /dev/null +++ b/src/applications/auth/engine/PhabricatorAuthContactNumberMFAEngine.php @@ -0,0 +1,10 @@ +setObject($object); } - abstract public function shouldRequireMFA(); + /** + * Do edits to this object REQUIRE that the user submit MFA? + * + * This is a strict requirement: users will need to add MFA to their accounts + * if they don't already have it. + * + * @return bool True to strictly require MFA. + */ + public function shouldRequireMFA() { + return false; + } + + /** + * Should edits to this object prompt for MFA if it's available? + * + * This is advisory: users without MFA on their accounts will be able to + * perform edits without being required to add MFA. + * + * @return bool True to prompt for MFA if available. + */ + public function shouldTryMFA() { + return false; + } } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index e77c2c7ba1..fda45fd3d5 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -4916,6 +4916,10 @@ abstract class PhabricatorApplicationTransactionEditor $require_mfa = $engine->shouldRequireMFA(); if (!$require_mfa) { + $try_mfa = $engine->shouldTryMFA(); + if ($try_mfa) { + $this->setShouldRequireMFA(true); + } return $xactions; } From ab2cbbd9f946ca164940acd223483d14877e122e Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 23 Jan 2019 12:15:05 -0800 Subject: [PATCH 27/42] Add a "test message" action for contact numbers Summary: Depends on D20024. See D20022. Put something in place temporarily until we build out validation at some point. Test Plan: Sent myself a test message. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20025 --- src/__phutil_library_map__.php | 4 ++ .../action/PhabricatorAuthTestSMSAction.php | 22 +++++++ .../PhabricatorAuthApplication.php | 2 + ...ricatorAuthContactNumberTestController.php | 64 +++++++++++++++++++ ...ricatorAuthContactNumberViewController.php | 8 +++ .../PhabricatorMetaMTAMailViewController.php | 3 + 6 files changed, 103 insertions(+) create mode 100644 src/applications/auth/action/PhabricatorAuthTestSMSAction.php create mode 100644 src/applications/auth/controller/contact/PhabricatorAuthContactNumberTestController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2045dd217b..feeed6ef74 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2212,6 +2212,7 @@ phutil_register_library_map(array( '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', @@ -2368,6 +2369,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', @@ -7924,6 +7926,7 @@ phutil_register_library_map(array( 'PhabricatorAuthContactNumberPrimaryTransaction' => 'PhabricatorAuthContactNumberTransactionType', 'PhabricatorAuthContactNumberQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthContactNumberStatusTransaction' => 'PhabricatorAuthContactNumberTransactionType', + 'PhabricatorAuthContactNumberTestController' => 'PhabricatorAuthContactNumberController', 'PhabricatorAuthContactNumberTransaction' => 'PhabricatorModularTransaction', 'PhabricatorAuthContactNumberTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuthContactNumberTransactionType' => 'PhabricatorModularTransactionType', @@ -8116,6 +8119,7 @@ phutil_register_library_map(array( 'PhabricatorAuthTemporaryTokenType' => 'Phobject', 'PhabricatorAuthTemporaryTokenTypeModule' => 'PhabricatorConfigModule', 'PhabricatorAuthTerminateSessionController' => 'PhabricatorAuthController', + 'PhabricatorAuthTestSMSAction' => 'PhabricatorSystemAction', 'PhabricatorAuthTryFactorAction' => 'PhabricatorSystemAction', 'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController', 'PhabricatorAuthValidateController' => 'PhabricatorAuthController', diff --git a/src/applications/auth/action/PhabricatorAuthTestSMSAction.php b/src/applications/auth/action/PhabricatorAuthTestSMSAction.php new file mode 100644 index 0000000000..d0f4a6bb7e --- /dev/null +++ b/src/applications/auth/action/PhabricatorAuthTestSMSAction.php @@ -0,0 +1,22 @@ +[1-9]\d*)/' => 'PhabricatorAuthContactNumberPrimaryController', + 'test/(?P[1-9]\d*)/' => + 'PhabricatorAuthContactNumberTestController', ), ), diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberTestController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberTestController.php new file mode 100644 index 0000000000..2c25fa3f4a --- /dev/null +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberTestController.php @@ -0,0 +1,64 @@ +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); + } + +} diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php index eaf84ea08a..027d288dbc 100644 --- a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php @@ -98,6 +98,14 @@ final class PhabricatorAuthContactNumberViewController ->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'); diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php index b831a9c9d6..d7d31ba254 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php @@ -187,6 +187,9 @@ final class PhabricatorMetaMTAMailViewController ->setStacked(true); $headers = $mail->getDeliveredHeaders(); + if (!$headers) { + $headers = array(); + } // Sort headers by name. $headers = isort($headers, 0); From e72684a4ba5d906599d146cdb52fde0fab506f40 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Thu, 3 Jan 2019 15:00:11 -0800 Subject: [PATCH 28/42] Add infrastructure for sending SMS via AWS SNS Summary: Ref T920. Ref T13235. This adds a `Future`, similar to `TwilioFuture`, for interacting with Amazon's SNS service. Also updates the documentation. Also makes the code consistent with the documentation by accepting a `media` argument. Test Plan: Clicked the "send test message" button from the Settings UI. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T13235, T920 Differential Revision: https://secure.phabricator.com/D19982 --- src/__phutil_library_map__.php | 4 ++ .../PhabricatorMailAmazonSNSAdapter.php | 63 +++++++++++++++++++ .../future/PhabricatorAmazonSNSFuture.php | 41 ++++++++++++ .../configuring_outbound_email.diviner | 10 +++ .../PhabricatorClusterMailersConfigType.php | 1 + 5 files changed, 119 insertions(+) create mode 100644 src/applications/metamta/adapter/PhabricatorMailAmazonSNSAdapter.php create mode 100644 src/applications/metamta/future/PhabricatorAmazonSNSFuture.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index feeed6ef74..c3bbc146dc 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2083,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', @@ -3425,6 +3426,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', @@ -7773,6 +7775,7 @@ phutil_register_library_map(array( 'PhabricatorAjaxRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorAlmanacApplication' => 'PhabricatorApplication', 'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider', + 'PhabricatorAmazonSNSFuture' => 'PhutilAWSFuture', 'PhabricatorAnchorView' => 'AphrontView', 'PhabricatorAphlictManagementDebugWorkflow' => 'PhabricatorAphlictManagementWorkflow', 'PhabricatorAphlictManagementNotifyWorkflow' => 'PhabricatorAphlictManagementWorkflow', @@ -9318,6 +9321,7 @@ phutil_register_library_map(array( 'PhabricatorMacroViewController' => 'PhabricatorMacroController', 'PhabricatorMailAdapter' => 'Phobject', 'PhabricatorMailAmazonSESAdapter' => 'PhabricatorMailAdapter', + 'PhabricatorMailAmazonSNSAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailAttachment' => 'Phobject', 'PhabricatorMailConfigTestCase' => 'PhabricatorTestCase', 'PhabricatorMailEmailEngine' => 'PhabricatorMailMessageEngine', diff --git a/src/applications/metamta/adapter/PhabricatorMailAmazonSNSAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAmazonSNSAdapter.php new file mode 100644 index 0000000000..b34e422dba --- /dev/null +++ b/src/applications/metamta/adapter/PhabricatorMailAmazonSNSAdapter.php @@ -0,0 +1,63 @@ + '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(); + } + +} diff --git a/src/applications/metamta/future/PhabricatorAmazonSNSFuture.php b/src/applications/metamta/future/PhabricatorAmazonSNSFuture.php new file mode 100644 index 0000000000..3be236eee2 --- /dev/null +++ b/src/applications/metamta/future/PhabricatorAmazonSNSFuture.php @@ -0,0 +1,41 @@ +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; + + } + +} diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner index 7691a88d24..b8b3835e18 100644 --- a/src/docs/user/configuration/configuring_outbound_email.diviner +++ b/src/docs/user/configuration/configuring_outbound_email.diviner @@ -97,6 +97,7 @@ The `type` field can be used to select these third-party mailers: - `ses`: Use Amazon SES. - `sendgrid`: Use SendGrid. - `postmark`: Use Postmark. + - `sns`: Use Amazon SNS (only for sending SMS messages). It also supports these local mailers: @@ -208,6 +209,15 @@ which "From" address to use by setting `metamta.default-address` in your config, then follow the Amazon SES verification process to verify it. You won't be able to send email until you do this! +Mailer: Amazon SNS +================== + +Amazon SNS is Amazon's cloud notification service. You can learn more at +. Note that this mailer is only able to send +SMS messages, not emails. + +To use this mailer, set `type` to `sns`, then configure the options similarly +to the SES configuration above. Mailer: SendGrid ================ diff --git a/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php b/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php index d23ee11d8b..e9b01f8cbf 100644 --- a/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php +++ b/src/infrastructure/cluster/config/PhabricatorClusterMailersConfigType.php @@ -45,6 +45,7 @@ final class PhabricatorClusterMailersConfigType 'options' => 'optional wild', 'inbound' => 'optional bool', 'outbound' => 'optional bool', + 'media' => 'optional list', )); } catch (Exception $ex) { throw $this->newException( From 5cfcef7f535c999e31189c95d038391549c1d3c2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 24 Jan 2019 14:59:34 -0800 Subject: [PATCH 29/42] Fix bad "$this" references in "Must Encrypt" mail after MailEngine changes Summary: See PHI1038. I missed these when pulling the code out. Test Plan: Sent "Must encrypt" mail, verified it made it through the queue in one piece. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20029 --- .../metamta/engine/PhabricatorMailEmailEngine.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/applications/metamta/engine/PhabricatorMailEmailEngine.php b/src/applications/metamta/engine/PhabricatorMailEmailEngine.php index fc00ccb3bb..ef7b92a7d3 100644 --- a/src/applications/metamta/engine/PhabricatorMailEmailEngine.php +++ b/src/applications/metamta/engine/PhabricatorMailEmailEngine.php @@ -90,9 +90,9 @@ final class PhabricatorMailEmailEngine if ($must_encrypt) { $parts = array(); - $encrypt_uri = $this->getMustEncryptURI(); + $encrypt_uri = $mail->getMustEncryptURI(); if (!strlen($encrypt_uri)) { - $encrypt_phid = $this->getRelatedPHID(); + $encrypt_phid = $mail->getRelatedPHID(); if ($encrypt_phid) { $encrypt_uri = urisprintf( '/object/%s/', @@ -111,7 +111,7 @@ final class PhabricatorMailEmailEngine 'secure channel. To view the message content, follow this '. 'link:'); - $parts[] = PhabricatorEnv::getProductionURI($this->getURI()); + $parts[] = PhabricatorEnv::getProductionURI($mail->getURI()); $body = implode("\n\n", $parts); } else { From 98c4cdc5bebf51c67bfbdc87532d2e1e679b8524 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 23 Jan 2019 14:32:00 -0800 Subject: [PATCH 30/42] Make the "PHP 7" setup warning more explicit about what it means Summary: See . Test Plan: o~o Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20027 --- .../PhabricatorPHPPreflightSetupCheck.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/applications/config/check/PhabricatorPHPPreflightSetupCheck.php b/src/applications/config/check/PhabricatorPHPPreflightSetupCheck.php index 7c9653f4ff..30c6036c8f 100644 --- a/src/applications/config/check/PhabricatorPHPPreflightSetupCheck.php +++ b/src/applications/config/check/PhabricatorPHPPreflightSetupCheck.php @@ -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', From 069160404fe802f6a20c10346a0187bbd43da5d7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 23 Jan 2019 13:29:20 -0800 Subject: [PATCH 31/42] Add a Duo API future Summary: Depends on D20025. Ref T13231. Although I'm not currently planning to actually upstream a Duo MFA provider, it's probably easiest to put most of the support pieces in the upstream until T5055. Test Plan: Used a test script to make some (mostly trivial) API calls and got valid results back, so I think the parameter signing is correct. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13231 Differential Revision: https://secure.phabricator.com/D20026 --- src/__phutil_library_map__.php | 2 + .../auth/future/PhabricatorDuoFuture.php | 150 ++++++++++++++++++ .../future/PhabricatorTwilioFuture.php | 2 +- 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/applications/auth/future/PhabricatorDuoFuture.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index c3bbc146dc..66ae2bae78 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2983,6 +2983,7 @@ phutil_register_library_map(array( 'PhabricatorDraftEngine' => 'applications/transactions/draft/PhabricatorDraftEngine.php', 'PhabricatorDraftInterface' => 'applications/transactions/draft/PhabricatorDraftInterface.php', 'PhabricatorDrydockApplication' => 'applications/drydock/application/PhabricatorDrydockApplication.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', @@ -8829,6 +8830,7 @@ phutil_register_library_map(array( 'PhabricatorDraftDAO' => 'PhabricatorLiskDAO', 'PhabricatorDraftEngine' => 'Phobject', 'PhabricatorDrydockApplication' => 'PhabricatorApplication', + 'PhabricatorDuoFuture' => 'FutureProxy', 'PhabricatorEdgeChangeRecord' => 'Phobject', 'PhabricatorEdgeChangeRecordTestCase' => 'PhabricatorTestCase', 'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants', diff --git a/src/applications/auth/future/PhabricatorDuoFuture.php b/src/applications/auth/future/PhabricatorDuoFuture.php new file mode 100644 index 0000000000..c6167ccdb6 --- /dev/null +++ b/src/applications/auth/future/PhabricatorDuoFuture.php @@ -0,0 +1,150 @@ +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); + + $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; + } + +} diff --git a/src/applications/metamta/future/PhabricatorTwilioFuture.php b/src/applications/metamta/future/PhabricatorTwilioFuture.php index 91b0588d23..8dc70329f8 100644 --- a/src/applications/metamta/future/PhabricatorTwilioFuture.php +++ b/src/applications/metamta/future/PhabricatorTwilioFuture.php @@ -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; From c9ff6ce390d68fdf176ab61fa2b19fc75f832c03 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 24 Jan 2019 10:16:30 -0800 Subject: [PATCH 32/42] Add CSRF to SMS challenges, and pave the way for more MFA types (including Duo) Summary: Depends on D20026. Ref T13222. Ref T13231. The primary change here is that we'll no longer send you an SMS if you hit an MFA gate without CSRF tokens. Then there's a lot of support for genralizing into Duo (and other push factors, potentially), I'll annotate things inline. Test Plan: Implemented Duo, elsewhere. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13231, T13222 Differential Revision: https://secure.phabricator.com/D20028 --- resources/celerity/map.php | 10 +- src/__phutil_library_map__.php | 2 + ...torHighSecurityRequestExceptionHandler.php | 30 +++-- .../engine/PhabricatorAuthSessionEngine.php | 26 +++- .../PhabricatorAuthFactorResultException.php | 17 +++ .../auth/factor/PhabricatorAuthFactor.php | 120 ++++++++++++++++++ .../factor/PhabricatorAuthFactorResult.php | 20 +++ .../auth/factor/PhabricatorSMSAuthFactor.php | 56 ++++---- .../auth/factor/PhabricatorTOTPAuthFactor.php | 63 +-------- .../auth/future/PhabricatorDuoFuture.php | 5 + .../auth/storage/PhabricatorAuthChallenge.php | 8 +- .../storage/PhabricatorAuthFactorConfig.php | 9 ++ src/view/phui/PHUIInfoView.php | 13 +- webroot/rsrc/css/phui/phui-form-view.css | 4 + webroot/rsrc/css/phui/phui-info-view.css | 9 ++ 15 files changed, 279 insertions(+), 113 deletions(-) create mode 100644 src/applications/auth/exception/PhabricatorAuthFactorResultException.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 651e95bf15..51bc9338d2 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'a66ea2e7', + 'core.pkg.css' => 'e0cb8094', 'core.pkg.js' => '5c737607', 'differential.pkg.css' => 'b8df73d4', 'differential.pkg.js' => '67c9ea4c', @@ -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,7 +825,7 @@ 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', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 66ae2bae78..6d2648ec30 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2239,6 +2239,7 @@ phutil_register_library_map(array( 'PhabricatorAuthFactorProviderTransactionType' => 'applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php', 'PhabricatorAuthFactorProviderViewController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php', 'PhabricatorAuthFactorResult' => 'applications/auth/factor/PhabricatorAuthFactorResult.php', + 'PhabricatorAuthFactorResultException' => 'applications/auth/exception/PhabricatorAuthFactorResultException.php', 'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php', 'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php', 'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php', @@ -7965,6 +7966,7 @@ phutil_register_library_map(array( 'PhabricatorAuthFactorProviderTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorAuthFactorProviderViewController' => 'PhabricatorAuthFactorProviderController', 'PhabricatorAuthFactorResult' => 'Phobject', + 'PhabricatorAuthFactorResultException' => 'Exception', 'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase', 'PhabricatorAuthFinishController' => 'PhabricatorAuthController', 'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO', diff --git a/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php b/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php index fe9af45666..2a737ecf5c 100644 --- a/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php +++ b/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php @@ -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( diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index 1fa2dadfd6..c3aa644e7e 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -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 @@ -480,6 +485,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { return $this->issueHighSecurityToken($session, true); } + $this->request = $request; foreach ($factors as $factor) { $factor->setSessionEngine($this); } @@ -523,10 +529,17 @@ final class PhabricatorAuthSessionEngine extends Phobject { $provider = $factor->getFactorProvider(); $impl = $provider->getFactor(); - $new_challenges = $impl->getNewIssuedChallenges( - $factor, - $viewer, - $issued_challenges); + try { + $new_challenges = $impl->getNewIssuedChallenges( + $factor, + $viewer, + $issued_challenges); + } catch (PhabricatorAuthFactorResultException $ex) { + $ok = false; + $validation_results[$factor_phid] = $ex->getResult(); + $challenge_map[$factor_phid] = $issued_challenges; + continue; + } foreach ($new_challenges as $new_challenge) { $issued_challenges[] = $new_challenge; @@ -546,7 +559,10 @@ final class PhabricatorAuthSessionEngine extends Phobject { continue; } - $ok = false; + if (!$result->getIsValid()) { + $ok = false; + } + $validation_results[$factor_phid] = $result; } diff --git a/src/applications/auth/exception/PhabricatorAuthFactorResultException.php b/src/applications/auth/exception/PhabricatorAuthFactorResultException.php new file mode 100644 index 0000000000..61595266e5 --- /dev/null +++ b/src/applications/auth/exception/PhabricatorAuthFactorResultException.php @@ -0,0 +1,17 @@ +result = $result; + parent::__construct(); + } + + public function getResult() { + return $this->result; + } + +} diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index f11b81549f..cf8087625f 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -232,6 +232,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); @@ -271,6 +281,34 @@ 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 )------------------------------------------ */ @@ -400,4 +438,86 @@ abstract class PhabricatorAuthFactor extends Phobject { 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 throwResult(PhabricatorAuthFactorResult $result) { + throw new PhabricatorAuthFactorResultException($result); + } + + 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(); + } + } diff --git a/src/applications/auth/factor/PhabricatorAuthFactorResult.php b/src/applications/auth/factor/PhabricatorAuthFactorResult.php index faa25b4f42..2282f162a9 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactorResult.php +++ b/src/applications/auth/factor/PhabricatorAuthFactorResult.php @@ -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; diff --git a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php index baa714c21d..a6f648af02 100644 --- a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php @@ -171,6 +171,38 @@ final class PhabricatorSMSAuthFactor return array(); } + if (!$this->loadUserContactNumber($viewer)) { + $result = $this->newResult() + ->setIsError(true) + ->setErrorMessage( + pht( + 'Your account has no primary contact number.')); + + $this->throwResult($result); + } + + if (!$this->isSMSMailerConfigured()) { + $result = $this->newResult() + ->setIsError(true) + ->setErrorMessage( + pht( + 'No outbound mailer which can deliver SMS messages is '. + 'configured.')); + + $this->throwResult($result); + } + + if (!$this->hasCSRF($config)) { + $result = $this->newResult() + ->setIsContinue(true) + ->setErrorMessage( + pht( + 'A text message with an authorization code will be sent to your '. + 'primary contact number.')); + + $this->throwResult($result); + } + // Otherwise, issue a new challenge. $challenge_code = $this->newSMSChallengeCode(); @@ -329,10 +361,6 @@ final class PhabricatorSMSAuthFactor private function sendSMSCodeToUser( PhutilOpaqueEnvelope $envelope, PhabricatorUser $user) { - - $uri = PhabricatorEnv::getURI('/'); - $uri = new PhutilURI($uri); - return id(new PhabricatorMetaMTAMail()) ->setMessageType(PhabricatorMailSMSMessage::MESSAGETYPE) ->addTos(array($user->getPHID())) @@ -341,7 +369,7 @@ final class PhabricatorSMSAuthFactor ->setBody( pht( 'Phabricator (%s) MFA Code: %s', - $uri->getDomain(), + $this->getInstallDisplayName(), $envelope->openEnvelope())) ->save(); } @@ -350,22 +378,4 @@ final class PhabricatorSMSAuthFactor return trim($code); } - private function getChallengeResponseParameterName( - PhabricatorAuthFactorConfig $config) { - return $this->getParameterName($config, 'sms.code'); - } - - private function getChallengeResponseFromRequest( - PhabricatorAuthFactorConfig $config, - AphrontRequest $request) { - - $name = $this->getChallengeResponseParameterName($config); - - $value = $request->getStr($name); - $value = (string)$value; - $value = trim($value); - - return $value; - } - } diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index 91799950e7..1401724125 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -90,7 +90,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { $secret, $issuer); - $qrcode = $this->renderQRCode($uri); + $qrcode = $this->newQRCode($uri); $form->appendChild($qrcode); $form->appendChild( @@ -390,49 +390,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; } @@ -470,24 +427,6 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { return null; } - private function getChallengeResponseParameterName( - PhabricatorAuthFactorConfig $config) { - return $this->getParameterName($config, 'totpcode'); - } - - private function getChallengeResponseFromRequest( - PhabricatorAuthFactorConfig $config, - AphrontRequest $request) { - - $name = $this->getChallengeResponseParameterName($config); - - $value = $request->getStr($name); - $value = (string)$value; - $value = trim($value); - - return $value; - } - protected function newMFASyncTokenProperties(PhabricatorUser $user) { return array( 'secret' => self::generateNewTOTPKey(), diff --git a/src/applications/auth/future/PhabricatorDuoFuture.php b/src/applications/auth/future/PhabricatorDuoFuture.php index c6167ccdb6..fd95906da1 100644 --- a/src/applications/auth/future/PhabricatorDuoFuture.php +++ b/src/applications/auth/future/PhabricatorDuoFuture.php @@ -112,6 +112,11 @@ final class PhabricatorDuoFuture $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) diff --git a/src/applications/auth/storage/PhabricatorAuthChallenge.php b/src/applications/auth/storage/PhabricatorAuthChallenge.php index 9e49ee154a..8fa07d712f 100644 --- a/src/applications/auth/storage/PhabricatorAuthChallenge.php +++ b/src/applications/auth/storage/PhabricatorAuthChallenge.php @@ -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() { diff --git a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php index 1ade16f681..e9a30a757e 100644 --- a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php @@ -71,6 +71,15 @@ final class PhabricatorAuthFactorConfig 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; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/view/phui/PHUIInfoView.php b/src/view/phui/PHUIInfoView.php index 69d0549299..af984f583e 100644 --- a/src/view/phui/PHUIInfoView.php +++ b/src/view/phui/PHUIInfoView.php @@ -8,6 +8,7 @@ final class PHUIInfoView extends AphrontTagView { const SEVERITY_NODATA = 'nodata'; const SEVERITY_SUCCESS = 'success'; const SEVERITY_PLAIN = 'plain'; + const SEVERITY_MFA = 'mfa'; private $title; private $errors = array(); @@ -73,20 +74,22 @@ final class PHUIInfoView extends AphrontTagView { switch ($this->getSeverity()) { case self::SEVERITY_ERROR: $icon = 'fa-exclamation-circle'; - break; + break; case self::SEVERITY_WARNING: $icon = 'fa-exclamation-triangle'; - break; + break; case self::SEVERITY_NOTICE: $icon = 'fa-info-circle'; - break; + break; case self::SEVERITY_PLAIN: case self::SEVERITY_NODATA: return null; - break; case self::SEVERITY_SUCCESS: $icon = 'fa-check-circle'; - break; + break; + case self::SEVERITY_MFA: + $icon = 'fa-lock'; + break; } $icon = id(new PHUIIconView()) diff --git a/webroot/rsrc/css/phui/phui-form-view.css b/webroot/rsrc/css/phui/phui-form-view.css index cd44b1135e..3368bcaafb 100644 --- a/webroot/rsrc/css/phui/phui-form-view.css +++ b/webroot/rsrc/css/phui/phui-form-view.css @@ -574,3 +574,7 @@ properly, and submit values. */ color: {$darkgreytext}; vertical-align: middle; } + +.mfa-form-enroll-button { + text-align: center; +} diff --git a/webroot/rsrc/css/phui/phui-info-view.css b/webroot/rsrc/css/phui/phui-info-view.css index 55400956e4..b4fafc6e59 100644 --- a/webroot/rsrc/css/phui/phui-info-view.css +++ b/webroot/rsrc/css/phui/phui-info-view.css @@ -93,6 +93,15 @@ h1.phui-info-view-head { color: {$red}; } +.phui-info-severity-mfa { + border-color: {$blue}; + border-left-width: 6px; +} + +.phui-info-severity-mfa .phui-info-icon { + color: {$blue}; +} + .phui-info-severity-warning { border-color: {$yellow}; border-left-width: 6px; From 03ac59a87708929e55ed7fdde466e9f1780e27b8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 28 Jan 2019 07:46:41 -0800 Subject: [PATCH 33/42] Don't put "spacePHID IN (...)" constraints in queries which will raise policy exceptions Summary: See T13240. Ref T13242. When we're issuing a query that will raise policy exceptions (i.e., give the user a "You Shall Not Pass" dialog if they can not see objects it loads), don't do space filtering in MySQL: when objects are filtered out in MySQL, we can't distinguish between "bad/invalid ID/object" and "policy filter", so we can't raise a policy exception. This leads to cases where viewing an object shows "You Shall Not Pass" if you can't see it for any non-Spaces reason, but "404" if the reason is Spaces. There's no product reason for this, it's just that `spacePHID IN (...)` is important for non-policy-raising queries (like a list of tasks) to reduce how much application filtering we need to do. Test Plan: Before: ``` $ git pull phabricator-ssh-exec: No repository "spellbook" exists! fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. ``` After: ``` $ git pull phabricator-ssh-exec: [You Shall Not Pass: Unknown Object (Repository)] This object is in a space you do not have permission to access. fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repository exists. ``` Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13242 Differential Revision: https://secure.phabricator.com/D20042 --- .../policy/PhabricatorCursorPagedPolicyAwareQuery.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 931840e8fb..9f7a69909a 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -2855,6 +2855,13 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery } } + // See T13240. If this query raises policy exceptions, don't filter objects + // in the MySQL layer. We want them to reach the application layer so we + // can reject them and raise an exception. + if ($this->shouldRaisePolicyExceptions()) { + return null; + } + $space_phids = array(); $include_null = false; From 8e5d9c6f0eb58db731d6579186f41dfd485a4812 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 25 Jan 2019 04:37:44 -0800 Subject: [PATCH 34/42] Allow MFA providers to be deprecated or disabled Summary: Ref T13222. Providers can now be deprecated (existing factors still work, but users can't add new factors for the provider) or disabled (factors stop working, also can't add new ones). Test Plan: - Enabled, deprecated, and disabled some providers. - Viewed provider detail, provider list. - Viewed MFA settings list. - Verified that I'm prompted for enabled + deprecated only at gates. - Tried to disable final provider, got an error. - Hit the MFA setup gate by enabling "Require MFA" with no providers, got a more useful message. - Immediately forced a user to the "MFA Setup Gate" by disabling their only active provider with another provider enabled ("We no longer support TOTP, you HAVE to finish Duo enrollment to continue starting Monday."). Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20031 --- src/__phutil_library_map__.php | 4 + .../PhabricatorAuthFactorProviderStatus.php | 103 ++++++++++++++++++ ...bricatorAuthNeedsMultiFactorController.php | 36 +++++- ...icatorAuthFactorProviderListController.php | 10 ++ ...icatorAuthFactorProviderViewController.php | 9 ++ ...habricatorAuthFactorProviderEditEngine.php | 10 ++ .../engine/PhabricatorAuthSessionEngine.php | 13 ++- .../auth/factor/PhabricatorAuthFactor.php | 8 +- .../auth/factor/PhabricatorSMSAuthFactor.php | 9 +- .../PhabricatorAuthFactorConfigQuery.php | 38 ++++++- .../storage/PhabricatorAuthFactorConfig.php | 6 + .../storage/PhabricatorAuthFactorProvider.php | 20 +++- ...torAuthFactorProviderStatusTransaction.php | 103 ++++++++++++++++++ .../people/storage/PhabricatorUser.php | 12 +- .../PhabricatorMultiFactorSettingsPanel.php | 59 +++++++--- 15 files changed, 406 insertions(+), 34 deletions(-) create mode 100644 src/applications/auth/constants/PhabricatorAuthFactorProviderStatus.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthFactorProviderStatusTransaction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6d2648ec30..fde9598e01 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2234,6 +2234,8 @@ phutil_register_library_map(array( 'PhabricatorAuthFactorProviderListController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.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', @@ -7961,6 +7963,8 @@ phutil_register_library_map(array( 'PhabricatorAuthFactorProviderListController' => 'PhabricatorAuthProviderController', 'PhabricatorAuthFactorProviderNameTransaction' => 'PhabricatorAuthFactorProviderTransactionType', 'PhabricatorAuthFactorProviderQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorAuthFactorProviderStatus' => 'Phobject', + 'PhabricatorAuthFactorProviderStatusTransaction' => 'PhabricatorAuthFactorProviderTransactionType', 'PhabricatorAuthFactorProviderTransaction' => 'PhabricatorModularTransaction', 'PhabricatorAuthFactorProviderTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuthFactorProviderTransactionType' => 'PhabricatorModularTransactionType', diff --git a/src/applications/auth/constants/PhabricatorAuthFactorProviderStatus.php b/src/applications/auth/constants/PhabricatorAuthFactorProviderStatus.php new file mode 100644 index 0000000000..61d4a12576 --- /dev/null +++ b/src/applications/auth/constants/PhabricatorAuthFactorProviderStatus.php @@ -0,0 +1,103 @@ +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, + ), + ); + } + +} diff --git a/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php b/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php index 92328b2000..259e4c6743 100644 --- a/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php +++ b/src/applications/auth/controller/PhabricatorAuthNeedsMultiFactorController.php @@ -197,6 +197,8 @@ final class PhabricatorAuthNeedsMultiFactorController ->addCancelButton('/', pht('Continue')); } + $views = array(); + $messages = array(); $messages[] = pht( @@ -210,7 +212,39 @@ final class PhabricatorAuthNeedsMultiFactorController ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setErrors($messages); - return $view; + $views[] = $view; + + + $providers = id(new PhabricatorAuthFactorProviderQuery()) + ->setViewer($viewer) + ->withStatuses( + array( + PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE, + )) + ->execute(); + if (!$providers) { + $messages = array(); + + $required_key = 'security.require-multi-factor-auth'; + + $messages[] = pht( + 'This install has the configuration option "%s" enabled, but does '. + 'not have any active multifactor providers configured. This means '. + 'you are required to add MFA, but are also prevented from doing so. '. + 'An administrator must disable "%s" or enable an MFA provider to '. + 'allow you to continue.', + $required_key, + $required_key); + + $view = id(new PHUIInfoView()) + ->setTitle(pht('Multi-Factor Authentication is Misconfigured')) + ->setSeverity(PHUIInfoView::SEVERITY_ERROR) + ->setErrors($messages); + + $views[] = $view; + } + + return $views; } } diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php index 293728cf36..d19671c3ce 100644 --- a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderListController.php @@ -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); } diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php index 67edf2f81b..3047c8714d 100644 --- a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php @@ -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; } diff --git a/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php index a0b5988589..c4e9a1dea5 100644 --- a/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php +++ b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php @@ -95,6 +95,8 @@ final class PhabricatorAuthFactorProviderEditEngine protected function buildCustomEditFields($object) { $factor_name = $object->getFactor()->getFactorName(); + $status_map = PhabricatorAuthFactorProviderStatus::getMap(); + return array( id(new PhabricatorStaticEditField()) ->setKey('displayType') @@ -109,6 +111,14 @@ 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), ); } diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index c3aa644e7e..46c4a9c672 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -473,9 +473,20 @@ final class PhabricatorAuthSessionEngine extends Phobject { $factors = id(new PhabricatorAuthFactorConfigQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) - ->setOrderVector(array('-id')) + ->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 // easier for users. A minor but desirable side effect is that when a user diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index cf8087625f..194f4acb59 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -54,11 +54,15 @@ abstract class PhabricatorAuthFactor extends Phobject { return null; } - public function canCreateNewConfiguration(PhabricatorUser $user) { + public function canCreateNewConfiguration( + PhabricatorAuthFactorProvider $provider, + PhabricatorUser $user) { return true; } - public function getConfigurationCreateDescription(PhabricatorUser $user) { + public function getConfigurationCreateDescription( + PhabricatorAuthFactorProvider $provider, + PhabricatorUser $user) { return null; } diff --git a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php index a6f648af02..d91dceb475 100644 --- a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php @@ -67,7 +67,10 @@ final class PhabricatorSMSAuthFactor return $messages; } - public function canCreateNewConfiguration(PhabricatorUser $user) { + public function canCreateNewConfiguration( + PhabricatorAuthFactorProvider $provider, + PhabricatorUser $user) { + if (!$this->loadUserContactNumber($user)) { return false; } @@ -75,7 +78,9 @@ final class PhabricatorSMSAuthFactor return true; } - public function getConfigurationCreateDescription(PhabricatorUser $user) { + public function getConfigurationCreateDescription( + PhabricatorAuthFactorProvider $provider, + PhabricatorUser $user) { $messages = array(); diff --git a/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php b/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php index 1674573b68..5f838f66ba 100644 --- a/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php +++ b/src/applications/auth/query/PhabricatorAuthFactorConfigQuery.php @@ -7,6 +7,7 @@ final class PhabricatorAuthFactorConfigQuery private $phids; private $userPHIDs; private $factorProviderPHIDs; + private $factorProviderStatuses; public function withIDs(array $ids) { $this->ids = $ids; @@ -28,6 +29,11 @@ final class PhabricatorAuthFactorConfigQuery return $this; } + public function withFactorProviderStatuses(array $statuses) { + $this->factorProviderStatuses = $statuses; + return $this; + } + public function newResultObject() { return new PhabricatorAuthFactorConfig(); } @@ -42,34 +48,54 @@ final class PhabricatorAuthFactorConfigQuery if ($this->ids !== null) { $where[] = qsprintf( $conn, - 'id IN (%Ld)', + 'config.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, - 'phid IN (%Ls)', + 'config.phid IN (%Ls)', $this->phids); } if ($this->userPHIDs !== null) { $where[] = qsprintf( $conn, - 'userPHID IN (%Ls)', + 'config.userPHID IN (%Ls)', $this->userPHIDs); } if ($this->factorProviderPHIDs !== null) { $where[] = qsprintf( $conn, - 'factorProviderPHID IN (%Ls)', + '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'); @@ -94,6 +120,10 @@ final class PhabricatorAuthFactorConfigQuery return $configs; } + protected function getPrimaryTableAlias() { + return 'config'; + } + public function getQueryApplicationClass() { return 'PhabricatorAuthApplication'; } diff --git a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php index e9a30a757e..ed5a27f543 100644 --- a/src/applications/auth/storage/PhabricatorAuthFactorConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorConfig.php @@ -80,6 +80,12 @@ final class PhabricatorAuthFactorConfig return $this; } + public function newSortVector() { + return id(new PhutilSortVector()) + ->addInt($this->getFactorProvider()->newStatus()->getOrder()) + ->addInt($this->getID()); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php index 40a51e741e..dfc948a423 100644 --- a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php @@ -14,15 +14,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() { @@ -117,6 +113,20 @@ final class PhabricatorAuthFactorProvider 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); + } + + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderStatusTransaction.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderStatusTransaction.php new file mode 100644 index 0000000000..37674f7b38 --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderStatusTransaction.php @@ -0,0 +1,103 @@ +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); + } + +} diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index e24024d96d..0b18c292c2 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -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) { diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php index d5cbf75951..c355b6e14a 100644 --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -53,8 +53,8 @@ final class PhabricatorMultiFactorSettingsPanel $factors = id(new PhabricatorAuthFactorConfigQuery()) ->setViewer($viewer) ->withUserPHIDs(array($user->getPHID())) - ->setOrderVector(array('-id')) ->execute(); + $factors = msort($factors, 'newSortVector'); $rows = array(); $rowc = array(); @@ -69,7 +69,16 @@ final class PhabricatorMultiFactorSettingsPanel $rowc[] = null; } + $status = $provider->newStatus(); + $status_icon = $status->getFactorIcon(); + $status_color = $status->getFactorColor(); + + $icon = id(new PHUIIconView()) + ->setIcon("{$status_icon} {$status_color}") + ->setTooltip(pht('Provider: %s', $status->getName())); + $rows[] = array( + $icon, javelin_tag( 'a', array( @@ -95,21 +104,24 @@ final class PhabricatorMultiFactorSettingsPanel pht("You haven't added any authentication factors to your account yet.")); $table->setHeaders( array( + null, pht('Name'), pht('Type'), pht('Created'), - '', + null, )); $table->setColumnClasses( array( + null, 'wide pri', - '', + null, 'right', 'action', )); $table->setRowClasses($rowc); $table->setDeviceVisibility( array( + true, true, false, false, @@ -129,12 +141,15 @@ final class PhabricatorMultiFactorSettingsPanel $add_color = PHUIButtonView::GREY; } + $can_add = (bool)$this->loadActiveMFAProviders(); + $buttons[] = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-plus') ->setText(pht('Add Auth Factor')) ->setHref($this->getPanelURI('?new=true')) ->setWorkflow(true) + ->setDisabled(!$can_add) ->setColor($add_color); $buttons[] = id(new PHUIButtonView()) @@ -155,21 +170,18 @@ final class PhabricatorMultiFactorSettingsPanel // Check that we have providers before we send the user through the MFA // gate, so you don't authenticate and then immediately get roadblocked. - $providers = id(new PhabricatorAuthFactorProviderQuery()) - ->setViewer($viewer) - ->withStatuses(array(PhabricatorAuthFactorProvider::STATUS_ACTIVE)) - ->execute(); + $providers = $this->loadActiveMFAProviders(); + if (!$providers) { return $this->newDialog() ->setTitle(pht('No MFA Providers')) ->appendParagraph( pht( - 'There are no active MFA providers. At least one active provider '. - 'must be available to add new MFA factors.')) + 'This install does not have any active MFA providers configured. '. + 'At least one provider must be configured and active before you '. + 'can add new MFA factors.')) ->addCancelButton($cancel_uri); } - $providers = mpull($providers, null, 'getPHID'); - $proivders = msortv($providers, 'newSortVector'); $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, @@ -184,8 +196,7 @@ final class PhabricatorMultiFactorSettingsPanel // Only let the user continue creating a factor for a given provider if // they actually pass the provider's checks. - $selected_factor = $selected_provider->getFactor(); - if (!$selected_factor->canCreateNewConfiguration($viewer)) { + if (!$selected_provider->canCreateNewConfiguration($viewer)) { $selected_provider = null; } } @@ -200,8 +211,7 @@ final class PhabricatorMultiFactorSettingsPanel $provider_uri = id(new PhutilURI($this->getPanelURI())) ->setQueryParam('providerPHID', $provider_phid); - $factor = $provider->getFactor(); - $is_enabled = $factor->canCreateNewConfiguration($viewer); + $is_enabled = $provider->canCreateNewConfiguration($viewer); $item = id(new PHUIObjectItemView()) ->setHeader($provider->getDisplayName()) @@ -216,7 +226,7 @@ final class PhabricatorMultiFactorSettingsPanel $item->setDisabled(true); } - $create_description = $factor->getConfigurationCreateDescription( + $create_description = $provider->getConfigurationCreateDescription( $viewer); if ($create_description) { $item->appendChild($create_description); @@ -424,5 +434,22 @@ final class PhabricatorMultiFactorSettingsPanel ->setDialog($dialog); } + private function loadActiveMFAProviders() { + $viewer = $this->getViewer(); + + $providers = id(new PhabricatorAuthFactorProviderQuery()) + ->setViewer($viewer) + ->withStatuses( + array( + PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE, + )) + ->execute(); + + $providers = mpull($providers, null, 'getPHID'); + $providers = msortv($providers, 'newSortVector'); + + return $providers; + } + } From 50abc873630b22b65530cb3d2a3871cd270d9a6c Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 25 Jan 2019 07:06:14 -0800 Subject: [PATCH 35/42] Expand outbound mailer documentation to mention SMS and include Twilio Summary: Depends on D20031. Ref T13222. Test Plan: Read "carefully". Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20032 --- .../configuring_outbound_email.diviner | 144 ++++++++++++++---- 1 file changed, 116 insertions(+), 28 deletions(-) diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner index b8b3835e18..4d18ba0eb2 100644 --- a/src/docs/user/configuration/configuring_outbound_email.diviner +++ b/src/docs/user/configuration/configuring_outbound_email.diviner @@ -1,33 +1,45 @@ @title Configuring Outbound Email @group config -Instructions for configuring Phabricator to send mail. +Instructions for configuring Phabricator to send email and other types of +messages, like text messages. Overview ======== -Phabricator can send outbound email through several different mail services, +Phabricator sends outbound messages through "mailers". Most mailers send +email and most messages are email messages, but mailers may also send other +types of messages (like text messages). + +Phabricator can send outbound messages through multiple different mailers, including a local mailer or various third-party services. Options include: -| Send Mail With | Setup | Cost | Inbound | Notes | -|---------|-------|------|---------|-------| -| Postmark | Easy | Cheap | Yes | Recommended | -| Mailgun | Easy | Cheap | Yes | Recommended | -| Amazon SES | Easy | Cheap | No | Recommended | -| SendGrid | Medium | Cheap | Yes | Discouraged | -| External SMTP | Medium | Varies | No | Gmail, etc. | -| Local SMTP | Hard | Free | No | sendmail, postfix, etc | -| Custom | Hard | Free | No | Write a custom mailer for some other service. | -| Drop in a Hole | Easy | Free | No | Drops mail in a deep, dark hole. | +| Send Mail With | Setup | Cost | Inbound | Media | Notes | +|----------------|-------|------|---------|-------|-------| +| Postmark | Easy | Cheap | Yes | Email | Recommended | +| Mailgun | Easy | Cheap | Yes | Email | Recommended | +| Amazon SES | Easy | Cheap | No | Email | | +| SendGrid | Medium | Cheap | Yes | Email | | +| Twilio | Easy | Cheap | No | SMS | Recommended | +| Amazon SNS | Easy | Cheap | No | SMS | Recommended | +| External SMTP | Medium | Varies | No | Email | Gmail, etc. | +| Local SMTP | Hard | Free | No | Email | sendmail, postfix, etc | +| Custom | Hard | Free | No | All | Write a custom mailer. | +| Drop in a Hole | Easy | Free | No | All | Drops mail in a deep, dark hole. | See below for details on how to select and configure mail delivery for each mailer. -Overall, Postmark and Mailgun are much easier to set up, and using one of them -is recommended. Both will also let you set up inbound email easily. +For email, Postmark or Mailgun are recommended because they make it easy to +set up inbound and outbound mail and have good track records in our production +services. Other services will also generally work well, but they may be more +difficult to set up. -If you have some internal mail service you'd like to use you can also write a -custom mailer, but this requires digging into the code. +For SMS, Twilio or SNS are recommended. They're also your only upstream +options. + +If you have some internal mail or messaging service you'd like to use you can +also write a custom mailer, but this requires digging into the code. Phabricator sends mail in the background, so the daemons need to be running for it to be able to deliver mail. You should receive setup warnings if they are @@ -91,13 +103,14 @@ The supported keys for each mailer are: types. Normally, you do not need to configure this. See below for a list of media types. -The `type` field can be used to select these third-party mailers: +The `type` field can be used to select these mailer services: - `mailgun`: Use Mailgun. - `ses`: Use Amazon SES. - `sendgrid`: Use SendGrid. - `postmark`: Use Postmark. - - `sns`: Use Amazon SNS (only for sending SMS messages). + - `twilio`: Use Twilio. + - `sns`: Use Amazon SNS. It also supports these local mailers: @@ -153,6 +166,12 @@ For alternatives and more information on configuration, see Mailer: Postmark ================ +| Media | Email +|---------| +| Inbound | Yes +|---------| + + Postmark is a third-party email delivery service. You can learn more at . @@ -183,8 +202,13 @@ documented at: Mailer: Mailgun =============== +| Media | Email +|---------| +| Inbound | Yes +|---------| + Mailgun is a third-party email delivery service. You can learn more at -. Mailgun is easy to configure and works well. +. Mailgun is easy to configure and works well. To use this mailer, set `type` to `mailgun`, then configure these `options`: @@ -195,8 +219,13 @@ To use this mailer, set `type` to `mailgun`, then configure these `options`: Mailer: Amazon SES ================== +| Media | Email +|---------| +| Inbound | No +|---------| + Amazon SES is Amazon's cloud email service. You can learn more at -. +. To use this mailer, set `type` to `ses`, then configure these `options`: @@ -209,21 +238,58 @@ which "From" address to use by setting `metamta.default-address` in your config, then follow the Amazon SES verification process to verify it. You won't be able to send email until you do this! +Mailer: Twilio +================== + +| Media | SMS +|---------| +| Inbound | No +|---------| + +Twilio is a third-party notification service. You can learn more at +. + + +To use this mailer, set `type` to `twilio`, then configure these options: + + - `account-sid`: Your Twilio Account SID. + - `auth-token`: Your Twilio Auth Token. + - `from-number`: Number to send text messages from, in E.164 format + (like `+15551237890`). + Mailer: Amazon SNS ================== +| Media | SMS +|---------| +| Inbound | No +|---------| + + Amazon SNS is Amazon's cloud notification service. You can learn more at -. Note that this mailer is only able to send +. Note that this mailer is only able to send SMS messages, not emails. -To use this mailer, set `type` to `sns`, then configure the options similarly -to the SES configuration above. +To use this mailer, set `type` to `sns`, then configure these options: + + - `access-key`: Required string. Your Amazon SNS access key. + - `secret-key`: Required string. Your Amazon SNS secret key. + - `endpoint`: Required string. Your Amazon SNS endpoint. + - `region`: Required string. Your Amazon SNS region. + +You can find the correct `region` value for your endpoint in the SNS +documentation. Mailer: SendGrid ================ +| Media | Email +|---------| +| Inbound | Yes +|---------| + SendGrid is a third-party email delivery service. You can learn more at -. +. You can configure SendGrid in two ways: you can send via SMTP or via the REST API. To use SMTP, configure Phabricator to use an `smtp` mailer. @@ -240,10 +306,16 @@ including an "API User". Make sure you're configuring your "API Key". Mailer: Sendmail ================ +| Media | Email +|---------| +| Inbound | Requires Configuration +|---------| + + This requires a `sendmail` binary to be installed on the system. Most MTAs -(e.g., sendmail, qmail, postfix) should do this, but your machine may not have -one installed by default. For install instructions, consult the documentation -for your favorite MTA. +(e.g., sendmail, qmail, postfix) should install one for you, but your machine +may not have one installed by default. For install instructions, consult the +documentation for your favorite MTA. Since you'll be sending the mail yourself, you are subject to things like SPF rules, blackholes, and MTA configuration which are beyond the scope of this @@ -258,6 +330,11 @@ configure. Mailer: SMTP ============ +| Media | Email +|---------| +| Inbound | Requires Configuration +|---------| + You can use this adapter to send mail via an external SMTP server, like Gmail. To use this mailer, set `type` to `smtp`, then configure these `options`: @@ -273,7 +350,15 @@ To use this mailer, set `type` to `smtp`, then configure these `options`: Disable Mail ============ -To disable mail, just don't configure any mailers. +| Media | All +|---------| +| Inbound | No +|---------| + + +To disable mail, just don't configure any mailers. (You can safely ignore the +setup warning reminding you to set up mailers if you don't plan to configure +any.) Testing and Debugging Outbound Email @@ -288,6 +373,9 @@ particular: Run `bin/mail help ` for more help on using these commands. +By default, `bin/mail send-test` sends email messages, but you can use +the `--type` flag to send different types of messages. + You can monitor daemons using the Daemon Console (`/daemon/`, or click **Daemon Console** from the homepage). From 2dd8a0fc6925fbc488c27b65af6a056431b6deeb Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 25 Jan 2019 08:04:06 -0800 Subject: [PATCH 36/42] Update documentation for MFA, including administrator guidance Summary: Depends on D20032. Ref T13222. Test Plan: Read documentation. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20033 --- .../user/userguide/multi_factor_auth.diviner | 132 +++++++++++++----- 1 file changed, 95 insertions(+), 37 deletions(-) diff --git a/src/docs/user/userguide/multi_factor_auth.diviner b/src/docs/user/userguide/multi_factor_auth.diviner index 44811e16f2..da174d519c 100644 --- a/src/docs/user/userguide/multi_factor_auth.diviner +++ b/src/docs/user/userguide/multi_factor_auth.diviner @@ -9,40 +9,39 @@ Overview Multi-factor authentication allows you to add additional credentials to your account to make it more secure. -This sounds complicated, but in most cases it just means that Phabricator will -make sure you have your mobile phone (by sending you a text message or having -you enter a code from a mobile application) before allowing you to log in or -take certain "high security" actions (like changing your password). +Once multi-factor authentication is configured on your account, you'll usually +use your mobile phone to provide an authorization code or an extra confirmation +when you try to log in to a new session or take certain actions (like changing +your password). Requiring you to prove you're really you by asking for something you know (your password) //and// something you have (your mobile phone) makes it much harder for attackers to access your account. The phone is an additional "factor" which protects your account from attacks. -Requiring re-authentication before performing high security actions further -limits the damage an attacker can do even if they manage to compromise a -login session. - How Multi-Factor Authentication Works ===================================== If you've configured multi-factor authentication and try to log in to your -account or take certain high security actions (like changing your password), +account or take certain sensitive actions (like changing your password), you'll be stopped and asked to enter additional credentials. -Usually, this means you'll receive an SMS with a security code on your phone, or -you'll open an app on your phone which will show you a security code. -In both cases, you'll enter the security code into Phabricator. +Usually, this means you'll receive an SMS with a authorization code on your +phone, or you'll open an app on your phone which will show you a authorization +code or ask you to confirm the action. If you're given a authorization code, +you'll enter it into Phabricator. If you're logging in, Phabricator will log you in after you enter the code. -If you're taking a high security action, Phabricator will put your account in -"high security" mode for a few minutes. In this mode, you can take high security -actions like changing passwords or SSH keys freely without entering any more -credentials. You can explicitly leave high security once you're done performing -account management, or your account will naturally return to normal security -after a short period of time. +If you're taking a sensitive action, Phabricator will sometimes put your +account in "high security" mode for a few minutes. In this mode, you can take +sensitive actions like changing passwords or SSH keys freely, without +entering any more credentials. + +You can explicitly leave high security once you're done performing account +management, or your account will naturally return to normal security after a +short period of time. While your account is in high security, you'll see a notification on screen with instructions for returning to normal security. @@ -52,8 +51,8 @@ Configuring Multi-Factor Authentication ======================================= To manage authentication factors for your account, go to -Settings > Multi-Factor Auth. You can use this control panel to add or remove -authentication factors from your account. +{nav Settings > Multi-Factor Auth}. You can use this control panel to add +or remove authentication factors from your account. You can also rename a factor by clicking the name. This can help you identify factors if you have several similar factors attached to your account. @@ -65,7 +64,7 @@ Factor: Mobile Phone App (TOTP) =============================== TOTP stands for "Time-based One-Time Password". This factor operates by having -you enter security codes from your mobile phone into Phabricator. The codes +you enter authorization codes from your mobile phone into Phabricator. The codes change every 30 seconds, so you will need to have your phone with you in order to enter them. @@ -79,23 +78,80 @@ application, so check any in-house documentation for details. In general, any TOTP application should work properly. After you've downloaded the application onto your phone, use the Phabricator -settings panel to add a factor to your account. You'll be prompted to enter a -master key into your phone, and then read a security code from your phone and -type it into Phabricator. +settings panel to add a factor to your account. You'll be prompted to scan a +QR code, and then read an authorization code from your phone and type it into +Phabricator. Later, when you need to authenticate, you'll follow this same process: launch -the application, read the security code, and type it into Phabricator. This will -prove you have your phone. +the application, read the authorization code, and type it into Phabricator. +This will prove you have your phone. Don't lose your phone! You'll need it to log into Phabricator in the future. -Recovering from Lost Factors -============================ +Factor: SMS +=========== -If you've lost a factor associated with your account (for example, your phone -has been lost or damaged), an administrator can strip the factor off your -account so that you can log in without it. +This factor operates by texting you a short authorization code when you try to +log in or perform a sensitive action. + +To use SMS, first add your phone number in {nav Settings > Contact Numbers}. +Once a primary contact number is configured on your account, you'll be able +to add an SMS factor. + +To enroll in SMS, you'll be sent a confirmation code to make sure your contact +number is correct and SMS is being delivered properly. Enter it when prompted. + +When you're asked to confirm your identity in the future, you'll be texted +an authorization code to enter into the prompt. + +(WARNING) SMS is a very weak factor and can be compromised or intercepted. For +details, see: . + + +Administration: Configuration +============================= + +New Phabricator installs start without any multi-factor providers enabled. +Users won't be able to add new factors until you set up multi-factor +authentication by configuring at least one provider. + +Configure new providers in {nav Auth > Multi-Factor}. + +Providers may be in these states: + + - **Active**: Users may add new factors. Users will be prompted to respond + to challenges from these providers when they take a sensitive action. + - **Deprecated**: Users may not add new factors, but they will still be + asked to respond to challenges from exising factors. + - **Disabled**: Users may not add new factors, and existing factors will + not be used. If MFA is required and a user only has disabled factors, + they will be forced to add a new factor. + +If you want to change factor types for your organization, the process will +normally look something like this: + + - Configure and test a new provider. + - Deprecate the old provider. + - Notify users that the old provider is deprecated and that they should move + to the new provider at their convenience, but before some upcoming + deadline. + - Once the deadline arrives, disable the old provider. + + +Administration: Requiring MFA +============================= + +As an administrator, you can require all users to add MFA to their accounts by +setting the `security.require-multi-factor-auth` option in Config. + + +Administration: Recovering from Lost Factors +============================================ + +If a user has lost a factor associated with their account (for example, their +phone has been lost or damaged), an administrator with host access can strip +the factor off their account so that they can log in without it. IMPORTANT: Before stripping factors from a user account, be absolutely certain that the user is who they claim to be! @@ -113,9 +169,10 @@ advance and require them to perform it. But no matter what you do, be certain the user (not an attacker //pretending// to be the user) is really the one making the request before stripping factors. -After verifying identity, administrators can strip authentication factors from -user accounts using the `bin/auth strip` command. For example, to strip all -factors from the account of a user who has lost their phone, run this command: +After verifying identity, administrators with host access can strip +authentication factors from user accounts using the `bin/auth strip` command. +For example, to strip all factors from the account of a user who has lost +their phone, run this command: ```lang=console # Strip all factors from a given user account. @@ -125,7 +182,7 @@ phabricator/ $ ./bin/auth strip --user --all-types You can run `bin/auth help strip` for more detail and all available flags and arguments. -This command can selectively strip types of factors. You can use +This command can selectively strip factors by factor type. You can use `bin/auth list-factors` to get a list of available factor types. ```lang=console @@ -133,8 +190,9 @@ This command can selectively strip types of factors. You can use phabricator/ $ ./bin/auth list-factors ``` -Once you've identified the factor types you want to strip, you can strip them -using the `--type` flag to specify one or more factor types: +Once you've identified the factor types you want to strip, you can strip +matching factors by using the `--type` flag to specify one or more factor +types: ```lang=console # Strip all SMS and TOTP factors for a user. From bce44385e1e358cc7065c63d798376c106e62753 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 25 Jan 2019 09:50:02 -0800 Subject: [PATCH 37/42] Add more factor details to the Settings factor list Summary: Depends on D20033. Ref T13222. Flesh this UI out a bit, and provide bit-strength information for TOTP. Also, stop users from adding multiple SMS factors since this is pointless (they all always text your primary contact number). Test Plan: {F6156245} {F6156246} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20034 --- .../auth/factor/PhabricatorAuthFactor.php | 19 +++++++++++++++++++ .../auth/factor/PhabricatorSMSAuthFactor.php | 18 ++++++++++++++++++ .../auth/factor/PhabricatorTOTPAuthFactor.php | 13 +++++++++++++ .../storage/PhabricatorAuthFactorProvider.php | 9 +++++++++ .../PhabricatorMultiFactorSettingsPanel.php | 10 ++++++++++ 5 files changed, 69 insertions(+) diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index 194f4acb59..4a4c6d4c3c 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -3,6 +3,7 @@ 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(); @@ -66,6 +67,13 @@ abstract class PhabricatorAuthFactor extends Phobject { return null; } + public function getConfigurationListDetails( + PhabricatorAuthFactorConfig $config, + PhabricatorAuthFactorProvider $provider, + PhabricatorUser $viewer) { + return null; + } + /** * Is this a factor which depends on the user's contact number? * @@ -524,4 +532,15 @@ abstract class PhabricatorAuthFactor extends Phobject { 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(); + } + } diff --git a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php index d91dceb475..58065a03c3 100644 --- a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php @@ -8,6 +8,10 @@ final class PhabricatorSMSAuthFactor } public function getFactorName() { + return pht('Text Message (SMS)'); + } + + public function getFactorShortName() { return pht('SMS'); } @@ -75,6 +79,10 @@ final class PhabricatorSMSAuthFactor return false; } + if ($this->loadConfigurationsForProvider($provider, $user)) { + return false; + } + return true; } @@ -96,6 +104,16 @@ final class PhabricatorSMSAuthFactor )); } + if ($this->loadConfigurationsForProvider($provider, $user)) { + $messages[] = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setErrors( + array( + pht( + 'You already have SMS authentication attached to your account.'), + )); + } + return $messages; } diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index 1401724125..f69840abdd 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -10,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 '. @@ -38,6 +42,15 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { '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, diff --git a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php index dfc948a423..50031d818d 100644 --- a/src/applications/auth/storage/PhabricatorAuthFactorProvider.php +++ b/src/applications/auth/storage/PhabricatorAuthFactorProvider.php @@ -126,6 +126,15 @@ final class PhabricatorAuthFactorProvider return $this->getFactor()->getConfigurationCreateDescription($this, $user); } + public function getConfigurationListDetails( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer) { + return $this->getFactor()->getConfigurationListDetails( + $config, + $this, + $viewer); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php index c355b6e14a..4da09dd324 100644 --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -77,6 +77,8 @@ final class PhabricatorMultiFactorSettingsPanel ->setIcon("{$status_icon} {$status_color}") ->setTooltip(pht('Provider: %s', $status->getName())); + $details = $provider->getConfigurationListDetails($factor, $viewer); + $rows[] = array( $icon, javelin_tag( @@ -86,7 +88,9 @@ final class PhabricatorMultiFactorSettingsPanel 'sigil' => 'workflow', ), $factor->getFactorName()), + $provider->getFactor()->getFactorShortName(), $provider->getDisplayName(), + $details, phabricator_datetime($factor->getDateCreated(), $viewer), javelin_tag( 'a', @@ -107,6 +111,8 @@ final class PhabricatorMultiFactorSettingsPanel null, pht('Name'), pht('Type'), + pht('Provider'), + pht('Details'), pht('Created'), null, )); @@ -115,6 +121,8 @@ final class PhabricatorMultiFactorSettingsPanel null, 'wide pri', null, + null, + null, 'right', 'action', )); @@ -125,6 +133,8 @@ final class PhabricatorMultiFactorSettingsPanel true, false, false, + false, + false, true, )); From 29b4fad94173ffc9fc4c94f7e3660d56b641d463 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 25 Jan 2019 10:02:29 -0800 Subject: [PATCH 38/42] Get rid of "throwResult()" for control flow in MFA factors Summary: Depends on D20034. Ref T13222. This is just cleanup -- I thought we'd have like two of these, but we ended up having a whole lot in Duo and a decent number in SMS. Just let factors return a result explicitly if they can make a decision early. I think using `instanceof` for control flow is a lesser evil than using `catch`, on the balance. Test Plan: `grep`, went through enroll/gate flows on SMS and Duo. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20035 --- src/__phutil_library_map__.php | 2 -- .../engine/PhabricatorAuthSessionEngine.php | 24 ++++++++++++------- .../PhabricatorAuthFactorResultException.php | 17 ------------- .../auth/factor/PhabricatorAuthFactor.php | 9 +++---- .../auth/factor/PhabricatorSMSAuthFactor.php | 12 +++------- 5 files changed, 24 insertions(+), 40 deletions(-) delete mode 100644 src/applications/auth/exception/PhabricatorAuthFactorResultException.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index fde9598e01..3573d30ce8 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2241,7 +2241,6 @@ phutil_register_library_map(array( 'PhabricatorAuthFactorProviderTransactionType' => 'applications/auth/xaction/PhabricatorAuthFactorProviderTransactionType.php', 'PhabricatorAuthFactorProviderViewController' => 'applications/auth/controller/mfa/PhabricatorAuthFactorProviderViewController.php', 'PhabricatorAuthFactorResult' => 'applications/auth/factor/PhabricatorAuthFactorResult.php', - 'PhabricatorAuthFactorResultException' => 'applications/auth/exception/PhabricatorAuthFactorResultException.php', 'PhabricatorAuthFactorTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthFactorTestCase.php', 'PhabricatorAuthFinishController' => 'applications/auth/controller/PhabricatorAuthFinishController.php', 'PhabricatorAuthHMACKey' => 'applications/auth/storage/PhabricatorAuthHMACKey.php', @@ -7970,7 +7969,6 @@ phutil_register_library_map(array( 'PhabricatorAuthFactorProviderTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorAuthFactorProviderViewController' => 'PhabricatorAuthFactorProviderController', 'PhabricatorAuthFactorResult' => 'Phobject', - 'PhabricatorAuthFactorResultException' => 'Exception', 'PhabricatorAuthFactorTestCase' => 'PhabricatorTestCase', 'PhabricatorAuthFinishController' => 'PhabricatorAuthController', 'PhabricatorAuthHMACKey' => 'PhabricatorAuthDAO', diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index 46c4a9c672..c052805224 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -540,14 +540,22 @@ final class PhabricatorAuthSessionEngine extends Phobject { $provider = $factor->getFactorProvider(); $impl = $provider->getFactor(); - try { - $new_challenges = $impl->getNewIssuedChallenges( - $factor, - $viewer, - $issued_challenges); - } catch (PhabricatorAuthFactorResultException $ex) { - $ok = false; - $validation_results[$factor_phid] = $ex->getResult(); + $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; } diff --git a/src/applications/auth/exception/PhabricatorAuthFactorResultException.php b/src/applications/auth/exception/PhabricatorAuthFactorResultException.php deleted file mode 100644 index 61595266e5..0000000000 --- a/src/applications/auth/exception/PhabricatorAuthFactorResultException.php +++ /dev/null @@ -1,17 +0,0 @@ -result = $result; - parent::__construct(); - } - - public function getResult() { - return $this->result; - } - -} diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index 4a4c6d4c3c..5f7adec997 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -141,6 +141,11 @@ abstract class PhabricatorAuthFactor extends Phobject { $viewer, $challenges); + if ($new_challenges instanceof PhabricatorAuthFactorResult) { + unset($unguarded); + return $new_challenges; + } + assert_instances_of($new_challenges, 'PhabricatorAuthChallenge'); foreach ($new_challenges as $new_challenge) { @@ -493,10 +498,6 @@ abstract class PhabricatorAuthFactor extends Phobject { $rows); } - final protected function throwResult(PhabricatorAuthFactorResult $result) { - throw new PhabricatorAuthFactorResultException($result); - } - final protected function getInstallDisplayName() { $uri = PhabricatorEnv::getURI('/'); $uri = new PhutilURI($uri); diff --git a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php index 58065a03c3..9d8ea592fe 100644 --- a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php @@ -195,35 +195,29 @@ final class PhabricatorSMSAuthFactor } if (!$this->loadUserContactNumber($viewer)) { - $result = $this->newResult() + return $this->newResult() ->setIsError(true) ->setErrorMessage( pht( 'Your account has no primary contact number.')); - - $this->throwResult($result); } if (!$this->isSMSMailerConfigured()) { - $result = $this->newResult() + return $this->newResult() ->setIsError(true) ->setErrorMessage( pht( 'No outbound mailer which can deliver SMS messages is '. 'configured.')); - - $this->throwResult($result); } if (!$this->hasCSRF($config)) { - $result = $this->newResult() + return $this->newResult() ->setIsContinue(true) ->setErrorMessage( pht( 'A text message with an authorization code will be sent to your '. 'primary contact number.')); - - $this->throwResult($result); } // Otherwise, issue a new challenge. From d24e66724d3bbfef0b0bf331c2a613d1c462a57b Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 25 Jan 2019 10:16:49 -0800 Subject: [PATCH 39/42] Convert "Rename User" from session MFA to one-shot MFA Summary: Depends on D20035. Ref T13222. - Allow individual transactions to request one-shot MFA if available. - Make "change username" request MFA. Test Plan: - Renamed a user, got prompted for MFA, provided it. - Saw that I no longer remain in high-security after performing the edit. - Grepped for other uses of `PhabricatorUserUsernameTransaction`, found none. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20036 --- .../PhabricatorPeopleRenameController.php | 8 +--- .../PhabricatorUserUsernameTransaction.php | 7 +++ ...habricatorApplicationTransactionEditor.php | 48 +++++++++++++------ .../PhabricatorModularTransactionType.php | 6 +++ 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/applications/people/controller/PhabricatorPeopleRenameController.php b/src/applications/people/controller/PhabricatorPeopleRenameController.php index 42ff2e7988..42eebfc8ae 100644 --- a/src/applications/people/controller/PhabricatorPeopleRenameController.php +++ b/src/applications/people/controller/PhabricatorPeopleRenameController.php @@ -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 { diff --git a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php index db134a5c78..b6d23b3511 100644 --- a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php @@ -89,4 +89,11 @@ final class PhabricatorUserUsernameTransaction return null; } + + public function shouldTryMFA( + $object, + PhabricatorApplicationTransaction $xaction) { + return true; + } + } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index fda45fd3d5..c6458b0631 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -4906,20 +4906,47 @@ abstract class PhabricatorApplicationTransactionEditor PhabricatorLiskDAO $object, array $xactions) { - $is_mfa = ($object instanceof PhabricatorEditEngineMFAInterface); - if (!$is_mfa) { + $has_engine = ($object instanceof PhabricatorEditEngineMFAInterface); + if ($has_engine) { + $engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object) + ->setViewer($this->getActor()); + $require_mfa = $engine->shouldRequireMFA(); + $try_mfa = $engine->shouldTryMFA(); + } else { + $require_mfa = false; + $try_mfa = false; + } + + // If the user is mentioning an MFA object on another object or creating + // a relationship like "parent" or "child" to this object, we always + // allow the edit to move forward without requiring MFA. + if ($this->getIsInverseEdgeEditor()) { return $xactions; } - $engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object) - ->setViewer($this->getActor()); - $require_mfa = $engine->shouldRequireMFA(); - if (!$require_mfa) { - $try_mfa = $engine->shouldTryMFA(); + // If the object hasn't already opted into MFA, see if any of the + // transactions want it. + if (!$try_mfa) { + foreach ($xactions as $xaction) { + $type = $xaction->getTransactionType(); + + $xtype = $this->getModularTransactionType($type); + if ($xtype) { + $xtype = clone $xtype; + $xtype->setStorage($xaction); + if ($xtype->shouldTryMFA($object, $xaction)) { + $try_mfa = true; + break; + } + } + } + } + if ($try_mfa) { $this->setShouldRequireMFA(true); } + return $xactions; } @@ -4937,13 +4964,6 @@ abstract class PhabricatorApplicationTransactionEditor return $xactions; } - // If the user is mentioning an MFA object on another object or creating - // a relationship like "parent" or "child" to this object, we allow the - // edit to move forward without requiring MFA. - if ($this->getIsInverseEdgeEditor()) { - return $xactions; - } - $template = $object->getApplicationTransactionTemplate(); $mfa_xaction = id(clone $template) diff --git a/src/applications/transactions/storage/PhabricatorModularTransactionType.php b/src/applications/transactions/storage/PhabricatorModularTransactionType.php index 3d2efe0501..abe7a31025 100644 --- a/src/applications/transactions/storage/PhabricatorModularTransactionType.php +++ b/src/applications/transactions/storage/PhabricatorModularTransactionType.php @@ -425,4 +425,10 @@ abstract class PhabricatorModularTransactionType return PhabricatorPolicyCapability::CAN_EDIT; } + public function shouldTryMFA( + $object, + PhabricatorApplicationTransaction $xaction) { + return false; + } + } From 44a0b3e83d9031729250c7bf1ae13bb828d6217b Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 25 Jan 2019 10:21:03 -0800 Subject: [PATCH 40/42] Replace "Show Secret" in Passphrase with one-shot MFA Summary: Depends on D20036. Ref T13222. Now that we support one-shot MFA, swap this from session MFA to one-shot MFA. Test Plan: Revealed a credential, was no longer left in high-security mode. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222 Differential Revision: https://secure.phabricator.com/D20037 --- .../controller/PassphraseCredentialRevealController.php | 9 +++------ .../passphrase/storage/PassphraseCredential.php | 4 ++++ .../xaction/PassphraseCredentialLookedAtTransaction.php | 6 ++++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php index 3a40d253c9..99b6711ae6 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php @@ -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); diff --git a/src/applications/passphrase/storage/PassphraseCredential.php b/src/applications/passphrase/storage/PassphraseCredential.php index b10d392d36..c470ea661f 100644 --- a/src/applications/passphrase/storage/PassphraseCredential.php +++ b/src/applications/passphrase/storage/PassphraseCredential.php @@ -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, diff --git a/src/applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php b/src/applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php index 3d8cb36f31..fc76ab0d56 100644 --- a/src/applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php +++ b/src/applications/passphrase/xaction/PassphraseCredentialLookedAtTransaction.php @@ -30,4 +30,10 @@ final class PassphraseCredentialLookedAtTransaction return 'blue'; } + public function shouldTryMFA( + $object, + PhabricatorApplicationTransaction $xaction) { + return true; + } + } From d8d4efe89e85a31850164bfa7bd234d9d230c517 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 25 Jan 2019 10:42:31 -0800 Subject: [PATCH 41/42] Require MFA to edit MFA providers Summary: Depends on D20037. Ref T13222. Ref T7667. Although administrators can now disable MFA from the web UI, at least require that they survive MFA gates to do so. T7667 (`bin/auth lock`) should provide a sturdier approach here in the long term. Test Plan: Created and edited MFA providers, was prompted for MFA. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13222, T7667 Differential Revision: https://secure.phabricator.com/D20038 --- src/__phutil_library_map__.php | 3 +++ .../engine/PhabricatorAuthFactorProviderMFAEngine.php | 10 ++++++++++ .../auth/storage/PhabricatorAuthFactorProvider.php | 10 +++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 src/applications/auth/engine/PhabricatorAuthFactorProviderMFAEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3573d30ce8..43553125f2 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2232,6 +2232,7 @@ phutil_register_library_map(array( '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', @@ -7954,12 +7955,14 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorExtendedPolicyInterface', + 'PhabricatorEditEngineMFAInterface', ), 'PhabricatorAuthFactorProviderController' => 'PhabricatorAuthProviderController', 'PhabricatorAuthFactorProviderEditController' => 'PhabricatorAuthFactorProviderController', 'PhabricatorAuthFactorProviderEditEngine' => 'PhabricatorEditEngine', 'PhabricatorAuthFactorProviderEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorAuthFactorProviderListController' => 'PhabricatorAuthProviderController', + 'PhabricatorAuthFactorProviderMFAEngine' => 'PhabricatorEditEngineMFAEngine', 'PhabricatorAuthFactorProviderNameTransaction' => 'PhabricatorAuthFactorProviderTransactionType', 'PhabricatorAuthFactorProviderQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthFactorProviderStatus' => 'Phobject', diff --git a/src/applications/auth/engine/PhabricatorAuthFactorProviderMFAEngine.php b/src/applications/auth/engine/PhabricatorAuthFactorProviderMFAEngine.php new file mode 100644 index 0000000000..39f80bb5b8 --- /dev/null +++ b/src/applications/auth/engine/PhabricatorAuthFactorProviderMFAEngine.php @@ -0,0 +1,10 @@ + Date: Fri, 25 Jan 2019 11:21:03 -0800 Subject: [PATCH 42/42] Bring Duo MFA upstream Summary: Depends on D20038. Ref T13231. Although I planned to keep this out of the upstream (see T13229) it ended up having enough pieces that I imagine it may need more fixes/updates than we can reasonably manage by copy/pasting stuff around. Until T5055, we don't really have good tools for managing this. Make my life easier by just upstreaming this. Test Plan: See T13231 for a bunch of workflow discussion. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13231 Differential Revision: https://secure.phabricator.com/D20039 --- src/__phutil_library_map__.php | 12 + ...habricatorAuthFactorProviderEditEngine.php | 12 +- .../auth/factor/PhabricatorAuthFactor.php | 15 +- .../auth/factor/PhabricatorDuoAuthFactor.php | 802 ++++++++++++++++++ .../auth/factor/PhabricatorSMSAuthFactor.php | 7 +- .../auth/factor/PhabricatorTOTPAuthFactor.php | 5 +- ...FactorProviderDuoCredentialTransaction.php | 65 ++ ...AuthFactorProviderDuoEnrollTransaction.php | 26 + ...thFactorProviderDuoHostnameTransaction.php | 59 ++ ...hFactorProviderDuoUsernamesTransaction.php | 26 + .../PhabricatorCredentialEditField.php | 43 + .../editfield/PhabricatorSpaceEditField.php | 1 - .../user/userguide/multi_factor_auth.diviner | 11 + 13 files changed, 1076 insertions(+), 8 deletions(-) create mode 100644 src/applications/auth/factor/PhabricatorDuoAuthFactor.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoCredentialTransaction.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoEnrollTransaction.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoHostnameTransaction.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoUsernamesTransaction.php create mode 100644 src/applications/transactions/editfield/PhabricatorCredentialEditField.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 43553125f2..c5723894d5 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2228,6 +2228,10 @@ phutil_register_library_map(array( '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', @@ -2800,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', @@ -2986,6 +2991,7 @@ 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', @@ -7958,6 +7964,10 @@ phutil_register_library_map(array( 'PhabricatorEditEngineMFAInterface', ), 'PhabricatorAuthFactorProviderController' => 'PhabricatorAuthProviderController', + 'PhabricatorAuthFactorProviderDuoCredentialTransaction' => 'PhabricatorAuthFactorProviderTransactionType', + 'PhabricatorAuthFactorProviderDuoEnrollTransaction' => 'PhabricatorAuthFactorProviderTransactionType', + 'PhabricatorAuthFactorProviderDuoHostnameTransaction' => 'PhabricatorAuthFactorProviderTransactionType', + 'PhabricatorAuthFactorProviderDuoUsernamesTransaction' => 'PhabricatorAuthFactorProviderTransactionType', 'PhabricatorAuthFactorProviderEditController' => 'PhabricatorAuthFactorProviderController', 'PhabricatorAuthFactorProviderEditEngine' => 'PhabricatorEditEngine', 'PhabricatorAuthFactorProviderEditor' => 'PhabricatorApplicationTransactionEditor', @@ -8633,6 +8643,7 @@ phutil_register_library_map(array( 'PhabricatorCountdownTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorCountdownView' => 'AphrontView', 'PhabricatorCountdownViewController' => 'PhabricatorCountdownController', + 'PhabricatorCredentialEditField' => 'PhabricatorEditField', 'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery', 'PhabricatorCustomField' => 'Phobject', 'PhabricatorCustomFieldApplicationSearchAnyFunctionDatasource' => 'PhabricatorTypeaheadDatasource', @@ -8837,6 +8848,7 @@ phutil_register_library_map(array( 'PhabricatorDraftDAO' => 'PhabricatorLiskDAO', 'PhabricatorDraftEngine' => 'Phobject', 'PhabricatorDrydockApplication' => 'PhabricatorApplication', + 'PhabricatorDuoAuthFactor' => 'PhabricatorAuthFactor', 'PhabricatorDuoFuture' => 'FutureProxy', 'PhabricatorEdgeChangeRecord' => 'Phobject', 'PhabricatorEdgeChangeRecordTestCase' => 'PhabricatorTestCase', diff --git a/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php index c4e9a1dea5..ab74350cc9 100644 --- a/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php +++ b/src/applications/auth/editor/PhabricatorAuthFactorProviderEditEngine.php @@ -93,11 +93,12 @@ final class PhabricatorAuthFactorProviderEditEngine } protected function buildCustomEditFields($object) { - $factor_name = $object->getFactor()->getFactorName(); + $factor = $object->getFactor(); + $factor_name = $factor->getFactorName(); $status_map = PhabricatorAuthFactorProviderStatus::getMap(); - return array( + $fields = array( id(new PhabricatorStaticEditField()) ->setKey('displayType') ->setLabel(pht('Factor Type')) @@ -120,6 +121,13 @@ final class PhabricatorAuthFactorProviderEditEngine ->setValue($object->getStatus()) ->setOptions($status_map), ); + + $factor_fields = $factor->newEditEngineFields($this, $object); + foreach ($factor_fields as $field) { + $fields[] = $field; + } + + return $fields; } } diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index 5f7adec997..345ace3df9 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -74,6 +74,12 @@ abstract class PhabricatorAuthFactor extends Phobject { return null; } + public function newEditEngineFields( + PhabricatorEditEngine $engine, + PhabricatorAuthFactorProvider $provider) { + return array(); + } + /** * Is this a factor which depends on the user's contact number? * @@ -331,6 +337,7 @@ abstract class PhabricatorAuthFactor extends Phobject { final protected function loadMFASyncToken( + PhabricatorAuthFactorProvider $provider, AphrontRequest $request, AphrontFormView $form, PhabricatorUser $user) { @@ -397,7 +404,9 @@ abstract class PhabricatorAuthFactor extends Phobject { ->setTokenCode($sync_key_digest) ->setTokenExpires($now + $sync_ttl); - $properties = $this->newMFASyncTokenProperties($user); + $properties = $this->newMFASyncTokenProperties( + $provider, + $user); foreach ($properties as $key => $value) { $sync_token->setTemporaryTokenProperty($key, $value); @@ -411,7 +420,9 @@ abstract class PhabricatorAuthFactor extends Phobject { return $sync_token; } - protected function newMFASyncTokenProperties(PhabricatorUser $user) { + protected function newMFASyncTokenProperties( + PhabricatorAuthFactorProvider $provider, + PhabricatorUser $user) { return array(); } diff --git a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php new file mode 100644 index 0000000000..f5e2455c79 --- /dev/null +++ b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php @@ -0,0 +1,802 @@ +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)); + } + +} diff --git a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php index 9d8ea592fe..ba46de980e 100644 --- a/src/applications/auth/factor/PhabricatorSMSAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorSMSAuthFactor.php @@ -140,7 +140,7 @@ final class PhabricatorSMSAuthFactor AphrontRequest $request, PhabricatorUser $user) { - $token = $this->loadMFASyncToken($request, $form, $user); + $token = $this->loadMFASyncToken($provider, $request, $form, $user); $code = $request->getStr('sms.code'); $e_code = true; @@ -364,7 +364,10 @@ final class PhabricatorSMSAuthFactor return head($contact_numbers); } - protected function newMFASyncTokenProperties(PhabricatorUser $user) { + protected function newMFASyncTokenProperties( + PhabricatorAuthFactorProvider $providerr, + PhabricatorUser $user) { + $sms_code = $this->newSMSChallengeCode(); $envelope = new PhutilOpaqueEnvelope($sms_code); diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index f69840abdd..ba6613c014 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -58,6 +58,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { PhabricatorUser $user) { $sync_token = $this->loadMFASyncToken( + $provider, $request, $form, $user); @@ -440,7 +441,9 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { return null; } - protected function newMFASyncTokenProperties(PhabricatorUser $user) { + protected function newMFASyncTokenProperties( + PhabricatorAuthFactorProvider $providerr, + PhabricatorUser $user) { return array( 'secret' => self::generateNewTOTPKey(), ); diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoCredentialTransaction.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoCredentialTransaction.php new file mode 100644 index 0000000000..532fc271f4 --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoCredentialTransaction.php @@ -0,0 +1,65 @@ +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; + } + +} diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoEnrollTransaction.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoEnrollTransaction.php new file mode 100644 index 0000000000..e1823274b8 --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoEnrollTransaction.php @@ -0,0 +1,26 @@ +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()); + } + +} diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoHostnameTransaction.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoHostnameTransaction.php new file mode 100644 index 0000000000..ce1838594e --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoHostnameTransaction.php @@ -0,0 +1,59 @@ +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; + } + +} diff --git a/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoUsernamesTransaction.php b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoUsernamesTransaction.php new file mode 100644 index 0000000000..8d9be1244f --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthFactorProviderDuoUsernamesTransaction.php @@ -0,0 +1,26 @@ +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()); + } + +} diff --git a/src/applications/transactions/editfield/PhabricatorCredentialEditField.php b/src/applications/transactions/editfield/PhabricatorCredentialEditField.php new file mode 100644 index 0000000000..7c70bf288e --- /dev/null +++ b/src/applications/transactions/editfield/PhabricatorCredentialEditField.php @@ -0,0 +1,43 @@ +credentialType = $credential_type; + return $this; + } + + public function getCredentialType() { + return $this->credentialType; + } + + public function setCredentials(array $credentials) { + $this->credentials = $credentials; + return $this; + } + + public function getCredentials() { + return $this->credentials; + } + + protected function newControl() { + $control = id(new PassphraseCredentialControl()) + ->setCredentialType($this->getCredentialType()) + ->setOptions($this->getCredentials()); + + return $control; + } + + protected function newHTTPParameterType() { + return new AphrontPHIDHTTPParameterType(); + } + + protected function newConduitParameterType() { + return new ConduitPHIDParameterType(); + } + +} diff --git a/src/applications/transactions/editfield/PhabricatorSpaceEditField.php b/src/applications/transactions/editfield/PhabricatorSpaceEditField.php index ee15f0b19e..c15213bd93 100644 --- a/src/applications/transactions/editfield/PhabricatorSpaceEditField.php +++ b/src/applications/transactions/editfield/PhabricatorSpaceEditField.php @@ -28,7 +28,6 @@ final class PhabricatorSpaceEditField return new ConduitPHIDParameterType(); } - public function shouldReadValueFromRequest() { return $this->getPolicyField()->shouldReadValueFromRequest(); } diff --git a/src/docs/user/userguide/multi_factor_auth.diviner b/src/docs/user/userguide/multi_factor_auth.diviner index da174d519c..eca85d0f92 100644 --- a/src/docs/user/userguide/multi_factor_auth.diviner +++ b/src/docs/user/userguide/multi_factor_auth.diviner @@ -109,6 +109,17 @@ an authorization code to enter into the prompt. details, see: . +Factor: Duo +=========== + +This factor supports integration with [[ https://duo.com/ | Duo Security ]], a +third-party authentication service popular with enterprises that have a lot of +policies to enforce. + +To use Duo, you'll install the Duo application on your phone. When you try +to take a sensitive action, you'll be asked to confirm it in the application. + + Administration: Configuration =============================