Skip to content

Commit

Permalink
Bob/6936 query for admin emails (#6962)
Browse files Browse the repository at this point in the history
* hook up new query wiring

* get happy path working

* add in error handling for not found cases

* add tests

* lint

* genericize expected list of ids

* codesmell

* update tests

* fix code issues

* one last code smell
  • Loading branch information
fzhao99 authored Nov 17, 2023
1 parent f3ddfb9 commit 7974f3a
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package gov.cdc.usds.simplereport.api.model.errors;

import graphql.ErrorClassification;
import graphql.ErrorType;
import graphql.GraphQLError;
import graphql.language.SourceLocation;
import java.util.Collections;
import java.util.List;

/** Exception to throw when a organization does not exist. */
public class NonexistentOrgException extends RuntimeException implements GraphQLError {

private static final long serialVersionUID = 1L;

public NonexistentOrgException() {
super("Cannot find organization.");
}

@Override // should-be-defaulted unused interface method
public List<SourceLocation> getLocations() {
return Collections.emptyList();
}

@Override
public ErrorClassification getErrorType() {
return ErrorType.ExecutionAborted;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,10 @@ public Optional<ApiFacility> facility(@Argument UUID id) {
public FacilityStats facilityStats(@Argument UUID facilityId) {
return this._organizationService.getFacilityStats(facilityId);
}

@QueryMapping
@AuthorizationConfiguration.RequireGlobalAdminUser
public List<UUID> getOrgAdminUserIds(@Argument UUID orgId) {
return _organizationService.getOrgAdminUserIds(orgId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import gov.cdc.usds.simplereport.api.model.errors.GenericGraphqlException;
import gov.cdc.usds.simplereport.api.model.errors.IllegalGraphqlArgumentException;
import gov.cdc.usds.simplereport.api.model.errors.IllegalGraphqlFieldAccessException;
import gov.cdc.usds.simplereport.api.model.errors.NonexistentOrgException;
import gov.cdc.usds.simplereport.api.model.errors.NonexistentUserException;
import gov.cdc.usds.simplereport.api.model.errors.OktaAccountUserException;
import gov.cdc.usds.simplereport.api.model.errors.PrivilegeUpdateFacilityAccessException;
Expand Down Expand Up @@ -66,6 +67,12 @@ public DataFetcherExceptionResolver dataFetcherExceptionResolver() {
return Mono.just(singletonList(new GenericGraphqlException(errorMessage, errorPath)));
}

if (exception instanceof NonexistentOrgException) {
String errorMessage =
String.format("header: Cannot find organization.; %s", defaultErrorBody);
return Mono.just(singletonList(new GenericGraphqlException(errorMessage, errorPath)));
}

if (exception instanceof OktaAccountUserException) {
String errorBody = "The user's account needs to be properly setup.";
String errorMessage =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
import gov.cdc.usds.simplereport.api.model.FacilityStats;
import gov.cdc.usds.simplereport.api.model.errors.IllegalGraphqlArgumentException;
import gov.cdc.usds.simplereport.api.model.errors.MisconfiguredUserException;
import gov.cdc.usds.simplereport.api.model.errors.NonexistentOrgException;
import gov.cdc.usds.simplereport.config.AuthorizationConfiguration;
import gov.cdc.usds.simplereport.config.authorization.OrganizationRoleClaims;
import gov.cdc.usds.simplereport.db.model.ApiUser;
import gov.cdc.usds.simplereport.db.model.DeviceType;
import gov.cdc.usds.simplereport.db.model.Facility;
import gov.cdc.usds.simplereport.db.model.FacilityBuilder;
import gov.cdc.usds.simplereport.db.model.Organization;
import gov.cdc.usds.simplereport.db.model.Provider;
import gov.cdc.usds.simplereport.db.model.auxiliary.PersonName;
import gov.cdc.usds.simplereport.db.model.auxiliary.StreetAddress;
import gov.cdc.usds.simplereport.db.repository.ApiUserRepository;
import gov.cdc.usds.simplereport.db.repository.DeviceTypeRepository;
import gov.cdc.usds.simplereport.db.repository.FacilityRepository;
import gov.cdc.usds.simplereport.db.repository.OrganizationRepository;
Expand All @@ -24,6 +27,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
Expand All @@ -40,6 +44,7 @@
@Slf4j
@RequiredArgsConstructor
public class OrganizationService {
private final ApiUserRepository apiUserRepository;

private final OrganizationRepository organizationRepository;
private final FacilityRepository facilityRepository;
Expand Down Expand Up @@ -466,4 +471,26 @@ public FacilityStats getFacilityStats(@Argument UUID facilityId) {
public UUID getPermissibleOrgId(UUID orgId) {
return orgId != null ? orgId : getCurrentOrganization().getInternalId();
}

@AuthorizationConfiguration.RequireGlobalAdminUser
public List<UUID> getOrgAdminUserIds(UUID orgId) {
Organization org =
organizationRepository.findById(orgId).orElseThrow(NonexistentOrgException::new);
List<String> adminUserEmails = oktaRepository.fetchAdminUserEmail(org);

return adminUserEmails.stream()
.map(
email -> {
Optional<ApiUser> foundUser = apiUserRepository.findByLoginEmail(email);
if (foundUser.isEmpty()) {
log.warn(
"Query for admin users in organization "
+ orgId
+ " found a user in Okta but not in the database. Skipping...");
}
return foundUser.map(user -> user.getInternalId()).orElse(null);
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}
1 change: 1 addition & 0 deletions backend/src/main/resources/graphql/admin.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ extend type Query {
pendingOrganizations: [PendingOrganization!]!
organization(id: ID!): Organization
facilityStats(facilityId: ID!): FacilityStats
getOrgAdminUserIds(orgId: ID!): [ID]
}
extend type Mutation {
resendToReportStream(testEventIds: [ID!]!, fhirOnly: Boolean!): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,20 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import gov.cdc.usds.simplereport.api.model.FacilityStats;
import gov.cdc.usds.simplereport.api.model.errors.IllegalGraphqlArgumentException;
import gov.cdc.usds.simplereport.api.model.errors.NonexistentOrgException;
import gov.cdc.usds.simplereport.api.model.errors.OrderingProviderRequiredException;
import gov.cdc.usds.simplereport.config.simplereport.DemoUserConfiguration;
import gov.cdc.usds.simplereport.db.model.DeviceType;
import gov.cdc.usds.simplereport.db.model.Facility;
import gov.cdc.usds.simplereport.db.model.Organization;
import gov.cdc.usds.simplereport.db.model.PatientSelfRegistrationLink;
import gov.cdc.usds.simplereport.db.model.auxiliary.PersonName;
import gov.cdc.usds.simplereport.db.model.auxiliary.StreetAddress;
import gov.cdc.usds.simplereport.db.repository.ApiUserRepository;
import gov.cdc.usds.simplereport.db.repository.DeviceTypeRepository;
import gov.cdc.usds.simplereport.db.repository.FacilityRepository;
import gov.cdc.usds.simplereport.db.repository.OrganizationRepository;
Expand Down Expand Up @@ -52,6 +56,8 @@ class OrganizationServiceTest extends BaseServiceTest<OrganizationService> {
@Autowired private DeviceTypeRepository deviceTypeRepository;
@Autowired @SpyBean private OktaRepository oktaRepository;
@Autowired @SpyBean private PersonRepository personRepository;
@Autowired ApiUserRepository _apiUserRepo;
@Autowired private DemoUserConfiguration userConfiguration;

@BeforeEach
void setupData() {
Expand Down Expand Up @@ -452,4 +458,44 @@ void updateFacilityTest() {
assertThat(updatedFacility.getDeviceTypes()).hasSize(2);
}
}

@Test
@WithSimpleReportSiteAdminUser
void getOrgAdminUserIds_success() {
Organization createdOrg = _dataFactory.saveValidOrganization();
List<String> adminUserEmails = oktaRepository.fetchAdminUserEmail(createdOrg);

List<UUID> expectedIds =
adminUserEmails.stream()
.map(email -> _apiUserRepo.findByLoginEmail(email).get().getInternalId())
.collect(Collectors.toList());

List<UUID> adminIds = _service.getOrgAdminUserIds(createdOrg.getInternalId());
assertThat(adminIds).isEqualTo(expectedIds);
}

@Test
@WithSimpleReportSiteAdminUser
void getOrgAdminUserIds_throws_forNonExistentOrg() {
UUID mismatchedUUID = UUID.fromString("5ebf893a-bb57-48ca-8fc2-1ef6b25e465b");
assertThrows(NonexistentOrgException.class, () -> _service.getOrgAdminUserIds(mismatchedUUID));
}

@Test
@WithSimpleReportSiteAdminUser
void getOrgAdminUserIds_skipsUser_forNonExistentUserInOrg() {
Organization createdOrg = _dataFactory.saveValidOrganization();
List<String> listWithAnExtraEmail = oktaRepository.fetchAdminUserEmail(createdOrg);
listWithAnExtraEmail.add("[email protected]");

when(oktaRepository.fetchAdminUserEmail(createdOrg)).thenReturn(listWithAnExtraEmail);
List<UUID> expectedIds =
listWithAnExtraEmail.stream()
.filter(email -> !email.equals("[email protected]"))
.map(email -> _apiUserRepo.findByLoginEmail(email).get().getInternalId())
.collect(Collectors.toList());

List<UUID> adminIds = _service.getOrgAdminUserIds(createdOrg.getInternalId());
assertThat(adminIds).isEqualTo(expectedIds);
}
}
5 changes: 5 additions & 0 deletions frontend/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ export type Query = {
facilities?: Maybe<Array<Maybe<Facility>>>;
facility?: Maybe<Facility>;
facilityStats?: Maybe<FacilityStats>;
getOrgAdminUserIds?: Maybe<Array<Maybe<Scalars["ID"]["output"]>>>;
organization?: Maybe<Organization>;
organizationLevelDashboardMetrics?: Maybe<OrganizationLevelDashboardMetrics>;
organizations: Array<Organization>;
Expand Down Expand Up @@ -741,6 +742,10 @@ export type QueryFacilityStatsArgs = {
facilityId: Scalars["ID"]["input"];
};

export type QueryGetOrgAdminUserIdsArgs = {
orgId: Scalars["ID"]["input"];
};

export type QueryOrganizationArgs = {
id: Scalars["ID"]["input"];
};
Expand Down

0 comments on commit 7974f3a

Please sign in to comment.