Skip to content

Commit

Permalink
REST endpoint for executing jobs
Browse files Browse the repository at this point in the history
Bug: T244826
Depends-On: If4f67a6fa0e26ade3fc0420e62fa836c2a3e4b2e
Depends-On: I197366ef6f490bb7676c21d99568e4ffd229673b
Change-Id: Ibd136521f963a71a8a21c861f0294f2b3b7d35eb
  • Loading branch information
clarakosi committed Feb 24, 2020
1 parent 7502e87 commit f19769a
Show file tree
Hide file tree
Showing 14 changed files with 1,355 additions and 194 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -8,3 +8,4 @@
.idea/
.vscode/
.eslintcache
.api-testing.config.json
2 changes: 2 additions & 0 deletions .mocharc.yaml
@@ -0,0 +1,2 @@
recursive: true
timeout: 0
17 changes: 0 additions & 17 deletions EventBus.notranslate-alias.php

This file was deleted.

3 changes: 0 additions & 3 deletions composer.json
@@ -1,7 +1,4 @@
{
"require": {
"firebase/php-jwt": "5.0.0"
},
"require-dev": {
"jakub-onderka/php-parallel-lint": "1.0.0",
"mediawiki/mediawiki-codesniffer": "29.0.0",
Expand Down
24 changes: 16 additions & 8 deletions extension.json
Expand Up @@ -23,7 +23,8 @@
"url": "http://localhost:8192/v1/events",
"timeout": 5
}
}
},
"EventBusEnableRunJobAPI": true
},
"AutoloadClasses": {
"EventBus": "includes/EventBus.php",
Expand All @@ -34,16 +35,14 @@
"EventBusRCFeedFormatter": "includes/adapters/rcfeed/EventBusRCFeedFormatter.php",
"JobQueueEventBus": "includes/JobQueueEventBus.php",
"JobExecutor": "includes/JobExecutor.php",
"SpecialRunSingleJob": "includes/SpecialRunSingleJob.php"
"MediaWiki\\Extension\\EventBus\\EventBodyValidator": "includes/Rest/EventBodyValidator.php",
"MediaWiki\\Extension\\EventBus\\RunSingleJobHandler": "includes/Rest/RunSingleJobHandler.php"
},
"MessagesDirs": {
"EventBus": [
"i18n"
]
},
"ExtensionMessagesFiles": {
"EventBusAliasNoTranslate": "EventBus.notranslate-alias.php"
},
"Hooks": {
"ArticleDeleteComplete": "EventBusHooks::onArticleDeleteComplete",
"ArticleUndelete": "EventBusHooks::onArticleUndelete",
Expand All @@ -58,9 +57,18 @@
"ChangeTagsAfterUpdateTags": "EventBusHooks::onChangeTagsAfterUpdateTags",
"CentralNoticeCampaignChange": "EventBusHooks::onCentralNoticeCampaignChange"
},
"SpecialPages": {
"RunSingleJob": "SpecialRunSingleJob"
},
"RestRoutes": [
{
"path": "/eventbus/v0/internal/job/execute",
"method": "POST",
"class": "MediaWiki\\Extension\\EventBus\\RunSingleJobHandler",
"services": [
"ReadOnlyMode",
"MainConfig",
"JobRunner"
]
}
],
"manifest_version": 1,
"load_composer_autoloader": true
}
13 changes: 11 additions & 2 deletions includes/EventFactory.php
@@ -1,6 +1,5 @@
<?php

use Firebase\JWT\JWT;
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
Expand Down Expand Up @@ -325,10 +324,20 @@ private static function signEvent( &$event ) {
}

$signingSecret = MediaWikiServices::getInstance()->getMainConfig()->get( 'SecretKey' );
$signature = hash( 'sha256', JWT::sign( $serialized_event, $signingSecret ) );
$signature = self::getEventSignature( $serialized_event, $signingSecret );

$event['mediawiki_signature'] = $signature;
}

/**
* @param string $serialized_event
* @param string $secretKey
* @return string
*/
public static function getEventSignature( $serialized_event, $secretKey ) {
return hash_hmac( 'sha1', $serialized_event, $secretKey );
}

