feat: Add support for headers and authentication headers in webhooks

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
This commit is contained in:
Côme Chilliet 2024-06-03 17:11:52 +02:00 committed by Côme Chilliet
parent 261f08e631
commit 85e0407aad
10 changed files with 93 additions and 18 deletions

View file

@ -11,6 +11,7 @@ return array(
'OCA\\Webhooks\\BackgroundJobs\\WebhookCall' => $baseDir . '/../lib/BackgroundJobs/WebhookCall.php',
'OCA\\Webhooks\\Command\\Index' => $baseDir . '/../lib/Command/Index.php',
'OCA\\Webhooks\\Controller\\WebhooksController' => $baseDir . '/../lib/Controller/WebhooksController.php',
'OCA\\Webhooks\\Db\\AuthMethod' => $baseDir . '/../lib/Db/AuthMethod.php',
'OCA\\Webhooks\\Db\\WebhookListener' => $baseDir . '/../lib/Db/WebhookListener.php',
'OCA\\Webhooks\\Db\\WebhookListenerMapper' => $baseDir . '/../lib/Db/WebhookListenerMapper.php',
'OCA\\Webhooks\\Listener\\WebhooksEventListener' => $baseDir . '/../lib/Listener/WebhooksEventListener.php',

View file

@ -26,6 +26,7 @@ class ComposerStaticInitWebhooks
'OCA\\Webhooks\\BackgroundJobs\\WebhookCall' => __DIR__ . '/..' . '/../lib/BackgroundJobs/WebhookCall.php',
'OCA\\Webhooks\\Command\\Index' => __DIR__ . '/..' . '/../lib/Command/Index.php',
'OCA\\Webhooks\\Controller\\WebhooksController' => __DIR__ . '/..' . '/../lib/Controller/WebhooksController.php',
'OCA\\Webhooks\\Db\\AuthMethod' => __DIR__ . '/..' . '/../lib/Db/AuthMethod.php',
'OCA\\Webhooks\\Db\\WebhookListener' => __DIR__ . '/..' . '/../lib/Db/WebhookListener.php',
'OCA\\Webhooks\\Db\\WebhookListenerMapper' => __DIR__ . '/..' . '/../lib/Db/WebhookListenerMapper.php',
'OCA\\Webhooks\\Listener\\WebhooksEventListener' => __DIR__ . '/..' . '/../lib/Listener/WebhooksEventListener.php',

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace OCA\Webhooks\BackgroundJobs;
use OCA\Webhooks\Db\AuthMethod;
use OCA\Webhooks\Db\WebhookListenerMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\QueuedJob;
@ -31,7 +32,16 @@ class WebhookCall extends QueuedJob {
$client = $this->clientService->newClient();
$options = [];
$options['body'] = json_encode($data);
$options['headers'] = $webhookListener->getHeaders();
try {
switch ($webhookListener->getAuthMethodEnum()) {
case AuthMethod::None:
break;
case AuthMethod::Header:
$authHeaders = $webhookListener->getAuthDataClear();
$options['headers'] = array_merge($options['headers'], $authHeaders);
break;
}
$response = $client->request($webhookListener->getHttpMethod(), $webhookListener->getUri(), $options);
$statusCode = $response->getStatusCode();
if ($statusCode >= 200 && $statusCode < 300) {

View file

@ -31,11 +31,10 @@ class Index extends Base {
protected function execute(InputInterface $input, OutputInterface $output): int {
$webhookListeners = array_map(
function (WebhookListener $listener): array {
$data = $listener->jsonSerialize();
$data['eventFilter'] = json_encode($data['eventFilter']);
return $data;
},
fn (WebhookListener $listener): array => array_map(
fn (string|array|null $value): ?string => (is_array($value) ? json_encode($value) : $value),
$listener->jsonSerialize()
),
$this->mapper->getAll()
);
$this->writeTableInOutputFormat($input, $output, $webhookListeners);

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace OCA\Webhooks\Controller;
use Doctrine\DBAL\Exception;
use OCA\Webhooks\Db\AuthMethod;
use OCA\Webhooks\Db\WebhookListenerMapper;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
@ -112,7 +113,7 @@ class WebhooksController extends OCSController {
$event,
$eventFilter,
$headers,
$authMethod,
AuthMethod::from($authMethod ?? AuthMethod::None->value),
$authData,
);
return new DataResponse($webhookListener);
@ -172,7 +173,7 @@ class WebhooksController extends OCSController {
$event,
$eventFilter,
$headers,
$authMethod,
AuthMethod::from($authMethod ?? AuthMethod::None->value),
$authData,
);
return new DataResponse($webhookListener);

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Webhooks\Db;
enum AuthMethod: string {
case None = 'none';
case Header = 'header';
}

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace OCA\Webhooks\Db;
use OCP\AppFramework\Db\Entity;
use OCP\Security\ICrypto;
/**
* @method void setUserId(string $userId)
@ -34,7 +35,7 @@ class WebhookListener extends Entity implements \JsonSerializable {
/** @var array */
protected $eventFilter;
/** @var ?string */
/** @var ?array */
protected $headers;
/** @var ?string */
@ -43,7 +44,15 @@ class WebhookListener extends Entity implements \JsonSerializable {
/** @var ?string */
protected $authData;
public function __construct() {
private ICrypto $crypto;
public function __construct(
?ICrypto $crypto = null,
) {
if ($crypto === null) {
$crypto = \OCP\Server::get(ICrypto::class);
}
$this->crypto = $crypto;
$this->addType('appId', 'string');
$this->addType('userId', 'string');
$this->addType('httpMethod', 'string');
@ -52,7 +61,26 @@ class WebhookListener extends Entity implements \JsonSerializable {
$this->addType('eventFilter', 'json');
$this->addType('headers', 'json');
$this->addType('authMethod', 'string');
$this->addType('authData', 'json');
$this->addType('authData', 'string');
}
public function getAuthMethodEnum(): AuthMethod {
return AuthMethod::from(parent::getAuthMethod());
}
public function getAuthDataClear(): array {
if ($this->authData === null) {
return [];
}
return json_decode($this->crypto->decrypt($this->getAuthData()), associative:true, flags:JSON_THROW_ON_ERROR);
}
public function setAuthDataClear(?array $data): void {
if ($data === null) {
$this->setAuthData(null);
return;
}
$this->setAuthData($this->crypto->encrypt(json_encode($data)));
}
public function jsonSerialize(): array {

View file

@ -64,7 +64,7 @@ class WebhookListenerMapper extends QBMapper {
string $event,
?array $eventFilter,
?array $headers,
?string $authMethod,
AuthMethod $authMethod,
?array $authData,
) {
$webhookListener = WebhookListener::fromParams(
@ -76,10 +76,10 @@ class WebhookListenerMapper extends QBMapper {
'event' => $event,
'eventFilter' => $eventFilter ?? [],
'headers' => $headers,
'authMethod' => $authMethod ?? 'none',
'authData' => $authData,
'authMethod' => $authMethod->value,
]
);
$webhookListener->setAuthDataClear($authData);
return $this->insert($webhookListener);
}
@ -92,7 +92,7 @@ class WebhookListenerMapper extends QBMapper {
string $event,
?array $eventFilter,
?array $headers,
?string $authMethod,
AuthMethod $authMethod,
?array $authData,
) {
$webhookListener = WebhookListener::fromParams(
@ -105,10 +105,10 @@ class WebhookListenerMapper extends QBMapper {
'event' => $event,
'eventFilter' => $eventFilter ?? [],
'headers' => $headers,
'authMethod' => $authMethod,
'authData' => $authData,
'authMethod' => $authMethod->value,
]
);
$webhookListener->setAuthDataClear($authData);
return $this->update($webhookListener);
}

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace OCA\Webhooks\Tests\Db;
use OCA\Webhooks\Db\AuthMethod;
use OCA\Webhooks\Db\WebhookListenerMapper;
use OCP\IDBConnection;
use OCP\User\Events\UserCreatedEvent;
@ -51,7 +52,7 @@ class WebhookListenerMapperTest extends TestCase {
UserCreatedEvent::class,
null,
null,
null,
AuthMethod::None,
null,
);
@ -60,4 +61,23 @@ class WebhookListenerMapperTest extends TestCase {
$listener1->resetUpdatedFields();
$this->assertEquals($listener1, $listener2);
}
public function testInsertListenerAndGetItWithAuthData() {
$listener1 = $this->mapper->addWebhookListener(
null,
'bob',
'POST',
'https://webhook.example.com/endpoint',
UserCreatedEvent::class,
null,
null,
AuthMethod::Header,
['secretHeader' => 'header'],
);
$listener2 = $this->mapper->getById($listener1->getId());
$listener1->resetUpdatedFields();
$this->assertEquals($listener1, $listener2);
}
}

View file

@ -7,7 +7,7 @@ declare(strict_types=1);
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Webhooks\Tests\Db;
namespace OCA\Webhooks\Tests\Service;
use OCA\Webhooks\Service\PHPMongoQuery;
use OCP\Files\Events\Node\NodeWrittenEvent;