Skip to content

Commit 7847d3b

Browse files
committed
feat: token deletion logic
Signed-off-by: Jana Peper <[email protected]>
1 parent 22f3f23 commit 7847d3b

File tree

9 files changed

+302
-3
lines changed

9 files changed

+302
-3
lines changed

apps/webhook_listeners/appinfo/info.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
]]>
2626
</description>
2727

28-
<version>1.4.1</version>
28+
<version>1.4.2</version>
2929
<licence>agpl</licence>
3030
<author>Côme Chilliet</author>
3131
<namespace>WebhookListeners</namespace>
@@ -56,4 +56,8 @@
5656
<admin-delegation>OCA\WebhookListeners\Settings\Admin</admin-delegation>
5757
<admin-delegation-section>OCA\WebhookListeners\Settings\AdminSection</admin-delegation-section>
5858
</settings>
59+
60+
<background-jobs>
61+
<job>OCA\WebhookListeners\BackgroundJobs\WebhookTokenCleanup</job>
62+
</background-jobs>
5963
</info>

apps/webhook_listeners/composer/composer/autoload_classmap.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
1010
'OCA\\WebhookListeners\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
1111
'OCA\\WebhookListeners\\BackgroundJobs\\WebhookCall' => $baseDir . '/../lib/BackgroundJobs/WebhookCall.php',
12+
'OCA\\WebhookListeners\\BackgroundJobs\\WebhookTokenCleanup' => $baseDir . '/../lib/BackgroundJobs/WebhookTokenCleanup.php',
1213
'OCA\\WebhookListeners\\Command\\ListWebhooks' => $baseDir . '/../lib/Command/ListWebhooks.php',
1314
'OCA\\WebhookListeners\\Controller\\WebhooksController' => $baseDir . '/../lib/Controller/WebhooksController.php',
1415
'OCA\\WebhookListeners\\Db\\AuthMethod' => $baseDir . '/../lib/Db/AuthMethod.php',
16+
'OCA\\WebhookListeners\\Db\\TemporaryToken' => $baseDir . '/../lib/Db/TemporaryToken.php',
17+
'OCA\\WebhookListeners\\Db\\TemporaryTokenMapper' => $baseDir . '/../lib/Db/TemporaryTokenMapper.php',
1518
'OCA\\WebhookListeners\\Db\\WebhookListener' => $baseDir . '/../lib/Db/WebhookListener.php',
1619
'OCA\\WebhookListeners\\Db\\WebhookListenerMapper' => $baseDir . '/../lib/Db/WebhookListenerMapper.php',
1720
'OCA\\WebhookListeners\\Listener\\WebhooksEventListener' => $baseDir . '/../lib/Listener/WebhooksEventListener.php',