/**
* Create a page delete event message
* @param string $stream the stream to send an event to
Expand Down
26 changes: 13 additions & 13 deletions includes/JobExecutor.php
Expand Up @@ -39,18 +39,18 @@ public function execute( $jobEvent ) {

if ( !$jobCreateResult['status'] ) {
$this->logger()->error( 'Failed creating job from description', [
'job_type' => $jobEvent['type'],
'message' => $jobCreateResult['message']
] );
'job_type' => $jobEvent['type'],
'message' => $jobCreateResult['message']
] );
$jobCreateResult['readonly'] = false;
return $jobCreateResult;
}

$job = $jobCreateResult['job'];
$this->logger()->debug( 'Beginning job execution', [
'job' => $job->toString(),
'job_type' => $job->getType()
] );
'job' => $job->toString(),
'job_type' => $job->getType()
] );

$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();

Expand All @@ -75,9 +75,9 @@ public function execute( $jobEvent ) {
if ( $status === false ) {
$message = $job->getLastError();
$this->logger()->error( 'Failed executing job: ' . $job->toString(), [
'job_type' => $job->getType(),
'error' => $message
] );
'job_type' => $job->getType(),
'error' => $message
] );
} elseif ( !is_bool( $status ) ) {
$message = 'Success, but no status returned';
$this->logger()->warning( 'Non-boolean result returned by job: ' . $job->toString(),
Expand All @@ -103,8 +103,8 @@ public function execute( $jobEvent ) {
MWExceptionHandler::rollbackMasterChangesAndLog( $e );
$status = false;
$message = 'Exception executing job: '
. $job->toString() . ' : '
. get_class( $e ) . ': ' . $e->getMessage();
. $job->toString() . ' : '
. get_class( $e ) . ': ' . $e->getMessage();
$this->logger()->error( $message,
[
'job_type' => $job->getType(),
Expand All @@ -118,8 +118,8 @@ public function execute( $jobEvent ) {
$job->teardown( $status );
} catch ( Exception $e ) {
$message = 'Exception tearing down job: '
. $job->toString() . ' : '
. get_class( $e ) . ': ' . $e->getMessage();
. $job->toString() . ' : '
. get_class( $e ) . ': ' . $e->getMessage();
$this->logger()->error( $message,
[
'job_type' => $job->getType(),
Expand Down
150 changes: 150 additions & 0 deletions includes/Rest/EventBodyValidator.php
@@ -0,0 +1,150 @@
<?php

namespace MediaWiki\Extension\EventBus;

use EventBus;
use EventFactory;
use Exception;
use Job;
use MediaWiki\Rest\HttpException;
use MediaWiki\Rest\RequestInterface;
use MediaWiki\Rest\Validator\BodyValidator;
use Psr\Log\LoggerInterface;

/**
* Class EventBodyValidator
*
* Validates the body
*/
class EventBodyValidator implements BodyValidator {

/**
* @var string
*/
private $secretKey;

/**
* @var LoggerInterface
*/
private $logger;

public function __construct( $secretKey, LoggerInterface $logger ) {
$this->secretKey = $secretKey;
$this->logger = $logger;
}

/**
* @param RequestInterface $request
* @return Job|mixed|void
* @throws HttpException
*/
public function validateBody( RequestInterface $request ) {
// get the info contained in the body
$event = null;
try {
$event = json_decode( $request->getBody()->getContents(), true );
} catch ( Exception $e ) {
throw new HttpException( "Could not decode the event", 500, [
'error' => $e->getMessage(),
] );
}

// check that we have the needed components of the event
if ( !isset( $event['database'] ) ||
!isset( $event['type'] ) ||
!isset( $event['params'] )
) {
$missingParams = [];
if ( !isset( $event['database'] ) ) { $missingParams[] = 'database';
}
if ( !isset( $event['type'] ) ) { $missingParams[] = 'type';
}
if ( !isset( $event['params'] ) ) { $missingParams[] = 'params';
}
throw new HttpException( 'Invalid event received', 400, [ 'missing_params' => $missingParams ] );
}

if ( !isset( $event['mediawiki_signature'] ) ) {
throw new HttpException( 'Missing mediawiki signature', 403 );
}

$signature = $event['mediawiki_signature'];
unset( $event['mediawiki_signature'] );

$serialized_event = EventBus::serializeEvents( $event );
$expected_signature = EventFactory::getEventSignature(
$serialized_event,
$this->secretKey
);

$verified = is_string( $signature )
&& hash_equals( $expected_signature, $signature );

if ( !$verified ) {
throw new HttpException( 'Invalid mediawiki signature', 403 );
}

// check if there are any base64-encoded parameters and if so decode them
foreach ( $event['params'] as $key => &$value ) {
if ( !is_string( $value ) ) {
continue;
}
if ( preg_match( '/^data:application\/octet-stream;base64,([\s\S]+)$/', $value, $match ) ) {
$value = base64_decode( $match[1], true );
if ( $value === false ) {

throw new HttpException(
'Internal Server Error',
500,
"base64_decode() failed for parameter {$key} ({$match[1]})" );
}
}
}
unset( $value );

return $this->getJobFromParams( $event );
}

/**
* @param array $jobEvent containing the job EventBus event
* @return Job|void
* @throws HttpException
*/
private function getJobFromParams( array $jobEvent ) {
try {
$job = Job::factory( $jobEvent['type'], $jobEvent['params'] );
} catch ( Exception $e ) {
return $this->throwJobErrors( [
'status' => false,
'error' => $e->getMessage(),
'type' => $jobEvent['type']
] );
}

if ( $job === null ) {
return $this->throwJobErrors( [
'status' => false,
'error' => 'Could not create a job from event',
'type' => $jobEvent['type']
] );
}

return $job;
}

/**
* @param array $jobResults
* @throws HttpException
*/
private function throwJobErrors( $jobResults ) {
$this->logger->error( 'Failed creating job from description', [
'job_type' => $jobResults['type'],
'error' => $jobResults['error']
] );

throw new HttpException( "Failed creating job from description",
400,
[ 'error' => $jobResults['error'] ]
);
}
}

0 comments on commit f19769a

Please sign in to comment.