diff --git a/resources/sql/patches/20131031.vcspassword.sql b/resources/sql/patches/20131031.vcspassword.sql new file mode 100644 index 0000000000..017600625c --- /dev/null +++ b/resources/sql/patches/20131031.vcspassword.sql @@ -0,0 +1,8 @@ +CREATE TABLE {$NAMESPACE}_repository.repository_vcspassword ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + userPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + passwordHash VARCHAR(50) NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (userPHID) +) ENGINE=InnoDB, CHARSET utf8; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 58872db41d..fb1be64b3d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -535,6 +535,7 @@ phutil_register_library_map(array( 'DiffusionSSHGitUploadPackWorkflow' => 'applications/diffusion/ssh/DiffusionSSHGitUploadPackWorkflow.php', 'DiffusionSSHGitWorkflow' => 'applications/diffusion/ssh/DiffusionSSHGitWorkflow.php', 'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php', + 'DiffusionSetPasswordPanel' => 'applications/diffusion/panel/DiffusionSetPasswordPanel.php', 'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php', 'DiffusionStableCommitNameQuery' => 'applications/diffusion/query/stablecommitname/DiffusionStableCommitNameQuery.php', 'DiffusionSvnCommitParentsQuery' => 'applications/diffusion/query/parents/DiffusionSvnCommitParentsQuery.php', @@ -1674,6 +1675,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php', 'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php', 'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php', + 'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php', 'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php', 'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php', 'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php', @@ -2729,6 +2731,7 @@ phutil_register_library_map(array( 'DiffusionSSHGitUploadPackWorkflow' => 'DiffusionSSHGitWorkflow', 'DiffusionSSHGitWorkflow' => 'DiffusionSSHWorkflow', 'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow', + 'DiffusionSetPasswordPanel' => 'PhabricatorSettingsPanel', 'DiffusionSetupException' => 'AphrontUsageException', 'DiffusionStableCommitNameQuery' => 'DiffusionQuery', 'DiffusionSvnCommitParentsQuery' => 'DiffusionCommitParentsQuery', @@ -4015,6 +4018,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryTestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO', 'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorSSHWorkflow' => 'PhutilArgumentWorkflow', 'PhabricatorSavedQuery' => diff --git a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php index 3d6c38384b..de29a507d6 100644 --- a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php +++ b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php @@ -84,6 +84,24 @@ final class PhabricatorDiffusionConfigOptions 'Regular expression to link external bug tracker. See '. 'http://tortoisesvn.net/docs/release/TortoiseSVN_en/'. 'tsvn-dug-bugtracker.html for further explanation.')), + $this->newOption('diffusion.allow-http-auth', 'bool', false) + ->setBoolOptions( + array( + pht('Allow HTTP Basic Auth'), + pht('Disable HTTP Basic Auth'), + )) + ->setSummary(pht('Enable HTTP Basic Auth for repositories.')) + ->setDescription( + pht( + "Phabricator can serve repositories over HTTP, using HTTP basic ". + "auth.\n\n". + "Because HTTP basic auth is less secure than SSH auth, it is ". + "disabled by default. You can enable it here if you'd like to use ". + "it anyway. There's nothing fundamentally insecure about it as ". + "long as Phabricator uses HTTPS, but it presents a much lower ". + "barrier to attackers than SSH does.\n\n". + "Consider using SSH for authenticated access to repositories ". + "instead of HTTP.")) ); } diff --git a/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php b/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php new file mode 100644 index 0000000000..ece9468e40 --- /dev/null +++ b/src/applications/diffusion/panel/DiffusionSetPasswordPanel.php @@ -0,0 +1,209 @@ +getUser(); + + $vcspassword = id(new PhabricatorRepositoryVCSPassword()) + ->loadOneWhere( + 'userPHID = %s', + $user->getPHID()); + if (!$vcspassword) { + $vcspassword = id(new PhabricatorRepositoryVCSPassword()); + $vcspassword->setUserPHID($user->getPHID()); + } + + $panel_uri = $this->getPanelURI('?saved=true'); + + $errors = array(); + + $e_password = true; + $e_confirm = true; + + if ($request->isFormPost()) { + if ($request->getBool('remove')) { + if ($vcspassword->getID()) { + $vcspassword->delete(); + return id(new AphrontRedirectResponse())->setURI($panel_uri); + } + } + + $new_password = $request->getStr('password'); + $confirm = $request->getStr('confirm'); + if (!strlen($new_password)) { + $e_password = pht('Required'); + $errors[] = pht('Password is required.'); + } else { + $e_password = null; + } + + if (!strlen($confirm)) { + $e_confirm = pht('Required'); + $errors[] = pht('You must confirm the new password.'); + } else { + $e_confirm = null; + } + + if (!$errors) { + $envelope = new PhutilOpaqueEnvelope($new_password); + + if ($new_password !== $confirm) { + $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)) { + $e_password = pht('Not Unique'); + $e_confirm = pht('Not Unique'); + $errors[] = pht( + 'This password is not unique. You must use a unique password.'); + } + + if (!$errors) { + $vcspassword->setPassword($envelope, $user); + $vcspassword->save(); + + return id(new AphrontRedirectResponse())->setURI($panel_uri); + } + } + } + + $title = pht('Set VCS Password'); + + $error_view = null; + if ($errors) { + $error_view = id(new AphrontErrorView()) + ->setTitle(pht('Form Errors')) + ->setErrors($errors); + } + + $form = id(new AphrontFormView()) + ->setUser($user) + ->appendRemarkupInstructions( + pht( + 'To access repositories hosted by Phabricator over HTTP, you must '. + 'set a version control password. This password should be unique.'. + "\n\n". + "This password applies to all repositories available over ". + "HTTP.")); + + if ($vcspassword->getID()) { + $form + ->appendChild( + id(new AphrontFormPasswordControl()) + ->setLabel(pht('Current Password')) + ->setDisabled(true) + ->setValue('********************')); + } else { + $form + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Current Password')) + ->setValue(phutil_tag('em', array(), pht('No Password Set')))); + } + + $form + ->appendChild( + id(new AphrontFormPasswordControl()) + ->setName('password') + ->setLabel(pht('New VCS Password')) + ->setError($e_password)) + ->appendChild( + id(new AphrontFormPasswordControl()) + ->setName('confirm') + ->setLabel(pht('Confirm VCS Password')) + ->setError($e_confirm)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Change Password'))); + + + if (!$vcspassword->getID()) { + $is_serious = PhabricatorEnv::getEnvConfig( + 'phabricator.serious-business'); + + $suggest = Filesystem::readRandomBytes(128); + $suggest = preg_replace('([^A-Za-z0-9/!().,;{}^&*%~])', '', $suggest); + $suggest = substr($suggest, 0, 20); + + if ($is_serious) { + $form->appendRemarkupInstructions( + pht( + 'Having trouble coming up with a good password? Try this randomly '. + 'generated one, made by a computer:'. + "\n\n". + "`%s`", + $suggest)); + } else { + $form->appendRemarkupInstructions( + pht( + 'Having trouble coming up with a good password? Try this '. + 'artisinal password, hand made in small batches by our expert '. + 'craftspeople: '. + "\n\n". + "`%s`", + $suggest)); + } + } + + $object_box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->setForm($form) + ->setFormError($error_view); + + $remove_form = id(new AphrontFormView()) + ->setUser($user); + + if ($vcspassword->getID()) { + $remove_form + ->addHiddenInput('remove', true) + ->appendRemarkupInstructions( + pht( + 'You can remove your VCS password, which will prevent your '. + 'account from accessing repositories.')) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Remove Password'))); + } else { + $remove_form->appendRemarkupInstructions( + pht( + 'You do not currently have a VCS password set. If you set one, you '. + 'can remove it here later.')); + } + + $remove_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Remove VCS Password')) + ->setForm($remove_form); + + $saved = null; + if ($request->getBool('saved')) { + $saved = id(new AphrontErrorView()) + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) + ->setTitle(pht('Password Updated')) + ->appendChild(pht('Your VCS password has been updated.')); + } + + return array( + $saved, + $object_box, + $remove_box, + ); + } + +} diff --git a/src/applications/repository/storage/PhabricatorRepositoryVCSPassword.php b/src/applications/repository/storage/PhabricatorRepositoryVCSPassword.php new file mode 100644 index 0000000000..ffdd65b958 --- /dev/null +++ b/src/applications/repository/storage/PhabricatorRepositoryVCSPassword.php @@ -0,0 +1,34 @@ +setPasswordHash($this->hashPassword($password, $user)); + } + + public function comparePassword( + PhutilOpaqueEnvelope $password, + PhabricatorUser $user) { + + $hash = $this->hashPassword($password, $user); + return ($hash == $this->getPasswordHash()); + } + + private function hashPassword( + PhutilOpaqueEnvelope $password, + PhabricatorUser $user) { + + if ($user->getPHID() != $this->getUserPHID()) { + throw new Exception("User does not match password user PHID!"); + } + + return PhabricatorHash::digestPassword($password, $user->getPHID()); + } + +} diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index d738b20172..f6bb232bd8 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -1716,6 +1716,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'type' => 'sql', 'name' => $this->getPatchPath('20131030.repostatusmessage.sql'), ), + '20131031.vcspassword.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('20131031.vcspassword.sql'), + ), ); } } diff --git a/src/infrastructure/util/PhabricatorHash.php b/src/infrastructure/util/PhabricatorHash.php index dc86813b56..1ba1bde4f8 100644 --- a/src/infrastructure/util/PhabricatorHash.php +++ b/src/infrastructure/util/PhabricatorHash.php @@ -23,6 +23,24 @@ final class PhabricatorHash { } + /** + * Digest a string into a password hash. This is similar to @{method:digest}, + * but requires a salt and iterates the hash to increase cost. + */ + public static function digestPassword(PhutilOpaqueEnvelope $envelope, $salt) { + $result = $envelope->openEnvelope(); + if (!$result) { + throw new Exception("Trying to digest empty password!"); + } + + for ($ii = 0; $ii < 1000; $ii++) { + $result = PhabricatorHash::digest($result, $salt); + } + + return $result; + } + + /** * Digest a string for use in, e.g., a MySQL index. This produces a short * (12-byte), case-sensitive alphanumeric string with 72 bits of entropy,