Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions apps/files_sharing/lib/Controller/ShareAPIController.php
Original file line number Diff line number Diff line change
Expand Up @@ -1532,11 +1532,14 @@ protected function canAccessShare(IShare $share, bool $checkGroups = true): bool
return true;
}

// If in the recipient group, you can see the share
// If in the recipient group (directly or via a nested group), you can see the share
if ($checkGroups && $share->getShareType() === IShare::TYPE_GROUP) {
$sharedWith = $this->groupManager->get($share->getSharedWith());
$user = $this->userManager->get($this->userId);
if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) {
if ($user !== null && in_array(
$share->getSharedWith(),
$this->groupManager->getUserEffectiveGroupIds($user),
true,
)) {
return true;
}
}
Expand Down Expand Up @@ -1662,11 +1665,14 @@ protected function canDeleteShareFromSelf(IShare $share): bool {
return false;
}

// If in the recipient group, you can delete the share from self
// If in the recipient group (directly or via a nested group), you can delete the share from self
if ($share->getShareType() === IShare::TYPE_GROUP) {
$sharedWith = $this->groupManager->get($share->getSharedWith());
$user = $this->userManager->get($this->userId);
if ($user !== null && $sharedWith !== null && $sharedWith->inGroup($user)) {
if ($user !== null && in_array(
$share->getSharedWith(),
$this->groupManager->getUserEffectiveGroupIds($user),
true,
)) {
return true;
}
}
Expand Down
8 changes: 7 additions & 1 deletion apps/files_sharing/lib/External/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,13 @@ private function canAccessShare(ExternalShare $share, IUser $user): bool {
$groupShare = $share;
}

if ($this->groupManager->get($groupShare->getUser())->inGroup($user)) {
// Honor nested-group membership: a user in a sub-group of the
// target group is also an effective recipient.
if (in_array(
$groupShare->getUser(),
$this->groupManager->getUserEffectiveGroupIds($user),
true,
)) {
return true;
}
}
Expand Down
20 changes: 17 additions & 3 deletions apps/files_sharing/lib/Listener/UserShareAcceptanceListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,23 @@ public function handle(Event $event): void {
return;
}

$users = $group->getUsers();
foreach ($users as $user) {
$this->handleAutoAccept($share, $user->getUID());
// Walk descendants so effective members reached via nested-group
// edges also get the share auto-accepted, matching what
// `IManager::getSharedWith` returns for them.
$seen = [];
foreach ($this->groupManager->getGroupEffectiveDescendantIds($group) as $gid) {
$descendant = $this->groupManager->get($gid);
if ($descendant === null) {
continue;
}
foreach ($descendant->getUsers() as $user) {
$uid = $user->getUID();
if (isset($seen[$uid])) {
continue;
}
$seen[$uid] = true;
$this->handleAutoAccept($share, $uid);
}
}
}
}
Expand Down
9 changes: 7 additions & 2 deletions apps/files_sharing/lib/Notification/Notifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,13 @@
throw new AlreadyProcessedException();
}

