diff --git a/apps/dav/lib/CalDAV/Federation/CalendarFederationConfig.php b/apps/dav/lib/CalDAV/Federation/CalendarFederationConfig.php index b7293bad9fa9f..cfbbbe029e6de 100644 --- a/apps/dav/lib/CalDAV/Federation/CalendarFederationConfig.php +++ b/apps/dav/lib/CalDAV/Federation/CalendarFederationConfig.php @@ -9,15 +9,36 @@ namespace OCA\DAV\CalDAV\Federation; -use OCP\AppFramework\Services\IAppConfig; +use OCP\IAppConfig; class CalendarFederationConfig { public function __construct( private readonly IAppConfig $appConfig, + private \OCP\GlobalScale\IConfig $gsConfig, ) { } public function isFederationEnabled(): bool { - return $this->appConfig->getAppValueBool('enableCalendarFederation', true); + return $this->appConfig->getValueBool('dav', 'enableCalendarFederation', true); + } + + /** + * Check if users are allowed to create federated shares + */ + public function isOutgoingServer2serverShareEnabled(): bool { + if ($this->gsConfig->onlyInternalFederation()) { + return false; + } + return $this->appConfig->getValueBool('files_sharing', 'outgoing_server2server_share_enabled', true); + } + + /** + * Check if users are allowed to receive federated shares + */ + public function isIncomingServer2serverShareEnabled(): bool { + if ($this->gsConfig->onlyInternalFederation()) { + return false; + } + return $this->appConfig->getValueBool('files_sharing', 'incoming_server2server_share_enabled', true); } } diff --git a/apps/dav/lib/CalDAV/Federation/CalendarFederationProvider.php b/apps/dav/lib/CalDAV/Federation/CalendarFederationProvider.php index 348852fb9310d..3a13c5886d0dc 100644 --- a/apps/dav/lib/CalDAV/Federation/CalendarFederationProvider.php +++ b/apps/dav/lib/CalDAV/Federation/CalendarFederationProvider.php @@ -55,6 +55,15 @@ public function shareReceived(ICloudFederationShare $share): string { ); } + if (!$this->calendarFederationConfig->isIncomingServer2serverShareEnabled()) { + $this->logger->debug('Received a federated calendar share which is not allowed on this instance'); + throw new ProviderCouldNotAddShareException( + 'Instance does not support receiving federated calendar shares', + '', + Http::STATUS_SERVICE_UNAVAILABLE, + ); + } + if (!in_array($share->getShareType(), $this->getSupportedShareTypes(), true)) { $this->logger->debug('Received a federation invite for invalid share type'); throw new ProviderCouldNotAddShareException( diff --git a/apps/dav/lib/CalDAV/Federation/FederationSharingService.php b/apps/dav/lib/CalDAV/Federation/FederationSharingService.php index 57df75b01a077..3ac9148b2a9c5 100644 --- a/apps/dav/lib/CalDAV/Federation/FederationSharingService.php +++ b/apps/dav/lib/CalDAV/Federation/FederationSharingService.php @@ -34,6 +34,7 @@ public function __construct( private readonly LoggerInterface $logger, private readonly ISecureRandom $random, private readonly SharingMapper $sharingMapper, + private readonly CalendarFederationConfig $config, ) { } @@ -70,6 +71,14 @@ private function decodeRemoteUserPrincipal(string $principal): ?string { public function shareWith(IShareable $shareable, string $principal, int $access): void { $baseError = 'Failed to create federated calendar share: '; + if (!$this->config->isOutgoingServer2serverShareEnabled()) { + $this->logger->error('cannot share with remote user because federated sharing is disabled on this instance', [ + 'shareable' => $shareable->getName(), + 'encodedShareWith' => $principal, + ]); + return; + } + // 1. Validate share data $shareWith = $this->decodeRemoteUserPrincipal($principal); if ($shareWith === null) { diff --git a/apps/dav/tests/unit/CalDAV/Federation/CalendarFederationConfigTest.php b/apps/dav/tests/unit/CalDAV/Federation/CalendarFederationConfigTest.php index b8b37eea16748..845a4ade77cc0 100644 --- a/apps/dav/tests/unit/CalDAV/Federation/CalendarFederationConfigTest.php +++ b/apps/dav/tests/unit/CalDAV/Federation/CalendarFederationConfigTest.php @@ -10,7 +10,8 @@ namespace OCA\DAV\Tests\unit\CalDAV\Federation; use OCA\DAV\CalDAV\Federation\CalendarFederationConfig; -use OCP\AppFramework\Services\IAppConfig; +use OCP\GlobalScale\IConfig; +use OCP\IAppConfig; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -19,14 +20,17 @@ class CalendarFederationConfigTest extends TestCase { private CalendarFederationConfig $config; private IAppConfig&MockObject $appConfig; + private IConfig&MockObject $gsConfig; protected function setUp(): void { parent::setUp(); $this->appConfig = $this->createMock(IAppConfig::class); + $this->gsConfig = $this->createMock(IConfig::class); $this->config = new CalendarFederationConfig( $this->appConfig, + $this->gsConfig, ); } @@ -40,10 +44,74 @@ public static function provideIsFederationEnabledData(): array { #[DataProvider(methodName: 'provideIsFederationEnabledData')] public function testIsFederationEnabled(bool $configValue): void { $this->appConfig->expects(self::once()) - ->method('getAppValueBool') - ->with('enableCalendarFederation', true) + ->method('getValueBool') + ->with('dav', 'enableCalendarFederation', true) ->willReturn($configValue); $this->assertEquals($configValue, $this->config->isFederationEnabled()); } + + public static function provideIsOutgoingServer2serverShareEnabledData(): array { + return [ + [false, false, false], + [false, true, true], + [true, false, false], + [true, false, false], + ]; + } + + #[DataProvider(methodName: 'provideIsOutgoingServer2serverShareEnabledData')] + public function testIsOutgoingServer2serverShareEnabled( + bool $globalScaleEnabled, + bool $expected, + bool $configValue, + ): void { + $this->gsConfig->expects(self::once()) + ->method('onlyInternalFederation') + ->willReturn($globalScaleEnabled); + + if (!$globalScaleEnabled) { + $this->appConfig->expects(self::once()) + ->method('getValueBool') + ->with('files_sharing', 'outgoing_server2server_share_enabled', true) + ->willReturn($configValue); + } else { + $this->appConfig->expects(self::never()) + ->method('getValueBool'); + } + + $this->assertEquals($expected, $this->config->isOutgoingServer2serverShareEnabled()); + } + + public static function provideIsIncomingServer2serverShareEnabledData(): array { + return [ + [false, false, false], + [false, true, true], + [true, false, false], + [true, false, true], + ]; + } + + #[DataProvider(methodName: 'provideIsIncomingServer2serverShareEnabledData')] + public function testIsIncomingServer2serverShareEnabled( + bool $globalScaleEnabled, + bool $expected, + bool $configValue, + ): void { + $this->gsConfig->expects(self::once()) + ->method('onlyInternalFederation') + ->willReturn($globalScaleEnabled); + + if (!$globalScaleEnabled) { + $this->appConfig->expects(self::once()) + ->method('getValueBool') + ->with('files_sharing', 'incoming_server2server_share_enabled', true) + ->willReturn($configValue); + } else { + $this->appConfig->expects(self::never()) + ->method('getValueBool'); + } + + $this->assertEquals($expected, $this->config->isIncomingServer2serverShareEnabled()); + } } diff --git a/apps/dav/tests/unit/CalDAV/Federation/CalendarFederationProviderTest.php b/apps/dav/tests/unit/CalDAV/Federation/CalendarFederationProviderTest.php index 6e6577b24fa13..24ffec58343e6 100644 --- a/apps/dav/tests/unit/CalDAV/Federation/CalendarFederationProviderTest.php +++ b/apps/dav/tests/unit/CalDAV/Federation/CalendarFederationProviderTest.php @@ -91,6 +91,10 @@ public function testShareReceived(): void { ->method('isFederationEnabled') ->willReturn(true); + $this->calendarFederationConfig->expects(self::once()) + ->method('isIncomingServer2serverShareEnabled') + ->willReturn(true); + $this->federatedCalendarMapper->expects(self::once()) ->method('findByUri') ->with( @@ -150,6 +154,10 @@ public function testShareReceivedWithExistingCalendar(): void { ->method('isFederationEnabled') ->willReturn(true); + $this->calendarFederationConfig->expects(self::once()) + ->method('isIncomingServer2serverShareEnabled') + ->willReturn(true); + $existingCalendar = new FederatedCalendarEntity(); $existingCalendar->setId(10); $existingCalendar->setPrincipaluri('principals/users/sharee1'); @@ -204,6 +212,10 @@ public function testShareReceivedWithInvalidProtocolVersion(): void { ->method('isFederationEnabled') ->willReturn(true); + $this->calendarFederationConfig->expects(self::once()) + ->method('isIncomingServer2serverShareEnabled') + ->willReturn(true); + $this->federatedCalendarMapper->expects(self::never()) ->method('insert'); $this->jobList->expects(self::never()) @@ -232,6 +244,10 @@ public function testShareReceivedWithoutProtocolVersion(): void { ->method('isFederationEnabled') ->willReturn(true); + $this->calendarFederationConfig->expects(self::once()) + ->method('isIncomingServer2serverShareEnabled') + ->willReturn(true); + $this->federatedCalendarMapper->expects(self::never()) ->method('insert'); $this->jobList->expects(self::never()) @@ -261,6 +277,30 @@ public function testShareReceivedWithDisabledConfig(): void { $this->calendarFederationProvider->shareReceived($share); } + public function testShareReceivedWithIncomingServer2serverShareDisabled(): void { + $share = $this->createMock(ICloudFederationShare::class); + $share->method('getShareType') + ->willReturn('user'); + + $this->calendarFederationConfig->expects(self::once()) + ->method('isFederationEnabled') + ->willReturn(true); + + $this->calendarFederationConfig->expects(self::once()) + ->method('isIncomingServer2serverShareEnabled') + ->willReturn(false); + + $this->federatedCalendarMapper->expects(self::never()) + ->method('insert'); + $this->jobList->expects(self::never()) + ->method('add'); + + $this->expectException(ProviderCouldNotAddShareException::class); + $this->expectExceptionMessage('Instance does not support receiving federated calendar shares'); + $this->expectExceptionCode(503); + $this->calendarFederationProvider->shareReceived($share); + } + public function testShareReceivedWithUnsupportedShareType(): void { $share = $this->createMock(ICloudFederationShare::class); $share->method('getShareType') @@ -270,6 +310,10 @@ public function testShareReceivedWithUnsupportedShareType(): void { ->method('isFederationEnabled') ->willReturn(true); + $this->calendarFederationConfig->expects(self::once()) + ->method('isIncomingServer2serverShareEnabled') + ->willReturn(true); + $this->federatedCalendarMapper->expects(self::never()) ->method('insert'); $this->jobList->expects(self::never()) @@ -322,6 +366,10 @@ public function testShareReceivedWithIncompleteProtocolData(array $protocol): vo ->method('isFederationEnabled') ->willReturn(true); + $this->calendarFederationConfig->expects(self::once()) + ->method('isIncomingServer2serverShareEnabled') + ->willReturn(true); + $this->federatedCalendarMapper->expects(self::never()) ->method('insert'); $this->jobList->expects(self::never()) @@ -359,6 +407,10 @@ public function testShareReceivedWithReadWriteAccess(): void { ->method('isFederationEnabled') ->willReturn(true); + $this->calendarFederationConfig->expects(self::once()) + ->method('isIncomingServer2serverShareEnabled') + ->willReturn(true); + $this->federatedCalendarMapper->expects(self::once()) ->method('findByUri') ->with( @@ -418,6 +470,10 @@ public function testShareReceivedWithUnsupportedAccess(): void { ->method('isFederationEnabled') ->willReturn(true); + $this->calendarFederationConfig->expects(self::once()) + ->method('isIncomingServer2serverShareEnabled') + ->willReturn(true); + $this->federatedCalendarMapper->expects(self::never()) ->method('insert'); $this->jobList->expects(self::never()) diff --git a/apps/dav/tests/unit/CalDAV/Federation/FederationSharingServiceTest.php b/apps/dav/tests/unit/CalDAV/Federation/FederationSharingServiceTest.php index 6df0a565c938b..9a1ae90cf8e04 100644 --- a/apps/dav/tests/unit/CalDAV/Federation/FederationSharingServiceTest.php +++ b/apps/dav/tests/unit/CalDAV/Federation/FederationSharingServiceTest.php @@ -10,6 +10,7 @@ namespace OCA\DAV\Tests\unit\CalDAV\Federation; use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\Federation\CalendarFederationConfig; use OCA\DAV\CalDAV\Federation\FederationSharingService; use OCA\DAV\DAV\Sharing\IShareable; use OCA\DAV\DAV\Sharing\SharingMapper; @@ -38,6 +39,7 @@ class FederationSharingServiceTest extends TestCase { private readonly LoggerInterface&MockObject $logger; private readonly ISecureRandom&MockObject $random; private readonly SharingMapper&MockObject $sharingMapper; + private readonly CalendarFederationConfig&MockObject $config; protected function setUp(): void { parent::setUp(); @@ -49,6 +51,7 @@ protected function setUp(): void { $this->logger = $this->createMock(LoggerInterface::class); $this->random = $this->createMock(ISecureRandom::class); $this->sharingMapper = $this->createMock(SharingMapper::class); + $this->config = $this->createMock(CalendarFederationConfig::class); $this->federationSharingService = new FederationSharingService( $this->federationManager, @@ -58,6 +61,7 @@ protected function setUp(): void { $this->logger, $this->random, $this->sharingMapper, + $this->config, ); } @@ -83,6 +87,10 @@ public function testShareWith(): void { ] }); + $this->config->expects(self::once()) + ->method('isOutgoingServer2serverShareEnabled') + ->willReturn(true); + $hostUser = $this->createMock(IUser::class); $hostUser->method('getCloudId') ->willReturn('host1@nextcloud.host'); @@ -195,6 +203,10 @@ public function testShareWithWithFailingFederationManager(): void { ] }); + $this->config->expects(self::once()) + ->method('isOutgoingServer2serverShareEnabled') + ->willReturn(true); + $hostUser = $this->createMock(IUser::class); $hostUser->method('getCloudId') ->willReturn('host1@nextcloud.host'); @@ -299,6 +311,10 @@ public function testShareWithWithUnsuccessfulResponse(): void { ] }); + $this->config->expects(self::once()) + ->method('isOutgoingServer2serverShareEnabled') + ->willReturn(true); + $hostUser = $this->createMock(IUser::class); $hostUser->method('getCloudId') ->willReturn('host1@nextcloud.host'); @@ -381,6 +397,34 @@ public function testShareWithWithUnsuccessfulResponse(): void { ); } + public function testShareWithWithOutgoingServer2serverShareDisabled(): void { + $shareable = $this->createMock(Calendar::class); + $shareable->method('getOwner') + ->willReturn('principals/users/host1'); + $shareable->method('getName') + ->willReturn('cal1'); + + $this->config->expects(self::once()) + ->method('isOutgoingServer2serverShareEnabled') + ->willReturn(false); + + $this->userManager->expects(self::never()) + ->method('get'); + + $this->federationManager->expects(self::never()) + ->method('sendCloudShare'); + $this->sharingMapper->expects(self::never()) + ->method('deleteShare'); + $this->sharingMapper->expects(self::never()) + ->method('shareWithToken'); + + $this->federationSharingService->shareWith( + $shareable, + 'principals/remote-users/cmVtb3RlMUBuZXh0Y2xvdWQucmVtb3Rl', + 3, // Read-only + ); + } + public static function provideInvalidRemoteUserPrincipalData(): array { return [ ['principals/users/foobar'], @@ -418,6 +462,9 @@ public function testShareWithWithUnknownUser(): void { $shareable->method('getOwner') ->willReturn('principals/users/host1'); + $this->config->method('isOutgoingServer2serverShareEnabled') + ->willReturn(true); + $this->userManager->expects(self::once()) ->method('get') ->with('host1') @@ -442,6 +489,9 @@ public function testShareWithWithInvalidShareable(): void { $shareable->method('getOwner') ->willReturn('principals/users/host1'); + $this->config->method('isOutgoingServer2serverShareEnabled') + ->willReturn(true); + $this->userManager->expects(self::once()) ->method('get') ->with('host1')