1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-02-02 09:58:24 +01:00

Refine core webhook implementation somewhat

Summary:
Depends on D19045. Ref T11330.

  - View/regenerate HMAC keys.
  - Pretty JSON.
  - Readable status transactions.
  - test, silent, secure flags.
  - Dates on request view.
  - More icons.
  - Can test any object.
  - GC for requests.

Test Plan: Went through each feature poking at it in the web UI and with `bin/webhook call ...` / `bin/garbage collect ...`.

Subscribers: ftdysa

Maniphest Tasks: T11330

Differential Revision: https://secure.phabricator.com/D19046
This commit is contained in:
epriestley 2018-02-09 10:45:18 -08:00
parent 0470125d9e
commit dc2995c4ca
13 changed files with 355 additions and 31 deletions

View file

@ -1447,12 +1447,14 @@ phutil_register_library_map(array(
'HeraldWebhookEditController' => 'applications/herald/controller/HeraldWebhookEditController.php', 'HeraldWebhookEditController' => 'applications/herald/controller/HeraldWebhookEditController.php',
'HeraldWebhookEditEngine' => 'applications/herald/editor/HeraldWebhookEditEngine.php', 'HeraldWebhookEditEngine' => 'applications/herald/editor/HeraldWebhookEditEngine.php',
'HeraldWebhookEditor' => 'applications/herald/editor/HeraldWebhookEditor.php', 'HeraldWebhookEditor' => 'applications/herald/editor/HeraldWebhookEditor.php',
'HeraldWebhookKeyController' => 'applications/herald/controller/HeraldWebhookKeyController.php',
'HeraldWebhookListController' => 'applications/herald/controller/HeraldWebhookListController.php', 'HeraldWebhookListController' => 'applications/herald/controller/HeraldWebhookListController.php',
'HeraldWebhookManagementWorkflow' => 'applications/herald/management/HeraldWebhookManagementWorkflow.php', 'HeraldWebhookManagementWorkflow' => 'applications/herald/management/HeraldWebhookManagementWorkflow.php',
'HeraldWebhookNameTransaction' => 'applications/herald/xaction/HeraldWebhookNameTransaction.php', 'HeraldWebhookNameTransaction' => 'applications/herald/xaction/HeraldWebhookNameTransaction.php',
'HeraldWebhookPHIDType' => 'applications/herald/phid/HeraldWebhookPHIDType.php', 'HeraldWebhookPHIDType' => 'applications/herald/phid/HeraldWebhookPHIDType.php',
'HeraldWebhookQuery' => 'applications/herald/query/HeraldWebhookQuery.php', 'HeraldWebhookQuery' => 'applications/herald/query/HeraldWebhookQuery.php',
'HeraldWebhookRequest' => 'applications/herald/storage/HeraldWebhookRequest.php', 'HeraldWebhookRequest' => 'applications/herald/storage/HeraldWebhookRequest.php',
'HeraldWebhookRequestGarbageCollector' => 'applications/herald/garbagecollector/HeraldWebhookRequestGarbageCollector.php',
'HeraldWebhookRequestListView' => 'applications/herald/view/HeraldWebhookRequestListView.php', 'HeraldWebhookRequestListView' => 'applications/herald/view/HeraldWebhookRequestListView.php',
'HeraldWebhookRequestPHIDType' => 'applications/herald/phid/HeraldWebhookRequestPHIDType.php', 'HeraldWebhookRequestPHIDType' => 'applications/herald/phid/HeraldWebhookRequestPHIDType.php',
'HeraldWebhookRequestQuery' => 'applications/herald/query/HeraldWebhookRequestQuery.php', 'HeraldWebhookRequestQuery' => 'applications/herald/query/HeraldWebhookRequestQuery.php',
@ -6735,12 +6737,14 @@ phutil_register_library_map(array(
'PhabricatorPolicyInterface', 'PhabricatorPolicyInterface',
'PhabricatorApplicationTransactionInterface', 'PhabricatorApplicationTransactionInterface',
'PhabricatorDestructibleInterface', 'PhabricatorDestructibleInterface',
'PhabricatorProjectInterface',
), ),
'HeraldWebhookCallManagementWorkflow' => 'HeraldWebhookManagementWorkflow', 'HeraldWebhookCallManagementWorkflow' => 'HeraldWebhookManagementWorkflow',
'HeraldWebhookController' => 'HeraldController', 'HeraldWebhookController' => 'HeraldController',
'HeraldWebhookEditController' => 'HeraldWebhookController', 'HeraldWebhookEditController' => 'HeraldWebhookController',
'HeraldWebhookEditEngine' => 'PhabricatorEditEngine', 'HeraldWebhookEditEngine' => 'PhabricatorEditEngine',
'HeraldWebhookEditor' => 'PhabricatorApplicationTransactionEditor', 'HeraldWebhookEditor' => 'PhabricatorApplicationTransactionEditor',
'HeraldWebhookKeyController' => 'HeraldWebhookController',
'HeraldWebhookListController' => 'HeraldWebhookController', 'HeraldWebhookListController' => 'HeraldWebhookController',
'HeraldWebhookManagementWorkflow' => 'PhabricatorManagementWorkflow', 'HeraldWebhookManagementWorkflow' => 'PhabricatorManagementWorkflow',
'HeraldWebhookNameTransaction' => 'HeraldWebhookTransactionType', 'HeraldWebhookNameTransaction' => 'HeraldWebhookTransactionType',
@ -6751,6 +6755,7 @@ phutil_register_library_map(array(
'PhabricatorPolicyInterface', 'PhabricatorPolicyInterface',
'PhabricatorExtendedPolicyInterface', 'PhabricatorExtendedPolicyInterface',
), ),
'HeraldWebhookRequestGarbageCollector' => 'PhabricatorGarbageCollector',
'HeraldWebhookRequestListView' => 'AphrontView', 'HeraldWebhookRequestListView' => 'AphrontView',
'HeraldWebhookRequestPHIDType' => 'PhabricatorPHIDType', 'HeraldWebhookRequestPHIDType' => 'PhabricatorPHIDType',
'HeraldWebhookRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HeraldWebhookRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',

View file

@ -68,10 +68,8 @@ final class PhabricatorHeraldApplication extends PhabricatorApplication {
'HeraldWebhookViewController', 'HeraldWebhookViewController',
$this->getEditRoutePattern('edit/') => 'HeraldWebhookEditController', $this->getEditRoutePattern('edit/') => 'HeraldWebhookEditController',
'test/(?P<id>\d+)/' => 'HeraldWebhookTestController', 'test/(?P<id>\d+)/' => 'HeraldWebhookTestController',
'key/' => array( 'key/(?P<action>view|cycle)/(?P<id>\d+)/' =>
'view/(?P<id>\d+)/' => 'HeraldWebhookViewKeyController', 'HeraldWebhookKeyController',
'cycle/(?P<id>\d+)/' => 'HeraldWebhookCycleKeyController',
),
), ),
), ),
); );

