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
57 changes: 57 additions & 0 deletions apps/provisioning_api/lib/Controller/AUserDataOCSController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use OCP\Group\ISubAdmin;
use OCP\IConfig;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
Expand Down Expand Up @@ -251,6 +252,62 @@ protected function getUserSubAdminGroupsData(string $userId): array {
return $groups;
}

/**
* Collect users matching the filters from a paginated source, skipping missing ones.
*
* Whole source pages are consumed, as sources with multiple backends apply $limit and
* $offset per backend. So more than $limit users may be returned, and fewer if no full
* page was found within 10 source pages.
*
* @param \Closure(?int, int): array<?IUser> $search Returns the source page for a limit and offset
* @param bool $enabled Only collect users with this enabled state
* @return array{list<IUser>, int, bool} The matching users, the source offset of the next page,
* and whether the source may have more users
*/
protected function searchFilteredUsers(\Closure $search, ?int $limit, int $offset, bool $enabled): array {
$users = [];
$rounds = 0;
while (true) {
$page = $search($limit, $offset);
foreach ($page as $user) {
if ($user !== null && $user->isEnabled() === $enabled) {
$users[] = $user;
}
}
if ($limit === null || $limit <= 0 || count($page) === 0) {
return [$users, $offset, false];
}
$offset += $limit;
if (count($users) >= $limit || ++$rounds >= 10) {
return [$users, $offset, true];
}
}
}

/**
* Build the `Link: <url>; rel="next"` header if there is a next page.
*
* @param array|bool $items The page of results, or whether more results are known to exist
* @param array<string, mixed> $params Query parameters for the next page
* @param int|null $forceOffset Offset of the next page when it is not `$offset + $limit`,
* e.g. because some results were skipped
* @return array{Link?: string}
*/
protected function buildOffsetNextPageLinkHeader(IURLGenerator $urlGenerator, array|bool $items, array $params, ?int $limit, int $offset, ?int $forceOffset = null): array {
$hasMore = is_bool($items) ? $items : ($limit !== null && $limit > 0 && count($items) >= $limit);
if (!$hasMore) {
return [];
}

$params = array_merge($params, [
'limit' => ($limit ?? 100),
'offset' => $forceOffset ?? $offset + ($limit ?? 100),
]);
$path = (string)parse_url($this->request->getRequestUri(), PHP_URL_PATH);
$url = $urlGenerator->getAbsoluteURL($path) . '?' . http_build_query($params);
return ['Link' => '<' . $url . '>; rel="next"'];
}

/**
* A full group has id, name, usercount, disabled, canAdd and canRemove. Only
* the displayname is cached; usercount/disabled are not cached. So this only
Expand Down
27 changes: 23 additions & 4 deletions apps/provisioning_api/lib/Controller/GroupsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
Expand All @@ -54,6 +55,7 @@ public function __construct(
IRootFolder $rootFolder,
private LoggerInterface $logger,
GroupDisplayNameCache $groupDisplayNameCache,
private IURLGenerator $urlGenerator,
) {
parent::__construct($appName,
$request,
Expand Down Expand Up @@ -189,14 +191,15 @@ public function getGroupUsers(string $groupId): DataResponse {
* @param string $search Text to search for
* @param int|null $limit Limit the amount of groups returned
* @param int $offset Offset for searching for groups
* @param bool|null $enabled Only return users with this enabled state, or all users if null. Use the `Link` header for the next page offset
*
* @return DataResponse<Http::STATUS_OK, array{users: array<string, Provisioning_APIUserDetails|array{id: string}>, groups: list<Provisioning_APIUserDetailsGroupDisplayname>}, array{}>
* @return DataResponse<Http::STATUS_OK, array{users: array<string, Provisioning_APIUserDetails|array{id: string}>, groups: list<Provisioning_APIUserDetailsGroupDisplayname>}, array{Link?: string}>
* @throws OCSException
*
* 200: Group users details returned
*/
#[NoAdminRequired]
public function getGroupUsersDetails(string $groupId, string $search = '', ?int $limit = null, int $offset = 0): DataResponse {
public function getGroupUsersDetails(string $groupId, string $search = '', ?int $limit = null, int $offset = 0, ?bool $enabled = null): DataResponse {
$groupId = urldecode($groupId);
$currentUser = $this->userSession->getUser();

Expand All @@ -212,7 +215,18 @@ 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);
$hasMore = null;
$nextOffset = null;
if ($enabled !== null) {
[$users, $nextOffset, $hasMore] = $this->searchFilteredUsers(
fn (?int $limit, int $offset): array => $group->searchUsers($search, $limit, $offset),
$limit,
$offset,
enabled: $enabled,
);
} else {
$users = $group->searchUsers($search, $limit, $offset);
}

// Extract required number
$usersDetails = [];
Expand All @@ -233,10 +247,15 @@ public function getGroupUsersDetails(string $groupId, string $search = '', ?int
// continue if a users ceased to exist.
}
}
$params = [
'search' => $search,
'enabled' => $enabled
];
$headers = $this->buildOffsetNextPageLinkHeader($this->urlGenerator, $hasMore ?? $users, $params, $limit, $offset, $nextOffset);
return new DataResponse([
'users' => $usersDetails,
'groups' => $this->findGroupsWithDisplayname($usersDetails),
]);
], headers: $headers);
}

