Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@

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\Events\FederatedInviteAcceptedEvent;
use OCA\CloudFederationAPI\ResponseDefinitions;
use OCA\DAV\Db\OcmTokenMapMapper;
use OCA\FederatedFileSharing\AddressHandler;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Db\DoesNotExistException;
Expand All @@ -32,6 +34,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 @@ -43,6 +46,7 @@
use OCP\Security\Signature\Exceptions\SignatoryNotFoundException;
use OCP\Security\Signature\IIncomingSignedRequest;
use OCP\Security\Signature\ISignatureManager;
use OCP\Server;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Util;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -91,7 +95,9 @@ 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<string, mixed> $protocol OCM protocol envelope. The controller only
* enforces that `protocol.name` is set; the inner shape is the provider's
* responsibility (see {@see IValidationAwareCloudFederationProvider}).
* @param string $shareType 'group' or 'user' share
* @param string $resourceType 'file', 'calendar',...
*
Expand Down Expand Up @@ -126,9 +132,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 @@ -148,6 +151,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 @@ -192,14 +196,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 @@ -490,6 +508,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 === '') {
$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
16 changes: 9 additions & 7 deletions apps/cloud_federation_api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,23 +161,25 @@
},
"protocol": {
"type": "object",
"description": "e,.g. ['name' => 'webdav', 'options' => ['username' => 'john', 'permissions' => 31]]",
"description": "Old format: ['name' => 'webdav', 'options' => ['sharedSecret' => '...', 'permissions' => '...']] or New format: ['name' => 'webdav', 'webdav' => ['uri' => '...', 'sharedSecret' => '...', 'permissions' => [...]]] or Multi format: ['name' => 'multi', 'webdav' => [...]]",
"required": [
"name",
"options"
"name"
],
"properties": {
"name": {
"type": "array",
"items": {
"type": "string"
}
"type": "string"
},
"options": {
"type": "object",
"additionalProperties": {
"type": "object"
}
},
"webdav": {
"type": "object",
"additionalProperties": {
"type": "object"
}
}
}
},
Expand Down
236 changes: 236 additions & 0 deletions apps/cloud_federation_api/tests/RequestHandlerControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OCA\CloudFederationApi\Tests;

use OC\OCM\OCMSignatoryManager;
use OCA\CloudFederationAPI\Config;
use OCA\CloudFederationAPI\Controller\RequestHandlerController;
use OCA\CloudFederationAPI\Db\FederatedInvite;
Expand All @@ -18,9 +19,15 @@
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Federation\Exceptions\BadRequestException;
use OCP\Federation\Exceptions\ProviderCouldNotAddShareException;
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\Federation\IValidationAwareCloudFederationProvider;
use OCP\IAppConfig;
use OCP\IGroupManager;
use OCP\IRequest;
Expand Down Expand Up @@ -134,4 +141,233 @@ public function testInviteAccepted(): void {

$this->assertEquals($json, $this->requestHandlerController->inviteAccepted($recipientProvider, $token, $recipientId, $recipientEmail, $recipientName));
}

public static function addShareProtocolDataProvider(): array {
return [
'legacy single-protocol' => [
['name' => 'webdav', 'options' => ['sharedSecret' => 'secret-legacy']],
],
'multi envelope, one inner protocol' => [
['name' => 'multi', 'webdav' => ['sharedSecret' => 'secret-multi-single', 'uri' => 'https://sender/webdav/']],
],
'multi envelope, multiple inner protocols' => [
[
'name' => 'multi',
'webdav' => ['sharedSecret' => 'secret-webdav', 'uri' => 'https://sender/webdav/'],
'webapp' => ['sharedSecret' => 'secret-webapp', 'uri' => 'https://sender/launch'],
],
],
'multi envelope, sharedSecret only on non-webdav entry' => [
[
'name' => 'multi',
'webapp' => ['sharedSecret' => 'secret-webapp-only', 'uri' => 'https://sender/launch'],
],
],
'multi envelope, no sharedSecret (provider-validated)' => [
['name' => 'multi', 'webdav' => ['uri' => 'https://sender/webdav/']],
],
];
}

