1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-10 08:52:39 +01:00

Add credential rotation and statuses (disabled, unsubscribed) to Phortune external email

Summary: Depends on D20737. Ref T13367. Allow external addresses to have their access key rotated. Account managers can disable them, and anyone with the link can permanently unsubscribe them.

Test Plan: Enabled/disabled addresses; permanently unsubscribed addresses.

Maniphest Tasks: T13367

Differential Revision: https://secure.phabricator.com/D20738
This commit is contained in:
epriestley 2019-08-23 09:32:52 -07:00
parent 8f6a1ab015
commit 4e13551e85
11 changed files with 434 additions and 4 deletions

View file

@ -5237,7 +5237,11 @@ phutil_register_library_map(array(
'PhortuneAccountEmailEditor' => 'applications/phortune/editor/PhortuneAccountEmailEditor.php',
'PhortuneAccountEmailPHIDType' => 'applications/phortune/phid/PhortuneAccountEmailPHIDType.php',
'PhortuneAccountEmailQuery' => 'applications/phortune/query/PhortuneAccountEmailQuery.php',
'PhortuneAccountEmailRotateController' => 'applications/phortune/controller/account/PhortuneAccountEmailRotateController.php',
'PhortuneAccountEmailRotateTransaction' => 'applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php',
'PhortuneAccountEmailStatus' => 'applications/phortune/constants/PhortuneAccountEmailStatus.php',
'PhortuneAccountEmailStatusController' => 'applications/phortune/controller/account/PhortuneAccountEmailStatusController.php',
'PhortuneAccountEmailStatusTransaction' => 'applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php',
'PhortuneAccountEmailTransaction' => 'applications/phortune/storage/PhortuneAccountEmailTransaction.php',
'PhortuneAccountEmailTransactionQuery' => 'applications/phortune/query/PhortuneAccountEmailTransactionQuery.php',
'PhortuneAccountEmailTransactionType' => 'applications/phortune/xaction/PhortuneAccountEmailTransactionType.php',
@ -5296,6 +5300,7 @@ phutil_register_library_map(array(
'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php',
'PhortuneExternalController' => 'applications/phortune/controller/external/PhortuneExternalController.php',
'PhortuneExternalOverviewController' => 'applications/phortune/controller/external/PhortuneExternalOverviewController.php',
'PhortuneExternalUnsubscribeController' => 'applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php',
'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php',
'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php',
'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php',
@ -11815,7 +11820,11 @@ phutil_register_library_map(array(
'PhortuneAccountEmailEditor' => 'PhabricatorApplicationTransactionEditor',
'PhortuneAccountEmailPHIDType' => 'PhabricatorPHIDType',
'PhortuneAccountEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhortuneAccountEmailRotateController' => 'PhortuneAccountController',
'PhortuneAccountEmailRotateTransaction' => 'PhortuneAccountEmailTransactionType',
'PhortuneAccountEmailStatus' => 'Phobject',
'PhortuneAccountEmailStatusController' => 'PhortuneAccountController',
'PhortuneAccountEmailStatusTransaction' => 'PhortuneAccountEmailTransactionType',
'PhortuneAccountEmailTransaction' => 'PhabricatorModularTransaction',
'PhortuneAccountEmailTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhortuneAccountEmailTransactionType' => 'PhabricatorModularTransactionType',
@ -11883,6 +11892,7 @@ phutil_register_library_map(array(
'PhortuneErrCode' => 'PhortuneConstants',
'PhortuneExternalController' => 'PhortuneController',
'PhortuneExternalOverviewController' => 'PhortuneExternalController',
'PhortuneExternalUnsubscribeController' => 'PhortuneExternalController',
'PhortuneInvoiceView' => 'AphrontTagView',
'PhortuneLandingController' => 'PhortuneController',
'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType',

View file

@ -87,7 +87,12 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
),
'addresses/' => array(
'' => 'PhortuneAccountEmailAddressesController',
'(?P<id>\d+)/' => 'PhortuneAccountEmailViewController',
'(?P<addressID>\d+)/' => array(
'' => 'PhortuneAccountEmailViewController',
'rotate/' => 'PhortuneAccountEmailRotateController',
'(?P<action>disable|enable)/'
=> 'PhortuneAccountEmailStatusController',
),
$this->getEditRoutePattern('edit/')
=> 'PhortuneAccountEmailEditController',
),
@ -106,6 +111,7 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
),
'external/(?P<addressKey>[^/]+)/(?P<accessKey>[^/]+)/' => array(
'' => 'PhortuneExternalOverviewController',
'unsubscribe/' => 'PhortuneExternalUnsubscribeController',
),
'merchant/' => array(
$this->getQueryRoutePattern()

View file

@ -0,0 +1,62 @@
<?php
final class PhortuneAccountEmailRotateController
extends PhortuneAccountController {
protected function shouldRequireAccountEditCapability() {
return true;
}
protected function handleAccountRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$account = $this->getAccount();
$address = id(new PhortuneAccountEmailQuery())
->setViewer($viewer)
->withAccountPHIDs(array($account->getPHID()))
->withIDs(array($request->getURIData('addressID')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$address) {
return new Aphront404Response();
}
$address_uri = $address->getURI();
if ($request->isFormOrHisecPost()) {
$xactions = array();
$xactions[] = $address->getApplicationTransactionTemplate()
->setTransactionType(
PhortuneAccountEmailRotateTransaction::TRANSACTIONTYPE)
->setNewValue(true);
$address->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->setCancelURI($address_uri)
->applyTransactions($address, $xactions);
return id(new AphrontRedirectResponse())->setURI($address_uri);
}
return $this->newDialog()
->setTitle(pht('Rotate Access Key'))
->appendParagraph(
pht(
'Rotate the access key for email address %s?',
phutil_tag('strong', array(), $address->getAddress())))
->appendParagraph(
pht(
'Existing access links which have been sent to this email address '.
'will stop working.'))
->addSubmitButton(pht('Rotate Access Key'))
->addCancelButton($address_uri);
}
}

View file

@ -0,0 +1,137 @@
<?php
final class PhortuneAccountEmailStatusController
extends PhortuneAccountController {
protected function shouldRequireAccountEditCapability() {
return true;
}
protected function handleAccountRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$account = $this->getAccount();
$address = id(new PhortuneAccountEmailQuery())
->setViewer($viewer)
->withAccountPHIDs(array($account->getPHID()))
->withIDs(array($request->getURIData('addressID')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$address) {
return new Aphront404Response();
}
$address_uri = $address->getURI();
$is_enable = false;
$is_disable = false;
$old_status = $address->getStatus();
switch ($request->getURIData('action')) {
case 'enable':
if ($old_status === PhortuneAccountEmailStatus::STATUS_ACTIVE) {
return $this->newDialog()
->setTitle(pht('Already Enabled'))
->appendParagraph(
pht(
'You can not enable this address because it is already '.
'active.'))
->addCancelButton($address_uri);
}
if ($old_status === PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED) {
return $this->newDialog()
->setTitle(pht('Permanently Unsubscribed'))
->appendParagraph(
pht(
'You can not enable this address because it has been '.
'permanently unsubscribed.'))
->addCancelButton($address_uri);
}
$new_status = PhortuneAccountEmailStatus::STATUS_ACTIVE;
$is_enable = true;
break;
case 'disable':
if ($old_status === PhortuneAccountEmailStatus::STATUS_DISABLED) {
return $this->newDialog()
->setTitle(pht('Already Disabled'))
->appendParagraph(
pht(
'You can not disabled this address because it is already '.
'disabled.'))
->addCancelButton($address_uri);
}
if ($old_status === PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED) {
return $this->newDialog()
->setTitle(pht('Permanently Unsubscribed'))
->appendParagraph(
pht(
'You can not disable this address because it has been '.
'permanently unsubscribed.'))
->addCancelButton($address_uri);
}
$new_status = PhortuneAccountEmailStatus::STATUS_DISABLED;
$is_disable = true;
break;
default:
return new Aphront404Response();
}
if ($request->isFormOrHisecPost()) {
$xactions = array();
$xactions[] = $address->getApplicationTransactionTemplate()
->setTransactionType(
PhortuneAccountEmailStatusTransaction::TRANSACTIONTYPE)
->setNewValue($new_status);
$address->getApplicationTransactionEditor()
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->setCancelURI($address_uri)
->applyTransactions($address, $xactions);
return id(new AphrontRedirectResponse())->setURI($address_uri);
}
$dialog = $this->newDialog();
$body = array();
if ($is_disable) {
$title = pht('Disable Address');
$body[] = pht(
'This address will no longer receive email, and access links will '.
'no longer function.');
$submit = pht('Disable Address');
} else {
$title = pht('Enable Address');
$body[] = pht(
'This address will receive email again, and existing links '.
'to access order history will work again.');
$submit = pht('Enable Address');
}
foreach ($body as $graph) {
$dialog->appendParagraph($graph);
}
return $dialog
->setTitle($title)
->addCancelButton($address_uri)
->addSubmitButton($submit);
}
}

View file

@ -14,7 +14,7 @@ final class PhortuneAccountEmailViewController
$address = id(new PhortuneAccountEmailQuery())
->setViewer($viewer)
->withAccountPHIDs(array($account->getPHID()))
->withIDs(array($request->getURIData('id')))
->withIDs(array($request->getURIData('addressID')))
->executeOne();
if (!$address) {
return new Aphront404Response();
@ -83,6 +83,56 @@ final class PhortuneAccountEmailViewController
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit));
switch ($address->getStatus()) {
case PhortuneAccountEmailStatus::STATUS_ACTIVE:
$disable_name = pht('Disable Address');
$disable_icon = 'fa-times';
$can_disable = true;
$disable_action = 'disable';
break;
case PhortuneAccountEmailStatus::STATUS_DISABLED:
$disable_name = pht('Enable Address');
$disable_icon = 'fa-check';
$can_disable = true;
$disable_action = 'enable';
break;
case PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED:
$disable_name = pht('Disable Address');
$disable_icon = 'fa-times';
$can_disable = false;
$disable_action = 'disable';
break;
}
$disable_uri = $this->getApplicationURI(
urisprintf(
'account/%d/addresses/%d/%s/',
$account->getID(),
$address->getID(),
$disable_action));
$curtain->addAction(
id(new PhabricatorActionView())
->setName($disable_name)
->setIcon($disable_icon)
->setHref($disable_uri)
->setDisabled(!$can_disable)
->setWorkflow(true));
$rotate_uri = $this->getApplicationURI(
urisprintf(
'account/%d/addresses/%d/rotate/',
$account->getID(),
$address->getID()));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Rotate Access Key'))
->setIcon('fa-refresh')
->setHref($rotate_uri)
->setDisabled(!$can_edit)
->setWorkflow(true));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Show External View'))
@ -100,7 +150,23 @@ final class PhortuneAccountEmailViewController
$view = id(new PHUIPropertyListView())
->setUser($viewer);
$access_key = $address->getAccessKey();
// This is not a meaningful security barrier: the full plaintext of the
// access key is visible on the page in the link target of the "Show
// External View" action. It's just here to make it clear "Rotate Access
// Key" actually does something.
$prefix_length = 4;
$visible_part = substr($access_key, 0, $prefix_length);
$masked_part = str_repeat(
"\xE2\x80\xA2",
strlen($access_key) - $prefix_length);
$access_display = $visible_part.$masked_part;
$access_display = phutil_tag('tt', array(), $access_display);
$view->addProperty(pht('Email Address'), $address->getAddress());
$view->addProperty(pht('Access Key'), $access_display);
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Email Address Details'))

View file

@ -83,7 +83,29 @@ abstract class PhortuneExternalController
return $dialog;
}
// TODO: Test that status is good.
switch ($email->getStatus()) {
case PhortuneAccountEmailStatus::STATUS_ACTIVE:
break;
case PhortuneAccountEmailStatus::STATUS_DISABLED:
return $this->newDialog()
->setTitle(pht('Address Disabled'))
->appendParagraph(
pht(
'This email address (%s) has been disabled and no longer has '.
'access to this payment account.',
$email_display));
case PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED:
return $this->newDialog()
->setTitle(pht('Permanently Unsubscribed'))
->appendParagraph(
pht(
'This email address (%s) has been permanently unsubscribed '.
'and no longer has access to this payment account.',
$email_display));
break;
default:
return new Aphront404Response();
}
$this->email = $email;

View file

@ -12,7 +12,14 @@ final class PhortuneExternalOverviewController
->setBorder(true);
$header = id(new PHUIHeaderView())
->setHeader(pht('Invoices and Receipts: %s', $account->getName()));
->setHeader(pht('Invoices and Receipts: %s', $account->getName()))
->addActionLink(
id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-times')
->setText(pht('Unsubscribe'))
->setHref($email->getUnsubscribeURI())
->setWorkflow(true));
$external_view = $this->newExternalView();
$invoices_view = $this->newInvoicesView();

View file

@ -0,0 +1,67 @@
<?php
final class PhortuneExternalUnsubscribeController
extends PhortuneExternalController {
protected function handleExternalRequest(AphrontRequest $request) {
$xviewer = $this->getExternalViewer();
$email = $this->getAccountEmail();
$account = $email->getAccount();
$email_uri = $email->getExternalURI();
if ($request->isFormOrHisecPost()) {
$xactions = array();
$xactions[] = $email->getApplicationTransactionTemplate()
->setTransactionType(
PhortuneAccountEmailStatusTransaction::TRANSACTIONTYPE)
->setNewValue(PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED);
$email->getApplicationTransactionEditor()
->setActor($xviewer)
->setActingAsPHID($email->getPHID())
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->setCancelURI($email_uri)
->applyTransactions($email, $xactions);
return id(new AphrontRedirectResponse())->setURI($email_uri);
}
$email_display = phutil_tag(
'strong',
array(),
$email->getAddress());
$account_display = phutil_tag(
'strong',
array(),
$account->getName());
$submit = pht(
'Permanently Unsubscribe (%s)',
$email->getAddress());
return $this->newDialog()
->setTitle(pht('Permanently Unsubscribe'))
->appendParagraph(
pht(
'Permanently unsubscribe this email address (%s) from this '.
'payment account (%s)?',
$email_display,
$account_display))
->appendParagraph(
pht(
'You will no longer receive email and access links will no longer '.
'function.'))
->appendParagraph(
pht(
'This action is permanent and can not be undone.'))
->addCancelButton($email_uri)
->addSubmitButton($submit);
}
}

View file

@ -85,6 +85,13 @@ final class PhortuneAccountEmail
$this->getAccessKey());
}
public function getUnsubscribeURI() {
return urisprintf(
'/phortune/external/%s/%s/unsubscribe/',
$this->getAddressKey(),
$this->getAccessKey());
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -0,0 +1,23 @@
<?php
final class PhortuneAccountEmailRotateTransaction
extends PhortuneAccountEmailTransactionType {
const TRANSACTIONTYPE = 'rotate';
public function generateOldValue($object) {
return false;
}
public function applyInternalEffects($object, $value) {
$access_key = Filesystem::readRandomCharacters(16);
$object->setAccessKey($access_key);
}
public function getTitle() {
return pht(
'%s rotated the access key for this email address.',
$this->renderAuthor());
}
}

View file

@ -0,0 +1,23 @@
<?php
final class PhortuneAccountEmailStatusTransaction
extends PhortuneAccountEmailTransactionType {
const TRANSACTIONTYPE = 'status';
public function generateOldValue($object) {
return $object->getStatus();
}
public function applyInternalEffects($object, $value) {
$object->setStatus($value);
}
public function getTitle() {
return pht(
'%s changed the status for this address to %s.',
$this->renderAuthor(),
$this->renderNewValue());
}
}