1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-26 08:42:41 +01:00

Move all account link / unlink to new registration flow

Summary:
Ref T1536. Currently, we have separate panels for each link/unlink and separate controllers for OAuth vs LDAP.

Instead, provide a single "External Accounts" panel which shows all linked accounts and allows you to link/unlink more easily.

Move link/unlink over to a full externalaccount-based workflow.

Test Plan:
  - Linked and unlinked OAuth accounts.
  - Linked and unlinked LDAP accounts.
  - Registered new accounts.
  - Exercised most/all of the error cases.

Reviewers: btrahan, chad

Reviewed By: btrahan

CC: aran, mbishopim3

Maniphest Tasks: T1536

Differential Revision: https://secure.phabricator.com/D6189
This commit is contained in:
epriestley 2013-06-17 06:12:45 -07:00
parent 61a0c6d6e3
commit b040f889de
24 changed files with 691 additions and 530 deletions

View file

@ -923,7 +923,7 @@ celerity_register_resource_map(array(
),
'auth-css' =>
array(
'uri' => '/res/8a95bad7/rsrc/css/application/auth/auth.css',
'uri' => '/res/81f72bfa/rsrc/css/application/auth/auth.css',
'type' => 'css',
'requires' =>
array(

View file

@ -814,7 +814,9 @@ phutil_register_library_map(array(
'PhabricatorAuditQuery' => 'applications/audit/query/PhabricatorAuditQuery.php',
'PhabricatorAuditReplyHandler' => 'applications/audit/mail/PhabricatorAuditReplyHandler.php',
'PhabricatorAuditStatusConstants' => 'applications/audit/constants/PhabricatorAuditStatusConstants.php',
'PhabricatorAuthConfirmLinkController' => 'applications/auth/controller/PhabricatorAuthConfirmLinkController.php',
'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php',
'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php',
'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php',
'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php',
'PhabricatorAuthProviderLDAP' => 'applications/auth/provider/PhabricatorAuthProviderLDAP.php',
@ -826,6 +828,7 @@ phutil_register_library_map(array(
'PhabricatorAuthProviderPassword' => 'applications/auth/provider/PhabricatorAuthProviderPassword.php',
'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php',
'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php',
'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php',
'PhabricatorAuthValidateController' => 'applications/auth/controller/PhabricatorAuthValidateController.php',
'PhabricatorAuthenticationConfigOptions' => 'applications/config/option/PhabricatorAuthenticationConfigOptions.php',
'PhabricatorBarePageExample' => 'applications/uiexample/examples/PhabricatorBarePageExample.php',
@ -1111,7 +1114,6 @@ phutil_register_library_map(array(
'PhabricatorLDAPProvider' => 'applications/auth/ldap/PhabricatorLDAPProvider.php',
'PhabricatorLDAPRegistrationController' => 'applications/auth/controller/PhabricatorLDAPRegistrationController.php',
'PhabricatorLDAPUnknownUserException' => 'applications/auth/ldap/PhabricatorLDAPUnknownUserException.php',
'PhabricatorLDAPUnlinkController' => 'applications/auth/controller/PhabricatorLDAPUnlinkController.php',
'PhabricatorLintEngine' => 'infrastructure/lint/PhabricatorLintEngine.php',
'PhabricatorLipsumArtist' => 'applications/lipsum/image/PhabricatorLipsumArtist.php',
'PhabricatorLipsumGenerateWorkflow' => 'applications/lipsum/management/PhabricatorLipsumGenerateWorkflow.php',
@ -1241,7 +1243,6 @@ phutil_register_library_map(array(
'PhabricatorOAuthServerTestCase' => 'applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php',
'PhabricatorOAuthServerTestController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTestController.php',
'PhabricatorOAuthServerTokenController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php',
'PhabricatorOAuthUnlinkController' => 'applications/auth/controller/PhabricatorOAuthUnlinkController.php',
'PhabricatorObjectHandle' => 'applications/phid/PhabricatorObjectHandle.php',
'PhabricatorObjectHandleConstants' => 'applications/phid/handle/const/PhabricatorObjectHandleConstants.php',
'PhabricatorObjectHandleData' => 'applications/phid/handle/PhabricatorObjectHandleData.php',
@ -1444,9 +1445,8 @@ phutil_register_library_map(array(
'PhabricatorSettingsPanelDisplayPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php',
'PhabricatorSettingsPanelEmailAddresses' => 'applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php',
'PhabricatorSettingsPanelEmailPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php',
'PhabricatorSettingsPanelExternalAccounts' => 'applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php',
'PhabricatorSettingsPanelHomePreferences' => 'applications/settings/panel/PhabricatorSettingsPanelHomePreferences.php',
'PhabricatorSettingsPanelLDAP' => 'applications/settings/panel/PhabricatorSettingsPanelLDAP.php',
'PhabricatorSettingsPanelOAuth' => 'applications/settings/panel/PhabricatorSettingsPanelOAuth.php',
'PhabricatorSettingsPanelPassword' => 'applications/settings/panel/PhabricatorSettingsPanelPassword.php',
'PhabricatorSettingsPanelProfile' => 'applications/settings/panel/PhabricatorSettingsPanelProfile.php',
'PhabricatorSettingsPanelSSHKeys' => 'applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php',
@ -2678,7 +2678,9 @@ phutil_register_library_map(array(
'PhabricatorAuditMailReceiver' => 'PhabricatorObjectMailReceiver',
'PhabricatorAuditPreviewController' => 'PhabricatorAuditController',
'PhabricatorAuditReplyHandler' => 'PhabricatorMailReplyHandler',
'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController',
'PhabricatorAuthController' => 'PhabricatorController',
'PhabricatorAuthLinkController' => 'PhabricatorAuthController',
'PhabricatorAuthLoginController' => 'PhabricatorAuthController',
'PhabricatorAuthProviderLDAP' => 'PhabricatorAuthProvider',
'PhabricatorAuthProviderOAuth' => 'PhabricatorAuthProvider',
@ -2689,6 +2691,7 @@ phutil_register_library_map(array(
'PhabricatorAuthProviderPassword' => 'PhabricatorAuthProvider',
'PhabricatorAuthRegisterController' => 'PhabricatorAuthController',
'PhabricatorAuthStartController' => 'PhabricatorAuthController',
'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController',
'PhabricatorAuthValidateController' => 'PhabricatorAuthController',
'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorBarePageExample' => 'PhabricatorUIExample',
@ -2971,7 +2974,6 @@ phutil_register_library_map(array(
'PhabricatorLDAPLoginController' => 'PhabricatorAuthController',
'PhabricatorLDAPRegistrationController' => 'PhabricatorAuthController',
'PhabricatorLDAPUnknownUserException' => 'Exception',
'PhabricatorLDAPUnlinkController' => 'PhabricatorAuthController',
'PhabricatorLintEngine' => 'PhutilLintEngine',
'PhabricatorLipsumGenerateWorkflow' => 'PhabricatorLipsumManagementWorkflow',
'PhabricatorLipsumManagementWorkflow' => 'PhutilArgumentWorkflow',
@ -3093,7 +3095,6 @@ phutil_register_library_map(array(
'PhabricatorOAuthServerTestCase' => 'PhabricatorTestCase',
'PhabricatorOAuthServerTestController' => 'PhabricatorOAuthServerController',
'PhabricatorOAuthServerTokenController' => 'PhabricatorAuthController',
'PhabricatorOAuthUnlinkController' => 'PhabricatorAuthController',
'PhabricatorObjectHandleStatus' => 'PhabricatorObjectHandleConstants',
'PhabricatorObjectItemListExample' => 'PhabricatorUIExample',
'PhabricatorObjectItemListView' => 'AphrontTagView',
@ -3302,9 +3303,8 @@ phutil_register_library_map(array(
'PhabricatorSettingsPanelDisplayPreferences' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelEmailAddresses' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelEmailPreferences' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelExternalAccounts' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelHomePreferences' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelLDAP' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelOAuth' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelPassword' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelProfile' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelSSHKeys' => 'PhabricatorSettingsPanel',

View file

@ -40,6 +40,10 @@ final class PhabricatorApplicationAuth extends PhabricatorApplication {
'register/(?:(?P<akey>[^/]+)/)?' => 'PhabricatorAuthRegisterController',
'start/' => 'PhabricatorAuthStartController',
'validate/' => 'PhabricatorAuthValidateController',
'unlink/(?P<pkey>[^/]+)/' => 'PhabricatorAuthUnlinkController',
'link/(?P<pkey>[^/]+)/' => 'PhabricatorAuthLinkController',
'confirmlink/(?P<akey>[^/]+)/'
=> 'PhabricatorAuthConfirmLinkController',
),
'/login/' => array(
@ -56,13 +60,11 @@ final class PhabricatorApplicationAuth extends PhabricatorApplication {
'(?P<provider>\w+)/' => array(
'login/' => 'PhabricatorOAuthLoginController',
'diagnose/' => 'PhabricatorOAuthDiagnosticsController',
'unlink/' => 'PhabricatorOAuthUnlinkController',
),
),
'/ldap/' => array(
'login/' => 'PhabricatorLDAPLoginController',
'unlink/' => 'PhabricatorLDAPUnlinkController',
),
);
}

View file

@ -0,0 +1,89 @@
<?php
final class PhabricatorAuthConfirmLinkController
extends PhabricatorAuthController {
private $accountKey;
public function willProcessRequest(array $data) {
$this->accountKey = idx($data, 'akey');
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$result = $this->loadAccountForRegistrationOrLinking($this->accountKey);
list($account, $provider, $response) = $result;
if ($response) {
return $response;
}
if (!$provider->shouldAllowAccountLink()) {
return $this->renderError(pht('This account is not linkable.'));
}
$panel_uri = '/settings/panel/external/';
if ($request->isFormPost()) {
$account->setUserPHID($viewer->getPHID());
$account->save();
$this->clearRegistrationCookies();
// TODO: Send the user email about the new account link.
return id(new AphrontRedirectResponse())->setURI($panel_uri);
}
// TODO: Provide more information about the external account. Clicking
// through this form blindly is dangerous.
// TODO: If the user has password authentication, require them to retype
// their password here.
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->setTitle(pht('Confirm %s Account Link', $provider->getProviderName()))
->addCancelButton($panel_uri)
->addSubmitButton(pht('Confirm Account Link'));
$form = id(new AphrontFormLayoutView())
->setFullWidth(true)
->appendChild(
phutil_tag(
'div',
array(
'class' => 'aphront-form-instructions',
),
pht(
"Confirm the link with this %s account. This account will be ".
"able to log in to your Phabricator account.",
$provider->getProviderName())));
$dialog->appendChild($form);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName(pht('Confirm Link'))
->setHref($panel_uri));
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName($provider->getProviderName()));
return $this->buildApplicationPage(
array(
$crumbs,
$dialog,
),
array(
'title' => pht('Confirm External Account Link'),
'dust' => true,
'device' => true,
));
}
}

View file

@ -29,6 +29,7 @@ abstract class PhabricatorAuthController extends PhabricatorController {
}
/**
* Log a user into a web session and return an @{class:AphrontResponse} which
* corresponds to continuing the login process.
@ -99,4 +100,108 @@ abstract class PhabricatorAuthController extends PhabricatorController {
));
}
protected function loadAccountForRegistrationOrLinking($account_key) {
$request = $this->getRequest();
$viewer = $request->getUser();
$account = null;
$provider = null;
$response = null;
if (!$account_key) {
$response = $this->renderError(
pht('Request did not include account key.'));
return array($account, $provider, $response);
}
$account = id(new PhabricatorExternalAccount())->loadOneWhere(
'accountSecret = %s',
$account_key);
if (!$account) {
$response = $this->renderError(pht('No valid linkable account.'));
return array($account, $provider, $response);
}
if ($account->getUserPHID()) {
if ($account->getUserPHID() != $viewer->getUserPHID()) {
$response = $this->renderError(
pht(
'The account you are attempting to register or link is already '.
'linked to another user.'));
} else {
$response = $this->renderError(
pht(
'The account you are attempting to link is already linked '.
'to your account.'));
}
return array($account, $provider, $response);
}
$registration_key = $request->getCookie('phreg');
// NOTE: This registration key check is not strictly necessary, because
// we're only creating new accounts, not linking existing accounts. It
// might be more hassle than it is worth, especially for email.
//
// The attack this prevents is getting to the registration screen, then
// copy/pasting the URL and getting someone else to click it and complete
// the process. They end up with an account bound to credentials you
// control. This doesn't really let you do anything meaningful, though,
// since you could have simply completed the process yourself.
if (!$registration_key) {
$response = $this->renderError(
pht(
'Your browser did not submit a registration key with the request. '.
'You must use the same browser to begin and complete registration. '.
'Check that cookies are enabled and try again.'));
return array($account, $provider, $response);
}
// We store the digest of the key rather than the key itself to prevent a
// theoretical attacker with read-only access to the database from
// hijacking registration sessions.
$actual = $account->getProperty('registrationKey');
$expect = PhabricatorHash::digest($registration_key);
if ($actual !== $expect) {
$response = $this->renderError(
pht(
'Your browser submitted a different registration key than the one '.
'associated with this account. You may need to clear your cookies.'));
return array($account, $provider, $response);
}
$other_account = id(new PhabricatorExternalAccount())->loadAllWhere(
'accountType = %s AND accountDomain = %s AND accountID = %s
AND id != %d',
$account->getAccountType(),
$account->getAccountDomain(),
$account->getAccountID(),
$account->getID());
if ($other_account) {
$response = $this->renderError(
pht(
'The account you are attempting to register with already belongs '.
'to another user.'));
return array($account, $provider, $response);
}
$provider = PhabricatorAuthProvider::getEnabledProviderByKey(
$account->getProviderKey());
if (!$provider) {
$response = $this->renderError(
pht(
'The account you are attempting to register with uses a nonexistent '.
'or disabled authentication provider (with key "%s"). An '.
'administrator may have recently disabled this provider.',
$account->getProviderKey()));
return array($account, $provider, $response);
}
return array($account, $provider, null);
}
}

View file

@ -0,0 +1,81 @@
<?php
final class PhabricatorAuthLinkController
extends PhabricatorAuthController {
private $providerKey;
public function willProcessRequest(array $data) {
$this->providerKey = $data['pkey'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$provider = PhabricatorAuthProvider::getEnabledProviderByKey(
$this->providerKey);
if (!$provider) {
return new Aphront404Response();
}
if (!$provider->shouldAllowAccountLink()) {
return $this->renderErrorPage(
pht('Account Not Linkable'),
array(
pht('This provider is not configured to allow linking.'),
));
}
$account = id(new PhabricatorExternalAccount())->loadOneWhere(
'accountType = %s AND accountDomain = %s AND userPHID = %s',
$provider->getProviderType(),
$provider->getProviderDomain(),
$viewer->getPHID());
if ($account) {
return $this->renderErrorPage(
pht('Account Already Linked'),
array(
pht(
'Your Phabricator account is already linked to an external '.
'account for this provider.'),
));
}
$panel_uri = '/settings/panel/external/';
$request->setCookie('phcid', Filesystem::readRandomCharacters(16));
$form = $provider->buildLinkForm($this);
if ($provider->isLoginFormAButton()) {
require_celerity_resource('auth-css');
$form = phutil_tag(
'div',
array(
'class' => 'phabricator-link-button pl',
),
$form);
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName(pht('Link Account'))
->setHref($panel_uri));
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName($provider->getProviderName()));
return $this->buildApplicationPage(
array(
$crumbs,
$form,
),
array(
'title' => pht('Link %s Account', $provider->getProviderName()),
'dust' => true,
'device' => true,
));
}
}

View file

@ -111,8 +111,23 @@ final class PhabricatorAuthLoginController
}
private function processRegisterUser(PhabricatorExternalAccount $account) {
$account_secret = $account->getAccountSecret();
$register_uri = $this->getApplicationURI('register/'.$account_secret.'/');
return $this->setAccountKeyAndContinue($account, $register_uri);
}
private function processLinkUser(PhabricatorExternalAccount $account) {
$account_secret = $account->getAccountSecret();
$confirm_uri = $this->getApplicationURI('confirmlink/'.$account_secret.'/');
return $this->setAccountKeyAndContinue($account, $confirm_uri);
}
private function setAccountKeyAndContinue(
PhabricatorExternalAccount $account,
$next_uri) {
if ($account->getUserPHID()) {
throw new Exception("Account is already registered.");
throw new Exception("Account is already registered or linked.");
}
// Regenerate the registration secret key, set it on the external account,
@ -131,14 +146,7 @@ final class PhabricatorAuthLoginController
$this->getRequest()->setCookie('phreg', $registration_key);
$account_secret = $account->getAccountSecret();
$register_uri = $this->getApplicationURI('register/'.$account_secret.'/');
return id(new AphrontRedirectResponse())->setURI($register_uri);
}
private function processLinkUser(PhabricatorExternalAccount $account) {
// TODO: Implement.
return new Aphront404Response();
return id(new AphrontRedirectResponse())->setURI($next_uri);
}
private function loadProvider() {
@ -160,19 +168,9 @@ final class PhabricatorAuthLoginController
}
protected function renderError($message) {
$title = pht('Login Failed');
$view = new AphrontErrorView();
$view->setTitle($title);
$view->appendChild($message);
return $this->buildApplicationPage(
$view,
array(
'title' => $title,
'device' => true,
'dust' => true,
));
return $this->renderErrorPage(
pht('Login Failed'),
array($message));
}
public function buildProviderPageResponse(

View file

@ -4,8 +4,6 @@ final class PhabricatorAuthRegisterController
extends PhabricatorAuthController {
private $accountKey;
private $account;
private $provider;
public function shouldRequireLogin() {
return false;
@ -23,16 +21,30 @@ final class PhabricatorAuthRegisterController
}
if (strlen($this->accountKey)) {
$response = $this->loadAccount();
$result = $this->loadAccountForRegistrationOrLinking($this->accountKey);
list($account, $provider, $response) = $result;
} else {
$response = $this->loadDefaultAccount();
list($account, $provider, $response) = $this->loadDefaultAccount();
}
if ($response) {
return $response;
}
$account = $this->account;
if (!$provider->shouldAllowRegistration()) {
// TODO: This is a routine error if you click "Login" on an external
// auth source which doesn't allow registration. The error should be
// more tailored.
return $this->renderError(
pht(
'The account you are attempting to register with uses an '.
'authentication provider ("%s") which does not allow registration. '.
'An administrator may have recently disabled registration with this '.
'provider.',
$provider->getProviderName()));
}
$user = new PhabricatorUser();
@ -88,7 +100,7 @@ final class PhabricatorAuthRegisterController
$can_edit_email = $profile->getCanEditEmail();
$can_edit_realname = $profile->getCanEditRealName();
$must_set_password = $this->provider->shouldRequireRegistrationPassword();
$must_set_password = $provider->shouldRequireRegistrationPassword();
$can_edit_anything = $profile->getCanEditAnything() || $must_set_password;
$force_verify = $profile->getShouldVerifyEmail();
@ -200,7 +212,7 @@ final class PhabricatorAuthRegisterController
}
$account->setUserPHID($user->getPHID());
$this->provider->willRegisterAccount($account);
$provider->willRegisterAccount($account);
$account->save();
$user->saveTransaction();
@ -315,134 +327,38 @@ final class PhabricatorAuthRegisterController
));
}
private function loadAccount() {
$request = $this->getRequest();
if (!$this->accountKey) {
return $this->renderError(pht('Request did not include account key.'));
}
$account = id(new PhabricatorExternalAccount())->loadOneWhere(
'accountSecret = %s',
$this->accountKey);
if (!$account) {
return $this->renderError(pht('No registration account.'));
}
if ($account->getUserPHID()) {
return $this->renderError(
pht(
'The account you are attempting to register with is already '.
'registered to another user.'));
}
$registration_key = $request->getCookie('phreg');
// NOTE: This registration key check is not strictly necessary, because
// we're only creating new accounts, not linking existing accounts. It
// might be more hassle than it is worth, especially for email.
//
// The attack this prevents is getting to the registration screen, then
// copy/pasting the URL and getting someone else to click it and complete
// the process. They end up with an account bound to credentials you
// control. This doesn't really let you do anything meaningful, though,
// since you could have simply completed the process yourself.
if (!$registration_key) {
return $this->renderError(
pht(
'Your browser did not submit a registration key with the request. '.
'You must use the same browser to begin and complete registration. '.
'Check that cookies are enabled and try again.'));
}
// We store the digest of the key rather than the key itself to prevent a
// theoretical attacker with read-only access to the database from
// hijacking registration sessions.
$actual = $account->getProperty('registrationKey');
$expect = PhabricatorHash::digest($registration_key);
if ($actual !== $expect) {
return $this->renderError(
pht(
'Your browser submitted a different registration key than the one '.
'associated with this account. You may need to clear your cookies.'));
}
$other_account = id(new PhabricatorExternalAccount())->loadAllWhere(
'accountType = %s AND accountDomain = %s AND accountID = %s
AND id != %d',
$account->getAccountType(),
$account->getAccountDomain(),
$account->getAccountID(),
$account->getID());
if ($other_account) {
return $this->renderError(
pht(
'The account you are attempting to register with already belongs '.
'to another user.'));
}
$provider = PhabricatorAuthProvider::getEnabledProviderByKey(
$account->getProviderKey());
if (!$provider) {
return $this->renderError(
pht(
'The account you are attempting to register with uses a nonexistent '.
'or disabled authentication provider (with key "%s"). An '.
'administrator may have recently disabled this provider.',
$account->getProviderKey()));
}
if (!$provider->shouldAllowRegistration()) {
// TODO: This is a routine error if you click "Login" on an external
// auth source which doesn't allow registration. The error should be
// more tailored.
return $this->renderError(
pht(
'The account you are attempting to register with uses an '.
'authentication provider ("%s") which does not allow registration. '.
'An administrator may have recently disabled registration with this '.
'provider.',
$provider->getProviderName()));
}
$this->account = $account;
$this->provider = $provider;
return null;
}
private function loadDefaultAccount() {
$providers = PhabricatorAuthProvider::getAllEnabledProviders();
foreach ($providers as $key => $provider) {
if (!$provider->shouldAllowRegistration()) {
$account = null;
$provider = null;
$response = null;
foreach ($providers as $key => $candidate_provider) {
if (!$candidate_provider->shouldAllowRegistration()) {
unset($providers[$key]);
continue;
}
if (!$provider->isDefaultRegistrationProvider()) {
if (!$candidate_provider->isDefaultRegistrationProvider()) {
unset($providers[$key]);
}
}
if (!$providers) {
return $this->renderError(
$response = $this->renderError(
pht(
"There are no configured default registration providers."));
return array($account, $provider, $response);
} else if (count($providers) > 1) {
return $this->renderError(
$response = $this->renderError(
pht(
"There are too many configured default registration providers."));
return array($account, $provider, $response);
}
$this->account = $provider->getDefaultExternalAccount();
$this->provider = $provider;
return null;
$provider = head($providers);
$account = $provider->getDefaultExternalAccount();
return array($account, $provider, $response);
}
private function loadProfilePicture(PhabricatorExternalAccount $account) {

View file

@ -0,0 +1,140 @@
<?php
final class PhabricatorAuthUnlinkController
extends PhabricatorAuthController {
private $providerKey;
public function willProcessRequest(array $data) {
$this->providerKey = $data['pkey'];
}
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
list($type, $domain) = explode(':', $this->providerKey, 2);
// Check that this account link actually exists. We don't require the
// provider to exist because we want users to be able to delete links to
// dead accounts if they want.
$account = id(new PhabricatorExternalAccount())->loadOneWhere(
'accountType = %s AND accountDomain = %s AND userPHID = %s',
$type,
$domain,
$viewer->getPHID());
if (!$account) {
return $this->renderNoAccountErrorDialog();
}
// Check that the provider (if it exists) allows accounts to be unlinked.
$provider_key = $this->providerKey;
$provider = PhabricatorAuthProvider::getEnabledProviderByKey($provider_key);
if ($provider) {
if (!$provider->shouldAllowAccountUnlink()) {
return $this->renderNotUnlinkableErrorDialog($provider);
}
}
// Check that this account isn't the last account which can be used to
// login. We prevent you from removing the last account.
if ($account->isUsableForLogin()) {
$other_accounts = id(new PhabricatorExternalAccount())->loadAllWhere(
'userPHID = %s',
$viewer->getPHID());
$valid_accounts = 0;
foreach ($other_accounts as $other_account) {
if ($other_account->isUsableForLogin()) {
$valid_accounts++;
}
}
if ($valid_accounts < 2) {
return $this->renderLastUsableAccountErrorDialog();
}
}
if ($request->isDialogFormPost()) {
$account->delete();
return id(new AphrontRedirectResponse())->setURI($this->getDoneURI());
}
return $this->renderConfirmDialog($account);
}
private function getDoneURI() {
return '/settings/panel/external/';
}
private function renderNoAccountErrorDialog() {
$dialog = id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setTitle(pht('No Such Account'))
->appendChild(
pht(
"You can not unlink this account because it is not linked."))
->addCancelButton($this->getDoneURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function renderNotUnlinkableErrorDialog(
PhabricatorAuthProvider $provider) {
$dialog = id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setTitle(pht('Permanent Account Link'))
->appendChild(
pht(
"You can not unlink this account because the administrator has ".
"configured Phabricator to make links to %s accounts permanent.",
$provider->getProviderName()))
->addCancelButton($this->getDoneURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function renderLastUsableAccountErrorDialog() {
$dialog = id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setTitle(pht('Last Valid Account'))
->appendChild(
pht(
"You can not unlink this account because you have no other ".
"valid login accounts. If you removed it, you would be unable ".
"to login. Add another authentication method before removing ".
"this one."))
->addCancelButton($this->getDoneURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function renderConfirmDialog() {
$provider_key = $this->providerKey;
$provider = PhabricatorAuthProvider::getEnabledProviderByKey($provider_key);
if ($provider) {
$title = pht('Unlink "%s" Account?', $provider->getProviderName());
$body = pht(
'You will no longer be able to use your %s account to '.
'log in to Phabricator.',
$provider->getProviderName());
} else {
$title = pht('Unlink Account?');
$body = pht(
'You will no longer be able to use this account to log in '.
'to Phabricator.');
}
$dialog = id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setTitle($title)
->appendChild($body)
->addSubmitButton(pht('Unlink Account'))
->addCancelButton($this->getDoneURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}

View file

@ -75,13 +75,7 @@ final class PhabricatorEmailTokenController
$next = '/';
if (!PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) {
$panels = id(new PhabricatorSettingsPanelOAuth())->buildPanels();
foreach ($panels as $panel) {
if ($panel->isEnabled()) {
$next = $panel->getPanelURI();
break;
}
}
$next = '/settings/panel/external/';
} else if (PhabricatorEnv::getEnvConfig('account.editable')) {
$next = (string)id(new PhutilURI('/settings/panel/password/'))
->setQueryParams(

View file

@ -1,38 +0,0 @@
<?php
final class PhabricatorLDAPUnlinkController extends PhabricatorAuthController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$ldap_account = id(new PhabricatorExternalAccount())->loadOneWhere(
'userPHID = %s AND accountType = %s AND accountDomain = %s',
$user->getPHID(),
'ldap',
'self');
if (!$ldap_account) {
return new Aphront400Response();
}
if (!$request->isDialogFormPost()) {
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle(pht('Really unlink account?'));
$dialog->appendChild(phutil_tag('p', array(), pht(
'You will not be able to login using this account '.
'once you unlink it. Continue?')));
$dialog->addSubmitButton(pht('Unlink Account'));
$dialog->addCancelButton('/settings/panel/ldap/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$ldap_account->delete();
return id(new AphrontRedirectResponse())
->setURI('/settings/panel/ldap/');
}
}

View file

@ -1,51 +0,0 @@
<?php
final class PhabricatorOAuthUnlinkController extends PhabricatorAuthController {
private $provider;
public function willProcessRequest(array $data) {
$this->provider = PhabricatorOAuthProvider::newProvider($data['provider']);
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$provider = $this->provider;
if ($provider->isProviderLinkPermanent()) {
throw new Exception(
pht("You may not unlink accounts from this OAuth provider."));
}
$provider_key = $provider->getProviderKey();
$oauth_info = PhabricatorUserOAuthInfo::loadOneByUserAndProviderKey(
$user,
$provider_key);
if (!$oauth_info) {
return new Aphront400Response();
}
if (!$request->isDialogFormPost()) {
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle(pht('Really unlink account?'));
$dialog->appendChild(phutil_tag('p', array(), pht(
'You will not be able to login using this account '.
'once you unlink it. Continue?')));
$dialog->addSubmitButton(pht('Unlink Account'));
$dialog->addCancelButton($provider->getSettingsPanelURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$oauth_info->delete();
return id(new AphrontRedirectResponse())
->setURI($provider->getSettingsPanelURI());
}
}

View file

@ -23,9 +23,7 @@ abstract class PhabricatorOAuthProvider {
abstract public function getTestURIs();
public function getSettingsPanelURI() {
$panel = new PhabricatorSettingsPanelOAuth();
$panel->setOAuthProvider($this);
return $panel->getPanelURI();
return '/settings/panel/external/';
}
/**

View file

@ -6,6 +6,14 @@ abstract class PhabricatorAuthProvider {
return $this->getAdapter()->getAdapterKey();
}
public function getProviderType() {
return $this->getAdapter()->getAdapterType();
}
public function getProviderDomain() {
return $this->getAdapter()->getAdapterDomain();
}
public static function getAllProviders() {
static $providers;
@ -56,8 +64,7 @@ abstract class PhabricatorAuthProvider {
abstract public function getAdapter();
public function isEnabled() {
// TODO: Remove once we switch to the new auth stuff.
return false;
return true;
}
abstract public function shouldAllowLogin();
@ -65,12 +72,25 @@ abstract class PhabricatorAuthProvider {
abstract public function shouldAllowAccountLink();
abstract public function shouldAllowAccountUnlink();
abstract public function buildLoginForm(
PhabricatorAuthStartController $controller);
public function buildLoginForm(
PhabricatorAuthStartController $controller) {
return $this->renderLoginForm($controller->getRequest(), $is_link = false);
}
abstract public function processLoginRequest(
PhabricatorAuthLoginController $controller);
public function buildLinkForm(
PhabricatorAuthLinkController $controller) {
return $this->renderLoginForm($controller->getRequest(), $is_link = true);
}
protected function renderLoginForm(
AphrontRequest $request,
$is_link) {
throw new Exception("Not implemented!");
}
public function createProviders() {
return array($this);
}
@ -144,7 +164,9 @@ abstract class PhabricatorAuthProvider {
$this->willSaveAccount($account);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$account->save();
unset($unguarded);
return $account;
}
@ -155,6 +177,10 @@ abstract class PhabricatorAuthProvider {
return PhabricatorEnv::getURI($uri);
}
protected function getCancelLinkURI() {
return '/settings/panel/external/';
}
public function isDefaultRegistrationProvider() {
return false;
}

View file

@ -49,29 +49,24 @@ final class PhabricatorAuthProviderLDAP
}
public function shouldAllowAccountLink() {
return false;
return true;
}
public function shouldAllowAccountUnlink() {
return false;
return true;
}
public function buildLoginForm(
PhabricatorAuthStartController $controller) {
$request = $controller->getRequest();
return $this->renderLoginForm($request);
}
private function renderLoginForm(AphrontRequest $request) {
protected function renderLoginForm(AphrontRequest $request, $is_link) {
$viewer = $request->getUser();
$dialog = id(new AphrontDialogView())
->setSubmitURI($this->getLoginURI())
->setUser($viewer);
if ($this->shouldAllowRegistration()) {
if ($is_link) {
$dialog->setTitle(pht('Link LDAP Account'));
$dialog->addSubmitButton(pht('Link Accounts'));
} else if ($this->shouldAllowRegistration()) {
$dialog->setTitle(pht('Login or Register with LDAP'));
$dialog->addSubmitButton(pht('Login or Register'));
} else {

View file

@ -40,13 +40,12 @@ abstract class PhabricatorAuthProviderOAuth extends PhabricatorAuthProvider {
return true;
}
public function buildLoginForm(
PhabricatorAuthStartController $controller) {
$request = $controller->getRequest();
protected function renderLoginForm(AphrontRequest $request, $is_link) {
$viewer = $request->getUser();
if ($this->shouldAllowRegistration()) {
if ($is_link) {
$button_text = pht('Link External Account');
} else if ($this->shouldAllowRegistration()) {
$button_text = pht('Login or Register');
} else {
$button_text = pht('Login');
@ -57,7 +56,6 @@ abstract class PhabricatorAuthProviderOAuth extends PhabricatorAuthProvider {
->setSpriteIcon($this->getLoginIcon());
$button = id(new PHUIButtonView())
->setTag('a')
->setSize(PHUIButtonView::BIG)
->setColor(PHUIButtonView::GREY)
->setIcon($icon)
@ -91,7 +89,6 @@ abstract class PhabricatorAuthProviderOAuth extends PhabricatorAuthProvider {
),
$content);
}
public function processLoginRequest(
PhabricatorAuthLoginController $controller) {

View file

@ -51,13 +51,16 @@ final class PhabricatorAuthProviderPassword
public function buildLoginForm(
PhabricatorAuthStartController $controller) {
$request = $controller->getRequest();
return $this->renderLoginForm($request);
return $this->renderPasswordLoginForm($request);
}
private function renderLoginForm(
public function buildLinkForm(
PhabricatorAuthLinkController $controller) {
throw new Exception("Password providers can't be linked.");
}
private function renderPasswordLoginForm(
AphrontRequest $request,
$require_captcha = false,
$captcha_valid = false) {
@ -195,7 +198,7 @@ final class PhabricatorAuthProviderPassword
$response = $controller->buildProviderPageResponse(
$this,
$this->renderLoginForm(
$this->renderPasswordLoginForm(
$request,
$require_captcha,
$captcha_valid));

View file

@ -57,4 +57,19 @@ final class PhabricatorExternalAccount extends PhabricatorUserDAO {
return idx($this->properties, $key, $default);
}
public function isUsableForLogin() {
$key = $this->getProviderKey();
$provider = PhabricatorAuthProvider::getEnabledProviderByKey($key);
if (!$provider) {
return false;
}
if (!$provider->shouldAllowLogin()) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,120 @@
<?php
final class PhabricatorSettingsPanelExternalAccounts
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'external';
}
public function getPanelName() {
return pht('External Accounts');
}
public function getPanelGroup() {
return pht('Authentication');
}
public function isEnabled() {
return true;
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$providers = PhabricatorAuthProvider::getAllProviders();
$accounts = id(new PhabricatorExternalAccount())->loadAllWhere(
'userPHID = %s',
$viewer->getPHID());
$linked_head = id(new PhabricatorHeaderView())
->setHeader(pht('Linked Accounts and Authentication'));
$linked = id(new PhabricatorObjectItemListView())
->setUser($viewer)
->setNoDataString(pht('You have no linked accounts.'));
$login_accounts = 0;
foreach ($accounts as $account) {
if ($account->isUsableForLogin()) {
$login_accounts++;
}
}
foreach ($accounts as $account) {
$item = id(new PhabricatorObjectItemView());
$provider = idx($providers, $account->getProviderKey());
if ($provider) {
$item->setHeader($provider->getProviderName());
$can_unlink = $provider->shouldAllowAccountUnlink();
if (!$can_unlink) {
$item->addAttribute(pht('Permanently Linked'));
}
} else {
$item->setHeader(
pht('Unknown Account ("%s")', $account->getProviderKey()));
$can_unlink = true;
}
$can_login = $account->isUsableForLogin();
if (!$can_login) {
$item->addAttribute(
pht(
'Disabled (an administrator has disabled login for this '.
'account provider).'));
}
$can_unlink = $can_unlink && (!$can_login || ($login_accounts > 1));
$item->addAction(
id(new PHUIListItemView())
->setIcon('delete')
->setWorkflow(true)
->setDisabled(!$can_unlink)
->setHref('/auth/unlink/'.$account->getProviderKey().'/'));
$linked->addItem($item);
}
$linkable_head = id(new PhabricatorHeaderView())
->setHeader(pht('Add External Account'));
$linkable = id(new PhabricatorObjectItemListView())
->setUser($viewer)
->setNoDataString(
pht('Your account is linked with all available providers.'));
$accounts = mpull($accounts, null, 'getProviderKey');
$providers = msort($providers, 'getProviderName');
foreach ($providers as $key => $provider) {
if (isset($accounts[$key])) {
continue;
}
if (!$provider->shouldAllowAccountLink()) {
continue;
}
$link_uri = '/auth/link/'.$provider->getProviderKey().'/';
$item = id(new PhabricatorObjectItemView());
$item->setHeader($provider->getProviderName());
$item->setHref($link_uri);
$item->addAction(
id(new PHUIListItemView())
->setIcon('link')
->setHref($link_uri));
$linkable->addItem($item);
}
return array(
$linked_head,
$linked,
$linkable_head,
$linkable,
);
}
}

View file

@ -1,92 +0,0 @@
<?php
final class PhabricatorSettingsPanelLDAP
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'ldap';
}
public function getPanelName() {
return pht('LDAP');
}
public function getPanelGroup() {
return pht('Linked Accounts');
}
public function isEnabled() {
$ldap_provider = new PhabricatorLDAPProvider();
return $ldap_provider->isProviderEnabled();
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$ldap_account = id(new PhabricatorExternalAccount())->loadOneWhere(
'userPHID = %s AND accountType = %s AND accountDomain = %s',
$user->getPHID(),
'ldap',
'self');
$forms = array();
if (!$ldap_account) {
$unlink = pht('Link LDAP Account');
$unlink_form = new AphrontFormView();
$unlink_form
->setUser($user)
->setAction('/ldap/login/')
->appendChild(hsprintf(
'<p class="aphront-form-instructions">%s</p>',
pht('There is currently no LDAP account linked to your Phabricator '.
'account. You can link an account, which will allow you to use it '.
'to log into Phabricator.')))
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('LDAP username'))
->setName('username'))
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('Password'))
->setName('password'))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht("Link LDAP Account \xC2\xBB")));
$forms['Link Account'] = $unlink_form;
} else {
$unlink = pht('Unlink LDAP Account');
$unlink_form = new AphrontFormView();
$unlink_form
->setUser($user)
->appendChild(hsprintf(
'<p class="aphront-form-instructions">%s</p>',
pht('You may unlink this account from your LDAP account. This will '.
'prevent you from logging in with your LDAP credentials.')))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/ldap/unlink/', $unlink));
$forms['Unlink Account'] = $unlink_form;
}
$header = new PhabricatorHeaderView();
$header->setHeader(pht('LDAP Account Settings'));
$formbox = new PHUIBoxView();
foreach ($forms as $name => $form) {
if ($name) {
$head = new PhabricatorHeaderView();
$head->setHeader($name);
$formbox->appendChild($head);
}
$formbox->appendChild($form);
}
return array(
$header,
$formbox,
);
}
}

View file

@ -1,156 +0,0 @@
<?php
final class PhabricatorSettingsPanelOAuth
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'oauth-'.$this->provider->getProviderKey();
}
public function getPanelName() {
return $this->provider->getProviderName();
}
public function getPanelGroup() {
return pht('Linked Accounts');
}
public function buildPanels() {
$panels = array();
$providers = PhabricatorOAuthProvider::getAllProviders();
foreach ($providers as $provider) {
$panel = clone $this;
$panel->setOAuthProvider($provider);
$panels[] = $panel;
}
return $panels;
}
public function isEnabled() {
return $this->provider->isProviderEnabled();
}
private $provider;
public function setOAuthProvider(PhabricatorOAuthProvider $oauth_provider) {
$this->provider = $oauth_provider;
return $this;
}
private function prepareAuthForm(AphrontFormView $form) {
$provider = $this->provider;
$auth_uri = $provider->getAuthURI();
$client_id = $provider->getClientID();
$redirect_uri = $provider->getRedirectURI();
$minimum_scope = $provider->getMinimumScope();
$form
->setAction($auth_uri)
->setMethod('GET')
->addHiddenInput('redirect_uri', $redirect_uri)
->addHiddenInput('client_id', $client_id)
->addHiddenInput('scope', $minimum_scope);
foreach ($provider->getExtraAuthParameters() as $key => $value) {
$form->addHiddenInput($key, $value);
}
return $form;
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$provider = $this->provider;
$notice = null;
$provider_name = $provider->getProviderName();
$provider_key = $provider->getProviderKey();
$oauth_info = PhabricatorUserOAuthInfo::loadOneByUserAndProviderKey(
$user,
$provider_key);
$form = new AphrontFormView();
$form->setUser($user);
$forms = array();
$forms[] = $form;
if (!$oauth_info) {
$form
->appendChild(hsprintf(
'<p class="aphront-form-instructions">%s</p>',
pht('There is currently no %s '.
'account linked to your Phabricator account. You can link an '.
'account, which will allow you to use it to log into Phabricator.',
$provider_name)));
$this->prepareAuthForm($form);
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht("Link %s Account \xC2\xBB", $provider_name)));
} else {
$form
->appendChild(hsprintf(
'<p class="aphront-form-instructions">%s</p>',
pht('Your account is linked with '.
'a %s account. You may use your %s credentials to log into '.
'Phabricator.',
$provider_name,
$provider_name)))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('%s ID', $provider_name))
->setValue($oauth_info->getOAuthUID()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('%s Name', $provider_name))
->setValue($oauth_info->getAccountName()))
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('%s URI', $provider_name))
->setValue($oauth_info->getAccountURI()));
if (!$provider->isProviderLinkPermanent()) {
$unlink = pht('Unlink %s Account', $provider_name);
$unlink_form = new AphrontFormView();
$unlink_form
->setUser($user)
->appendChild(hsprintf(
'<p class="aphront-form-instructions">%s</p>',
pht('You may unlink this account from your %s account. This will '.
'prevent you from logging in with your %s credentials.',
$provider_name,
$provider_name)))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/oauth/'.$provider_key.'/unlink/', $unlink));
$forms['Unlink Account'] = $unlink_form;
}
}
$header = new PhabricatorHeaderView();
$header->setHeader(pht('%s Account Settings', $provider_name));
$formbox = new PHUIBoxView();
foreach ($forms as $name => $form) {
if ($name) {
$head = new PhabricatorHeaderView();
$head->setHeader($name);
$formbox->appendChild($head);
}
$formbox->appendChild($form);
}
return id(new AphrontNullView())
->appendChild(
array(
$notice,
$header,
$formbox,
));
}
}

View file

@ -14,7 +14,7 @@ final class PHUIButtonView extends AphrontTagView {
private $text;
private $subtext;
private $color;
private $tag = 'a';
private $tag = 'button';
private $dropdown;
private $icon;

View file

@ -18,6 +18,7 @@ final class PHUIListItemView extends AphrontTagView {
private $icon;
private $selected;
private $containerAttrs;
private $disabled;
public function setSelected($selected) {
$this->selected = $selected;
@ -104,6 +105,15 @@ final class PHUIListItemView extends AphrontTagView {
);
}
public function setDisabled($disabled) {
$this->disabled = $disabled;
return $this;
}
public function getDisabled() {
return $this->disabled;
}
protected function getTagContent() {
$name = null;
$icon = null;
@ -126,10 +136,15 @@ final class PHUIListItemView extends AphrontTagView {
}
if ($this->icon) {
$icon_name = $this->icon;
if ($this->getDisabled()) {
$icon_name .= '-grey';
}
$icon = id(new PHUIIconView())
->addClass('phui-list-item-icon')
->setSpriteSheet(PHUIIconView::SPRITE_ICONS)
->setSpriteIcon($this->icon);
->setSpriteIcon($icon_name);
}
return phutil_tag(

View file

@ -8,7 +8,7 @@
}
.phabricator-login-buttons .phabricator-login-button .button {
width: 180px;
width: 216px;
}
.device-desktop .phabricator-login-buttons .aphront-multi-column-column-last {
@ -18,3 +18,7 @@
.device .phabricator-login-buttons {
text-align: center;
}
.phabricator-link-button {
text-align: center;
}