mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-10 00:42:41 +01:00
Support invites in the registration and login flow
Summary: Ref T7152. This substantially completes the upstream login flow. Basically, we just cookie you and push you through normal registration, with slight changes: - All providers allow registration if you have an invite. - Most providers get minor text changes to say "Register" instead of "Login" or "Login or Register". - The Username/Password provider changes to just a "choose a username" form. - We show the user that they're accepting an invite, and who invited them. Then on actual registration: - Accepting an invite auto-verifies the address. - Accepting an invite auto-approves the account. - Your email is set to the invite email and locked. - Invites get to reassign nonprimary, unverified addresses from other accounts. But 98% of the code is the same. Test Plan: - Accepted an invite. - Verified a new address on an existing account via invite. - Followed a bad invite link. - Tried to accept a verified invite. - Reassigned an email by accepting an unverified, nonprimary invite on a new account. - Verified that reassigns appear in the activity log. {F291493} {F291494} {F291495} {F291496} {F291497} {F291498} {F291499} Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T7152 Differential Revision: https://secure.phabricator.com/D11737
This commit is contained in:
parent
6f90fbdef8
commit
7797443428
9 changed files with 237 additions and 48 deletions
|
@ -57,6 +57,12 @@ final class PhabricatorCookies extends Phobject {
|
|||
const COOKIE_HISEC = 'jump_to_hisec';
|
||||
|
||||
|
||||
/**
|
||||
* Stores an invite code.
|
||||
*/
|
||||
const COOKIE_INVITE = 'invite';
|
||||
|
||||
|
||||
/* -( Client ID Cookie )--------------------------------------------------- */
|
||||
|
||||
|
||||
|
|
|
@ -108,6 +108,9 @@ abstract class PhabricatorAuthController extends PhabricatorController {
|
|||
|
||||
// Clear the client ID / OAuth state key.
|
||||
$request->clearCookie(PhabricatorCookies::COOKIE_CLIENTID);
|
||||
|
||||
// Clear the invite cookie.
|
||||
$request->clearCookie(PhabricatorCookies::COOKIE_INVITE);
|
||||
}
|
||||
|
||||
private function buildLoginValidateResponse(PhabricatorUser $user) {
|
||||
|
@ -246,4 +249,57 @@ abstract class PhabricatorAuthController extends PhabricatorController {
|
|||
return array($account, $provider, null);
|
||||
}
|
||||
|
||||
protected function loadInvite() {
|
||||
$invite_cookie = PhabricatorCookies::COOKIE_INVITE;
|
||||
$invite_code = $this->getRequest()->getCookie($invite_cookie);
|
||||
if (!$invite_code) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$engine = id(new PhabricatorAuthInviteEngine())
|
||||
->setViewer($this->getViewer())
|
||||
->setUserHasConfirmedVerify(true);
|
||||
|
||||
try {
|
||||
return $engine->processInviteCode($invite_code);
|
||||
} catch (Exception $ex) {
|
||||
// If this fails for any reason, just drop the invite. In normal
|
||||
// circumstances, we gave them a detailed explanation of any error
|
||||
// before they jumped into this workflow.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function renderInviteHeader(PhabricatorAuthInvite $invite) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$invite_author = id(new PhabricatorPeopleQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(array($invite->getAuthorPHID()))
|
||||
->needProfileImage(true)
|
||||
->executeOne();
|
||||
|
||||
// If we can't load the author for some reason, just drop this message.
|
||||
// We lose the value of contextualizing things without author details.
|
||||
if (!$invite_author) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$invite_item = id(new PHUIObjectItemView())
|
||||
->setHeader(pht('Welcome to Phabricator!'))
|
||||
->setImageURI($invite_author->getProfileImageURI())
|
||||
->addAttribute(
|
||||
pht(
|
||||
'%s has invited you to join Phabricator.',
|
||||
$invite_author->getFullName()));
|
||||
|
||||
$invite_list = id(new PHUIObjectItemListView())
|
||||
->addItem($invite_item)
|
||||
->setFlush(true);
|
||||
|
||||
return id(new PHUIBoxView())
|
||||
->addMargin(PHUI::MARGIN_LARGE)
|
||||
->appendChild($invite_list);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,8 +17,10 @@ final class PhabricatorAuthInviteController
|
|||
$engine->setUserHasConfirmedVerify(true);
|
||||
}
|
||||
|
||||
$invite_code = $request->getURIData('code');
|
||||
|
||||
try {
|
||||
$invite = $engine->processInviteCode($request->getURIData('code'));
|
||||
$invite = $engine->processInviteCode($invite_code);
|
||||
} catch (PhabricatorAuthInviteDialogException $ex) {
|
||||
$response = $this->newDialog()
|
||||
->setTitle($ex->getTitle())
|
||||
|
@ -48,10 +50,13 @@ final class PhabricatorAuthInviteController
|
|||
return id(new AphrontRedirectResponse())->setURI('/');
|
||||
}
|
||||
|
||||
// Give the user a cookie with the invite code and send them through
|
||||
// normal registration. We'll adjust the flow there.
|
||||
$request->setCookie(
|
||||
PhabricatorCookies::COOKIE_INVITE,
|
||||
$invite_code);
|
||||
|
||||
// TODO: This invite is good, but we need to drive the user through
|
||||
// registration.
|
||||
throw new Exception(pht('TODO: Build invite/registration workflow.'));
|
||||
return id(new AphrontRedirectResponse())->setURI('/auth/start/');
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -38,19 +38,25 @@ final class PhabricatorAuthRegisterController
|
|||
return $response;
|
||||
}
|
||||
|
||||
$invite = $this->loadInvite();
|
||||
|
||||
if (!$provider->shouldAllowRegistration()) {
|
||||
if ($invite) {
|
||||
// If the user has an invite, we allow them to register with any
|
||||
// provider, even a login-only provider.
|
||||
} else {
|
||||
// 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.
|
||||
|
||||
// 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()));
|
||||
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();
|
||||
|
@ -59,9 +65,15 @@ final class PhabricatorAuthRegisterController
|
|||
$default_realname = $account->getRealName();
|
||||
|
||||
$default_email = $account->getEmail();
|
||||
|
||||
if ($invite) {
|
||||
$default_email = $invite->getEmailAddress();
|
||||
}
|
||||
|
||||
if (!PhabricatorUserEmail::isValidAddress($default_email)) {
|
||||
$default_email = null;
|
||||
}
|
||||
|
||||
if ($default_email !== null) {
|
||||
// We should bypass policy here becase e.g. limiting an application use
|
||||
// to a subset of users should not allow the others to overwrite
|
||||
|
@ -105,7 +117,13 @@ final class PhabricatorAuthRegisterController
|
|||
'address = %s',
|
||||
$default_email);
|
||||
if ($same_email) {
|
||||
$default_email = null;
|
||||
if ($invite) {
|
||||
// We're allowing this to continue. The fact that we loaded the
|
||||
// invite means that the address is nonprimary and unverified and
|
||||
// we're OK to steal it.
|
||||
} else {
|
||||
$default_email = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -166,7 +184,13 @@ final class PhabricatorAuthRegisterController
|
|||
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
|
||||
$min_len = (int)$min_len;
|
||||
|
||||
if ($request->isFormPost() || !$can_edit_anything) {
|
||||
$from_invite = $request->getStr('invite');
|
||||
if ($from_invite && $can_edit_username) {
|
||||
$value_username = $request->getStr('username');
|
||||
$e_username = null;
|
||||
}
|
||||
|
||||
if (($request->isFormPost() || !$can_edit_anything) && !$from_invite) {
|
||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||
|
||||
if ($must_set_password) {
|
||||
|
@ -252,28 +276,48 @@ final class PhabricatorAuthRegisterController
|
|||
}
|
||||
|
||||
try {
|
||||
$verify_email = false;
|
||||
|
||||
if ($force_verify) {
|
||||
$verify_email = true;
|
||||
} else {
|
||||
$verify_email =
|
||||
($account->getEmailVerified()) &&
|
||||
($value_email === $default_email);
|
||||
}
|
||||
|
||||
if ($provider->shouldTrustEmails() &&
|
||||
$value_email === $default_email) {
|
||||
$verify_email = true;
|
||||
if ($value_email === $default_email) {
|
||||
if ($account->getEmailVerified()) {
|
||||
$verify_email = true;
|
||||
}
|
||||
|
||||
if ($provider->shouldTrustEmails()) {
|
||||
$verify_email = true;
|
||||
}
|
||||
|
||||
if ($invite) {
|
||||
$verify_email = true;
|
||||
}
|
||||
}
|
||||
|
||||
$email_obj = id(new PhabricatorUserEmail())
|
||||
->setAddress($value_email)
|
||||
->setIsVerified((int)$verify_email);
|
||||
$email_obj = null;
|
||||
if ($invite) {
|
||||
// If we have a valid invite, this email may exist but be
|
||||
// nonprimary and unverified, so we'll reassign it.
|
||||
$email_obj = id(new PhabricatorUserEmail())->loadOneWhere(
|
||||
'address = %s',
|
||||
$value_email);
|
||||
}
|
||||
if (!$email_obj) {
|
||||
$email_obj = id(new PhabricatorUserEmail())
|
||||
->setAddress($value_email);
|
||||
}
|
||||
|
||||
$email_obj->setIsVerified((int)$verify_email);
|
||||
|
||||
$user->setUsername($value_username);
|
||||
$user->setRealname($value_realname);
|
||||
|
||||
if ($is_setup) {
|
||||
$must_approve = false;
|
||||
} else if ($invite) {
|
||||
$must_approve = false;
|
||||
} else {
|
||||
$must_approve = PhabricatorEnv::getEnvConfig(
|
||||
'auth.require-approval');
|
||||
|
@ -285,12 +329,18 @@ final class PhabricatorAuthRegisterController
|
|||
$user->setIsApproved(1);
|
||||
}
|
||||
|
||||
if ($invite) {
|
||||
$allow_reassign_email = true;
|
||||
} else {
|
||||
$allow_reassign_email = false;
|
||||
}
|
||||
|
||||
$user->openTransaction();
|
||||
|
||||
$editor = id(new PhabricatorUserEditor())
|
||||
->setActor($user);
|
||||
|
||||
$editor->createNewUser($user, $email_obj);
|
||||
$editor->createNewUser($user, $email_obj, $allow_reassign_email);
|
||||
if ($must_set_password) {
|
||||
$envelope = new PhutilOpaqueEnvelope($value_password);
|
||||
$editor->changePassword($user, $envelope);
|
||||
|
@ -314,6 +364,10 @@ final class PhabricatorAuthRegisterController
|
|||
$this->sendWaitingForApprovalEmail($user);
|
||||
}
|
||||
|
||||
if ($invite) {
|
||||
$invite->setAcceptedByPHID($user->getPHID())->save();
|
||||
}
|
||||
|
||||
return $this->loginUser($user);
|
||||
} catch (AphrontDuplicateKeyQueryException $exception) {
|
||||
$same_username = id(new PhabricatorUser())->loadOneWhere(
|
||||
|
@ -374,21 +428,30 @@ final class PhabricatorAuthRegisterController
|
|||
->setError($e_username));
|
||||
}
|
||||
|
||||
if ($can_edit_realname) {
|
||||
$form->appendChild(
|
||||
id(new AphrontFormTextControl())
|
||||
->setLabel(pht('Real Name'))
|
||||
->setName('realName')
|
||||
->setValue($value_realname)
|
||||
->setError($e_realname));
|
||||
}
|
||||
|
||||
if ($must_set_password) {
|
||||
$form->appendChild(
|
||||
id(new AphrontFormPasswordControl())
|
||||
->setLabel(pht('Password'))
|
||||
->setName('password')
|
||||
->setError($e_password));
|
||||
$form->appendChild(
|
||||
id(new AphrontFormPasswordControl())
|
||||
->setLabel(pht('Confirm Password'))
|
||||
->setName('confirm')
|
||||
->setError($e_password)
|
||||
->setCaption(
|
||||
$min_len
|
||||
? pht('Minimum length of %d characters.', $min_len)
|
||||
: null));
|
||||
$form->appendChild(
|
||||
id(new AphrontFormPasswordControl())
|
||||
->setLabel(pht('Confirm Password'))
|
||||
->setName('confirm')
|
||||
->setError($e_password));
|
||||
}
|
||||
|
||||
if ($can_edit_email) {
|
||||
|
@ -401,15 +464,6 @@ final class PhabricatorAuthRegisterController
|
|||
->setError($e_email));
|
||||
}
|
||||
|
||||
if ($can_edit_realname) {
|
||||
$form->appendChild(
|
||||
id(new AphrontFormTextControl())
|
||||
->setLabel(pht('Real Name'))
|
||||
->setName('realName')
|
||||
->setValue($value_realname)
|
||||
->setError($e_realname));
|
||||
}
|
||||
|
||||
if ($must_set_password) {
|
||||
$form->appendChild(
|
||||
id(new AphrontFormRecaptchaControl())
|
||||
|
@ -459,10 +513,16 @@ final class PhabricatorAuthRegisterController
|
|||
->setForm($form)
|
||||
->setFormErrors($errors);
|
||||
|
||||
$invite_header = null;
|
||||
if ($invite) {
|
||||
$invite_header = $this->renderInviteHeader($invite);
|
||||
}
|
||||
|
||||
return $this->buildApplicationPage(
|
||||
array(
|
||||
$crumbs,
|
||||
$welcome_view,
|
||||
$invite_header,
|
||||
$object_box,
|
||||
),
|
||||
array(
|
||||
|
|
|
@ -109,14 +109,21 @@ final class PhabricatorAuthStartController
|
|||
}
|
||||
}
|
||||
|
||||
$invite = $this->loadInvite();
|
||||
|
||||
$not_buttons = array();
|
||||
$are_buttons = array();
|
||||
$providers = msort($providers, 'getLoginOrder');
|
||||
foreach ($providers as $provider) {
|
||||
if ($provider->isLoginFormAButton()) {
|
||||
$are_buttons[] = $provider->buildLoginForm($this);
|
||||
if ($invite) {
|
||||
$form = $provider->buildInviteForm($this);
|
||||
} else {
|
||||
$not_buttons[] = $provider->buildLoginForm($this);
|
||||
$form = $provider->buildLoginForm($this);
|
||||
}
|
||||
if ($provider->isLoginFormAButton()) {
|
||||
$are_buttons[] = $form;
|
||||
} else {
|
||||
$not_buttons[] = $form;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,6 +166,11 @@ final class PhabricatorAuthStartController
|
|||
$login_message = PhabricatorEnv::getEnvConfig('auth.login-message');
|
||||
$login_message = phutil_safe_html($login_message);
|
||||
|
||||
$invite_message = null;
|
||||
if ($invite) {
|
||||
$invite_message = $this->renderInviteHeader($invite);
|
||||
}
|
||||
|
||||
$crumbs = $this->buildApplicationCrumbs();
|
||||
$crumbs->addTextCrumb(pht('Login'));
|
||||
$crumbs->setBorder(true);
|
||||
|
@ -167,6 +179,7 @@ final class PhabricatorAuthStartController
|
|||
array(
|
||||
$crumbs,
|
||||
$login_message,
|
||||
$invite_message,
|
||||
$out,
|
||||
),
|
||||
array(
|
||||
|
|
|
@ -250,7 +250,7 @@ final class PhabricatorAuthInviteEngine extends Phobject {
|
|||
}
|
||||
|
||||
private function getLogoutURI() {
|
||||
return '/auth/logout/';
|
||||
return '/logout/';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -158,6 +158,10 @@ abstract class PhabricatorAuthProvider {
|
|||
return $this->renderLoginForm($controller->getRequest(), $mode = 'start');
|
||||
}
|
||||
|
||||
public function buildInviteForm(PhabricatorAuthStartController $controller) {
|
||||
return $this->renderLoginForm($controller->getRequest(), $mode = 'invite');
|
||||
}
|
||||
|
||||
abstract public function processLoginRequest(
|
||||
PhabricatorAuthLoginController $controller);
|
||||
|
||||
|
@ -401,6 +405,8 @@ abstract class PhabricatorAuthProvider {
|
|||
$button_text = pht('Link External Account');
|
||||
} else if ($mode == 'refresh') {
|
||||
$button_text = pht('Refresh Account Link');
|
||||
} else if ($mode == 'invite') {
|
||||
$button_text = pht('Register Account');
|
||||
} else if ($this->shouldAllowRegistration()) {
|
||||
$button_text = pht('Login or Register');
|
||||
} else {
|
||||
|
|
|
@ -135,6 +135,29 @@ final class PhabricatorPasswordAuthProvider extends PhabricatorAuthProvider {
|
|||
return $this->renderPasswordLoginForm($request);
|
||||
}
|
||||
|
||||
public function buildInviteForm(
|
||||
PhabricatorAuthStartController $controller) {
|
||||
$request = $controller->getRequest();
|
||||
$viewer = $request->getViewer();
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->setUser($viewer)
|
||||
->addHiddenInput('invite', true)
|
||||
->appendChild(
|
||||
id(new AphrontFormTextControl())
|
||||
->setLabel(pht('Username'))
|
||||
->setName('username'));
|
||||
|
||||
$dialog = id(new AphrontDialogView())
|
||||
->setUser($viewer)
|
||||
->setTitle(pht('Register an Account'))
|
||||
->appendForm($form)
|
||||
->setSubmitURI('/auth/register/')
|
||||
->addSubmitButton(pht('Continue'));
|
||||
|
||||
return $dialog;
|
||||
}
|
||||
|
||||
public function buildLinkForm(
|
||||
PhabricatorAuthLinkController $controller) {
|
||||
throw new Exception("Password providers can't be linked.");
|
||||
|
|
|
@ -23,14 +23,25 @@ final class PhabricatorUserEditor extends PhabricatorEditor {
|
|||
*/
|
||||
public function createNewUser(
|
||||
PhabricatorUser $user,
|
||||
PhabricatorUserEmail $email) {
|
||||
PhabricatorUserEmail $email,
|
||||
$allow_reassign = false) {
|
||||
|
||||
if ($user->getID()) {
|
||||
throw new Exception('User has already been created!');
|
||||
}
|
||||
|
||||
$is_reassign = false;
|
||||
if ($email->getID()) {
|
||||
throw new Exception('Email has already been created!');
|
||||
if ($allow_reassign) {
|
||||
if ($email->getIsPrimary()) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Primary email addresses can not be reassigned.'));
|
||||
}
|
||||
$is_reassign = true;
|
||||
} else {
|
||||
throw new Exception('Email has already been created!');
|
||||
}
|
||||
}
|
||||
|
||||
if (!PhabricatorUser::validateUsername($user->getUsername())) {
|
||||
|
@ -71,6 +82,15 @@ final class PhabricatorUserEditor extends PhabricatorEditor {
|
|||
$log->setNewValue($email->getAddress());
|
||||
$log->save();
|
||||
|
||||
if ($is_reassign) {
|
||||
$log = PhabricatorUserLog::initializeNewLog(
|
||||
$this->requireActor(),
|
||||
$user->getPHID(),
|
||||
PhabricatorUserLog::ACTION_EMAIL_REASSIGN);
|
||||
$log->setNewValue($email->getAddress());
|
||||
$log->save();
|
||||
}
|
||||
|
||||
$user->saveTransaction();
|
||||
|
||||
return $this;
|
||||
|
|
Loading…
Reference in a new issue