apps/webhook_listeners/composer/composer/autoload_static.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ class ComposerStaticInitWebhookListeners
2424
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
2525
'OCA\\WebhookListeners\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
2626
'OCA\\WebhookListeners\\BackgroundJobs\\WebhookCall' => __DIR__ . '/..' . '/../lib/BackgroundJobs/WebhookCall.php',
27+
'OCA\\WebhookListeners\\BackgroundJobs\\WebhookTokenCleanup' => __DIR__ . '/..' . '/../lib/BackgroundJobs/WebhookTokenCleanup.php',
2728
'OCA\\WebhookListeners\\Command\\ListWebhooks' => __DIR__ . '/..' . '/../lib/Command/ListWebhooks.php',
2829
'OCA\\WebhookListeners\\Controller\\WebhooksController' => __DIR__ . '/..' . '/../lib/Controller/WebhooksController.php',
2930
'OCA\\WebhookListeners\\Db\\AuthMethod' => __DIR__ . '/..' . '/../lib/Db/AuthMethod.php',
31+
'OCA\\WebhookListeners\\Db\\TemporaryToken' => __DIR__ . '/..' . '/../lib/Db/TemporaryToken.php',
32+
'OCA\\WebhookListeners\\Db\\TemporaryTokenMapper' => __DIR__ . '/..' . '/../lib/Db/TemporaryTokenMapper.php',
3033
'OCA\\WebhookListeners\\Db\\WebhookListener' => __DIR__ . '/..' . '/../lib/Db/WebhookListener.php',
3134
'OCA\\WebhookListeners\\Db\\WebhookListenerMapper' => __DIR__ . '/..' . '/../lib/Db/WebhookListenerMapper.php',
3235
'OCA\\WebhookListeners\\Listener\\WebhooksEventListener' => __DIR__ . '/..' . '/../lib/Listener/WebhooksEventListener.php',
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\WebhookListeners\BackgroundJobs;
11+
12+
use OCA\WebhookListeners\Db\TemporaryTokenMapper;
13+
use OCP\AppFramework\Utility\ITimeFactory;
14+
use OCP\BackgroundJob\TimedJob;
15+
16+
class WebhookTokenCleanup extends TimedJob {
17+
18+
public function __construct(
19+
private TemporaryTokenMapper $tokenMapper,
20+
ITimeFactory $timeFactory,
21+
) {
22+
parent::__construct($timeFactory);
23+
// every 5 min
24+
$this->setInterval(1 * 5 * 60);
25+
}
26+
27+
/**
28+
* @param array $argument
29+
*/
30+
protected function run($argument): void {
31+
32+
$this->tokenMapper->invalidateOldTokens();
33+
34+
35+
}
36+
37+
38+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\WebhookListeners\Db;
11+
12+
use OCP\AppFramework\Db\Entity;
13+
use OCP\Security\ICrypto;
14+
use OCP\Server;
15+
16+
17+
/**
18+
* @method int getTokenId()
19+
* @method string getToken()
20+
* @method ?string getUserId()
21+
* @method int getCreationDatetime()
22+
* @psalm-suppress PropertyNotSetInConstructor
23+
*/
24+
class TemporaryToken extends Entity implements \JsonSerializable {
25+
/**
26+
* @var int id of the token that was created for a webhook
27+
*/
28+
protected $tokenId;
29+
30+
/**
31+
* @var string the token
32+
*/
33+
protected $token;
34+
35+
/**
36+
* @var ?string id of the user wich the token belongs to
37+
* @psalm-suppress PropertyNotSetInConstructor
38+
*/
39+
protected $userId = null;
40+
41+
/**
42+
* @var int
43+
* @psalm-suppress PropertyNotSetInConstructor
44+
*/
45+
protected $creationDatetime;
46+
47+
48+
49+
50+
public function __construct() {
51+
$this->addType('tokenId', 'integer');
52+
$this->addType('token', 'string');
53+
$this->addType('userId', 'string');
54+
$this->addType('creationDatetime', 'integer');
55+
}
56+
57+
public function jsonSerialize(): array {
58+
$fields = array_keys($this->getFieldTypes());
59+
return array_combine(
60+
$fields,
61+
array_map(
62+
fn ($field) => $this->getter($field),
63+
$fields
64+
)
65+
);
66+
}
67+
68+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\WebhookListeners\Db;
11+
12+
use OCP\AppFramework\Db\DoesNotExistException;
13+
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
14+
use OCP\AppFramework\Db\QBMapper;
15+
use OCP\AppFramework\Utility\ITimeFactory;
16+
use OC\Authentication\Token\PublicKeyTokenMapper;
17+
use OCP\DB\Exception;
18+
use OCP\DB\QueryBuilder\IQueryBuilder;
19+
use OCP\IDBConnection;
20+
use Psr\Log\LoggerInterface;
21+
22+
/**
23+
* @template-extends QBMapper<TemporaryToken>
24+
*/
25+
26+
class TemporaryTokenMapper extends QBMapper {
27+
public const TABLE_NAME = 'webhook_tokens';
28+
public const TOKEN_LIFETIME = 1 * 1 * 60; // one hour in seconds
29+
30+
31+
public function __construct(
32+
IDBConnection $db,
33+
private LoggerInterface $logger,
34+
private ITimeFactory $time,
35+
private PublicKeyTokenMapper $tokenMapper,
36+
) {
37+
parent::__construct($db, self::TABLE_NAME, TemporaryToken::class);
38+
}
39+
40+
/**
41+
* @throws DoesNotExistException
42+
* @throws MultipleObjectsReturnedException
43+
* @throws Exception
44+
*/
45+
public function getById(int $id): TemporaryToken {
46+
$qb = $this->db->getQueryBuilder();
47+
48+
$qb->select('*')
49+
->from($this->getTableName())
50+
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
51+
52+
return $this->findEntity($qb);
53+
}
54+
55+
/**
56+
* @throws Exception
57+
* @return WebhookListener[]
58+
*/
59+
public function getAll(): array {
60+
$qb = $this->db->getQueryBuilder();
61+
62+
$qb->select('*')
63+
->from($this->getTableName());
64+
65+
return $this->findEntities($qb);
66+
}
67+
68+
public function getOlderThan($olderThan): array {
69+
$qb = $this->db->getQueryBuilder();
70+
71+
$qb->select('*')
72+
->from($this->getTableName())
73+
->where($qb->expr()->lt('creation_datetime', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT)));
74+
75+
return $this->findEntities($qb);
76+
}
77+
78+
79+
/**
80+
* @throws Exception
81+
*/
82+
public function addTemporaryToken(
83+
int $tokenId,
84+
string $token,
85+
?string $userId,
86+
int $creationDatetime,
87+
): TemporaryToken {
88+
$tempToken = TemporaryToken::fromParams(
89+
[
90+
'tokenId' => $tokenId,
91+
'token' => $token,
92+
'userId' => $userId,
93+
'creationDatetime' => $creationDatetime,
94+
]
95+
);
96+
return $this->insert($tempToken);
97+
}
98+
99+
/**
100+
* @throws Exception
101+
*/
102+
public function deleteByTokenId(int $tokenId): bool {
103+
$qb = $this->db->getQueryBuilder();
104+
105+
$qb->delete($this->getTableName())
106+
->where($qb->expr()->eq('token_id', $qb->createNamedParameter($tokenId, IQueryBuilder::PARAM_INT)));
107+
108+
return $qb->executeStatement() > 0;
109+
}
110+
111+
public function invalidateOldTokens(int $token_lifetime = self::TOKEN_LIFETIME) {
112+
$olderThan = $this->time->getTime() - $token_lifetime;
113+
error_log("OLDER THAN");
114+
$tokensToDelete = $this->getOlderThan($olderThan);
115+
error_log(json_encode($tokensToDelete));
116+
117+
$this->logger->debug('Invalidating temporary webhook tokens older than ' . date('c', $olderThan), ['app' => 'cron']);
118+
foreach ($tokensToDelete as $token) {
119+
$this->tokenMapper->invalidate($token->getToken()); // delete token itself
120+
$this->deleteByTokenId($token->getTokenId()); // delete db row in webhook_temporary_tokens
121+
}
122+
123+
}
124+
}

apps/webhook_listeners/lib/Migration/Version1500Date20251007130000.php

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
use Closure;
1313
use OCA\WebhookListeners\Db\WebhookListenerMapper;
14+
use OCA\WebhookListeners\Db\TemporaryTokenMapper;
1415
use OCP\DB\ISchemaWrapper;
1516
use OCP\DB\Types;
1617
use OCP\Migration\IOutput;
@@ -34,9 +35,38 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt
3435
'notnull' => false,
3536
]);
3637
}
38+
}
3739

