diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-05-12 18:40:35 +0200 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-07-17 16:59:36 +0200 |
commit | a18e61a1e5cd55f573f2e7b58c91adccfd872d91 (patch) | |
tree | f8647932face47a3f2a4f609ba11e90f32c3f672 | |
parent | 2cbfdcc4932857cc9f335aab86b37cc31cfa863a (diff) | |
download | nextcloud-server-a18e61a1e5cd55f573f2e7b58c91adccfd872d91.tar.gz nextcloud-server-a18e61a1e5cd55f573f2e7b58c91adccfd872d91.zip |
feat(files_sharing): add config option for extending link-share permissions
This allows the admin to control the behavior whether link shares with
READ permissions should be extended to also gain SHARE permissions,
allowing users (public share receivers) to add the share to their cloud.
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r-- | apps/files_sharing/lib/Controller/ShareAPIController.php | 7 | ||||
-rw-r--r-- | apps/files_sharing/tests/ApiTest.php | 38 | ||||
-rw-r--r-- | apps/files_sharing/tests/Controller/ShareAPIControllerTest.php | 12 | ||||
-rw-r--r-- | apps/settings/lib/Settings/Admin/Sharing.php | 4 | ||||
-rw-r--r-- | apps/settings/src/components/AdminSettingsSharingForm.vue | 5 | ||||
-rw-r--r-- | apps/settings/tests/Settings/Admin/SharingTest.php | 15 | ||||
-rw-r--r-- | build/integration/features/bootstrap/Sharing.php | 2 | ||||
-rw-r--r-- | build/integration/features/bootstrap/SharingContext.php | 1 | ||||
-rw-r--r-- | core/AppInfo/Application.php | 3 | ||||
-rw-r--r-- | core/AppInfo/ConfigLexicon.php | 43 | ||||
-rw-r--r-- | lib/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | lib/composer/composer/autoload_static.php | 1 |
12 files changed, 120 insertions, 12 deletions
diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index a19eb50faf7..7591493167f 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace OCA\Files_Sharing\Controller; use Exception; +use OC\Core\AppInfo\ConfigLexicon; use OC\Files\FileInfo; use OC\Files\Storage\Wrapper\Wrapper; use OCA\Circles\Api\v1\Circles; @@ -41,6 +42,7 @@ use OCP\Files\Mount\IShareOwnerlessMount; use OCP\Files\Node; use OCP\Files\NotFoundException; use OCP\HintException; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IDateTimeZone; use OCP\IGroupManager; @@ -88,6 +90,7 @@ class ShareAPIController extends OCSController { private IURLGenerator $urlGenerator, private IL10N $l, private IConfig $config, + private IAppConfig $appConfig, private IAppManager $appManager, private ContainerInterface $serverContainer, private IUserStatusManager $userStatusManager, @@ -969,9 +972,9 @@ class ShareAPIController extends OCSController { : Constants::PERMISSION_READ; } - // TODO: It might make sense to have a dedicated setting to allow/deny converting link shares into federated ones if ($this->hasPermission($permissions, Constants::PERMISSION_READ) - && $this->shareManager->outgoingServer2ServerSharesAllowed()) { + && $this->shareManager->outgoingServer2ServerSharesAllowed() + && $this->appConfig->getValueBool('core', ConfigLexicon::SHAREAPI_ALLOW_FEDERATION_ON_PUBLIC_SHARES)) { $permissions |= Constants::PERMISSION_SHARE; } diff --git a/apps/files_sharing/tests/ApiTest.php b/apps/files_sharing/tests/ApiTest.php index bb686c1ea8c..676809eebff 100644 --- a/apps/files_sharing/tests/ApiTest.php +++ b/apps/files_sharing/tests/ApiTest.php @@ -7,6 +7,7 @@ */ namespace OCA\Files_Sharing\Tests; +use OC\Core\AppInfo\ConfigLexicon; use OC\Files\Cache\Scanner; use OC\Files\FileInfo; use OC\Files\Filesystem; @@ -21,6 +22,7 @@ use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\Constants; use OCP\Files\Folder; use OCP\Files\IRootFolder; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IDateTimeZone; use OCP\IGroupManager; @@ -35,6 +37,7 @@ use OCP\Server; use OCP\Share\IProviderFactory; use OCP\Share\IShare; use OCP\UserStatus\IManager as IUserStatusManager; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; @@ -50,11 +53,9 @@ class ApiTest extends TestCase { private static $tempStorage; - /** @var Folder */ - private $userFolder; - - /** @var string */ - private $subsubfolder; + private Folder $userFolder; + private string $subsubfolder; + protected IAppConfig&MockObject $appConfig; protected function setUp(): void { parent::setUp(); @@ -81,6 +82,8 @@ class ApiTest extends TestCase { $mount->getStorage()->getScanner()->scan('', Scanner::SCAN_RECURSIVE); $this->userFolder = \OC::$server->getUserFolder(self::TEST_FILES_SHARING_API_USER1); + + $this->appConfig = $this->createMock(IAppConfig::class); } protected function tearDown(): void { @@ -126,6 +129,7 @@ class ApiTest extends TestCase { Server::get(IURLGenerator::class), $l, $config, + $this->appConfig, $appManager, $serverContainer, $userStatusManager, @@ -233,8 +237,12 @@ class ApiTest extends TestCase { /** * @group RoutingWeirdness + * @dataProvider dataAllowFederationOnPublicShares */ - public function testCreateShareLinkPublicUpload(): void { + public function testCreateShareLinkPublicUpload(array $appConfig, int $permissions): void { + $this->appConfig->method('getValueBool') + ->willReturnMap([$appConfig]); + $ocs = $this->createOCS(self::TEST_FILES_SHARING_API_USER1); $result = $ocs->createShare($this->folder, Constants::PERMISSION_ALL, IShare::TYPE_LINK, null, 'true'); $ocs->cleanup(); @@ -245,7 +253,7 @@ class ApiTest extends TestCase { | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE - | Constants::PERMISSION_SHARE, + | $permissions, $data['permissions'] ); $this->assertEmpty($data['expiration']); @@ -1005,8 +1013,13 @@ class ApiTest extends TestCase { /** * @medium + * @dataProvider dataAllowFederationOnPublicShares */ - public function testUpdateShareUpload(): void { + public function testUpdateShareUpload(array $appConfig, int $permissions): void { + $this->appConfig->method('getValueBool')->willReturnMap([ + $appConfig, + ]); + $node1 = $this->userFolder->get($this->folder); $share1 = $this->shareManager->newShare(); $share1->setNode($node1) @@ -1026,7 +1039,7 @@ class ApiTest extends TestCase { | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE - | Constants::PERMISSION_SHARE, + | $permissions, $share1->getPermissions() ); @@ -1034,6 +1047,13 @@ class ApiTest extends TestCase { $this->shareManager->deleteShare($share1); } + public static function dataAllowFederationOnPublicShares(): array { + return [ + [['core', ConfigLexicon::SHAREAPI_ALLOW_FEDERATION_ON_PUBLIC_SHARES, false, false], 0], + [['core', ConfigLexicon::SHAREAPI_ALLOW_FEDERATION_ON_PUBLIC_SHARES, false, true], Constants::PERMISSION_SHARE], + ]; + } + /** * @medium */ diff --git a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php index 8bb220d684b..abc405fc21c 100644 --- a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php @@ -22,6 +22,7 @@ use OCP\Files\Mount\IMountPoint; use OCP\Files\Mount\IShareOwnerlessMount; use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorage; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IDateTimeZone; use OCP\IGroup; @@ -71,6 +72,7 @@ class ShareAPIControllerTest extends TestCase { private IURLGenerator&MockObject $urlGenerator; private IL10N&MockObject $l; private IConfig&MockObject $config; + private IAppConfig&MockObject $appConfig; private IAppManager&MockObject $appManager; private ContainerInterface&MockObject $serverContainer; private IUserStatusManager&MockObject $userStatusManager; @@ -103,6 +105,7 @@ class ShareAPIControllerTest extends TestCase { return vsprintf($text, $parameters); }); $this->config = $this->createMock(IConfig::class); + $this->appConfig = $this->createMock(IAppConfig::class); $this->appManager = $this->createMock(IAppManager::class); $this->serverContainer = $this->createMock(ContainerInterface::class); $this->userStatusManager = $this->createMock(IUserStatusManager::class); @@ -127,6 +130,7 @@ class ShareAPIControllerTest extends TestCase { $this->urlGenerator, $this->l, $this->config, + $this->appConfig, $this->appManager, $this->serverContainer, $this->userStatusManager, @@ -155,6 +159,7 @@ class ShareAPIControllerTest extends TestCase { $this->urlGenerator, $this->l, $this->config, + $this->appConfig, $this->appManager, $this->serverContainer, $this->userStatusManager, @@ -838,6 +843,7 @@ class ShareAPIControllerTest extends TestCase { $this->urlGenerator, $this->l, $this->config, + $this->appConfig, $this->appManager, $this->serverContainer, $this->userStatusManager, @@ -1469,6 +1475,7 @@ class ShareAPIControllerTest extends TestCase { $this->urlGenerator, $this->l, $this->config, + $this->appConfig, $this->appManager, $this->serverContainer, $this->userStatusManager, @@ -1856,6 +1863,7 @@ class ShareAPIControllerTest extends TestCase { $this->urlGenerator, $this->l, $this->config, + $this->appConfig, $this->appManager, $this->serverContainer, $this->userStatusManager, @@ -1954,6 +1962,7 @@ class ShareAPIControllerTest extends TestCase { $this->urlGenerator, $this->l, $this->config, + $this->appConfig, $this->appManager, $this->serverContainer, $this->userStatusManager, @@ -2380,6 +2389,7 @@ class ShareAPIControllerTest extends TestCase { $this->urlGenerator, $this->l, $this->config, + $this->appConfig, $this->appManager, $this->serverContainer, $this->userStatusManager, @@ -2451,6 +2461,7 @@ class ShareAPIControllerTest extends TestCase { $this->urlGenerator, $this->l, $this->config, + $this->appConfig, $this->appManager, $this->serverContainer, $this->userStatusManager, @@ -2689,6 +2700,7 @@ class ShareAPIControllerTest extends TestCase { $this->urlGenerator, $this->l, $this->config, + $this->appConfig, $this->appManager, $this->serverContainer, $this->userStatusManager, diff --git a/apps/settings/lib/Settings/Admin/Sharing.php b/apps/settings/lib/Settings/Admin/Sharing.php index 3dede7fbdb4..e038b2a6231 100644 --- a/apps/settings/lib/Settings/Admin/Sharing.php +++ b/apps/settings/lib/Settings/Admin/Sharing.php @@ -6,10 +6,12 @@ */ namespace OCA\Settings\Settings\Admin; +use OC\Core\AppInfo\ConfigLexicon; use OCP\App\IAppManager; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; use OCP\Constants; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; @@ -20,6 +22,7 @@ use OCP\Util; class Sharing implements IDelegatedSettings { public function __construct( private IConfig $config, + private IAppConfig $appConfig, private IL10N $l, private IManager $shareManager, private IAppManager $appManager, @@ -47,6 +50,7 @@ class Sharing implements IDelegatedSettings { 'allowPublicUpload' => $this->getHumanBooleanConfig('core', 'shareapi_allow_public_upload', true), 'allowResharing' => $this->getHumanBooleanConfig('core', 'shareapi_allow_resharing', true), 'allowShareDialogUserEnumeration' => $this->getHumanBooleanConfig('core', 'shareapi_allow_share_dialog_user_enumeration', true), + 'allowFederationOnPublicShares' => $this->appConfig->getValueBool('core', ConfigLexicon::SHAREAPI_ALLOW_FEDERATION_ON_PUBLIC_SHARES), 'restrictUserEnumerationToGroup' => $this->getHumanBooleanConfig('core', 'shareapi_restrict_user_enumeration_to_group'), 'restrictUserEnumerationToPhone' => $this->getHumanBooleanConfig('core', 'shareapi_restrict_user_enumeration_to_phone'), 'restrictUserEnumerationFullMatch' => $this->getHumanBooleanConfig('core', 'shareapi_restrict_user_enumeration_full_match', true), diff --git a/apps/settings/src/components/AdminSettingsSharingForm.vue b/apps/settings/src/components/AdminSettingsSharingForm.vue index 1973781edee..c582e9febee 100644 --- a/apps/settings/src/components/AdminSettingsSharingForm.vue +++ b/apps/settings/src/components/AdminSettingsSharingForm.vue @@ -48,6 +48,10 @@ <NcCheckboxRadioSwitch :checked.sync="settings.allowPublicUpload"> {{ t('settings', 'Allow public uploads') }} </NcCheckboxRadioSwitch> + <NcCheckboxRadioSwitch v-model="settings.allowFederationOnPublicShares"> + {{ t('settings', 'Allow public shares to be added to other clouds by federation.') }} + {{ t('settings', 'This will add share permissions to all newly created link shares.') }} + </NcCheckboxRadioSwitch> <NcCheckboxRadioSwitch :checked.sync="settings.enableLinkPasswordByDefault"> {{ t('settings', 'Always ask for a password') }} </NcCheckboxRadioSwitch> @@ -241,6 +245,7 @@ interface IShareSettings { allowPublicUpload: boolean allowResharing: boolean allowShareDialogUserEnumeration: boolean + allowFederationOnPublicShares: boolean restrictUserEnumerationToGroup: boolean restrictUserEnumerationToPhone: boolean restrictUserEnumerationFullMatch: boolean diff --git a/apps/settings/tests/Settings/Admin/SharingTest.php b/apps/settings/tests/Settings/Admin/SharingTest.php index 62e3440fa54..12ab5c3cada 100644 --- a/apps/settings/tests/Settings/Admin/SharingTest.php +++ b/apps/settings/tests/Settings/Admin/SharingTest.php @@ -11,6 +11,7 @@ use OCP\App\IAppManager; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; use OCP\Constants; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IL10N; use OCP\IURLGenerator; @@ -19,18 +20,22 @@ use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; class SharingTest extends TestCase { + private Sharing $admin; + private IConfig&MockObject $config; + private IAppConfig&MockObject $appConfig; private IL10N&MockObject $l10n; private IManager&MockObject $shareManager; private IAppManager&MockObject $appManager; private IURLGenerator&MockObject $urlGenerator; private IInitialState&MockObject $initialState; - private Sharing $admin; protected function setUp(): void { parent::setUp(); $this->config = $this->createMock(IConfig::class); + $this->appConfig = $this->createMock(IAppConfig::class); $this->l10n = $this->createMock(IL10N::class); + $this->shareManager = $this->createMock(IManager::class); $this->appManager = $this->createMock(IAppManager::class); $this->urlGenerator = $this->createMock(IURLGenerator::class); @@ -38,6 +43,7 @@ class SharingTest extends TestCase { $this->admin = new Sharing( $this->config, + $this->appConfig, $this->l10n, $this->shareManager, $this->appManager, @@ -48,6 +54,12 @@ class SharingTest extends TestCase { } public function testGetFormWithoutExcludedGroups(): void { + $this->appConfig + ->method('getValueBool') + ->willReturnMap([ + ['core', 'shareapi_allow_federation_on_public_shares', false, false, true], + ]); + $this->config ->method('getAppValue') ->willReturnMap([ @@ -103,6 +115,7 @@ class SharingTest extends TestCase { 'allowPublicUpload' => true, 'allowResharing' => true, 'allowShareDialogUserEnumeration' => true, + 'allowFederationOnPublicShares' => true, 'restrictUserEnumerationToGroup' => false, 'restrictUserEnumerationToPhone' => false, 'restrictUserEnumerationFullMatch' => true, diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php index 039ee7d1121..0cc490ff110 100644 --- a/build/integration/features/bootstrap/Sharing.php +++ b/build/integration/features/bootstrap/Sharing.php @@ -337,6 +337,8 @@ trait Sharing { return $this->isExpectedUrl((string)$data->$field, 'index.php/s/'); } elseif ($contentExpected == $data->$field) { return true; + } else { + print($data->$field); } return false; } diff --git a/build/integration/features/bootstrap/SharingContext.php b/build/integration/features/bootstrap/SharingContext.php index fe4d3bb6bf1..a9dd99108a9 100644 --- a/build/integration/features/bootstrap/SharingContext.php +++ b/build/integration/features/bootstrap/SharingContext.php @@ -29,6 +29,7 @@ class SharingContext implements Context, SnippetAcceptingContext { $this->deleteServerConfig('core', 'shareapi_default_expire_date'); $this->deleteServerConfig('core', 'shareapi_expire_after_n_days'); $this->deleteServerConfig('core', 'link_defaultExpDays'); + $this->deleteServerConfig('core', 'shareapi_allow_federation_on_public_shares'); $this->deleteServerConfig('files_sharing', 'outgoing_server2server_share_enabled'); $this->deleteServerConfig('core', 'shareapi_allow_view_without_download'); diff --git a/core/AppInfo/Application.php b/core/AppInfo/Application.php index b94f010b02b..f1fe7d763e3 100644 --- a/core/AppInfo/Application.php +++ b/core/AppInfo/Application.php @@ -78,6 +78,9 @@ class Application extends App implements IBootstrap { // Tags $context->registerEventListener(UserDeletedEvent::class, TagManager::class); + + // config lexicon + $context->registerConfigLexicon(ConfigLexicon::class); } public function boot(IBootContext $context): void { diff --git a/core/AppInfo/ConfigLexicon.php b/core/AppInfo/ConfigLexicon.php new file mode 100644 index 00000000000..cc7fd1a3b09 --- /dev/null +++ b/core/AppInfo/ConfigLexicon.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\Core\AppInfo; + +use NCU\Config\Lexicon\ConfigLexiconEntry; +use NCU\Config\Lexicon\ConfigLexiconStrictness; +use NCU\Config\Lexicon\IConfigLexicon; +use NCU\Config\ValueType; + +/** + * Config Lexicon for core. + * + * Please Add & Manage your Config Keys in that file and keep the Lexicon up to date! + */ +class ConfigLexicon implements IConfigLexicon { + public const SHAREAPI_ALLOW_FEDERATION_ON_PUBLIC_SHARES = 'shareapi_allow_federation_on_public_shares'; + + public function getStrictness(): ConfigLexiconStrictness { + return ConfigLexiconStrictness::IGNORE; + } + + public function getAppConfigs(): array { + return [ + new ConfigLexiconEntry( + key: self::SHAREAPI_ALLOW_FEDERATION_ON_PUBLIC_SHARES, + type: ValueType::BOOL, + lazy: true, + defaultRaw: true, + definition: 'adds share permission to public shares to allow adding them to your Nextcloud (federation)', + ), + ]; + } + + public function getUserConfigs(): array { + return []; + } +} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index b3505bdd8e0..16926e77775 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1224,6 +1224,7 @@ return array( 'OC\\Contacts\\ContactsMenu\\Providers\\ProfileProvider' => $baseDir . '/lib/private/Contacts/ContactsMenu/Providers/ProfileProvider.php', 'OC\\ContextChat\\ContentManager' => $baseDir . '/lib/private/ContextChat/ContentManager.php', 'OC\\Core\\AppInfo\\Application' => $baseDir . '/core/AppInfo/Application.php', + 'OC\\Core\\AppInfo\\ConfigLexicon' => $baseDir . '/core/AppInfo/ConfigLexicon.php', 'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => $baseDir . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php', 'OC\\Core\\BackgroundJobs\\CheckForUserCertificates' => $baseDir . '/core/BackgroundJobs/CheckForUserCertificates.php', 'OC\\Core\\BackgroundJobs\\CleanupLoginFlowV2' => $baseDir . '/core/BackgroundJobs/CleanupLoginFlowV2.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 15ae0a9373f..daa1a1940dd 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1265,6 +1265,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Contacts\\ContactsMenu\\Providers\\ProfileProvider' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/Providers/ProfileProvider.php', 'OC\\ContextChat\\ContentManager' => __DIR__ . '/../../..' . '/lib/private/ContextChat/ContentManager.php', 'OC\\Core\\AppInfo\\Application' => __DIR__ . '/../../..' . '/core/AppInfo/Application.php', + 'OC\\Core\\AppInfo\\ConfigLexicon' => __DIR__ . '/../../..' . '/core/AppInfo/ConfigLexicon.php', 'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php', 'OC\\Core\\BackgroundJobs\\CheckForUserCertificates' => __DIR__ . '/../../..' . '/core/BackgroundJobs/CheckForUserCertificates.php', 'OC\\Core\\BackgroundJobs\\CleanupLoginFlowV2' => __DIR__ . '/../../..' . '/core/BackgroundJobs/CleanupLoginFlowV2.php', |