diff --git a/src/applications/auth/constants/PhabricatorCookies.php b/src/applications/auth/constants/PhabricatorCookies.php index 8f3f417efe..855537b0d4 100644 --- a/src/applications/auth/constants/PhabricatorCookies.php +++ b/src/applications/auth/constants/PhabricatorCookies.php @@ -1,166 +1,172 @@ getCookie(self::COOKIE_CLIENTID); if (!strlen($value)) { $request->setTemporaryCookie( self::COOKIE_CLIENTID, Filesystem::readRandomCharacters(16)); } } /* -( Next URI Cookie )---------------------------------------------------- */ /** * Set the Next URI cookie. We only write the cookie if it wasn't recently * written, to avoid writing over a real URI with a bunch of "humans.txt" * stuff. See T3793 for discussion. * * @param AphrontRequest Request to write to. * @param string URI to write. * @param bool Write this cookie even if we have a fresh * cookie already. * @return void * * @task next */ public static function setNextURICookie( AphrontRequest $request, $next_uri, $force = false) { if (!$force) { $cookie_value = $request->getCookie(self::COOKIE_NEXTURI); list($set_at, $current_uri) = self::parseNextURICookie($cookie_value); // If the cookie was set within the last 2 minutes, don't overwrite it. // Primarily, this prevents browser requests for resources which do not // exist (like "humans.txt" and various icons) from overwriting a normal // URI like "/feed/". if ($set_at > (time() - 120)) { return; } } $new_value = time().','.$next_uri; $request->setTemporaryCookie(self::COOKIE_NEXTURI, $new_value); } /** * Read the URI out of the Next URI cookie. * * @param AphrontRequest Request to examine. * @return string|null Next URI cookie's URI value. * * @task next */ public static function getNextURICookie(AphrontRequest $request) { $cookie_value = $request->getCookie(self::COOKIE_NEXTURI); list($set_at, $next_uri) = self::parseNextURICookie($cookie_value); return $next_uri; } /** * Parse a Next URI cookie into its components. * * @param string Raw cookie value. * @return list List of timestamp and URI. * * @task next */ private static function parseNextURICookie($cookie) { // Old cookies look like: /uri // New cookies look like: timestamp,/uri if (!strlen($cookie)) { return null; } if (strpos($cookie, ',') !== false) { list($timestamp, $uri) = explode(',', $cookie, 2); return array((int)$timestamp, $uri); } return array(0, $cookie); } } diff --git a/src/applications/auth/controller/PhabricatorAuthController.php b/src/applications/auth/controller/PhabricatorAuthController.php index e62f4e29af..09a2cb63c5 100644 --- a/src/applications/auth/controller/PhabricatorAuthController.php +++ b/src/applications/auth/controller/PhabricatorAuthController.php @@ -1,249 +1,305 @@ buildStandardPageView(); $page->setApplicationName(pht('Login')); $page->setBaseURI('/login/'); $page->setTitle(idx($data, 'title')); $page->appendChild($view); $response = new AphrontWebpageResponse(); return $response->setContent($page->render()); } protected function renderErrorPage($title, array $messages) { $view = new PHUIErrorView(); $view->setTitle($title); $view->setErrors($messages); return $this->buildApplicationPage( $view, array( 'title' => $title, )); } /** * Returns true if this install is newly setup (i.e., there are no user * accounts yet). In this case, we enter a special mode to permit creation * of the first account form the web UI. */ protected function isFirstTimeSetup() { // If there are any auth providers, this isn't first time setup, even if // we don't have accounts. if (PhabricatorAuthProvider::getAllEnabledProviders()) { return false; } // Otherwise, check if there are any user accounts. If not, we're in first // time setup. $any_users = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->setLimit(1) ->execute(); return !$any_users; } /** * Log a user into a web session and return an @{class:AphrontResponse} which * corresponds to continuing the login process. * * Normally, this is a redirect to the validation controller which makes sure * the user's cookies are set. However, event listeners can intercept this * event and do something else if they prefer. * * @param PhabricatorUser User to log the viewer in as. * @return AphrontResponse Response which continues the login process. */ protected function loginUser(PhabricatorUser $user) { $response = $this->buildLoginValidateResponse($user); $session_type = PhabricatorAuthSession::TYPE_WEB; $event_type = PhabricatorEventType::TYPE_AUTH_WILLLOGINUSER; $event_data = array( 'user' => $user, 'type' => $session_type, 'response' => $response, 'shouldLogin' => true, ); $event = id(new PhabricatorEvent($event_type, $event_data)) ->setUser($user); PhutilEventEngine::dispatchEvent($event); $should_login = $event->getValue('shouldLogin'); if ($should_login) { $session_key = id(new PhabricatorAuthSessionEngine()) ->establishSession($session_type, $user->getPHID(), $partial = true); // NOTE: We allow disabled users to login and roadblock them later, so // there's no check for users being disabled here. $request = $this->getRequest(); $request->setCookie( PhabricatorCookies::COOKIE_USERNAME, $user->getUsername()); $request->setCookie( PhabricatorCookies::COOKIE_SESSION, $session_key); $this->clearRegistrationCookies(); } return $event->getValue('response'); } protected function clearRegistrationCookies() { $request = $this->getRequest(); // Clear the registration key. $request->clearCookie(PhabricatorCookies::COOKIE_REGISTRATION); // 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) { $validate_uri = new PhutilURI($this->getApplicationURI('validate/')); $validate_uri->setQueryParam('expect', $user->getUsername()); return id(new AphrontRedirectResponse())->setURI((string)$validate_uri); } protected function renderError($message) { return $this->renderErrorPage( pht('Authentication Error'), array( $message, )); } 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); } // NOTE: We're using the omnipotent user because the actual user may not // be logged in yet, and because we want to tailor an error message to // distinguish between "not usable" and "does not exist". We do explicit // checks later on to make sure this account is valid for the intended // operation. This requires edit permission for completeness and consistency // but it won't actually be meaningfully checked because we're using the // ominpotent user. $account = id(new PhabricatorExternalAccountQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withAccountSecrets(array($account_key)) ->needImages(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$account) { $response = $this->renderError(pht('No valid linkable account.')); return array($account, $provider, $response); } if ($account->getUserPHID()) { if ($account->getUserPHID() != $viewer->getPHID()) { $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( PhabricatorCookies::COOKIE_REGISTRATION); // 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); } + 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); + } + } diff --git a/src/applications/auth/controller/PhabricatorAuthInviteController.php b/src/applications/auth/controller/PhabricatorAuthInviteController.php index 360e9f1e80..5a0dd72813 100644 --- a/src/applications/auth/controller/PhabricatorAuthInviteController.php +++ b/src/applications/auth/controller/PhabricatorAuthInviteController.php @@ -1,58 +1,63 @@ getViewer(); $engine = id(new PhabricatorAuthInviteEngine()) ->setViewer($viewer); if ($request->isFormPost()) { $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()) ->appendParagraph($ex->getBody()); $submit_text = $ex->getSubmitButtonText(); if ($submit_text) { $response->addSubmitButton($submit_text); } $submit_uri = $ex->getSubmitButtonURI(); if ($submit_uri) { $response->setSubmitURI($submit_uri); } $cancel_uri = $ex->getCancelButtonURI(); $cancel_text = $ex->getCancelButtonText(); if ($cancel_uri && $cancel_text) { $response->addCancelButton($cancel_uri, $cancel_text); } else if ($cancel_uri) { $response->addCancelButton($cancel_uri); } return $response; } catch (PhabricatorAuthInviteRegisteredException $ex) { // We're all set on processing this invite, just send the user home. 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/'); } } diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php index a359305b6b..413fcea4b0 100644 --- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php +++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php @@ -1,598 +1,658 @@ accountKey = idx($data, 'akey'); } public function processRequest() { $request = $this->getRequest(); if ($request->getUser()->isLoggedIn()) { return $this->renderError(pht('You are already logged in.')); } $is_setup = false; if (strlen($this->accountKey)) { $result = $this->loadAccountForRegistrationOrLinking($this->accountKey); list($account, $provider, $response) = $result; $is_default = false; } else if ($this->isFirstTimeSetup()) { list($account, $provider, $response) = $this->loadSetupAccount(); $is_default = true; $is_setup = true; } else { list($account, $provider, $response) = $this->loadDefaultAccount(); $is_default = true; } if ($response) { return $response; } - if (!$provider->shouldAllowRegistration()) { + $invite = $this->loadInvite(); - // 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. + 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. - 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(); $default_username = $account->getUsername(); $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 // configured application emails $application_email = id(new PhabricatorMetaMTAApplicationEmailQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withAddresses(array($default_email)) ->executeOne(); if ($application_email) { $default_email = null; } } if ($default_email !== null) { // If the account source provided an email, but it's not allowed by // the configuration, roadblock the user. Previously, we let the user // pick a valid email address instead, but this does not align well with // user expectation and it's not clear the cases it enables are valuable. // See discussion in T3472. if (!PhabricatorUserEmail::isAllowedAddress($default_email)) { return $this->renderError( array( pht( 'The account you are attempting to register with has an invalid '. 'email address (%s). This Phabricator install only allows '. 'registration with specific email addresses:', $default_email), phutil_tag('br'), phutil_tag('br'), PhabricatorUserEmail::describeAllowedAddresses(), )); } // If the account source provided an email, but another account already // has that email, just pretend we didn't get an email. // TODO: See T3472. if ($default_email !== null) { $same_email = id(new PhabricatorUserEmail())->loadOneWhere( '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; + } } } } $profile = id(new PhabricatorRegistrationProfile()) ->setDefaultUsername($default_username) ->setDefaultEmail($default_email) ->setDefaultRealName($default_realname) ->setCanEditUsername(true) ->setCanEditEmail(($default_email === null)) ->setCanEditRealName(true) ->setShouldVerifyEmail(false); $event_type = PhabricatorEventType::TYPE_AUTH_WILLREGISTERUSER; $event_data = array( 'account' => $account, 'profile' => $profile, ); $event = id(new PhabricatorEvent($event_type, $event_data)) ->setUser($user); PhutilEventEngine::dispatchEvent($event); $default_username = $profile->getDefaultUsername(); $default_email = $profile->getDefaultEmail(); $default_realname = $profile->getDefaultRealName(); $can_edit_username = $profile->getCanEditUsername(); $can_edit_email = $profile->getCanEditEmail(); $can_edit_realname = $profile->getCanEditRealName(); $must_set_password = $provider->shouldRequireRegistrationPassword(); $can_edit_anything = $profile->getCanEditAnything() || $must_set_password; $force_verify = $profile->getShouldVerifyEmail(); // Automatically verify the administrator's email address during first-time // setup. if ($is_setup) { $force_verify = true; } $value_username = $default_username; $value_realname = $default_realname; $value_email = $default_email; $value_password = null; $errors = array(); $require_real_name = PhabricatorEnv::getEnvConfig('user.require-real-name'); $e_username = strlen($value_username) ? null : true; $e_realname = $require_real_name ? true : null; $e_email = strlen($value_email) ? null : true; $e_password = true; $e_captcha = true; $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) { $e_captcha = pht('Again'); $captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request); if (!$captcha_ok) { $errors[] = pht('Captcha response is incorrect, try again.'); $e_captcha = pht('Invalid'); } } if ($can_edit_username) { $value_username = $request->getStr('username'); if (!strlen($value_username)) { $e_username = pht('Required'); $errors[] = pht('Username is required.'); } else if (!PhabricatorUser::validateUsername($value_username)) { $e_username = pht('Invalid'); $errors[] = PhabricatorUser::describeValidUsername(); } else { $e_username = null; } } if ($must_set_password) { $value_password = $request->getStr('password'); $value_confirm = $request->getStr('confirm'); if (!strlen($value_password)) { $e_password = pht('Required'); $errors[] = pht('You must choose a password.'); } else if ($value_password !== $value_confirm) { $e_password = pht('No Match'); $errors[] = pht('Password and confirmation must match.'); } else if (strlen($value_password) < $min_len) { $e_password = pht('Too Short'); $errors[] = pht( 'Password is too short (must be at least %d characters long).', $min_len); } else if ( PhabricatorCommonPasswords::isCommonPassword($value_password)) { $e_password = pht('Very Weak'); $errors[] = pht( 'Password is pathologically weak. This password is one of the '. 'most common passwords in use, and is extremely easy for '. 'attackers to guess. You must choose a stronger password.'); } else { $e_password = null; } } if ($can_edit_email) { $value_email = $request->getStr('email'); if (!strlen($value_email)) { $e_email = pht('Required'); $errors[] = pht('Email is required.'); } else if (!PhabricatorUserEmail::isValidAddress($value_email)) { $e_email = pht('Invalid'); $errors[] = PhabricatorUserEmail::describeValidAddresses(); } else if (!PhabricatorUserEmail::isAllowedAddress($value_email)) { $e_email = pht('Disallowed'); $errors[] = PhabricatorUserEmail::describeAllowedAddresses(); } else { $e_email = null; } } if ($can_edit_realname) { $value_realname = $request->getStr('realName'); if (!strlen($value_realname) && $require_real_name) { $e_realname = pht('Required'); $errors[] = pht('Real name is required.'); } else { $e_realname = null; } } if (!$errors) { $image = $this->loadProfilePicture($account); if ($image) { $user->setProfileImagePHID($image->getPHID()); } 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'); } if ($must_approve) { $user->setIsApproved(0); } else { $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); } if ($is_setup) { $editor->makeAdminUser($user, true); } $account->setUserPHID($user->getPHID()); $provider->willRegisterAccount($account); $account->save(); $user->saveTransaction(); if (!$email_obj->getIsVerified()) { $email_obj->sendVerificationEmail($user); } if ($must_approve) { $this->sendWaitingForApprovalEmail($user); } + if ($invite) { + $invite->setAcceptedByPHID($user->getPHID())->save(); + } + return $this->loginUser($user); } catch (AphrontDuplicateKeyQueryException $exception) { $same_username = id(new PhabricatorUser())->loadOneWhere( 'userName = %s', $user->getUserName()); $same_email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $value_email); if ($same_username) { $e_username = pht('Duplicate'); $errors[] = pht('Another user already has that username.'); } if ($same_email) { // TODO: See T3340. $e_email = pht('Duplicate'); $errors[] = pht('Another user already has that email.'); } if (!$same_username && !$same_email) { throw $exception; } } } unset($unguarded); } $form = id(new AphrontFormView()) ->setUser($request->getUser()); if (!$is_default) { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('External Account')) ->setValue( id(new PhabricatorAuthAccountView()) ->setUser($request->getUser()) ->setExternalAccount($account) ->setAuthProvider($provider))); } if ($can_edit_username) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Phabricator Username')) ->setName('username') ->setValue($value_username) ->setError($e_username)); } else { $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel(pht('Phabricator Username')) ->setValue($value_username) ->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) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') ->setValue($value_email) ->setCaption(PhabricatorUserEmail::describeAllowedAddresses()) ->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()) ->setLabel(pht('Captcha')) ->setError($e_captcha)); } $submit = id(new AphrontFormSubmitControl()); if ($is_setup) { $submit ->setValue(pht('Create Admin Account')); } else { $submit ->addCancelButton($this->getApplicationURI('start/')) ->setValue(pht('Register Phabricator Account')); } $form->appendChild($submit); $crumbs = $this->buildApplicationCrumbs(); if ($is_setup) { $crumbs->addTextCrumb(pht('Setup Admin Account')); $title = pht('Welcome to Phabricator'); } else { $crumbs->addTextCrumb(pht('Register')); $crumbs->addTextCrumb($provider->getProviderName()); $title = pht('Phabricator Registration'); } $welcome_view = null; if ($is_setup) { $welcome_view = id(new PHUIErrorView()) ->setSeverity(PHUIErrorView::SEVERITY_NOTICE) ->setTitle(pht('Welcome to Phabricator')) ->appendChild( pht( 'Installation is complete. Register your administrator account '. 'below to log in. You will be able to configure options and add '. 'other authentication mechanisms (like LDAP or OAuth) later on.')); } $object_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->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( 'title' => $title, )); } private function loadDefaultAccount() { $providers = PhabricatorAuthProvider::getAllEnabledProviders(); $account = null; $provider = null; $response = null; foreach ($providers as $key => $candidate_provider) { if (!$candidate_provider->shouldAllowRegistration()) { unset($providers[$key]); continue; } if (!$candidate_provider->isDefaultRegistrationProvider()) { unset($providers[$key]); } } if (!$providers) { $response = $this->renderError( pht( 'There are no configured default registration providers.')); return array($account, $provider, $response); } else if (count($providers) > 1) { $response = $this->renderError( pht( 'There are too many configured default registration providers.')); return array($account, $provider, $response); } $provider = head($providers); $account = $provider->getDefaultExternalAccount(); return array($account, $provider, $response); } private function loadSetupAccount() { $provider = new PhabricatorPasswordAuthProvider(); $provider->attachProviderConfig( id(new PhabricatorAuthProviderConfig()) ->setShouldAllowRegistration(1) ->setShouldAllowLogin(1) ->setIsEnabled(true)); $account = $provider->getDefaultExternalAccount(); $response = null; return array($account, $provider, $response); } private function loadProfilePicture(PhabricatorExternalAccount $account) { $phid = $account->getProfileImagePHID(); if (!$phid) { return null; } // NOTE: Use of omnipotent user is okay here because the registering user // can not control the field value, and we can't use their user object to // do meaningful policy checks anyway since they have not registered yet. // Reaching this means the user holds the account secret key and the // registration secret key, and thus has permission to view the image. $file = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($phid)) ->executeOne(); if (!$file) { return null; } try { $xformer = new PhabricatorImageTransformer(); return $xformer->executeProfileTransform( $file, $width = 50, $min_height = 50, $max_height = 50); } catch (Exception $ex) { phlog($ex); return null; } } protected function renderError($message) { return $this->renderErrorPage( pht('Registration Failed'), array($message)); } private function sendWaitingForApprovalEmail(PhabricatorUser $user) { $title = '[Phabricator] '.pht( 'New User "%s" Awaiting Approval', $user->getUsername()); $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection( pht( 'Newly registered user "%s" is awaiting account approval by an '. 'administrator.', $user->getUsername())); $body->addLinkSection( pht('APPROVAL QUEUE'), PhabricatorEnv::getProductionURI( '/people/query/approval/')); $body->addLinkSection( pht('DISABLE APPROVAL QUEUE'), PhabricatorEnv::getProductionURI( '/config/edit/auth.require-approval/')); $admins = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIsAdmin(true) ->execute(); if (!$admins) { return; } $mail = id(new PhabricatorMetaMTAMail()) ->addTos(mpull($admins, 'getPHID')) ->setSubject($title) ->setBody($body->render()) ->saveAndSend(); } } diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php index 34ea44030d..7ea10690e5 100644 --- a/src/applications/auth/controller/PhabricatorAuthStartController.php +++ b/src/applications/auth/controller/PhabricatorAuthStartController.php @@ -1,230 +1,243 @@ getUser(); if ($viewer->isLoggedIn()) { // Kick the user home if they are already logged in. return id(new AphrontRedirectResponse())->setURI('/'); } if ($request->isAjax()) { return $this->processAjaxRequest(); } if ($request->isConduit()) { return $this->processConduitRequest(); } // If the user gets this far, they aren't logged in, so if they have a // user session token we can conclude that it's invalid: if it was valid, // they'd have been logged in above and never made it here. Try to clear // it and warn the user they may need to nuke their cookies. $session_token = $request->getCookie(PhabricatorCookies::COOKIE_SESSION); if (strlen($session_token)) { $kind = PhabricatorAuthSessionEngine::getSessionKindFromToken( $session_token); switch ($kind) { case PhabricatorAuthSessionEngine::KIND_ANONYMOUS: // If this is an anonymous session. It's expected that they won't // be logged in, so we can just continue. break; default: // The session cookie is invalid, so clear it. $request->clearCookie(PhabricatorCookies::COOKIE_USERNAME); $request->clearCookie(PhabricatorCookies::COOKIE_SESSION); return $this->renderError( pht( 'Your login session is invalid. Try reloading the page and '. 'logging in again. If that does not work, clear your browser '. 'cookies.')); } } $providers = PhabricatorAuthProvider::getAllEnabledProviders(); foreach ($providers as $key => $provider) { if (!$provider->shouldAllowLogin()) { unset($providers[$key]); } } if (!$providers) { if ($this->isFirstTimeSetup()) { // If this is a fresh install, let the user register their admin // account. return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI('/register/')); } return $this->renderError( pht( 'This Phabricator install is not configured with any enabled '. 'authentication providers which can be used to log in. If you '. 'have accidentally locked yourself out by disabling all providers, '. 'you can use `phabricator/bin/auth recover ` to '. 'recover access to an administrative account.')); } $next_uri = $request->getStr('next'); if (!strlen($next_uri)) { if ($this->getDelegatingController()) { // Only set a next URI from the request path if this controller was // delegated to, which happens when a user tries to view a page which // requires them to login. // If this controller handled the request directly, we're on the main // login page, and never want to redirect the user back here after they // login. $next_uri = (string)$this->getRequest()->getRequestURI(); } } if (!$request->isFormPost()) { if (strlen($next_uri)) { PhabricatorCookies::setNextURICookie($request, $next_uri); } PhabricatorCookies::setClientIDCookie($request); } if (!$request->getURIData('loggedout') && count($providers) == 1) { $auto_login_provider = head($providers); $auto_login_config = $auto_login_provider->getProviderConfig(); if ($auto_login_provider instanceof PhabricatorPhabricatorAuthProvider && $auto_login_config->getShouldAutoLogin()) { $auto_login_adapter = $provider->getAdapter(); $auto_login_adapter->setState($provider->getAuthCSRFCode($request)); return id(new AphrontRedirectResponse()) ->setIsExternal(true) ->setURI($provider->getAdapter()->getAuthenticateURI()); } } + $invite = $this->loadInvite(); + $not_buttons = array(); $are_buttons = array(); $providers = msort($providers, 'getLoginOrder'); foreach ($providers as $provider) { + if ($invite) { + $form = $provider->buildInviteForm($this); + } else { + $form = $provider->buildLoginForm($this); + } if ($provider->isLoginFormAButton()) { - $are_buttons[] = $provider->buildLoginForm($this); + $are_buttons[] = $form; } else { - $not_buttons[] = $provider->buildLoginForm($this); + $not_buttons[] = $form; } } $out = array(); $out[] = $not_buttons; if ($are_buttons) { require_celerity_resource('auth-css'); foreach ($are_buttons as $key => $button) { $are_buttons[$key] = phutil_tag( 'div', array( 'class' => 'phabricator-login-button mmb', ), $button); } // If we only have one button, add a second pretend button so that we // always have two columns. This makes it easier to get the alignments // looking reasonable. if (count($are_buttons) == 1) { $are_buttons[] = null; } $button_columns = id(new AphrontMultiColumnView()) ->setFluidLayout(true); $are_buttons = array_chunk($are_buttons, ceil(count($are_buttons) / 2)); foreach ($are_buttons as $column) { $button_columns->addColumn($column); } $out[] = phutil_tag( 'div', array( 'class' => 'phabricator-login-buttons', ), $button_columns); } $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); return $this->buildApplicationPage( array( $crumbs, $login_message, + $invite_message, $out, ), array( 'title' => pht('Login to Phabricator'), )); } private function processAjaxRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); // We end up here if the user clicks a workflow link that they need to // login to use. We give them a dialog saying "You need to login...". if ($request->isDialogFormPost()) { return id(new AphrontRedirectResponse())->setURI( $request->getRequestURI()); } $dialog = new AphrontDialogView(); $dialog->setUser($viewer); $dialog->setTitle(pht('Login Required')); $dialog->appendChild(pht('You must login to continue.')); $dialog->addSubmitButton(pht('Login')); $dialog->addCancelButton('/'); return id(new AphrontDialogResponse())->setDialog($dialog); } private function processConduitRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); // A common source of errors in Conduit client configuration is getting // the request path wrong. The client will end up here, so make some // effort to give them a comprehensible error message. $request_path = $this->getRequest()->getPath(); $conduit_path = '/api/'; $example_path = '/api/conduit.ping'; $message = pht( 'ERROR: You are making a Conduit API request to "%s", but the correct '. 'HTTP request path to use in order to access a COnduit method is "%s" '. '(for example, "%s"). Check your configuration.', $request_path, $conduit_path, $example_path); return id(new AphrontPlainTextResponse())->setContent($message); } protected function renderError($message) { return $this->renderErrorPage( pht('Authentication Failure'), array($message)); } } diff --git a/src/applications/auth/engine/PhabricatorAuthInviteEngine.php b/src/applications/auth/engine/PhabricatorAuthInviteEngine.php index 2745bb51e0..bbdb6000f8 100644 --- a/src/applications/auth/engine/PhabricatorAuthInviteEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthInviteEngine.php @@ -1,256 +1,256 @@ viewer = $viewer; return $this; } public function getViewer() { if (!$this->viewer) { throw new Exception(pht('Call setViewer() before getViewer()!')); } return $this->viewer; } public function setUserHasConfirmedVerify($confirmed) { $this->userHasConfirmedVerify = $confirmed; return $this; } private function shouldVerify() { return $this->userHasConfirmedVerify; } public function processInviteCode($code) { $viewer = $this->getViewer(); $invite = id(new PhabricatorAuthInviteQuery()) ->setViewer($viewer) ->withVerificationCodes(array($code)) ->executeOne(); if (!$invite) { throw id(new PhabricatorAuthInviteInvalidException( pht('Bad Invite Code'), pht( 'The invite code in the link you clicked is invalid. Check that '. 'you followed the link correctly.'))) ->setCancelButtonURI('/') ->setCancelButtonText(pht('Curses!')); } $accepted_phid = $invite->getAcceptedByPHID(); if ($accepted_phid) { if ($accepted_phid == $viewer->getPHID()) { throw id(new PhabricatorAuthInviteInvalidException( pht('Already Accepted'), pht( 'You have already accepted this invitation.'))) ->setCancelButtonURI('/') ->setCancelButtonText(pht('Awesome')); } else { throw id(new PhabricatorAuthInviteInvalidException( pht('Already Accepted'), pht( 'The invite code in the link you clicked has already '. 'been accepted.'))) ->setCancelButtonURI('/') ->setCancelButtonText(pht('Continue')); } } $email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $invite->getEmailAddress()); if ($viewer->isLoggedIn()) { $this->handleLoggedInInvite($invite, $viewer, $email); } if ($email) { $other_user = $this->loadUserForEmail($email); if ($email->getIsVerified()) { throw id(new PhabricatorAuthInviteLoginException( pht('Already Registered'), pht( 'The email address you just clicked a link from is already '. 'verified and associated with a registered account (%s). Log '. 'in to continue.', phutil_tag('strong', array(), $other_user->getName())))) ->setCancelButtonText(pht('Log In')) ->setCancelButtonURI($this->getLoginURI()); } else if ($email->getIsPrimary()) { throw id(new PhabricatorAuthInviteLoginException( pht('Already Registered'), pht( 'The email address you just clicked a link from is already '. 'the primary email address for a registered account (%s). Log '. 'in to continue.', phutil_tag('strong', array(), $other_user->getName())))) ->setCancelButtonText(pht('Log In')) ->setCancelButtonURI($this->getLoginURI()); } else if (!$this->shouldVerify()) { throw id(new PhabricatorAuthInviteVerifyException( pht('Already Associated'), pht( 'The email address you just clicked a link from is already '. 'associated with a registered account (%s), but is not '. 'verified. Log in to that account to continue. If you can not '. 'log in, you can register a new account.', phutil_tag('strong', array(), $other_user->getName())))) ->setCancelButtonText(pht('Log In')) ->setCancelButtonURI($this->getLoginURI()) ->setSubmitButtonText(pht('Register New Account')); } else { // NOTE: The address is not verified and not a primary address, so // we will eventually steal it if the user completes registration. } } // The invite and email address are OK, but the user needs to register. return $invite; } private function handleLoggedInInvite( PhabricatorAuthInvite $invite, PhabricatorUser $viewer, PhabricatorUserEmail $email = null) { if ($email && ($email->getUserPHID() !== $viewer->getPHID())) { $other_user = $this->loadUserForEmail($email); if ($email->getIsVerified()) { throw id(new PhabricatorAuthInviteAccountException( pht('Wrong Account'), pht( 'You are logged in as %s, but the email address you just '. 'clicked a link from is already verified and associated '. 'with another account (%s). Switch accounts, then try again.', phutil_tag('strong', array(), $viewer->getUsername()), phutil_tag('strong', array(), $other_user->getName())))) ->setSubmitButtonText(pht('Log Out')) ->setSubmitButtonURI($this->getLogoutURI()) ->setCancelButtonURI('/'); } else if ($email->getIsPrimary()) { // NOTE: We never steal primary addresses from other accounts, even // if they are unverified. This would leave the other account with // no address. Users can use password recovery to access the other // account if they really control the address. throw id(new PhabricatorAuthInviteAccountException( pht('Wrong Acount'), pht( 'You are logged in as %s, but the email address you just '. 'clicked a link from is already the primary email address '. 'for another account (%s). Switch accounts, then try again.', phutil_tag('strong', array(), $viewer->getUsername()), phutil_tag('strong', array(), $other_user->getName())))) ->setSubmitButtonText(pht('Log Out')) ->setSubmitButtonURI($this->getLogoutURI()) ->setCancelButtonURI('/'); } else if (!$this->shouldVerify()) { throw id(new PhabricatorAuthInviteVerifyException( pht('Verify Email'), pht( 'You are logged in as %s, but the email address (%s) you just '. 'clicked a link from is already associated with another '. 'account (%s). You can log out to switch accounts, or verify '. 'the address and attach it to your current account. Attach '. 'email address %s to user account %s?', phutil_tag('strong', array(), $viewer->getUsername()), phutil_tag('strong', array(), $invite->getEmailAddress()), phutil_tag('strong', array(), $other_user->getName()), phutil_tag('strong', array(), $invite->getEmailAddress()), phutil_tag('strong', array(), $viewer->getUsername())))) ->setSubmitButtonText( pht( 'Verify %s', $invite->getEmailAddress())) ->setCancelButtonText(pht('Log Out')) ->setCancelButtonURI($this->getLogoutURI()); } } if (!$email) { $email = id(new PhabricatorUserEmail()) ->setAddress($invite->getEmailAddress()) ->setIsVerified(0) ->setIsPrimary(0); } if (!$email->getIsVerified()) { // We're doing this check here so that we can verify the address if // it's already attached to the viewer's account, just not verified. if (!$this->shouldVerify()) { throw id(new PhabricatorAuthInviteVerifyException( pht('Verify Email'), pht( 'Verify this email address (%s) and attach it to your '. 'account (%s)?', phutil_tag('strong', array(), $invite->getEmailAddress()), phutil_tag('strong', array(), $viewer->getUsername())))) ->setSubmitButtonText( pht( 'Verify %s', $invite->getEmailAddress())) ->setCancelButtonURI('/'); } $editor = id(new PhabricatorUserEditor()) ->setActor($viewer); // If this is a new email, add it to the user's account. if (!$email->getUserPHID()) { $editor->addEmail($viewer, $email); } // If another user added this email (but has not verified it), // take it from them. $editor->reassignEmail($viewer, $email); $editor->verifyEmail($viewer, $email); } $invite->setAcceptedByPHID($viewer->getPHID()); $invite->save(); // If we make it here, the user was already logged in with the email // address attached to their account and verified, or we attached it to // their account (if it was not already attached) and verified it. throw new PhabricatorAuthInviteRegisteredException(); } private function loadUserForEmail(PhabricatorUserEmail $email) { $user = id(new PhabricatorHandleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($email->getUserPHID())) ->executeOne(); if (!$user) { throw new Exception( pht( 'Email record ("%s") has bad associated user PHID ("%s").', $email->getAddress(), $email->getUserPHID())); } return $user; } private function getLoginURI() { return '/auth/start/'; } private function getLogoutURI() { - return '/auth/logout/'; + return '/logout/'; } } diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php index 0745fd2afc..ef3c024bb0 100644 --- a/src/applications/auth/provider/PhabricatorAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorAuthProvider.php @@ -1,486 +1,492 @@ providerConfig = $config; return $this; } public function hasProviderConfig() { return (bool)$this->providerConfig; } public function getProviderConfig() { if ($this->providerConfig === null) { throw new Exception( 'Call attachProviderConfig() before getProviderConfig()!'); } return $this->providerConfig; } public function getConfigurationHelp() { return null; } public function getDefaultProviderConfig() { return id(new PhabricatorAuthProviderConfig()) ->setProviderClass(get_class($this)) ->setIsEnabled(1) ->setShouldAllowLogin(1) ->setShouldAllowRegistration(1) ->setShouldAllowLink(1) ->setShouldAllowUnlink(1); } public function getNameForCreate() { return $this->getProviderName(); } public function getDescriptionForCreate() { return null; } public function getProviderKey() { return $this->getAdapter()->getAdapterKey(); } public function getProviderType() { return $this->getAdapter()->getAdapterType(); } public function getProviderDomain() { return $this->getAdapter()->getAdapterDomain(); } public static function getAllBaseProviders() { static $providers; if ($providers === null) { $objects = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); $providers = $objects; } return $providers; } public static function getAllProviders() { static $providers; if ($providers === null) { $objects = self::getAllBaseProviders(); $configs = id(new PhabricatorAuthProviderConfigQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->execute(); $providers = array(); foreach ($configs as $config) { if (!isset($objects[$config->getProviderClass()])) { // This configuration is for a provider which is not installed. continue; } $object = clone $objects[$config->getProviderClass()]; $object->attachProviderConfig($config); $key = $object->getProviderKey(); if (isset($providers[$key])) { throw new Exception( pht( "Two authentication providers use the same provider key ". "('%s'). Each provider must be identified by a unique key.", $key)); } $providers[$key] = $object; } } return $providers; } public static function getAllEnabledProviders() { $providers = self::getAllProviders(); foreach ($providers as $key => $provider) { if (!$provider->isEnabled()) { unset($providers[$key]); } } return $providers; } public static function getEnabledProviderByKey($provider_key) { return idx(self::getAllEnabledProviders(), $provider_key); } abstract public function getProviderName(); abstract public function getAdapter(); public function isEnabled() { return $this->getProviderConfig()->getIsEnabled(); } public function shouldAllowLogin() { return $this->getProviderConfig()->getShouldAllowLogin(); } public function shouldAllowRegistration() { return $this->getProviderConfig()->getShouldAllowRegistration(); } public function shouldAllowAccountLink() { return $this->getProviderConfig()->getShouldAllowLink(); } public function shouldAllowAccountUnlink() { return $this->getProviderConfig()->getShouldAllowUnlink(); } public function shouldTrustEmails() { return $this->shouldAllowEmailTrustConfiguration() && $this->getProviderConfig()->getShouldTrustEmails(); } /** * Should we allow the adapter to be marked as "trusted". This is true for * all adapters except those that allow the user to type in emails (see * @{class:PhabricatorPasswordAuthProvider}). */ public function shouldAllowEmailTrustConfiguration() { return true; } public function buildLoginForm(PhabricatorAuthStartController $controller) { 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); public function buildLinkForm(PhabricatorAuthLinkController $controller) { return $this->renderLoginForm($controller->getRequest(), $mode = 'link'); } public function shouldAllowAccountRefresh() { return true; } public function buildRefreshForm( PhabricatorAuthLinkController $controller) { return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh'); } protected function renderLoginForm(AphrontRequest $request, $mode) { throw new PhutilMethodNotImplementedException(); } public function createProviders() { return array($this); } protected function willSaveAccount(PhabricatorExternalAccount $account) { return; } public function willRegisterAccount(PhabricatorExternalAccount $account) { return; } protected function loadOrCreateAccount($account_id) { if (!strlen($account_id)) { throw new Exception('loadOrCreateAccount(...): empty account ID!'); } $adapter = $this->getAdapter(); $adapter_class = get_class($adapter); if (!strlen($adapter->getAdapterType())) { throw new Exception( "AuthAdapter (of class '{$adapter_class}') has an invalid ". "implementation: no adapter type."); } if (!strlen($adapter->getAdapterDomain())) { throw new Exception( "AuthAdapter (of class '{$adapter_class}') has an invalid ". "implementation: no adapter domain."); } $account = id(new PhabricatorExternalAccount())->loadOneWhere( 'accountType = %s AND accountDomain = %s AND accountID = %s', $adapter->getAdapterType(), $adapter->getAdapterDomain(), $account_id); if (!$account) { $account = id(new PhabricatorExternalAccount()) ->setAccountType($adapter->getAdapterType()) ->setAccountDomain($adapter->getAdapterDomain()) ->setAccountID($account_id); } $account->setUsername($adapter->getAccountName()); $account->setRealName($adapter->getAccountRealName()); $account->setEmail($adapter->getAccountEmail()); $account->setAccountURI($adapter->getAccountURI()); $account->setProfileImagePHID(null); $image_uri = $adapter->getAccountImageURI(); if ($image_uri) { try { $name = PhabricatorSlug::normalize($this->getProviderName()); $name = $name.'-profile.jpg'; // TODO: If the image has not changed, we do not need to make a new // file entry for it, but there's no convenient way to do this with // PhabricatorFile right now. The storage will get shared, so the impact // here is negligible. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $image_file = PhabricatorFile::newFromFileDownload( $image_uri, array( 'name' => $name, 'canCDN' => true, )); unset($unguarded); if ($image_file) { $account->setProfileImagePHID($image_file->getPHID()); } } catch (Exception $ex) { // Log this but proceed, it's not especially important that we // be able to pull profile images. phlog($ex); } } $this->willSaveAccount($account); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $account->save(); unset($unguarded); return $account; } public function getLoginURI() { $app = PhabricatorApplication::getByClass('PhabricatorAuthApplication'); return $app->getApplicationURI('/login/'.$this->getProviderKey().'/'); } public function getSettingsURI() { return '/settings/panel/external/'; } public function getStartURI() { $app = PhabricatorApplication::getByClass('PhabricatorAuthApplication'); $uri = $app->getApplicationURI('/start/'); return $uri; } public function isDefaultRegistrationProvider() { return false; } public function shouldRequireRegistrationPassword() { return false; } public function getDefaultExternalAccount() { throw new PhutilMethodNotImplementedException(); } public function getLoginOrder() { return '500-'.$this->getProviderName(); } protected function getLoginIcon() { return 'Generic'; } public function isLoginFormAButton() { return false; } public function renderConfigPropertyTransactionTitle( PhabricatorAuthProviderConfigTransaction $xaction) { return null; } public function readFormValuesFromProvider() { return array(); } public function readFormValuesFromRequest(AphrontRequest $request) { return array(); } public function processEditForm( AphrontRequest $request, array $values) { $errors = array(); $issues = array(); return array($errors, $issues, $values); } public function extendEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues) { return; } public function willRenderLinkedAccount( PhabricatorUser $viewer, PHUIObjectItemView $item, PhabricatorExternalAccount $account) { $account_view = id(new PhabricatorAuthAccountView()) ->setExternalAccount($account) ->setAuthProvider($this); $item->appendChild( phutil_tag( 'div', array( 'class' => 'mmr mml mst mmb', ), $account_view)); } /** * Return true to use a two-step configuration (setup, configure) instead of * the default single-step configuration. In practice, this means that * creating a new provider instance will redirect back to the edit page * instead of the provider list. * * @return bool True if this provider uses two-step configuration. */ public function hasSetupStep() { return false; } /** * Render a standard login/register button element. * * The `$attributes` parameter takes these keys: * * - `uri`: URI the button should take the user to when clicked. * - `method`: Optional HTTP method the button should use, defaults to GET. * * @param AphrontRequest HTTP request. * @param string Request mode string. * @param map Additional parameters, see above. * @return wild Login button. */ protected function renderStandardLoginButton( AphrontRequest $request, $mode, array $attributes = array()) { PhutilTypeSpec::checkMap( $attributes, array( 'method' => 'optional string', 'uri' => 'string', 'sigil' => 'optional string', )); $viewer = $request->getUser(); $adapter = $this->getAdapter(); if ($mode == 'link') { $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 { $button_text = pht('Login'); } $icon = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) ->setSpriteIcon($this->getLoginIcon()); $button = id(new PHUIButtonView()) ->setSize(PHUIButtonView::BIG) ->setColor(PHUIButtonView::GREY) ->setIcon($icon) ->setText($button_text) ->setSubtext($this->getProviderName()); $uri = $attributes['uri']; $uri = new PhutilURI($uri); $params = $uri->getQueryParams(); $uri->setQueryParams(array()); $content = array($button); foreach ($params as $key => $value) { $content[] = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => $key, 'value' => $value, )); } return phabricator_form( $viewer, array( 'method' => idx($attributes, 'method', 'GET'), 'action' => (string)$uri, 'sigil' => idx($attributes, 'sigil'), ), $content); } public function renderConfigurationFooter() { return null; } public function getAuthCSRFCode(AphrontRequest $request) { $phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID); if (!strlen($phcid)) { throw new Exception( pht( 'Your browser did not submit a "%s" cookie with client state '. 'information in the request. Check that cookies are enabled. '. 'If this problem persists, you may need to clear your cookies.', PhabricatorCookies::COOKIE_CLIENTID)); } return PhabricatorHash::digest($phcid); } protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) { $expect = $this->getAuthCSRFCode($request); if (!strlen($actual)) { throw new Exception( pht( 'The authentication provider did not return a client state '. 'parameter in its response, but one was expected. If this '. 'problem persists, you may need to clear your cookies.')); } if ($actual !== $expect) { throw new Exception( pht( 'The authentication provider did not return the correct client '. 'state parameter in its response. If this problem persists, you may '. 'need to clear your cookies.')); } } } diff --git a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php index 1d583e1096..c329a97b38 100644 --- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php @@ -1,355 +1,378 @@ 'color: #009900', ), pht('Yes')); $no = phutil_tag( 'strong', array( 'style' => 'color: #990000', ), pht('Not Installed')); $best_hasher_name = null; try { $best_hasher = PhabricatorPasswordHasher::getBestHasher(); $best_hasher_name = $best_hasher->getHashName(); } catch (PhabricatorPasswordHasherUnavailableException $ex) { // There are no suitable hashers. The user might be able to enable some, // so we don't want to fatal here. We'll fatal when users try to actually // use this stuff if it isn't fixed before then. Until then, we just // don't highlight a row. In practice, at least one hasher should always // be available. } $rows = array(); $rowc = array(); foreach ($hashers as $hasher) { $is_installed = $hasher->canHashPasswords(); $rows[] = array( $hasher->getHumanReadableName(), $hasher->getHashName(), $hasher->getHumanReadableStrength(), ($is_installed ? $yes : $no), ($is_installed ? null : $hasher->getInstallInstructions()), ); $rowc[] = ($best_hasher_name == $hasher->getHashName()) ? 'highlighted' : null; } $table = new AphrontTableView($rows); $table->setRowClasses($rowc); $table->setHeaders( array( pht('Algorithm'), pht('Name'), pht('Strength'), pht('Installed'), pht('Install Instructions'), )); $table->setColumnClasses( array( '', '', '', '', 'wide', )); $header = id(new PHUIHeaderView()) ->setHeader(pht('Password Hash Algorithms')) ->setSubheader( pht( 'Stronger algorithms are listed first. The highlighted algorithm '. 'will be used when storing new hashes. Older hashes will be '. 'upgraded to the best algorithm over time.')); return id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($table); } public function getDescriptionForCreate() { return pht( 'Allow users to login or register using a username and password.'); } public function getAdapter() { if (!$this->adapter) { $adapter = new PhutilEmptyAuthAdapter(); $adapter->setAdapterType('password'); $adapter->setAdapterDomain('self'); $this->adapter = $adapter; } return $this->adapter; } public function getLoginOrder() { // Make sure username/password appears first if it is enabled. return '100-'.$this->getProviderName(); } public function shouldAllowAccountLink() { return false; } public function shouldAllowAccountUnlink() { return false; } public function isDefaultRegistrationProvider() { return true; } public function buildLoginForm( PhabricatorAuthStartController $controller) { $request = $controller->getRequest(); 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."); } private function renderPasswordLoginForm( AphrontRequest $request, $require_captcha = false, $captcha_valid = false) { $viewer = $request->getUser(); $dialog = id(new AphrontDialogView()) ->setSubmitURI($this->getLoginURI()) ->setUser($viewer) ->setTitle(pht('Login to Phabricator')) ->addSubmitButton(pht('Login')); if ($this->shouldAllowRegistration()) { $dialog->addCancelButton( '/auth/register/', pht('Register New Account')); } $dialog->addFooter( phutil_tag( 'a', array( 'href' => '/login/email/', ), pht('Forgot your password?'))); $v_user = nonempty( $request->getStr('username'), $request->getCookie(PhabricatorCookies::COOKIE_USERNAME)); $e_user = null; $e_pass = null; $e_captcha = null; $errors = array(); if ($require_captcha && !$captcha_valid) { if (AphrontFormRecaptchaControl::hasCaptchaResponse($request)) { $e_captcha = pht('Invalid'); $errors[] = pht('CAPTCHA was not entered correctly.'); } else { $e_captcha = pht('Required'); $errors[] = pht('Too many login failures recently. You must '. 'submit a CAPTCHA with your login request.'); } } else if ($request->isHTTPPost()) { // NOTE: This is intentionally vague so as not to disclose whether a // given username or email is registered. $e_user = pht('Invalid'); $e_pass = pht('Invalid'); $errors[] = pht('Username or password are incorrect.'); } if ($errors) { $errors = id(new PHUIErrorView())->setErrors($errors); } $form = id(new PHUIFormLayoutView()) ->setFullWidth(true) ->appendChild($errors) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Username or Email') ->setName('username') ->setValue($v_user) ->setError($e_user)) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel('Password') ->setName('password') ->setError($e_pass)); if ($require_captcha) { $form->appendChild( id(new AphrontFormRecaptchaControl()) ->setError($e_captcha)); } $dialog->appendChild($form); return $dialog; } public function processLoginRequest( PhabricatorAuthLoginController $controller) { $request = $controller->getRequest(); $viewer = $request->getUser(); $require_captcha = false; $captcha_valid = false; if (AphrontFormRecaptchaControl::isRecaptchaEnabled()) { $failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP( PhabricatorUserLog::ACTION_LOGIN_FAILURE, 60 * 15); if (count($failed_attempts) > 5) { $require_captcha = true; $captcha_valid = AphrontFormRecaptchaControl::processCaptcha($request); } } $response = null; $account = null; $log_user = null; if ($request->isFormPost()) { if (!$require_captcha || $captcha_valid) { $username_or_email = $request->getStr('username'); if (strlen($username_or_email)) { $user = id(new PhabricatorUser())->loadOneWhere( 'username = %s', $username_or_email); if (!$user) { $user = PhabricatorUser::loadOneWithEmailAddress( $username_or_email); } if ($user) { $envelope = new PhutilOpaqueEnvelope($request->getStr('password')); if ($user->comparePassword($envelope)) { $account = $this->loadOrCreateAccount($user->getPHID()); $log_user = $user; // If the user's password is stored using a less-than-optimal // hash, upgrade them to the strongest available hash. $hash_envelope = new PhutilOpaqueEnvelope( $user->getPasswordHash()); if (PhabricatorPasswordHasher::canUpgradeHash($hash_envelope)) { $user->setPassword($envelope); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $user->save(); unset($unguarded); } } } } } } if (!$account) { if ($request->isFormPost()) { $log = PhabricatorUserLog::initializeNewLog( null, $log_user ? $log_user->getPHID() : null, PhabricatorUserLog::ACTION_LOGIN_FAILURE); $log->save(); } $request->clearCookie(PhabricatorCookies::COOKIE_USERNAME); $response = $controller->buildProviderPageResponse( $this, $this->renderPasswordLoginForm( $request, $require_captcha, $captcha_valid)); } return array($account, $response); } public function shouldRequireRegistrationPassword() { return true; } public function getDefaultExternalAccount() { $adapter = $this->getAdapter(); return id(new PhabricatorExternalAccount()) ->setAccountType($adapter->getAdapterType()) ->setAccountDomain($adapter->getAdapterDomain()); } protected function willSaveAccount(PhabricatorExternalAccount $account) { parent::willSaveAccount($account); $account->setUserPHID($account->getAccountID()); } public function willRegisterAccount(PhabricatorExternalAccount $account) { parent::willRegisterAccount($account); $account->setAccountID($account->getUserPHID()); } public static function getPasswordProvider() { $providers = self::getAllEnabledProviders(); foreach ($providers as $provider) { if ($provider instanceof PhabricatorPasswordAuthProvider) { return $provider; } } return null; } public function willRenderLinkedAccount( PhabricatorUser $viewer, PHUIObjectItemView $item, PhabricatorExternalAccount $account) { return; } public function shouldAllowAccountRefresh() { return false; } public function shouldAllowEmailTrustConfiguration() { return false; } } diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php index 08efa2441c..f5b9c87cde 100644 --- a/src/applications/people/editor/PhabricatorUserEditor.php +++ b/src/applications/people/editor/PhabricatorUserEditor.php @@ -1,657 +1,677 @@ 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())) { $valid = PhabricatorUser::describeValidUsername(); throw new Exception("Username is invalid! {$valid}"); } // Always set a new user's email address to primary. $email->setIsPrimary(1); // If the primary address is already verified, also set the verified flag // on the user themselves. if ($email->getIsVerified()) { $user->setIsEmailVerified(1); } $this->willAddEmail($email); $user->openTransaction(); try { $user->save(); $email->setUserPHID($user->getPHID()); $email->save(); } catch (AphrontDuplicateKeyQueryException $ex) { // We might have written the user but failed to write the email; if // so, erase the IDs we attached. $user->setID(null); $user->setPHID(null); $user->killTransaction(); throw $ex; } $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), $user->getPHID(), PhabricatorUserLog::ACTION_CREATE); $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; } /** * @task edit */ public function updateUser( PhabricatorUser $user, PhabricatorUserEmail $email = null) { if (!$user->getID()) { throw new Exception('User has not been created yet!'); } $user->openTransaction(); $user->save(); if ($email) { $email->save(); } $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), $user->getPHID(), PhabricatorUserLog::ACTION_EDIT); $log->save(); $user->saveTransaction(); return $this; } /** * @task edit */ public function changePassword( PhabricatorUser $user, PhutilOpaqueEnvelope $envelope) { if (!$user->getID()) { throw new Exception('User has not been created yet!'); } $user->openTransaction(); $user->reload(); $user->setPassword($envelope); $user->save(); $log = PhabricatorUserLog::initializeNewLog( $this->requireActor(), $user->getPHID(), PhabricatorUserLog::ACTION_CHANGE_PASSWORD); $log->save(); $user->saveTransaction(); } /** * @task edit */ public function changeUsername(PhabricatorUser $user, $username) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } if (!PhabricatorUser::validateUsername($username)) { $valid = PhabricatorUser::describeValidUsername(); throw new Exception("Username is invalid! {$valid}"); } $old_username = $user->getUsername(); $user->openTransaction(); $user->reload(); $user->setUsername($username); try { $user->save(); } catch (AphrontDuplicateKeyQueryException $ex) { $user->setUsername($old_username); $user->killTransaction(); throw $ex; } $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_CHANGE_USERNAME); $log->setOldValue($old_username); $log->setNewValue($username); $log->save(); $user->saveTransaction(); $user->sendUsernameChangeEmail($actor, $old_username); } /* -( Editing Roles )------------------------------------------------------ */ /** * @task role */ public function makeAdminUser(PhabricatorUser $user, $admin) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsAdmin() == $admin) { $user->endWriteLocking(); $user->killTransaction(); return $this; } $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_ADMIN); $log->setOldValue($user->getIsAdmin()); $log->setNewValue($admin); $user->setIsAdmin((int)$admin); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task role */ public function makeSystemAgentUser(PhabricatorUser $user, $system_agent) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsSystemAgent() == $system_agent) { $user->endWriteLocking(); $user->killTransaction(); return $this; } $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_SYSTEM_AGENT); $log->setOldValue($user->getIsSystemAgent()); $log->setNewValue($system_agent); $user->setIsSystemAgent((int)$system_agent); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task role */ public function disableUser(PhabricatorUser $user, $disable) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsDisabled() == $disable) { $user->endWriteLocking(); $user->killTransaction(); return $this; } $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_DISABLE); $log->setOldValue($user->getIsDisabled()); $log->setNewValue($disable); $user->setIsDisabled((int)$disable); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task role */ public function approveUser(PhabricatorUser $user, $approve) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsApproved() == $approve) { $user->endWriteLocking(); $user->killTransaction(); return $this; } $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_APPROVE); $log->setOldValue($user->getIsApproved()); $log->setNewValue($approve); $user->setIsApproved($approve); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /* -( Adding, Removing and Changing Email )-------------------------------- */ /** * @task email */ public function addEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } if ($email->getID()) { throw new Exception('Email has already been created!'); } // Use changePrimaryEmail() to change primary email. $email->setIsPrimary(0); $email->setUserPHID($user->getPHID()); $this->willAddEmail($email); $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); try { $email->save(); } catch (AphrontDuplicateKeyQueryException $ex) { $user->endWriteLocking(); $user->killTransaction(); throw $ex; } $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_ADD); $log->setNewValue($email->getAddress()); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task email */ public function removeEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } if (!$email->getID()) { throw new Exception('Email has not been created yet!'); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); $email->reload(); if ($email->getIsPrimary()) { throw new Exception("Can't remove primary email!"); } if ($email->getUserPHID() != $user->getPHID()) { throw new Exception('Email not owned by user!'); } $email->delete(); $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_REMOVE); $log->setOldValue($email->getAddress()); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); $this->revokePasswordResetLinks($user); return $this; } /** * @task email */ public function changePrimaryEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } if (!$email->getID()) { throw new Exception('Email has not been created yet!'); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); $email->reload(); if ($email->getUserPHID() != $user->getPHID()) { throw new Exception('User does not own email!'); } if ($email->getIsPrimary()) { throw new Exception('Email is already primary!'); } if (!$email->getIsVerified()) { throw new Exception('Email is not verified!'); } $old_primary = $user->loadPrimaryEmail(); if ($old_primary) { $old_primary->setIsPrimary(0); $old_primary->save(); } $email->setIsPrimary(1); $email->save(); $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_PRIMARY); $log->setOldValue($old_primary ? $old_primary->getAddress() : null); $log->setNewValue($email->getAddress()); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); if ($old_primary) { $old_primary->sendOldPrimaryEmail($user, $email); } $email->sendNewPrimaryEmail($user); $this->revokePasswordResetLinks($user); return $this; } /** * Verify a user's email address. * * This verifies an individual email address. If the address is the user's * primary address and their account was not previously verified, their * account is marked as email verified. * * @task email */ public function verifyEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception('User has not been created yet!'); } if (!$email->getID()) { throw new Exception('Email has not been created yet!'); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); $email->reload(); if ($email->getUserPHID() != $user->getPHID()) { throw new Exception(pht('User does not own email!')); } if (!$email->getIsVerified()) { $email->setIsVerified(1); $email->save(); $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_VERIFY); $log->setNewValue($email->getAddress()); $log->save(); } if (!$user->getIsEmailVerified()) { // If the user just verified their primary email address, mark their // account as email verified. $user_primary = $user->loadPrimaryEmail(); if ($user_primary->getID() == $email->getID()) { $user->setIsEmailVerified(1); $user->save(); } } $user->endWriteLocking(); $user->saveTransaction(); } /** * Reassign an unverified email address. */ public function reassignEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception(pht('User has not been created yet!')); } if (!$email->getID()) { throw new Exception(pht('Email has not been created yet!')); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); $email->reload(); $old_user = $email->getUserPHID(); if ($old_user != $user->getPHID()) { if ($email->getIsVerified()) { throw new Exception( pht( 'Verified email addresses can not be reassigned.')); } if ($email->getIsPrimary()) { throw new Exception( pht( 'Primary email addresses can not be reassigned.')); } $email->setUserPHID($user->getPHID()); $email->save(); $log = PhabricatorUserLog::initializeNewLog( $actor, $user->getPHID(), PhabricatorUserLog::ACTION_EMAIL_REASSIGN); $log->setNewValue($email->getAddress()); $log->save(); } $user->endWriteLocking(); $user->saveTransaction(); } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ private function willAddEmail(PhabricatorUserEmail $email) { // Hard check before write to prevent creation of disallowed email // addresses. Normally, the application does checks and raises more // user friendly errors for us, but we omit the courtesy checks on some // pathways like administrative scripts for simplicity. if (!PhabricatorUserEmail::isValidAddress($email->getAddress())) { throw new Exception(PhabricatorUserEmail::describeValidAddresses()); } if (!PhabricatorUserEmail::isAllowedAddress($email->getAddress())) { throw new Exception(PhabricatorUserEmail::describeAllowedAddresses()); } $application_email = id(new PhabricatorMetaMTAApplicationEmailQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withAddresses(array($email->getAddress())) ->executeOne(); if ($application_email) { throw new Exception($application_email->getInUseMessage()); } } private function revokePasswordResetLinks(PhabricatorUser $user) { // Revoke any outstanding password reset links. If an attacker compromises // an account, changes the email address, and sends themselves a password // reset link, it could otherwise remain live for a short period of time // and allow them to compromise the account again later. PhabricatorAuthTemporaryToken::revokeTokens( $user, array($user->getPHID()), array( PhabricatorAuthSessionEngine::ONETIME_TEMPORARY_TOKEN_TYPE, PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE, )); } }