Merge pull request #55811 from nextcloud/fix/public-share-group-exclusion-access

fix(sharing): Allow public share access for everyone
pull/56499/merge
F. E Noel Nfebe 2025-12-02 15:41:59 +07:00 committed by GitHub
commit 68b9108ca6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 118 additions and 5 deletions

@ -1413,7 +1413,7 @@ class Manager implements IManager {
} }
$share = null; $share = null;
try { try {
if ($this->shareApiAllowLinks()) { if ($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') === 'yes') {
$provider = $this->factory->getProviderForType(IShare::TYPE_LINK); $provider = $this->factory->getProviderForType(IShare::TYPE_LINK);
$share = $provider->getShareByToken($token); $share = $provider->getShareByToken($token);
} }
@ -1496,6 +1496,17 @@ class Manager implements IManager {
} }
} }
} }
// For link and email shares, verify the share owner can still create such shares
if ($share->getShareType() === IShare::TYPE_LINK || $share->getShareType() === IShare::TYPE_EMAIL) {
$shareOwner = $this->userManager->get($share->getShareOwner());
if ($shareOwner === null) {
throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
}
if (!$this->userCanCreateLinkShares($shareOwner)) {
throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
}
}
} }
/** /**
@ -1742,14 +1753,15 @@ class Manager implements IManager {
/** /**
* Is public link sharing enabled * Is public link sharing enabled
* *
* @param ?IUser $user User to check against group exclusions, defaults to current session user
* @return bool * @return bool
*/ */
public function shareApiAllowLinks() { public function shareApiAllowLinks(?IUser $user = null) {
if ($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') { if ($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') {
return false; return false;
} }
$user = $this->userSession->getUser(); $user = $user ?? $this->userSession->getUser();
if ($user) { if ($user) {
$excludedGroups = json_decode($this->config->getAppValue('core', 'shareapi_allow_links_exclude_groups', '[]')); $excludedGroups = json_decode($this->config->getAppValue('core', 'shareapi_allow_links_exclude_groups', '[]'));
if ($excludedGroups) { if ($excludedGroups) {
@ -1761,6 +1773,16 @@ class Manager implements IManager {
return true; return true;
} }
/**
* Check if a specific user can create link shares
*
* @param IUser $user The user to check
* @return bool
*/
protected function userCanCreateLinkShares(IUser $user): bool {
return $this->shareApiAllowLinks($user);
}
/** /**
* Is password on public link requires * Is password on public link requires
* *

@ -298,10 +298,12 @@ interface IManager {
/** /**
* Is public link sharing enabled * Is public link sharing enabled
* *
* @param ?IUser $user User to check against group exclusions, defaults to current session user
* @return bool * @return bool
* @since 9.0.0 * @since 9.0.0
* @since 33.0.0 Added optional $user parameter
*/ */
public function shareApiAllowLinks(); public function shareApiAllowLinks(?IUser $user = null);
/** /**
* Is password on public link required * Is password on public link required

@ -3325,21 +3325,29 @@ class ManagerTest extends \Test\TestCase {
public function testGetShareByTokenPublicUploadDisabled(): void { public function testGetShareByTokenPublicUploadDisabled(): void {
$this->config $this->config
->expects($this->exactly(3)) ->expects($this->exactly(5))
->method('getAppValue') ->method('getAppValue')
->willReturnMap([ ->willReturnMap([
['core', 'shareapi_allow_links', 'yes', 'yes'], ['core', 'shareapi_allow_links', 'yes', 'yes'],
['core', 'shareapi_allow_public_upload', 'yes', 'no'], ['core', 'shareapi_allow_public_upload', 'yes', 'no'],
['files_sharing', 'hide_disabled_user_shares', 'no', 'no'], ['files_sharing', 'hide_disabled_user_shares', 'no', 'no'],
['core', 'shareapi_allow_links_exclude_groups', '[]', '[]'],
]); ]);
$share = $this->manager->newShare(); $share = $this->manager->newShare();
$share->setShareType(IShare::TYPE_LINK) $share->setShareType(IShare::TYPE_LINK)
->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE); ->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE);
$share->setSharedWith('sharedWith'); $share->setSharedWith('sharedWith');
$share->setShareOwner('shareOwner');
$folder = $this->createMock(\OC\Files\Node\Folder::class); $folder = $this->createMock(\OC\Files\Node\Folder::class);
$share->setNode($folder); $share->setNode($folder);
$shareOwner = $this->createMock(IUser::class);
$this->userManager->expects($this->once())
->method('get')
->with('shareOwner')
->willReturn($shareOwner);
$this->defaultProvider->expects($this->once()) $this->defaultProvider->expects($this->once())
->method('getShareByToken') ->method('getShareByToken')
->willReturn('validToken') ->willReturn('validToken')
@ -3350,6 +3358,87 @@ class ManagerTest extends \Test\TestCase {
$this->assertSame(Constants::PERMISSION_READ, $res->getPermissions()); $this->assertSame(Constants::PERMISSION_READ, $res->getPermissions());
} }
public function testGetShareByTokenShareOwnerExcludedFromLinkShares(): void {
$this->expectException(ShareNotFound::class);
$this->expectExceptionMessage('The requested share does not exist anymore');
$this->config
->expects($this->exactly(4))
->method('getAppValue')
->willReturnMap([
['core', 'shareapi_allow_links', 'yes', 'yes'],
['files_sharing', 'hide_disabled_user_shares', 'no', 'no'],
['core', 'shareapi_allow_links_exclude_groups', '[]', '["excludedGroup"]'],
]);
$this->l->expects($this->once())
->method('t')
->willReturnArgument(0);
$share = $this->manager->newShare();
$share->setShareType(IShare::TYPE_LINK)
->setPermissions(Constants::PERMISSION_READ);
$share->setShareOwner('shareOwner');
$file = $this->createMock(File::class);
$share->setNode($file);
$shareOwner = $this->createMock(IUser::class);
$this->userManager->expects($this->once())
->method('get')
->with('shareOwner')
->willReturn($shareOwner);
$this->groupManager->expects($this->once())
->method('getUserGroupIds')
->with($shareOwner)
->willReturn(['excludedGroup', 'otherGroup']);
$this->defaultProvider->expects($this->once())
->method('getShareByToken')
->with('token')
->willReturn($share);
$this->manager->getShareByToken('token');
}
public function testGetShareByTokenShareOwnerNotExcludedFromLinkShares(): void {
$this->config
->expects($this->exactly(4))
->method('getAppValue')
->willReturnMap([
['core', 'shareapi_allow_links', 'yes', 'yes'],
['files_sharing', 'hide_disabled_user_shares', 'no', 'no'],
['core', 'shareapi_allow_links_exclude_groups', '[]', '["excludedGroup"]'],
]);
$share = $this->manager->newShare();
$share->setShareType(IShare::TYPE_LINK)
->setPermissions(Constants::PERMISSION_READ);
$share->setShareOwner('shareOwner');
$file = $this->createMock(File::class);
$share->setNode($file);
$shareOwner = $this->createMock(IUser::class);
$this->userManager->expects($this->once())
->method('get')
->with('shareOwner')
->willReturn($shareOwner);
$this->groupManager->expects($this->once())
->method('getUserGroupIds')
->with($shareOwner)
->willReturn(['allowedGroup', 'otherGroup']);
$this->defaultProvider->expects($this->once())
->method('getShareByToken')
->with('token')
->willReturn($share);
$res = $this->manager->getShareByToken('token');
$this->assertSame($share, $res);
}
public function testCheckPasswordNoLinkShare(): void { public function testCheckPasswordNoLinkShare(): void {
$share = $this->createMock(IShare::class); $share = $this->createMock(IShare::class);
$share->method('getShareType')->willReturn(IShare::TYPE_USER); $share->method('getShareType')->willReturn(IShare::TYPE_USER);