From 7974f3a996bb4cd8713425700397e09629a32d97 Mon Sep 17 00:00:00 2001 From: fzhao99 Date: Fri, 17 Nov 2023 15:01:29 -0500 Subject: [PATCH] Bob/6936 query for admin emails (#6962) * 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 --- .../model/errors/NonexistentOrgException.java | 28 +++++++++++ .../organization/OrganizationResolver.java | 6 +++ .../simplereport/config/GraphQlConfig.java | 7 +++ .../service/OrganizationService.java | 27 +++++++++++ .../src/main/resources/graphql/admin.graphqls | 1 + .../service/OrganizationServiceTest.java | 46 +++++++++++++++++++ frontend/src/generated/graphql.tsx | 5 ++ 7 files changed, 120 insertions(+) create mode 100644 backend/src/main/java/gov/cdc/usds/simplereport/api/model/errors/NonexistentOrgException.java diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/api/model/errors/NonexistentOrgException.java b/backend/src/main/java/gov/cdc/usds/simplereport/api/model/errors/NonexistentOrgException.java new file mode 100644 index 0000000000..bef8ac5364 --- /dev/null +++ b/backend/src/main/java/gov/cdc/usds/simplereport/api/model/errors/NonexistentOrgException.java @@ -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 getLocations() { + return Collections.emptyList(); + } + + @Override + public ErrorClassification getErrorType() { + return ErrorType.ExecutionAborted; + } +} diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/api/organization/OrganizationResolver.java b/backend/src/main/java/gov/cdc/usds/simplereport/api/organization/OrganizationResolver.java index 81d1daf987..9d93603f48 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/api/organization/OrganizationResolver.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/api/organization/OrganizationResolver.java @@ -125,4 +125,10 @@ public Optional facility(@Argument UUID id) { public FacilityStats facilityStats(@Argument UUID facilityId) { return this._organizationService.getFacilityStats(facilityId); } + + @QueryMapping + @AuthorizationConfiguration.RequireGlobalAdminUser + public List getOrgAdminUserIds(@Argument UUID orgId) { + return _organizationService.getOrgAdminUserIds(orgId); + } } diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/config/GraphQlConfig.java b/backend/src/main/java/gov/cdc/usds/simplereport/config/GraphQlConfig.java index dcd20f5e35..bb4009c256 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/config/GraphQlConfig.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/config/GraphQlConfig.java @@ -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; @@ -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 = diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/service/OrganizationService.java b/backend/src/main/java/gov/cdc/usds/simplereport/service/OrganizationService.java index da033d878c..b1fe498b24 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/service/OrganizationService.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/service/OrganizationService.java @@ -4,8 +4,10 @@ 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; @@ -13,6 +15,7 @@ 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; @@ -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; @@ -40,6 +44,7 @@ @Slf4j @RequiredArgsConstructor public class OrganizationService { + private final ApiUserRepository apiUserRepository; private final OrganizationRepository organizationRepository; private final FacilityRepository facilityRepository; @@ -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 getOrgAdminUserIds(UUID orgId) { + Organization org = + organizationRepository.findById(orgId).orElseThrow(NonexistentOrgException::new); + List adminUserEmails = oktaRepository.fetchAdminUserEmail(org); + + return adminUserEmails.stream() + .map( + email -> { + Optional 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()); + } } diff --git a/backend/src/main/resources/graphql/admin.graphqls b/backend/src/main/resources/graphql/admin.graphqls index 1a7ac2512b..f463e71e19 100644 --- a/backend/src/main/resources/graphql/admin.graphqls +++ b/backend/src/main/resources/graphql/admin.graphqls @@ -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 diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/service/OrganizationServiceTest.java b/backend/src/test/java/gov/cdc/usds/simplereport/service/OrganizationServiceTest.java index 222a7d0d7d..9caa3a84fb 100644 --- a/backend/src/test/java/gov/cdc/usds/simplereport/service/OrganizationServiceTest.java +++ b/backend/src/test/java/gov/cdc/usds/simplereport/service/OrganizationServiceTest.java @@ -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; @@ -52,6 +56,8 @@ class OrganizationServiceTest extends BaseServiceTest { @Autowired private DeviceTypeRepository deviceTypeRepository; @Autowired @SpyBean private OktaRepository oktaRepository; @Autowired @SpyBean private PersonRepository personRepository; + @Autowired ApiUserRepository _apiUserRepo; + @Autowired private DemoUserConfiguration userConfiguration; @BeforeEach void setupData() { @@ -452,4 +458,44 @@ void updateFacilityTest() { assertThat(updatedFacility.getDeviceTypes()).hasSize(2); } } + + @Test + @WithSimpleReportSiteAdminUser + void getOrgAdminUserIds_success() { + Organization createdOrg = _dataFactory.saveValidOrganization(); + List adminUserEmails = oktaRepository.fetchAdminUserEmail(createdOrg); + + List expectedIds = + adminUserEmails.stream() + .map(email -> _apiUserRepo.findByLoginEmail(email).get().getInternalId()) + .collect(Collectors.toList()); + + List 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 listWithAnExtraEmail = oktaRepository.fetchAdminUserEmail(createdOrg); + listWithAnExtraEmail.add("nonexistent@example.com"); + + when(oktaRepository.fetchAdminUserEmail(createdOrg)).thenReturn(listWithAnExtraEmail); + List expectedIds = + listWithAnExtraEmail.stream() + .filter(email -> !email.equals("nonexistent@example.com")) + .map(email -> _apiUserRepo.findByLoginEmail(email).get().getInternalId()) + .collect(Collectors.toList()); + + List adminIds = _service.getOrgAdminUserIds(createdOrg.getInternalId()); + assertThat(adminIds).isEqualTo(expectedIds); + } } diff --git a/frontend/src/generated/graphql.tsx b/frontend/src/generated/graphql.tsx index 3b2620fbe9..80651a07ae 100644 --- a/frontend/src/generated/graphql.tsx +++ b/frontend/src/generated/graphql.tsx @@ -701,6 +701,7 @@ export type Query = { facilities?: Maybe>>; facility?: Maybe; facilityStats?: Maybe; + getOrgAdminUserIds?: Maybe>>; organization?: Maybe; organizationLevelDashboardMetrics?: Maybe; organizations: Array; @@ -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"]; };