feat(webhooks): Add support for a userid filter

This allows to register a userId to filter on along with the webhooks.
This webhook will then only be triggered if the given userId is the one
in session. This is more efficient than filtering by user in the event
filter because the listener is not even registered if the user id does
not match.

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
This commit is contained in:
Côme Chilliet 2024-06-24 16:19:03 +02:00
parent 164f4a3ea3
commit 44d707c97b
No known key found for this signature in database
GPG key ID: A3E2F658B28C760A
7 changed files with 42 additions and 10 deletions

View file

@ -16,6 +16,7 @@ use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IUserSession;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
@ -40,9 +41,10 @@ class Application extends App implements IBootstrap {
): void {
/** @var WebhookListenerMapper */
$mapper = $container->get(WebhookListenerMapper::class);
$userSession = $container->get(IUserSession::class);
/* Listen to all events with at least one webhook configured */
$configuredEvents = $mapper->getAllConfiguredEvents();
$configuredEvents = $mapper->getAllConfiguredEvents($userSession->getUser()?->getUID());
foreach ($configuredEvents as $eventName) {
$logger->debug("Listening to {$eventName}");
$dispatcher->addServiceListener(

View file

@ -126,6 +126,7 @@ class WebhooksController extends OCSController {
string $uri,
string $event,
?array $eventFilter,
?string $userIdFilter,
?array $headers,
?string $authMethod,
#[\SensitiveParameter]
@ -150,6 +151,7 @@ class WebhooksController extends OCSController {
$uri,
$event,
$eventFilter,
$userIdFilter,
$headers,
$authMethod,
$authData,
@ -193,6 +195,7 @@ class WebhooksController extends OCSController {
string $uri,
string $event,
?array $eventFilter,
?string $userIdFilter,
?array $headers,
?string $authMethod,
#[\SensitiveParameter]
@ -218,6 +221,7 @@ class WebhooksController extends OCSController {
$uri,
$event,
$eventFilter,
$userIdFilter,
$headers,
$authMethod,
$authData,

View file

@ -59,6 +59,12 @@ class WebhookListener extends Entity implements \JsonSerializable {
*/
protected $eventFilter;
/**
* @var string
* If not empty, id of the user that needs to be connected for the webhook to trigger
*/
protected $userIdFilter;
/**
* @var ?array
*/
@ -90,6 +96,7 @@ class WebhookListener extends Entity implements \JsonSerializable {
$this->addType('uri', 'string');
$this->addType('event', 'string');
$this->addType('eventFilter', 'json');
$this->addType('userIdFilter', 'string');
$this->addType('headers', 'json');
$this->addType('authMethod', 'string');
$this->addType('authData', 'string');

View file

@ -25,7 +25,7 @@ use OCP\IDBConnection;
class WebhookListenerMapper extends QBMapper {
public const TABLE_NAME = 'webhook_listeners';
private const EVENTS_CACHE_KEY = 'eventsUsedInWebhooks';
private const EVENTS_CACHE_KEY_PREFIX = 'eventsUsedInWebhooks';
private ?ICache $cache = null;
@ -77,6 +77,7 @@ class WebhookListenerMapper extends QBMapper {
string $uri,
string $event,
?array $eventFilter,
?string $userIdFilter,
?array $headers,
AuthMethod $authMethod,
#[\SensitiveParameter]
@ -95,12 +96,13 @@ class WebhookListenerMapper extends QBMapper {
'uri' => $uri,
'event' => $event,
'eventFilter' => $eventFilter ?? [],
'userIdFilter' => $userIdFilter ?? '',
'headers' => $headers,
'authMethod' => $authMethod->value,
]
);
$webhookListener->setAuthDataClear($authData);
$this->cache?->remove(self::EVENTS_CACHE_KEY);
$this->cache?->remove($this->buildCacheKey($userIdFilter));
return $this->insert($webhookListener);
}
@ -115,6 +117,7 @@ class WebhookListenerMapper extends QBMapper {
string $uri,
string $event,
?array $eventFilter,
?string $userIdFilter,
?array $headers,
AuthMethod $authMethod,
#[\SensitiveParameter]
@ -134,12 +137,13 @@ class WebhookListenerMapper extends QBMapper {
'uri' => $uri,
'event' => $event,
'eventFilter' => $eventFilter ?? [],
'userIdFilter' => $userIdFilter ?? '',
'headers' => $headers,
'authMethod' => $authMethod->value,
]
);
$webhookListener->setAuthDataClear($authData);
$this->cache?->remove(self::EVENTS_CACHE_KEY);
$this->cache?->remove($this->buildCacheKey($userIdFilter));
return $this->update($webhookListener);
}
@ -159,11 +163,12 @@ class WebhookListenerMapper extends QBMapper {
* @throws Exception
* @return list<string>
*/
private function getAllConfiguredEventsFromDatabase(): array {
private function getAllConfiguredEventsFromDatabase(string $userId): array {
$qb = $this->db->getQueryBuilder();
$qb->selectDistinct('event')
->from($this->getTableName());
->from($this->getTableName())
->where($qb->expr()->in('user_id_filter', $qb->createNamedParameter(['',$userId], IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR));
$result = $qb->executeQuery();
@ -181,14 +186,15 @@ class WebhookListenerMapper extends QBMapper {
* @throws Exception
* @return list<string>
*/
public function getAllConfiguredEvents(): array {
$events = $this->cache?->get(self::EVENTS_CACHE_KEY);
public function getAllConfiguredEvents(?string $userId = null): array {
$cacheKey = $this->buildCacheKey($userId);
$events = $this->cache?->get($cacheKey);
if ($events !== null) {
return json_decode($events);
}
$events = $this->getAllConfiguredEventsFromDatabase();
$events = $this->getAllConfiguredEventsFromDatabase($userId ?? '');
// cache for 5 minutes
$this->cache?->set(self::EVENTS_CACHE_KEY, json_encode($events), 300);
$this->cache?->set($cacheKey, json_encode($events), 300);
return $events;
}
@ -217,4 +223,8 @@ class WebhookListenerMapper extends QBMapper {
return $this->findEntities($qb);
}
private function buildCacheKey(?string $userIdFilter = ''): string {
return self::EVENTS_CACHE_KEY_PREFIX.'_'.($userIdFilter ?? '');
}
}

View file

@ -53,6 +53,10 @@ class Version1000Date20240527153425 extends SimpleMigrationStep {
$table->addColumn('event_filter', Types::TEXT, [
'notnull' => false,
]);
$table->addColumn('user_id_filter', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('headers', Types::TEXT, [
'notnull' => false,
]);

View file

@ -17,6 +17,7 @@ namespace OCA\WebhookListeners;
* uri: string,
* event?: string,
* eventFilter?: array<string,mixed>,
* userIdFilter?: string,
* headers?: array<string,string>,
* authMethod: string,
* authData?: array<string,mixed>,

View file

@ -58,6 +58,7 @@ class WebhookListenerMapperTest extends TestCase {
UserCreatedEvent::class,
null,
null,
null,
AuthMethod::None,
null,
);
@ -72,6 +73,7 @@ class WebhookListenerMapperTest extends TestCase {
NodeWrittenEvent::class,
null,
null,
null,
AuthMethod::None,
null,
);
@ -92,6 +94,7 @@ class WebhookListenerMapperTest extends TestCase {
NodeWrittenEvent::class,
null,
null,
null,
AuthMethod::None,
null,
);
@ -111,6 +114,7 @@ class WebhookListenerMapperTest extends TestCase {
NodeWrittenEvent::class,
null,
null,
null,
AuthMethod::Header,
['secretHeader' => 'header'],
);