Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/webhook_listeners/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Administrators can configure webhook listeners via the app's OCS API. The app al
]]>
</description>

<version>1.4.1</version>
<version>1.5.0</version>
<licence>agpl</licence>
<author>Côme Chilliet</author>
<namespace>WebhookListeners</namespace>
Expand Down Expand Up @@ -49,4 +49,8 @@ Administrators can configure webhook listeners via the app's OCS API. The app al
<admin-delegation>OCA\WebhookListeners\Settings\Admin</admin-delegation>
<admin-delegation-section>OCA\WebhookListeners\Settings\AdminSection</admin-delegation-section>
</settings>

<background-jobs>
<job>OCA\WebhookListeners\BackgroundJobs\WebhookTokenCleanup</job>
</background-jobs>
</info>
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'OCA\\WebhookListeners\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
'OCA\\WebhookListeners\\BackgroundJobs\\WebhookCall' => $baseDir . '/../lib/BackgroundJobs/WebhookCall.php',
'OCA\\WebhookListeners\\BackgroundJobs\\WebhookTokenCleanup' => $baseDir . '/../lib/BackgroundJobs/WebhookTokenCleanup.php',
'OCA\\WebhookListeners\\Command\\ListWebhooks' => $baseDir . '/../lib/Command/ListWebhooks.php',
'OCA\\WebhookListeners\\Controller\\WebhooksController' => $baseDir . '/../lib/Controller/WebhooksController.php',
'OCA\\WebhookListeners\\Db\\AuthMethod' => $baseDir . '/../lib/Db/AuthMethod.php',
'OCA\\WebhookListeners\\Db\\EphemeralToken' => $baseDir . '/../lib/Db/EphemeralToken.php',
'OCA\\WebhookListeners\\Db\\EphemeralTokenMapper' => $baseDir . '/../lib/Db/EphemeralTokenMapper.php',
'OCA\\WebhookListeners\\Db\\WebhookListener' => $baseDir . '/../lib/Db/WebhookListener.php',
'OCA\\WebhookListeners\\Db\\WebhookListenerMapper' => $baseDir . '/../lib/Db/WebhookListenerMapper.php',
'OCA\\WebhookListeners\\Listener\\WebhooksEventListener' => $baseDir . '/../lib/Listener/WebhooksEventListener.php',
'OCA\\WebhookListeners\\Migration\\Version1000Date20240527153425' => $baseDir . '/../lib/Migration/Version1000Date20240527153425.php',
'OCA\\WebhookListeners\\Migration\\Version1001Date20240716184935' => $baseDir . '/../lib/Migration/Version1001Date20240716184935.php',
'OCA\\WebhookListeners\\Migration\\Version1500Date20251007130000' => $baseDir . '/../lib/Migration/Version1500Date20251007130000.php',
'OCA\\WebhookListeners\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
'OCA\\WebhookListeners\\Service\\PHPMongoQuery' => $baseDir . '/../lib/Service/PHPMongoQuery.php',
'OCA\\WebhookListeners\\Service\\TokenService' => $baseDir . '/../lib/Service/TokenService.php',
'OCA\\WebhookListeners\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php',
'OCA\\WebhookListeners\\Settings\\AdminSection' => $baseDir . '/../lib/Settings/AdminSection.php',
);
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,21 @@ class ComposerStaticInitWebhookListeners
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'OCA\\WebhookListeners\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
'OCA\\WebhookListeners\\BackgroundJobs\\WebhookCall' => __DIR__ . '/..' . '/../lib/BackgroundJobs/WebhookCall.php',
'OCA\\WebhookListeners\\BackgroundJobs\\WebhookTokenCleanup' => __DIR__ . '/..' . '/../lib/BackgroundJobs/WebhookTokenCleanup.php',
'OCA\\WebhookListeners\\Command\\ListWebhooks' => __DIR__ . '/..' . '/../lib/Command/ListWebhooks.php',
'OCA\\WebhookListeners\\Controller\\WebhooksController' => __DIR__ . '/..' . '/../lib/Controller/WebhooksController.php',
'OCA\\WebhookListeners\\Db\\AuthMethod' => __DIR__ . '/..' . '/../lib/Db/AuthMethod.php',
'OCA\\WebhookListeners\\Db\\EphemeralToken' => __DIR__ . '/..' . '/../lib/Db/EphemeralToken.php',
'OCA\\WebhookListeners\\Db\\EphemeralTokenMapper' => __DIR__ . '/..' . '/../lib/Db/EphemeralTokenMapper.php',
'OCA\\WebhookListeners\\Db\\WebhookListener' => __DIR__ . '/..' . '/../lib/Db/WebhookListener.php',
'OCA\\WebhookListeners\\Db\\WebhookListenerMapper' => __DIR__ . '/..' . '/../lib/Db/WebhookListenerMapper.php',
'OCA\\WebhookListeners\\Listener\\WebhooksEventListener' => __DIR__ . '/..' . '/../lib/Listener/WebhooksEventListener.php',
'OCA\\WebhookListeners\\Migration\\Version1000Date20240527153425' => __DIR__ . '/..' . '/../lib/Migration/Version1000Date20240527153425.php',
'OCA\\WebhookListeners\\Migration\\Version1001Date20240716184935' => __DIR__ . '/..' . '/../lib/Migration/Version1001Date20240716184935.php',
'OCA\\WebhookListeners\\Migration\\Version1500Date20251007130000' => __DIR__ . '/..' . '/../lib/Migration/Version1500Date20251007130000.php',
'OCA\\WebhookListeners\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
'OCA\\WebhookListeners\\Service\\PHPMongoQuery' => __DIR__ . '/..' . '/../lib/Service/PHPMongoQuery.php',
'OCA\\WebhookListeners\\Service\\TokenService' => __DIR__ . '/..' . '/../lib/Service/TokenService.php',
'OCA\\WebhookListeners\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php',
'OCA\\WebhookListeners\\Settings\\AdminSection' => __DIR__ . '/..' . '/../lib/Settings/AdminSection.php',
);
Expand Down
5 changes: 5 additions & 0 deletions apps/webhook_listeners/lib/BackgroundJobs/WebhookCall.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use OCA\AppAPI\PublicFunctions;
use OCA\WebhookListeners\Db\AuthMethod;
use OCA\WebhookListeners\Db\WebhookListenerMapper;
use OCA\WebhookListeners\Service\TokenService;
use OCP\App\IAppManager;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\QueuedJob;
Expand All @@ -30,6 +31,7 @@ public function __construct(
private WebhookListenerMapper $mapper,
private LoggerInterface $logger,
private IAppManager $appManager,
private TokenService $tokenService,
ITimeFactory $timeFactory,
) {
parent::__construct($timeFactory);
Expand All @@ -42,6 +44,9 @@ protected function run($argument): void {
[$data, $webhookId] = $argument;
$webhookListener = $this->mapper->getById($webhookId);
$client = $this->clientService->newClient();

// adding Ephemeral auth tokens to the call
$data['tokens'] = $this->tokenService->getTokens($webhookListener, $data['user']['uid'] ?? null);
$options = [
'verify' => $this->certificateManager->getAbsoluteBundlePath(),
'headers' => $webhookListener->getHeaders() ?? [],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\WebhookListeners\BackgroundJobs;

use OCA\WebhookListeners\Db\EphemeralTokenMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;

class WebhookTokenCleanup extends TimedJob {

public function __construct(
private EphemeralTokenMapper $tokenMapper,
ITimeFactory $timeFactory,
) {
parent::__construct($timeFactory);
// every 5 min
$this->setInterval(5 * 60);
}

/**
* @param array $argument
*/
protected function run($argument): void {
$this->tokenMapper->invalidateOldTokens();
}
}
14 changes: 14 additions & 0 deletions apps/webhook_listeners/lib/Controller/WebhooksController.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ public function show(int $id): DataResponse {
* @param ?array<string,string> $headers Array of headers to send
* @param "none"|"header"|null $authMethod Authentication method to use
* @param ?array<string,mixed> $authData Array of data for authentication
* @param ?array{user_ids?:list<string>,user_roles?:list<string>} $tokenNeeded
* List of user ids for which to include auth tokens in the event.
* Has two fields: "user_ids" list of user uids for which tokens are needed, "user_roles" list of roles (users not defined by their ID but by the role they have in the webhook event) for which tokens can be included.
* Possible roles: "owner" for the user creating the webhook, "trigger" for the user triggering the webhook call.
* Requested auth tokens are valid for 1 hour after receiving them in the event call request.
*
* @return DataResponse<Http::STATUS_OK, WebhookListenersWebhookInfo, array{}>
*
Expand All @@ -134,6 +139,7 @@ public function create(
?string $authMethod,
#[\SensitiveParameter]
?array $authData,
?array $tokenNeeded = null,
): DataResponse {
$appId = null;
if ($this->session->get('app_api') === true) {
Expand All @@ -156,6 +162,7 @@ public function create(
$headers,
$authMethod,
$authData,
$tokenNeeded,
);
return new DataResponse($webhookListener->jsonSerialize());
} catch (\UnexpectedValueException $e) {
Expand All @@ -180,6 +187,11 @@ public function create(
* @param ?array<string,string> $headers Array of headers to send
* @param "none"|"header"|null $authMethod Authentication method to use
* @param ?array<string,mixed> $authData Array of data for authentication
* @param ?array{user_ids?:list<string>,user_roles?:list<string>} $tokenNeeded
* List of user ids for which to include auth tokens in the event.
* Has two fields: "user_ids" list of user uids for which tokens are needed, "user_roles" list of roles (users not defined by their ID but by the role they have in the webhook event) for which tokens can be included.
* Possible roles: "owner" for the user creating the webhook, "trigger" for the user triggering the webhook call.
* Requested auth tokens are valid for 1 hour after receiving them in the event call request.
*
* @return DataResponse<Http::STATUS_OK, WebhookListenersWebhookInfo, array{}>
*
Expand All @@ -203,6 +215,7 @@ public function update(
?string $authMethod,
#[\SensitiveParameter]
?array $authData,
?array $tokenNeeded = null,
): DataResponse {
$appId = null;
if ($this->session->get('app_api') === true) {
Expand All @@ -226,6 +239,7 @@ public function update(
$headers,
$authMethod,
$authData,
$tokenNeeded,
);
return new DataResponse($webhookListener->jsonSerialize());
} catch (\UnexpectedValueException $e) {
Expand Down
54 changes: 54 additions & 0 deletions apps/webhook_listeners/lib/Db/EphemeralToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\WebhookListeners\Db;

use OCP\AppFramework\Db\Entity;

/**
* @method int getTokenId()
* @method ?string getUserId()
* @method int getCreatedAt()
* @psalm-suppress PropertyNotSetInConstructor
*/
class EphemeralToken extends Entity implements \JsonSerializable {
/**
* @var int id of the token in the oc_authtoken db table
*/
protected $tokenId;

/**
* @var ?string id of the user wich the token belongs to
* @psalm-suppress PropertyNotSetInConstructor
*/
protected $userId = null;

/**
* @var int token creation timestamp
* @psalm-suppress PropertyNotSetInConstructor
*/
protected $createdAt;

public function __construct() {
$this->addType('tokenId', 'integer');
$this->addType('userId', 'string');
$this->addType('createdAt', 'integer');
}

public function jsonSerialize(): array {
$fields = array_keys($this->getFieldTypes());
return array_combine(
$fields,
array_map(
fn ($field) => $this->getter($field),
$fields
)
);
}
}
121 changes: 121 additions & 0 deletions apps/webhook_listeners/lib/Db/EphemeralTokenMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\WebhookListeners\Db;

use OC\Authentication\Token\PublicKeyTokenMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use Psr\Log\LoggerInterface;

/**
* @template-extends QBMapper<EphemeralToken>
*/

class EphemeralTokenMapper extends QBMapper {
public const TABLE_NAME = 'webhook_tokens';
public const TOKEN_LIFETIME = 1 * 1 * 60; // one hour in seconds

public function __construct(
IDBConnection $db,
private LoggerInterface $logger,
private ITimeFactory $time,
private PublicKeyTokenMapper $tokenMapper,
) {
parent::__construct($db, self::TABLE_NAME, EphemeralToken::class);
}

/**
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
* @throws Exception
*/
public function getById(int $id): EphemeralToken {
$qb = $this->db->getQueryBuilder();

$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));

return $this->findEntity($qb);
}

/**
* @throws Exception
* @return EphemeralToken[]
*/
public function getAll(): array {
$qb = $this->db->getQueryBuilder();

$qb->select('*')
->from($this->getTableName());

return $this->findEntities($qb);
}


/**
* @param int $olderThan
* @return EphemeralToken[]
* @throws Exception
*/
public function getOlderThan($olderThan): array {
$qb = $this->db->getQueryBuilder();

$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->lt('created_at', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT)));

return $this->findEntities($qb);
}

/**
* @throws Exception
*/
public function addEphemeralToken(
int $tokenId,
?string $userId,
int $createdAt,
): EphemeralToken {
$tempToken = EphemeralToken::fromParams(
[
'tokenId' => $tokenId,
'userId' => $userId,
'createdAt' => $createdAt,
]
);
return $this->insert($tempToken);
}
public function invalidateOldTokens(int $token_lifetime = self::TOKEN_LIFETIME) {
$olderThan = $this->time->getTime() - $token_lifetime;
try {
$tokensToDelete = $this->getOlderThan($olderThan);
} catch (Exception $e) {
$this->logger->error('Webhook token deletion failed: ' . $e->getMessage(), ['exception' => $e]);
return;
}


$this->logger->debug('Invalidating ephemeral webhook tokens older than ' . date('c', $olderThan), ['app' => 'webhook_listeners']);
foreach ($tokensToDelete as $token) {
try {
$this->tokenMapper->delete($this->tokenMapper->getTokenById($token->getTokenId())); // delete token itself
$this->delete($token); // delete db row in webhook_tokens
} catch (Exception $e) {
$this->logger->error('Webhook token deletion failed: ' . $e->getMessage(), ['exception' => $e]);
}

}
}
}
9 changes: 9 additions & 0 deletions apps/webhook_listeners/lib/Db/WebhookListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* @method ?string getAuthData()
* @method void setAuthData(?string $data)
* @method string getAuthMethod()
* @method ?array getTokenNeeded()
* @psalm-suppress PropertyNotSetInConstructor
*/
class WebhookListener extends Entity implements \JsonSerializable {
Expand Down Expand Up @@ -84,8 +85,15 @@ class WebhookListener extends Entity implements \JsonSerializable {
*/
protected $authData = null;

/**
* @var array
* @psalm-suppress PropertyNotSetInConstructor
*/
protected $tokenNeeded;

private ICrypto $crypto;


public function __construct(
?ICrypto $crypto = null,
) {
Expand All @@ -103,6 +111,7 @@ public function __construct(
$this->addType('headers', 'json');
$this->addType('authMethod', 'string');
$this->addType('authData', 'string');
$this->addType('tokenNeeded', 'json');
}

public function getAuthMethodEnum(): AuthMethod {
Expand Down
Loading
Loading