View file

@ -0,0 +1,56 @@
<?php
final class HeraldWebhookKeyController
extends HeraldWebhookController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$hook = id(new HeraldWebhookQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$hook) {
return new Aphront404Response();
}
$action = $request->getURIData('action');
if ($action === 'cycle') {
if (!$request->isFormPost()) {
return $this->newDialog()
->setTitle(pht('Regenerate HMAC Key'))
->appendParagraph(
pht(
'Regenerate the HMAC key used to sign requests made by this '.
'webhook?'))
->appendParagraph(
pht(
'Requests which are currently authenticated with the old key '.
'may fail.'))
->addCancelButton($hook->getURI())
->addSubmitButton(pht('Regnerate Key'));
} else {
$hook->regenerateHMACKey()->save();
}
}
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormTextControl())
->setLabel(pht('HMAC Key'))
->setValue($hook->getHMACKey()));
return $this->newDialog()
->setTitle(pht('Webhook HMAC Key'))
->appendForm($form)
->addCancelButton($hook->getURI(), pht('Done'));
}
}

View file

@ -19,11 +19,41 @@ final class HeraldWebhookTestController
return new Aphront404Response(); return new Aphront404Response();
} }
$v_object = null;
$e_object = null;
$errors = array();
if ($request->isFormPost()) { if ($request->isFormPost()) {
$v_object = $request->getStr('object');
if (!strlen($v_object)) {
$object = $hook; $object = $hook;
} else {
$objects = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames(array($v_object))
->execute();
if ($objects) {
$object = head($objects);
} else {
$e_object = pht('Invalid');
$errors[] = pht('Specified object could not be loaded.');
}
}
if (!$errors) {
$xaction_query =
PhabricatorApplicationTransactionQuery::newQueryForObject($object);
$xactions = $xaction_query
->withObjectPHIDs(array($object->getPHID()))
->setViewer($viewer)
->setLimit(10)
->execute();
$request = HeraldWebhookRequest::initializeNewWebhookRequest($hook) $request = HeraldWebhookRequest::initializeNewWebhookRequest($hook)
->setObjectPHID($object->getPHID()) ->setObjectPHID($object->getPHID())
->setIsTestAction(true)
->setTransactionPHIDs(mpull($xactions, 'getPHID'))
->save(); ->save();
$request->queueCall(); $request->queueCall();
@ -32,13 +62,31 @@ final class HeraldWebhookTestController
return id(new AphrontRedirectResponse())->setURI($next_uri); return id(new AphrontRedirectResponse())->setURI($next_uri);
} }
}
$instructions = <<<EOREMARKUP
Optionally, choose an object to generate test data for (like `D123` or `T234`).
The 10 most recent transactions for the object will be submitted to the webhook.
EOREMARKUP;
$form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormTextControl())
->setLabel(pht('Object'))
->setName('object')
->setError($e_object)
->setValue($v_object));
return $this->newDialog() return $this->newDialog()
->setErrors($errors)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setTitle(pht('New Test Request')) ->setTitle(pht('New Test Request'))
->appendParagraph( ->appendParagraph(new PHUIRemarkupView($viewer, $instructions))
pht('This will make a new test request to the configured URI.')) ->appendForm($form)
->addCancelButton($hook->getURI()) ->addCancelButton($hook->getURI())
->addSubmitButton(pht('Make Request')); ->addSubmitButton(pht('Test Webhook'));
} }

