From 91d084624b9c2c88f093ba4b244f6e3fdab8bcc6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 20 Nov 2013 09:13:35 -0800 Subject: [PATCH] Passphrase v0 Summary: Ref T4122. Implements a credential management application for the uses described in T4122. @chad, this needs an icon, HA HA HAHA HA BWW HA HA HA bwahaha Test Plan: See screenshots. Reviewers: btrahan, chad Reviewed By: btrahan CC: chad, aran Maniphest Tasks: T4122 Differential Revision: https://secure.phabricator.com/D7608 --- resources/sql/patches/20131119.passphrase.sql | 46 ++++ src/__phutil_library_map__.php | 52 ++++ .../editor/HarbormasterBuildPlanEditor.php | 11 +- .../PhabricatorApplicationPassphrase.php | 46 ++++ .../controller/PassphraseController.php | 40 +++ .../PassphraseCredentialCreateController.php | 78 ++++++ .../PassphraseCredentialDestroyController.php | 67 +++++ .../PassphraseCredentialEditController.php | 241 ++++++++++++++++++ .../PassphraseCredentialListController.php | 63 +++++ .../PassphraseCredentialRevealController.php | 75 ++++++ .../PassphraseCredentialViewController.php | 170 ++++++++++++ .../PassphraseCredentialType.php | 28 ++ .../PassphraseCredentialTypePassword.php | 30 +++ .../PassphraseCredentialTypeSSHPrivateKey.php | 10 + ...sphraseCredentialTypeSSHPrivateKeyFile.php | 26 ++ ...sphraseCredentialTypeSSHPrivateKeyText.php | 22 ++ .../PassphraseCredentialTransactionEditor.php | 164 ++++++++++++ .../phid/PassphrasePHIDTypeCredential.php | 76 ++++++ .../query/PassphraseCredentialQuery.php | 137 ++++++++++ .../PassphraseCredentialSearchEngine.php | 73 ++++++ .../PassphraseCredentialTransactionQuery.php | 10 + .../storage/PassphraseCredential.php | 75 ++++++ .../PassphraseCredentialTransaction.php | 106 ++++++++ .../passphrase/storage/PassphraseDAO.php | 9 + .../passphrase/storage/PassphraseSecret.php | 13 + .../constants/PhabricatorPolicyType.php | 8 +- .../policy/storage/PhabricatorPolicy.php | 10 +- ...habricatorApplicationTransactionEditor.php | 33 +++ .../patch/PhabricatorBuiltinPatchList.php | 8 + .../form/control/AphrontFormPolicyControl.php | 1 + 30 files changed, 1715 insertions(+), 13 deletions(-) create mode 100644 resources/sql/patches/20131119.passphrase.sql create mode 100644 src/applications/passphrase/application/PhabricatorApplicationPassphrase.php create mode 100644 src/applications/passphrase/controller/PassphraseController.php create mode 100644 src/applications/passphrase/controller/PassphraseCredentialCreateController.php create mode 100644 src/applications/passphrase/controller/PassphraseCredentialDestroyController.php create mode 100644 src/applications/passphrase/controller/PassphraseCredentialEditController.php create mode 100644 src/applications/passphrase/controller/PassphraseCredentialListController.php create mode 100644 src/applications/passphrase/controller/PassphraseCredentialRevealController.php create mode 100644 src/applications/passphrase/controller/PassphraseCredentialViewController.php create mode 100644 src/applications/passphrase/credentialtype/PassphraseCredentialType.php create mode 100644 src/applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php create mode 100644 src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php create mode 100644 src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php create mode 100644 src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php create mode 100644 src/applications/passphrase/editor/PassphraseCredentialTransactionEditor.php create mode 100644 src/applications/passphrase/phid/PassphrasePHIDTypeCredential.php create mode 100644 src/applications/passphrase/query/PassphraseCredentialQuery.php create mode 100644 src/applications/passphrase/query/PassphraseCredentialSearchEngine.php create mode 100644 src/applications/passphrase/query/PassphraseCredentialTransactionQuery.php create mode 100644 src/applications/passphrase/storage/PassphraseCredential.php create mode 100644 src/applications/passphrase/storage/PassphraseCredentialTransaction.php create mode 100644 src/applications/passphrase/storage/PassphraseDAO.php create mode 100644 src/applications/passphrase/storage/PassphraseSecret.php diff --git a/resources/sql/patches/20131119.passphrase.sql b/resources/sql/patches/20131119.passphrase.sql new file mode 100644 index 0000000000..3e1761867c --- /dev/null +++ b/resources/sql/patches/20131119.passphrase.sql @@ -0,0 +1,46 @@ +CREATE TABLE {$NAMESPACE}_passphrase.passphrase_credential ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + name VARCHAR(255) NOT NULL, + credentialType VARCHAR(64) NOT NULL COLLATE utf8_bin, + providesType VARCHAR(64) NOT NULL COLLATE utf8_bin, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + description LONGTEXT NOT NULL COLLATE utf8_bin, + username VARCHAR(255) NOT NULL, + secretID INT UNSIGNED, + isDestroyed BOOL NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + + UNIQUE KEY `key_phid` (phid), + KEY `key_type` (credentialType), + KEY `key_provides` (providesType), + UNIQUE KEY `key_secret` (secretID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_passphrase.passphrase_secret ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + secretData LONGBLOB NOT NULL +) ENGINE=InnoDB, COLLATE utf8_general_ci; + +CREATE TABLE {$NAMESPACE}_passphrase.passphrase_credentialtransaction ( + id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + phid VARCHAR(64) NOT NULL COLLATE utf8_bin, + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + objectPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, + viewPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + editPolicy VARCHAR(64) NOT NULL COLLATE utf8_bin, + commentPHID VARCHAR(64) COLLATE utf8_bin, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL COLLATE utf8_bin, + oldValue LONGTEXT NOT NULL COLLATE utf8_bin, + newValue LONGTEXT NOT NULL COLLATE utf8_bin, + contentSource LONGTEXT NOT NULL COLLATE utf8_bin, + metadata LONGTEXT NOT NULL COLLATE utf8_bin, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + + UNIQUE KEY `key_phid` (phid), + KEY `key_object` (objectPHID) +) ENGINE=InnoDB, COLLATE utf8_general_ci; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 57c6fde33b..fcb1ae4d1a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -943,6 +943,27 @@ phutil_register_library_map(array( 'PackageDeleteMail' => 'applications/owners/mail/PackageDeleteMail.php', 'PackageMail' => 'applications/owners/mail/PackageMail.php', 'PackageModifyMail' => 'applications/owners/mail/PackageModifyMail.php', + 'PassphraseController' => 'applications/passphrase/controller/PassphraseController.php', + 'PassphraseCredential' => 'applications/passphrase/storage/PassphraseCredential.php', + 'PassphraseCredentialCreateController' => 'applications/passphrase/controller/PassphraseCredentialCreateController.php', + 'PassphraseCredentialDestroyController' => 'applications/passphrase/controller/PassphraseCredentialDestroyController.php', + 'PassphraseCredentialEditController' => 'applications/passphrase/controller/PassphraseCredentialEditController.php', + 'PassphraseCredentialListController' => 'applications/passphrase/controller/PassphraseCredentialListController.php', + 'PassphraseCredentialQuery' => 'applications/passphrase/query/PassphraseCredentialQuery.php', + 'PassphraseCredentialRevealController' => 'applications/passphrase/controller/PassphraseCredentialRevealController.php', + 'PassphraseCredentialSearchEngine' => 'applications/passphrase/query/PassphraseCredentialSearchEngine.php', + 'PassphraseCredentialTransaction' => 'applications/passphrase/storage/PassphraseCredentialTransaction.php', + 'PassphraseCredentialTransactionEditor' => 'applications/passphrase/editor/PassphraseCredentialTransactionEditor.php', + 'PassphraseCredentialTransactionQuery' => 'applications/passphrase/query/PassphraseCredentialTransactionQuery.php', + 'PassphraseCredentialType' => 'applications/passphrase/credentialtype/PassphraseCredentialType.php', + 'PassphraseCredentialTypePassword' => 'applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php', + 'PassphraseCredentialTypeSSHPrivateKey' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php', + 'PassphraseCredentialTypeSSHPrivateKeyFile' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php', + 'PassphraseCredentialTypeSSHPrivateKeyText' => 'applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php', + 'PassphraseCredentialViewController' => 'applications/passphrase/controller/PassphraseCredentialViewController.php', + 'PassphraseDAO' => 'applications/passphrase/storage/PassphraseDAO.php', + 'PassphrasePHIDTypeCredential' => 'applications/passphrase/phid/PassphrasePHIDTypeCredential.php', + 'PassphraseSecret' => 'applications/passphrase/storage/PassphraseSecret.php', 'PasteCapabilityDefaultView' => 'applications/paste/capability/PasteCapabilityDefaultView.php', 'PasteCreateMailReceiver' => 'applications/paste/mail/PasteCreateMailReceiver.php', 'PasteEmbedView' => 'applications/paste/view/PasteEmbedView.php', @@ -998,6 +1019,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationOwners' => 'applications/owners/application/PhabricatorApplicationOwners.php', 'PhabricatorApplicationPHIDTypeApplication' => 'applications/meta/phid/PhabricatorApplicationPHIDTypeApplication.php', 'PhabricatorApplicationPHPAST' => 'applications/phpast/application/PhabricatorApplicationPHPAST.php', + 'PhabricatorApplicationPassphrase' => 'applications/passphrase/application/PhabricatorApplicationPassphrase.php', 'PhabricatorApplicationPaste' => 'applications/paste/application/PhabricatorApplicationPaste.php', 'PhabricatorApplicationPeople' => 'applications/people/application/PhabricatorApplicationPeople.php', 'PhabricatorApplicationPhame' => 'applications/phame/application/PhabricatorApplicationPhame.php', @@ -3299,6 +3321,35 @@ phutil_register_library_map(array( 'PackageDeleteMail' => 'PackageMail', 'PackageMail' => 'PhabricatorMail', 'PackageModifyMail' => 'PackageMail', + 'PassphraseController' => 'PhabricatorController', + 'PassphraseCredential' => + array( + 0 => 'PassphraseDAO', + 1 => 'PhabricatorPolicyInterface', + ), + 'PassphraseCredentialCreateController' => 'PassphraseController', + 'PassphraseCredentialDestroyController' => 'PassphraseController', + 'PassphraseCredentialEditController' => 'PassphraseController', + 'PassphraseCredentialListController' => + array( + 0 => 'PassphraseController', + 1 => 'PhabricatorApplicationSearchResultsControllerInterface', + ), + 'PassphraseCredentialQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PassphraseCredentialRevealController' => 'PassphraseController', + 'PassphraseCredentialSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PassphraseCredentialTransaction' => 'PhabricatorApplicationTransaction', + 'PassphraseCredentialTransactionEditor' => 'PhabricatorApplicationTransactionEditor', + 'PassphraseCredentialTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PassphraseCredentialType' => 'Phobject', + 'PassphraseCredentialTypePassword' => 'PassphraseCredentialType', + 'PassphraseCredentialTypeSSHPrivateKey' => 'PassphraseCredentialType', + 'PassphraseCredentialTypeSSHPrivateKeyFile' => 'PassphraseCredentialTypeSSHPrivateKey', + 'PassphraseCredentialTypeSSHPrivateKeyText' => 'PassphraseCredentialTypeSSHPrivateKey', + 'PassphraseCredentialViewController' => 'PassphraseController', + 'PassphraseDAO' => 'PhabricatorLiskDAO', + 'PassphrasePHIDTypeCredential' => 'PhabricatorPHIDType', + 'PassphraseSecret' => 'PassphraseDAO', 'PasteCapabilityDefaultView' => 'PhabricatorPolicyCapability', 'PasteCreateMailReceiver' => 'PhabricatorMailReceiver', 'PasteEmbedView' => 'AphrontView', @@ -3353,6 +3404,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationOwners' => 'PhabricatorApplication', 'PhabricatorApplicationPHIDTypeApplication' => 'PhabricatorPHIDType', 'PhabricatorApplicationPHPAST' => 'PhabricatorApplication', + 'PhabricatorApplicationPassphrase' => 'PhabricatorApplication', 'PhabricatorApplicationPaste' => 'PhabricatorApplication', 'PhabricatorApplicationPeople' => 'PhabricatorApplication', 'PhabricatorApplicationPhame' => 'PhabricatorApplication', diff --git a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php index f1a045e35f..78c7623e1e 100644 --- a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php +++ b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php @@ -64,14 +64,11 @@ final class HarbormasterBuildPlanEditor switch ($type) { case HarbormasterBuildPlanTransaction::TYPE_NAME: - $missing_name = true; - if (strlen($object->getName()) && empty($xactions)) { - $missing_name = false; - } else if (strlen(last($xactions)->getNewValue())) { - $missing_name = false; - } + $missing = $this->validateIsEmptyTextField( + $object->getName(), + $xactions); - if ($missing_name) { + if ($missing) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), diff --git a/src/applications/passphrase/application/PhabricatorApplicationPassphrase.php b/src/applications/passphrase/application/PhabricatorApplicationPassphrase.php new file mode 100644 index 0000000000..493372262b --- /dev/null +++ b/src/applications/passphrase/application/PhabricatorApplicationPassphrase.php @@ -0,0 +1,46 @@ +\d+)' => 'PassphraseCredentialViewController', + '/passphrase/' => array( + '(?:query/(?P[^/]+)/)?' + => 'PassphraseCredentialListController', + 'create/' => 'PassphraseCredentialCreateController', + 'edit/(?:(?P\d+)/)?' => 'PassphraseCredentialEditController', + 'destroy/(?P\d+)/' => 'PassphraseCredentialDestroyController', + 'reveal/(?P\d+)/' => 'PassphraseCredentialRevealController', + )); + } + +} diff --git a/src/applications/passphrase/controller/PassphraseController.php b/src/applications/passphrase/controller/PassphraseController.php new file mode 100644 index 0000000000..77268c1cbe --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseController.php @@ -0,0 +1,40 @@ +getRequest()->getUser(); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + + if ($for_app) { + $nav->addFilter('create', pht('Create Credential')); + } + + id(new PassphraseCredentialSearchEngine()) + ->setViewer($user) + ->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + + public function buildApplicationMenu() { + return $this->buildSideNavView(true)->getMenu(); + } + + public function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $crumbs->addAction( + id(new PHUIListItemView()) + ->setName(pht('Create Credential')) + ->setHref($this->getApplicationURI('create/')) + ->setIcon('create')); + + return $crumbs; + } + +} diff --git a/src/applications/passphrase/controller/PassphraseCredentialCreateController.php b/src/applications/passphrase/controller/PassphraseCredentialCreateController.php new file mode 100644 index 0000000000..0ae3da1e73 --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseCredentialCreateController.php @@ -0,0 +1,78 @@ +getRequest(); + $viewer = $request->getUser(); + + $types = PassphraseCredentialType::getAllTypes(); + $types = mpull($types, null, 'getCredentialType'); + $types = msort($types, 'getCredentialTypeName'); + + $errors = array(); + $e_type = null; + + if ($request->isFormPost()) { + $type = $request->getStr('type'); + if (empty($types[$type])) { + $errors[] = pht('You must choose a credential type.'); + $e_type = pht('Required'); + } + + if (!$errors) { + $uri = $this->getApplicationURI('edit/?type='.$type); + return id(new AphrontRedirectResponse())->setURI($uri); + } + } + + $error_view = null; + if ($errors) { + $error_view = id(new AphrontErrorView()) + ->setErrors($errors); + } + + $types_control = id(new AphrontFormRadioButtonControl()) + ->setName('type') + ->setLabel(pht('Credential Type')) + ->setError($e_type); + + foreach ($types as $type) { + $types_control->addButton( + $type->getCredentialType(), + $type->getCredentialTypeName(), + $type->getCredentialTypeDescription()); + } + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild($types_control) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Continue')) + ->addCancelButton($this->getApplicationURI())); + + $title = pht('New Credential'); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName(pht('Create'))); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Create New Credential')) + ->setFormError($error_view) + ->setForm($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + ), + array( + 'title' => $title, + 'device' => true, + )); + } + +} diff --git a/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php b/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php new file mode 100644 index 0000000000..30c744e96f --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php @@ -0,0 +1,67 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $credential = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$credential) { + return new Aphront404Response(); + } + + $type = PassphraseCredentialType::getTypeByConstant( + $credential->getCredentialType()); + if (!$type) { + throw new Exception(pht('Credential has invalid type "%s"!', $type)); + } + + $view_uri = '/K'.$credential->getID(); + + if ($request->isFormPost()) { + + $xactions = array(); + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType(PassphraseCredentialTransaction::TYPE_DESTROY) + ->setNewValue(1); + + $editor = id(new PassphraseCredentialTransactionEditor()) + ->setActor($viewer) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request) + ->applyTransactions($credential, $xactions); + + return id(new AphrontRedirectResponse())->setURI($view_uri); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle(pht('Really destroy credential?')) + ->appendChild( + pht( + 'This credential will be deactivated and the secret will be '. + 'unrecoverably destroyed. Anything relying on this credential will '. + 'cease to function. This operation can not be undone.')) + ->addSubmitButton(pht('Destroy Credential')) + ->addCancelButton($view_uri); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/passphrase/controller/PassphraseCredentialEditController.php b/src/applications/passphrase/controller/PassphraseCredentialEditController.php new file mode 100644 index 0000000000..4b319dd94c --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseCredentialEditController.php @@ -0,0 +1,241 @@ +id = idx($data, 'id'); + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + if ($this->id) { + $credential = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$credential) { + return new Aphront404Response(); + } + + $type = PassphraseCredentialType::getTypeByConstant( + $credential->getCredentialType()); + if (!$type) { + throw new Exception(pht('Credential has invalid type "%s"!', $type)); + } + + $is_new = false; + } else { + $type_const = $request->getStr('type'); + $type = PassphraseCredentialType::getTypeByConstant($type_const); + if (!$type) { + return new Aphront404Response(); + } + + $credential = PassphraseCredential::initializeNewCredential($viewer) + ->setCredentialType($type->getCredentialType()) + ->setProvidesType($type->getProvidesType()); + + $is_new = true; + } + + $errors = array(); + + $v_name = $credential->getName(); + $e_name = true; + + $v_desc = $credential->getDescription(); + + $v_username = $credential->getUsername(); + $e_username = true; + + $bullet = "\xE2\x80\xA2"; + + $v_secret = $credential->getSecretID() ? str_repeat($bullet, 32) : null; + + $validation_exception = null; + if ($request->isFormPost()) { + $v_name = $request->getStr('name'); + $v_desc = $request->getStr('description'); + $v_username = $request->getStr('username'); + $v_secret = $request->getStr('secret'); + $v_view_policy = $request->getStr('viewPolicy'); + $v_edit_policy = $request->getStr('editPolicy'); + + $type_name = PassphraseCredentialTransaction::TYPE_NAME; + $type_desc = PassphraseCredentialTransaction::TYPE_DESCRIPTION; + $type_username = PassphraseCredentialTransaction::TYPE_USERNAME; + $type_destroy = PassphraseCredentialTransaction::TYPE_DESTROY; + $type_secret_id = PassphraseCredentialTransaction::TYPE_SECRET_ID; + $type_view_policy = PhabricatorTransactions::TYPE_VIEW_POLICY; + $type_edit_policy = PhabricatorTransactions::TYPE_EDIT_POLICY; + + $xactions = array(); + + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType($type_name) + ->setNewValue($v_name); + + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType($type_desc) + ->setNewValue($v_desc); + + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType($type_username) + ->setNewValue($v_username); + + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType($type_view_policy) + ->setNewValue($v_view_policy); + + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType($type_edit_policy) + ->setNewValue($v_edit_policy); + + // Open a transaction in case we're writing a new secret; this limits + // the amount of code which handles secret plaintexts. + $credential->openTransaction(); + + $min_secret = str_replace($bullet, '', trim($v_secret)); + if (strlen($min_secret)) { + // If the credential was previously destroyed, restore it when it is + // edited if a secret is provided. + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType($type_destroy) + ->setNewValue(0); + + $new_secret = id(new PassphraseSecret()) + ->setSecretData($v_secret) + ->save(); + $xactions[] = id(new PassphraseCredentialTransaction()) + ->setTransactionType($type_secret_id) + ->setNewValue($new_secret->getID()); + } + + try { + $editor = id(new PassphraseCredentialTransactionEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request) + ->applyTransactions($credential, $xactions); + + $credential->saveTransaction(); + + return id(new AphrontRedirectResponse()) + ->setURI('/K'.$credential->getID()); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $credential->killTransaction(); + + $validation_exception = $ex; + + $e_name = $ex->getShortMessage($type_name); + $e_username = $ex->getShortMessage($type_username); + + $credential->setViewPolicy($v_view_policy); + $credential->setEditPolicy($v_edit_policy); + } + } + + $policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($credential) + ->execute(); + + $secret_control = $type->newSecretControl(); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('name') + ->setLabel(pht('Name')) + ->setValue($v_name) + ->setError($e_name)) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) + ->setName('description') + ->setLabel(pht('Description')) + ->setValue($v_desc)) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('Credential Type')) + ->setValue($type->getCredentialTypeName())) + ->appendChild( + id(new AphrontFormDividerControl())) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setName('viewPolicy') + ->setPolicyObject($credential) + ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) + ->setPolicies($policies)) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setName('editPolicy') + ->setPolicyObject($credential) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) + ->setPolicies($policies)) + ->appendChild( + id(new AphrontFormDividerControl())) + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('username') + ->setLabel(pht('Login/Username')) + ->setValue($v_username) + ->setError($e_username)) + ->appendChild( + $secret_control + ->setName('secret') + ->setLabel($type->getSecretLabel()) + ->setValue($v_secret)); + + $form->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Save')) + ->addCancelButton($this->getApplicationURI())); + + $crumbs = $this->buildApplicationCrumbs(); + + if ($is_new) { + $title = pht('Create Credential'); + $header = pht('Create New Credential'); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName(pht('Create'))); + } else { + $title = pht('Edit Credential'); + $header = pht('Edit Credential %s', 'K'.$credential->getID()); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName('K'.$credential->getID()) + ->setHref('/K'.$credential->getID())); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName(pht('Edit'))); + } + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText($header) + ->setValidationException($validation_exception) + ->setForm($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + ), + array( + 'title' => $title, + 'device' => true, + )); + } + +} diff --git a/src/applications/passphrase/controller/PassphraseCredentialListController.php b/src/applications/passphrase/controller/PassphraseCredentialListController.php new file mode 100644 index 0000000000..bc1bac0258 --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseCredentialListController.php @@ -0,0 +1,63 @@ +queryKey = idx($data, 'queryKey'); + } + + public function processRequest() { + $request = $this->getRequest(); + $controller = id(new PhabricatorApplicationSearchController($request)) + ->setQueryKey($this->queryKey) + ->setSearchEngine(new PassphraseCredentialSearchEngine()) + ->setNavigation($this->buildSideNavView()); + + return $this->delegateToController($controller); + } + + public function renderResultsList( + array $credentials, + PhabricatorSavedQuery $query) { + assert_instances_of($credentials, 'PassphraseCredential'); + + $viewer = $this->getRequest()->getUser(); + + $list = new PHUIObjectItemListView(); + $list->setUser($viewer); + foreach ($credentials as $credential) { + + $item = id(new PHUIObjectItemView()) + ->setObjectName('K'.$credential->getID()) + ->setHeader($credential->getName()) + ->setHref('/K'.$credential->getID()) + ->setObject($credential); + + $item->addAttribute( + pht('Login: %s', $credential->getUsername())); + + if ($credential->getIsDestroyed()) { + $item->addIcon('disable', pht('Destroyed')); + $item->setDisabled(true); + } + + $type = PassphraseCredentialType::getTypeByConstant( + $credential->getCredentialType()); + if ($type) { + $item->addIcon('wrench', $type->getCredentialTypeName()); + } + + $list->addItem($item); + } + + return $list; + } + +} diff --git a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php new file mode 100644 index 0000000000..88de484955 --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php @@ -0,0 +1,75 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $credential = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->needSecrets(true) + ->executeOne(); + if (!$credential) { + return new Aphront404Response(); + } + + $view_uri = '/K'.$credential->getID(); + + if ($request->isFormPost()) { + if ($credential->getSecret()) { + $body = id(new PHUIFormLayoutView()) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel(pht('Plaintext')) + ->setValue($credential->getSecret()->openEnvelope())); + } else { + $body = pht('This credential has no associated secret.'); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle(pht('Credential Secret')) + ->appendChild($body) + ->addCancelButton($view_uri, pht('Done')); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + + $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); + if ($is_serious) { + $body = pht( + 'The secret associated with this credential will be shown in plain '. + 'text on your screen.'); + } else { + $body = pht( + 'The secret associated with this credential will be shown in plain '. + 'text on your screen. Before continuing, wrap your arms around your '. + 'monitor to create a human shield, keeping it safe from prying eyes. '. + 'Protect company secrets!'); + } + + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setTitle(pht('Really show secret?')) + ->appendChild($body) + ->addSubmitButton(pht('Show Secret')) + ->addCancelButton($view_uri); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/passphrase/controller/PassphraseCredentialViewController.php b/src/applications/passphrase/controller/PassphraseCredentialViewController.php new file mode 100644 index 0000000000..3a4e9d6b9b --- /dev/null +++ b/src/applications/passphrase/controller/PassphraseCredentialViewController.php @@ -0,0 +1,170 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $credential = id(new PassphraseCredentialQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$credential) { + return new Aphront404Response(); + } + + $type = PassphraseCredentialType::getTypeByConstant( + $credential->getCredentialType()); + if (!$type) { + throw new Exception(pht('Credential has invalid type "%s"!', $type)); + } + + $xactions = id(new PassphraseCredentialTransactionQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($credential->getPHID())) + ->execute(); + + $engine = id(new PhabricatorMarkupEngine()) + ->setViewer($viewer); + + $timeline = id(new PhabricatorApplicationTransactionView()) + ->setUser($viewer) + ->setObjectPHID($credential->getPHID()) + ->setTransactions($xactions); + + $title = pht('%s %s', 'K'.$credential->getID(), $credential->getName()); + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addCrumb( + id(new PhabricatorCrumbView()) + ->setName('K'.$credential->getID())); + + $header = $this->buildHeaderView($credential); + $actions = $this->buildActionView($credential); + $properties = $this->buildPropertyView($credential, $type, $actions); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + + return $this->buildApplicationPage( + array( + $crumbs, + $box, + $timeline, + ), + array( + 'title' => $title, + 'device' => true, + )); + } + + private function buildHeaderView(PassphraseCredential $credential) { + $viewer = $this->getRequest()->getUser(); + + $header = id(new PHUIHeaderView()) + ->setUser($viewer) + ->setHeader($credential->getName()) + ->setPolicyObject($credential); + + if ($credential->getIsDestroyed()) { + $header->setStatus('reject', 'red', pht('Destroyed')); + } + + return $header; + } + + private function buildActionView(PassphraseCredential $credential) { + $viewer = $this->getRequest()->getUser(); + + $id = $credential->getID(); + + $actions = id(new PhabricatorActionListView()) + ->setObjectURI('/K'.$id) + ->setUser($viewer); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $credential, + PhabricatorPolicyCapability::CAN_EDIT); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Credential')) + ->setIcon('edit') + ->setHref($this->getApplicationURI("edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + if (!$credential->getIsDestroyed()) { + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Destroy Credential')) + ->setIcon('delete') + ->setHref($this->getApplicationURI("destroy/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Show Secret')) + ->setIcon('preview') + ->setHref($this->getApplicationURI("reveal/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + } + + + return $actions; + } + + private function buildPropertyView( + PassphraseCredential $credential, + PassphraseCredentialType $type, + PhabricatorActionListView $actions) { + $viewer = $this->getRequest()->getUser(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($credential) + ->setActionList($actions); + + $properties->addProperty( + pht('Credential Type'), + $type->getCredentialTypeName()); + + $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( + $viewer, + $credential); + + $properties->addProperty( + pht('Editable By'), + $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); + + $properties->addProperty( + pht('Username'), + $credential->getUsername()); + + $description = $credential->getDescription(); + if (strlen($description)) { + $properties->addSectionHeader( + pht('Description'), + PHUIPropertyListView::ICON_SUMMARY); + $properties->addTextContent( + PhabricatorMarkupEngine::renderOneObject( + id(new PhabricatorMarkupOneOff()) + ->setContent($description), + 'default', + $viewer)); + } + + return $properties; + } + +} diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialType.php b/src/applications/passphrase/credentialtype/PassphraseCredentialType.php new file mode 100644 index 0000000000..8da833dfb4 --- /dev/null +++ b/src/applications/passphrase/credentialtype/PassphraseCredentialType.php @@ -0,0 +1,28 @@ +setAncestorClass(__CLASS__) + ->loadObjects(); + return $types; + } + + public static function getTypeByConstant($constant) { + $all = self::getAllTypes(); + $all = mpull($all, null, 'getCredentialType'); + return idx($all, $constant); + } + +} diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php b/src/applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php new file mode 100644 index 0000000000..47da20d84f --- /dev/null +++ b/src/applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php @@ -0,0 +1,30 @@ +getTransactionType()) { + case PassphraseCredentialTransaction::TYPE_NAME: + if ($this->getIsNewObject()) { + return null; + } + return $object->getName(); + case PassphraseCredentialTransaction::TYPE_DESCRIPTION: + return $object->getDescription(); + case PassphraseCredentialTransaction::TYPE_USERNAME: + return $object->getUsername(); + case PassphraseCredentialTransaction::TYPE_SECRET_ID: + return $object->getSecretID(); + case PassphraseCredentialTransaction::TYPE_DESTROY: + return $object->getIsDestroyed(); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + switch ($xaction->getTransactionType()) { + case PassphraseCredentialTransaction::TYPE_NAME: + case PassphraseCredentialTransaction::TYPE_DESCRIPTION: + case PassphraseCredentialTransaction::TYPE_USERNAME: + case PassphraseCredentialTransaction::TYPE_SECRET_ID: + case PassphraseCredentialTransaction::TYPE_DESTROY: + return $xaction->getNewValue(); + } + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + switch ($xaction->getTransactionType()) { + case PassphraseCredentialTransaction::TYPE_NAME: + $object->setName($xaction->getNewValue()); + return; + case PassphraseCredentialTransaction::TYPE_DESCRIPTION: + $object->setDescription($xaction->getNewValue()); + return; + case PassphraseCredentialTransaction::TYPE_USERNAME: + $object->setUsername($xaction->getNewValue()); + return; + case PassphraseCredentialTransaction::TYPE_SECRET_ID: + $old_id = $object->getSecretID(); + if ($old_id) { + $this->destroySecret($old_id); + } + $object->setSecretID($xaction->getNewValue()); + return; + case PassphraseCredentialTransaction::TYPE_DESTROY: + // When destroying a credential, wipe out its secret. + $is_destroyed = $xaction->getNewValue(); + $object->setIsDestroyed($is_destroyed); + if ($is_destroyed) { + $secret_id = $object->getSecretID(); + if ($secret_id) { + $this->destroySecret($secret_id); + $object->setSecretID(null); + } + } + return; + } + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PassphraseCredentialTransaction::TYPE_NAME: + case PassphraseCredentialTransaction::TYPE_DESCRIPTION: + case PassphraseCredentialTransaction::TYPE_USERNAME: + case PassphraseCredentialTransaction::TYPE_SECRET_ID: + case PassphraseCredentialTransaction::TYPE_DESTROY: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + private function destroySecret($secret_id) { + $table = new PassphraseSecret(); + queryfx( + $table->establishConnection('w'), + 'DELETE FROM %T WHERE id = %d', + $table->getTableName(), + $secret_id); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case PassphraseCredentialTransaction::TYPE_NAME: + $missing = $this->validateIsEmptyTextField( + $object->getName(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('Credential name is required.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } + break; + case PassphraseCredentialTransaction::TYPE_USERNAME: + $missing = $this->validateIsEmptyTextField( + $object->getUsername(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('Username is required.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } + break; + } + + return $errors; + } + + +} diff --git a/src/applications/passphrase/phid/PassphrasePHIDTypeCredential.php b/src/applications/passphrase/phid/PassphrasePHIDTypeCredential.php new file mode 100644 index 0000000000..ac7fedaa93 --- /dev/null +++ b/src/applications/passphrase/phid/PassphrasePHIDTypeCredential.php @@ -0,0 +1,76 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $credential = $objects[$phid]; + $id = $credential->getID(); + $name = $credential->getName(); + + $handle->setName("K{$id}"); + $handle->setFullName("K{$id} {$name}"); + $handle->setURI("/K{$id}"); + + if ($credential->getIsDestroyed()) { + $handle->setStatus(PhabricatorObjectHandleStatus::STATUS_CLOSED); + } + } + } + + public function canLoadNamedObject($name) { + return preg_match('/^K\d*[1-9]\d*$/i', $name); + } + + public function loadNamedObjects( + PhabricatorObjectQuery $query, + array $names) { + + $id_map = array(); + foreach ($names as $name) { + $id = (int)substr($name, 1); + $id_map[$id][] = $name; + } + + $objects = id(new PassphraseCredentialQuery()) + ->setViewer($query->getViewer()) + ->withIDs(array_keys($id_map)) + ->execute(); + + $results = array(); + foreach ($objects as $id => $object) { + foreach (idx($id_map, $id, array()) as $name) { + $results[$name] = $object; + } + } + + return $results; + } + +} diff --git a/src/applications/passphrase/query/PassphraseCredentialQuery.php b/src/applications/passphrase/query/PassphraseCredentialQuery.php new file mode 100644 index 0000000000..349a4abee7 --- /dev/null +++ b/src/applications/passphrase/query/PassphraseCredentialQuery.php @@ -0,0 +1,137 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withCredentialTypes(array $credential_types) { + $this->credentialTypes = $credential_types; + return $this; + } + + public function withProvidesTypes(array $provides_types) { + $this->providesTypes = $provides_types; + return $this; + } + + public function withIsDestroyed($destroyed) { + $this->isDestroyed = $destroyed; + return $this; + } + + public function needSecrets($need_secrets) { + $this->needSecrets = $need_secrets; + return $this; + } + + protected function loadPage() { + $table = new PassphraseCredential(); + $conn_r = $table->establishConnection('r'); + + $rows = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + return $table->loadAllFromArray($rows); + } + + protected function willFilterPage(array $page) { + if ($this->needSecrets) { + $secret_ids = mpull($page, 'getSecretID'); + $secret_ids = array_filter($secret_ids); + + $secrets = array(); + if ($secret_ids) { + $secret_objects = id(new PassphraseSecret())->loadAllWhere( + 'id IN (%Ld)', + $secret_ids); + foreach ($secret_objects as $secret) { + $secret_data = $secret->getSecretData(); + $secrets[$secret->getID()] = new PhutilOpaqueEnvelope($secret_data); + } + } + + foreach ($page as $key => $credential) { + $secret_id = $credential->getSecretID(); + if (!$secret_id) { + $credential->attachSecret(null); + } else if (isset($secrets[$secret_id])) { + $credential->attachSecret($secrets[$secret_id]); + } else { + unset($page[$key]); + } + } + } + + return $page; + } + + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { + $where = array(); + + $where[] = $this->buildPagingClause($conn_r); + + if ($this->ids) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->credentialTypes) { + $where[] = qsprintf( + $conn_r, + 'credentialType in (%Ls)', + $this->credentialTypes); + } + + if ($this->providesTypes) { + $where[] = qsprintf( + $conn_r, + 'providesType IN (%Ls)', + $this->providesTypes); + } + + if ($this->isDestroyed !== null) { + $where[] = qsprintf( + $conn_r, + 'isDestroyed = %d', + (int)$this->isDestroyed); + } + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorApplicationPassphrase'; + } + +} diff --git a/src/applications/passphrase/query/PassphraseCredentialSearchEngine.php b/src/applications/passphrase/query/PassphraseCredentialSearchEngine.php new file mode 100644 index 0000000000..ab2144f213 --- /dev/null +++ b/src/applications/passphrase/query/PassphraseCredentialSearchEngine.php @@ -0,0 +1,73 @@ +setParameter( + 'isDestroyed', + $this->readBoolFromRequest($request, 'isDestroyed')); + + return $saved; + } + + public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { + $query = id(new PassphraseCredentialQuery()); + + $destroyed = $saved->getParameter('isDestroyed'); + if ($destroyed !== null) { + $query->withIsDestroyed($destroyed); + } + + return $query; + } + + public function buildSearchForm( + AphrontFormView $form, + PhabricatorSavedQuery $saved_query) { + + $form->appendChild( + id(new AphrontFormSelectControl()) + ->setName('isDestroyed') + ->setLabel(pht('Status')) + ->setValue($this->getBoolFromQuery($saved_query, 'isDestroyed')) + ->setOptions( + array( + '' => pht('Show All Credentials'), + 'false' => pht('Show Only Active Credentials'), + 'true' => pht('Show Only Destroyed Credentials'), + ))); + + } + + protected function getURI($path) { + return '/passphrase/'.$path; + } + + public function getBuiltinQueryNames() { + $names = array( + 'active' => pht('Active Credentials'), + 'all' => pht('All Credentials'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + case 'active': + return $query->setParameter('isDestroyed', false); + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + +} diff --git a/src/applications/passphrase/query/PassphraseCredentialTransactionQuery.php b/src/applications/passphrase/query/PassphraseCredentialTransactionQuery.php new file mode 100644 index 0000000000..ebc5237091 --- /dev/null +++ b/src/applications/passphrase/query/PassphraseCredentialTransactionQuery.php @@ -0,0 +1,10 @@ +setName('') + ->setUsername('') + ->setIsDestroyed(0) + ->setViewPolicy($actor->getPHID()) + ->setEditPolicy($actor->getPHID()); + } + + public function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PassphrasePHIDTypeCredential::TYPECONST); + } + + public function attachSecret(PhutilOpaqueEnvelope $secret = null) { + $this->secret = $secret; + return $this; + } + + public function getSecret() { + return $this->assertAttached($this->secret); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + +} diff --git a/src/applications/passphrase/storage/PassphraseCredentialTransaction.php b/src/applications/passphrase/storage/PassphraseCredentialTransaction.php new file mode 100644 index 0000000000..52739a4808 --- /dev/null +++ b/src/applications/passphrase/storage/PassphraseCredentialTransaction.php @@ -0,0 +1,106 @@ +getOldValue(); + switch ($this->getTransactionType()) { + case self::TYPE_DESCRIPTION: + return ($old === null); + case self::TYPE_USERNAME: + return !strlen($old); + } + return parent::shouldHide(); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + $author_phid = $this->getAuthorPHID(); + + switch ($this->getTransactionType()) { + case self::TYPE_NAME: + if ($old === null) { + return pht( + '%s created this credential.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s renamed this credential from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); + } + break; + case self::TYPE_DESCRIPTION: + return pht( + '%s updated the description for this credential.', + $this->renderHandleLink($author_phid)); + case self::TYPE_USERNAME: + if (strlen($old)) { + return pht( + '%s changed the username for this credential from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); + } else { + return pht( + '%s set the username for this credential to "%s".', + $this->renderHandleLink($author_phid), + $new); + } + break; + case self::TYPE_SECRET_ID: + return pht( + '%s updated the secret for this credential.', + $this->renderHandleLink($author_phid)); + case self::TYPE_DESTROY: + return pht( + '%s destroyed this credential.', + $this->renderHandleLink($author_phid)); + } + + return parent::getTitle(); + } + + public function hasChangeDetails() { + switch ($this->getTransactionType()) { + case self::TYPE_DESCRIPTION: + return true; + } + return parent::hasChangeDetails(); + } + + public function renderChangeDetails(PhabricatorUser $viewer) { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $view = id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setUser($viewer) + ->setOldText(json_encode($old)) + ->setNewText(json_encode($new)); + + return $view->render(); + } + + +} diff --git a/src/applications/passphrase/storage/PassphraseDAO.php b/src/applications/passphrase/storage/PassphraseDAO.php new file mode 100644 index 0000000000..ccc1e519ed --- /dev/null +++ b/src/applications/passphrase/storage/PassphraseDAO.php @@ -0,0 +1,9 @@ + false, + ) + parent::getConfiguration(); + } + +} diff --git a/src/applications/policy/constants/PhabricatorPolicyType.php b/src/applications/policy/constants/PhabricatorPolicyType.php index 4f107db876..51e8cb4c4d 100644 --- a/src/applications/policy/constants/PhabricatorPolicyType.php +++ b/src/applications/policy/constants/PhabricatorPolicyType.php @@ -3,6 +3,7 @@ final class PhabricatorPolicyType extends PhabricatorPolicyConstants { const TYPE_GLOBAL = 'global'; + const TYPE_USER = 'user'; const TYPE_CUSTOM = 'custom'; const TYPE_PROJECT = 'project'; const TYPE_MASKED = 'masked'; @@ -10,8 +11,9 @@ final class PhabricatorPolicyType extends PhabricatorPolicyConstants { public static function getPolicyTypeOrder($type) { static $map = array( self::TYPE_GLOBAL => 0, - self::TYPE_CUSTOM => 1, - self::TYPE_PROJECT => 2, + self::TYPE_USER => 1, + self::TYPE_CUSTOM => 2, + self::TYPE_PROJECT => 3, self::TYPE_MASKED => 9, ); return idx($map, $type, 9); @@ -21,6 +23,8 @@ final class PhabricatorPolicyType extends PhabricatorPolicyConstants { switch ($type) { case self::TYPE_GLOBAL: return pht('Basic Policies'); + case self::TYPE_USER: + return pht('User Policies'); case self::TYPE_CUSTOM: return pht('Advanced'); case self::TYPE_PROJECT: diff --git a/src/applications/policy/storage/PhabricatorPolicy.php b/src/applications/policy/storage/PhabricatorPolicy.php index d124a10e5e..f52ab4ecbc 100644 --- a/src/applications/policy/storage/PhabricatorPolicy.php +++ b/src/applications/policy/storage/PhabricatorPolicy.php @@ -66,6 +66,10 @@ final class PhabricatorPolicy $policy->setType(PhabricatorPolicyType::TYPE_PROJECT); $policy->setName($handle->getName()); break; + case PhabricatorPeoplePHIDTypeUser::TYPECONST: + $policy->setType(PhabricatorPolicyType::TYPE_USER); + $policy->setName($handle->getFullName()); + break; case PhabricatorPolicyPHIDTypePolicy::TYPECONST: // TODO: This creates a weird handle-based version of a rule policy. // It behaves correctly, but can't be applied since it doesn't have @@ -138,17 +142,15 @@ final class PhabricatorPolicy PhabricatorPolicies::POLICY_NOONE => 'policy-noone', ); return idx($map, $this->getPHID(), 'policy-unknown'); - break; + case PhabricatorPolicyType::TYPE_USER: + return 'policy-user'; case PhabricatorPolicyType::TYPE_PROJECT: return 'policy-project'; - break; case PhabricatorPolicyType::TYPE_CUSTOM: case PhabricatorPolicyType::TYPE_MASKED: return 'policy-custom'; - break; default: return 'policy-unknown'; - break; } } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index d1a6e2a1ea..8a6363e904 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1129,6 +1129,39 @@ abstract class PhabricatorApplicationTransactionEditor } + /** + * Check for a missing text field. + * + * A text field is missing if the object has no value and there are no + * transactions which set a value, or if the transactions remove the value. + * This method is intended to make implementing @{method:validateTransaction} + * more convenient: + * + * $missing = $this->validateIsEmptyTextField( + * $object->getName(), + * $xactions); + * + * This will return `true` if the net effect of the object and transactions + * is an empty field. + * + * @param wild Current field value. + * @param list Transactions editing the + * field. + * @return bool True if the field will be an empty text field after edits. + */ + protected function validateIsEmptyTextField($field_value, array $xactions) { + if (strlen($field_value) && empty($xactions)) { + return false; + } + + if ($xactions && strlen(last($xactions)->getNewValue())) { + return false; + } + + return true; + } + + /* -( Implicit CCs )------------------------------------------------------- */ diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index cdc627db8b..2506aea071 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -212,6 +212,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'type' => 'db', 'name' => 'nuance', ), + 'db.passphrase' => array( + 'type' => 'db', + 'name' => 'passphrase', + ), '0000.legacy.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('0000.legacy.sql'), @@ -1760,6 +1764,10 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList { 'type' => 'php', 'name' => $this->getPatchPath('20131118.ownerorder.php'), ), + '20131119.passphrase.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('20131119.passphrase.sql'), + ), ); } } diff --git a/src/view/form/control/AphrontFormPolicyControl.php b/src/view/form/control/AphrontFormPolicyControl.php index 8f9a7b7441..b9f6deafa9 100644 --- a/src/view/form/control/AphrontFormPolicyControl.php +++ b/src/view/form/control/AphrontFormPolicyControl.php @@ -103,6 +103,7 @@ final class AphrontFormPolicyControl extends AphrontFormControl { $options, array( PhabricatorPolicyType::TYPE_GLOBAL, + PhabricatorPolicyType::TYPE_USER, PhabricatorPolicyType::TYPE_CUSTOM, PhabricatorPolicyType::TYPE_PROJECT, ));