mirror of
https://github.com/nextcloud/server.git
synced 2026-05-28 04:32:30 -04:00
Merge pull request #55790 from nextcloud/feat/webhook-tokens
Feat(webhook_listeners): add auth tokens to webhook call
This commit is contained in:
commit
4dffa1a46a
15 changed files with 561 additions and 1 deletions
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -9,16 +9,21 @@ return array(
|
|||
'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',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ namespace OCA\WebhookListeners\BackgroundJobs;
|
|||
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;
|
||||
|
|
@ -30,6 +31,7 @@ class WebhookCall extends QueuedJob {
|
|||
private WebhookListenerMapper $mapper,
|
||||
private LoggerInterface $logger,
|
||||
private IAppManager $appManager,
|
||||
private TokenService $tokenService,
|
||||
ITimeFactory $timeFactory,
|
||||
) {
|
||||
parent::__construct($timeFactory);
|
||||
|
|
@ -42,6 +44,9 @@ class WebhookCall extends QueuedJob {
|
|||
[$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() ?? [],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -112,6 +112,11 @@ class WebhooksController extends OCSController {
|
|||
* @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{}>
|
||||
*
|
||||
|
|
@ -134,6 +139,7 @@ class WebhooksController extends OCSController {
|
|||
?string $authMethod,
|
||||
#[\SensitiveParameter]
|
||||
?array $authData,
|
||||
?array $tokenNeeded = null,
|
||||
): DataResponse {
|
||||
$appId = null;
|
||||
if ($this->session->get('app_api') === true) {
|
||||
|
|
@ -156,6 +162,7 @@ class WebhooksController extends OCSController {
|
|||
$headers,
|
||||
$authMethod,
|
||||
$authData,
|
||||
$tokenNeeded,
|
||||
);
|
||||
return new DataResponse($webhookListener->jsonSerialize());
|
||||
} catch (\UnexpectedValueException $e) {
|
||||
|
|
@ -180,6 +187,11 @@ class WebhooksController extends OCSController {
|
|||
* @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{}>
|
||||
*
|
||||
|
|
@ -203,6 +215,7 @@ class WebhooksController extends OCSController {
|
|||
?string $authMethod,
|
||||
#[\SensitiveParameter]
|
||||
?array $authData,
|
||||
?array $tokenNeeded = null,
|
||||
): DataResponse {
|
||||
$appId = null;
|
||||
if ($this->session->get('app_api') === true) {
|
||||
|
|
@ -226,6 +239,7 @@ class WebhooksController extends OCSController {
|
|||
$headers,
|
||||
$authMethod,
|
||||
$authData,
|
||||
$tokenNeeded,
|
||||
);
|
||||
return new DataResponse($webhookListener->jsonSerialize());
|
||||
} catch (\UnexpectedValueException $e) {
|
||||
|
|
|
|||
54
apps/webhook_listeners/lib/Db/EphemeralToken.php
Normal file
54
apps/webhook_listeners/lib/Db/EphemeralToken.php
Normal file
|
|
@ -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
apps/webhook_listeners/lib/Db/EphemeralTokenMapper.php
Normal file
121
apps/webhook_listeners/lib/Db/EphemeralTokenMapper.php
Normal file
|
|
@ -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]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ use OCP\Server;
|
|||
* @method ?string getAuthData()
|
||||
* @method void setAuthData(?string $data)
|
||||
* @method string getAuthMethod()
|
||||
* @method ?array getTokenNeeded()
|
||||
* @psalm-suppress PropertyNotSetInConstructor
|
||||
*/
|
||||
class WebhookListener extends Entity implements \JsonSerializable {
|
||||
|
|
@ -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,
|
||||
) {
|
||||
|
|
@ -103,6 +111,7 @@ class WebhookListener extends Entity implements \JsonSerializable {
|
|||
$this->addType('headers', 'json');
|
||||
$this->addType('authMethod', 'string');
|
||||
$this->addType('authData', 'string');
|
||||
$this->addType('tokenNeeded', 'json');
|
||||
}
|
||||
|
||||
public function getAuthMethodEnum(): AuthMethod {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ class WebhookListenerMapper extends QBMapper {
|
|||
AuthMethod $authMethod,
|
||||
#[\SensitiveParameter]
|
||||
?array $authData,
|
||||
?array $tokenNeeded = [],
|
||||
): WebhookListener {
|
||||
/* Remove any superfluous antislash */
|
||||
$event = ltrim($event, '\\');
|
||||
|
|
@ -99,6 +100,7 @@ class WebhookListenerMapper extends QBMapper {
|
|||
'userIdFilter' => $userIdFilter ?? '',
|
||||
'headers' => $headers,
|
||||
'authMethod' => $authMethod->value,
|
||||
'tokenNeeded' => $tokenNeeded ?? [],
|
||||
]
|
||||
);
|
||||
$webhookListener->setAuthDataClear($authData);
|
||||
|
|
@ -122,6 +124,7 @@ class WebhookListenerMapper extends QBMapper {
|
|||
AuthMethod $authMethod,
|
||||
#[\SensitiveParameter]
|
||||
?array $authData,
|
||||
?array $tokenNeeded = [],
|
||||
): WebhookListener {
|
||||
/* Remove any superfluous antislash */
|
||||
$event = ltrim($event, '\\');
|
||||
|
|
@ -140,6 +143,7 @@ class WebhookListenerMapper extends QBMapper {
|
|||
'userIdFilter' => $userIdFilter ?? '',
|
||||
'headers' => $headers,
|
||||
'authMethod' => $authMethod->value,
|
||||
'tokenNeeded' => $tokenNeeded ?? [],
|
||||
]
|
||||
);
|
||||
$webhookListener->setAuthDataClear($authData);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\WebhookListeners\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCA\WebhookListeners\Db\EphemeralTokenMapper;
|
||||
use OCA\WebhookListeners\Db\WebhookListenerMapper;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
class Version1500Date20251007130000 extends SimpleMigrationStep {
|
||||
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure(): ISchemaWrapper $schemaClosure
|
||||
* @param array $options
|
||||
* @return null|ISchemaWrapper
|
||||
*/
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
|
||||
$schema = $schemaClosure();
|
||||
$schemaHasChanged = false;
|
||||
|
||||
if ($schema->hasTable(WebhookListenerMapper::TABLE_NAME)) {
|
||||
$table = $schema->getTable(WebhookListenerMapper::TABLE_NAME);
|
||||
if (!$table->hasColumn('token_needed')) {
|
||||
$schemaHasChanged = true;
|
||||
$table->addColumn('token_needed', Types::TEXT, [
|
||||
'notnull' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$schema->hasTable(EphemeralTokenMapper::TABLE_NAME)) {
|
||||
$schemaHasChanged = true;
|
||||
$table = $schema->createTable(EphemeralTokenMapper::TABLE_NAME);
|
||||
$table->addColumn('id', Types::BIGINT, [
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->addColumn('token_id', Types::BIGINT, [
|
||||
'notnull' => true,
|
||||
'length' => 4,
|
||||
'unsigned' => true,
|
||||
]);
|
||||
$table->addColumn('user_id', Types::STRING, [
|
||||
'notnull' => false,
|
||||
'length' => 64,
|
||||
]);
|
||||
$table->addColumn('created_at', Types::BIGINT, [
|
||||
'notnull' => true,
|
||||
'length' => 4,
|
||||
'unsigned' => true,
|
||||
]);
|
||||
$table->setPrimaryKey(['id']);
|
||||
}
|
||||
return $schemaHasChanged ? $schema : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ namespace OCA\WebhookListeners;
|
|||
* headers?: array<string,string>,
|
||||
* authMethod: string,
|
||||
* authData?: array<string,mixed>,
|
||||
* tokenNeeded?: ?array{user_ids?:array<string,string>,user_roles?:array<string,string>},
|
||||
* }
|
||||
*/
|
||||
class ResponseDefinitions {
|
||||
|
|
|
|||
122
apps/webhook_listeners/lib/Service/TokenService.php
Normal file
122
apps/webhook_listeners/lib/Service/TokenService.php
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\WebhookListeners\Service;
|
||||
|
||||
use OC\Authentication\Token\IProvider;
|
||||
use OCA\WebhookListeners\Db\EphemeralTokenMapper;
|
||||
use OCA\WebhookListeners\Db\WebhookListener;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\Authentication\Token\IToken;
|
||||
use OCP\IUserManager;
|
||||
use OCP\L10N\IFactory;
|
||||
use OCP\Security\ISecureRandom;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class TokenService {
|
||||
public function __construct(
|
||||
private IProvider $tokenProvider,
|
||||
private ISecureRandom $random,
|
||||
private EphemeralTokenMapper $tokenMapper,
|
||||
private LoggerInterface $logger,
|
||||
private ITimeFactory $time,
|
||||
private IFactory $l10nFactory,
|
||||
private IUserManager $userManager,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* creates an array which includes two arrays of tokens: 'user_ids' and 'user_roles'
|
||||
* The array ['user_ids' => ['jane', 'bob'], 'user_roles' => ['owner', 'trigger']]
|
||||
* as requested tokens in the registered webhook produces a result like
|
||||
* ['user_ids' => [['jane' => 'abcdtokenabcd1'], ['bob','=> 'abcdtokenabcd2']], 'user_roles' => ['owner' => ['admin' => 'abcdtokenabcd3'], 'trigger' => ['user1' => 'abcdtokenabcd4']]]
|
||||
* Created auth tokens are valid for 1 hour.
|
||||
*
|
||||
* @param WebhookListener $webhookListener
|
||||
* @param ?string $triggerUserId the user that triggered the webhook call
|
||||
* @return array{user_ids?:array<string,string>,user_roles?:array{owner?:array<string,string>,trigger?:array<string,string>}}
|
||||
*/
|
||||
public function getTokens(WebhookListener $webhookListener, ?string $triggerUserId): array {
|
||||
$tokens = [
|
||||
'user_ids' => [],
|
||||
'user_roles' => [],
|
||||
];
|
||||
$tokenNeeded = $webhookListener->getTokenNeeded();
|
||||
if (isset($tokenNeeded['user_ids'])) {
|
||||
foreach ($tokenNeeded['user_ids'] as $userId) {
|
||||
try {
|
||||
$tokens['user_ids'][$userId] = $this->createEphemeralToken($userId);
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Webhook token creation for user ' . $userId . ' failed: ' . $e->getMessage(), ['exception' => $e]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if (isset($tokenNeeded['user_roles'])) {
|
||||
foreach ($tokenNeeded['user_roles'] as $user_role) {
|
||||
switch ($user_role) {
|
||||
case 'owner':
|
||||
// token for the person who created the flow
|
||||
$ownerId = $webhookListener->getUserId();
|
||||
if (is_null($ownerId)) { // no owner uid available
|
||||
break;
|
||||
}
|
||||
$tokens['user_roles']['owner'] = [
|
||||
$ownerId => $this->createEphemeralToken($ownerId)
|
||||
];
|
||||
break;
|
||||
case 'trigger':
|
||||
// token for the person who triggered the webhook
|
||||
if (is_null($triggerUserId)) { // no trigger uid available
|
||||
break;
|
||||
}
|
||||
$tokens['user_roles']['trigger'] = [
|
||||
$triggerUserId => $this->createEphemeralToken($triggerUserId)
|
||||
];
|
||||
break;
|
||||
default:
|
||||
$this->logger->error('Webhook token creation for user role ' . $user_role . ' not defined. ', ['Not defined' => $user_role]);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return $tokens;
|
||||
}
|
||||
private function createEphemeralToken(string $userId): string {
|
||||
$token = $this->generateRandomDeviceToken();
|
||||
|
||||
// we need the user`s language to have the token name showing up in the session list in the correct language
|
||||
$user = $this->userManager->get($userId);
|
||||
$lang = $this->l10nFactory->getUserLanguage($user);
|
||||
$l = $this->l10nFactory->get('webhook_listeners', $lang);
|
||||
$name = $l->t('Ephemeral webhook authentication');
|
||||
$password = null;
|
||||
$deviceToken = $this->tokenProvider->generateToken(
|
||||
$token,
|
||||
$userId,
|
||||
$userId,
|
||||
$password,
|
||||
$name,
|
||||
IToken::PERMANENT_TOKEN);
|
||||
|
||||
$this->tokenMapper->addEphemeralToken(
|
||||
$deviceToken->getId(),
|
||||
$userId,
|
||||
$this->time->getTime());
|
||||
return $token;
|
||||
}
|
||||
|
||||
private function generateRandomDeviceToken(): string {
|
||||
$groups = [];
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$groups[] = $this->random->generate(5, ISecureRandom::CHAR_HUMAN_READABLE);
|
||||
}
|
||||
return implode('-', $groups);
|
||||
}
|
||||
}
|
||||
|
|
@ -92,6 +92,24 @@
|
|||
"additionalProperties": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"tokenNeeded": {
|
||||
"type": "object",
|
||||
"nullable": true,
|
||||
"properties": {
|
||||
"user_ids": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"user_roles": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -304,6 +322,26 @@
|
|||
"additionalProperties": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"tokenNeeded": {
|
||||
"type": "object",
|
||||
"nullable": true,
|
||||
"default": null,
|
||||
"description": "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.",
|
||||
"properties": {
|
||||
"user_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"user_roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -703,6 +741,26 @@
|
|||
"additionalProperties": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"tokenNeeded": {
|
||||
"type": "object",
|
||||
"nullable": true,
|
||||
"default": null,
|
||||
"description": "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.",
|
||||
"properties": {
|
||||
"user_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"user_roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
58
openapi.json
58
openapi.json
|
|
@ -4687,6 +4687,24 @@
|
|||
"additionalProperties": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"tokenNeeded": {
|
||||
"type": "object",
|
||||
"nullable": true,
|
||||
"properties": {
|
||||
"user_ids": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"user_roles": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38254,6 +38272,26 @@
|
|||
"additionalProperties": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"tokenNeeded": {
|
||||
"type": "object",
|
||||
"nullable": true,
|
||||
"default": null,
|
||||
"description": "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.",
|
||||
"properties": {
|
||||
"user_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"user_roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38653,6 +38691,26 @@
|
|||
"additionalProperties": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"tokenNeeded": {
|
||||
"type": "object",
|
||||
"nullable": true,
|
||||
"default": null,
|
||||
"description": "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.",
|
||||
"properties": {
|
||||
"user_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"user_roles": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue