Skip to content

Commit

Permalink
Add backend support for Manage Users pagination (#8370)
Browse files Browse the repository at this point in the history
* Add backend pagination support for manage users

* Add paginated search backend

* Add missing override annotation

* Update unit tests

* Handle zero users in org

* Update unit test

* Fix sonar issues

* Move constants to resolver
  • Loading branch information
mpbrown authored Jan 15, 2025
1 parent 4bedc2e commit d63536b
Show file tree
Hide file tree
Showing 10 changed files with 350 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package gov.cdc.usds.simplereport.api.apiuser;

import gov.cdc.usds.simplereport.api.model.ApiUserWithStatus;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.domain.Page;

/**
* When the page content contains zero entries, this could be due to either: - zero search results
* for the query string - OR due to zero users in the entire organization. The value for total users
* in the org helps differentiate this.
*/
@Getter
@Setter
@AllArgsConstructor
public class ManageUsersPageWrapper {

Page<ApiUserWithStatus> pageContent;

int totalUsersInOrg;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
/** Resolver for the graphql User type */
@Controller
public class UserResolver {
public static final int DEFAULT_OKTA_USER_PAGE_SIZE = 10;
public static final int DEFAULT_OKTA_USER_PAGE_OFFSET = 0;

private ApiUserService _userService;

Expand All @@ -38,6 +40,19 @@ public List<ApiUserWithStatus> usersWithStatus() {
return _userService.getUsersAndStatusInCurrentOrg();
}

@QueryMapping
public ManageUsersPageWrapper usersWithStatusPage(
@Argument int pageNumber, @Argument String searchQuery) {
if (pageNumber < 0) {
pageNumber = DEFAULT_OKTA_USER_PAGE_OFFSET;
}
if (!searchQuery.isBlank()) {
return _userService.searchUsersAndStatusInCurrentOrgPaged(
pageNumber, DEFAULT_OKTA_USER_PAGE_SIZE, searchQuery);
}
return _userService.getPagedUsersAndStatusInCurrentOrg(pageNumber, DEFAULT_OKTA_USER_PAGE_SIZE);
}

@QueryMapping
public User user(@Argument UUID id, @Argument String email) {
if (!StringUtils.isBlank(email)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public class DemoOktaRepository implements OktaRepository {
@Value("${simple-report.authorization.environment-name:DEV}")
private String environment;

private static final String NON_EXISTANT_ORG_GET_USERS_ERROR =
"Cannot get Okta users from nonexistent organization.";
private final OrganizationExtractor organizationExtractor;
private final CurrentTenantDataAccessContextHolder tenantDataContextHolder;

Expand Down Expand Up @@ -257,22 +259,34 @@ public void resendActivationEmail(String username) {
// returns ALL users including inactive ones
public Set<String> getAllUsersForOrganization(Organization org) {
if (!orgUsernamesMap.containsKey(org.getExternalId())) {
throw new IllegalGraphqlArgumentException(
"Cannot get Okta users from nonexistent organization.");
throw new IllegalGraphqlArgumentException(NON_EXISTANT_ORG_GET_USERS_ERROR);
}
return orgUsernamesMap.get(org.getExternalId()).stream()
.collect(Collectors.toUnmodifiableSet());
}

public Map<String, UserStatus> getAllUsersWithStatusForOrganization(Organization org) {
if (!orgUsernamesMap.containsKey(org.getExternalId())) {
throw new IllegalGraphqlArgumentException(
"Cannot get Okta users from nonexistent organization.");
throw new IllegalGraphqlArgumentException(NON_EXISTANT_ORG_GET_USERS_ERROR);
}
return orgUsernamesMap.get(org.getExternalId()).stream()
.collect(Collectors.toMap(u -> u, u -> getUserStatus(u)));
}

@Override
public Map<String, UserStatus> getPagedUsersWithStatusForOrganization(
Organization org, int pageNumber, int pageSize) {
if (!orgUsernamesMap.containsKey(org.getExternalId())) {
throw new IllegalGraphqlArgumentException(NON_EXISTANT_ORG_GET_USERS_ERROR);
}
List<String> allOrgUsernamesList =
orgUsernamesMap.get(org.getExternalId()).stream().sorted().collect(Collectors.toList());
int startIndex = pageNumber * pageSize;
int endIndex = Math.min((startIndex + pageSize), allOrgUsernamesList.size());
List<String> pageContent = allOrgUsernamesList.subList(startIndex, endIndex);
return pageContent.stream().collect(Collectors.toMap(u -> u, this::getUserStatus));
}

// this method doesn't mean much in a demo env
public void createOrganization(Organization org) {
String externalId = org.getExternalId();
Expand Down Expand Up @@ -399,7 +413,8 @@ public void reset() {
allUsernames.clear();
}

public Integer getUsersInSingleFacility(Facility facility) {
@Override
public Integer getUsersCountInSingleFacility(Facility facility) {
Integer accessCount = 0;

for (OrganizationRoleClaims existingClaims : usernameOrgRolesMap.values()) {
Expand All @@ -417,6 +432,11 @@ public Integer getUsersInSingleFacility(Facility facility) {
return accessCount;
}

@Override
public Integer getUsersCountInOrganization(Organization org) {
return orgUsernamesMap.get(org.getExternalId()).size();
}

@Override
public String getApplicationStatusForHealthCheck() {
return ACTIVE_LITERAL;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,19 @@ public Map<String, UserStatus> getAllUsersWithStatusForOrganization(Organization
.collect(Collectors.toMap(u -> u.getProfile().getLogin(), User::getStatus));
}

@Override
public Map<String, UserStatus> getPagedUsersWithStatusForOrganization(
Organization org, int pageNumber, int pageSize) {
Group orgDefaultOktaGroup = getDefaultOktaGroup(org);
int afterIndex = pageNumber * pageSize;
List<User> groupUsers =
groupApi.listGroupUsers(orgDefaultOktaGroup.getId(), String.valueOf(afterIndex), pageSize);
return groupUsers.stream()
.collect(
Collectors.toMap(
u -> Objects.requireNonNull(u.getProfile()).getLogin(), User::getStatus));
}

private List<User> getAllUsersForOrg(Organization org) {
PagedList<User> pagedUserList = new PagedList<>();
List<User> allUsers = new ArrayList<>();
Expand Down Expand Up @@ -658,27 +671,39 @@ public Optional<OrganizationRoleClaims> getOrganizationRoleClaimsForUser(String
getUserOrThrowError(username, "Cannot get org external ID for nonexistent user"));
}

public Integer getUsersInSingleFacility(Facility facility) {
String facilityAccessGroupName =
generateFacilityGroupName(
facility.getOrganization().getExternalId(), facility.getInternalId());

List<Group> facilityAccessGroup =
groupApi.listGroups(facilityAccessGroupName, null, null, 1, "stats", null, null, null);
private Integer getUsersCountInOktaGroup(String groupName) {
List<Group> groupList =
groupApi.listGroups(groupName, null, null, 1, "stats", null, null, null);

if (facilityAccessGroup.isEmpty()) {
if (groupList.isEmpty()) {
return 0;
}

try {
LinkedHashMap<String, Object> stats =
(LinkedHashMap) facilityAccessGroup.get(0).getEmbedded().get("stats");
(LinkedHashMap) groupList.get(0).getEmbedded().get("stats");
return ((Integer) stats.get("usersCount"));
} catch (NullPointerException e) {
throw new BadRequestException("Unable to retrieve okta group stats", e);
}
}

@Override
public Integer getUsersCountInSingleFacility(Facility facility) {
String facilityAccessGroupName =
generateFacilityGroupName(
facility.getOrganization().getExternalId(), facility.getInternalId());

return getUsersCountInOktaGroup(facilityAccessGroupName);
}

@Override
public Integer getUsersCountInOrganization(Organization org) {
String orgDefaultGroupName =
generateRoleGroupName(org.getExternalId(), OrganizationRole.getDefault());
return getUsersCountInOktaGroup(orgDefaultGroupName);
}

public PartialOktaUser findUser(String username) {
User user =
getUserOrThrowError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ List<String> updateUserPrivilegesAndGroupAccess(

Map<String, UserStatus> getAllUsersWithStatusForOrganization(Organization org);

/**
* @param org Organization being queried
* @param pageNumber Starts at page number 0
* @param pageSize Number of results per page
* @return Map of usernames to the user status in Okta
*/
Map<String, UserStatus> getPagedUsersWithStatusForOrganization(
Organization org, int pageNumber, int pageSize);

void createOrganization(Organization org);

void activateOrganization(Organization org);
Expand All @@ -83,7 +92,9 @@ List<String> updateUserPrivilegesAndGroupAccess(

Optional<OrganizationRoleClaims> getOrganizationRoleClaimsForUser(String username);

Integer getUsersInSingleFacility(Facility facility);
Integer getUsersCountInSingleFacility(Facility facility);

Integer getUsersCountInOrganization(Organization org);

PartialOktaUser findUser(String username);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import gov.cdc.usds.simplereport.api.ApiUserContextHolder;
import gov.cdc.usds.simplereport.api.CurrentAccountRequestContextHolder;
import gov.cdc.usds.simplereport.api.WebhookContextHolder;
import gov.cdc.usds.simplereport.api.apiuser.ManageUsersPageWrapper;
import gov.cdc.usds.simplereport.api.model.ApiUserWithStatus;
import gov.cdc.usds.simplereport.api.model.Role;
import gov.cdc.usds.simplereport.api.model.errors.ConflictingUserException;
Expand Down Expand Up @@ -46,6 +47,9 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.ScopeNotActiveException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -615,6 +619,59 @@ public List<ApiUser> getUsersInCurrentOrg() {
return usersInOrg;
}

@AuthorizationConfiguration.RequirePermissionManageUsers
public ManageUsersPageWrapper getPagedUsersAndStatusInCurrentOrg(int pageNumber, int pageSize) {
Organization org = _orgService.getCurrentOrganization();

final Map<String, UserStatus> emailsToStatus =
_oktaRepo.getPagedUsersWithStatusForOrganization(org, pageNumber, pageSize);
List<ApiUser> users = _apiUserRepo.findAllByLoginEmailInOrderByName(emailsToStatus.keySet());
List<ApiUserWithStatus> userWithStatusList =
users.stream()
.map(u -> new ApiUserWithStatus(u, emailsToStatus.get(u.getLoginEmail())))
.toList();

Integer userCountInOrg = _oktaRepo.getUsersCountInOrganization(org);
PageRequest pageRequest = PageRequest.of(pageNumber, pageSize);
Page<ApiUserWithStatus> pageContent =
new PageImpl<>(userWithStatusList, pageRequest, userCountInOrg);

return new ManageUsersPageWrapper(pageContent, userCountInOrg);
}

@AuthorizationConfiguration.RequirePermissionManageUsers
public ManageUsersPageWrapper searchUsersAndStatusInCurrentOrgPaged(
int pageNumber, int pageSize, String searchQuery) {
List<ApiUserWithStatus> allUsers = getUsersAndStatusInCurrentOrg();

List<ApiUserWithStatus> totalFilteredUsersList =
allUsers.stream()
.filter(
u -> {
String firstName =
u.getFirstName() == null ? "" : String.format("%s ", u.getFirstName());
String middleName =
u.getMiddleName() == null ? "" : String.format("%s ", u.getMiddleName());
String fullName = firstName + middleName + u.getLastName();
return fullName.toLowerCase().contains(searchQuery.toLowerCase());
})
.toList();

int totalSearchResults = totalFilteredUsersList.size();
int startIndex = pageNumber * pageSize;
int endIndex = Math.min((startIndex + pageSize), totalFilteredUsersList.size());

Organization org = _orgService.getCurrentOrganization();
Integer userCountInOrg = _oktaRepo.getUsersCountInOrganization(org);

List<ApiUserWithStatus> filteredSublist = totalFilteredUsersList.subList(startIndex, endIndex);
PageRequest pageRequest = PageRequest.of(pageNumber, pageSize);
Page<ApiUserWithStatus> pageContent =
new PageImpl<>(filteredSublist, pageRequest, totalSearchResults);

return new ManageUsersPageWrapper(pageContent, userCountInOrg);
}

// To be addressed in #8108
@AuthorizationConfiguration.RequirePermissionManageUsers
public List<ApiUserWithStatus> getUsersAndStatusInCurrentOrg() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ public FacilityStats getFacilityStats(@Argument UUID facilityId) {
usersWithSingleFacilityAccess =
dbAuthorizationService.getUsersWithSingleFacilityAccessCount(facility);
} else {
usersWithSingleFacilityAccess = this.oktaRepository.getUsersInSingleFacility(facility);
usersWithSingleFacilityAccess = this.oktaRepository.getUsersCountInSingleFacility(facility);
}
return FacilityStats.builder()
.usersSingleAccessCount(usersWithSingleFacilityAccess)
Expand Down
Loading

0 comments on commit d63536b

Please sign in to comment.