1
0
Fork 0
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:
epriestley 2015-02-11 06:06:28 -08:00
parent 6f90fbdef8
commit 7797443428
9 changed files with 237 additions and 48 deletions

View file

@ -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 )--------------------------------------------------- */

View file

@ -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);
}
}

View file

@ -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/');
}

View file

@ -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(

View file

@ -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(

View file

@ -250,7 +250,7 @@ final class PhabricatorAuthInviteEngine extends Phobject {
}
private function getLogoutURI() {
return '/auth/logout/';
return '/logout/';
}
}

View file

@ -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 {

View file

@ -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.");

View file

@ -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;