diff --git a/resources/sql/patches/064.subporjects.sql b/resources/sql/patches/064.subprojects.sql similarity index 95% rename from resources/sql/patches/064.subporjects.sql rename to resources/sql/patches/064.subprojects.sql index 379e1bdc77..acb722c380 100644 --- a/resources/sql/patches/064.subporjects.sql +++ b/resources/sql/patches/064.subprojects.sql @@ -2,10 +2,10 @@ ALTER TABLE phabricator_project.project ADD subprojectPHIDs longblob NOT NULL; UPDATE phabricator_project.project SET subprojectPHIDs = '[]'; - + CREATE TABLE phabricator_project.project_subproject ( projectPHID varchar(64) BINARY NOT NULL, subprojectPHID varchar(64) BINARY NOT NULL, PRIMARY KEY (subprojectPHID, projectPHID), UNIQUE KEY (projectPHID, subprojectPHID) -); \ No newline at end of file +) ENGINE=InnoDB; \ No newline at end of file diff --git a/resources/sql/patches/065.sshkeys.sql b/resources/sql/patches/065.sshkeys.sql new file mode 100644 index 0000000000..a475eb93f1 --- /dev/null +++ b/resources/sql/patches/065.sshkeys.sql @@ -0,0 +1,12 @@ +CREATE TABLE phabricator_user.user_sshkey ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + userPHID varchar(64) BINARY NOT NULL, + key (userPHID), + name varchar(255), + keyType varchar(255), + keyBody varchar(32768) BINARY, + unique key (keyBody(512)), + keyComment varchar(255), + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index df5e7a6a2a..ac17b92b1c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -576,6 +576,8 @@ phutil_register_library_map(array( 'PhabricatorUserOAuthSettingsPanelController' => 'applications/people/controller/settings/panels/oauth', 'PhabricatorUserPreferences' => 'applications/people/storage/preferences', 'PhabricatorUserProfile' => 'applications/people/storage/profile', + 'PhabricatorUserSSHKey' => 'applications/people/storage/usersshkey', + 'PhabricatorUserSSHKeysSettingsPanelController' => 'applications/people/controller/settings/panels/sshkeys', 'PhabricatorUserSettingsController' => 'applications/people/controller/settings', 'PhabricatorUserSettingsPanelController' => 'applications/people/controller/settings/panels/base', 'PhabricatorWorker' => 'infrastructure/daemon/workers/worker', @@ -1094,6 +1096,8 @@ phutil_register_library_map(array( 'PhabricatorUserOAuthSettingsPanelController' => 'PhabricatorUserSettingsPanelController', 'PhabricatorUserPreferences' => 'PhabricatorUserDAO', 'PhabricatorUserProfile' => 'PhabricatorUserDAO', + 'PhabricatorUserSSHKey' => 'PhabricatorUserDAO', + 'PhabricatorUserSSHKeysSettingsPanelController' => 'PhabricatorUserSettingsPanelController', 'PhabricatorUserSettingsController' => 'PhabricatorPeopleController', 'PhabricatorUserSettingsPanelController' => 'PhabricatorPeopleController', 'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO', diff --git a/src/applications/people/controller/settings/PhabricatorUserSettingsController.php b/src/applications/people/controller/settings/PhabricatorUserSettingsController.php index e164daf6f4..e61a4088e2 100644 --- a/src/applications/people/controller/settings/PhabricatorUserSettingsController.php +++ b/src/applications/people/controller/settings/PhabricatorUserSettingsController.php @@ -34,6 +34,7 @@ class PhabricatorUserSettingsController extends PhabricatorPeopleController { 'email' => 'Email', // 'password' => 'Password', 'conduit' => 'Conduit Certificate', + 'sshkeys' => 'SSH Public Keys', ); $oauth_providers = PhabricatorOAuthProvider::getAllProviders(); @@ -60,6 +61,9 @@ class PhabricatorUserSettingsController extends PhabricatorPeopleController { case 'conduit': $delegate = new PhabricatorUserConduitSettingsPanelController($request); break; + case 'sshkeys': + $delegate = new PhabricatorUserSSHKeysSettingsPanelController($request); + break; default: if (empty($this->pages[$this->page])) { return new Aphront404Response(); diff --git a/src/applications/people/controller/settings/__init__.php b/src/applications/people/controller/settings/__init__.php index 67477364d7..3c0f51f711 100644 --- a/src/applications/people/controller/settings/__init__.php +++ b/src/applications/people/controller/settings/__init__.php @@ -13,6 +13,7 @@ phutil_require_module('phabricator', 'applications/people/controller/settings/pa phutil_require_module('phabricator', 'applications/people/controller/settings/panels/conduit'); phutil_require_module('phabricator', 'applications/people/controller/settings/panels/email'); phutil_require_module('phabricator', 'applications/people/controller/settings/panels/oauth'); +phutil_require_module('phabricator', 'applications/people/controller/settings/panels/sshkeys'); phutil_require_module('phabricator', 'view/layout/sidenav'); phutil_require_module('phutil', 'markup'); diff --git a/src/applications/people/controller/settings/panels/sshkeys/PhabricatorUserSSHKeysSettingsPanelController.php b/src/applications/people/controller/settings/panels/sshkeys/PhabricatorUserSSHKeysSettingsPanelController.php new file mode 100644 index 0000000000..a30670d657 --- /dev/null +++ b/src/applications/people/controller/settings/panels/sshkeys/PhabricatorUserSSHKeysSettingsPanelController.php @@ -0,0 +1,264 @@ +getRequest(); + $user = $request->getUser(); + + $edit = $request->getStr('edit'); + $delete = $request->getStr('delete'); + if (!$edit && !$delete) { + return $this->renderKeyListView(); + } + + $id = nonempty($edit, $delete); + + if ($id && is_numeric($id)) { + // NOTE: Prevent editing/deleting of keys you don't own. + $key = id(new PhabricatorUserSSHKey())->loadOneWhere( + 'userPHID = %s AND id = %d', + $user->getPHID(), + $id); + if (!$key) { + return new Aphront404Response(); + } + } else { + $key = new PhabricatorUserSSHKey(); + $key->setUserPHID($user->getPHID()); + } + + if ($delete) { + return $this->processDelete($key); + } + + $e_name = true; + $e_key = true; + $errors = array(); + $entire_key = $key->getEntireKey(); + if ($request->isFormPost()) { + $key->setName($request->getStr('name')); + $entire_key = $request->getStr('key'); + + if (!strlen($entire_key)) { + $errors[] = 'You must provide an SSH Public Key.'; + $e_key = 'Required'; + } else { + $parts = str_replace("\n", '', trim($entire_key)); + $parts = preg_split('/\s+/', $parts); + if (count($parts) == 2) { + $parts[] = ''; // Add an empty comment part. + } else if (count($parts) == 3) { + // This is the expected case. + } else { + if (preg_match('/private\s*key/i', $entire_key)) { + // Try to give the user a better error message if it looks like + // they uploaded a private key. + $e_key = 'Invalid'; + $errors[] = 'Provide your public key, not your private key!'; + } else { + $e_key = 'Invalid'; + $errors[] = 'Provided public key is not properly formatted.'; + } + } + + if (!$errors) { + list($type, $body, $comment) = $parts; + if (!preg_match('/^ssh-dsa|ssh-rsa$/', $type)) { + $e_key = 'Invalid'; + $errors[] = 'Public key should be "ssh-dsa" or "ssh-rsa".'; + } else { + $key->setKeyType($type); + $key->setKeyBody($body); + $key->setKeyComment($comment); + + $e_key = null; + } + } + } + + if (!strlen($key->getName())) { + $errors[] = 'You must name this public key.'; + $e_name = 'Required'; + } else { + $e_name = null; + } + + if (!$errors) { + try { + $key->save(); + return id(new AphrontRedirectResponse()) + ->setURI(self::PANEL_BASE_URI); + } catch (AphrontQueryDuplicateKeyException $ex) { + $e_key = 'Duplicate'; + $errors[] = 'This public key is already associated with a user '. + 'account.'; + } + } + } + + $error_view = null; + if ($errors) { + $error_view = new AphrontErrorView(); + $error_view->setTitle('Form Errors'); + $error_view->setErrors($errors); + } + + $is_new = !$key->getID(); + + if ($is_new) { + $header = 'Add New SSH Public Key'; + $save = 'Add Key'; + } else { + $header = 'Edit SSH Public Key'; + $save = 'Save Changes'; + } + + $form = id(new AphrontFormView()) + ->setUser($user) + ->addHiddenInput('edit', $is_new ? 'true' : $key->getID()) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Name') + ->setName('name') + ->setValue($key->getName()) + ->setError($e_name)) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel('Public Key') + ->setName('key') + ->setValue($entire_key) + ->setError($e_key)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton(self::PANEL_BASE_URI) + ->setValue($save)); + + $panel = new AphrontPanelView(); + $panel->setHeader($header); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + $panel->appendChild($form); + + return id(new AphrontNullView()) + ->appendChild( + array( + $error_view, + $panel, + )); + } + + private function renderKeyListView() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $keys = id(new PhabricatorUserSSHKey())->loadAllWhere( + 'userPHID = %s', + $user->getPHID()); + + $rows = array(); + foreach ($keys as $key) { + $rows[] = array( + phutil_render_tag( + 'a', + array( + 'href' => '/settings/page/sshkeys/?edit='.$key->getID(), + ), + phutil_escape_html($key->getName())), + phutil_escape_html($key->getKeyComment()), + phutil_escape_html($key->getKeyType()), + phabricator_date($key->getDateCreated(), $user), + phabricator_time($key->getDateCreated(), $user), + javelin_render_tag( + 'a', + array( + 'href' => '/settings/page/sshkeys/?delete='.$key->getID(), + 'class' => 'small grey button', + 'sigil' => 'workflow', + ), + 'Delete'), + ); + } + + $table = new AphrontTableView($rows); + $table->setNoDataString("You haven't added any SSH Public Keys."); + $table->setHeaders( + array( + 'Name', + 'Comment', + 'Type', + 'Created', + 'Time', + '', + )); + $table->setColumnClasses( + array( + 'wide pri', + '', + '', + '', + 'right', + 'action', + )); + + $panel = new AphrontPanelView(); + $panel->addButton( + phutil_render_tag( + 'a', + array( + 'href' => '/settings/page/sshkeys/?edit=true', + 'class' => 'green button', + ), + 'Add New Public Key')); + $panel->setHeader('SSH Public Keys'); + $panel->appendChild($table); + + return $panel; + } + + private function processDelete(PhabricatorUserSSHKey $key) { + $request = $this->getRequest(); + $user = $request->getUser(); + + $name = phutil_escape_html($key->getName()); + + if ($request->isDialogFormPost()) { + $key->delete(); + return id(new AphrontReloadResponse()) + ->setURI(self::PANEL_BASE_URI); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($user) + ->addHiddenInput('delete', $key->getID()) + ->setTitle('Really delete SSH Public Key?') + ->appendChild( + '

The key "'.$name.'" will be permanently deleted, '. + 'and you will not longer be able to use the corresponding private key '. + 'to authenticate.

') + ->addSubmitButton('Delete Public Key') + ->addCancelButton(self::PANEL_BASE_URI); + + return id(new AphrontDialogResponse()) + ->setDialog($dialog); + } + +} diff --git a/src/applications/people/controller/settings/panels/sshkeys/__init__.php b/src/applications/people/controller/settings/panels/sshkeys/__init__.php new file mode 100644 index 0000000000..5784093da5 --- /dev/null +++ b/src/applications/people/controller/settings/panels/sshkeys/__init__.php @@ -0,0 +1,31 @@ +getKeyType(), + $this->getKeyBody(), + $this->getKeyComment(), + ); + return trim(implode(' ', $parts)); + } + +} diff --git a/src/applications/people/storage/usersshkey/__init__.php b/src/applications/people/storage/usersshkey/__init__.php new file mode 100644 index 0000000000..795399a9eb --- /dev/null +++ b/src/applications/people/storage/usersshkey/__init__.php @@ -0,0 +1,12 @@ +