$group = $this->groupManager->get($share->getSharedWith());
if ($group === null || !$group->inGroup($user)) {
// Honor nested-group membership so a user in a sub-group of the
// recipient group still receives the pending-share notification.
if (!in_array(
$share->getSharedWith(),
$this->groupManager->getUserEffectiveGroupIds($user),
true,
)) {
throw new AlreadyProcessedException();
}

Expand All @@ -188,8 +193,8 @@
],
'group' => [
'type' => 'user-group',
'id' => $group->getGID(),

Check failure on line 196 in apps/files_sharing/lib/Notification/Notifier.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedVariable

apps/files_sharing/lib/Notification/Notifier.php:196:15: UndefinedVariable: Cannot find referenced variable $group (see https://psalm.dev/024)
'name' => $group->getDisplayName(),

Check failure on line 197 in apps/files_sharing/lib/Notification/Notifier.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedVariable

apps/files_sharing/lib/Notification/Notifier.php:197:17: UndefinedVariable: Cannot find referenced variable $group (see https://psalm.dev/024)
],
'user' => [
'type' => 'user',
Expand Down
19 changes: 10 additions & 9 deletions apps/files_sharing/tests/Controller/ShareAPIControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -438,9 +438,9 @@ public function testDeleteSharedWithMyGroup(): void {
->method('get')
->with($this->currentUser)
->willReturn($user);
$group->method('inGroup')
$this->groupManager->method('getUserEffectiveGroupIds')
->with($user)
->willReturn(true);
->willReturn(['group']);

$node->expects($this->once())
->method('lock')
Expand Down Expand Up @@ -1780,16 +1780,17 @@ public function testCanAccessShareAsGroupMember(string $group, bool $expected):
->with($this->currentUser)
->willReturn($user);

$group = $this->createMock(IGroup::class);
$group->method('inGroup')->with($user)->willReturn(true);
$group2 = $this->createMock(IGroup::class);
$group2->method('inGroup')->with($user)->willReturn(false);

$groupMock = $this->createMock(IGroup::class);
$group2Mock = $this->createMock(IGroup::class);
$this->groupManager->method('get')->willReturnMap([
['group', $group],
['group2', $group2],
['group', $groupMock],
['group2', $group2Mock],
['group-null', null],
]);
// Only "group" contains the current user (directly or via nesting).
$this->groupManager->method('getUserEffectiveGroupIds')
->with($user)
->willReturn(['group']);

if ($expected) {
$this->assertTrue($this->invokePrivate($this->ocs, 'canAccessShare', [$share]));
Expand Down
6 changes: 6 additions & 0 deletions apps/provisioning_api/appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
['root' => '/cloud', 'name' => 'Groups#getGroupUsers', 'url' => '/groups/{groupId}/users', 'verb' => 'GET', 'requirements' => ['groupId' => '.+']],
['root' => '/cloud', 'name' => 'Groups#getGroupUsersDetails', 'url' => '/groups/{groupId}/users/details', 'verb' => 'GET', 'requirements' => ['groupId' => '.+']],
['root' => '/cloud', 'name' => 'Groups#getSubAdminsOfGroup', 'url' => '/groups/{groupId}/subadmins', 'verb' => 'GET', 'requirements' => ['groupId' => '.+']],
['root' => '/cloud', 'name' => 'Groups#getSubGroups', 'url' => '/groups/{groupId}/subgroups', 'verb' => 'GET', 'requirements' => ['groupId' => '.+']],
['root' => '/cloud', 'name' => 'Groups#addSubGroup', 'url' => '/groups/{groupId}/subgroups', 'verb' => 'POST', 'requirements' => ['groupId' => '.+']],
['root' => '/cloud', 'name' => 'Groups#removeSubGroup', 'url' => '/groups/{groupId}/subgroups/{subGroupId}', 'verb' => 'DELETE', 'requirements' => ['groupId' => '.+', 'subGroupId' => '.+']],
['root' => '/cloud', 'name' => 'Groups#getGroupSubAdmins', 'url' => '/groups/{groupId}/subadmins/groups', 'verb' => 'GET', 'requirements' => ['groupId' => '.+']],
['root' => '/cloud', 'name' => 'Groups#addGroupSubAdmin', 'url' => '/groups/{groupId}/subadmins/groups', 'verb' => 'POST', 'requirements' => ['groupId' => '.+']],
['root' => '/cloud', 'name' => 'Groups#removeGroupSubAdmin', 'url' => '/groups/{groupId}/subadmins/groups/{adminGroupId}', 'verb' => 'DELETE', 'requirements' => ['groupId' => '.+', 'adminGroupId' => '.+']],
['root' => '/cloud', 'name' => 'Groups#addGroup', 'url' => '/groups', 'verb' => 'POST'],
['root' => '/cloud', 'name' => 'Groups#getGroup', 'url' => '/groups/{groupId}', 'verb' => 'GET', 'requirements' => ['groupId' => '.+']],
['root' => '/cloud', 'name' => 'Groups#updateGroup', 'url' => '/groups/{groupId}', 'verb' => 'PUT', 'requirements' => ['groupId' => '.+']],
Expand Down
207 changes: 200 additions & 7 deletions apps/provisioning_api/lib/Controller/GroupsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,20 @@ public function getGroupUsers(string $groupId): DataResponse {
$isAdmin = $this->groupManager->isAdmin($user->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($user->getUID());
if ($isAdmin || $isDelegatedAdmin || $isSubadminOfGroup || $isMember) {
$users = $this->groupManager->get($groupId)->getUsers();
$users = array_map(function ($user) {
/** @var IUser $user */
return $user->getUID();
}, $users);
// Honor nested-group edges: return the union of direct members
// and every descendant group's direct members.
$users = [];
foreach ($this->groupManager->getGroupEffectiveDescendantIds($group) as $gid) {
$descendant = $this->groupManager->get($gid);
if ($descendant === null) {
continue;
}
foreach ($descendant->getUsers() as $member) {
$users[$member->getUID()] = true;
}
}
/** @var list<string> $users */
$users = array_values($users);
$users = array_values(array_keys($users));
return new DataResponse(['users' => $users]);
}

Expand Down Expand Up @@ -208,7 +215,31 @@ public function getGroupUsersDetails(string $groupId, string $search = '', ?int
$isAdmin = $this->groupManager->isAdmin($currentUser->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentUser->getUID());
if ($isAdmin || $isDelegatedAdmin || $isSubadminOfGroup) {
$users = $group->searchUsers($search, $limit, $offset);
// Honor nested-group edges: the effective user set of a parent
// group is the union of its own direct members and the members
// of every descendant group.
$users = [];
$seen = [];
foreach ($this->groupManager->getGroupEffectiveDescendantIds($group) as $gid) {
$descendant = $this->groupManager->get($gid);
if ($descendant === null) {
continue;
}
foreach ($descendant->searchUsers($search) as $user) {
$uid = $user->getUID();
if (isset($seen[$uid])) {
continue;
}
$seen[$uid] = true;
$users[] = $user;
}
}
if ($offset > 0) {
$users = array_slice($users, $offset);
}
if ($limit !== null && $limit >= 0) {
$users = array_slice($users, 0, $limit);
}

// Extract required number
$usersDetails = [];
Expand Down Expand Up @@ -351,4 +382,166 @@ public function getSubAdminsOfGroup(string $groupId): DataResponse {

return new DataResponse($uids);
}

/**
* Get the direct subgroups of a group (one level deep).
*
* Only edges stored in the nested-group table are returned; transitive
* descendants are not. Use effective-member endpoints if you need the
* full set of users reachable via nesting.
*
* @param string $groupId ID of the parent group
* @return DataResponse<Http::STATUS_OK, list<string>, array{}>
* @throws OCSException
*
* 200: Direct subgroups returned
*/
#[AuthorizedAdminSetting(settings: Users::class)]
public function getSubGroups(string $groupId): DataResponse {
$groupId = urldecode($groupId);
$group = $this->groupManager->get($groupId);
if ($group === null) {
throw new OCSException('Group does not exist', 101);
}
$direct = $this->groupManager->getDirectChildGroupIds($groupId);
return new DataResponse($direct);
}

/**
* Add a subgroup to a group
*
* @param string $groupId ID of the parent group
* @param string $subGroupId ID of the group to add as a subgroup
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
* @throws OCSException
*
* 200: Subgroup added
*/
#[AuthorizedAdminSetting(settings: Users::class)]
#[PasswordConfirmationRequired]
public function addSubGroup(string $groupId, string $subGroupId): DataResponse {
$groupId = urldecode($groupId);
$parent = $this->groupManager->get($groupId);
if ($parent === null) {
throw new OCSException('Parent group does not exist', 101);
}
$child = $this->groupManager->get($subGroupId);
if ($child === null) {
throw new OCSException('Subgroup does not exist', 102);
}
try {
$this->groupManager->addSubGroup($parent, $child);
} catch (\OCP\Group\Exception\CycleDetectedException $e) {
throw new OCSException($e->getMessage(), 103);
} catch (\OCP\Group\Exception\NestedGroupsNotSupportedException $e) {
throw new OCSException('Nested groups are not supported by this backend', 104);
}
return new DataResponse();
}

/**
* Remove a subgroup from a group
*
* @param string $groupId ID of the parent group
* @param string $subGroupId ID of the subgroup to remove
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
* @throws OCSException
*
* 200: Subgroup removed
*/
#[AuthorizedAdminSetting(settings: Users::class)]
#[PasswordConfirmationRequired]
public function removeSubGroup(string $groupId, string $subGroupId): DataResponse {
$groupId = urldecode($groupId);
$subGroupId = urldecode($subGroupId);
$parent = $this->groupManager->get($groupId);
if ($parent === null) {
throw new OCSException('Parent group does not exist', 101);
}
$child = $this->groupManager->get($subGroupId);
if ($child === null) {
throw new OCSException('Subgroup does not exist', 102);
}
try {
$this->groupManager->removeSubGroup($parent, $child);
} catch (\OCP\Group\Exception\NestedGroupsNotSupportedException $e) {
throw new OCSException('Nested groups are not supported by this backend', 104);
}
return new DataResponse();
}

/**
* Get the groups designated as sub-admins of a group
*
* @param string $groupId ID of the group
* @return DataResponse<Http::STATUS_OK, list<string>, array{}>
* @throws OCSException
*
* 200: Admin groups returned
*/
#[AuthorizedAdminSetting(settings: Users::class)]
public function getGroupSubAdmins(string $groupId): DataResponse {
$groupId = urldecode($groupId);
$group = $this->groupManager->get($groupId);
if ($group === null) {
throw new OCSException('Group does not exist', 101);
}
$adminGroups = $this->groupManager->getSubAdmin()->getGroupSubAdminsOfGroup($group);
/** @var list<string> $gids */
$gids = array_map(static fn (IGroup $g): string => $g->getGID(), $adminGroups);
return new DataResponse($gids);
}

/**
* Designate a group as sub-admin of another group
*
* @param string $groupId ID of the group to be administered
* @param string $adminGroupId ID of the group to grant sub-admin rights
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
* @throws OCSException
*
* 200: Group sub-admin created
*/
#[AuthorizedAdminSetting(settings: Users::class)]
#[PasswordConfirmationRequired]
public function addGroupSubAdmin(string $groupId, string $adminGroupId): DataResponse {
$groupId = urldecode($groupId);
$group = $this->groupManager->get($groupId);
if ($group === null) {
throw new OCSException('Group does not exist', 101);
}
$adminGroup = $this->groupManager->get($adminGroupId);
if ($adminGroup === null) {
throw new OCSException('Admin group does not exist', 102);
}
$this->groupManager->getSubAdmin()->createGroupSubAdmin($adminGroup, $group);
return new DataResponse();
}

/**
* Revoke sub-admin rights for a group
*
* @param string $groupId ID of the group
* @param string $adminGroupId ID of the admin group
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
* @throws OCSException
*
* 200: Group sub-admin removed
*/
#[AuthorizedAdminSetting(settings: Users::class)]
#[PasswordConfirmationRequired]
public function removeGroupSubAdmin(string $groupId, string $adminGroupId): DataResponse {
$groupId = urldecode($groupId);
$adminGroupId = urldecode($adminGroupId);
$group = $this->groupManager->get($groupId);
if ($group === null) {
throw new OCSException('Group does not exist', 101);
}
$adminGroup = $this->groupManager->get($adminGroupId);
if ($adminGroup === null) {
throw new OCSException('Admin group does not exist', 102);
}
$this->groupManager->getSubAdmin()->deleteGroupSubAdmin($adminGroup, $group);
return new DataResponse();
}
}
Loading
Loading