/**
* @dataProvider addShareProtocolDataProvider
*/
public function testAddShareForwardsProtocolToProvider(array $protocol): void {
$this->appConfig->method('getValueBool')
->with('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, true)
->willReturn(true);

$cloudId = $this->createMock(ICloudId::class);
$cloudId->method('getUser')->willReturn('bob');
$this->cloudIdManager->method('resolveCloudId')->willReturn($cloudId);

$user = $this->createMock(IUser::class);
$user->method('getDisplayName')->willReturn('Bob');
$user->method('getUID')->willReturn('bob');
$this->userManager->method('userExists')->with('bob')->willReturn(true);
$this->userManager->method('get')->with('bob')->willReturn($user);

$this->config->method('getSupportedShareTypes')->with('file')->willReturn(['user']);

$capturedShare = null;
$share = $this->createMock(ICloudFederationShare::class);
$share->expects(self::once())
->method('setProtocol')
->willReturnCallback(function (array $p) use (&$capturedShare): void {
$capturedShare = $p;
});
$this->cloudFederationFactory->method('getCloudFederationShare')->willReturn($share);

$provider = $this->createMock(ICloudFederationProvider::class);
$provider->expects(self::once())->method('shareReceived')->with($share);
$this->cloudFederationProviderManager->method('getCloudFederationProvider')->with('file')->willReturn($provider);

$response = $this->requestHandlerController->addShare(
shareWith: 'bob@receiver.example.org',
name: 'doc.odt',
description: null,
providerId: 'abc',
owner: 'alice@sender.example.org',
ownerDisplayName: 'Alice',
sharedBy: 'alice@sender.example.org',
sharedByDisplayName: 'Alice',
protocol: $protocol,
shareType: 'user',
resourceType: 'file',
);

self::assertSame(Http::STATUS_CREATED, $response->getStatus());
self::assertSame($protocol, $capturedShare);
}

/**
* Returns the standard mock wiring (signing disabled, cloud-id, user, supported share types).
* Tests that exercise the controller's dispatch path call this and then plug in the share/provider.
*
* @return ICloudFederationShare&MockObject
*/
private function setUpHappyPathMocks(string $resourceType = 'file'): ICloudFederationShare {
$this->appConfig->method('getValueBool')
->with('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, true)
->willReturn(true);

$cloudId = $this->createMock(ICloudId::class);
$cloudId->method('getUser')->willReturn('bob');
$this->cloudIdManager->method('resolveCloudId')->willReturn($cloudId);

$user = $this->createMock(IUser::class);
$user->method('getDisplayName')->willReturn('Bob');
$user->method('getUID')->willReturn('bob');
$this->userManager->method('userExists')->with('bob')->willReturn(true);
$this->userManager->method('get')->with('bob')->willReturn($user);

$this->config->method('getSupportedShareTypes')->with($resourceType)->willReturn(['user']);

$share = $this->createMock(ICloudFederationShare::class);
$this->cloudFederationFactory->method('getCloudFederationShare')->willReturn($share);
return $share;
}

public function testAddShareCallsValidateShareOnValidationAwareProvider(): void {
$share = $this->setUpHappyPathMocks();

$provider = $this->createMock(IValidationAwareCloudFederationProvider::class);
$order = [];
$provider->expects(self::once())
->method('validateShare')
->with($share)
->willReturnCallback(function () use (&$order): void {
$order[] = 'validate';
});
$provider->expects(self::once())
->method('shareReceived')
->with($share)
->willReturnCallback(function () use (&$order): string {
$order[] = 'receive';
return '';
});
$this->cloudFederationProviderManager->method('getCloudFederationProvider')->with('file')->willReturn($provider);

$response = $this->requestHandlerController->addShare(
shareWith: 'bob@receiver.example.org',
name: 'doc.odt',
description: null,
providerId: 'abc',
owner: 'alice@sender.example.org',
ownerDisplayName: 'Alice',
sharedBy: 'alice@sender.example.org',
sharedByDisplayName: 'Alice',
protocol: ['name' => 'webdav', 'options' => ['sharedSecret' => 's']],
shareType: 'user',
resourceType: 'file',
);

self::assertSame(Http::STATUS_CREATED, $response->getStatus());
self::assertSame(['validate', 'receive'], $order);
}

public function testAddShareMapsBadRequestExceptionToFourHundred(): void {
$share = $this->setUpHappyPathMocks();

$provider = $this->createMock(IValidationAwareCloudFederationProvider::class);
$provider->expects(self::once())
->method('validateShare')
->willThrowException(new BadRequestException(['protocol.webdav.sharedSecret']));
$provider->expects(self::never())->method('shareReceived');
$this->cloudFederationProviderManager->method('getCloudFederationProvider')->with('file')->willReturn($provider);

$response = $this->requestHandlerController->addShare(
shareWith: 'bob@receiver.example.org',
name: 'doc.odt',
description: null,
providerId: 'abc',
owner: 'alice@sender.example.org',
ownerDisplayName: 'Alice',
sharedBy: 'alice@sender.example.org',
sharedByDisplayName: 'Alice',
protocol: ['name' => 'webdav'],
shareType: 'user',
resourceType: 'file',
);

self::assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
$body = $response->getData();
self::assertSame('RESOURCE_NOT_FOUND', $body['message']);
self::assertSame([
['name' => 'protocol.webdav.sharedSecret', 'message' => 'NOT_FOUND'],
], $body['validationErrors']);
}

public function testAddShareSurfacesProviderExceptionCodeAsHttpStatus(): void {
$this->setUpHappyPathMocks();

$provider = $this->createMock(ICloudFederationProvider::class);
$provider->expects(self::once())
->method('shareReceived')
->willThrowException(new ProviderCouldNotAddShareException('Server does not support federated cloud sharing', '', Http::STATUS_SERVICE_UNAVAILABLE));
$this->cloudFederationProviderManager->method('getCloudFederationProvider')->with('file')->willReturn($provider);

$response = $this->requestHandlerController->addShare(
shareWith: 'bob@receiver.example.org',
name: 'doc.odt',
description: null,
providerId: 'abc',
owner: 'alice@sender.example.org',
ownerDisplayName: 'Alice',
sharedBy: 'alice@sender.example.org',
sharedByDisplayName: 'Alice',
protocol: ['name' => 'webdav', 'options' => ['sharedSecret' => 's']],
shareType: 'user',
resourceType: 'file',
);

self::assertSame(Http::STATUS_SERVICE_UNAVAILABLE, $response->getStatus());
self::assertSame('Server does not support federated cloud sharing', $response->getData()['message']);
}

public function testAddShareRejectsProtocolMissingName(): void {
$this->appConfig->method('getValueBool')
->with('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, true)
->willReturn(true);

$this->cloudFederationProviderManager->expects(self::never())
->method('getCloudFederationProvider');

$response = $this->requestHandlerController->addShare(
shareWith: 'bob@receiver.example.org',
name: 'doc.odt',
description: null,
providerId: 'abc',
owner: 'alice@sender.example.org',
ownerDisplayName: 'Alice',
sharedBy: 'alice@sender.example.org',
sharedByDisplayName: 'Alice',
protocol: ['webdav' => ['sharedSecret' => 'secret', 'uri' => 'https://sender/webdav/']],
shareType: 'user',
resourceType: 'file',
);

self::assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus());
self::assertSame('Missing arguments', $response->getData()['message']);
}
}
Loading