Skip to content

Commit

Permalink
Fetch group info for users from DB when oktaMigrationEnabled is true (#…
Browse files Browse the repository at this point in the history
…8105)

* Fetch group info for users from DB when oktaMigrationEnabled is true

* Address PR comments

* Lint backend

* Address PR comments
  • Loading branch information
emyl3 authored Sep 19, 2024
1 parent ad1df86 commit 8f61a00
Show file tree
Hide file tree
Showing 16 changed files with 577 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
import gov.cdc.usds.simplereport.api.model.accountrequest.WaitlistRequest;
import gov.cdc.usds.simplereport.api.model.errors.BadRequestException;
import gov.cdc.usds.simplereport.api.model.errors.IllegalGraphqlArgumentException;
import gov.cdc.usds.simplereport.config.FeatureFlagsConfig;
import gov.cdc.usds.simplereport.db.model.ApiUser;
import gov.cdc.usds.simplereport.db.model.Organization;
import gov.cdc.usds.simplereport.db.model.OrganizationQueueItem;
import gov.cdc.usds.simplereport.idp.repository.OktaRepository;
import gov.cdc.usds.simplereport.properties.SendGridProperties;
import gov.cdc.usds.simplereport.service.ApiUserService;
import gov.cdc.usds.simplereport.service.DbAuthorizationService;
import gov.cdc.usds.simplereport.service.OrganizationQueueService;
import gov.cdc.usds.simplereport.service.OrganizationService;
import gov.cdc.usds.simplereport.service.email.EmailService;
Expand All @@ -27,6 +30,7 @@
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.owasp.encoder.Encode;
Expand All @@ -47,11 +51,13 @@
@RequiredArgsConstructor
@Slf4j
public class AccountRequestController {
private final OrganizationService _os;
private final OrganizationQueueService _oqs;
private final ApiUserService _aus;
private final EmailService _es;
private final SendGridProperties sendGridProperties;
private final OrganizationService _orgService;
private final OrganizationQueueService _orgQueueService;
private final ApiUserService _apiUserService;
private final DbAuthorizationService _dbAuthService;
private final EmailService _emailService;
private final FeatureFlagsConfig _featureFlagsConfig;
private final SendGridProperties _sendGridProperties;
private final ObjectMapper objectMapper = new ObjectMapper();
private final OktaRepository _oktaRepo;

Expand Down Expand Up @@ -80,7 +86,7 @@ public void submitWaitlistRequest(@Valid @RequestBody WaitlistRequest request)
log.info("Waitlist request submitted: {}", sanitizedLog);
}
String subject = "New waitlist request";
_es.send(sendGridProperties.getWaitlistRecipient(), subject, request);
_emailService.send(_sendGridProperties.getWaitlistRecipient(), subject, request);
}

