Skip to content
Open
4 changes: 4 additions & 0 deletions apps/cloud_federation_api/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@
<dependencies>
<nextcloud min-version="35" max-version="35"/>
</dependencies>

<background-jobs>
<job>OCA\CloudFederationAPI\BackgroundJob\CleanupExpiredOcmTokensJob</job>
</background-jobs>
</info>
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'OCA\\CloudFederationAPI\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
'OCA\\CloudFederationAPI\\BackgroundJob\\CleanupExpiredOcmTokensJob' => $baseDir . '/../lib/BackgroundJob/CleanupExpiredOcmTokensJob.php',
'OCA\\CloudFederationAPI\\Capabilities' => $baseDir . '/../lib/Capabilities.php',
'OCA\\CloudFederationAPI\\Config' => $baseDir . '/../lib/Config.php',
'OCA\\CloudFederationAPI\\Controller\\OCMRequestController' => $baseDir . '/../lib/Controller/OCMRequestController.php',
'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => $baseDir . '/../lib/Controller/RequestHandlerController.php',
'OCA\\CloudFederationAPI\\Controller\\TokenController' => $baseDir . '/../lib/Controller/TokenController.php',
'OCA\\CloudFederationAPI\\Db\\FederatedInvite' => $baseDir . '/../lib/Db/FederatedInvite.php',
'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => $baseDir . '/../lib/Db/FederatedInviteMapper.php',
'OCA\\CloudFederationAPI\\Db\\OcmTokenMap' => $baseDir . '/../lib/Db/OcmTokenMap.php',
'OCA\\CloudFederationAPI\\Db\\OcmTokenMapMapper' => $baseDir . '/../lib/Db/OcmTokenMapMapper.php',
'OCA\\CloudFederationAPI\\Events\\FederatedInviteAcceptedEvent' => $baseDir . '/../lib/Events/FederatedInviteAcceptedEvent.php',
'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => $baseDir . '/../lib/Migration/Version1016Date202502262004.php',
'OCA\\CloudFederationAPI\\Migration\\Version1017Date20260306120000' => $baseDir . '/../lib/Migration/Version1017Date20260306120000.php',
'OCA\\CloudFederationAPI\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
);
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,19 @@ class ComposerStaticInitCloudFederationAPI
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'OCA\\CloudFederationAPI\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
'OCA\\CloudFederationAPI\\BackgroundJob\\CleanupExpiredOcmTokensJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupExpiredOcmTokensJob.php',
'OCA\\CloudFederationAPI\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php',
'OCA\\CloudFederationAPI\\Config' => __DIR__ . '/..' . '/../lib/Config.php',
'OCA\\CloudFederationAPI\\Controller\\OCMRequestController' => __DIR__ . '/..' . '/../lib/Controller/OCMRequestController.php',
'OCA\\CloudFederationAPI\\Controller\\RequestHandlerController' => __DIR__ . '/..' . '/../lib/Controller/RequestHandlerController.php',
'OCA\\CloudFederationAPI\\Controller\\TokenController' => __DIR__ . '/..' . '/../lib/Controller/TokenController.php',
'OCA\\CloudFederationAPI\\Db\\FederatedInvite' => __DIR__ . '/..' . '/../lib/Db/FederatedInvite.php',
'OCA\\CloudFederationAPI\\Db\\FederatedInviteMapper' => __DIR__ . '/..' . '/../lib/Db/FederatedInviteMapper.php',
'OCA\\CloudFederationAPI\\Db\\OcmTokenMap' => __DIR__ . '/..' . '/../lib/Db/OcmTokenMap.php',
'OCA\\CloudFederationAPI\\Db\\OcmTokenMapMapper' => __DIR__ . '/..' . '/../lib/Db/OcmTokenMapMapper.php',
'OCA\\CloudFederationAPI\\Events\\FederatedInviteAcceptedEvent' => __DIR__ . '/..' . '/../lib/Events/FederatedInviteAcceptedEvent.php',
'OCA\\CloudFederationAPI\\Migration\\Version1016Date202502262004' => __DIR__ . '/..' . '/../lib/Migration/Version1016Date202502262004.php',
'OCA\\CloudFederationAPI\\Migration\\Version1017Date20260306120000' => __DIR__ . '/..' . '/../lib/Migration/Version1017Date20260306120000.php',
'OCA\\CloudFederationAPI\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

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