View file

@ -94,10 +94,15 @@ final class HeraldWebhookViewController
$title = $hook->getName(); $title = $hook->getName();
$status_icon = $hook->getStatusIcon();
$status_color = $hook->getStatusColor();
$status_name = $hook->getStatusDisplayName();
$header = id(new PHUIHeaderView()) $header = id(new PHUIHeaderView())
->setHeader($title) ->setHeader($title)
->setViewer($viewer) ->setViewer($viewer)
->setPolicyObject($hook) ->setPolicyObject($hook)
->setStatus($status_icon, $status_color, $status_name)
->setHeaderIcon('fa-cloud-upload'); ->setHeaderIcon('fa-cloud-upload');
return $header; return $header;
@ -117,6 +122,9 @@ final class HeraldWebhookViewController
$edit_uri = $this->getApplicationURI("webhook/edit/{$id}/"); $edit_uri = $this->getApplicationURI("webhook/edit/{$id}/");
$test_uri = $this->getApplicationURI("webhook/test/{$id}/"); $test_uri = $this->getApplicationURI("webhook/test/{$id}/");
$key_view_uri = $this->getApplicationURI("webhook/key/view/{$id}/");
$key_cycle_uri = $this->getApplicationURI("webhook/key/cycle/{$id}/");
$curtain->addAction( $curtain->addAction(
id(new PhabricatorActionView()) id(new PhabricatorActionView())
->setName(pht('Edit Webhook')) ->setName(pht('Edit Webhook'))
@ -133,6 +141,22 @@ final class HeraldWebhookViewController
->setWorkflow(true) ->setWorkflow(true)
->setHref($test_uri)); ->setHref($test_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('View HMAC Key'))
->setIcon('fa-key')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($key_view_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Regenerate HMAC Key'))
->setIcon('fa-refresh')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($key_cycle_uri));
return $curtain; return $curtain;
} }