@SuppressWarnings("checkstyle:illegalcatch")
Expand All @@ -98,13 +104,14 @@ public AccountResponse submitOrganizationAccountRequestAddToQueue(
OrganizationUtils.generateOrgExternalId(organizationName, parsedStateCode);

String requestEmail = Translators.parseEmail(request.getEmail());
boolean userExists = _aus.userExists(requestEmail);
boolean userExists = _apiUserService.userExists(requestEmail);
if (userExists) {
throw new BadRequestException(
"This email address is already associated with a SimpleReport user.");
}

OrganizationQueueItem item = _oqs.queueNewRequest(organizationName, orgExternalId, request);
OrganizationQueueItem item =
_orgQueueService.queueNewRequest(organizationName, orgExternalId, request);

return new AccountResponse(item.getExternalId());
} catch (BadRequestException e) {
Expand All @@ -125,7 +132,7 @@ private String checkForDuplicateOrg(String organizationName, String state, Strin
}
organizationName = organizationName.replaceAll("\\s{2,}", " ");

List<Organization> potentialDuplicates = _os.getOrganizationsByName(organizationName);
List<Organization> potentialDuplicates = _orgService.getOrganizationsByName(organizationName);
// Not a duplicate org, can be safely created
if (potentialDuplicates.isEmpty()) {
return organizationName;
Expand All @@ -136,8 +143,9 @@ private String checkForDuplicateOrg(String organizationName, String state, Strin
Optional<Organization> duplicateOrg =
potentialDuplicates.stream().filter(o -> o.getExternalId().startsWith(state)).findFirst();
if (duplicateOrg.isPresent()) {
if (_oktaRepo.fetchAdminUserEmail(duplicateOrg.get()).stream()
.anyMatch(Predicate.isEqual(email))) {
List<String> adminUserEmails = getOrgAdminUserEmails(duplicateOrg.get());

if (adminUserEmails.stream().anyMatch(Predicate.isEqual(email))) {
// Special toasts are shown to admin users trying to re-register their org.
String message =
duplicateOrg.get().getIdentityVerified()
Expand All @@ -156,6 +164,19 @@ private String checkForDuplicateOrg(String organizationName, String state, Strin
return String.join("-", organizationName, state);
}

private List<String> getOrgAdminUserEmails(Organization org) {
List<String> adminUserEmails;
if (_featureFlagsConfig.isOktaMigrationEnabled()) {
adminUserEmails =
_dbAuthService.getOrgAdminUsers(org).stream()
.map(ApiUser::getLoginEmail)
.collect(Collectors.toList());
} else {
adminUserEmails = _oktaRepo.fetchAdminUserEmail(org);
}
return adminUserEmails;
}

private void logOrganizationAccountRequest(@RequestBody @Valid OrganizationAccountRequest request)
throws JsonProcessingException {
if (log.isInfoEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package gov.cdc.usds.simplereport.db.repository;

import gov.cdc.usds.simplereport.config.authorization.OrganizationRole;
import gov.cdc.usds.simplereport.db.model.ApiUser;
import gov.cdc.usds.simplereport.db.model.Facility;
import gov.cdc.usds.simplereport.db.model.Organization;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
Expand All @@ -11,7 +14,9 @@
public interface ApiUserRepository extends EternalSystemManagedEntityRepository<ApiUser> {

String NAME_ORDER =
" order by nameInfo.lastName, nameInfo.firstName, nameInfo.middleName, internalId";
" order by e.nameInfo.lastName, e.nameInfo.firstName, e.nameInfo.middleName, e.internalId";
String API_USER_ROLE_LEFT_JOIN = " LEFT JOIN api_user_role aur ON aur.apiUser = e";
String BY_ORG_AND_UNDELETED_USER = " WHERE aur.organization = :org AND e.isDeleted = false";

// Defining this method explicitly means that findById() will not be able to find soft-deleted
// users,
Expand All @@ -30,4 +35,32 @@ public interface ApiUserRepository extends EternalSystemManagedEntityRepository<

@Query(BASE_QUERY + " and loginEmail IN :emails" + NAME_ORDER)
List<ApiUser> findAllByLoginEmailInOrderByName(Collection<String> emails);

@Query(
value =
"from #{#entityName} e"
+ API_USER_ROLE_LEFT_JOIN
+ BY_ORG_AND_UNDELETED_USER
+ NAME_ORDER)
List<ApiUser> findAllByOrganization(Organization org);

@Query(
value =
"from #{#entityName} e"
+ API_USER_ROLE_LEFT_JOIN
+ BY_ORG_AND_UNDELETED_USER
+ " AND aur.role = :role"
+ NAME_ORDER)
List<ApiUser> findAllByOrganizationAndRole(Organization org, OrganizationRole role);

@Query(
value =
"from #{#entityName} e"
+ API_USER_ROLE_LEFT_JOIN
+ " LEFT JOIN api_user_facility auf ON auf.apiUser = e"
+ " WHERE auf.facility = :facility"
+ " AND e.isDeleted = false"
+ " AND aur.role IN :roles"
+ " GROUP BY e")
List<ApiUser> findAllByFacilityAndRoles(Facility facility, Collection<OrganizationRole> roles);
}
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,12 @@ public void activateOrganization(Organization org) {
inactiveUsernames.removeAll(orgUsernamesMap.get(org.getExternalId()));
}

@Override
public String activateUser(String username) {
inactiveUsernames.remove(username);
return "activationToken";
}

// this method means nothing in a demo env
public String activateOrganizationWithSingleUser(Organization org) {
activateOrganization(org);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,13 @@ private String activateUser(User user) {
}
}

@Override
public String activateUser(String username) {
User oktaUser =
getUserOrThrowError(username, "Cannot activate Okta user with unrecognized username");
return activateUser(oktaUser);
}

@Override
public void activateOrganization(Organization org) {
var users = getOrgAdminUsers(org);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ List<String> updateUserPrivilegesAndGroupAccess(

void activateOrganization(Organization org);

String activateUser(String username);

String activateOrganizationWithSingleUser(Organization org);

List<String> fetchAdminUserEmail(Organization org);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ public class ApiUserService {

@Autowired private ApiUserContextHolder _apiUserContextHolder;

@Autowired private DbAuthorizationService _dbAuthService;

@Autowired private DbOrgRoleClaimsService _dbOrgRoleClaimsService;

@Autowired private FeatureFlagsConfig _featureFlagsConfig;
Expand Down Expand Up @@ -146,11 +148,9 @@ private UserInfo reprovisionUser(
throw new ConflictingUserException();
}

OrganizationRoleClaims claims =
_oktaRepo
.getOrganizationRoleClaimsForUser(apiUser.getLoginEmail())
.orElseThrow(MisconfiguredUserException::new);
if (!org.getExternalId().equals(claims.getOrganizationExternalId())) {
String currentOrgExternalId = getOrgExternalId(apiUser);

if (!org.getExternalId().equals(currentOrgExternalId)) {
throw new ConflictingUserException();
}

Expand All @@ -160,20 +160,22 @@ private UserInfo reprovisionUser(

Set<OrganizationRole> roles = getOrganizationRoles(role, accessAllFacilities);
Set<Facility> facilitiesFound = getFacilitiesToGiveAccess(org, roles, facilities);
Optional<OrganizationRoleClaims> oktaClaims =
_oktaRepo.updateUserPrivileges(apiUser.getLoginEmail(), org, facilitiesFound, roles);
Optional<OrganizationRoles> orgRoles = oktaClaims.map(c -> _orgService.getOrganizationRoles(c));
Optional<OrganizationRoles> updatedOrgRoles;

apiUser.setNameInfo(name);
apiUser.setIsDeleted(false);
apiUser.setFacilities(facilitiesFound);
apiUser.setRoles(roles, org);

Optional<OrganizationRoleClaims> oktaClaims =
_oktaRepo.updateUserPrivileges(apiUser.getLoginEmail(), org, facilitiesFound, roles);
updatedOrgRoles = oktaClaims.map(c -> _orgService.getOrganizationRoles(c));

if (_featureFlagsConfig.isOktaMigrationEnabled()) {
orgRoles = Optional.ofNullable(getOrgRolesFromDB(apiUser));
updatedOrgRoles = Optional.ofNullable(getOrgRolesFromDB(apiUser));
}

UserInfo user = new UserInfo(apiUser, orgRoles, false);
UserInfo user = new UserInfo(apiUser, updatedOrgRoles, false);

log.info(
"User with id={} re-provisioned by user with id={}",
Expand Down Expand Up @@ -247,14 +249,11 @@ public UserInfo updateUserPrivileges(
UUID userId, boolean accessAllFacilities, Set<UUID> facilities, Role role) {
ApiUser apiUser = getApiUser(userId);
String username = apiUser.getLoginEmail();
OrganizationRoleClaims orgClaims =
_oktaRepo
.getOrganizationRoleClaimsForUser(username)
.orElseThrow(MisconfiguredUserException::new);
Organization org = _orgService.getOrganization(orgClaims.getOrganizationExternalId());

String orgExternalId = getOrgExternalId(apiUser);
Organization org = _orgService.getOrganization(orgExternalId);
Set<OrganizationRole> roles = getOrganizationRoles(role, accessAllFacilities);
Set<Facility> facilitiesFound = getFacilitiesToGiveAccess(org, roles, facilities);

Optional<OrganizationRoleClaims> newOrgClaims =
_oktaRepo.updateUserPrivileges(username, org, facilitiesFound, roles);
Optional<OrganizationRoles> orgRoles =
Expand Down Expand Up @@ -597,10 +596,17 @@ public UserInfo getCurrentUserInfo() {
@AuthorizationConfiguration.RequirePermissionManageUsers
public List<ApiUser> getUsersInCurrentOrg() {
Organization org = _orgService.getCurrentOrganization();
final Set<String> orgUserEmails = _oktaRepo.getAllUsersForOrganization(org);
return _apiUserRepo.findAllByLoginEmailInOrderByName(orgUserEmails);
List<ApiUser> usersInOrg;
if (_featureFlagsConfig.isOktaMigrationEnabled()) {
usersInOrg = _dbAuthService.getUsersInOrganization(org);
} else {
final Set<String> orgUserEmails = _oktaRepo.getAllUsersForOrganization(org);
usersInOrg = _apiUserRepo.findAllByLoginEmailInOrderByName(orgUserEmails);
}
return usersInOrg;
}

// To be addressed in #8108
@AuthorizationConfiguration.RequirePermissionManageUsers
public List<ApiUserWithStatus> getUsersAndStatusInCurrentOrg() {
Organization org = _orgService.getCurrentOrganization();
Expand Down Expand Up @@ -829,4 +835,23 @@ private OrganizationRoles getOrgRolesFromDB(ApiUser apiUser) {
_dbOrgRoleClaimsService.getOrganizationRoleClaims(apiUser);
return _orgService.getOrganizationRoles(orgRoleClaims);
}

private String getOrgExternalId(ApiUser apiUser) {
String orgExternalId;
if (_featureFlagsConfig.isOktaMigrationEnabled()) {
Optional<Organization> org = apiUser.getOrganizations().stream().findFirst();
if (org.isPresent()) {
orgExternalId = org.get().getExternalId();
} else {
throw new MisconfiguredUserException();
}
} else {
OrganizationRoleClaims claims =
_oktaRepo
.getOrganizationRoleClaimsForUser(apiUser.getLoginEmail())
.orElseThrow(MisconfiguredUserException::new);
orgExternalId = claims.getOrganizationExternalId();
}
return orgExternalId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package gov.cdc.usds.simplereport.service;

import gov.cdc.usds.simplereport.config.AuthorizationConfiguration;
import gov.cdc.usds.simplereport.config.authorization.OrganizationRole;
import gov.cdc.usds.simplereport.db.model.ApiUser;
import gov.cdc.usds.simplereport.db.model.Facility;
import gov.cdc.usds.simplereport.db.model.Organization;
import gov.cdc.usds.simplereport.db.repository.ApiUserRepository;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Component
@Slf4j
@RequiredArgsConstructor
public class DbAuthorizationService {
private final ApiUserRepository _userRepo;

/**
* Fetches a list of ApiUsers that belong to an Organization sorted by last name, first name, and
* middle name
*
* @param org - Organization
* @return List of ApiUsers that belong to the org
*/
@AuthorizationConfiguration.RequirePermissionManageUsers
public List<ApiUser> getUsersInOrganization(Organization org) {
return _userRepo.findAllByOrganization(org);
}

/**
* Fetches a list of ApiUsers that belong to an Organization and has the ADMIN role, sorted by
* last name, first name, and middle name
*
* @param org - Organization
* @return List of ApiUsers with ADMIN role in the org
*/
public List<ApiUser> getOrgAdminUsers(Organization org) {
return _userRepo.findAllByOrganizationAndRole(org, OrganizationRole.ADMIN);
}

/**
* Fetches a count of ApiUsers that have permission to access the one defined facility and do not
* have the ALL_FACILITIES and/or ADMIN roles
*
* @param facility - Facility to get count for
* @return Integer - count of ApiUsers
*/
public Integer getUsersWithSingleFacilityAccessCount(Facility facility) {
List<ApiUser> users =
_userRepo.findAllByFacilityAndRoles(
facility, List.of(OrganizationRole.USER, OrganizationRole.ENTRY_ONLY));
return users.stream()
.filter(user -> user.getFacilities().size() <= 1)
.collect(Collectors.toList())
.size();
}
}
Loading

0 comments on commit 8f61a00

Please sign in to comment.