mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-01 19:22:42 +01:00
Allow administrators to change usernames
Summary: Give them a big essay about how it's dangerous, but allow them to do it formally. Because the username is part of the password salt, users must change their passwords after a username change. Make password reset links work for already-logged-in-users since there's no reason not to (if you have a reset link, you can log out and use it) and it's much less confusing if you get this email and are already logged in. Depends on: D2651 Test Plan: Changed a user's username to all kinds of crazy things. Clicked reset links in email. Tried to make invalid/nonsense name changes. Reviewers: btrahan, vrana Reviewed By: btrahan CC: aran Maniphest Tasks: T1303 Differential Revision: https://secure.phabricator.com/D2657
This commit is contained in:
parent
0a7b4591ef
commit
0e1bbbd489
5 changed files with 201 additions and 45 deletions
|
@ -36,22 +36,6 @@ final class PhabricatorEmailTokenController
|
||||||
return new Aphront400Response();
|
return new Aphront400Response();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($request->getUser()->getPHID()) {
|
|
||||||
$view = new AphrontRequestFailureView();
|
|
||||||
$view->setHeader('Already Logged In');
|
|
||||||
$view->appendChild(
|
|
||||||
'<p>You are already logged in.</p>');
|
|
||||||
$view->appendChild(
|
|
||||||
'<div class="aphront-failure-continue">'.
|
|
||||||
'<a class="button" href="/">Return Home</a>'.
|
|
||||||
'</div>');
|
|
||||||
return $this->buildStandardPageResponse(
|
|
||||||
$view,
|
|
||||||
array(
|
|
||||||
'title' => 'Already Logged In',
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = $this->token;
|
$token = $this->token;
|
||||||
$email = $request->getStr('email');
|
$email = $request->getStr('email');
|
||||||
|
|
||||||
|
@ -103,10 +87,12 @@ final class PhabricatorEmailTokenController
|
||||||
// enough, without requiring users to go through a second round of email
|
// enough, without requiring users to go through a second round of email
|
||||||
// verification.
|
// verification.
|
||||||
|
|
||||||
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||||
$target_email->setIsVerified(1);
|
$target_email->setIsVerified(1);
|
||||||
$target_email->save();
|
$target_email->save();
|
||||||
|
|
||||||
$session_key = $target_user->establishSession('web');
|
$session_key = $target_user->establishSession('web');
|
||||||
|
unset($unguarded);
|
||||||
|
|
||||||
$request->setCookie('phusr', $target_user->getUsername());
|
$request->setCookie('phusr', $target_user->getUsername());
|
||||||
$request->setCookie('phsid', $session_key);
|
$request->setCookie('phsid', $session_key);
|
||||||
|
|
||||||
|
|
|
@ -148,6 +148,49 @@ final class PhabricatorUserEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 (AphrontQueryDuplicateKeyException $ex) {
|
||||||
|
$user->setUsername($old_username);
|
||||||
|
$user->killTransaction();
|
||||||
|
throw $ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
$log = PhabricatorUserLog::newLog(
|
||||||
|
$this->actor,
|
||||||
|
$user,
|
||||||
|
PhabricatorUserLog::ACTION_CHANGE_USERNAME);
|
||||||
|
$log->setOldValue($old_username);
|
||||||
|
$log->setNewValue($username);
|
||||||
|
$log->save();
|
||||||
|
|
||||||
|
$user->saveTransaction();
|
||||||
|
|
||||||
|
$user->sendUsernameChangeEmail($actor, $old_username);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -( Editing Roles )------------------------------------------------------ */
|
/* -( Editing Roles )------------------------------------------------------ */
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -41,23 +41,24 @@ final class PhabricatorPeopleEditController
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
return new Aphront404Response();
|
return new Aphront404Response();
|
||||||
}
|
}
|
||||||
|
$base_uri = '/people/edit/'.$user->getID().'/';
|
||||||
} else {
|
} else {
|
||||||
$user = new PhabricatorUser();
|
$user = new PhabricatorUser();
|
||||||
|
$base_uri = '/people/edit/';
|
||||||
}
|
}
|
||||||
|
|
||||||
$views = array(
|
$nav = new AphrontSideNavFilterView();
|
||||||
'basic' => 'Basic Information',
|
$nav->setBaseURI(new PhutilURI($base_uri));
|
||||||
'role' => 'Edit Roles',
|
$nav->addFilter('basic', 'Basic Information');
|
||||||
'cert' => 'Conduit Certificate',
|
$nav->addFilter('role', 'Edit Roles');
|
||||||
);
|
$nav->addFilter('cert', 'Conduit Certificate');
|
||||||
|
$nav->addSpacer();
|
||||||
|
$nav->addFilter('rename', 'Change Username');
|
||||||
|
|
||||||
if (!$user->getID()) {
|
if (!$user->getID()) {
|
||||||
$view = 'basic';
|
$this->view = 'basic';
|
||||||
} else if (isset($views[$this->view])) {
|
|
||||||
$view = $this->view;
|
|
||||||
} else {
|
|
||||||
$view = 'basic';
|
|
||||||
}
|
}
|
||||||
|
$view = $nav->selectFilter($this->view, 'basic');
|
||||||
|
|
||||||
$content = array();
|
$content = array();
|
||||||
|
|
||||||
|
@ -79,6 +80,11 @@ final class PhabricatorPeopleEditController
|
||||||
case 'cert':
|
case 'cert':
|
||||||
$response = $this->processCertificateRequest($user);
|
$response = $this->processCertificateRequest($user);
|
||||||
break;
|
break;
|
||||||
|
case 'rename':
|
||||||
|
$response = $this->processRenameRequest($user);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return new Aphront404Response();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($response instanceof AphrontResponse) {
|
if ($response instanceof AphrontResponse) {
|
||||||
|
@ -88,21 +94,8 @@ final class PhabricatorPeopleEditController
|
||||||
$content[] = $response;
|
$content[] = $response;
|
||||||
|
|
||||||
if ($user->getID()) {
|
if ($user->getID()) {
|
||||||
$side_nav = new AphrontSideNavView();
|
$nav->appendChild($content);
|
||||||
$side_nav->appendChild($content);
|
$content = $nav;
|
||||||
foreach ($views as $key => $name) {
|
|
||||||
$side_nav->addNavItem(
|
|
||||||
phutil_render_tag(
|
|
||||||
'a',
|
|
||||||
array(
|
|
||||||
'href' => '/people/edit/'.$user->getID().'/'.$key.'/',
|
|
||||||
'class' => ($key == $view)
|
|
||||||
? 'aphront-side-nav-selected'
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
phutil_escape_html($name)));
|
|
||||||
}
|
|
||||||
$content = $side_nav;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->buildStandardPageResponse(
|
return $this->buildStandardPageResponse(
|
||||||
|
@ -444,7 +437,6 @@ final class PhabricatorPeopleEditController
|
||||||
$request = $this->getRequest();
|
$request = $this->getRequest();
|
||||||
$admin = $request->getUser();
|
$admin = $request->getUser();
|
||||||
|
|
||||||
|
|
||||||
$form = new AphrontFormView();
|
$form = new AphrontFormView();
|
||||||
$form
|
$form
|
||||||
->setUser($admin)
|
->setUser($admin)
|
||||||
|
@ -481,6 +473,100 @@ final class PhabricatorPeopleEditController
|
||||||
return array($panel);
|
return array($panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function processRenameRequest(PhabricatorUser $user) {
|
||||||
|
$request = $this->getRequest();
|
||||||
|
$admin = $request->getUser();
|
||||||
|
|
||||||
|
$e_username = true;
|
||||||
|
$username = $user->getUsername();
|
||||||
|
|
||||||
|
$errors = array();
|
||||||
|
if ($request->isFormPost()) {
|
||||||
|
|
||||||
|
$username = $request->getStr('username');
|
||||||
|
if (!strlen($username)) {
|
||||||
|
$e_username = 'Required';
|
||||||
|
$errors[] = 'New username is required.';
|
||||||
|
} else if ($username == $user->getUsername()) {
|
||||||
|
$e_username = 'Invalid';
|
||||||
|
$errors[] = 'New username must be different from old username.';
|
||||||
|
} else if (!PhabricatorUser::validateUsername($username)) {
|
||||||
|
$e_username = 'Invalid';
|
||||||
|
$errors[] = PhabricatorUser::describeValidUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$errors) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
id(new PhabricatorUserEditor())
|
||||||
|
->setActor($admin)
|
||||||
|
->changeUsername($user, $username);
|
||||||
|
|
||||||
|
return id(new AphrontRedirectResponse())
|
||||||
|
->setURI($request->getRequestURI()->alter('saved', true));
|
||||||
|
} catch (AphrontQueryDuplicateKeyException $ex) {
|
||||||
|
$e_username = 'Not Unique';
|
||||||
|
$errors[] = 'Another user already has that username.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($errors) {
|
||||||
|
$errors = id(new AphrontErrorView())
|
||||||
|
->setTitle('Form Errors')
|
||||||
|
->setErrors($errors);
|
||||||
|
} else {
|
||||||
|
$errors = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = new AphrontFormView();
|
||||||
|
$form
|
||||||
|
->setUser($admin)
|
||||||
|
->setAction($request->getRequestURI())
|
||||||
|
->appendChild(
|
||||||
|
'<p class="aphront-form-instructions">'.
|
||||||
|
'<strong>Be careful when renaming users!</strong> '.
|
||||||
|
'The old username will no longer be tied to the user, so anything '.
|
||||||
|
'which uses it (like old commit messages) will no longer associate '.
|
||||||
|
'correctly. And if you give a user a username which some other user '.
|
||||||
|
'used to have, username lookups will begin returning the wrong '.
|
||||||
|
'user.'.
|
||||||
|
'</p>'.
|
||||||
|
'<p class="aphront-form-instructions">'.
|
||||||
|
'It is generally safe to rename newly created users (and test users '.
|
||||||
|
'and so on), but less safe to rename established users and unsafe '.
|
||||||
|
'to reissue a username.'.
|
||||||
|
'</p>'.
|
||||||
|
'<p class="aphront-form-instructions">'.
|
||||||
|
'Users who rely on password auth will need to reset their password '.
|
||||||
|
'after their username is changed (their username is part of the '.
|
||||||
|
'salt in the password hash). They will receive an email with '.
|
||||||
|
'instructions on how to do this.'.
|
||||||
|
'</p>')
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormStaticControl())
|
||||||
|
->setLabel('Old Username')
|
||||||
|
->setValue($user->getUsername()))
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormTextControl())
|
||||||
|
->setLabel('New Username')
|
||||||
|
->setValue($username)
|
||||||
|
->setName('username')
|
||||||
|
->setError($e_username))
|
||||||
|
->appendChild(
|
||||||
|
id(new AphrontFormSubmitControl())
|
||||||
|
->setValue('Change Username'));
|
||||||
|
|
||||||
|
$panel = new AphrontPanelView();
|
||||||
|
$panel->setHeader('Change Username');
|
||||||
|
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
|
||||||
|
$panel->appendChild($form);
|
||||||
|
|
||||||
|
return array($errors, $panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private function getRoleInstructions() {
|
private function getRoleInstructions() {
|
||||||
$roles_link = phutil_render_tag(
|
$roles_link = phutil_render_tag(
|
||||||
'a',
|
'a',
|
||||||
|
|
|
@ -549,6 +549,46 @@ EOBODY;
|
||||||
->saveAndSend();
|
->saveAndSend();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function sendUsernameChangeEmail(
|
||||||
|
PhabricatorUser $admin,
|
||||||
|
$old_username) {
|
||||||
|
|
||||||
|
$admin_username = $admin->getUserName();
|
||||||
|
$admin_realname = $admin->getRealName();
|
||||||
|
$new_username = $this->getUserName();
|
||||||
|
|
||||||
|
$password_instructions = null;
|
||||||
|
if (PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) {
|
||||||
|
$uri = $this->getEmailLoginURI();
|
||||||
|
$password_instructions = <<<EOTXT
|
||||||
|
If you use a password to login, you'll need to reset it before you can login
|
||||||
|
again. You can reset your password by following this link:
|
||||||
|
|
||||||
|
{$uri}
|
||||||
|
|
||||||
|
And, of course, you'll need to use your new username to login from now on. If
|
||||||
|
you use OAuth to login, nothing should change.
|
||||||
|
|
||||||
|
EOTXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = <<<EOBODY
|
||||||
|
{$admin_username} ({$admin_realname}) has changed your Phabricator username.
|
||||||
|
|
||||||
|
Old Username: {$old_username}
|
||||||
|
New Username: {$new_username}
|
||||||
|
|
||||||
|
{$password_instructions}
|
||||||
|
EOBODY;
|
||||||
|
|
||||||
|
$mail = id(new PhabricatorMetaMTAMail())
|
||||||
|
->addTos(array($this->getPHID()))
|
||||||
|
->setSubject('[Phabricator] Username Changed')
|
||||||
|
->setBody($body)
|
||||||
|
->setFrom($admin->getPHID())
|
||||||
|
->saveAndSend();
|
||||||
|
}
|
||||||
|
|
||||||
public static function describeValidUsername() {
|
public static function describeValidUsername() {
|
||||||
return 'Usernames must contain only numbers, letters, period, underscore '.
|
return 'Usernames must contain only numbers, letters, period, underscore '.
|
||||||
'and hyphen, and can not end with a period.';
|
'and hyphen, and can not end with a period.';
|
||||||
|
|
|
@ -37,6 +37,7 @@ final class PhabricatorUserLog extends PhabricatorUserDAO {
|
||||||
const ACTION_EMAIL_ADD = 'email-add';
|
const ACTION_EMAIL_ADD = 'email-add';
|
||||||
|
|
||||||
const ACTION_CHANGE_PASSWORD = 'change-password';
|
const ACTION_CHANGE_PASSWORD = 'change-password';
|
||||||
|
const ACTION_CHANGE_USERNAME = 'change-username';
|
||||||
|
|
||||||
protected $actorPHID;
|
protected $actorPHID;
|
||||||
protected $userPHID;
|
protected $userPHID;
|
||||||
|
|
Loading…
Reference in a new issue