@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+236 -47
+6
src/applications/auth/constants/PhabricatorCookies.php
··· 57 57 const COOKIE_HISEC = 'jump_to_hisec'; 58 58 59 59 60 + /** 61 + * Stores an invite code. 62 + */ 63 + const COOKIE_INVITE = 'invite'; 64 + 65 + 60 66 /* -( Client ID Cookie )--------------------------------------------------- */ 61 67 62 68
+56
src/applications/auth/controller/PhabricatorAuthController.php
··· 108 108 109 109 // Clear the client ID / OAuth state key. 110 110 $request->clearCookie(PhabricatorCookies::COOKIE_CLIENTID); 111 + 112 + // Clear the invite cookie. 113 + $request->clearCookie(PhabricatorCookies::COOKIE_INVITE); 111 114 } 112 115 113 116 private function buildLoginValidateResponse(PhabricatorUser $user) { ··· 244 247 } 245 248 246 249 return array($account, $provider, null); 250 + } 251 + 252 + protected function loadInvite() { 253 + $invite_cookie = PhabricatorCookies::COOKIE_INVITE; 254 + $invite_code = $this->getRequest()->getCookie($invite_cookie); 255 + if (!$invite_code) { 256 + return null; 257 + } 258 + 259 + $engine = id(new PhabricatorAuthInviteEngine()) 260 + ->setViewer($this->getViewer()) 261 + ->setUserHasConfirmedVerify(true); 262 + 263 + try { 264 + return $engine->processInviteCode($invite_code); 265 + } catch (Exception $ex) { 266 + // If this fails for any reason, just drop the invite. In normal 267 + // circumstances, we gave them a detailed explanation of any error 268 + // before they jumped into this workflow. 269 + return null; 270 + } 271 + } 272 + 273 + protected function renderInviteHeader(PhabricatorAuthInvite $invite) { 274 + $viewer = $this->getViewer(); 275 + 276 + $invite_author = id(new PhabricatorPeopleQuery()) 277 + ->setViewer($viewer) 278 + ->withPHIDs(array($invite->getAuthorPHID())) 279 + ->needProfileImage(true) 280 + ->executeOne(); 281 + 282 + // If we can't load the author for some reason, just drop this message. 283 + // We lose the value of contextualizing things without author details. 284 + if (!$invite_author) { 285 + return null; 286 + } 287 + 288 + $invite_item = id(new PHUIObjectItemView()) 289 + ->setHeader(pht('Welcome to Phabricator!')) 290 + ->setImageURI($invite_author->getProfileImageURI()) 291 + ->addAttribute( 292 + pht( 293 + '%s has invited you to join Phabricator.', 294 + $invite_author->getFullName())); 295 + 296 + $invite_list = id(new PHUIObjectItemListView()) 297 + ->addItem($invite_item) 298 + ->setFlush(true); 299 + 300 + return id(new PHUIBoxView()) 301 + ->addMargin(PHUI::MARGIN_LARGE) 302 + ->appendChild($invite_list); 247 303 } 248 304 249 305 }
+9 -4
src/applications/auth/controller/PhabricatorAuthInviteController.php
··· 17 17 $engine->setUserHasConfirmedVerify(true); 18 18 } 19 19 20 + $invite_code = $request->getURIData('code'); 21 + 20 22 try { 21 - $invite = $engine->processInviteCode($request->getURIData('code')); 23 + $invite = $engine->processInviteCode($invite_code); 22 24 } catch (PhabricatorAuthInviteDialogException $ex) { 23 25 $response = $this->newDialog() 24 26 ->setTitle($ex->getTitle()) ··· 48 50 return id(new AphrontRedirectResponse())->setURI('/'); 49 51 } 50 52 53 + // Give the user a cookie with the invite code and send them through 54 + // normal registration. We'll adjust the flow there. 55 + $request->setCookie( 56 + PhabricatorCookies::COOKIE_INVITE, 57 + $invite_code); 51 58 52 - // TODO: This invite is good, but we need to drive the user through 53 - // registration. 54 - throw new Exception(pht('TODO: Build invite/registration workflow.')); 59 + return id(new AphrontRedirectResponse())->setURI('/auth/start/'); 55 60 } 56 61 57 62
+98 -38
src/applications/auth/controller/PhabricatorAuthRegisterController.php
··· 38 38 return $response; 39 39 } 40 40 41 + $invite = $this->loadInvite(); 42 + 41 43 if (!$provider->shouldAllowRegistration()) { 44 + if ($invite) { 45 + // If the user has an invite, we allow them to register with any 46 + // provider, even a login-only provider. 47 + } else { 48 + // TODO: This is a routine error if you click "Login" on an external 49 + // auth source which doesn't allow registration. The error should be 50 + // more tailored. 42 51 43 - // TODO: This is a routine error if you click "Login" on an external 44 - // auth source which doesn't allow registration. The error should be 45 - // more tailored. 46 - 47 - return $this->renderError( 48 - pht( 49 - 'The account you are attempting to register with uses an '. 50 - 'authentication provider ("%s") which does not allow registration. '. 51 - 'An administrator may have recently disabled registration with this '. 52 - 'provider.', 53 - $provider->getProviderName())); 52 + return $this->renderError( 53 + pht( 54 + 'The account you are attempting to register with uses an '. 55 + 'authentication provider ("%s") which does not allow '. 56 + 'registration. An administrator may have recently disabled '. 57 + 'registration with this provider.', 58 + $provider->getProviderName())); 59 + } 54 60 } 55 61 56 62 $user = new PhabricatorUser(); ··· 59 65 $default_realname = $account->getRealName(); 60 66 61 67 $default_email = $account->getEmail(); 68 + 69 + if ($invite) { 70 + $default_email = $invite->getEmailAddress(); 71 + } 72 + 62 73 if (!PhabricatorUserEmail::isValidAddress($default_email)) { 63 74 $default_email = null; 64 75 } 76 + 65 77 if ($default_email !== null) { 66 78 // We should bypass policy here becase e.g. limiting an application use 67 79 // to a subset of users should not allow the others to overwrite ··· 105 117 'address = %s', 106 118 $default_email); 107 119 if ($same_email) { 108 - $default_email = null; 120 + if ($invite) { 121 + // We're allowing this to continue. The fact that we loaded the 122 + // invite means that the address is nonprimary and unverified and 123 + // we're OK to steal it. 124 + } else { 125 + $default_email = null; 126 + } 109 127 } 110 128 } 111 129 } ··· 166 184 $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length'); 167 185 $min_len = (int)$min_len; 168 186 169 - if ($request->isFormPost() || !$can_edit_anything) { 187 + $from_invite = $request->getStr('invite'); 188 + if ($from_invite && $can_edit_username) { 189 + $value_username = $request->getStr('username'); 190 + $e_username = null; 191 + } 192 + 193 + if (($request->isFormPost() || !$can_edit_anything) && !$from_invite) { 170 194 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 171 195 172 196 if ($must_set_password) { ··· 252 276 } 253 277 254 278 try { 279 + $verify_email = false; 280 + 255 281 if ($force_verify) { 256 282 $verify_email = true; 257 - } else { 258 - $verify_email = 259 - ($account->getEmailVerified()) && 260 - ($value_email === $default_email); 261 283 } 262 284 263 - if ($provider->shouldTrustEmails() && 264 - $value_email === $default_email) { 265 - $verify_email = true; 285 + if ($value_email === $default_email) { 286 + if ($account->getEmailVerified()) { 287 + $verify_email = true; 288 + } 289 + 290 + if ($provider->shouldTrustEmails()) { 291 + $verify_email = true; 292 + } 293 + 294 + if ($invite) { 295 + $verify_email = true; 296 + } 266 297 } 267 298 268 - $email_obj = id(new PhabricatorUserEmail()) 269 - ->setAddress($value_email) 270 - ->setIsVerified((int)$verify_email); 299 + $email_obj = null; 300 + if ($invite) { 301 + // If we have a valid invite, this email may exist but be 302 + // nonprimary and unverified, so we'll reassign it. 303 + $email_obj = id(new PhabricatorUserEmail())->loadOneWhere( 304 + 'address = %s', 305 + $value_email); 306 + } 307 + if (!$email_obj) { 308 + $email_obj = id(new PhabricatorUserEmail()) 309 + ->setAddress($value_email); 310 + } 311 + 312 + $email_obj->setIsVerified((int)$verify_email); 271 313 272 314 $user->setUsername($value_username); 273 315 $user->setRealname($value_realname); 274 316 275 317 if ($is_setup) { 318 + $must_approve = false; 319 + } else if ($invite) { 276 320 $must_approve = false; 277 321 } else { 278 322 $must_approve = PhabricatorEnv::getEnvConfig( ··· 285 329 $user->setIsApproved(1); 286 330 } 287 331 332 + if ($invite) { 333 + $allow_reassign_email = true; 334 + } else { 335 + $allow_reassign_email = false; 336 + } 337 + 288 338 $user->openTransaction(); 289 339 290 340 $editor = id(new PhabricatorUserEditor()) 291 341 ->setActor($user); 292 342 293 - $editor->createNewUser($user, $email_obj); 343 + $editor->createNewUser($user, $email_obj, $allow_reassign_email); 294 344 if ($must_set_password) { 295 345 $envelope = new PhutilOpaqueEnvelope($value_password); 296 346 $editor->changePassword($user, $envelope); ··· 312 362 313 363 if ($must_approve) { 314 364 $this->sendWaitingForApprovalEmail($user); 365 + } 366 + 367 + if ($invite) { 368 + $invite->setAcceptedByPHID($user->getPHID())->save(); 315 369 } 316 370 317 371 return $this->loginUser($user); ··· 374 428 ->setError($e_username)); 375 429 } 376 430 431 + if ($can_edit_realname) { 432 + $form->appendChild( 433 + id(new AphrontFormTextControl()) 434 + ->setLabel(pht('Real Name')) 435 + ->setName('realName') 436 + ->setValue($value_realname) 437 + ->setError($e_realname)); 438 + } 439 + 377 440 if ($must_set_password) { 378 441 $form->appendChild( 379 442 id(new AphrontFormPasswordControl()) 380 443 ->setLabel(pht('Password')) 381 444 ->setName('password') 445 + ->setError($e_password)); 446 + $form->appendChild( 447 + id(new AphrontFormPasswordControl()) 448 + ->setLabel(pht('Confirm Password')) 449 + ->setName('confirm') 382 450 ->setError($e_password) 383 451 ->setCaption( 384 452 $min_len 385 453 ? pht('Minimum length of %d characters.', $min_len) 386 454 : null)); 387 - $form->appendChild( 388 - id(new AphrontFormPasswordControl()) 389 - ->setLabel(pht('Confirm Password')) 390 - ->setName('confirm') 391 - ->setError($e_password)); 392 455 } 393 456 394 457 if ($can_edit_email) { ··· 399 462 ->setValue($value_email) 400 463 ->setCaption(PhabricatorUserEmail::describeAllowedAddresses()) 401 464 ->setError($e_email)); 402 - } 403 - 404 - if ($can_edit_realname) { 405 - $form->appendChild( 406 - id(new AphrontFormTextControl()) 407 - ->setLabel(pht('Real Name')) 408 - ->setName('realName') 409 - ->setValue($value_realname) 410 - ->setError($e_realname)); 411 465 } 412 466 413 467 if ($must_set_password) { ··· 459 513 ->setForm($form) 460 514 ->setFormErrors($errors); 461 515 516 + $invite_header = null; 517 + if ($invite) { 518 + $invite_header = $this->renderInviteHeader($invite); 519 + } 520 + 462 521 return $this->buildApplicationPage( 463 522 array( 464 523 $crumbs, 465 524 $welcome_view, 525 + $invite_header, 466 526 $object_box, 467 527 ), 468 528 array(
+15 -2
src/applications/auth/controller/PhabricatorAuthStartController.php
··· 109 109 } 110 110 } 111 111 112 + $invite = $this->loadInvite(); 113 + 112 114 $not_buttons = array(); 113 115 $are_buttons = array(); 114 116 $providers = msort($providers, 'getLoginOrder'); 115 117 foreach ($providers as $provider) { 118 + if ($invite) { 119 + $form = $provider->buildInviteForm($this); 120 + } else { 121 + $form = $provider->buildLoginForm($this); 122 + } 116 123 if ($provider->isLoginFormAButton()) { 117 - $are_buttons[] = $provider->buildLoginForm($this); 124 + $are_buttons[] = $form; 118 125 } else { 119 - $not_buttons[] = $provider->buildLoginForm($this); 126 + $not_buttons[] = $form; 120 127 } 121 128 } 122 129 ··· 159 166 $login_message = PhabricatorEnv::getEnvConfig('auth.login-message'); 160 167 $login_message = phutil_safe_html($login_message); 161 168 169 + $invite_message = null; 170 + if ($invite) { 171 + $invite_message = $this->renderInviteHeader($invite); 172 + } 173 + 162 174 $crumbs = $this->buildApplicationCrumbs(); 163 175 $crumbs->addTextCrumb(pht('Login')); 164 176 $crumbs->setBorder(true); ··· 167 179 array( 168 180 $crumbs, 169 181 $login_message, 182 + $invite_message, 170 183 $out, 171 184 ), 172 185 array(
+1 -1
src/applications/auth/engine/PhabricatorAuthInviteEngine.php
··· 250 250 } 251 251 252 252 private function getLogoutURI() { 253 - return '/auth/logout/'; 253 + return '/logout/'; 254 254 } 255 255 256 256 }
+6
src/applications/auth/provider/PhabricatorAuthProvider.php
··· 158 158 return $this->renderLoginForm($controller->getRequest(), $mode = 'start'); 159 159 } 160 160 161 + public function buildInviteForm(PhabricatorAuthStartController $controller) { 162 + return $this->renderLoginForm($controller->getRequest(), $mode = 'invite'); 163 + } 164 + 161 165 abstract public function processLoginRequest( 162 166 PhabricatorAuthLoginController $controller); 163 167 ··· 401 405 $button_text = pht('Link External Account'); 402 406 } else if ($mode == 'refresh') { 403 407 $button_text = pht('Refresh Account Link'); 408 + } else if ($mode == 'invite') { 409 + $button_text = pht('Register Account'); 404 410 } else if ($this->shouldAllowRegistration()) { 405 411 $button_text = pht('Login or Register'); 406 412 } else {
+23
src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
··· 135 135 return $this->renderPasswordLoginForm($request); 136 136 } 137 137 138 + public function buildInviteForm( 139 + PhabricatorAuthStartController $controller) { 140 + $request = $controller->getRequest(); 141 + $viewer = $request->getViewer(); 142 + 143 + $form = id(new AphrontFormView()) 144 + ->setUser($viewer) 145 + ->addHiddenInput('invite', true) 146 + ->appendChild( 147 + id(new AphrontFormTextControl()) 148 + ->setLabel(pht('Username')) 149 + ->setName('username')); 150 + 151 + $dialog = id(new AphrontDialogView()) 152 + ->setUser($viewer) 153 + ->setTitle(pht('Register an Account')) 154 + ->appendForm($form) 155 + ->setSubmitURI('/auth/register/') 156 + ->addSubmitButton(pht('Continue')); 157 + 158 + return $dialog; 159 + } 160 + 138 161 public function buildLinkForm( 139 162 PhabricatorAuthLinkController $controller) { 140 163 throw new Exception("Password providers can't be linked.");
+22 -2
src/applications/people/editor/PhabricatorUserEditor.php
··· 23 23 */ 24 24 public function createNewUser( 25 25 PhabricatorUser $user, 26 - PhabricatorUserEmail $email) { 26 + PhabricatorUserEmail $email, 27 + $allow_reassign = false) { 27 28 28 29 if ($user->getID()) { 29 30 throw new Exception('User has already been created!'); 30 31 } 31 32 33 + $is_reassign = false; 32 34 if ($email->getID()) { 33 - throw new Exception('Email has already been created!'); 35 + if ($allow_reassign) { 36 + if ($email->getIsPrimary()) { 37 + throw new Exception( 38 + pht( 39 + 'Primary email addresses can not be reassigned.')); 40 + } 41 + $is_reassign = true; 42 + } else { 43 + throw new Exception('Email has already been created!'); 44 + } 34 45 } 35 46 36 47 if (!PhabricatorUser::validateUsername($user->getUsername())) { ··· 70 81 PhabricatorUserLog::ACTION_CREATE); 71 82 $log->setNewValue($email->getAddress()); 72 83 $log->save(); 84 + 85 + if ($is_reassign) { 86 + $log = PhabricatorUserLog::initializeNewLog( 87 + $this->requireActor(), 88 + $user->getPHID(), 89 + PhabricatorUserLog::ACTION_EMAIL_REASSIGN); 90 + $log->setNewValue($email->getAddress()); 91 + $log->save(); 92 + } 73 93 74 94 $user->saveTransaction(); 75 95