mirror of
https://github.com/nextcloud/server.git
synced 2026-06-20 14:09:38 -04:00
Shares using the OCM multi-protocol envelope (name multi, with the secret carried in a sibling protocol entry such as webdav) were rejected with Missing sharedSecret in protocol. Scan every protocol entry for the shared secret during validation, resolve the secret from the matching entry, and let the files provider serve the webdav entry of a multi envelope. Covers the file and folder resource types. Signed-off-by: Micke Nordin <kano@sunet.se>
264 lines
9.7 KiB
PHP
264 lines
9.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OCA\CloudFederationApi\Tests;
|
|
|
|
use OC\Federation\CloudFederationNotification;
|
|
use OCA\CloudFederationAPI\Config;
|
|
use OCA\CloudFederationAPI\Controller\RequestHandlerController;
|
|
use OCA\CloudFederationAPI\Db\FederatedInvite;
|
|
use OCA\CloudFederationAPI\Db\FederatedInviteMapper;
|
|
use OCP\AppFramework\Http;
|
|
use OCP\AppFramework\Http\JSONResponse;
|
|
use OCP\AppFramework\Utility\ITimeFactory;
|
|
use OCP\EventDispatcher\IEventDispatcher;
|
|
use OCP\Federation\ICloudFederationFactory;
|
|
use OCP\Federation\ICloudFederationProvider;
|
|
use OCP\Federation\ICloudFederationProviderManager;
|
|
use OCP\Federation\ICloudFederationShare;
|
|
use OCP\Federation\ICloudId;
|
|
use OCP\Federation\ICloudIdManager;
|
|
use OCP\IAppConfig;
|
|
use OCP\IGroupManager;
|
|
use OCP\IRequest;
|
|
use OCP\IURLGenerator;
|
|
use OCP\IUser;
|
|
use OCP\IUserManager;
|
|
use OCP\OCM\Events\OCMNotificationReceivedEvent;
|
|
use OCP\OCM\IOCMDiscoveryService;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use Psr\Log\LoggerInterface;
|
|
use Test\TestCase;
|
|
|
|
class RequestHandlerControllerTest extends TestCase {
|
|
private IRequest&MockObject $request;
|
|
private LoggerInterface&MockObject $logger;
|
|
private IUserManager&MockObject $userManager;
|
|
private IGroupManager&MockObject $groupManager;
|
|
private IURLGenerator&MockObject $urlGenerator;
|
|
private ICloudFederationProviderManager&MockObject $cloudFederationProviderManager;
|
|
private Config&MockObject $config;
|
|
private IEventDispatcher&MockObject $eventDispatcher;
|
|
private FederatedInviteMapper&MockObject $federatedInviteMapper;
|
|
private IAppConfig&MockObject $appConfig;
|
|
|
|
private ICloudFederationFactory&MockObject $cloudFederationFactory;
|
|
private ICloudIdManager&MockObject $cloudIdManager;
|
|
private IOCMDiscoveryService&MockObject $discoveryService;
|
|
private ITimeFactory&MockObject $timeFactory;
|
|
|
|
private RequestHandlerController $requestHandlerController;
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
|
|
$this->request = $this->createMock(IRequest::class);
|
|
$this->logger = $this->createMock(LoggerInterface::class);
|
|
$this->userManager = $this->createMock(IUserManager::class);
|
|
$this->groupManager = $this->createMock(IGroupManager::class);
|
|
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
|
$this->cloudFederationProviderManager = $this->createMock(ICloudFederationProviderManager::class);
|
|
$this->config = $this->createMock(Config::class);
|
|
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
|
|
$this->federatedInviteMapper = $this->createMock(FederatedInviteMapper::class);
|
|
$this->appConfig = $this->createMock(IAppConfig::class);
|
|
$this->cloudFederationFactory = $this->createMock(ICloudFederationFactory::class);
|
|
$this->cloudIdManager = $this->createMock(ICloudIdManager::class);
|
|
$this->discoveryService = $this->createMock(IOCMDiscoveryService::class);
|
|
$this->timeFactory = $this->createMock(ITimeFactory::class);
|
|
|
|
$this->requestHandlerController = new RequestHandlerController(
|
|
'cloud_federation_api',
|
|
$this->request,
|
|
$this->logger,
|
|
$this->userManager,
|
|
$this->groupManager,
|
|
$this->urlGenerator,
|
|
$this->cloudFederationProviderManager,
|
|
$this->config,
|
|
$this->eventDispatcher,
|
|
$this->federatedInviteMapper,
|
|
$this->appConfig,
|
|
$this->cloudFederationFactory,
|
|
$this->cloudIdManager,
|
|
$this->discoveryService,
|
|
$this->timeFactory,
|
|
);
|
|
}
|
|
|
|
public function testInviteAccepted(): void {
|
|
$token = 'token';
|
|
$userId = 'userId';
|
|
$invite = new FederatedInvite();
|
|
$invite->setCreatedAt(1);
|
|
$invite->setUserId($userId);
|
|
$invite->setToken($token);
|
|
|
|
$this->federatedInviteMapper->expects(self::once())
|
|
->method('findByToken')
|
|
->with($token)
|
|
->willReturn($invite);
|
|
|
|
$this->federatedInviteMapper->expects(self::once())
|
|
->method('update')
|
|
->willReturnArgument(0);
|
|
|
|
$user = $this->createMock(IUser::class);
|
|
$user->method('getUID')
|
|
->willReturn($userId);
|
|
$user->method('getEMailAddress')
|
|
->willReturn('email');
|
|
$user->method('getDisplayName')
|
|
->willReturn('displayName');
|
|
|
|
$this->userManager->expects(self::once())
|
|
->method('get')
|
|
->with($userId)
|
|
->willReturn($user);
|
|
|
|
$recipientProvider = 'http://127.0.0.1';
|
|
$recipientId = 'remote';
|
|
$recipientEmail = 'remote@example.org';
|
|
$recipientName = 'Remote Remoteson';
|
|
$response = ['userID' => $userId, 'email' => 'email', 'name' => 'displayName'];
|
|
$json = new JSONResponse($response, Http::STATUS_OK);
|
|
|
|
$this->assertEquals($json, $this->requestHandlerController->inviteAccepted($recipientProvider, $token, $recipientId, $recipientEmail, $recipientName));
|
|
}
|
|
|
|
public function testNotificationReceived(): void {
|
|
$notificationType = 'SHARE_ACCEPTED';
|
|
$resourceType = 'file';
|
|
$providerId = '1337';
|
|
$notification = ['sharedSecret' => 'secret'];
|
|
$notificationObject = new CloudFederationNotification();
|
|
|
|
$this->appConfig->method('getValueBool')->willReturn(true);
|
|
$provider = $this->createMock(ICloudFederationProvider::class);
|
|
$provider->method('notificationReceived')->willReturn([]);
|
|
$this->cloudFederationFactory->method('getCloudFederationNotification')
|
|
->willReturn($notificationObject);
|
|
$this->cloudFederationProviderManager->method('getCloudFederationProvider')
|
|
->willReturn($provider);
|
|
|
|
$this->eventDispatcher->expects(self::once())
|
|
->method('dispatchTyped')
|
|
->with(self::callback(
|
|
fn (
|
|
OCMNotificationReceivedEvent $event)
|
|
=> $event->getNotification() === $notificationObject
|
|
)
|
|
);
|
|
$response = $this->requestHandlerController->receiveNotification(
|
|
$notificationType,
|
|
$resourceType,
|
|
$providerId,
|
|
$notification
|
|
);
|
|
self::assertEquals(Http::STATUS_CREATED, $response->getStatus());
|
|
self::assertEquals([
|
|
'notificationType' => $notificationType,
|
|
'resourceType' => $resourceType,
|
|
'providerId' => $providerId,
|
|
'notification' => $notification,
|
|
], $notificationObject->getMessage());
|
|
|
|
}
|
|
|
|
public function testAddShareRejectsProtocolWithoutSharedSecret(): void {
|
|
// Disable signature verification so we reach the protocol validation.
|
|
$this->appConfig->method('getValueBool')->willReturn(true);
|
|
|
|
$protocol = [
|
|
'name' => 'multi',
|
|
'webdav' => ['requirements' => ['must-exchange-token']],
|
|
];
|
|
|
|
$result = $this->requestHandlerController->addShare(
|
|
'bob@https://bob.example.com', 'Jupyter', '', '8',
|
|
'alice@alice.example.com', 'alice', 'alice@alice.example.com', 'alice',
|
|
$protocol, 'user', 'file',
|
|
);
|
|
|
|
$this->assertInstanceOf(JSONResponse::class, $result);
|
|
$this->assertEquals(Http::STATUS_BAD_REQUEST, $result->getStatus());
|
|
$this->assertSame('Missing sharedSecret in protocol', $result->getData()['message']);
|
|
}
|
|
|
|
public function testAddShareAcceptsMultiProtocolSharedSecret(): void {
|
|
// Disable signature verification so we reach the protocol validation.
|
|
$this->appConfig->method('getValueBool')->willReturn(true);
|
|
// No supported share types: the share passes the sharedSecret gate and is
|
|
// rejected later with 501, proving the multi envelope validated.
|
|
$this->config->method('getSupportedShareTypes')->willReturn([]);
|
|
|
|
$protocol = [
|
|
'name' => 'multi',
|
|
'webdav' => ['sharedSecret' => 'XHRcgrx1X8uZELY8kxApldZtzoreH8Wj', 'requirements' => ['must-exchange-token']],
|
|
'webapp' => ['sharedSecret' => 'XHRcgrx1X8uZELY8kxApldZtzoreH8Wj', 'uri' => 'https://app.example/open'],
|
|
];
|
|
|
|
$result = $this->requestHandlerController->addShare(
|
|
'bob@https://bob.example.com', 'Jupyter', '', '8',
|
|
'alice@alice.example.com', 'alice', 'alice@alice.example.com', 'alice',
|
|
$protocol, 'user', 'file',
|
|
);
|
|
|
|
$this->assertInstanceOf(JSONResponse::class, $result);
|
|
$this->assertEquals(Http::STATUS_NOT_IMPLEMENTED, $result->getStatus());
|
|
$this->assertNotSame('Missing sharedSecret in protocol', $result->getData()['message'] ?? '');
|
|
}
|
|
|
|
public function testAddShareRoutesFolderResourceTypeMultiProtocol(): void {
|
|
// Disable signature verification so we reach the share handling.
|
|
$this->appConfig->method('getValueBool')->willReturn(true);
|
|
// The files provider is registered for both 'file' and 'folder'.
|
|
$this->config->method('getSupportedShareTypes')->with('folder')->willReturn(['user']);
|
|
|
|
$cloudId = $this->createMock(ICloudId::class);
|
|
$cloudId->method('getUser')->willReturn('bob');
|
|
$this->cloudIdManager->method('resolveCloudId')->willReturn($cloudId);
|
|
|
|
$this->userManager->method('userExists')->with('bob')->willReturn(true);
|
|
|
|
$share = $this->createMock(ICloudFederationShare::class);
|
|
$this->cloudFederationFactory->method('getCloudFederationShare')->willReturn($share);
|
|
|
|
$provider = $this->createMock(ICloudFederationProvider::class);
|
|
$provider->expects($this->once())
|
|
->method('shareReceived')
|
|
->with($share)
|
|
->willReturn('share-id-1');
|
|
$this->cloudFederationProviderManager->expects($this->once())
|
|
->method('getCloudFederationProvider')
|
|
->with('folder')
|
|
->willReturn($provider);
|
|
|
|
$recipient = $this->createMock(IUser::class);
|
|
$recipient->method('getDisplayName')->willReturn('Bob');
|
|
$recipient->method('getUID')->willReturn('bob');
|
|
$this->userManager->method('get')->with('bob')->willReturn($recipient);
|
|
|
|
$protocol = [
|
|
'name' => 'multi',
|
|
'webdav' => ['sharedSecret' => 'XHRcgrx1X8uZELY8kxApldZtzoreH8Wj', 'requirements' => ['must-exchange-token']],
|
|
'webapp' => ['sharedSecret' => 'XHRcgrx1X8uZELY8kxApldZtzoreH8Wj', 'uri' => 'https://app.example/open'],
|
|
];
|
|
|
|
$result = $this->requestHandlerController->addShare(
|
|
'bob@https://bob.example.com', 'Jupyter', '', '8',
|
|
'alice@alice.example.com', 'alice', 'alice@alice.example.com', 'alice',
|
|
$protocol, 'user', 'folder',
|
|
);
|
|
|
|
$this->assertInstanceOf(JSONResponse::class, $result);
|
|
$this->assertEquals(Http::STATUS_CREATED, $result->getStatus());
|
|
$this->assertSame('Bob', $result->getData()['recipientDisplayName']);
|
|
}
|
|
}
|