38-
return $schema;
40+
if (!$schema->hasTable(TemporaryTokenMapper::TABLE_NAME)) {
41+
$table = $schema->createTable(TemporaryTokenMapper::TABLE_NAME);
42+
$table->addColumn('id', 'integer', [
43+
'autoincrement' => true,
44+
'notnull' => true,
45+
]);
46+
47+
$table->addColumn('token_id', 'integer', [
48+
'notnull' => true,
49+
'length' => 4,
50+
'unsigned' => true,
51+
]);
52+
$table->addColumn('token', 'string', [
53+
'notnull' => false,
54+
'length' => 200,
55+
]);
56+
$table->addColumn('user_id', 'string', [
57+
'notnull' => false,
58+
'length' => 64,
59+
]);
60+
$table->addColumn('creation_datetime', 'integer', [
61+
'notnull' => true,
62+
'length' => 4,
63+
'unsigned' => true,
64+
]);
65+
66+
$table->setPrimaryKey(['id']);
67+
3968
}
40-
return null;
69+
return $schema;
70+
4171
}
4272
}

apps/webhook_listeners/lib/Service/TokenService.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@
88
namespace OCA\WebhookListeners\Service;
99

1010
use OC\Authentication\Token\IProvider;
11+
use OCA\WebhookListeners\Db\TemporaryTokenMapper;
1112
use OCA\WebhookListeners\Db\WebhookListener;
13+
use OCP\AppFramework\Utility\ITimeFactory;
1214
use OCP\Authentication\Token\IToken;
1315
use OCP\Security\ISecureRandom;
1416

1517
class TokenService {
1618
public function __construct(
1719
private IProvider $tokenProvider,
1820
private ISecureRandom $random,
21+
private TemporaryTokenMapper $tokenMapper,
22+
private ITimeFactory $time,
1923
) {
2024
}
2125

@@ -77,6 +81,7 @@ private function createTemporaryToken(string $userId): string {
7781
$name = 'Ephemeral webhook authentication';
7882
$password = null;
7983
$deviceToken = $this->tokenProvider->generateToken($token, $userId, $userId, $password, $name, IToken::PERMANENT_TOKEN);
84+
$this->tokenMapper->addTemporaryToken($deviceToken->getId(), $deviceToken->getToken(), $userId, $this->time->getTime());
8085
return $token;
8186
}
8287

apps/webhook_listeners/openapi.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@
9292
"additionalProperties": {
9393
"type": "object"
9494
}
95+
},
96+
"tokenNeeded": {
97+
"type": "object",
98+
"additionalProperties": {
99+
"type": "object"
100+
}
95101
}
96102
}
97103
}
@@ -304,6 +310,15 @@
304310
"additionalProperties": {
305311
"type": "object"
306312
}
313+
},
314+
"tokenNeeded": {
315+
"type": "object",
316+
"nullable": true,
317+
"default": null,
318+
"description": "List of user ids for which to include auth tokens in the event. Has two fields: \"users\" list of user uids for which tokens are needed, \"functions\" list of functions for which tokens can be included. Possible functions: \"owner\" for the user creating the webhook, \"trigger\" for the user triggering the webhook call",
319+
"additionalProperties": {
320+
"type": "object"
321+
}
307322
}
308323
}
309324
}
@@ -703,6 +718,15 @@
703718
"additionalProperties": {
704719
"type": "object"
705720
}
721+
},
722+
"tokenNeeded": {
723+
"type": "object",
724+
"nullable": true,
725+
"default": null,
726+
"description": "List of user ids for which to include auth tokens in the event. Has two fields: \"users\" list of user uids for which tokens are needed, \"functions\" list of functions for which tokens can be included. Possible functions: \"owner\" for the user creating the webhook, \"trigger\" for the user triggering the webhook call",
727+
"additionalProperties": {
728+
"type": "object"
729+
}
706730
}
707731
}
708732
}

0 commit comments

Comments
 (0)