View file

@ -0,0 +1,29 @@
<?php
final class HeraldWebhookRequestGarbageCollector
extends PhabricatorGarbageCollector {
const COLLECTORCONST = 'herald.webhooks';
public function getCollectorName() {
return pht('Herald Webhooks');
}
public function getDefaultRetentionPolicy() {
return phutil_units('7 days in seconds');
}
protected function collectGarbage() {
$table = new HeraldWebhookRequest();
$conn_w = $table->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE dateCreated < %d LIMIT 100',
$table->getTableName(),
$this->getGarbageEpoch());
return ($conn_w->getAffectedRows() == 100);
}
}

View file

@ -6,7 +6,7 @@ final class HeraldWebhookCallManagementWorkflow
protected function didConstruct() { protected function didConstruct() {
$this $this
->setName('call') ->setName('call')
->setExamples('**call** --id __id__') ->setExamples('**call** --id __id__ [--object __object__]')
->setSynopsis(pht('Call a webhook.')) ->setSynopsis(pht('Call a webhook.'))
->setArguments( ->setArguments(
array( array(
@ -15,6 +15,19 @@ final class HeraldWebhookCallManagementWorkflow
'param' => 'id', 'param' => 'id',
'help' => pht('Webhook ID to call'), 'help' => pht('Webhook ID to call'),
), ),
array(
'name' => 'object',
'param' => 'object',
'help' => pht('Submit transactions for a particular object.'),
),
array(
'name' => 'silent',
'help' => pht('Set the "silent" flag on the request.'),
),
array(
'name' => 'secure',
'help' => pht('Set the "secure" flag on the request.'),
),
)); ));
} }
@ -39,12 +52,38 @@ final class HeraldWebhookCallManagementWorkflow
$id)); $id));
} }
$object_name = $args->getArg('object');
if ($object_name === null) {
$object = $hook; $object = $hook;
} else {
$objects = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withNames(array($object_name))
->execute();
if (!$objects) {
throw new PhutilArgumentUsageException(
pht(
'Unable to load specified object ("%s").',
$object_name));
}
$object = head($objects);
}
$application_phid = id(new PhabricatorHeraldApplication())->getPHID(); $xaction_query =
PhabricatorApplicationTransactionQuery::newQueryForObject($object);
$xactions = $xaction_query
->withObjectPHIDs(array($object->getPHID()))
->setViewer($viewer)
->setLimit(10)
->execute();
$request = HeraldWebhookRequest::initializeNewWebhookRequest($hook) $request = HeraldWebhookRequest::initializeNewWebhookRequest($hook)
->setObjectPHID($object->getPHID()) ->setObjectPHID($object->getPHID())
->setIsTestAction(true)
->setIsSilentAction((bool)$args->getArg('silent'))
->setIsSecureAction((bool)$args->getArg('secure'))
->setTransactionPHIDs(mpull($xactions, 'getPHID'))
->save(); ->save();
PhabricatorWorker::setRunAllTasksInProcess(true); PhabricatorWorker::setRunAllTasksInProcess(true);

View file

@ -80,9 +80,16 @@ final class HeraldWebhookSearchEngine
->setViewer($viewer); ->setViewer($viewer);
foreach ($hooks as $hook) { foreach ($hooks as $hook) {
$item = id(new PHUIObjectItemView()) $item = id(new PHUIObjectItemView())
->setObjectName(pht('Hook %d', $hook->getID())) ->setObjectName(pht('Webhook %d', $hook->getID()))
->setHeader($hook->getName()) ->setHeader($hook->getName())
->setHref($hook->getURI()); ->setHref($hook->getURI())
->addAttribute($hook->getWebhookURI());
$item->addIcon($hook->getStatusIcon(), $hook->getStatusDisplayName());
if ($hook->isDisabled()) {
$item->setDisabled(true);
}
$list->addItem($item); $list->addItem($item);
} }

