diff --git a/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php b/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php --- a/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php +++ b/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php @@ -2,6 +2,10 @@ final class DiffusionSetPasswordPanel extends PhabricatorSettingsPanel { + public function isEditableByAdministrators() { + return true; + } + public function getPanelKey() { return 'vcspassword'; } @@ -19,7 +23,8 @@ } public function processRequest(AphrontRequest $request) { - $user = $request->getUser(); + $viewer = $request->getUser(); + $user = $this->getUser(); $vcspassword = id(new PhabricatorRepositoryVCSPassword()) ->loadOneWhere( @@ -68,7 +73,12 @@ $e_password = pht('Does Not Match'); $e_confirm = pht('Does Not Match'); $errors[] = pht('Password and confirmation do not match.'); - } else if ($user->comparePassword($envelope)) { + } else if ($viewer->comparePassword($envelope)) { + // NOTE: The above test is against $viewer (not $user), so that the + // error message below makes sense in the case that the two are + // different, and because an admin reusing their own password is bad, + // while system agents generally do not have passwords anyway. + $e_password = pht('Not Unique'); $e_confirm = pht('Not Unique'); $errors[] = pht( @@ -97,7 +107,7 @@ $title = pht('Set VCS Password'); $form = id(new AphrontFormView()) - ->setUser($user) + ->setUser($viewer) ->appendRemarkupInstructions( pht( 'To access repositories hosted by Phabricator over HTTP, you must '. @@ -193,7 +203,7 @@ ->setFormErrors($errors); $remove_form = id(new AphrontFormView()) - ->setUser($user); + ->setUser($viewer); if ($vcspassword->getID()) { $remove_form diff --git a/src/applications/people/controller/PhabricatorPeopleEditController.php b/src/applications/people/controller/PhabricatorPeopleEditController.php --- a/src/applications/people/controller/PhabricatorPeopleEditController.php +++ b/src/applications/people/controller/PhabricatorPeopleEditController.php @@ -35,7 +35,6 @@ $nav->setBaseURI(new PhutilURI($base_uri)); $nav->addLabel(pht('User Information')); $nav->addFilter('basic', pht('Basic Information')); - $nav->addFilter('cert', pht('Conduit Certificate')); $nav->addFilter('profile', pht('View Profile'), '/p/'.$user->getUsername().'/'); @@ -60,9 +59,6 @@ case 'basic': $response = $this->processBasicRequest($user); break; - case 'cert': - $response = $this->processCertificateRequest($user); - break; default: return new Aphront404Response(); } @@ -327,47 +323,6 @@ return array($form_box); } - private function processCertificateRequest($user) { - $request = $this->getRequest(); - $admin = $request->getUser(); - - $inst = pht('You can use this certificate '. - 'to write scripts or bots which interface with Phabricator over '. - 'Conduit.'); - $form = new AphrontFormView(); - $form - ->setUser($admin) - ->setAction($request->getRequestURI()) - ->appendChild( - phutil_tag('p', array('class' => 'aphront-form-instructions'), $inst)); - - if ($user->getIsSystemAgent()) { - $form - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('Username')) - ->setValue($user->getUsername())) - ->appendChild( - id(new AphrontFormTextAreaControl()) - ->setLabel(pht('Certificate')) - ->setValue($user->getConduitCertificate())); - } else { - $form->appendChild( - id(new AphrontFormStaticControl()) - ->setLabel(pht('Certificate')) - ->setValue( - pht('You may only view the certificates of System Agents.'))); - } - - $title = pht('Conduit Certificate'); - - $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) - ->setForm($form); - - return array($form_box); - } - private function getRoleInstructions() { $roles_link = phutil_tag( 'a', diff --git a/src/applications/people/controller/PhabricatorPeopleProfileController.php b/src/applications/people/controller/PhabricatorPeopleProfileController.php --- a/src/applications/people/controller/PhabricatorPeopleProfileController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileController.php @@ -64,6 +64,14 @@ ->setWorkflow(!$can_edit)); if ($viewer->getIsAdmin()) { + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon('wrench') + ->setName(pht('Edit Settings')) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setHref('/settings/'.$user->getID().'/')); + if ($user->getIsAdmin()) { $empower_icon = 'lower-priority'; $empower_name = pht('Remove Administrator'); diff --git a/src/applications/settings/application/PhabricatorApplicationSettings.php b/src/applications/settings/application/PhabricatorApplicationSettings.php --- a/src/applications/settings/application/PhabricatorApplicationSettings.php +++ b/src/applications/settings/application/PhabricatorApplicationSettings.php @@ -21,7 +21,8 @@ public function getRoutes() { return array( '/settings/' => array( - '(?:panel/(?P[^/]+)/)?' => 'PhabricatorSettingsMainController', + '(?:(?P\d+)/)?(?:panel/(?P[^/]+)/)?' + => 'PhabricatorSettingsMainController', 'adjust/' => 'PhabricatorSettingsAdjustController', ), ); diff --git a/src/applications/settings/controller/PhabricatorSettingsMainController.php b/src/applications/settings/controller/PhabricatorSettingsMainController.php --- a/src/applications/settings/controller/PhabricatorSettingsMainController.php +++ b/src/applications/settings/controller/PhabricatorSettingsMainController.php @@ -3,14 +3,48 @@ final class PhabricatorSettingsMainController extends PhabricatorController { + private $id; private $key; + private $user; + + private function getUser() { + return $this->user; + } + + private function isSelf() { + $viewer_phid = $this->getRequest()->getUser()->getPHID(); + $user_phid = $this->getUser()->getPHID(); + return ($viewer_phid == $user_phid); + } public function willProcessRequest(array $data) { + $this->id = idx($data, 'id'); $this->key = idx($data, 'key'); } public function processRequest() { $request = $this->getRequest(); + $viewer = $request->getUser(); + + if ($this->id) { + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + + if (!$user) { + return new Aphront404Response(); + } + + $this->user = $user; + } else { + $this->user = $viewer; + } $panels = $this->buildPanels(); $nav = $this->renderSideNav($panels); @@ -19,13 +53,27 @@ $panel = $panels[$key]; + $panel->setUser($this->getUser()); + $panel->setViewer($viewer); $response = $panel->processRequest($request); if ($response instanceof AphrontResponse) { return $response; } - $nav->appendChild($response); + $crumbs = $this->buildApplicationCrumbs(); + if (!$this->isSelf()) { + $crumbs->addTextCrumb( + $this->getUser()->getUsername(), + '/p/'.$this->getUser()->getUsername().'/'); + } + $crumbs->addTextCrumb($panel->getPanelName()); + $nav->appendChild( + array( + $crumbs, + $response, + )); + return $this->buildApplicationPage( $nav, array( @@ -54,6 +102,13 @@ if (!$panel->isEnabled()) { continue; } + + if (!$this->isSelf()) { + if (!$panel->isEditableByAdministrators()) { + continue; + } + } + if (!empty($result[$key])) { throw new Exception(pht( "Two settings panels share the same panel key ('%s'): %s, %s.", @@ -61,17 +116,29 @@ get_class($panel), get_class($result[$key]))); } + $result[$key] = $panel; } $result = msort($result, 'getPanelSortKey'); + if (!$result) { + throw new Exception(pht('No settings panels are available.')); + } + return $result; } private function renderSideNav(array $panels) { $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI($this->getApplicationURI('/panel/'))); + + if ($this->isSelf()) { + $base_uri = 'panel/'; + } else { + $base_uri = $this->getUser()->getID().'/panel/'; + } + + $nav->setBaseURI(new PhutilURI($this->getApplicationURI($base_uri))); $group = null; foreach ($panels as $panel) { diff --git a/src/applications/settings/panel/PhabricatorSettingsPanel.php b/src/applications/settings/panel/PhabricatorSettingsPanel.php --- a/src/applications/settings/panel/PhabricatorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanel.php @@ -17,6 +17,27 @@ */ abstract class PhabricatorSettingsPanel { + private $user; + private $viewer; + + public function setUser(PhabricatorUser $user) { + $this->user = $user; + return $this; + } + + public function getUser() { + return $this->user; + } + + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + /* -( Panel Configuration )------------------------------------------------ */ @@ -86,6 +107,18 @@ } + /** + * Return true if this panel is available to administrators while editing + * system agent accounts. + * + * @return bool True to enable edit by administrators. + * @task config + */ + public function isEditableByAdministrators() { + return false; + } + + /* -( Panel Implementation )----------------------------------------------- */ @@ -117,7 +150,15 @@ final public function getPanelURI($path = '') { $key = $this->getPanelKey(); $key = phutil_escape_uri($key); - return '/settings/panel/'.$key.'/'.ltrim($path, '/'); + + $path = ltrim($path, '/'); + + if ($this->getUser()->getPHID() != $this->getViewer()->getPHID()) { + $user_id = $this->getUser()->getID(); + return "/settings/{$user_id}/panel/{$key}/{$path}"; + } else { + return "/settings/panel/{$key}/{$path}"; + } } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelConduit.php b/src/applications/settings/panel/PhabricatorSettingsPanelConduit.php --- a/src/applications/settings/panel/PhabricatorSettingsPanelConduit.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelConduit.php @@ -3,12 +3,16 @@ final class PhabricatorSettingsPanelConduit extends PhabricatorSettingsPanel { + public function isEditableByAdministrators() { + return true; + } + public function getPanelKey() { return 'conduit'; } public function getPanelName() { - return pht('Conduit'); + return pht('Conduit Certificate'); } public function getPanelGroup() { @@ -16,12 +20,13 @@ } public function processRequest(AphrontRequest $request) { - $user = $request->getUser(); + $user = $this->getUser(); + $viewer = $request->getUser(); if ($request->isFormPost()) { if (!$request->isDialogFormPost()) { $dialog = new AphrontDialogView(); - $dialog->setUser($user); + $dialog->setUser($viewer); $dialog->setTitle(pht('Really regenerate session?')); $dialog->setSubmitURI($this->getPanelURI()); $dialog->addSubmitButton(pht('Regenerate')); @@ -69,7 +74,7 @@ $cert_form = new AphrontFormView(); $cert_form - ->setUser($user) + ->setUser($viewer) ->appendChild(phutil_tag( 'p', array('class' => 'aphront-form-instructions'), @@ -93,7 +98,7 @@ $regen_form = new AphrontFormView(); $regen_form - ->setUser($user) + ->setUser($viewer) ->setAction($this->getPanelURI()) ->setWorkflow(true) ->appendChild(phutil_tag( diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php --- a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php @@ -3,6 +3,10 @@ final class PhabricatorSettingsPanelSSHKeys extends PhabricatorSettingsPanel { + public function isEditableByAdministrators() { + return true; + } + public function getPanelKey() { return 'ssh'; } @@ -20,8 +24,8 @@ } public function processRequest(AphrontRequest $request) { - - $user = $request->getUser(); + $viewer = $request->getUser(); + $user = $this->getUser(); $generate = $request->getStr('generate'); if ($generate) { @@ -37,7 +41,7 @@ $id = nonempty($edit, $delete); if ($id && is_numeric($id)) { - // NOTE: Prevent editing/deleting of keys you don't own. + // NOTE: This prevents editing/deleting of keys not owned by the user. $key = id(new PhabricatorUserSSHKey())->loadOneWhere( 'userPHID = %s AND id = %d', $user->getPHID(), @@ -142,7 +146,7 @@ } $form = id(new AphrontFormView()) - ->setUser($user) + ->setUser($viewer) ->addHiddenInput('edit', $is_new ? 'true' : $key->getID()) ->appendChild( id(new AphrontFormTextControl()) @@ -170,8 +174,8 @@ } private function renderKeyListView(AphrontRequest $request) { - - $user = $request->getUser(); + $user = $this->getUser(); + $viewer = $request->getUser(); $keys = id(new PhabricatorUserSSHKey())->loadAllWhere( 'userPHID = %s', @@ -188,8 +192,8 @@ $key->getName()), $key->getKeyComment(), $key->getKeyType(), - phabricator_date($key->getDateCreated(), $user), - phabricator_time($key->getDateCreated(), $user), + phabricator_date($key->getDateCreated(), $viewer), + phabricator_time($key->getDateCreated(), $viewer), javelin_tag( 'a', array( @@ -266,7 +270,8 @@ AphrontRequest $request, PhabricatorUserSSHKey $key) { - $user = $request->getUser(); + $viewer = $request->getUser(); + $user = $this->getUser(); $name = phutil_tag('strong', array(), $key->getName()); @@ -277,7 +282,7 @@ } $dialog = id(new AphrontDialogView()) - ->setUser($user) + ->setUser($viewer) ->addHiddenInput('delete', $key->getID()) ->setTitle(pht('Really delete SSH Public Key?')) ->appendChild(phutil_tag('p', array(), pht( @@ -291,10 +296,12 @@ ->setDialog($dialog); } - private function processGenerate( - AphrontRequest $request) { + private function processGenerate(AphrontRequest $request) { + $user = $this->getUser(); $viewer = $request->getUser(); + $is_self = ($user->getPHID() == $viewer->getPHID()); + if ($request->isFormPost()) { $keys = PhabricatorSSHKeyGenerator::generateKeypair(); list($public_key, $private_key) = $keys; @@ -308,7 +315,7 @@ )); $key = id(new PhabricatorUserSSHKey()) - ->setUserPHID($viewer->getPHID()) + ->setUserPHID($user->getPHID()) ->setName('id_rsa_phabricator') ->setKeyType('rsa') ->setKeyBody($public_key) @@ -320,6 +327,17 @@ // disabling workflow on cancel so the page reloads, showing the new // key. + if ($is_self) { + $what_happened = pht( + 'The public key has been associated with your Phabricator '. + 'account. Use the button below to download the private key.'); + } else { + $what_happened = pht( + 'The public key has been associated with the %s account. '. + 'Use the button below to download the private key.', + phutil_tag('strong', array(), $user->getUsername())); + } + $dialog = id(new AphrontDialogView()) ->setTitle(pht('Download Private Key')) ->setUser($viewer) @@ -329,10 +347,7 @@ ->appendParagraph( pht( 'Successfully generated a new keypair.')) - ->appendParagraph( - pht( - 'The public key has been associated with your Phabricator '. - 'account. Use the button below to download the private key.')) + ->appendParagraph($what_happened) ->appendParagraph( pht( 'After you download the private key, it will be destroyed. '. @@ -350,13 +365,22 @@ try { PhabricatorSSHKeyGenerator::assertCanGenerateKeypair(); + + if ($is_self) { + $explain = pht( + 'This will generate an SSH keypair, associate the public key '. + 'with your account, and let you download the private key.'); + } else { + $explain = pht( + 'This will generate an SSH keypair, associate the public key with '. + 'the %s account, and let you download the private key.', + phutil_tag('strong', array(), $user->getUsername())); + } + $dialog ->addHiddenInput('generate', true) ->setTitle(pht('Generate New Keypair')) - ->appendParagraph( - pht( - "This will generate an SSH keypair, associate the public key ". - "with your account, and let you download the private key.")) + ->appendParagraph($explain) ->appendParagraph( pht( "Phabricator will not retain a copy of the private key."))