@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
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Allow any transaction group to be signed with a one-shot "Sign With MFA" action

Summary:
Depends on D19896. Ref T13222. See PHI873. Add a core "Sign With MFA" transaction type which prompts you for MFA and marks your transactions as MFA'd.

This is a one-shot gate and does not keep you in MFA.

Test Plan:
- Used "Sign with MFA", got prompted for MFA, answered MFA, saw transactions apply with MFA metadata and markers.
- Tried to sign alone, got appropriate errors.
- Tried to sign no-op changes, got appropriate errors.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13222

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

+265 -33
+2
src/__phutil_library_map__.php
··· 2231 2231 'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php', 2232 2232 'PhabricatorAuthLoginHandler' => 'applications/auth/handler/PhabricatorAuthLoginHandler.php', 2233 2233 'PhabricatorAuthLogoutConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthLogoutConduitAPIMethod.php', 2234 + 'PhabricatorAuthMFAEditEngineExtension' => 'applications/auth/engineextension/PhabricatorAuthMFAEditEngineExtension.php', 2234 2235 'PhabricatorAuthMainMenuBarExtension' => 'applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php', 2235 2236 'PhabricatorAuthManagementCachePKCS8Workflow' => 'applications/auth/management/PhabricatorAuthManagementCachePKCS8Workflow.php', 2236 2237 'PhabricatorAuthManagementLDAPWorkflow' => 'applications/auth/management/PhabricatorAuthManagementLDAPWorkflow.php', ··· 7885 7886 'PhabricatorAuthLoginController' => 'PhabricatorAuthController', 7886 7887 'PhabricatorAuthLoginHandler' => 'Phobject', 7887 7888 'PhabricatorAuthLogoutConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod', 7889 + 'PhabricatorAuthMFAEditEngineExtension' => 'PhabricatorEditEngineExtension', 7888 7890 'PhabricatorAuthMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension', 7889 7891 'PhabricatorAuthManagementCachePKCS8Workflow' => 'PhabricatorAuthManagementWorkflow', 7890 7892 'PhabricatorAuthManagementLDAPWorkflow' => 'PhabricatorAuthManagementWorkflow',
+52
src/applications/auth/engineextension/PhabricatorAuthMFAEditEngineExtension.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthMFAEditEngineExtension 4 + extends PhabricatorEditEngineExtension { 5 + 6 + const EXTENSIONKEY = 'auth.mfa'; 7 + const FIELDKEY = 'mfa'; 8 + 9 + public function getExtensionPriority() { 10 + return 12000; 11 + } 12 + 13 + public function isExtensionEnabled() { 14 + return true; 15 + } 16 + 17 + public function getExtensionName() { 18 + return pht('MFA'); 19 + } 20 + 21 + public function supportsObject( 22 + PhabricatorEditEngine $engine, 23 + PhabricatorApplicationTransactionInterface $object) { 24 + return true; 25 + } 26 + 27 + public function buildCustomEditFields( 28 + PhabricatorEditEngine $engine, 29 + PhabricatorApplicationTransactionInterface $object) { 30 + 31 + $mfa_type = PhabricatorTransactions::TYPE_MFA; 32 + 33 + $viewer = $engine->getViewer(); 34 + 35 + $mfa_field = id(new PhabricatorApplyEditField()) 36 + ->setViewer($viewer) 37 + ->setKey(self::FIELDKEY) 38 + ->setLabel(pht('MFA')) 39 + ->setIsFormField(false) 40 + ->setCommentActionLabel(pht('Sign With MFA')) 41 + ->setCommentActionOrder(12000) 42 + ->setActionDescription( 43 + pht('You will be prompted to provide MFA when you submit.')) 44 + ->setDescription(pht('Sign this transaction group with MFA.')) 45 + ->setTransactionType($mfa_type); 46 + 47 + return array( 48 + $mfa_field, 49 + ); 50 + } 51 + 52 + }
+1
src/applications/transactions/constants/PhabricatorTransactions.php
··· 16 16 const TYPE_COLUMNS = 'core:columns'; 17 17 const TYPE_SUBTYPE = 'core:subtype'; 18 18 const TYPE_HISTORY = 'core:history'; 19 + const TYPE_MFA = 'core:mfa'; 19 20 20 21 const COLOR_RED = 'red'; 21 22 const COLOR_ORANGE = 'orange';
+5 -1
src/applications/transactions/editengine/PhabricatorEditEngine.php
··· 1105 1105 $editor = $object->getApplicationTransactionEditor() 1106 1106 ->setActor($viewer) 1107 1107 ->setContentSourceFromRequest($request) 1108 + ->setCancelURI($cancel_uri) 1108 1109 ->setContinueOnNoEffect(true); 1109 1110 1110 1111 try { ··· 1785 1786 $controller = $this->getController(); 1786 1787 $request = $controller->getRequest(); 1787 1788 1788 - if (!$request->isFormPost()) { 1789 + // NOTE: We handle hisec inside the transaction editor with "Sign With MFA" 1790 + // comment actions. 1791 + if (!$request->isFormOrHisecPost()) { 1789 1792 return new Aphront400Response(); 1790 1793 } 1791 1794 ··· 1919 1922 ->setContinueOnNoEffect($request->isContinueRequest()) 1920 1923 ->setContinueOnMissingFields(true) 1921 1924 ->setContentSourceFromRequest($request) 1925 + ->setCancelURI($view_uri) 1922 1926 ->setRaiseWarnings(!$request->getBool('editEngine.warnings')) 1923 1927 ->setIsPreview($is_preview); 1924 1928
+174 -31
src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
··· 84 84 85 85 private $transactionQueue = array(); 86 86 private $sendHistory = false; 87 + private $shouldRequireMFA = false; 88 + private $hasRequiredMFA = false; 89 + private $request; 90 + private $cancelURI; 87 91 88 92 const STORAGE_ENCODING_BINARY = 'binary'; 89 93 ··· 284 288 return $this->raiseWarnings; 285 289 } 286 290 291 + public function setShouldRequireMFA($should_require_mfa) { 292 + if ($this->hasRequiredMFA) { 293 + throw new Exception( 294 + pht( 295 + 'Call to setShouldRequireMFA() is too late: this Editor has already '. 296 + 'checked for MFA requirements.')); 297 + } 298 + 299 + $this->shouldRequireMFA = $should_require_mfa; 300 + return $this; 301 + } 302 + 303 + public function getShouldRequireMFA() { 304 + return $this->shouldRequireMFA; 305 + } 306 + 287 307 public function getTransactionTypesForObject($object) { 288 308 $old = $this->object; 289 309 try { ··· 328 348 $types[] = PhabricatorTransactions::TYPE_SPACE; 329 349 } 330 350 351 + $types[] = PhabricatorTransactions::TYPE_MFA; 352 + 331 353 $template = $this->object->getApplicationTransactionTemplate(); 332 354 if ($template instanceof PhabricatorModularTransaction) { 333 355 $xtypes = $template->newModularTransactionTypes(); ··· 383 405 return null; 384 406 case PhabricatorTransactions::TYPE_SUBTYPE: 385 407 return $object->getEditEngineSubtype(); 408 + case PhabricatorTransactions::TYPE_MFA: 409 + return null; 386 410 case PhabricatorTransactions::TYPE_SUBSCRIBERS: 387 411 return array_values($this->subscribers); 388 412 case PhabricatorTransactions::TYPE_VIEW_POLICY: ··· 473 497 case PhabricatorTransactions::TYPE_SUBTYPE: 474 498 case PhabricatorTransactions::TYPE_HISTORY: 475 499 return $xaction->getNewValue(); 500 + case PhabricatorTransactions::TYPE_MFA: 501 + return true; 476 502 case PhabricatorTransactions::TYPE_SPACE: 477 503 $space_phid = $xaction->getNewValue(); 478 504 if (!strlen($space_phid)) { ··· 611 637 case PhabricatorTransactions::TYPE_CREATE: 612 638 case PhabricatorTransactions::TYPE_HISTORY: 613 639 case PhabricatorTransactions::TYPE_SUBTYPE: 640 + case PhabricatorTransactions::TYPE_MFA: 614 641 case PhabricatorTransactions::TYPE_TOKEN: 615 642 case PhabricatorTransactions::TYPE_VIEW_POLICY: 616 643 case PhabricatorTransactions::TYPE_EDIT_POLICY: ··· 673 700 case PhabricatorTransactions::TYPE_CREATE: 674 701 case PhabricatorTransactions::TYPE_HISTORY: 675 702 case PhabricatorTransactions::TYPE_SUBTYPE: 703 + case PhabricatorTransactions::TYPE_MFA: 676 704 case PhabricatorTransactions::TYPE_EDGE: 677 705 case PhabricatorTransactions::TYPE_TOKEN: 678 706 case PhabricatorTransactions::TYPE_VIEW_POLICY: ··· 850 878 $xaction->setIsSilentTransaction(true); 851 879 } 852 880 853 - if ($actor->hasHighSecuritySession()) { 854 - $xaction->setIsMFATransaction(true); 855 - } 856 - 857 881 return $xaction; 858 882 } 859 883 ··· 893 917 } 894 918 895 919 public function setContentSourceFromRequest(AphrontRequest $request) { 920 + $this->setRequest($request); 896 921 return $this->setContentSource( 897 922 PhabricatorContentSource::newFromRequest($request)); 898 923 } ··· 901 926 return $this->contentSource; 902 927 } 903 928 929 + public function setRequest(AphrontRequest $request) { 930 + $this->request = $request; 931 + return $this; 932 + } 933 + 934 + public function getRequest() { 935 + return $this->request; 936 + } 937 + 938 + public function setCancelURI($cancel_uri) { 939 + $this->cancelURI = $cancel_uri; 940 + return $this; 941 + } 942 + 943 + public function getCancelURI() { 944 + return $this->cancelURI; 945 + } 946 + 904 947 final public function applyTransactions( 905 948 PhabricatorLiskDAO $object, 906 949 array $xactions) { ··· 966 1009 $warnings); 967 1010 } 968 1011 } 1012 + } 969 1013 1014 + foreach ($xactions as $xaction) { 1015 + $this->adjustTransactionValues($object, $xaction); 1016 + } 1017 + 1018 + // Now that we've merged and combined transactions, check for required 1019 + // capabilities. Note that we're doing this before filtering 1020 + // transactions: if you try to apply an edit which you do not have 1021 + // permission to apply, we want to give you a permissions error even 1022 + // if the edit would have no effect. 1023 + $this->applyCapabilityChecks($object, $xactions); 1024 + 1025 + $xactions = $this->filterTransactions($object, $xactions); 1026 + 1027 + if (!$is_preview) { 970 1028 $this->willApplyTransactions($object, $xactions); 1029 + 1030 + $this->hasRequiredMFA = true; 1031 + if ($this->getShouldRequireMFA()) { 1032 + $this->requireMFA($object, $xactions); 1033 + } 971 1034 972 1035 if ($object->getID()) { 973 1036 $this->buildOldRecipientLists($object, $xactions); ··· 993 1056 if ($this->shouldApplyInitialEffects($object, $xactions)) { 994 1057 $this->applyInitialEffects($object, $xactions); 995 1058 } 996 - 997 - foreach ($xactions as $xaction) { 998 - $this->adjustTransactionValues($object, $xaction); 999 - } 1000 - 1001 - // Now that we've merged and combined transactions, check for required 1002 - // capabilities. Note that we're doing this before filtering 1003 - // transactions: if you try to apply an edit which you do not have 1004 - // permission to apply, we want to give you a permissions error even 1005 - // if the edit would have no effect. 1006 - $this->applyCapabilityChecks($object, $xactions); 1007 - 1008 - // See T13186. Fatal hard if this object has an older 1009 - // "requireCapabilities()" method. The code may rely on this method being 1010 - // called to apply policy checks, so err on the side of safety and fatal. 1011 - // TODO: Remove this check after some time has passed. 1012 - if (method_exists($this, 'requireCapabilities')) { 1013 - throw new Exception( 1014 - pht( 1015 - 'Editor (of class "%s") implements obsolete policy method '. 1016 - 'requireCapabilities(). The implementation for this Editor '. 1017 - 'MUST be updated. See <%s> for discussion.', 1018 - get_class($this), 1019 - 'https://secure.phabricator.com/T13186')); 1020 - } 1021 - 1022 - $xactions = $this->filterTransactions($object, $xactions); 1023 1059 1024 1060 // TODO: Once everything is on EditEngine, just use getIsNewObject() to 1025 1061 // figure this out instead. ··· 1579 1615 // This is a special magic transaction which sends you history via 1580 1616 // email and is only partially supported in the upstream. You don't 1581 1617 // need any capabilities to apply it. 1618 + return null; 1619 + case PhabricatorTransactions::TYPE_MFA: 1620 + // Signing a transaction group with MFA does not require permissions 1621 + // on its own. 1582 1622 return null; 1583 1623 case PhabricatorTransactions::TYPE_EDGE: 1584 1624 return $this->getLegacyRequiredEdgeCapabilities($xaction); ··· 2272 2312 array $xactions) { 2273 2313 2274 2314 $type_comment = PhabricatorTransactions::TYPE_COMMENT; 2315 + $type_mfa = PhabricatorTransactions::TYPE_MFA; 2275 2316 2276 2317 $no_effect = array(); 2277 2318 $has_comment = false; 2278 2319 $any_effect = false; 2320 + 2321 + $meta_xactions = array(); 2279 2322 foreach ($xactions as $key => $xaction) { 2323 + if ($xaction->getTransactionType() === $type_mfa) { 2324 + $meta_xactions[$key] = $xaction; 2325 + continue; 2326 + } 2327 + 2280 2328 if ($this->transactionHasEffect($object, $xaction)) { 2281 2329 if ($xaction->getTransactionType() != $type_comment) { 2282 2330 $any_effect = true; ··· 2286 2334 } else { 2287 2335 $no_effect[$key] = $xaction; 2288 2336 } 2337 + 2289 2338 if ($xaction->hasComment()) { 2290 2339 $has_comment = true; 2291 2340 } 2292 2341 } 2293 2342 2343 + // If every transaction is a meta-transaction applying to the transaction 2344 + // group, these transactions are junk. 2345 + if (count($meta_xactions) == count($xactions)) { 2346 + $no_effect = $xactions; 2347 + $any_effect = false; 2348 + } 2349 + 2294 2350 if (!$no_effect) { 2295 2351 return $xactions; 2352 + } 2353 + 2354 + // If none of the transactions have an effect, the meta-transactions also 2355 + // have no effect. Add them to the "no effect" list so we get a full set 2356 + // of errors for everything. 2357 + if (!$any_effect) { 2358 + $no_effect += $meta_xactions; 2296 2359 } 2297 2360 2298 2361 if (!$this->getContinueOnNoEffect() && !$this->getIsPreview()) { ··· 2371 2434 break; 2372 2435 case PhabricatorTransactions::TYPE_SUBTYPE: 2373 2436 $errors[] = $this->validateSubtypeTransactions( 2437 + $object, 2438 + $xactions, 2439 + $type); 2440 + break; 2441 + case PhabricatorTransactions::TYPE_MFA: 2442 + $errors[] = $this->validateMFATransactions( 2374 2443 $object, 2375 2444 $xactions, 2376 2445 $type); ··· 2551 2620 $xaction); 2552 2621 continue; 2553 2622 } 2623 + } 2624 + 2625 + return $errors; 2626 + } 2627 + 2628 + private function validateMFATransactions( 2629 + PhabricatorLiskDAO $object, 2630 + array $xactions, 2631 + $transaction_type) { 2632 + $errors = array(); 2633 + 2634 + $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( 2635 + 'userPHID = %s', 2636 + $this->getActingAsPHID()); 2637 + 2638 + foreach ($xactions as $xaction) { 2639 + if (!$factors) { 2640 + $errors[] = new PhabricatorApplicationTransactionValidationError( 2641 + $transaction_type, 2642 + pht('No MFA'), 2643 + pht( 2644 + 'You do not have any MFA factors attached to your account, so '. 2645 + 'you can not sign this transaction group with MFA. Add MFA to '. 2646 + 'your account in Settings.'), 2647 + $xaction); 2648 + } 2649 + } 2650 + 2651 + if ($xactions) { 2652 + $this->setShouldRequireMFA(true); 2554 2653 } 2555 2654 2556 2655 return $errors; ··· 4705 4804 ->setMetadataValue('inline.details', $inline_details) 4706 4805 ->setOldValue($old_value) 4707 4806 ->setNewValue($new_value); 4807 + } 4808 + 4809 + private function requireMFA(PhabricatorLiskDAO $object, array $xactions) { 4810 + $editor_class = get_class($this); 4811 + 4812 + $object_phid = $object->getPHID(); 4813 + if ($object_phid) { 4814 + $workflow_key = sprintf( 4815 + 'editor(%s).phid(%s)', 4816 + $editor_class, 4817 + $object_phid); 4818 + } else { 4819 + $workflow_key = sprintf( 4820 + 'editor(%s).new()', 4821 + $editor_class); 4822 + } 4823 + 4824 + $actor = $this->getActor(); 4825 + 4826 + $request = $this->getRequest(); 4827 + if ($request === null) { 4828 + throw new Exception( 4829 + pht( 4830 + 'This transaction group requires MFA to apply, but the Editor was '. 4831 + 'not configured with a Request. This workflow can not perform an '. 4832 + 'MFA check.')); 4833 + } 4834 + 4835 + $cancel_uri = $this->getCancelURI(); 4836 + if ($cancel_uri === null) { 4837 + throw new Exception( 4838 + pht( 4839 + 'This transaction group requires MFA to apply, but the Editor was '. 4840 + 'not configured with a Cancel URI. This workflow can not perform '. 4841 + 'an MFA check.')); 4842 + } 4843 + 4844 + id(new PhabricatorAuthSessionEngine()) 4845 + ->setWorkflowKey($workflow_key) 4846 + ->requireHighSecurityToken($actor, $request, $cancel_uri); 4847 + 4848 + foreach ($xactions as $xaction) { 4849 + $xaction->setIsMFATransaction(true); 4850 + } 4708 4851 } 4709 4852 4710 4853 }
+30
src/applications/transactions/storage/PhabricatorApplicationTransaction.php
··· 473 473 return 'fa-th-large'; 474 474 case PhabricatorTransactions::TYPE_COLUMNS: 475 475 return 'fa-columns'; 476 + case PhabricatorTransactions::TYPE_MFA: 477 + return 'fa-vcard'; 476 478 } 477 479 478 480 return 'fa-pencil'; ··· 510 512 return 'sky'; 511 513 } 512 514 break; 515 + case PhabricatorTransactions::TYPE_MFA; 516 + return 'pink'; 513 517 } 514 518 return null; 515 519 } ··· 835 839 return pht( 836 840 'You have not moved this object to any columns it is not '. 837 841 'already in.'); 842 + case PhabricatorTransactions::TYPE_MFA: 843 + return pht( 844 + 'You can not sign a transaction group that has no other '. 845 + 'effects.'); 838 846 } 839 847 840 848 return pht( ··· 1076 1084 } 1077 1085 break; 1078 1086 1087 + 1088 + case PhabricatorTransactions::TYPE_MFA: 1089 + return pht( 1090 + '%s signed these changes with MFA.', 1091 + $this->renderHandleLink($author_phid)); 1092 + 1079 1093 default: 1080 1094 // In developer mode, provide a better hint here about which string 1081 1095 // we're missing. ··· 1238 1252 } 1239 1253 break; 1240 1254 1255 + case PhabricatorTransactions::TYPE_MFA: 1256 + return null; 1257 + 1241 1258 } 1242 1259 1243 1260 return $this->getTitle(); ··· 1320 1337 // (which are shown anyway) but less interesting than any other type of 1321 1338 // transaction. 1322 1339 return 0.75; 1340 + case PhabricatorTransactions::TYPE_MFA: 1341 + // We want MFA signatures to render at the top of transaction groups, 1342 + // on top of the things they signed. 1343 + return 10; 1323 1344 } 1324 1345 1325 1346 return 1.0; ··· 1433 1454 if ($this->getContentSource()) { 1434 1455 $this_source = $this->getContentSource()->getSource(); 1435 1456 } 1457 + 1458 + $type_mfa = PhabricatorTransactions::TYPE_MFA; 1436 1459 1437 1460 foreach ($group as $xaction) { 1438 1461 // Don't group transactions by different authors. ··· 1476 1499 $is_mfa = $this->getIsMFATransaction(); 1477 1500 if ($is_mfa != $xaction->getIsMFATransaction()) { 1478 1501 return false; 1502 + } 1503 + 1504 + // Don't group two "Sign with MFA" transactions together. 1505 + if ($this->getTransactionType() === $type_mfa) { 1506 + if ($xaction->getTransactionType() === $type_mfa) { 1507 + return false; 1508 + } 1479 1509 } 1480 1510 } 1481 1511
+1 -1
src/view/phui/PHUITimelineEventView.php
··· 605 605 // provide a hint that it was extra authentic. 606 606 if ($this->getIsMFA()) { 607 607 $extra[] = id(new PHUIIconView()) 608 - ->setIcon('fa-vcard', 'green') 608 + ->setIcon('fa-vcard', 'pink') 609 609 ->setTooltip(pht('MFA Authenticated')); 610 610 } 611 611 }