View file

@ -5,7 +5,8 @@ final class HeraldWebhook
implements implements
PhabricatorPolicyInterface, PhabricatorPolicyInterface,
PhabricatorApplicationTransactionInterface, PhabricatorApplicationTransactionInterface,
PhabricatorDestructibleInterface { PhabricatorDestructibleInterface,
PhabricatorProjectInterface {
protected $name; protected $name;
protected $webhookURI; protected $webhookURI;
@ -44,7 +45,7 @@ final class HeraldWebhook
->setStatus(self::HOOKSTATUS_ENABLED) ->setStatus(self::HOOKSTATUS_ENABLED)
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setEditPolicy($viewer->getPHID()) ->setEditPolicy($viewer->getPHID())
->setHmacKey(Filesystem::readRandomCharacters(32)); ->regenerateHMACKey();
} }
public function getURI() { public function getURI() {
@ -56,16 +57,79 @@ final class HeraldWebhook
} }
public static function getStatusDisplayNameMap() { public static function getStatusDisplayNameMap() {
return array( $specs = self::getStatusSpecifications();
self::HOOKSTATUS_FIREHOSE => pht('Firehose'), return ipull($specs, 'name', 'key');
self::HOOKSTATUS_ENABLED => pht('Enabled'), }
self::HOOKSTATUS_DISABLED => pht('Disabled'),
private static function getStatusSpecifications() {
$specs = array(
array(
'key' => self::HOOKSTATUS_FIREHOSE,
'name' => pht('Firehose'),
'color' => 'orange',
'icon' => 'fa-star-o',
),
array(
'key' => self::HOOKSTATUS_ENABLED,
'name' => pht('Enabled'),
'color' => 'bluegrey',
'icon' => 'fa-check',
),
array(
'key' => self::HOOKSTATUS_DISABLED,
'name' => pht('Disabled'),
'color' => 'dark',
'icon' => 'fa-ban',
),
); );
return ipull($specs, null, 'key');
}
private static function getSpecificationForStatus($status) {
$specs = self::getStatusSpecifications();
if (isset($specs[$status])) {
return $specs[$status];
}
return array(
'key' => $status,
'name' => pht('Unknown ("%s")', $status),
'icon' => 'fa-question',
'color' => 'indigo',
);
}
public static function getDisplayNameForStatus($status) {
$spec = self::getSpecificationForStatus($status);
return $spec['name'];
}
public static function getIconForStatus($status) {
$spec = self::getSpecificationForStatus($status);
return $spec['icon'];
}
public static function getColorForStatus($status) {
$spec = self::getSpecificationForStatus($status);
return $spec['color'];
} }
public function getStatusDisplayName() { public function getStatusDisplayName() {
$status = $this->getStatus(); $status = $this->getStatus();
return idx($this->getStatusDisplayNameMap(), $status); return self::getDisplayNameForStatus($status);
}
public function getStatusIcon() {
$status = $this->getStatus();
return self::getIconForStatus($status);
}
public function getStatusColor() {
$status = $this->getStatus();
return self::getColorForStatus($status);
} }
public function getErrorBackoffWindow() { public function getErrorBackoffWindow() {
@ -101,6 +165,9 @@ final class HeraldWebhook
return false; return false;
} }
public function regenerateHMACKey() {
return $this->setHMACKey(Filesystem::readRandomCharacters(32));
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */ /* -( PhabricatorPolicyInterface )----------------------------------------- */

View file

@ -116,6 +116,30 @@ final class HeraldWebhookRequest
return $this->getProperty('transactionPHIDs', array()); return $this->getProperty('transactionPHIDs', array());
} }
public function setIsSilentAction($bool) {
return $this->setProperty('silent', $bool);
}
public function getIsSilentAction() {
return $this->getProperty('silent', false);
}
public function setIsTestAction($bool) {
return $this->setProperty('test', $bool);
}
public function getIsTestAction() {
return $this->getProperty('test', false);
}
public function setIsSecureAction($bool) {
return $this->setProperty('secure', $bool);
}
public function getIsSecureAction() {
return $this->getProperty('secure', false);
}
public function queueCall() { public function queueCall() {
PhabricatorWorker::scheduleTask( PhabricatorWorker::scheduleTask(
'HeraldWebhookWorker', 'HeraldWebhookWorker',

View file

@ -44,12 +44,20 @@ final class HeraldWebhookRequestListView
$rowc[] = null; $rowc[] = null;
} }
$last_epoch = $request->getLastRequestEpoch();
if ($request->getLastRequestEpoch()) {
$last_request = phabricator_datetime($last_epoch, $viewer);
} else {
$last_request = null;
}
$rows[] = array( $rows[] = array(
$request->getID(), $request->getID(),
$icon, $icon,
$handles[$request->getObjectPHID()]->renderLink(), $handles[$request->getObjectPHID()]->renderLink(),
$request->getErrorType(), $request->getErrorType(),
$request->getErrorCode(), $request->getErrorCode(),
$last_request,
); );
} }
@ -62,6 +70,7 @@ final class HeraldWebhookRequestListView
pht('Object'), pht('Object'),
pht('Type'), pht('Type'),
pht('Code'), pht('Code'),
pht('Requested At'),
)) ))
->setColumnClasses( ->setColumnClasses(
array( array(
@ -70,6 +79,7 @@ final class HeraldWebhookRequestListView
'wide', 'wide',
'', '',
'', '',
'',
)); ));
return $table; return $table;