throw new OCSException('The requested group could not be found', OCSController::RESPOND_NOT_FOUND);
Expand Down
90 changes: 74 additions & 16 deletions apps/provisioning_api/lib/Controller/UsersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,34 +152,65 @@ public function getUsers(string $search = '', ?int $limit = null, int $offset =
* @param string $search Text to search for
* @param int|null $limit Limit the amount of groups returned
* @param int $offset Offset for searching for groups
* @return DataResponse<Http::STATUS_OK, array{users: array<string, Provisioning_APIUserDetails|array{id: string}>, groups: list<Provisioning_APIUserDetailsGroupDisplayname>}, array{}>
* @param bool|null $enabled Only return users with this enabled state, or all users if null. Use the `Link` header for the next page offset
* @return DataResponse<Http::STATUS_OK, array{users: array<string, Provisioning_APIUserDetails|array{id: string}>, groups: list<Provisioning_APIUserDetailsGroupDisplayname>}, array{Link?: string}>
*
* 200: Users details returned
*/
#[NoAdminRequired]
public function getUsersDetails(string $search = '', ?int $limit = null, int $offset = 0): DataResponse {
public function getUsersDetails(string $search = '', ?int $limit = null, int $offset = 0, ?bool $enabled = null): DataResponse {
$currentUser = $this->userSession->getUser();
$users = [];
$hasMore = null;
$nextOffset = null;

// Admin? Or SubAdmin?
$uid = $currentUser->getUID();
$subAdminManager = $this->groupManager->getSubAdmin();
$isAdmin = $this->groupManager->isAdmin($uid);
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid);
if ($isAdmin || $isDelegatedAdmin) {
$users = $this->userManager->search($search, $limit, $offset);
$users = array_keys($users);
} elseif ($subAdminManager->isSubAdmin($currentUser)) {
$subAdminOfGroups = $subAdminManager->getSubAdminsGroups($currentUser);
foreach ($subAdminOfGroups as $key => $group) {
$subAdminOfGroups[$key] = $group->getGID();
$searchUsers = fn (?int $limit, int $offset): array => $this->userManager->search($search, $limit, $offset);
if ($enabled !== null) {
[$users, $nextOffset, $hasMore] = $this->searchFilteredUsers(
$searchUsers,
$limit,
$offset,
enabled: $enabled,
);
$users = array_map(fn (IUser $user): string => $user->getUID(), $users);
} else {
$users = $searchUsers($limit, $offset);
$users = array_keys($users);
}
} elseif ($subAdminManager->isSubAdmin($currentUser)) {
if ($enabled !== null) {
$users = [];
/* The groups are searched together, so offset and limit are applied to the combined list */
$tempLimit = ($limit === null ? null : $limit + $offset);
foreach ($subAdminManager->getSubAdminsGroups($currentUser) as $group) {
foreach ($group->searchUsers($search) as $user) {
if ($user->isEnabled() === $enabled) {
$users[$user->getUID()] = $user->getUID();
}
}
if (($tempLimit !== null) && (count($users) >= $tempLimit)) {
break;
}
}
$users = array_slice(array_values($users), $offset, $limit);
} else {
$subAdminOfGroups = $subAdminManager->getSubAdminsGroups($currentUser);
foreach ($subAdminOfGroups as $key => $group) {
$subAdminOfGroups[$key] = $group->getGID();
}

$users = [];
foreach ($subAdminOfGroups as $group) {
$users[] = array_keys($this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
$users = [];
foreach ($subAdminOfGroups as $group) {
$users[] = array_keys($this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
}
$users = array_merge(...$users);
}
$users = array_merge(...$users);
}

$usersDetails = [];
Expand All @@ -203,10 +234,15 @@ public function getUsersDetails(string $search = '', ?int $limit = null, int $of
}
}

$params = [
'search' => $search,
'enabled' => $enabled
];
$headers = $this->buildOffsetNextPageLinkHeader($this->urlGenerator, $hasMore ?? $users, $params, $limit, $offset, $nextOffset);
return new DataResponse([
'users' => $usersDetails,
'groups' => $this->findGroupsWithDisplayname($usersDetails),
]);
], headers: $headers);
}

/**
Expand Down Expand Up @@ -297,14 +333,16 @@ public function getDisabledUsersDetails(string $search = '', ?int $limit = null,
* @param string $search Text to search for
* @param ?int $limit Limit the amount of users returned
* @param int $offset Offset
* @return DataResponse<Http::STATUS_OK, array{users: array<string, Provisioning_APIUserDetails|array{id: string}>}, array{}>
* @param bool|null $enabled Only return users with this enabled state, or all users if null. Use the `Link` header for the next page offset
* @return DataResponse<Http::STATUS_OK, array{users: array<string, Provisioning_APIUserDetails|array{id: string}>}, array{Link?: string}>
*
* 200: Users details returned based on last logged in information
*/
#[AuthorizedAdminSetting(settings:Users::class)]
public function getLastLoggedInUsers(string $search = '',
?int $limit = null,
int $offset = 0,
?bool $enabled = null,
): DataResponse {
$currentUser = $this->userSession->getUser();
if ($currentUser === null) {
Expand All @@ -318,9 +356,24 @@ public function getLastLoggedInUsers(string $search = '',
}

$users = [];
$hasMore = null;
$nextOffset = null;

// For Admin alone user sorting based on lastLogin. For sub admin and groups this is not supported
$users = $this->userManager->getLastLoggedInUsers($limit, $offset, $search);
if ($enabled !== null) {
[$users, $nextOffset, $hasMore] = $this->searchFilteredUsers(
fn (?int $limit, int $offset): array => array_map(
fn (string $userId): ?IUser => $this->userManager->get($userId),
$this->userManager->getLastLoggedInUsers($limit, $offset, $search),
),
$limit,
$offset,
enabled: $enabled,
);
$users = array_map(fn (IUser $user): string => $user->getUID(), $users);
} else {
$users = $this->userManager->getLastLoggedInUsers($limit, $offset, $search);
}

$usersDetails = [];
foreach ($users as $userId) {
Expand All @@ -342,9 +395,14 @@ public function getLastLoggedInUsers(string $search = '',
}
}

$params = [
'search' => $search,
'enabled' => $enabled
];
$headers = $this->buildOffsetNextPageLinkHeader($this->urlGenerator, $hasMore ?? $users, $params, $limit, $offset, $nextOffset);
return new DataResponse([
'users' => $usersDetails
]);
], headers: $headers);
}


Expand Down
17 changes: 17 additions & 0 deletions apps/provisioning_api/openapi-administration.json
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,16 @@
"default": 0
}
},
{
"name": "enabled",
"in": "query",
"description": "Only return users with this enabled state, or all users if null. Use the `Link` header for the next page offset",
"schema": {
"type": "boolean",
"nullable": true,
"default": null
}
},
{
"name": "OCS-APIRequest",
"in": "header",
Expand All @@ -1488,6 +1498,13 @@
"responses": {
"200": {
"description": "Users details returned based on last logged in information",
"headers": {
"Link": {
"schema": {
"type": "string"
}
}
},
"content": {
"application/json": {
"schema": {
Expand Down
Loading
Loading