Page MenuHomePhabricator

D7608.id17168.diff

D7608.id17168.diff

diff --git a/resources/sql/patches/20131119.passphrase.sql b/resources/sql/patches/20131119.passphrase.sql
new file mode 100644
--- /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
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -943,6 +943,27 @@
'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 @@
'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 @@
'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 @@
'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
--- a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php
+++ b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php
@@ -64,14 +64,11 @@
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
--- /dev/null
+++ b/src/applications/passphrase/application/PhabricatorApplicationPassphrase.php
@@ -0,0 +1,46 @@
+<?php
+
+final class PhabricatorApplicationPassphrase extends PhabricatorApplication {
+
+ public function getBaseURI() {
+ return '/passphrase/';
+ }
+
+ public function getShortDescription() {
+ return pht('Credential Management');
+ }
+
+ public function getIconName() {
+ return 'passphrase';
+ }
+
+ public function getTitleGlyph() {
+ return "\xE2\x97\x88";
+ }
+
+ public function getFlavorText() {
+ return pht('Put your secrets in a lockbox.');
+ }
+
+ public function getApplicationGroup() {
+ return self::GROUP_UTILITIES;
+ }
+
+ public function isBeta() {
+ return true;
+ }
+
+ public function getRoutes() {
+ return array(
+ '/K(?P<id>\d+)' => 'PassphraseCredentialViewController',
+ '/passphrase/' => array(
+ '(?:query/(?P<queryKey>[^/]+)/)?'
+ => 'PassphraseCredentialListController',
+ 'create/' => 'PassphraseCredentialCreateController',
+ 'edit/(?:(?P<id>\d+)/)?' => 'PassphraseCredentialEditController',
+ 'destroy/(?P<id>\d+)/' => 'PassphraseCredentialDestroyController',
+ 'reveal/(?P<id>\d+)/' => 'PassphraseCredentialRevealController',
+ ));
+ }
+
+}
diff --git a/src/applications/passphrase/controller/PassphraseController.php b/src/applications/passphrase/controller/PassphraseController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/controller/PassphraseController.php
@@ -0,0 +1,40 @@
+<?php
+
+abstract class PassphraseController extends PhabricatorController {
+
+ public function buildSideNavView($for_app = false) {
+ $user = $this->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
--- /dev/null
+++ b/src/applications/passphrase/controller/PassphraseCredentialCreateController.php
@@ -0,0 +1,78 @@
+<?php
+
+final class PassphraseCredentialCreateController extends PassphraseController {
+
+ public function processRequest() {
+ $request = $this->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
--- /dev/null
+++ b/src/applications/passphrase/controller/PassphraseCredentialDestroyController.php
@@ -0,0 +1,67 @@
+<?php
+
+final class PassphraseCredentialDestroyController
+ extends PassphraseController {
+
+ private $id;
+
+ public function willProcessRequest(array $data) {
+ $this->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
--- /dev/null
+++ b/src/applications/passphrase/controller/PassphraseCredentialEditController.php
@@ -0,0 +1,241 @@
+<?php
+
+final class PassphraseCredentialEditController extends PassphraseController {
+
+ private $id;
+
+ public function willProcessRequest(array $data) {
+ $this->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
--- /dev/null
+++ b/src/applications/passphrase/controller/PassphraseCredentialListController.php
@@ -0,0 +1,63 @@
+<?php
+
+final class PassphraseCredentialListController extends PassphraseController
+ implements PhabricatorApplicationSearchResultsControllerInterface {
+
+ private $queryKey;
+
+ public function shouldAllowPublic() {
+ return true;
+ }
+
+ public function willProcessRequest(array $data) {
+ $this->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
--- /dev/null
+++ b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php
@@ -0,0 +1,75 @@
+<?php
+
+final class PassphraseCredentialRevealController
+ extends PassphraseController {
+
+ private $id;
+
+ public function willProcessRequest(array $data) {
+ $this->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
--- /dev/null
+++ b/src/applications/passphrase/controller/PassphraseCredentialViewController.php
@@ -0,0 +1,170 @@
+<?php
+
+final class PassphraseCredentialViewController extends PassphraseController {
+
+ private $id;
+
+ public function willProcessRequest(array $data) {
+ $this->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
--- /dev/null
+++ b/src/applications/passphrase/credentialtype/PassphraseCredentialType.php
@@ -0,0 +1,28 @@
+<?php
+
+abstract class PassphraseCredentialType extends Phobject {
+
+ abstract public function getCredentialType();
+ abstract public function getProvidesType();
+ abstract public function getCredentialTypeName();
+ abstract public function getCredentialTypeDescription();
+ abstract public function getSecretLabel();
+
+ public function newSecretControl() {
+ return new AphrontFormTextAreaControl();
+ }
+
+ public static function getAllTypes() {
+ $types = id(new PhutilSymbolLoader())
+ ->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
--- /dev/null
+++ b/src/applications/passphrase/credentialtype/PassphraseCredentialTypePassword.php
@@ -0,0 +1,30 @@
+<?php
+
+final class PassphraseCredentialTypePassword
+ extends PassphraseCredentialType {
+
+ public function getCredentialType() {
+ return 'password';
+ }
+
+ public function getProvidesType() {
+ return 'provides/password';
+ }
+
+ public function getCredentialTypeName() {
+ return pht('Password');
+ }
+
+ public function getCredentialTypeDescription() {
+ return pht('Store a plaintext password.');
+ }
+
+ public function getSecretLabel() {
+ return pht('Password');
+ }
+
+ public function newSecretControl() {
+ return new AphrontFormPasswordControl();
+ }
+
+}
diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php b/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKey.php
@@ -0,0 +1,10 @@
+<?php
+
+abstract class PassphraseCredentialTypeSSHPrivateKey
+ extends PassphraseCredentialType {
+
+ final public function getProvidesType() {
+ return 'provides/ssh-key-file';
+ }
+
+}
diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php b/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyFile.php
@@ -0,0 +1,26 @@
+<?php
+
+final class PassphraseCredentialTypeSSHPrivateKeyFile
+ extends PassphraseCredentialTypeSSHPrivateKey {
+
+ public function getCredentialType() {
+ return 'ssh-key-file';
+ }
+
+ public function getCredentialTypeName() {
+ return pht('SSH Private Key File');
+ }
+
+ public function getCredentialTypeDescription() {
+ return pht('Store the path on disk to an SSH private key.');
+ }
+
+ public function getSecretLabel() {
+ return pht('Path On Disk');
+ }
+
+ public function newSecretControl() {
+ return new AphrontFormTextControl();
+ }
+
+}
diff --git a/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php b/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/credentialtype/PassphraseCredentialTypeSSHPrivateKeyText.php
@@ -0,0 +1,22 @@
+<?php
+
+final class PassphraseCredentialTypeSSHPrivateKeyText
+ extends PassphraseCredentialTypeSSHPrivateKey {
+
+ public function getCredentialType() {
+ return 'ssh-key-text';
+ }
+
+ public function getCredentialTypeName() {
+ return pht('SSH Private Key');
+ }
+
+ public function getCredentialTypeDescription() {
+ return pht('Store the plaintext of an SSH private key.');
+ }
+
+ public function getSecretLabel() {
+ return pht('Private Key');
+ }
+
+}
diff --git a/src/applications/passphrase/editor/PassphraseCredentialTransactionEditor.php b/src/applications/passphrase/editor/PassphraseCredentialTransactionEditor.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/editor/PassphraseCredentialTransactionEditor.php
@@ -0,0 +1,164 @@
+<?php
+
+final class PassphraseCredentialTransactionEditor
+ extends PhabricatorApplicationTransactionEditor {
+
+ public function getTransactionTypes() {
+ $types = parent::getTransactionTypes();
+
+ $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
+ $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
+
+ $types[] = PassphraseCredentialTransaction::TYPE_NAME;
+ $types[] = PassphraseCredentialTransaction::TYPE_DESCRIPTION;
+ $types[] = PassphraseCredentialTransaction::TYPE_USERNAME;
+ $types[] = PassphraseCredentialTransaction::TYPE_SECRET_ID;
+ $types[] = PassphraseCredentialTransaction::TYPE_DESTROY;
+
+ return $types;
+ }
+
+ protected function getCustomTransactionOldValue(
+ PhabricatorLiskDAO $object,
+ PhabricatorApplicationTransaction $xaction) {
+ switch ($xaction->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
--- /dev/null
+++ b/src/applications/passphrase/phid/PassphrasePHIDTypeCredential.php
@@ -0,0 +1,76 @@
+<?php
+
+final class PassphrasePHIDTypeCredential extends PhabricatorPHIDType {
+
+ const TYPECONST = 'CDTL';
+
+ public function getTypeConstant() {
+ return self::TYPECONST;
+ }
+
+ public function getTypeName() {
+ return pht('Credential');
+ }
+
+ public function newObject() {
+ return new PassphraseCredential();
+ }
+
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+
+ return id(new PassphraseCredentialQuery())
+ ->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
--- /dev/null
+++ b/src/applications/passphrase/query/PassphraseCredentialQuery.php
@@ -0,0 +1,137 @@
+<?php
+
+final class PassphraseCredentialQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+ private $credentialTypes;
+ private $providesTypes;
+ private $isDestroyed;
+
+ private $needSecrets;
+
+ public function withIDs(array $ids) {
+ $this->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
--- /dev/null
+++ b/src/applications/passphrase/query/PassphraseCredentialSearchEngine.php
@@ -0,0 +1,73 @@
+<?php
+
+final class PassphraseCredentialSearchEngine
+ extends PhabricatorApplicationSearchEngine {
+
+ public function buildSavedQueryFromRequest(AphrontRequest $request) {
+ $saved = new PhabricatorSavedQuery();
+
+ $saved->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
--- /dev/null
+++ b/src/applications/passphrase/query/PassphraseCredentialTransactionQuery.php
@@ -0,0 +1,10 @@
+<?php
+
+final class PassphraseCredentialTransactionQuery
+ extends PhabricatorApplicationTransactionQuery {
+
+ public function getTemplateApplicationTransaction() {
+ return new PassphraseCredentialTransaction();
+ }
+
+}
diff --git a/src/applications/passphrase/storage/PassphraseCredential.php b/src/applications/passphrase/storage/PassphraseCredential.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/storage/PassphraseCredential.php
@@ -0,0 +1,75 @@
+<?php
+
+final class PassphraseCredential extends PassphraseDAO
+ implements PhabricatorPolicyInterface {
+
+ protected $name;
+ protected $credentialType;
+ protected $providesType;
+ protected $viewPolicy;
+ protected $editPolicy;
+ protected $description;
+ protected $username;
+ protected $secretID;
+ protected $isDestroyed;
+
+ private $secret = self::ATTACHABLE;
+
+ public static function initializeNewCredential(PhabricatorUser $actor) {
+ return id(new PassphraseCredential())
+ ->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
--- /dev/null
+++ b/src/applications/passphrase/storage/PassphraseCredentialTransaction.php
@@ -0,0 +1,106 @@
+<?php
+
+final class PassphraseCredentialTransaction
+ extends PhabricatorApplicationTransaction {
+
+ const TYPE_NAME = 'passphrase:name';
+ const TYPE_DESCRIPTION = 'passphrase:description';
+ const TYPE_USERNAME = 'passphrase:username';
+ const TYPE_SECRET_ID = 'passphrase:secretID';
+ const TYPE_DESTROY = 'passphrase:destroy';
+
+ public function getApplicationName() {
+ return 'passphrase';
+ }
+
+ public function getApplicationTransactionType() {
+ return PassphrasePHIDTypeCredential::TYPECONST;
+ }
+
+ public function getApplicationTransactionCommentObject() {
+ return null;
+ }
+
+ public function shouldHide() {
+ $old = $this->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
--- /dev/null
+++ b/src/applications/passphrase/storage/PassphraseDAO.php
@@ -0,0 +1,9 @@
+<?php
+
+abstract class PassphraseDAO extends PhabricatorLiskDAO {
+
+ public function getApplicationName() {
+ return 'passphrase';
+ }
+
+}
diff --git a/src/applications/passphrase/storage/PassphraseSecret.php b/src/applications/passphrase/storage/PassphraseSecret.php
new file mode 100644
--- /dev/null
+++ b/src/applications/passphrase/storage/PassphraseSecret.php
@@ -0,0 +1,13 @@
+<?php
+
+final class PassphraseSecret extends PassphraseDAO {
+
+ protected $secretData;
+
+ public function getConfiguration() {
+ return array(
+ self::CONFIG_TIMESTAMPS => false,
+ ) + parent::getConfiguration();
+ }
+
+}
diff --git a/src/applications/policy/constants/PhabricatorPolicyType.php b/src/applications/policy/constants/PhabricatorPolicyType.php
--- a/src/applications/policy/constants/PhabricatorPolicyType.php
+++ b/src/applications/policy/constants/PhabricatorPolicyType.php
@@ -3,15 +3,17 @@
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';
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 @@
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
--- a/src/applications/policy/storage/PhabricatorPolicy.php
+++ b/src/applications/policy/storage/PhabricatorPolicy.php
@@ -66,6 +66,10 @@
$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,16 @@
PhabricatorPolicies::POLICY_NOONE => 'policy-noone',
);
return idx($map, $this->getPHID(), 'policy-unknown');
- break;
+ case PhabricatorPolicyType::TYPE_USER:
+ // TODO: Develop a "policy-user" icon with just one silhouette in it.
+ return 'policy-all';
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
--- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
+++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
@@ -1129,6 +1129,39 @@
}
+ /**
+ * 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<PhabricatorApplicationTransaction> 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
--- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
+++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php
@@ -212,6 +212,10 @@
'type' => 'db',
'name' => 'nuance',
),
+ 'db.passphrase' => array(
+ 'type' => 'db',
+ 'name' => 'passphrase',
+ ),
'0000.legacy.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('0000.legacy.sql'),
@@ -1756,6 +1760,10 @@
'type' => 'php',
'name' => $this->getPatchPath('20131112.userverified.2.mig.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
--- a/src/view/form/control/AphrontFormPolicyControl.php
+++ b/src/view/form/control/AphrontFormPolicyControl.php
@@ -103,6 +103,7 @@
$options,
array(
PhabricatorPolicyType::TYPE_GLOBAL,
+ PhabricatorPolicyType::TYPE_USER,
PhabricatorPolicyType::TYPE_CUSTOM,
PhabricatorPolicyType::TYPE_PROJECT,
));

File Metadata

Mime Type
text/x-diff
Storage Engine
amazon-s3
Storage Format
Raw Data
Storage Handle
phabricator/gc/aq/6o74sspltkhqhbzn
Default Alt Text
D7608.id17168.diff (63 KB)

Event Timeline