View file

@ -143,10 +143,15 @@ final class HeraldWebhookWorker
'object' => array( 'object' => array(
'phid' => $object->getPHID(), 'phid' => $object->getPHID(),
), ),
'action' => array(
'test' => $request->getIsTestAction(),
'silent' => $request->getIsSilentAction(),
'secure' => $request->getIsSecureAction(),
),
'transactions' => $xaction_data, 'transactions' => $xaction_data,
); );
$payload = phutil_json_encode($payload); $payload = id(new PhutilJSON())->encodeFormatted($payload);
$key = $hook->getHmacKey(); $key = $hook->getHmacKey();
$signature = PhabricatorHash::digestHMACSHA256($payload, $key); $signature = PhabricatorHash::digestHMACSHA256($payload, $key);
$uri = $hook->getWebhookURI(); $uri = $hook->getWebhookURI();

View file

@ -14,20 +14,32 @@ final class HeraldWebhookStatusTransaction
} }
public function getTitle() { public function getTitle() {
$old_value = $this->getOldValue();
$new_value = $this->getNewValue();
$old_status = HeraldWebhook::getDisplayNameForStatus($old_value);
$new_status = HeraldWebhook::getDisplayNameForStatus($new_value);
return pht( return pht(
'%s changed hook status from %s to %s.', '%s changed hook status from %s to %s.',
$this->renderAuthor(), $this->renderAuthor(),
$this->renderOldValue(), $this->renderValue($old_status),
$this->renderNewValue()); $this->renderValue($new_status));
} }
public function getTitleForFeed() { public function getTitleForFeed() {
$old_value = $this->getOldValue();
$new_value = $this->getNewValue();
$old_status = HeraldWebhook::getDisplayNameForStatus($old_value);
$new_status = HeraldWebhook::getDisplayNameForStatus($new_value);
return pht( return pht(
'%s changed %s from %s to %s.', '%s changed %s from %s to %s.',
$this->renderAuthor(), $this->renderAuthor(),
$this->renderObject(), $this->renderObject(),
$this->renderOldValue(), $this->renderValue($old_status),
$this->renderNewValue()); $this->renderValue($new_status));
} }
public function validateTransactions($object, array $xactions) { public function validateTransactions($object, array $xactions) {