Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F85834
D7608.id17168.diff
All Users
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
63 KB
Referenced Files
None
Subscribers
None
D7608.id17168.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D7608: Passphrase v0
Attached
Detach File
Event Timeline
Log In to Comment