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 31f3924f05381..3af74789602c3 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');
}
}
@@ -1938,6 +1946,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]);