diff --git a/apps/settings/lib/Settings/Admin/Sharing.php b/apps/settings/lib/Settings/Admin/Sharing.php index f4d3a5c107b93..fc8b811eccdfa 100644 --- a/apps/settings/lib/Settings/Admin/Sharing.php +++ b/apps/settings/lib/Settings/Admin/Sharing.php @@ -63,6 +63,7 @@ public function getForm() { $excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', ''); $linksExcludedGroups = $this->config->getAppValue('core', 'shareapi_allow_links_exclude_groups', ''); $excludedPasswordGroups = $this->config->getAppValue('core', 'shareapi_enforce_links_password_excluded_groups', ''); + $onlyShareWithGroupMembersExcludeGroupList = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', ''); $parameters = [ // Built-In Sharing @@ -83,6 +84,7 @@ public function getForm() { 'passwordExcludedGroups' => json_decode($excludedPasswordGroups) ?? [], 'passwordExcludedGroupsFeatureEnabled' => $this->config->getSystemValueBool('sharing.allow_disabled_password_enforcement_groups', false), 'onlyShareWithGroupMembers' => $this->shareManager->shareWithGroupMembersOnly(), + 'onlyShareWithGroupMembersExcludeGroupList' => json_decode($onlyShareWithGroupMembersExcludeGroupList) ?? [], 'defaultExpireDate' => $this->getHumanBooleanConfig('core', 'shareapi_default_expire_date'), 'expireAfterNDays' => $this->config->getAppValue('core', 'shareapi_expire_after_n_days', '7'), 'enforceExpireDate' => $this->getHumanBooleanConfig('core', 'shareapi_enforce_expire_date'), diff --git a/apps/settings/src/components/AdminSettingsSharingForm.vue b/apps/settings/src/components/AdminSettingsSharingForm.vue index de23adf67d2df..2aa278821203a 100644 --- a/apps/settings/src/components/AdminSettingsSharingForm.vue +++ b/apps/settings/src/components/AdminSettingsSharingForm.vue @@ -37,6 +37,13 @@ {{ t('settings', 'Restrict users to only share with users in their groups') }} +
+ + +
@@ -218,6 +225,7 @@ interface IShareSettings { passwordExcludedGroups: string[] passwordExcludedGroupsFeatureEnabled: boolean onlyShareWithGroupMembers: boolean + onlyShareWithGroupMembersExcludeGroupList: string[] defaultExpireDate: boolean expireAfterNDays: string enforceExpireDate: boolean diff --git a/apps/settings/tests/Settings/Admin/SharingTest.php b/apps/settings/tests/Settings/Admin/SharingTest.php index b34c494577439..7dd4e56bcd267 100644 --- a/apps/settings/tests/Settings/Admin/SharingTest.php +++ b/apps/settings/tests/Settings/Admin/SharingTest.php @@ -163,6 +163,7 @@ public function testGetFormWithoutExcludedGroups(): void { 'allowLinksExcludeGroups' => [], 'passwordExcludedGroups' => [], 'passwordExcludedGroupsFeatureEnabled' => false, + 'onlyShareWithGroupMembersExcludeGroupList' => [], ] ], ); @@ -254,6 +255,7 @@ public function testGetFormWithExcludedGroups(): void { 'allowLinksExcludeGroups' => [], 'passwordExcludedGroups' => [], 'passwordExcludedGroupsFeatureEnabled' => false, + 'onlyShareWithGroupMembersExcludeGroupList' => [], ] ], ); diff --git a/lib/private/Collaboration/Collaborators/GroupPlugin.php b/lib/private/Collaboration/Collaborators/GroupPlugin.php index 1c98b904e764e..91e665db783a0 100644 --- a/lib/private/Collaboration/Collaborators/GroupPlugin.php +++ b/lib/private/Collaboration/Collaborators/GroupPlugin.php @@ -49,11 +49,16 @@ public function __construct( private IConfig $config, private IGroupManager $groupManager, private IUserSession $userSession, + private mixed $shareWithGroupOnlyExcludeGroupsList = [], ) { $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; $this->groupSharingDisabled = $this->config->getAppValue('core', 'shareapi_allow_group_sharing', 'yes') === 'no'; + + if ($this->shareWithGroupOnly) { + $this->shareWithGroupOnlyExcludeGroupsList = json_decode($this->config->getAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', ''), true) ?? []; + } } public function search($search, $limit, $offset, ISearchResult $searchResult): bool { @@ -81,6 +86,9 @@ public function search($search, $limit, $offset, ISearchResult $searchResult): b return $group->getGID(); }, $userGroups); $groupIds = array_intersect($groupIds, $userGroups); + + // ShareWithGroupOnly filtering + $groupIds = array_diff($groupIds, $this->shareWithGroupOnlyExcludeGroupsList); } $lowerSearch = strtolower($search); diff --git a/lib/private/Collaboration/Collaborators/MailPlugin.php b/lib/private/Collaboration/Collaborators/MailPlugin.php index 37ebf2fb129a0..44f67f5aeba72 100644 --- a/lib/private/Collaboration/Collaborators/MailPlugin.php +++ b/lib/private/Collaboration/Collaborators/MailPlugin.php @@ -61,6 +61,7 @@ public function __construct( private KnownUserService $knownUserService, private IUserSession $userSession, private IMailer $mailer, + private mixed $shareWithGroupOnlyExcludeGroupsList = [], ) { $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; @@ -68,6 +69,10 @@ public function __construct( $this->shareeEnumerationPhone = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; $this->shareeEnumerationFullMatch = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes'; $this->shareeEnumerationFullMatchEmail = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes'; + + if ($this->shareWithGroupOnly) { + $this->shareWithGroupOnlyExcludeGroupsList = json_decode($this->config->getAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', ''), true) ?? []; + } } /** @@ -127,6 +132,10 @@ public function search($search, $limit, $offset, ISearchResult $searchResult): b * Check if the user may share with the user associated with the e-mail of the just found contact */ $userGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser()); + + // ShareWithGroupOnly filtering + $userGroups = array_diff($userGroups, $this->shareWithGroupOnlyExcludeGroupsList); + $found = false; foreach ($userGroups as $userGroup) { if ($this->groupManager->isInGroup($contact['UID'], $userGroup)) { diff --git a/lib/private/Collaboration/Collaborators/UserPlugin.php b/lib/private/Collaboration/Collaborators/UserPlugin.php index 1bd6762d2e0a7..005b0d0581211 100644 --- a/lib/private/Collaboration/Collaborators/UserPlugin.php +++ b/lib/private/Collaboration/Collaborators/UserPlugin.php @@ -67,6 +67,7 @@ public function __construct( private IUserSession $userSession, private KnownUserService $knownUserService, private IUserStatusManager $userStatusManager, + private mixed $shareWithGroupOnlyExcludeGroupsList = [], ) { $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; @@ -76,6 +77,10 @@ public function __construct( $this->shareeEnumerationFullMatchUserId = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_userid', 'yes') === 'yes'; $this->shareeEnumerationFullMatchEmail = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_email', 'yes') === 'yes'; $this->shareeEnumerationFullMatchIgnoreSecondDisplayName = $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match_ignore_second_dn', 'no') === 'yes'; + + if ($this->shareWithGroupOnly) { + $this->shareWithGroupOnlyExcludeGroupsList = json_decode($this->config->getAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', ''), true) ?? []; + } } public function search($search, $limit, $offset, ISearchResult $searchResult): bool { @@ -85,6 +90,10 @@ public function search($search, $limit, $offset, ISearchResult $searchResult): b $currentUserId = $this->userSession->getUser()->getUID(); $currentUserGroups = $this->groupManager->getUserGroupIds($this->userSession->getUser()); + + // ShareWithGroupOnly filtering + $currentUserGroups = array_diff($currentUserGroups, $this->shareWithGroupOnlyExcludeGroupsList); + if ($this->shareWithGroupOnly || $this->shareeEnumerationInGroupOnly) { // Search in all the groups this user is part of foreach ($currentUserGroups as $userGroupId) { diff --git a/lib/private/Contacts/ContactsMenu/ContactsStore.php b/lib/private/Contacts/ContactsMenu/ContactsStore.php index eeb6ae56bc1e6..721ca828ad872 100644 --- a/lib/private/Contacts/ContactsMenu/ContactsStore.php +++ b/lib/private/Contacts/ContactsMenu/ContactsStore.php @@ -177,6 +177,9 @@ public function getContacts(IUser $user, ?string $filter, ?int $limit = null, ?i * 3. if the `shareapi_only_share_with_group_members` config option is * enabled it will filter all users which doesn't have a common group * with the current user. + * If enabled, the 'shareapi_only_share_with_group_members_exclude_group_list' + * config option may specify some groups excluded from the principle of + * belonging to the same group. * * @param Entry[] $entries * @return Entry[] the filtered contacts @@ -210,6 +213,13 @@ private function filterContacts( } } + // ownGroupsOnly : some groups may be excluded + if ($ownGroupsOnly) { + $excludeGroupsFromOwnGroups = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', ''); + $excludeGroupsFromOwnGroupsList = json_decode($excludeGroupsFromOwnGroups, true) ?? []; + $selfGroups = array_diff($selfGroups, $excludeGroupsFromOwnGroupsList); + } + $selfUID = $self->getUID(); return array_values(array_filter($entries, function (IEntry $entry) use ($skipLocal, $ownGroupsOnly, $selfGroups, $selfUID, $disallowEnumeration, $restrictEnumerationGroup, $restrictEnumerationPhone, $allowEnumerationFullMatch, $filter) { diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 0188c0897d9d3..102a27a18c196 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -548,6 +548,11 @@ protected function userCreateChecks(IShare $share) { $this->groupManager->getUserGroupIds($sharedBy), $this->groupManager->getUserGroupIds($sharedWith) ); + + // optional excluded groups + $excludedGroups = $this->shareWithGroupMembersOnlyExcludeGroupsList(); + $groups = array_diff($groups, $excludedGroups); + if (empty($groups)) { $message_t = $this->l->t('Sharing is only allowed with group members'); throw new \Exception($message_t); @@ -608,7 +613,10 @@ protected function groupCreateChecks(IShare $share) { if ($this->shareWithGroupMembersOnly()) { $sharedBy = $this->userManager->get($share->getSharedBy()); $sharedWith = $this->groupManager->get($share->getSharedWith()); - if (is_null($sharedWith) || !$sharedWith->inGroup($sharedBy)) { + + // optional excluded groups + $excludedGroups = $this->shareWithGroupMembersOnlyExcludeGroupsList(); + if (is_null($sharedWith) || in_array($share->getSharedWith(), $excludedGroups) || !$sharedWith->inGroup($sharedBy)) { throw new \Exception('Sharing is only allowed within your own groups'); } } @@ -1944,6 +1952,21 @@ public function shareWithGroupMembersOnly() { return $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; } + /** + * If shareWithGroupMembersOnly is enabled, return an optional + * list of groups that must be excluded from the principle of + * belonging to the same group. + * + * @return array + */ + public function shareWithGroupMembersOnlyExcludeGroupsList() { + if (!$this->shareWithGroupMembersOnly()) { + return []; + } + $excludeGroups = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members_exclude_group_list', ''); + return json_decode($excludeGroups, true) ?? []; + } + /** * Check if users can share with groups * diff --git a/lib/public/Share/IManager.php b/lib/public/Share/IManager.php index 9ac224ed7ef30..07517dd7eb5e6 100644 --- a/lib/public/Share/IManager.php +++ b/lib/public/Share/IManager.php @@ -415,6 +415,15 @@ public function shareApiLinkAllowPublicUpload(); */ public function shareWithGroupMembersOnly(); + /** + * If shareWithGroupMembersOnly is enabled, return an optional + * list of groups that must be excluded from the principle of + * belonging to the same group. + * @return array + * @since 27.0.0 + */ + public function shareWithGroupMembersOnlyExcludeGroupsList(); + /** * Check if users can share with groups * @return bool diff --git a/tests/lib/Share20/ManagerTest.php b/tests/lib/Share20/ManagerTest.php index c770181799f0f..eaa26ef7e856e 100644 --- a/tests/lib/Share20/ManagerTest.php +++ b/tests/lib/Share20/ManagerTest.php @@ -1569,6 +1569,7 @@ public function testUserCreateChecksShareWithGroupMembersOnlyDifferentGroups() { ->method('getAppValue') ->willReturnMap([ ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], + ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], ]); self::invokePrivate($this->manager, 'userCreateChecks', [$share]); @@ -1602,6 +1603,7 @@ public function testUserCreateChecksShareWithGroupMembersOnlySharedGroup() { ->method('getAppValue') ->willReturnMap([ ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], + ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], ]); $this->defaultProvider @@ -1794,6 +1796,7 @@ public function testGroupCreateChecksShareWithGroupMembersOnlyNotInGroup() { ->willReturnMap([ ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], ['core', 'shareapi_allow_group_sharing', 'yes', 'yes'], + ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], ]); self::invokePrivate($this->manager, 'groupCreateChecks', [$share]); @@ -1817,6 +1820,7 @@ public function testGroupCreateChecksShareWithGroupMembersOnlyNullGroup() { ->willReturnMap([ ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], ['core', 'shareapi_allow_group_sharing', 'yes', 'yes'], + ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], ]); $this->assertNull($this->invokePrivate($this->manager, 'groupCreateChecks', [$share])); @@ -1846,6 +1850,7 @@ public function testGroupCreateChecksShareWithGroupMembersOnlyInGroup() { ->willReturnMap([ ['core', 'shareapi_only_share_with_group_members', 'no', 'yes'], ['core', 'shareapi_allow_group_sharing', 'yes', 'yes'], + ['core', 'shareapi_only_share_with_group_members_exclude_group_list', '', '[]'], ]); self::invokePrivate($this->manager, 'groupCreateChecks', [$share]);