namespace OCA\CloudFederationAPI\BackgroundJob;

use OCA\CloudFederationAPI\Db\OcmTokenMapMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;

/**
* Periodically purge expired OCM access token mappings from ocm_token_map.
*
* The corresponding oc_authtoken entries (TEMPORARY_TOKEN with an expires
* timestamp) are cleaned up by Nextcloud's own token expiry jobs.
*/
class CleanupExpiredOcmTokensJob extends TimedJob {
public function __construct(
ITimeFactory $timeFactory,
private readonly OcmTokenMapMapper $mapper,
) {
parent::__construct($timeFactory);

$this->setInterval(6 * 60 * 60); // run every 6 hours
$this->setTimeSensitivity(self::TIME_INSENSITIVE);
}

#[\Override]
protected function run($argument): void {
$this->mapper->deleteExpired($this->time->getTime());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

namespace OCA\CloudFederationAPI\Controller;

use OC\Authentication\Token\PublicKeyTokenProvider;
use OC\OCM\OCMSignatoryManager;
use OCA\CloudFederationAPI\Config;
use OCA\CloudFederationAPI\Db\FederatedInviteMapper;
use OCA\CloudFederationAPI\Db\OcmTokenMapMapper;
use OCA\CloudFederationAPI\Events\FederatedInviteAcceptedEvent;
use OCA\CloudFederationAPI\ResponseDefinitions;
use OCP\AppFramework\Controller;
Expand All @@ -31,6 +33,7 @@
use OCP\Federation\ICloudFederationProviderManager;
use OCP\Federation\ICloudIdManager;
use OCP\Federation\ISignedCloudFederationProvider;
use OCP\Federation\IValidationAwareCloudFederationProvider;
use OCP\IAppConfig;
use OCP\IGroupManager;
use OCP\IRequest;
Expand All @@ -39,6 +42,7 @@
use OCP\OCM\IOCMDiscoveryService;
use OCP\Security\Signature\Exceptions\IncomingRequestException;
use OCP\Security\Signature\IIncomingSignedRequest;
use OCP\Server;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Util;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -85,7 +89,7 @@ public function __construct(
* @param string|null $ownerDisplayName Display name of the user who shared the item
* @param string|null $sharedBy Provider specific UID of the user who shared the resource
* @param string|null $sharedByDisplayName Display name of the user who shared the resource
* @param array{name: list<string>, options: array<string, mixed>} $protocol e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]]
* @param array{name: string, options?: array<string, mixed>, webdav?: array<string, mixed>} $protocol Old format: ['name' => 'webdav', 'options' => ['sharedSecret' => '...', 'permissions' => '...']] or New format: ['name' => 'webdav', 'webdav' => ['uri' => '...', 'sharedSecret' => '...', 'permissions' => [...]]]
* @param string $shareType 'group' or 'user' share
* @param string $resourceType 'file', 'calendar',...
*
Expand All @@ -94,6 +98,10 @@ public function __construct(
* 201: The notification was successfully received. The display name of the recipient might be returned in the body
* 400: Bad request due to invalid parameters, e.g. when `shareWith` is not found or required properties are missing
* 501: Share type or the resource type is not supported
*
* @psalm-suppress InvalidReturnType
* @psalm-suppress InvalidReturnStatement
* @psalm-suppress LessSpecificReturnStatement
*/
#[PublicPage]
#[NoCSRFRequired]
Expand All @@ -120,9 +128,6 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $
|| $shareType === null
|| !is_array($protocol)
|| !isset($protocol['name'])
|| !isset($protocol['options'])
|| !is_array($protocol['options'])
|| !isset($protocol['options']['sharedSecret'])
) {
return new JSONResponse(
[
Expand All @@ -133,6 +138,20 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $
);
}

$protocolName = $protocol['name'];
$hasOldFormat = isset($protocol['options']) && is_array($protocol['options']) && isset($protocol['options']['sharedSecret']);
$hasNewFormat = isset($protocol[$protocolName]) && is_array($protocol[$protocolName]) && isset($protocol[$protocolName]['sharedSecret']);

if (!$hasOldFormat && !$hasNewFormat) {
return new JSONResponse(
[
'message' => 'Missing sharedSecret in protocol',
'validationErrors' => [],
],
Http::STATUS_BAD_REQUEST
);
}

$supportedShareTypes = $this->config->getSupportedShareTypes($resourceType);
if (!in_array($shareType, $supportedShareTypes)) {
return new JSONResponse(
Expand All @@ -142,6 +161,7 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $
}

$cloudId = $this->cloudIdManager->resolveCloudId($shareWith);
$shareWithCloudId = $shareWith; // preserve full cloud ID for factory capability discovery
$shareWith = $cloudId->getUser();

if ($shareType === 'user') {
Expand Down Expand Up @@ -186,14 +206,28 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $

try {
$provider = $this->cloudFederationProviderManager->getCloudFederationProvider($resourceType);
$share = $this->factory->getCloudFederationShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, '', $shareType, $resourceType);
// Pass the original cloud ID so the factory can discover capabilities without warning.
// Then reset shareWith to the local username that shareReceived() needs for user lookup.
$share = $this->factory->getCloudFederationShare($shareWithCloudId, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, '', $shareType, $resourceType);
$share->setShareWith($shareWith);
$share->setProtocol($protocol);
if ($provider instanceof IValidationAwareCloudFederationProvider) {
$provider->validateShare($share);
}
$provider->shareReceived($share);
} catch (ProviderDoesNotExistsException|ProviderCouldNotAddShareException $e) {
} catch (BadRequestException $e) {
return new JSONResponse($e->getReturnMessage(), Http::STATUS_BAD_REQUEST);
} catch (ProviderDoesNotExistsException $e) {
return new JSONResponse(
['message' => $e->getMessage()],
Http::STATUS_NOT_IMPLEMENTED
);
} catch (ProviderCouldNotAddShareException $e) {
$status = $e->getCode() ?: Http::STATUS_NOT_IMPLEMENTED;
return new JSONResponse(
['message' => $e->getMessage()],
$status
);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return new JSONResponse(
Expand Down Expand Up @@ -477,6 +511,12 @@ private function confirmNotificationIdentity(
$provider = $this->cloudFederationProviderManager->getCloudFederationProvider($resourceType);
if ($provider instanceof ISignedCloudFederationProvider || $provider instanceof \NCU\Federation\ISignedCloudFederationProvider) {
$identity = $provider->getFederationIdFromSharedSecret($sharedSecret, $notification);
if ($identity === '') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to know which type of auth it is, instead of guessing, maybe with token-exchange capability

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The request we are receiving may be for a share that does not correspond with the remote's capabilities, for example because it is old

$tokenProvider = Server::get(PublicKeyTokenProvider::class);
$accessTokenDb = $tokenProvider->getToken($sharedSecret);
$mapping = Server::get(OcmTokenMapMapper::class)->getByAccessTokenId($accessTokenDb->getId());
$identity = $provider->getFederationIdFromSharedSecret($mapping->getRefreshToken(), $notification);
}
} else {
$this->logger->debug('cloud federation provider {provider} does not implements ISignedCloudFederationProvider', ['provider' => $provider::class]);
return;
Expand Down
Loading
Loading