diff --git a/scripts/user/account_admin.php b/scripts/user/account_admin.php index d0ae23287..2b7053777 100755 --- a/scripts/user/account_admin.php +++ b/scripts/user/account_admin.php @@ -1,175 +1,216 @@ #!/usr/bin/env php <?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. */ $root = dirname(dirname(dirname(__FILE__))); require_once $root.'/scripts/__init_script__.php'; echo "Enter a username to create a new account or edit an existing account."; $username = phutil_console_prompt("Enter a username:"); if (!strlen($username)) { echo "Cancelled.\n"; exit(1); } if (!PhabricatorUser::validateUsername($username)) { $valid = PhabricatorUser::describeValidUsername(); echo "The username '{$username}' is invalid. {$valid}\n"; exit(1); } $user = id(new PhabricatorUser())->loadOneWhere( 'username = %s', $username); if (!$user) { $original = new PhabricatorUser(); echo "There is no existing user account '{$username}'.\n"; $ok = phutil_console_confirm( "Do you want to create a new '{$username}' account?", $default_no = false); if (!$ok) { echo "Cancelled.\n"; exit(1); } $user = new PhabricatorUser(); $user->setUsername($username); $is_new = true; } else { $original = clone $user; echo "There is an existing user account '{$username}'.\n"; $ok = phutil_console_confirm( "Do you want to edit the existing '{$username}' account?", $default_no = false); if (!$ok) { echo "Cancelled.\n"; exit(1); } $is_new = false; } $user_realname = $user->getRealName(); if (strlen($user_realname)) { $realname_prompt = ' ['.$user_realname.']'; } else { $realname_prompt = ''; } $realname = nonempty( phutil_console_prompt("Enter user real name{$realname_prompt}:"), $user_realname); $user->setRealName($realname); // When creating a new user we prompt for an email address; when editing an // existing user we just skip this because it would be quite involved to provide // a reasonable CLI interface for editing multiple addresses and managing email // verification and primary addresses. -$new_email = null; +$create_email = null; if ($is_new) { do { $email = phutil_console_prompt("Enter user email address:"); $duplicate = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', $email); if ($duplicate) { echo "ERROR: There is already a user with that email address. ". "Each user must have a unique email address.\n"; } else { break; } } while (true); - $new_email = $email; + $create_email = $email; } $changed_pass = false; // This disables local echo, so the user's password is not shown as they type // it. phutil_passthru('stty -echo'); $password = phutil_console_prompt( "Enter a password for this user [blank to leave unchanged]:"); phutil_passthru('stty echo'); if (strlen($password)) { $changed_pass = $password; } +$is_system_agent = $user->getIsSystemAgent(); +$set_system_agent = phutil_console_confirm( + 'Should this user be a system agent?', + $default_no = !$is_system_agent); + +$verify_email = null; +$set_verified = false; +// Allow administrators to verify primary email addresses at this time in edit +// scenarios. (Create will work just fine from here as we auto-verify email +// on create.) +if (!$is_new) { + $verify_email = $user->loadPrimaryEmail(); + if (!$verify_email->getIsVerified()) { + $set_verified = phutil_console_confirm( + 'Should the primary email address be verified?', + $default_no = true + ); + } else { + // already verified so let's not make a fuss + $verify_email = null; + } +} + $is_admin = $user->getIsAdmin(); $set_admin = phutil_console_confirm( 'Should this user be an administrator?', $default_no = !$is_admin); echo "\n\nACCOUNT SUMMARY\n\n"; $tpl = "%12s %-30s %-30s\n"; printf($tpl, null, 'OLD VALUE', 'NEW VALUE'); printf($tpl, 'Username', $original->getUsername(), $user->getUsername()); printf($tpl, 'Real Name', $original->getRealName(), $user->getRealName()); -if ($new_email) { - printf($tpl, 'Email', '', $new_email); +if ($is_new) { + printf($tpl, 'Email', '', $create_email); } printf($tpl, 'Password', null, ($changed_pass !== false) ? 'Updated' : 'Unchanged'); +printf( + $tpl, + 'System Agent', + $original->getIsSystemAgent() ? 'Y' : 'N', + $set_system_agent ? 'Y' : 'N'); + +if ($verify_email) { + printf( + $tpl, + 'Verify Email', + $verify_email->getIsVerified() ? 'Y' : 'N', + $set_verified ? 'Y' : 'N'); +} + printf( $tpl, 'Admin', $original->getIsAdmin() ? 'Y' : 'N', $set_admin ? 'Y' : 'N'); echo "\n"; if (!phutil_console_confirm("Save these changes?", $default_no = false)) { echo "Cancelled.\n"; exit(1); } $user->openTransaction(); $editor = new PhabricatorUserEditor(); // TODO: This is wrong, but we have a chicken-and-egg problem when you use // this script to create the first user. $editor->setActor($user); - if ($new_email) { + if ($is_new) { $email = id(new PhabricatorUserEmail()) - ->setAddress($new_email) + ->setAddress($create_email) ->setIsVerified(1); $editor->createNewUser($user, $email); } else { - $editor->updateUser($user); + if ($verify_email) { + $verify_email->setIsVerified($set_verified ? 1 : 0); + } + $editor->updateUser($user, $verify_email); } $editor->makeAdminUser($user, $set_admin); + $editor->makeSystemAgentUser($user, $set_system_agent); if ($changed_pass !== false) { $envelope = new PhutilOpaqueEnvelope($changed_pass); $editor->changePassword($user, $envelope); } $user->saveTransaction(); echo "Saved changes.\n"; diff --git a/src/applications/people/PhabricatorUserEditor.php b/src/applications/people/PhabricatorUserEditor.php index 1ad29c091..5e6fd7054 100644 --- a/src/applications/people/PhabricatorUserEditor.php +++ b/src/applications/people/PhabricatorUserEditor.php @@ -1,538 +1,582 @@ <?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!"); } 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); $this->willAddEmail($email); $user->openTransaction(); try { $user->save(); $email->setUserPHID($user->getPHID()); $email->save(); } catch (AphrontQueryDuplicateKeyException $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::newLog( $this->actor, $user, PhabricatorUserLog::ACTION_CREATE); $log->setNewValue($email->getAddress()); $log->save(); $user->saveTransaction(); return $this; } /** * @task edit */ - public function updateUser(PhabricatorUser $user) { + public function updateUser( + PhabricatorUser $user, + PhabricatorUserEmail $email = null) { if (!$user->getID()) { throw new Exception("User has not been created yet!"); } $actor = $this->requireActor(); $user->openTransaction(); $user->save(); + if ($email) { + $email->save(); + } $log = PhabricatorUserLog::newLog( $actor, $user, 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::newLog( $this->actor, $user, 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 (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 )------------------------------------------------------ */ /** * @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 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::newLog( + $actor, + $user, + PhabricatorUserLog::ACTION_SYSTEM_AGENT); + $log->setOldValue($user->getIsSystemAgent()); + $log->setNewValue($system_agent); + + $user->setIsSystemAgent($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::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; } /** * @task role */ public function deleteUser(PhabricatorUser $user, $disable) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } if ($actor->getPHID() == $user->getPHID()) { throw new Exception("You can not delete yourself!"); } $user->openTransaction(); $ldaps = id(new PhabricatorUserLDAPInfo())->loadAllWhere( 'userID = %d', $user->getID()); foreach ($ldaps as $ldap) { $ldap->delete(); } $oauths = id(new PhabricatorUserOAuthInfo())->loadAllWhere( 'userID = %d', $user->getID()); foreach ($oauths as $oauth) { $oauth->delete(); } $prefs = id(new PhabricatorUserPreferences())->loadAllWhere( 'userPHID = %s', $user->getPHID()); foreach ($prefs as $pref) { $pref->delete(); } $profiles = id(new PhabricatorUserProfile())->loadAllWhere( 'userPHID = %s', $user->getPHID()); foreach ($profiles as $profile) { $profile->delete(); } $keys = id(new PhabricatorUserSSHKey())->loadAllWhere( 'userPHID = %s', $user->getPHID()); foreach ($keys as $key) { $key->delete(); } $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s', $user->getPHID()); foreach ($emails as $email) { $email->delete(); } $log = PhabricatorUserLog::newLog( $actor, $user, PhabricatorUserLog::ACTION_DELETE); $log->save(); $user->delete(); $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 (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; } /** * @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()); } } } diff --git a/src/applications/people/controller/PhabricatorPeopleEditController.php b/src/applications/people/controller/PhabricatorPeopleEditController.php index f27d3ee6d..b15bb157c 100644 --- a/src/applications/people/controller/PhabricatorPeopleEditController.php +++ b/src/applications/people/controller/PhabricatorPeopleEditController.php @@ -1,692 +1,696 @@ <?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. */ final class PhabricatorPeopleEditController extends PhabricatorPeopleController { public function shouldRequireAdmin() { return true; } private $id; private $view; public function willProcessRequest(array $data) { $this->id = idx($data, 'id'); $this->view = idx($data, 'view'); } public function processRequest() { $request = $this->getRequest(); $admin = $request->getUser(); if ($this->id) { $user = id(new PhabricatorUser())->load($this->id); if (!$user) { return new Aphront404Response(); } $base_uri = '/people/edit/'.$user->getID().'/'; } else { $user = new PhabricatorUser(); $base_uri = '/people/edit/'; } $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($base_uri)); $nav->addLabel('User Information'); $nav->addFilter('basic', 'Basic Information'); $nav->addFilter('role', 'Edit Roles'); $nav->addFilter('cert', 'Conduit Certificate'); $nav->addFilter('profile', 'View Profile', '/p/'.$user->getUsername().'/'); $nav->addSpacer(); $nav->addLabel('Special'); $nav->addFilter('rename', 'Change Username'); $nav->addFilter('delete', 'Delete User'); if (!$user->getID()) { $this->view = 'basic'; } $view = $nav->selectFilter($this->view, 'basic'); $content = array(); if ($request->getStr('saved')) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle('Changes Saved'); $notice->appendChild('<p>Your changes were saved.</p>'); $content[] = $notice; } switch ($view) { case 'basic': $response = $this->processBasicRequest($user); break; case 'role': $response = $this->processRoleRequest($user); break; case 'cert': $response = $this->processCertificateRequest($user); break; case 'rename': $response = $this->processRenameRequest($user); break; case 'delete': $response = $this->processDeleteRequest($user); break; default: return new Aphront404Response(); } if ($response instanceof AphrontResponse) { return $response; } $content[] = $response; if ($user->getID()) { $nav->appendChild($content); } else { $nav = $this->buildSideNavView(); $nav->selectFilter('edit'); $nav->appendChild($content); } return $this->buildApplicationPage( $nav, array( 'title' => 'Edit User', )); } private function processBasicRequest(PhabricatorUser $user) { $request = $this->getRequest(); $admin = $request->getUser(); $e_username = true; $e_realname = true; $e_email = true; $errors = array(); $welcome_checked = true; $new_email = null; $request = $this->getRequest(); if ($request->isFormPost()) { $welcome_checked = $request->getInt('welcome'); + $is_new = !$user->getID(); - if (!$user->getID()) { + if ($is_new) { $user->setUsername($request->getStr('username')); $new_email = $request->getStr('email'); if (!strlen($new_email)) { $errors[] = 'Email is required.'; $e_email = 'Required'; } else if (!PhabricatorUserEmail::isAllowedAddress($new_email)) { $e_email = 'Invalid'; $errors[] = PhabricatorUserEmail::describeAllowedAddresses(); } else { $e_email = null; } - if ($request->getStr('role') == 'agent') { - $user->setIsSystemAgent(true); - } } $user->setRealName($request->getStr('realname')); if (!strlen($user->getUsername())) { $errors[] = "Username is required."; $e_username = 'Required'; } else if (!PhabricatorUser::validateUsername($user->getUsername())) { $errors[] = PhabricatorUser::describeValidUsername(); $e_username = 'Invalid'; } else { $e_username = null; } if (!strlen($user->getRealName())) { $errors[] = 'Real name is required.'; $e_realname = 'Required'; } else { $e_realname = null; } if (!$errors) { try { - $is_new = !$user->getID(); if (!$is_new) { id(new PhabricatorUserEditor()) ->setActor($admin) ->updateUser($user); } else { $email = id(new PhabricatorUserEmail()) ->setAddress($new_email) ->setIsVerified(0); id(new PhabricatorUserEditor()) ->setActor($admin) ->createNewUser($user, $email); + + if ($request->getStr('role') == 'agent') { + id(new PhabricatorUserEditor()) + ->setActor($admin) + ->makeSystemAgentUser($user, true); + } + } if ($welcome_checked) { $user->sendWelcomeEmail($admin); } $response = id(new AphrontRedirectResponse()) ->setURI('/people/edit/'.$user->getID().'/?saved=true'); return $response; } catch (AphrontQueryDuplicateKeyException $ex) { $errors[] = 'Username and email must be unique.'; $same_username = id(new PhabricatorUser()) ->loadOneWhere('username = %s', $user->getUsername()); $same_email = id(new PhabricatorUserEmail()) ->loadOneWhere('address = %s', $new_email); if ($same_username) { $e_username = 'Duplicate'; } if ($same_email) { $e_email = 'Duplicate'; } } } } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle('Form Errors') ->setErrors($errors); } $form = new AphrontFormView(); $form->setUser($admin); if ($user->getID()) { $form->setAction('/people/edit/'.$user->getID().'/'); } else { $form->setAction('/people/edit/'); } if ($user->getID()) { $is_immutable = true; } else { $is_immutable = false; } $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Username') ->setName('username') ->setValue($user->getUsername()) ->setError($e_username) ->setDisabled($is_immutable)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Real Name') ->setName('realname') ->setValue($user->getRealName()) ->setError($e_realname)); if (!$user->getID()) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel('Email') ->setName('email') ->setDisabled($is_immutable) ->setValue($new_email) ->setCaption(PhabricatorUserEmail::describeAllowedAddresses()) ->setError($e_email)); } else { $email = $user->loadPrimaryEmail(); if ($email) { $status = $email->getIsVerified() ? 'Verified' : 'Unverified'; } else { $status = 'No Email Address'; } $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Email') ->setValue($status)); $form->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'welcome', 1, 'Re-send "Welcome to Phabricator" email.', false)); } $form->appendChild($this->getRoleInstructions()); if (!$user->getID()) { $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Role') ->setName('role') ->setValue('user') ->setOptions( array( 'user' => 'Normal User', 'agent' => 'System Agent', )) ->setCaption( 'You can create a "system agent" account for bots, scripts, '. 'etc.')) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'welcome', 1, 'Send "Welcome to Phabricator" email.', $welcome_checked)); } else { $roles = array(); if ($user->getIsSystemAgent()) { $roles[] = 'System Agent'; } if ($user->getIsAdmin()) { $roles[] = 'Admin'; } if ($user->getIsDisabled()) { $roles[] = 'Disabled'; } if (!$roles) { $roles[] = 'Normal User'; } $roles = implode(', ', $roles); $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Roles') ->setValue($roles)); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Save')); $panel = new AphrontPanelView(); if ($user->getID()) { $panel->setHeader('Edit User'); } else { $panel->setHeader('Create New User'); } $panel->appendChild($form); $panel->setWidth(AphrontPanelView::WIDTH_FORM); return array($error_view, $panel); } private function processRoleRequest(PhabricatorUser $user) { $request = $this->getRequest(); $admin = $request->getUser(); $is_self = ($user->getID() == $admin->getID()); $errors = array(); if ($request->isFormPost()) { $log_template = PhabricatorUserLog::newLog( $admin, $user, null); $logs = array(); if ($is_self) { $errors[] = "You can not edit your own role."; } else { $new_admin = (bool)$request->getBool('is_admin'); $old_admin = (bool)$user->getIsAdmin(); if ($new_admin != $old_admin) { id(new PhabricatorUserEditor()) ->setActor($admin) ->makeAdminUser($user, $new_admin); } $new_disabled = (bool)$request->getBool('is_disabled'); $old_disabled = (bool)$user->getIsDisabled(); if ($new_disabled != $old_disabled) { id(new PhabricatorUserEditor()) ->setActor($admin) ->disableUser($user, $new_disabled); } } if (!$errors) { return id(new AphrontRedirectResponse()) ->setURI($request->getRequestURI()->alter('saved', 'true')); } } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle('Form Errors') ->setErrors($errors); } $form = id(new AphrontFormView()) ->setUser($admin) ->setAction($request->getRequestURI()->alter('saved', null)); if ($is_self) { $form->appendChild( '<p class="aphront-form-instructions">NOTE: You can not edit your own '. 'role.</p>'); } $form ->appendChild($this->getRoleInstructions()) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'is_admin', 1, 'Administrator', $user->getIsAdmin()) ->setDisabled($is_self)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'is_disabled', 1, 'Disabled', $user->getIsDisabled()) ->setDisabled($is_self)) ->appendChild( id(new AphrontFormCheckboxControl()) ->addCheckbox( 'is_agent', 1, 'System Agent (Bot/Script User)', $user->getIsSystemAgent()) ->setDisabled(true)); if (!$is_self) { $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Edit Role')); } $panel = new AphrontPanelView(); $panel->setHeader('Edit Role'); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->appendChild($form); return array($error_view, $panel); } private function processCertificateRequest($user) { $request = $this->getRequest(); $admin = $request->getUser(); $form = new AphrontFormView(); $form ->setUser($admin) ->setAction($request->getRequestURI()) ->appendChild( '<p class="aphront-form-instructions">You can use this certificate '. 'to write scripts or bots which interface with Phabricator over '. 'Conduit.</p>'); if ($user->getIsSystemAgent()) { $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Username') ->setValue($user->getUsername())) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel('Certificate') ->setValue($user->getConduitCertificate())); } else { $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Certificate') ->setValue( 'You may only view the certificates of System Agents.')); } $panel = new AphrontPanelView(); $panel->setHeader('Conduit Certificate'); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->appendChild($form); 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 processDeleteRequest(PhabricatorUser $user) { $request = $this->getRequest(); $admin = $request->getUser(); if ($user->getPHID() == $admin->getPHID()) { $error = new AphrontErrorView(); $error->setTitle('You Shall Journey No Farther'); $error->appendChild( '<p>As you stare into the gaping maw of the abyss, something holds '. 'you back.</p>'. '<p>You can not delete your own account.</p>'); return $error; } $e_username = true; $username = null; $errors = array(); if ($request->isFormPost()) { $username = $request->getStr('username'); if (!strlen($username)) { $e_username = 'Required'; $errors[] = 'You must type the username to confirm deletion.'; } else if ($username != $user->getUsername()) { $e_username = 'Invalid'; $errors[] = 'You must type the username correctly.'; } if (!$errors) { id(new PhabricatorUserEditor()) ->setActor($admin) ->deleteUser($user); return id(new AphrontRedirectResponse())->setURI('/people/'); } } 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 deleting users!</strong> '. 'If this user interacted with anything, it is generally better '. 'to disable them, not delete them. If you delete them, it will '. 'no longer be possible to search for their objects, for example, '. 'and you will lose other information about their history. Disabling '. 'them instead will prevent them from logging in but not destroy '. 'any of their data.'. '</p>'. '<p class="aphront-form-instructions">'. 'It is generally safe to delete newly created users (and test users '. 'and so on), but less safe to delete established users. If '. 'possible, disable them instead.'. '</p>') ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Username') ->setValue($user->getUsername())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Confirm') ->setValue($username) ->setName('username') ->setCaption("Type the username again to confirm deletion.") ->setError($e_username)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Delete User')); $panel = new AphrontPanelView(); $panel->setHeader('Delete User'); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->appendChild($form); return array($errors, $panel); } private function getRoleInstructions() { $roles_link = phutil_render_tag( 'a', array( 'href' => PhabricatorEnv::getDoclink( 'article/User_Guide_Account_Roles.html'), 'target' => '_blank', ), 'User Guide: Account Roles'); return '<p class="aphront-form-instructions">'. 'For a detailed explanation of account roles, see '. $roles_link.'.'. '</p>'; } } diff --git a/src/applications/people/storage/PhabricatorUserLog.php b/src/applications/people/storage/PhabricatorUserLog.php index 7e8a7df5e..238165de7 100644 --- a/src/applications/people/storage/PhabricatorUserLog.php +++ b/src/applications/people/storage/PhabricatorUserLog.php @@ -1,118 +1,119 @@ <?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. */ final class PhabricatorUserLog extends PhabricatorUserDAO { const ACTION_LOGIN = 'login'; const ACTION_LOGOUT = 'logout'; const ACTION_LOGIN_FAILURE = 'login-fail'; const ACTION_RESET_PASSWORD = 'reset-pass'; const ACTION_CREATE = 'create'; const ACTION_EDIT = 'edit'; const ACTION_ADMIN = 'admin'; + const ACTION_SYSTEM_AGENT = 'system-agent'; const ACTION_DISABLE = 'disable'; const ACTION_DELETE = 'delete'; const ACTION_CONDUIT_CERTIFICATE = 'conduit-cert'; const ACTION_CONDUIT_CERTIFICATE_FAILURE = 'conduit-cert-fail'; const ACTION_EMAIL_PRIMARY = 'email-primary'; const ACTION_EMAIL_REMOVE = 'email-remove'; const ACTION_EMAIL_ADD = 'email-add'; const ACTION_CHANGE_PASSWORD = 'change-password'; const ACTION_CHANGE_USERNAME = 'change-username'; protected $actorPHID; protected $userPHID; protected $action; protected $oldValue; protected $newValue; protected $details = array(); protected $remoteAddr; protected $session; public static function newLog( PhabricatorUser $actor = null, PhabricatorUser $user = null, $action) { $log = new PhabricatorUserLog(); if ($actor) { $log->setActorPHID($actor->getPHID()); } if ($user) { $log->setUserPHID($user->getPHID()); } else { $log->setUserPHID(''); } if ($action) { $log->setAction($action); } return $log; } public static function loadRecentEventsFromThisIP($action, $timespan) { return id(new PhabricatorUserLog())->loadAllWhere( 'action = %s AND remoteAddr = %s AND dateCreated > %d ORDER BY dateCreated DESC', $action, idx($_SERVER, 'REMOTE_ADDR'), time() - $timespan); } public function save() { if (!$this->remoteAddr) { $this->remoteAddr = idx($_SERVER, 'REMOTE_ADDR', ''); } if (!$this->session) { $this->setSession(idx($_COOKIE, 'phsid')); } $this->details['host'] = php_uname('n'); $this->details['user_agent'] = idx($_SERVER, 'HTTP_USER_AGENT'); return parent::save(); } public function setSession($session) { // Store the hash of the session, not the actual session key, so that // seeing the logs doesn't compromise all the sessions which appear in // them. This just prevents casual leaks, like in a screenshot. if (strlen($session)) { $this->session = PhabricatorHash::digest($session); } return $this; } public function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( 'oldValue' => self::SERIALIZATION_JSON, 'newValue' => self::SERIALIZATION_JSON, 'details' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } }