@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator

Add a "Lock Permanently" action to Passphrase

Summary: Fixes T4931. Each new credential should come with the ability to lock the credential permanently, so that no one can ever edit again. Each existing credential must allow user to lock existing credential.

Test Plan: Create new credential, verify that you can lock it before saving it. Open existing unlocked credential, verify that option to lock it exists. Once credential is locked, the option to reveal it should be disabled, and editing the credential won't allow username/password updates.

Reviewers: #blessed_reviewers, epriestley

Reviewed By: #blessed_reviewers, epriestley

Subscribers: epriestley, Korvin

Maniphest Tasks: T4931

Differential Revision: https://secure.phabricator.com/D8947

authored by

lkassianik and committed by
epriestley
d7b7b193 c995e93b

+183 -32
+2
resources/sql/autopatches/20140501.passphraselockcredential.sql
··· 1 + ALTER TABLE {$NAMESPACE}_passphrase.passphrase_credential 2 + ADD COLUMN isLocked BOOL NOT NULL;
+2
src/__phutil_library_map__.php
··· 1046 1046 'PassphraseCredentialDestroyController' => 'applications/passphrase/controller/PassphraseCredentialDestroyController.php', 1047 1047 'PassphraseCredentialEditController' => 'applications/passphrase/controller/PassphraseCredentialEditController.php', 1048 1048 'PassphraseCredentialListController' => 'applications/passphrase/controller/PassphraseCredentialListController.php', 1049 + 'PassphraseCredentialLockController' => 'applications/passphrase/controller/PassphraseCredentialLockController.php', 1049 1050 'PassphraseCredentialPublicController' => 'applications/passphrase/controller/PassphraseCredentialPublicController.php', 1050 1051 'PassphraseCredentialQuery' => 'applications/passphrase/query/PassphraseCredentialQuery.php', 1051 1052 'PassphraseCredentialRevealController' => 'applications/passphrase/controller/PassphraseCredentialRevealController.php', ··· 3806 3807 0 => 'PassphraseController', 3807 3808 1 => 'PhabricatorApplicationSearchResultsControllerInterface', 3808 3809 ), 3810 + 'PassphraseCredentialLockController' => 'PassphraseController', 3809 3811 'PassphraseCredentialPublicController' => 'PassphraseController', 3810 3812 'PassphraseCredentialQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 3811 3813 'PassphraseCredentialRevealController' => 'PassphraseController',
+1
src/applications/passphrase/application/PhabricatorApplicationPassphrase.php
··· 41 41 'destroy/(?P<id>\d+)/' => 'PassphraseCredentialDestroyController', 42 42 'reveal/(?P<id>\d+)/' => 'PassphraseCredentialRevealController', 43 43 'public/(?P<id>\d+)/' => 'PassphraseCredentialPublicController', 44 + 'lock/(?P<id>\d+)/' => 'PassphraseCredentialLockController', 44 45 )); 45 46 } 46 47
+3 -5
src/applications/passphrase/controller/PassphraseCredentialDestroyController.php
··· 50 50 return id(new AphrontRedirectResponse())->setURI($view_uri); 51 51 } 52 52 53 - $dialog = id(new AphrontDialogView()) 53 + return $this->newDialog() 54 54 ->setUser($viewer) 55 55 ->setTitle(pht('Really destroy credential?')) 56 56 ->appendChild( 57 57 pht( 58 58 'This credential will be deactivated and the secret will be '. 59 - 'unrecoverably destroyed. Anything relying on this credential will '. 60 - 'cease to function. This operation can not be undone.')) 59 + 'unrecoverably destroyed. Anything relying on this credential '. 60 + 'will cease to function. This operation can not be undone.')) 61 61 ->addSubmitButton(pht('Destroy Credential')) 62 62 ->addCancelButton($view_uri); 63 - 64 - return id(new AphrontDialogResponse())->setDialog($dialog); 65 63 } 66 64 67 65 }
+46 -16
src/applications/passphrase/controller/PassphraseCredentialEditController.php
··· 93 93 $v_username = $request->getStr('username'); 94 94 $v_view_policy = $request->getStr('viewPolicy'); 95 95 $v_edit_policy = $request->getStr('editPolicy'); 96 + $v_is_locked = $request->getStr('lock'); 96 97 97 98 $v_secret = $request->getStr('secret'); 98 99 $v_password = $request->getStr('password'); ··· 126 127 $type_username = PassphraseCredentialTransaction::TYPE_USERNAME; 127 128 $type_destroy = PassphraseCredentialTransaction::TYPE_DESTROY; 128 129 $type_secret_id = PassphraseCredentialTransaction::TYPE_SECRET_ID; 130 + $type_is_locked = PassphraseCredentialTransaction::TYPE_LOCK; 129 131 $type_view_policy = PhabricatorTransactions::TYPE_VIEW_POLICY; 130 132 $type_edit_policy = PhabricatorTransactions::TYPE_EDIT_POLICY; 131 133 ··· 140 142 ->setNewValue($v_desc); 141 143 142 144 $xactions[] = id(new PassphraseCredentialTransaction()) 143 - ->setTransactionType($type_username) 144 - ->setNewValue($v_username); 145 - 146 - $xactions[] = id(new PassphraseCredentialTransaction()) 147 145 ->setTransactionType($type_view_policy) 148 146 ->setNewValue($v_view_policy); 149 147 ··· 155 153 // the amount of code which handles secret plaintexts. 156 154 $credential->openTransaction(); 157 155 158 - $min_secret = str_replace($bullet, '', trim($v_decrypt)); 159 - if (strlen($min_secret)) { 160 - // If the credential was previously destroyed, restore it when it is 161 - // edited if a secret is provided. 156 + if (!$credential->getIsLocked()) { 162 157 $xactions[] = id(new PassphraseCredentialTransaction()) 163 - ->setTransactionType($type_destroy) 164 - ->setNewValue(0); 158 + ->setTransactionType($type_username) 159 + ->setNewValue($v_username); 160 + 161 + $min_secret = str_replace($bullet, '', trim($v_decrypt)); 162 + if (strlen($min_secret)) { 163 + // If the credential was previously destroyed, restore it when it is 164 + // edited if a secret is provided. 165 + $xactions[] = id(new PassphraseCredentialTransaction()) 166 + ->setTransactionType($type_destroy) 167 + ->setNewValue(0); 165 168 166 - $new_secret = id(new PassphraseSecret()) 167 - ->setSecretData($v_decrypt) 168 - ->save(); 169 + $new_secret = id(new PassphraseSecret()) 170 + ->setSecretData($v_decrypt) 171 + ->save(); 172 + $xactions[] = id(new PassphraseCredentialTransaction()) 173 + ->setTransactionType($type_secret_id) 174 + ->setNewValue($new_secret->getID()); 175 + } 176 + 169 177 $xactions[] = id(new PassphraseCredentialTransaction()) 170 - ->setTransactionType($type_secret_id) 171 - ->setNewValue($new_secret->getID()); 178 + ->setTransactionType($type_is_locked) 179 + ->setNewValue($v_is_locked); 172 180 } 173 181 174 182 try { ··· 210 218 ->execute(); 211 219 212 220 $secret_control = $type->newSecretControl(); 221 + $credential_is_locked = $credential->getIsLocked(); 213 222 214 223 $form = id(new AphrontFormView()) 215 224 ->setUser($viewer) ··· 245 254 ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) 246 255 ->setPolicies($policies)) 247 256 ->appendChild( 248 - id(new AphrontFormDividerControl())) 257 + id(new AphrontFormDividerControl())); 258 + 259 + if ($credential_is_locked) { 260 + $form->appendRemarkupInstructions( 261 + pht('This credential is permanently locked and can not be edited.')); 262 + } 263 + 264 + $form 249 265 ->appendChild( 250 266 id(new AphrontFormTextControl()) 251 267 ->setName('username') 252 268 ->setLabel(pht('Login/Username')) 253 269 ->setValue($v_username) 270 + ->setDisabled($credential_is_locked) 254 271 ->setError($e_username)) 255 272 ->appendChild( 256 273 $secret_control 257 274 ->setName('secret') 258 275 ->setLabel($type->getSecretLabel()) 276 + ->setDisabled($credential_is_locked) 259 277 ->setValue($v_secret)); 260 278 261 279 if ($type->shouldShowPasswordField()) { ··· 263 281 id(new AphrontFormPasswordControl()) 264 282 ->setName('password') 265 283 ->setLabel($type->getPasswordLabel()) 284 + ->setDisabled($credential_is_locked) 266 285 ->setError($e_password)); 286 + } 287 + 288 + if ($is_new) { 289 + $form->appendChild( 290 + id(new AphrontFormCheckboxControl()) 291 + ->addCheckbox( 292 + 'lock', 293 + 1, 294 + pht('Lock Permanently'), 295 + $v_is_locked) 296 + ->setDisabled($credential_is_locked)); 267 297 } 268 298 269 299 $crumbs = $this->buildApplicationCrumbs();
+74
src/applications/passphrase/controller/PassphraseCredentialLockController.php
··· 1 + <?php 2 + 3 + final class PassphraseCredentialLockController 4 + extends PassphraseController { 5 + 6 + private $id; 7 + 8 + public function willProcessRequest(array $data) { 9 + $this->id = $data['id']; 10 + } 11 + 12 + public function processRequest() { 13 + $request = $this->getRequest(); 14 + $viewer = $request->getUser(); 15 + 16 + $credential = id(new PassphraseCredentialQuery()) 17 + ->setViewer($viewer) 18 + ->withIDs(array($this->id)) 19 + ->requireCapabilities( 20 + array( 21 + PhabricatorPolicyCapability::CAN_VIEW, 22 + PhabricatorPolicyCapability::CAN_EDIT, 23 + )) 24 + ->executeOne(); 25 + if (!$credential) { 26 + return new Aphront404Response(); 27 + } 28 + 29 + $type = PassphraseCredentialType::getTypeByConstant( 30 + $credential->getCredentialType()); 31 + if (!$type) { 32 + throw new Exception(pht('Credential has invalid type "%s"!', $type)); 33 + } 34 + 35 + $view_uri = '/K'.$credential->getID(); 36 + 37 + if ($credential->getIsLocked()) { 38 + return $this->newDialog() 39 + ->setTitle(pht('Credential Already Locked')) 40 + ->appendChild( 41 + pht( 42 + 'This credential has been locked and the secret is '. 43 + 'hidden forever. Anything relying on this credential will '. 44 + 'still function. This operation can not be undone.')) 45 + ->addCancelButton($view_uri, pht('Close')); 46 + } 47 + 48 + if ($request->isFormPost()) { 49 + $xactions = array(); 50 + $xactions[] = id(new PassphraseCredentialTransaction()) 51 + ->setTransactionType(PassphraseCredentialTransaction::TYPE_LOCK) 52 + ->setNewValue(1); 53 + 54 + $editor = id(new PassphraseCredentialTransactionEditor()) 55 + ->setActor($viewer) 56 + ->setContinueOnMissingFields(true) 57 + ->setContentSourceFromRequest($request) 58 + ->applyTransactions($credential, $xactions); 59 + 60 + return id(new AphrontRedirectResponse())->setURI($view_uri); 61 + } 62 + 63 + return $this->newDialog() 64 + ->setTitle(pht('Really lock credential?')) 65 + ->appendChild( 66 + pht( 67 + 'This credential will be locked and the secret will be '. 68 + 'hidden forever. Anything relying on this credential will '. 69 + 'still function. This operation can not be undone.')) 70 + ->addSubmitButton(pht('Lock Credential')) 71 + ->addCancelButton($view_uri); 72 + } 73 + 74 + }
+16 -7
src/applications/passphrase/controller/PassphraseCredentialRevealController.php
··· 33 33 $viewer, 34 34 $request, 35 35 $view_uri); 36 + $is_locked = $credential->getIsLocked(); 37 + 38 + if ($is_locked) { 39 + return $this->newDialog() 40 + ->setUser($viewer) 41 + ->setTitle(pht('Credential is locked')) 42 + ->appendChild( 43 + pht( 44 + 'This credential can not be shown, because it is locked.')) 45 + ->addCancelButton($view_uri); 46 + } 36 47 37 48 if ($request->isFormPost()) { 38 49 if ($credential->getSecret()) { ··· 73 84 } 74 85 75 86 $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); 87 + 76 88 if ($is_serious) { 77 89 $body = pht( 78 90 'The secret associated with this credential will be shown in plain '. ··· 80 92 } else { 81 93 $body = pht( 82 94 'The secret associated with this credential will be shown in plain '. 83 - 'text on your screen. Before continuing, wrap your arms around your '. 84 - 'monitor to create a human shield, keeping it safe from prying eyes. '. 85 - 'Protect company secrets!'); 95 + 'text on your screen. Before continuing, wrap your arms around '. 96 + 'your monitor to create a human shield, keeping it safe from '. 97 + 'prying eyes. Protect company secrets!'); 86 98 } 87 - 88 - $dialog = id(new AphrontDialogView()) 99 + return $this->newDialog() 89 100 ->setUser($viewer) 90 101 ->setTitle(pht('Really show secret?')) 91 102 ->appendChild($body) 92 103 ->addSubmitButton(pht('Show Secret')) 93 104 ->addCancelButton($view_uri); 94 - 95 - return id(new AphrontDialogResponse())->setDialog($dialog); 96 105 } 97 106 98 107 }
+20 -2
src/applications/passphrase/controller/PassphraseCredentialViewController.php
··· 85 85 86 86 $id = $credential->getID(); 87 87 88 + $is_locked = $credential->getIsLocked(); 89 + if ($is_locked) { 90 + $credential_lock_text = pht('Locked Permanently'); 91 + $credential_lock_icon = 'lock'; 92 + } else { 93 + $credential_lock_text = pht('Lock Permanently'); 94 + $credential_lock_icon = 'unlock'; 95 + } 96 + 88 97 $actions = id(new PhabricatorActionListView()) 89 98 ->setObjectURI('/K'.$id) 90 99 ->setUser($viewer); ··· 116 125 ->setName(pht('Show Secret')) 117 126 ->setIcon('preview') 118 127 ->setHref($this->getApplicationURI("reveal/{$id}/")) 119 - ->setDisabled(!$can_edit) 128 + ->setDisabled(!$can_edit || $is_locked) 120 129 ->setWorkflow(true)); 121 130 122 131 if ($type->hasPublicKey()) { ··· 125 134 ->setName(pht('Show Public Key')) 126 135 ->setIcon('download-alt') 127 136 ->setHref($this->getApplicationURI("public/{$id}/")) 128 - ->setWorkflow(true)); 137 + ->setWorkflow(true) 138 + ->setDisabled($is_locked)); 129 139 } 140 + 141 + $actions->addAction( 142 + id(new PhabricatorActionView()) 143 + ->setName($credential_lock_text) 144 + ->setIcon($credential_lock_icon) 145 + ->setHref($this->getApplicationURI("lock/{$id}/")) 146 + ->setDisabled($is_locked) 147 + ->setWorkflow(true)); 130 148 } 131 149 132 150
+11 -2
src/applications/passphrase/editor/PassphraseCredentialTransactionEditor.php
··· 15 15 $types[] = PassphraseCredentialTransaction::TYPE_SECRET_ID; 16 16 $types[] = PassphraseCredentialTransaction::TYPE_DESTROY; 17 17 $types[] = PassphraseCredentialTransaction::TYPE_LOOKEDATSECRET; 18 + $types[] = PassphraseCredentialTransaction::TYPE_LOCK; 18 19 19 20 return $types; 20 21 } ··· 35 36 case PassphraseCredentialTransaction::TYPE_SECRET_ID: 36 37 return $object->getSecretID(); 37 38 case PassphraseCredentialTransaction::TYPE_DESTROY: 38 - return $object->getIsDestroyed(); 39 + return (int)$object->getIsDestroyed(); 40 + case PassphraseCredentialTransaction::TYPE_LOCK: 41 + return (int)$object->getIsLocked(); 39 42 case PassphraseCredentialTransaction::TYPE_LOOKEDATSECRET: 40 43 return null; 41 44 } ··· 51 54 case PassphraseCredentialTransaction::TYPE_DESCRIPTION: 52 55 case PassphraseCredentialTransaction::TYPE_USERNAME: 53 56 case PassphraseCredentialTransaction::TYPE_SECRET_ID: 54 - case PassphraseCredentialTransaction::TYPE_DESTROY: 55 57 case PassphraseCredentialTransaction::TYPE_LOOKEDATSECRET: 56 58 return $xaction->getNewValue(); 59 + case PassphraseCredentialTransaction::TYPE_DESTROY: 60 + case PassphraseCredentialTransaction::TYPE_LOCK: 61 + return (int)$xaction->getNewValue(); 57 62 } 58 63 return parent::getCustomTransactionNewValue($object, $xaction); 59 64 } ··· 98 103 return; 99 104 case PassphraseCredentialTransaction::TYPE_LOOKEDATSECRET: 100 105 return; 106 + case PassphraseCredentialTransaction::TYPE_LOCK: 107 + $object->setIsLocked((int)$xaction->getNewValue()); 108 + return; 101 109 } 102 110 103 111 return parent::applyCustomInternalTransaction($object, $xaction); ··· 114 122 case PassphraseCredentialTransaction::TYPE_SECRET_ID: 115 123 case PassphraseCredentialTransaction::TYPE_DESTROY: 116 124 case PassphraseCredentialTransaction::TYPE_LOOKEDATSECRET: 125 + case PassphraseCredentialTransaction::TYPE_LOCK: 117 126 case PhabricatorTransactions::TYPE_VIEW_POLICY: 118 127 case PhabricatorTransactions::TYPE_EDIT_POLICY: 119 128 return;
+1
src/applications/passphrase/storage/PassphraseCredential.php
··· 12 12 protected $username; 13 13 protected $secretID; 14 14 protected $isDestroyed; 15 + protected $isLocked = 0; 15 16 16 17 private $secret = self::ATTACHABLE; 17 18
+7
src/applications/passphrase/storage/PassphraseCredentialTransaction.php
··· 9 9 const TYPE_SECRET_ID = 'passphrase:secretID'; 10 10 const TYPE_DESTROY = 'passphrase:destroy'; 11 11 const TYPE_LOOKEDATSECRET = 'passphrase:lookedAtSecret'; 12 + const TYPE_LOCK = 'passphrase:lock'; 12 13 13 14 public function getApplicationName() { 14 15 return 'passphrase'; ··· 26 27 $old = $this->getOldValue(); 27 28 switch ($this->getTransactionType()) { 28 29 case self::TYPE_DESCRIPTION: 30 + return ($old === null); 31 + case self::TYPE_LOCK: 29 32 return ($old === null); 30 33 case self::TYPE_USERNAME: 31 34 return !strlen($old); ··· 83 86 case self::TYPE_LOOKEDATSECRET: 84 87 return pht( 85 88 '%s examined the secret plaintext for this credential.', 89 + $this->renderHandleLink($author_phid)); 90 + case self::TYPE_LOCK: 91 + return pht( 92 + '%s locked this credential.', 86 93 $this->renderHandleLink($author_phid)); 87 94 } 88 95