2012-05-25 16:30:44 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Copyright 2012 Facebook, Inc.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Editor class for creating and adjusting users. This class guarantees data
|
|
|
|
* integrity and writes logs when user information changes.
|
|
|
|
*
|
|
|
|
* @task config Configuration
|
|
|
|
* @task edit Creating and Editing Users
|
|
|
|
* @task role Editing Roles
|
|
|
|
* @task email Adding, Removing and Changing Email
|
|
|
|
* @task internal Internals
|
|
|
|
*/
|
|
|
|
final class PhabricatorUserEditor {
|
|
|
|
|
|
|
|
private $actor;
|
|
|
|
private $logs = array();
|
|
|
|
|
|
|
|
|
|
|
|
/* -( Configuration )------------------------------------------------------ */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task config
|
|
|
|
*/
|
|
|
|
public function setActor(PhabricatorUser $actor) {
|
|
|
|
$this->actor = $actor;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* -( Creating and Editing Users )----------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task edit
|
|
|
|
*/
|
|
|
|
public function createNewUser(
|
|
|
|
PhabricatorUser $user,
|
|
|
|
PhabricatorUserEmail $email) {
|
|
|
|
|
|
|
|
if ($user->getID()) {
|
|
|
|
throw new Exception("User has already been created!");
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($email->getID()) {
|
|
|
|
throw new Exception("Email has already been created!");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Always set a new user's email address to primary.
|
|
|
|
$email->setIsPrimary(1);
|
|
|
|
|
Allow restriction of permitted email domains
Summary:
Allow allowed email addresses to be restricted to certain domains. This implies email must be verified.
This probably isn't QUITE ready for prime-time without a few other tweaks (better administrative tools, notably) but we're nearly there.
Test Plan:
- With no restrictions:
- Registered with OAuth
- Created an account with accountadmin
- Added an email
- With restrictions:
- Tried to OAuth register with a restricted address, was prompted to provide a valid one.
- Tried to OAuth register with a valid address, worked fine.
- Tried to accountadmin a restricted address, got blocked.
- Tried to accountadmin a valid address, worked fine.
- Tried to add a restricted address, blocked.
- Tried to add a valid address, worked fine.
- Created a user with People with an invalid address, got blocked.
- Created a user with People with a valid address, worked fine.
Reviewers: btrahan, csilvers
Reviewed By: csilvers
CC: aran, joe, csilvers
Maniphest Tasks: T1184
Differential Revision: https://secure.phabricator.com/D2581
2012-05-26 15:04:35 +02:00
|
|
|
$this->willAddEmail($email);
|
|
|
|
|
2012-05-25 16:30:44 +02:00
|
|
|
$user->openTransaction();
|
|
|
|
try {
|
Fix transaction handling in PhabricatorUserEditor->createNewUser()
Summary:
See https://github.com/facebook/phabricator/issues/117
- The $user save can hit a duplicate key exception like the email, but we don't handle it correctly.
- When the $user saves but the $email does not, the $user is left with a (rolled-back, invalid) ID. This makes the UI glitch out a bit. Wipe the ID if we abort the transaction.
- We show the "Required" star marker even if the email is filled in.
The ID issue is sort of a general problem, but I think it's fairly rare: you must be doing inserts on related objects and the caller must catch the transaction failure and attempt to handle it in some way.
I can think of three approaches:
- Manually "roll back" the objects inside the transaction, as here. Seems OK if this really is a rare problem.
- Automatically roll back the 'id' and 'phid' columns (if they exist). Seems reasonable but maybe more complicated than necessary. Won't get every case right. For instance, if we inserted a third object here and that failed, $email would still have the userPHID set.
- Automatically roll back the entire object. We can do this by cloning all the writable fields. Seems like it might be way too magical, but maybe the right solution? Might have weird bugs with nonwritable fields and other random stuff.
We can trigger the rollback by storing objects we updated on the transaction, and either throwing them away or rolling them back on saveTransaction() / killTransaction().
These fancier approaches all seem to have some tradeoffs though, and I don't think we need to pick one yet, since this has only caused problems in one case.
Test Plan: Tried to create a new user (via People -> Create New User) with a duplicate username. Got a proper UI message with no exception and no UI glitchiness.
Reviewers: btrahan, vrana, hgrimberg, hgrimberg01
Reviewed By: hgrimberg01
CC: aran
Differential Revision: https://secure.phabricator.com/D2650
2012-06-05 15:46:01 +02:00
|
|
|
$user->save();
|
|
|
|
$email->setUserPHID($user->getPHID());
|
2012-05-25 16:30:44 +02:00
|
|
|
$email->save();
|
|
|
|
} catch (AphrontQueryDuplicateKeyException $ex) {
|
Fix transaction handling in PhabricatorUserEditor->createNewUser()
Summary:
See https://github.com/facebook/phabricator/issues/117
- The $user save can hit a duplicate key exception like the email, but we don't handle it correctly.
- When the $user saves but the $email does not, the $user is left with a (rolled-back, invalid) ID. This makes the UI glitch out a bit. Wipe the ID if we abort the transaction.
- We show the "Required" star marker even if the email is filled in.
The ID issue is sort of a general problem, but I think it's fairly rare: you must be doing inserts on related objects and the caller must catch the transaction failure and attempt to handle it in some way.
I can think of three approaches:
- Manually "roll back" the objects inside the transaction, as here. Seems OK if this really is a rare problem.
- Automatically roll back the 'id' and 'phid' columns (if they exist). Seems reasonable but maybe more complicated than necessary. Won't get every case right. For instance, if we inserted a third object here and that failed, $email would still have the userPHID set.
- Automatically roll back the entire object. We can do this by cloning all the writable fields. Seems like it might be way too magical, but maybe the right solution? Might have weird bugs with nonwritable fields and other random stuff.
We can trigger the rollback by storing objects we updated on the transaction, and either throwing them away or rolling them back on saveTransaction() / killTransaction().
These fancier approaches all seem to have some tradeoffs though, and I don't think we need to pick one yet, since this has only caused problems in one case.
Test Plan: Tried to create a new user (via People -> Create New User) with a duplicate username. Got a proper UI message with no exception and no UI glitchiness.
Reviewers: btrahan, vrana, hgrimberg, hgrimberg01
Reviewed By: hgrimberg01
CC: aran
Differential Revision: https://secure.phabricator.com/D2650
2012-06-05 15:46:01 +02:00
|
|
|
// 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);
|
|
|
|
|
2012-05-25 16:30:44 +02:00
|
|
|
$user->killTransaction();
|
|
|
|
throw $ex;
|
|
|
|
}
|
|
|
|
|
|
|
|
$log = PhabricatorUserLog::newLog(
|
|
|
|
$this->actor,
|
|
|
|
$user,
|
|
|
|
PhabricatorUserLog::ACTION_CREATE);
|
|
|
|
$log->setNewValue($email->getAddress());
|
|
|
|
$log->save();
|
|
|
|
|
|
|
|
$user->saveTransaction();
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task edit
|
|
|
|
*/
|
|
|
|
public function updateUser(PhabricatorUser $user) {
|
|
|
|
if (!$user->getID()) {
|
|
|
|
throw new Exception("User has not been created yet!");
|
|
|
|
}
|
|
|
|
|
|
|
|
$actor = $this->requireActor();
|
|
|
|
$user->openTransaction();
|
|
|
|
$user->save();
|
|
|
|
|
|
|
|
$log = PhabricatorUserLog::newLog(
|
|
|
|
$actor,
|
|
|
|
$user,
|
|
|
|
PhabricatorUserLog::ACTION_EDIT);
|
|
|
|
$log->save();
|
|
|
|
|
|
|
|
$user->saveTransaction();
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task edit
|
|
|
|
*/
|
|
|
|
public function changePassword(PhabricatorUser $user, $password) {
|
|
|
|
if (!$user->getID()) {
|
|
|
|
throw new Exception("User has not been created yet!");
|
|
|
|
}
|
|
|
|
|
|
|
|
$user->openTransaction();
|
|
|
|
$user->reload();
|
|
|
|
|
|
|
|
$user->setPassword($password);
|
|
|
|
$user->save();
|
|
|
|
|
|
|
|
$log = PhabricatorUserLog::newLog(
|
|
|
|
$this->actor,
|
|
|
|
$user,
|
|
|
|
PhabricatorUserLog::ACTION_CHANGE_PASSWORD);
|
|
|
|
$log->save();
|
|
|
|
|
|
|
|
$user->saveTransaction();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* -( 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::newLog(
|
|
|
|
$actor,
|
|
|
|
$user,
|
|
|
|
PhabricatorUserLog::ACTION_ADMIN);
|
|
|
|
$log->setOldValue($user->getIsAdmin());
|
|
|
|
$log->setNewValue($admin);
|
|
|
|
|
|
|
|
$user->setIsAdmin($admin);
|
|
|
|
$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::newLog(
|
|
|
|
$actor,
|
|
|
|
$user,
|
|
|
|
PhabricatorUserLog::ACTION_DISABLE);
|
|
|
|
$log->setOldValue($user->getIsDisabled());
|
|
|
|
$log->setNewValue($disable);
|
|
|
|
|
|
|
|
$user->setIsDisabled($disable);
|
|
|
|
$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());
|
|
|
|
|
Allow restriction of permitted email domains
Summary:
Allow allowed email addresses to be restricted to certain domains. This implies email must be verified.
This probably isn't QUITE ready for prime-time without a few other tweaks (better administrative tools, notably) but we're nearly there.
Test Plan:
- With no restrictions:
- Registered with OAuth
- Created an account with accountadmin
- Added an email
- With restrictions:
- Tried to OAuth register with a restricted address, was prompted to provide a valid one.
- Tried to OAuth register with a valid address, worked fine.
- Tried to accountadmin a restricted address, got blocked.
- Tried to accountadmin a valid address, worked fine.
- Tried to add a restricted address, blocked.
- Tried to add a valid address, worked fine.
- Created a user with People with an invalid address, got blocked.
- Created a user with People with a valid address, worked fine.
Reviewers: btrahan, csilvers
Reviewed By: csilvers
CC: aran, joe, csilvers
Maniphest Tasks: T1184
Differential Revision: https://secure.phabricator.com/D2581
2012-05-26 15:04:35 +02:00
|
|
|
$this->willAddEmail($email);
|
|
|
|
|
2012-05-25 16:30:44 +02:00
|
|
|
$user->openTransaction();
|
|
|
|
$user->beginWriteLocking();
|
|
|
|
|
|
|
|
$user->reload();
|
|
|
|
|
|
|
|
try {
|
|
|
|
$email->save();
|
|
|
|
} catch (AphrontQueryDuplicateKeyException $ex) {
|
|
|
|
$user->endWriteLocking();
|
|
|
|
$user->killTransaction();
|
|
|
|
|
|
|
|
throw $ex;
|
|
|
|
}
|
|
|
|
|
|
|
|
$log = PhabricatorUserLog::newLog(
|
|
|
|
$this->actor,
|
|
|
|
$user,
|
|
|
|
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::newLog(
|
|
|
|
$this->actor,
|
|
|
|
$user,
|
|
|
|
PhabricatorUserLog::ACTION_EMAIL_REMOVE);
|
|
|
|
$log->setOldValue($email->getAddress());
|
|
|
|
$log->save();
|
|
|
|
|
|
|
|
$user->endWriteLocking();
|
|
|
|
$user->saveTransaction();
|
|
|
|
|
|
|
|
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::newLog(
|
|
|
|
$actor,
|
|
|
|
$user,
|
|
|
|
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);
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* -( Internals )---------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task internal
|
|
|
|
*/
|
|
|
|
private function requireActor() {
|
|
|
|
if (!$this->actor) {
|
|
|
|
throw new Exception("User edit requires actor!");
|
|
|
|
}
|
|
|
|
return $this->actor;
|
|
|
|
}
|
|
|
|
|
Allow restriction of permitted email domains
Summary:
Allow allowed email addresses to be restricted to certain domains. This implies email must be verified.
This probably isn't QUITE ready for prime-time without a few other tweaks (better administrative tools, notably) but we're nearly there.
Test Plan:
- With no restrictions:
- Registered with OAuth
- Created an account with accountadmin
- Added an email
- With restrictions:
- Tried to OAuth register with a restricted address, was prompted to provide a valid one.
- Tried to OAuth register with a valid address, worked fine.
- Tried to accountadmin a restricted address, got blocked.
- Tried to accountadmin a valid address, worked fine.
- Tried to add a restricted address, blocked.
- Tried to add a valid address, worked fine.
- Created a user with People with an invalid address, got blocked.
- Created a user with People with a valid address, worked fine.
Reviewers: btrahan, csilvers
Reviewed By: csilvers
CC: aran, joe, csilvers
Maniphest Tasks: T1184
Differential Revision: https://secure.phabricator.com/D2581
2012-05-26 15:04:35 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @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::isAllowedAddress($email->getAddress())) {
|
|
|
|
throw new Exception(PhabricatorUserEmail::describeAllowedAddresses());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-05-25 16:30:44 +02:00
|
|
|
}
|