Skip to content

Commit 8469904

Browse files
authored
Merge pull request #46076 from nextcloud/enh/webhooks-user-id-filter
feat(webhooks): Add support for a userid filter
2 parents 72b6db4 + 5365882 commit 8469904

9 files changed

Lines changed: 124 additions & 13 deletions

File tree

apps/webhook_listeners/lib/AppInfo/Application.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use OCP\AppFramework\Bootstrap\IBootstrap;
1717
use OCP\AppFramework\Bootstrap\IRegistrationContext;
1818
use OCP\EventDispatcher\IEventDispatcher;
19+
use OCP\IUserSession;
1920
use Psr\Container\ContainerInterface;
2021
use Psr\Log\LoggerInterface;
2122

@@ -40,9 +41,10 @@ private function registerRuleListeners(
4041
): void {
4142
/** @var WebhookListenerMapper */
4243
$mapper = $container->get(WebhookListenerMapper::class);
44+
$userSession = $container->get(IUserSession::class);
4345

4446
/* Listen to all events with at least one webhook configured */
45-
$configuredEvents = $mapper->getAllConfiguredEvents();
47+
$configuredEvents = $mapper->getAllConfiguredEvents($userSession->getUser()?->getUID());
4648
foreach ($configuredEvents as $eventName) {
4749
$logger->debug("Listening to {$eventName}");
4850
$dispatcher->addServiceListener(

apps/webhook_listeners/lib/Controller/WebhooksController.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ public function show(int $id): DataResponse {
107107
* @param string $uri Webhook URI endpoint
108108
* @param string $event Event class name to listen to
109109
* @param ?array<string,mixed> $eventFilter Mongo filter to apply to the serialized data to decide if firing
110+
* @param ?string $userIdFilter User id to filter on. The webhook will only be called by requests from this user. Empty or null means no filtering.
110111
* @param ?array<string,string> $headers Array of headers to send
111112
* @param "none"|"headers"|null $authMethod Authentication method to use
112113
* @param ?array<string,mixed> $authData Array of data for authentication
@@ -126,6 +127,7 @@ public function create(
126127
string $uri,
127128
string $event,
128129
?array $eventFilter,
130+
?string $userIdFilter,
129131
?array $headers,
130132
?string $authMethod,
131133
#[\SensitiveParameter]
@@ -150,6 +152,7 @@ public function create(
150152
$uri,
151153
$event,
152154
$eventFilter,
155+
$userIdFilter,
153156
$headers,
154157
$authMethod,
155158
$authData,
@@ -173,6 +176,7 @@ public function create(
173176
* @param string $uri Webhook URI endpoint
174177
* @param string $event Event class name to listen to
175178
* @param ?array<string,mixed> $eventFilter Mongo filter to apply to the serialized data to decide if firing
179+
* @param ?string $userIdFilter User id to filter on. The webhook will only be called by requests from this user. Empty or null means no filtering.
176180
* @param ?array<string,string> $headers Array of headers to send
177181
* @param "none"|"headers"|null $authMethod Authentication method to use
178182
* @param ?array<string,mixed> $authData Array of data for authentication
@@ -193,6 +197,7 @@ public function update(
193197
string $uri,
194198
string $event,
195199
?array $eventFilter,
200+
?string $userIdFilter,
196201
?array $headers,
197202
?string $authMethod,
198203
#[\SensitiveParameter]
@@ -218,6 +223,7 @@ public function update(
218223
$uri,
219224
$event,
220225
$eventFilter,
226+
$userIdFilter,
221227
$headers,
222228
$authMethod,
223229
$authData,

apps/webhook_listeners/lib/Db/WebhookListener.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ class WebhookListener extends Entity implements \JsonSerializable {
5959
*/
6060
protected $eventFilter;
6161

62+
/**
63+
* @var ?string
64+
* If not empty, id of the user that needs to be connected for the webhook to trigger
65+
* @psalm-suppress PropertyNotSetInConstructor
66+
*/
67+
protected $userIdFilter;
68+
6269
/**
6370
* @var ?array
6471
*/
@@ -90,6 +97,7 @@ public function __construct(
9097
$this->addType('uri', 'string');
9198
$this->addType('event', 'string');
9299
$this->addType('eventFilter', 'json');
100+
$this->addType('userIdFilter', 'string');
93101
$this->addType('headers', 'json');
94102
$this->addType('authMethod', 'string');
95103
$this->addType('authData', 'string');

apps/webhook_listeners/lib/Db/WebhookListenerMapper.php

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
class WebhookListenerMapper extends QBMapper {
2626
public const TABLE_NAME = 'webhook_listeners';
2727

28-
private const EVENTS_CACHE_KEY = 'eventsUsedInWebhooks';
28+
private const EVENTS_CACHE_KEY_PREFIX = 'eventsUsedInWebhooks';
2929

3030
private ?ICache $cache = null;
3131

@@ -77,6 +77,7 @@ public function addWebhookListener(
7777
string $uri,
7878
string $event,
7979
?array $eventFilter,
80+
?string $userIdFilter,
8081
?array $headers,
8182
AuthMethod $authMethod,
8283
#[\SensitiveParameter]
@@ -95,12 +96,13 @@ public function addWebhookListener(
9596
'uri' => $uri,
9697
'event' => $event,
9798
'eventFilter' => $eventFilter ?? [],
99+
'userIdFilter' => $userIdFilter ?? '',
98100
'headers' => $headers,
99101
'authMethod' => $authMethod->value,
100102
]
101103
);
102104
$webhookListener->setAuthDataClear($authData);
103-
$this->cache?->remove(self::EVENTS_CACHE_KEY);
105+
$this->cache?->remove($this->buildCacheKey($userIdFilter));
104106
return $this->insert($webhookListener);
105107
}
106108

@@ -115,6 +117,7 @@ public function updateWebhookListener(
115117
string $uri,
116118
string $event,
117119
?array $eventFilter,
120+
?string $userIdFilter,
118121
?array $headers,
119122
AuthMethod $authMethod,
120123
#[\SensitiveParameter]
@@ -134,12 +137,13 @@ public function updateWebhookListener(
134137
'uri' => $uri,
135138
'event' => $event,
136139
'eventFilter' => $eventFilter ?? [],
140+
'userIdFilter' => $userIdFilter ?? '',
137141
'headers' => $headers,
138142
'authMethod' => $authMethod->value,
139143
]
140144
);
141145
$webhookListener->setAuthDataClear($authData);
142-
$this->cache?->remove(self::EVENTS_CACHE_KEY);
146+
$this->cache?->remove($this->buildCacheKey($userIdFilter));
143147
return $this->update($webhookListener);
144148
}
145149

@@ -159,11 +163,16 @@ public function deleteById(int $id): bool {
159163
* @throws Exception
160164
* @return list<string>
161165
*/
162-
private function getAllConfiguredEventsFromDatabase(): array {
166+
private function getAllConfiguredEventsFromDatabase(string $userId): array {
163167
$qb = $this->db->getQueryBuilder();
164168

165169
$qb->selectDistinct('event')
166-
->from($this->getTableName());
170+
->from($this->getTableName())
171+
->where($qb->expr()->emptyString('user_id_filter'));
172+
173+
if ($userId !== '') {
174+
$qb->orWhere($qb->expr()->eq('user_id_filter', $qb->createNamedParameter($userId)));
175+
}
167176

168177
$result = $qb->executeQuery();
169178

@@ -181,27 +190,40 @@ private function getAllConfiguredEventsFromDatabase(): array {
181190
* @throws Exception
182191
* @return list<string>
183192
*/
184-
public function getAllConfiguredEvents(): array {
185-
$events = $this->cache?->get(self::EVENTS_CACHE_KEY);
193+
public function getAllConfiguredEvents(?string $userId = null): array {
194+
$cacheKey = $this->buildCacheKey($userId);
195+
$events = $this->cache?->get($cacheKey);
186196
if ($events !== null) {
187197
return json_decode($events);
188198
}
189-
$events = $this->getAllConfiguredEventsFromDatabase();
199+
$events = $this->getAllConfiguredEventsFromDatabase($userId ?? '');
190200
// cache for 5 minutes
191-
$this->cache?->set(self::EVENTS_CACHE_KEY, json_encode($events), 300);
201+
$this->cache?->set($cacheKey, json_encode($events), 300);
192202
return $events;
193203
}
194204

195205
/**
196206
* @throws Exception
197207
*/
198-
public function getByEvent(string $event): array {
208+
public function getByEvent(string $event, ?string $userId = null): array {
199209
$qb = $this->db->getQueryBuilder();
200210

201211
$qb->select('*')
202212
->from($this->getTableName())
203213
->where($qb->expr()->eq('event', $qb->createNamedParameter($event, IQueryBuilder::PARAM_STR)));
204214

215+
216+
if ($userId === '' || $userId === null) {
217+
$qb->andWhere($qb->expr()->emptyString('user_id_filter'));
218+
} else {
219+
$qb->andWhere(
220+
$qb->expr()->orX(
221+
$qb->expr()->emptyString('user_id_filter'),
222+
$qb->expr()->eq('user_id_filter', $qb->createNamedParameter($userId)),
223+
)
224+
);
225+
}
226+
205227
return $this->findEntities($qb);
206228
}
207229

@@ -217,4 +239,8 @@ public function getByUri(string $uri): array {
217239

218240
return $this->findEntities($qb);
219241
}
242+
243+
private function buildCacheKey(?string $userIdFilter): string {
244+
return self::EVENTS_CACHE_KEY_PREFIX.'_'.($userIdFilter ?? '');
245+
}
220246
}

apps/webhook_listeners/lib/Listener/WebhooksEventListener.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ public function __construct(
3434
}
3535

3636
public function handle(Event $event): void {
37-
$webhookListeners = $this->mapper->getByEvent($event::class);
3837
$user = $this->userSession->getUser();
38+
$webhookListeners = $this->mapper->getByEvent($event::class, $user?->getUID());
3939

4040
foreach ($webhookListeners as $webhookListener) {
4141
// TODO add group membership to be able to filter on it

apps/webhook_listeners/lib/Migration/Version1000Date20240527153425.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,17 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
4747
'notnull' => true,
4848
'length' => 4000,
4949
]);
50-
$table->addColumn('event', Types::TEXT, [
50+
$table->addColumn('event', Types::STRING, [
5151
'notnull' => true,
52+
'length' => 4000,
5253
]);
5354
$table->addColumn('event_filter', Types::TEXT, [
5455
'notnull' => false,
5556
]);
57+
$table->addColumn('user_id_filter', Types::STRING, [
58+
'notnull' => false,
59+
'length' => 64,
60+
]);
5661
$table->addColumn('headers', Types::TEXT, [
5762
'notnull' => false,
5863
]);

apps/webhook_listeners/lib/ResponseDefinitions.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* uri: string,
1818
* event?: string,
1919
* eventFilter?: array<string,mixed>,
20+
* userIdFilter?: string,
2021
* headers?: array<string,string>,
2122
* authMethod: string,
2223
* authData?: array<string,mixed>,

apps/webhook_listeners/openapi.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@
7575
"type": "object"
7676
}
7777
},
78+
"userIdFilter": {
79+
"type": "string"
80+
},
7881
"headers": {
7982
"type": "object",
8083
"additionalProperties": {
@@ -223,6 +226,11 @@
223226
"type": "object"
224227
}
225228
},
229+
"userIdFilter": {
230+
"type": "string",
231+
"nullable": true,
232+
"description": "User id to filter on. The webhook will only be called by requests from this user. Empty or null means no filtering."
233+
},
226234
"headers": {
227235
"type": "object",
228236
"nullable": true,
@@ -501,6 +509,11 @@
501509
"type": "object"
502510
}
503511
},
512+
"userIdFilter": {
513+
"type": "string",
514+
"nullable": true,
515+
"description": "User id to filter on. The webhook will only be called by requests from this user. Empty or null means no filtering."
516+
},
504517
"headers": {
505518
"type": "object",
506519
"nullable": true,

apps/webhook_listeners/tests/Db/WebhookListenerMapperTest.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public function testInsertListenerWithNotSupportedEvent() {
5858
UserCreatedEvent::class,
5959
null,
6060
null,
61+
null,
6162
AuthMethod::None,
6263
null,
6364
);
@@ -72,6 +73,7 @@ public function testInsertListenerAndGetIt() {
7273
NodeWrittenEvent::class,
7374
null,
7475
null,
76+
null,
7577
AuthMethod::None,
7678
null,
7779
);
@@ -92,6 +94,7 @@ public function testInsertListenerAndGetItByUri() {
9294
NodeWrittenEvent::class,
9395
null,
9496
null,
97+
null,
9598
AuthMethod::None,
9699
null,
97100
);
@@ -111,6 +114,7 @@ public function testInsertListenerAndGetItWithAuthData() {
111114
NodeWrittenEvent::class,
112115
null,
113116
null,
117+
null,
114118
AuthMethod::Header,
115119
['secretHeader' => 'header'],
116120
);
@@ -120,4 +124,50 @@ public function testInsertListenerAndGetItWithAuthData() {
120124
$listener1->resetUpdatedFields();
121125
$this->assertEquals($listener1, $listener2);
122126
}
127+
128+
public function testInsertListenerAndGetItByEventAndUser() {
129+
$listener1 = $this->mapper->addWebhookListener(
130+
null,
131+
'bob',
132+
'POST',
133+
'https://webhook.example.com/endpoint',
134+
NodeWrittenEvent::class,
135+
null,
136+
'alice',
137+
null,
138+
AuthMethod::None,
139+
null,
140+
);
141+
$listener1->resetUpdatedFields();
142+
143+
$this->assertEquals([NodeWrittenEvent::class], $this->mapper->getAllConfiguredEvents('alice'));
144+
$this->assertEquals([], $this->mapper->getAllConfiguredEvents(''));
145+
$this->assertEquals([], $this->mapper->getAllConfiguredEvents('otherUser'));
146+
147+
$this->assertEquals([$listener1], $this->mapper->getByEvent(NodeWrittenEvent::class, 'alice'));
148+
$this->assertEquals([], $this->mapper->getByEvent(NodeWrittenEvent::class, ''));
149+
$this->assertEquals([], $this->mapper->getByEvent(NodeWrittenEvent::class, 'otherUser'));
150+
151+
/* Add a second listener with no user filter */
152+
$listener2 = $this->mapper->addWebhookListener(
153+
null,
154+
'bob',
155+
'POST',
156+
'https://webhook.example.com/endpoint',
157+
NodeWrittenEvent::class,
158+
null,
159+
'',
160+
null,
161+
AuthMethod::None,
162+
null,
163+
);
164+
$listener2->resetUpdatedFields();
165+
166+
$this->assertEquals([NodeWrittenEvent::class], $this->mapper->getAllConfiguredEvents('alice'));
167+
$this->assertEquals([NodeWrittenEvent::class], $this->mapper->getAllConfiguredEvents(''));
168+
169+
$this->assertEquals([$listener1, $listener2], $this->mapper->getByEvent(NodeWrittenEvent::class, 'alice'));
170+
$this->assertEquals([$listener2], $this->mapper->getByEvent(NodeWrittenEvent::class, 'otherUser'));
171+
$this->assertEquals([$listener2], $this->mapper->getByEvent(NodeWrittenEvent::class));
172+
}
123173
}

0 commit comments

Comments
 (0)