diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/config/authorization/DemoAuthenticationConfiguration.java b/backend/src/main/java/gov/cdc/usds/simplereport/config/authorization/DemoAuthenticationConfiguration.java index c5c988ea0b..4af95cae4a 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/config/authorization/DemoAuthenticationConfiguration.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/config/authorization/DemoAuthenticationConfiguration.java @@ -1,10 +1,12 @@ package gov.cdc.usds.simplereport.config.authorization; import gov.cdc.usds.simplereport.config.BeanProfiles; +import gov.cdc.usds.simplereport.config.FeatureFlagsConfig; import gov.cdc.usds.simplereport.config.simplereport.DemoUserConfiguration; import gov.cdc.usds.simplereport.config.simplereport.DemoUserConfiguration.DemoUser; import gov.cdc.usds.simplereport.idp.repository.OktaRepository; import gov.cdc.usds.simplereport.service.AuthorizationService; +import gov.cdc.usds.simplereport.service.DbOrgRoleClaimsService; import gov.cdc.usds.simplereport.service.model.IdentityAttributes; import gov.cdc.usds.simplereport.service.model.IdentitySupplier; import jakarta.servlet.Filter; @@ -86,8 +88,11 @@ public FilterRegistrationBean identityFilter() { public AuthorizationService getDemoAuthorizationService( OktaRepository oktaRepo, IdentitySupplier supplier, - DemoUserConfiguration demoUserConfiguration) { - return new DemoAuthorizationService(oktaRepo, supplier, demoUserConfiguration); + DemoUserConfiguration demoUserConfiguration, + DbOrgRoleClaimsService dbOrgRoleClaimsService, + FeatureFlagsConfig featureFlagsConfig) { + return new DemoAuthorizationService( + oktaRepo, supplier, demoUserConfiguration, dbOrgRoleClaimsService, featureFlagsConfig); } @Bean @@ -149,14 +154,20 @@ public static class DemoAuthorizationService implements AuthorizationService { private final IdentitySupplier _getCurrentUser; private final OktaRepository _oktaRepo; private final Set _adminGroupMemberSet; + private final DbOrgRoleClaimsService _dbOrgRoleClaimsService; + private final FeatureFlagsConfig _featureFlagsConfig; public DemoAuthorizationService( OktaRepository oktaRepo, IdentitySupplier getCurrent, - DemoUserConfiguration demoUserConfiguration) { + DemoUserConfiguration demoUserConfiguration, + DbOrgRoleClaimsService dbOrgRoleClaimsService, + FeatureFlagsConfig featureFlagsConfig) { super(); this._getCurrentUser = getCurrent; this._oktaRepo = oktaRepo; + this._dbOrgRoleClaimsService = dbOrgRoleClaimsService; + this._featureFlagsConfig = featureFlagsConfig; _adminGroupMemberSet = demoUserConfiguration.getSiteAdminEmails().stream() @@ -168,7 +179,14 @@ public List findAllOrganizationRoles() { String username = Optional.ofNullable(_getCurrentUser.get()).orElseThrow().getUsername(); Optional claims = _oktaRepo.getOrganizationRoleClaimsForUser(username); - return claims.isEmpty() ? List.of() : List.of(claims.get()); + List oktaOrgRoleClaims = + claims.isEmpty() ? List.of() : List.of(claims.get()); + if (!isSiteAdmin() && _featureFlagsConfig.isOktaMigrationEnabled()) { + List dbOrgRoleClaims = + _dbOrgRoleClaimsService.getOrganizationRoleClaims(username); + return dbOrgRoleClaims; + } + return oktaOrgRoleClaims; } @Override diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUser.java b/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUser.java index a44d85eb8f..9abb4ff3ff 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUser.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUser.java @@ -87,6 +87,12 @@ public void setRoles(Set newOrgRoles, Organization org) { } } + public Set getOrganizations() { + return this.roleAssignments.stream() + .map(ApiUserRole::getOrganization) + .collect(Collectors.toSet()); + } + public ApiUser clearRolesAndFacilities() { this.roleAssignments.clear(); this.facilityAssignments.clear(); diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUserRole.java b/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUserRole.java index 59e118184b..f13ae839ae 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUserRole.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/db/model/ApiUserRole.java @@ -20,6 +20,7 @@ public class ApiUserRole extends AuditedEntity { @ManyToOne @JoinColumn(name = "organization_id", nullable = false) + @Getter private Organization organization; @Column(nullable = false, columnDefinition = "organization_role") diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/service/ApiUserService.java b/backend/src/main/java/gov/cdc/usds/simplereport/service/ApiUserService.java index e94ed68d26..2348e6f7f4 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/service/ApiUserService.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/service/ApiUserService.java @@ -76,6 +76,8 @@ public class ApiUserService { @Autowired private ApiUserContextHolder _apiUserContextHolder; + @Autowired private DbOrgRoleClaimsService _dbOrgRoleClaimsService; + @Autowired private FeatureFlagsConfig _featureFlagsConfig; private void createUserUpdatedAuditLog(Object authorId, Object updatedUserId) { @@ -158,15 +160,19 @@ private UserInfo reprovisionUser( Set roles = getOrganizationRoles(role, accessAllFacilities); Set facilitiesFound = getFacilitiesToGiveAccess(org, roles, facilities); - Optional roleClaims = + Optional oktaClaims = _oktaRepo.updateUserPrivileges(apiUser.getLoginEmail(), org, facilitiesFound, roles); + Optional orgRoles = oktaClaims.map(c -> _orgService.getOrganizationRoles(c)); apiUser.setNameInfo(name); apiUser.setIsDeleted(false); apiUser.setFacilities(facilitiesFound); apiUser.setRoles(roles, org); - Optional orgRoles = roleClaims.map(c -> _orgService.getOrganizationRoles(c)); + if (_featureFlagsConfig.isOktaMigrationEnabled()) { + orgRoles = Optional.ofNullable(getOrgRolesFromDB(apiUser)); + } + UserInfo user = new UserInfo(apiUser, orgRoles, false); log.info( @@ -194,9 +200,14 @@ private UserInfo createUserHelper( apiUser.setFacilities(facilitiesFound); apiUser.setRoles(roles, org); - Optional roleClaims = + Optional oktaClaims = _oktaRepo.createUser(userIdentity, org, facilitiesFound, roles, active); - Optional orgRoles = roleClaims.map(c -> _orgService.getOrganizationRoles(c)); + Optional orgRoles = oktaClaims.map(c -> _orgService.getOrganizationRoles(c)); + + if (_featureFlagsConfig.isOktaMigrationEnabled()) { + orgRoles = Optional.ofNullable(getOrgRolesFromDB(apiUser)); + } + UserInfo user = new UserInfo(apiUser, orgRoles, false); log.info( @@ -220,14 +231,10 @@ public UserInfo updateUser(UUID userId, PersonName name) { apiUser = _apiUserRepo.save(apiUser); IdentityAttributes userIdentity = new IdentityAttributes(username, name); - Optional roleClaims = _oktaRepo.updateUser(userIdentity); - Optional orgRoles = roleClaims.map(_orgService::getOrganizationRoles); - if (!_featureFlagsConfig.isOktaMigrationEnabled() && orgRoles.isPresent()) { - setRolesAndFacilities(orgRoles.get(), apiUser); - } + Optional oktaRoleClaims = _oktaRepo.updateUser(userIdentity); - UserInfo user = new UserInfo(apiUser, orgRoles, false); + UserInfo user = consolidateUser(apiUser, oktaRoleClaims); createUserUpdatedAuditLog( apiUser.getInternalId(), getCurrentApiUser().getInternalId().toString()); @@ -277,17 +284,13 @@ public UserInfo updateUserEmail(UUID userId, String email) { apiUser.setLoginEmail(email); apiUser = _apiUserRepo.save(apiUser); - Optional roleClaims = _oktaRepo.updateUserEmail(userIdentity, email); - Optional orgRoles = roleClaims.map(_orgService::getOrganizationRoles); - - if (!_featureFlagsConfig.isOktaMigrationEnabled() && orgRoles.isPresent()) { - setRolesAndFacilities(orgRoles.get(), apiUser); - } + Optional oktaRoleClaims = + _oktaRepo.updateUserEmail(userIdentity, email); createUserUpdatedAuditLog( apiUser.getInternalId(), getCurrentApiUser().getInternalId().toString()); - return new UserInfo(apiUser, orgRoles, false); + return consolidateUser(apiUser, oktaRoleClaims); } @AuthorizationConfiguration.RequirePermissionManageTargetUser @@ -295,17 +298,12 @@ public UserInfo resetUserPassword(UUID userId) { ApiUser apiUser = getApiUser(userId); String username = apiUser.getLoginEmail(); _oktaRepo.resetUserPassword(username); - OrganizationRoleClaims orgClaims = + OrganizationRoleClaims oktaClaims = _oktaRepo .getOrganizationRoleClaimsForUser(username) .orElseThrow(MisconfiguredUserException::new); - Organization org = _orgService.getOrganization(orgClaims.getOrganizationExternalId()); - OrganizationRoles orgRoles = _orgService.getOrganizationRoles(org, orgClaims); - if (!_featureFlagsConfig.isOktaMigrationEnabled()) { - setRolesAndFacilities(orgRoles, apiUser); - } - return new UserInfo(apiUser, Optional.of(orgRoles), false); + return consolidateUser(apiUser, Optional.ofNullable(oktaClaims)); } @AuthorizationConfiguration.RequirePermissionManageTargetUser @@ -313,16 +311,11 @@ public UserInfo resetUserMfa(UUID userId) { ApiUser apiUser = getApiUser(userId); String username = apiUser.getLoginEmail(); _oktaRepo.resetUserMfa(username); - OrganizationRoleClaims orgClaims = + OrganizationRoleClaims oktaClaims = _oktaRepo .getOrganizationRoleClaimsForUser(username) .orElseThrow(MisconfiguredUserException::new); - Organization org = _orgService.getOrganization(orgClaims.getOrganizationExternalId()); - OrganizationRoles orgRoles = _orgService.getOrganizationRoles(org, orgClaims); - if (!_featureFlagsConfig.isOktaMigrationEnabled()) { - setRolesAndFacilities(orgRoles, apiUser); - } - return new UserInfo(apiUser, Optional.of(orgRoles), false); + return consolidateUser(apiUser, Optional.ofNullable(oktaClaims)); } @AuthorizationConfiguration.RequirePermissionManageTargetUserNotSelf @@ -346,16 +339,11 @@ public UserInfo reactivateUser(UUID userId) { ApiUser apiUser = getApiUser(userId); String username = apiUser.getLoginEmail(); _oktaRepo.reactivateUser(username); - OrganizationRoleClaims orgClaims = + OrganizationRoleClaims oktaClaims = _oktaRepo .getOrganizationRoleClaimsForUser(username) .orElseThrow(MisconfiguredUserException::new); - Organization org = _orgService.getOrganization(orgClaims.getOrganizationExternalId()); - OrganizationRoles orgRoles = _orgService.getOrganizationRoles(org, orgClaims); - if (!_featureFlagsConfig.isOktaMigrationEnabled()) { - setRolesAndFacilities(orgRoles, apiUser); - } - return new UserInfo(apiUser, Optional.of(orgRoles), false); + return consolidateUser(apiUser, Optional.ofNullable(oktaClaims)); } // This method is to re-send the invitation email to join SimpleReport @@ -364,16 +352,11 @@ public UserInfo resendActivationEmail(UUID userId) { ApiUser apiUser = getApiUser(userId); String username = apiUser.getLoginEmail(); _oktaRepo.resendActivationEmail(username); - OrganizationRoleClaims orgClaims = + OrganizationRoleClaims oktaClaims = _oktaRepo .getOrganizationRoleClaimsForUser(username) .orElseThrow(MisconfiguredUserException::new); - Organization org = _orgService.getOrganization(orgClaims.getOrganizationExternalId()); - OrganizationRoles orgRoles = _orgService.getOrganizationRoles(org, orgClaims); - if (!_featureFlagsConfig.isOktaMigrationEnabled()) { - setRolesAndFacilities(orgRoles, apiUser); - } - return new UserInfo(apiUser, Optional.of(orgRoles), false); + return consolidateUser(apiUser, Optional.ofNullable(oktaClaims)); } /** @@ -598,14 +581,8 @@ public UserInfo getCurrentUserInfoForWhoAmI() { ApiUser currentUser = getCurrentApiUser(); Optional currentOrgRoles = _orgService.getCurrentOrganizationRoles(); boolean isAdmin = _authService.isSiteAdmin(); - if (!_featureFlagsConfig.isOktaMigrationEnabled() && currentOrgRoles.isPresent() && !isAdmin) { - try { - setRolesAndFacilities(currentOrgRoles.get(), currentUser); - } catch (PrivilegeUpdateFacilityAccessException e) { - log.warn( - "Could not migrate roles and facilities for user with id={} because facilities were invalid", - currentUser.getInternalId()); - } + if (!_featureFlagsConfig.isOktaMigrationEnabled() && !isAdmin) { + setRolesAndFacilities(currentOrgRoles, currentUser); } return new UserInfo(currentUser, currentOrgRoles, isAdmin); } @@ -644,11 +621,7 @@ public UserInfo getUser(final UUID userId) { final ApiUser apiUser = optApiUser.get(); PartialOktaUser oktaUser = _oktaRepo.findUser(apiUser.getLoginEmail()); - return consolidateUser( - apiUser, - oktaUser.getOrganizationRoleClaims(), - oktaUser.getStatus(), - oktaUser.isSiteAdmin()); + return consolidateUser(apiUser, oktaUser); } @AuthorizationConfiguration.RequireGlobalAdminUser @@ -710,52 +683,46 @@ public UserInfo getUserByLoginEmail(String loginEmail) { throw new RestrictedAccessUserException(); } - return consolidateUser( - apiUser, - oktaUser.getOrganizationRoleClaims(), - oktaUser.getStatus(), - oktaUser.isSiteAdmin()); + return consolidateUser(apiUser, oktaUser); } catch (IllegalGraphqlArgumentException | UnidentifiedUserException e) { throw new OktaAccountUserException(); } } - private UserInfo consolidateUser( - ApiUser apiUser, - Optional optClaims, - UserStatus userStatus, - boolean isSiteAdmin) { - - OrganizationRoleClaims claims = optClaims.orElseThrow(UnidentifiedUserException::new); - - // use the target user's org so response is built correctly even if site admin is the requester - Organization org = _orgService.getOrganization(claims.getOrganizationExternalId()); + private OrganizationRoles getOrganizationRoles( + Optional oktaClaims, ApiUser apiUser, boolean isSiteAdmin) { + OrganizationRoles orgRoles = null; + if (oktaClaims.isPresent()) { + orgRoles = _orgService.getOrganizationRoles(oktaClaims.get()); + } - List facilities = _orgService.getFacilities(org); - Set facilitiesSet = new HashSet<>(facilities); + if (!isSiteAdmin) { + if (_featureFlagsConfig.isOktaMigrationEnabled()) { + orgRoles = getOrgRolesFromDB(apiUser); + } else { + setRolesAndFacilities(Optional.ofNullable(orgRoles), apiUser); + } + } - boolean allFacilityAccess = claims.grantsAllFacilityAccess(); - Set accessibleFacilities = - allFacilityAccess - ? facilitiesSet - : facilities.stream() - .filter(f -> claims.getFacilities().contains(f.getInternalId())) - .collect(Collectors.toSet()); + return orgRoles; + } + private UserInfo consolidateUser(ApiUser apiUser, PartialOktaUser oktaUser) { + boolean isSiteAdmin = oktaUser.isSiteAdmin(); + UserStatus userStatus = oktaUser.getStatus(); + OrganizationRoleClaims oktaClaims = + oktaUser.getOrganizationRoleClaims().orElseThrow(UnidentifiedUserException::new); OrganizationRoles orgRoles = - new OrganizationRoles(org, accessibleFacilities, claims.getGrantedRoles()); - if (!_featureFlagsConfig.isOktaMigrationEnabled() && !isSiteAdmin) { - try { - setRolesAndFacilities(orgRoles, apiUser); - } catch (PrivilegeUpdateFacilityAccessException e) { - log.warn( - "Could not migrate roles and facilities for user with id={} because facilities were invalid", - apiUser.getInternalId()); - } - } + getOrganizationRoles(Optional.ofNullable(oktaClaims), apiUser, isSiteAdmin); + return new UserInfo(apiUser, Optional.of(orgRoles), isSiteAdmin, userStatus); } + private UserInfo consolidateUser(ApiUser apiUser, Optional oktaClaims) { + OrganizationRoles orgRoles = getOrganizationRoles(oktaClaims, apiUser, false); + return new UserInfo(apiUser, Optional.of(orgRoles), false); + } + @AuthorizationConfiguration.RequireGlobalAdminUser public void updateUserPrivilegesAndGroupAccess( String username, String orgExternalId, boolean allFacilitiesAccess, Role role) { @@ -835,17 +802,31 @@ private Set dedupeFoundAndPassedInFacilityIds( * @param apiUser * @return ApiUser */ - private ApiUser setRolesAndFacilities(OrganizationRoles orgRoles, ApiUser apiUser) { - Organization org = orgRoles.getOrganization(); - Set roles = orgRoles.getGrantedRoles(); - List facilitiesInternalIds = - orgRoles.getFacilities().stream() - .map(IdentifiedEntity::getInternalId) - .collect(Collectors.toList()); - Set facilitiesToGiveAccessTo = - getFacilitiesToGiveAccess(org, roles, new HashSet<>(facilitiesInternalIds)); - apiUser.setFacilities(facilitiesToGiveAccessTo); - apiUser.setRoles(roles, org); + private ApiUser setRolesAndFacilities(Optional orgRoles, ApiUser apiUser) { + try { + if (orgRoles.isEmpty()) { + throw new MisconfiguredUserException(); + } + Organization org = orgRoles.get().getOrganization(); + Set roles = orgRoles.get().getGrantedRoles(); + List facilitiesInternalIds = + orgRoles.get().getFacilities().stream() + .map(IdentifiedEntity::getInternalId) + .collect(Collectors.toList()); + Set facilitiesToGiveAccessTo = + getFacilitiesToGiveAccess(org, roles, new HashSet<>(facilitiesInternalIds)); + apiUser.setFacilities(facilitiesToGiveAccessTo); + apiUser.setRoles(roles, org); + } catch (MisconfiguredUserException | PrivilegeUpdateFacilityAccessException e) { + log.warn( + "Could not migrate roles and facilities for user with id={}", apiUser.getInternalId()); + } return apiUser; } + + private OrganizationRoles getOrgRolesFromDB(ApiUser apiUser) { + OrganizationRoleClaims orgRoleClaims = + _dbOrgRoleClaimsService.getOrganizationRoleClaims(apiUser); + return _orgService.getOrganizationRoles(orgRoleClaims); + } } diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/service/DbOrgRoleClaimsService.java b/backend/src/main/java/gov/cdc/usds/simplereport/service/DbOrgRoleClaimsService.java new file mode 100644 index 0000000000..74af45bd43 --- /dev/null +++ b/backend/src/main/java/gov/cdc/usds/simplereport/service/DbOrgRoleClaimsService.java @@ -0,0 +1,144 @@ +package gov.cdc.usds.simplereport.service; + +import gov.cdc.usds.simplereport.api.model.errors.MisconfiguredUserException; +import gov.cdc.usds.simplereport.api.model.errors.NonexistentUserException; +import gov.cdc.usds.simplereport.config.authorization.OrganizationRole; +import gov.cdc.usds.simplereport.config.authorization.OrganizationRoleClaims; +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 gov.cdc.usds.simplereport.service.model.IdentitySupplier; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RequiredArgsConstructor +public class DbOrgRoleClaimsService { + private final ApiUserRepository _userRepo; + private final IdentitySupplier _getCurrentUser; + + /** + * Fetches the user by username and returns a list of OrganizationRoleClaims from the DB throws an + * exception if user does not have ONLY one org + * + * @param username - String of user email/username + * @return List of OrganizationRoleClaims from the DB + */ + public List getOrganizationRoleClaims(String username) { + try { + ApiUser user = + _userRepo.findByLoginEmail(username).orElseThrow(NonexistentUserException::new); + return List.of(getOrganizationRoleClaims(user)); + } catch (NonexistentUserException | MisconfiguredUserException e) { + return new ArrayList<>(); + } + } + + public OrganizationRoleClaims getOrganizationRoleClaims(ApiUser user) { + Set orgs = user.getOrganizations(); + if (orgs.size() != 1) { + log.error("Misconfigured organizations in DB for User ID: {}", user.getInternalId()); + throw new MisconfiguredUserException(); + } + Set roles = user.getRoles(); + Set facilityUUIDs = + user.getFacilities().stream().map(Facility::getInternalId).collect(Collectors.toSet()); + + String orgExternalId = orgs.stream().findFirst().get().getExternalId(); + return new OrganizationRoleClaims(orgExternalId, facilityUUIDs, roles); + } + + /** + * Compares two lists of OrganizationRoleClaims and checks if they are equal; If they are not + * equal, a message is logged with the affected User ID + * + * @param oktaClaims - List from Okta + * @param dbClaims - List from DB + * @return boolean + */ + public boolean checkOrgRoleClaimsEquality( + List oktaClaims, List dbClaims) { + boolean hasEqualRoleClaims = false; + if (oktaClaims.size() == dbClaims.size()) { + List sanitizedOktaClaims = sanitizeOktaOrgRoleClaims(oktaClaims); + hasEqualRoleClaims = + sanitizedOktaClaims.stream() + .allMatch( + sanitizedOktaClaim -> + dbClaims.stream() + .anyMatch(dbClaim -> equalOrgRoleClaim(sanitizedOktaClaim, dbClaim))); + } + if (!hasEqualRoleClaims) { + logUnequalClaims(); + } + + return hasEqualRoleClaims; + } + + /** Logs a message saying OrganizationRoleClaims are unequal with the affected User ID */ + private void logUnequalClaims() { + // WIP: Currently assumes check is for the current user + // This may change based on where checkOrgRoleClaimsEquality is called + String username = _getCurrentUser.get().getUsername(); + ApiUser user = _userRepo.findByLoginEmail(username).orElseThrow(NonexistentUserException::new); + log.error( + "Okta OrganizationRoleClaims do not match database OrganizationRoleClaims for User ID: {}", + user.getInternalId()); + } + + /** + * Removes NO_ACCESS OrganizationRole in order to compare with OrganizationRole from DB, NO_ACCESS + * role does not exist in DB, only in Okta + * + * @param oktaClaims - List from Okta + * @return list of OrganizationRoleClaims without NO_ACCESS OrganizationRole + */ + private List sanitizeOktaOrgRoleClaims( + List oktaClaims) { + return oktaClaims.stream() + .map( + oktaClaim -> { + Set orgRoles = + oktaClaim.getGrantedRoles().stream() + .filter(c -> c != OrganizationRole.NO_ACCESS) + .collect(Collectors.toSet()); + return new OrganizationRoleClaims( + oktaClaim.getOrganizationExternalId(), oktaClaim.getFacilities(), orgRoles); + }) + .collect(Collectors.toList()); + } + + /** + * Compares two OrganizationRoleClaims for equality + * + * @param oktaClaim - OrganizationRoleClaims from Okta + * @param dbClaim - OrganizationRoleClaims from DB + * @return boolean + */ + private boolean equalOrgRoleClaim( + OrganizationRoleClaims oktaClaim, OrganizationRoleClaims dbClaim) { + Set oktaOrgRoles = oktaClaim.getGrantedRoles(); + Set dbOrgRoles = dbClaim.getGrantedRoles(); + boolean equalRoles = CollectionUtils.isEqualCollection(oktaOrgRoles, dbOrgRoles); + + Set oktaFacilities = oktaClaim.getFacilities(); + Set dbFacilities = dbClaim.getFacilities(); + boolean equalFacilities = CollectionUtils.isEqualCollection(oktaFacilities, dbFacilities); + + String oktaExternalOrgId = oktaClaim.getOrganizationExternalId(); + String dbExternalOrgId = dbClaim.getOrganizationExternalId(); + boolean equalOrg = StringUtils.equals(oktaExternalOrgId, dbExternalOrgId); + + return equalRoles && equalFacilities && equalOrg; + } +} diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/service/LoggedInAuthorizationService.java b/backend/src/main/java/gov/cdc/usds/simplereport/service/LoggedInAuthorizationService.java index ab8446840f..932ef74191 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/service/LoggedInAuthorizationService.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/service/LoggedInAuthorizationService.java @@ -2,12 +2,14 @@ import gov.cdc.usds.simplereport.config.AuthorizationProperties; import gov.cdc.usds.simplereport.config.BeanProfiles; +import gov.cdc.usds.simplereport.config.FeatureFlagsConfig; import gov.cdc.usds.simplereport.config.authorization.OrganizationExtractor; import gov.cdc.usds.simplereport.config.authorization.OrganizationRoleClaims; import gov.cdc.usds.simplereport.config.authorization.TenantDataAuthenticationProvider; import gov.cdc.usds.simplereport.service.errors.NobodyAuthenticatedException; import java.util.List; import java.util.Set; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Profile; import org.springframework.security.core.Authentication; @@ -19,16 +21,25 @@ /** Real-world implementation of AuthorizationService for IDP-integrated environments. */ @Component +@Slf4j @Profile("!" + BeanProfiles.NO_SECURITY) public class LoggedInAuthorizationService implements AuthorizationService { private OrganizationExtractor _extractor; private AuthorizationProperties _authProperties; + private DbOrgRoleClaimsService _dbOrgRoleClaimsService; + private FeatureFlagsConfig _featureFlagsConfig; + public LoggedInAuthorizationService( - OrganizationExtractor extractor, AuthorizationProperties authProperties) { + OrganizationExtractor extractor, + AuthorizationProperties authProperties, + DbOrgRoleClaimsService dbOrgRoleClaimsService, + FeatureFlagsConfig featureFlagsConfig) { this._extractor = extractor; this._authProperties = authProperties; + this._dbOrgRoleClaimsService = dbOrgRoleClaimsService; + this._featureFlagsConfig = featureFlagsConfig; } private Authentication getCurrentAuth() { @@ -42,7 +53,18 @@ private Authentication getCurrentAuth() { @Override public List findAllOrganizationRoles() { Authentication currentAuth = getCurrentAuth(); - return _extractor.convert(currentAuth.getAuthorities()); + List oktaOrgRoleClaims = + _extractor.convert(currentAuth.getAuthorities()); + + if (!isSiteAdmin()) { + String username = currentAuth.getName(); + List dbOrgRoleClaims = + _dbOrgRoleClaimsService.getOrganizationRoleClaims(username); + if (_featureFlagsConfig.isOktaMigrationEnabled()) { + return dbOrgRoleClaims; + } + } + return oktaOrgRoleClaims; } @Override diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/api/BaseFullStackTest.java b/backend/src/test/java/gov/cdc/usds/simplereport/api/BaseFullStackTest.java index 67be31209c..789d9d877e 100644 --- a/backend/src/test/java/gov/cdc/usds/simplereport/api/BaseFullStackTest.java +++ b/backend/src/test/java/gov/cdc/usds/simplereport/api/BaseFullStackTest.java @@ -14,7 +14,6 @@ import gov.cdc.usds.simplereport.db.repository.SupportedDiseaseRepository; import gov.cdc.usds.simplereport.idp.repository.DemoOktaRepository; import gov.cdc.usds.simplereport.logging.LoggingConstants; -import gov.cdc.usds.simplereport.service.ApiUserService; import gov.cdc.usds.simplereport.service.AuditLoggerService; import gov.cdc.usds.simplereport.service.DiseaseService; import gov.cdc.usds.simplereport.service.OrganizationService; @@ -48,11 +47,9 @@ @ActiveProfiles("test") public abstract class BaseFullStackTest { - @Autowired private CurrentTenantDataAccessContextHolder _tenantDataAccessContextHolder; @Autowired private DbTruncator _truncator; @Autowired protected TestDataFactory _dataFactory; @SpyBean protected OrganizationService _orgService; - @SpyBean protected ApiUserService _apiUserService; @Autowired protected DemoOktaRepository _oktaRepo; @SpyBean AuditLoggerService auditLoggerServiceSpy; @Captor private ArgumentCaptor auditLogCaptor; diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/api/BaseNonSpringBootTestConfiguration.java b/backend/src/test/java/gov/cdc/usds/simplereport/api/BaseNonSpringBootTestConfiguration.java index c8f1c16058..0606f6c553 100644 --- a/backend/src/test/java/gov/cdc/usds/simplereport/api/BaseNonSpringBootTestConfiguration.java +++ b/backend/src/test/java/gov/cdc/usds/simplereport/api/BaseNonSpringBootTestConfiguration.java @@ -1,6 +1,9 @@ package gov.cdc.usds.simplereport.api; +import gov.cdc.usds.simplereport.config.FeatureFlagsConfig; +import gov.cdc.usds.simplereport.db.repository.ApiUserRepository; import gov.cdc.usds.simplereport.service.ApiUserService; +import gov.cdc.usds.simplereport.service.DbOrgRoleClaimsService; import gov.cdc.usds.simplereport.service.DiseaseService; import gov.cdc.usds.simplereport.service.OrganizationService; import org.springframework.boot.test.mock.mockito.MockBean; @@ -11,9 +14,12 @@ public class BaseNonSpringBootTestConfiguration { // Dependencies of TenantDataAccessFilter + @MockBean private ApiUserRepository _mockApiUserRepository; @MockBean private ApiUserService _mockApiUserService; @MockBean private ApiUserContextHolder _mockApiUserContextHolder; + @MockBean private DbOrgRoleClaimsService _mockDbOrgRoleClaimsService; @MockBean private OrganizationService _mockOrganizationService; @MockBean private CurrentTenantDataAccessContextHolder _mockContextHolder; @MockBean private DiseaseService _mockDiseaseService; + @MockBean private FeatureFlagsConfig _featureFlagsConfig; } diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/db/repository/BaseRepositoryTest.java b/backend/src/test/java/gov/cdc/usds/simplereport/db/repository/BaseRepositoryTest.java index 8ecab3abbe..fa1614dc1f 100644 --- a/backend/src/test/java/gov/cdc/usds/simplereport/db/repository/BaseRepositoryTest.java +++ b/backend/src/test/java/gov/cdc/usds/simplereport/db/repository/BaseRepositoryTest.java @@ -1,6 +1,7 @@ package gov.cdc.usds.simplereport.db.repository; import gov.cdc.usds.simplereport.config.DataSourceConfiguration; +import gov.cdc.usds.simplereport.service.DbOrgRoleClaimsService; import gov.cdc.usds.simplereport.service.OrganizationInitializingService; import gov.cdc.usds.simplereport.test_util.DbTruncator; import gov.cdc.usds.simplereport.test_util.SliceTestConfiguration; @@ -25,6 +26,7 @@ @AutoConfigureTestDatabase(replace = Replace.NONE) @Import({ SliceTestConfiguration.class, + DbOrgRoleClaimsService.class, DbTruncator.class, DataSourceConfiguration.class, SpringTemplateEngine.class diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/service/ApiUserServiceTest.java b/backend/src/test/java/gov/cdc/usds/simplereport/service/ApiUserServiceTest.java index 5702a91278..c15fa18d1b 100644 --- a/backend/src/test/java/gov/cdc/usds/simplereport/service/ApiUserServiceTest.java +++ b/backend/src/test/java/gov/cdc/usds/simplereport/service/ApiUserServiceTest.java @@ -4,6 +4,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.reset; @@ -21,6 +23,7 @@ import gov.cdc.usds.simplereport.api.model.errors.PrivilegeUpdateFacilityAccessException; import gov.cdc.usds.simplereport.api.model.errors.RestrictedAccessUserException; import gov.cdc.usds.simplereport.api.model.errors.UnidentifiedFacilityException; +import gov.cdc.usds.simplereport.config.FeatureFlagsConfig; import gov.cdc.usds.simplereport.config.authorization.OrganizationRole; import gov.cdc.usds.simplereport.db.model.ApiUser; import gov.cdc.usds.simplereport.db.model.Facility; @@ -46,6 +49,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.security.access.AccessDeniedException; import org.springframework.test.context.TestPropertySource; @@ -57,6 +61,8 @@ class ApiUserServiceTest extends BaseServiceTest { @Autowired @SpyBean OktaRepository _oktaRepo; @Autowired OrganizationService _organizationService; @Autowired FacilityRepository facilityRepository; + @Autowired @SpyBean DbOrgRoleClaimsService _dbOrgRoleClaimsService; + @MockBean FeatureFlagsConfig _featureFlagsConfig; @Autowired private TestDataFactory _dataFactory; Set emptySet = Collections.emptySet(); @@ -118,7 +124,7 @@ void getUsersAndStatusInCurrentOrg_success() { @Test @WithSimpleReportOrgAdminUser - void getUser_adminUser_success() { + void getUser_withAdminUser_withOktaMigrationDisabled_success() { initSampleData(); final String email = "allfacilities@example.com"; // member of DIS_ORG @@ -126,6 +132,7 @@ void getUser_adminUser_success() { UserInfo userInfo = _service.getUser(apiUser.getInternalId()); + verify(_dbOrgRoleClaimsService, times(0)).getOrganizationRoleClaims((ApiUser) any()); assertEquals(email, userInfo.getEmail()); roleCheck( userInfo, @@ -135,17 +142,22 @@ void getUser_adminUser_success() { @Test @WithSimpleReportOrgAdminUser - void getUser_adminUserWrongOrg_error() { + void getUser_withAdminUser_withOktaMigrationEnabled_success() { initSampleData(); + when(_featureFlagsConfig.isOktaMigrationEnabled()).thenReturn(true); + final String email = "allfacilities@example.com"; // member of DIS_ORG + ApiUser apiUser = _apiUserRepo.findByLoginEmail(email).get(); - // captain@pirate.com is a member of DAT_ORG, but requester is admin of DIS_ORG - ApiUser apiUser = _apiUserRepo.findByLoginEmail("captain@pirate.com").get(); - assertSecurityError(() -> _service.getUser(apiUser.getInternalId())); + UserInfo userInfo = _service.getUser(apiUser.getInternalId()); + + verify(_dbOrgRoleClaimsService, times(1)).getOrganizationRoleClaims((ApiUser) any()); + assertEquals(email, userInfo.getEmail()); + roleCheck(userInfo, EnumSet.of(OrganizationRole.USER, OrganizationRole.ALL_FACILITIES)); } @Test @WithSimpleReportSiteAdminUser - void getUser_superUser_success() { + void getUser_withSuperUser_withOktaMigrationDisabled_success() { initSampleData(); final String email = "allfacilities@example.com"; // member of DIS_ORG @@ -153,15 +165,55 @@ void getUser_superUser_success() { UserInfo userInfo = _service.getUser(apiUser.getInternalId()); + verify(_dbOrgRoleClaimsService, times(0)).getOrganizationRoleClaims((ApiUser) any()); assertEquals(email, userInfo.getEmail()); roleCheck( userInfo, EnumSet.of( OrganizationRole.NO_ACCESS, OrganizationRole.USER, OrganizationRole.ALL_FACILITIES)); + + // check roles and facilities for site admin were not created + String currentUsername = _service.getCurrentUserInfo().getEmail(); + assertEquals(TestUserIdentities.SITE_ADMIN_USER, currentUsername); + ApiUser siteAdminUser = _apiUserRepo.findByLoginEmail(currentUsername).get(); + assertTrue(siteAdminUser.getRoles().isEmpty()); + assertTrue(siteAdminUser.getFacilities().isEmpty()); } @Test - void getUser_standardUser_error() { + @WithSimpleReportSiteAdminUser + void getUser_withSuperUser_withOktaMigrationEnabled_success() { + initSampleData(); + when(_featureFlagsConfig.isOktaMigrationEnabled()).thenReturn(true); + + final String email = "allfacilities@example.com"; // member of DIS_ORG + ApiUser apiUser = _apiUserRepo.findByLoginEmail(email).get(); + + UserInfo userInfo = _service.getUser(apiUser.getInternalId()); + + verify(_dbOrgRoleClaimsService, times(1)).getOrganizationRoleClaims((ApiUser) any()); + assertEquals(email, userInfo.getEmail()); + roleCheck(userInfo, EnumSet.of(OrganizationRole.USER, OrganizationRole.ALL_FACILITIES)); + + // check roles and facilities for site admin were not created + String currentUsername = _service.getCurrentUserInfo().getEmail(); + ApiUser siteAdminUser = _apiUserRepo.findByLoginEmail(currentUsername).get(); + assertTrue(siteAdminUser.getRoles().isEmpty()); + assertTrue(siteAdminUser.getFacilities().isEmpty()); + } + + @Test + @WithSimpleReportOrgAdminUser + void getUser_withAdminUserInDifferentOrg_error() { + initSampleData(); + + // captain@pirate.com is a member of DAT_ORG, but requester is admin of DIS_ORG + ApiUser apiUser = _apiUserRepo.findByLoginEmail("captain@pirate.com").get(); + assertSecurityError(() -> _service.getUser(apiUser.getInternalId())); + } + + @Test + void getUser_withStandardUser_error() { initSampleData(); ApiUser apiUser = _apiUserRepo.findByLoginEmail("allfacilities@example.com").get(); @@ -170,9 +222,9 @@ void getUser_standardUser_error() { @Test @WithSimpleReportOrgAdminUser - void createUserInCurrentOrg_orgAdmin_success() { + void createUserInCurrentOrg_withOrgAdmin_withOktaMigrationDisabled_success() { initSampleData(); - var facilityIdSet = + Set facilityIdSet = facilityRepository .findAllByOrganization(_organizationService.getCurrentOrganization()) .stream() @@ -185,6 +237,7 @@ void createUserInCurrentOrg_orgAdmin_success() { Role.USER, false, facilityIdSet); + verify(_dbOrgRoleClaimsService, times(0)).getOrganizationRoleClaims((ApiUser) any()); assertEquals("newuser@example.com", newUserInfo.getEmail()); @@ -204,7 +257,36 @@ void createUserInCurrentOrg_orgAdmin_success() { @Test @WithSimpleReportOrgAdminUser - void createUserInCurrentOrg_reprovisionDeletedUser_success() { + void createUserInCurrentOrg_withOrgAdmin_withOktaMigrationEnabled_success() { + initSampleData(); + when(_featureFlagsConfig.isOktaMigrationEnabled()).thenReturn(true); + Set facilityIdSet = + facilityRepository + .findAllByOrganization(_organizationService.getCurrentOrganization()) + .stream() + .map(IdentifiedEntity::getInternalId) + .collect(Collectors.toSet()); + UserInfo newUserInfo = + _service.createUserInCurrentOrg( + "newuser@example.com", + new PersonName("First", "Middle", "Last", "Jr"), + Role.USER, + false, + facilityIdSet); + verify(_dbOrgRoleClaimsService, times(1)).getOrganizationRoleClaims((ApiUser) any()); + + assertEquals("newuser@example.com", newUserInfo.getEmail()); + assertThat(facilityIdSet) + .hasSameElementsAs( + newUserInfo.getFacilities().stream() + .map(IdentifiedEntity::getInternalId) + .collect(Collectors.toSet())); + assertThat(newUserInfo.getRoles()).hasSameElementsAs(List.of(OrganizationRole.USER)); + } + + @Test + @WithSimpleReportOrgAdminUser + void createUserInCurrentOrg_reprovisionDeletedUser_withOktaMigrationDisabled_success() { initSampleData(); // disable a user from this organization ApiUser orgUser = _apiUserRepo.findByLoginEmail("nobody@example.com").get(); @@ -219,6 +301,7 @@ void createUserInCurrentOrg_reprovisionDeletedUser_success() { Role.USER, true, Set.of()); + verify(_dbOrgRoleClaimsService, times(0)).getOrganizationRoleClaims((ApiUser) any()); // the user will be re-enabled and updated assertEquals("nobody@example.com", reprovisionedUserInfo.getEmail()); @@ -247,6 +330,45 @@ void createUserInCurrentOrg_reprovisionDeletedUser_success() { OrganizationRole.ALL_FACILITIES)); } + @Test + @WithSimpleReportOrgAdminUser + void createUserInCurrentOrg_reprovisionDeletedUser_withOktaMigrationEnabled_success() { + initSampleData(); + when(_featureFlagsConfig.isOktaMigrationEnabled()).thenReturn(true); + + // disable a user from this organization + ApiUser orgUser = _apiUserRepo.findByLoginEmail("nobody@example.com").get(); + orgUser.setIsDeleted(true); + _apiUserRepo.save(orgUser); + _oktaRepo.setUserIsActive(orgUser.getLoginEmail(), false); + + UserInfo reprovisionedUserInfo = + _service.createUserInCurrentOrg( + "nobody@example.com", + new PersonName("First", "Middle", "Last", "Jr"), + Role.USER, + true, + Set.of()); + verify(_dbOrgRoleClaimsService, times(1)).getOrganizationRoleClaims((ApiUser) any()); + + // the user will be re-enabled and updated + assertEquals("nobody@example.com", reprovisionedUserInfo.getEmail()); + + var facilities = + facilityRepository + .findAllByOrganization(_organizationService.getCurrentOrganization()) + .stream() + .map(IdentifiedEntity::getInternalId) + .toList(); + assertThat( + reprovisionedUserInfo.getFacilities().stream() + .map(IdentifiedEntity::getInternalId) + .toList()) + .hasSameElementsAs(facilities); + assertThat(reprovisionedUserInfo.getRoles()) + .hasSameElementsAs(List.of(OrganizationRole.USER, OrganizationRole.ALL_FACILITIES)); + } + @Test @WithSimpleReportOrgAdminUser void createUserInCurrentOrg_enabledUserExists_error() { @@ -355,16 +477,55 @@ void editUserEmail_orgAdmin_success() { assertEquals(userInfo.getEmail(), newEmail); } + @Test + @WithSimpleReportSiteAdminUser + void resetUserPassword_withSiteAdmin_withOktaMigrationDisabled_success() { + initSampleData(); + + final String email = "allfacilities@example.com"; // member of DIS_ORG + ApiUser apiUser = _apiUserRepo.findByLoginEmail(email).get(); + + UserInfo userInfo = _service.resetUserPassword(apiUser.getInternalId()); + verify(_oktaRepo, times(1)).resetUserPassword(email); + verify(_dbOrgRoleClaimsService, times(0)).getOrganizationRoleClaims((ApiUser) any()); + + assertEquals(apiUser.getInternalId(), userInfo.getInternalId()); + + // check roles and facilities for site admin were not created + String currentUsername = _service.getCurrentUserInfo().getEmail(); + assertEquals(TestUserIdentities.SITE_ADMIN_USER, currentUsername); + ApiUser siteAdminUser = _apiUserRepo.findByLoginEmail(currentUsername).get(); + assertTrue(siteAdminUser.getRoles().isEmpty()); + assertTrue(siteAdminUser.getFacilities().isEmpty()); + } + @Test @WithSimpleReportOrgAdminUser - void resetUserPassword_orgAdmin_success() { + void resetUserPassword_withOrgAdmin_withOktaMigrationDisabled_success() { initSampleData(); + when(_featureFlagsConfig.isOktaMigrationEnabled()).thenReturn(true); final String email = "allfacilities@example.com"; // member of DIS_ORG ApiUser apiUser = _apiUserRepo.findByLoginEmail(email).get(); UserInfo userInfo = _service.resetUserPassword(apiUser.getInternalId()); verify(_oktaRepo, times(1)).resetUserPassword(email); + verify(_dbOrgRoleClaimsService, times(1)).getOrganizationRoleClaims((ApiUser) any()); + + assertEquals(apiUser.getInternalId(), userInfo.getInternalId()); + } + + @Test + @WithSimpleReportOrgAdminUser + void resetUserPassword_withOrgAdmin_withOktaMigrationEnabled_success() { + initSampleData(); + + final String email = "allfacilities@example.com"; // member of DIS_ORG + ApiUser apiUser = _apiUserRepo.findByLoginEmail(email).get(); + + UserInfo userInfo = _service.resetUserPassword(apiUser.getInternalId()); + verify(_oktaRepo, times(1)).resetUserPassword(email); + verify(_dbOrgRoleClaimsService, times(0)).getOrganizationRoleClaims((ApiUser) any()); assertEquals(apiUser.getInternalId(), userInfo.getInternalId()); } @@ -412,19 +573,47 @@ void resendActivationEmail_orgAdmin_success() { @Test @WithSimpleReportSiteAdminUser - void getUserByLoginEmail_success() { + void getUserByLoginEmail_withOktaMigrationDisabled_success() { + initSampleData(); + + String email = "allfacilities@example.com"; + ApiUser apiUser = _apiUserRepo.findByLoginEmail(email).get(); + UserInfo userInfo = _service.getUserByLoginEmail(email); + + verify(_dbOrgRoleClaimsService, times(0)).getOrganizationRoleClaims((ApiUser) any()); + assertEquals(apiUser.getInternalId(), userInfo.getInternalId()); + assertEquals(email, userInfo.getEmail()); + assertEquals(UserStatus.ACTIVE, userInfo.getUserStatus()); + assertEquals(false, userInfo.getIsAdmin()); + assertThat(userInfo.getFacilities()).hasSize(2); + assertThat(userInfo.getPermissions()).hasSize(10); + } + + @Test + @WithSimpleReportSiteAdminUser + void getUserByLoginEmail_withOktaMigrationEnabled_success() { initSampleData(); + when(_featureFlagsConfig.isOktaMigrationEnabled()).thenReturn(true); String email = "allfacilities@example.com"; ApiUser apiUser = _apiUserRepo.findByLoginEmail(email).get(); UserInfo userInfo = _service.getUserByLoginEmail(email); + verify(_dbOrgRoleClaimsService, times(1)).getOrganizationRoleClaims((ApiUser) any()); + assertEquals(apiUser.getInternalId(), userInfo.getInternalId()); assertEquals(email, userInfo.getEmail()); assertEquals(UserStatus.ACTIVE, userInfo.getUserStatus()); assertEquals(false, userInfo.getIsAdmin()); assertThat(userInfo.getFacilities()).hasSize(2); assertThat(userInfo.getPermissions()).hasSize(10); + + // check roles and facilities for site admin were not created + String currentUsername = _service.getCurrentUserInfo().getEmail(); + assertEquals(TestUserIdentities.SITE_ADMIN_USER, currentUsername); + ApiUser siteAdminUser = _apiUserRepo.findByLoginEmail(currentUsername).get(); + assertTrue(siteAdminUser.getRoles().isEmpty()); + assertTrue(siteAdminUser.getFacilities().isEmpty()); } @Test @@ -502,14 +691,31 @@ void getUserByLoginEmail_not_authorized() { @Test @WithSimpleReportSiteAdminUser - void getUserByLoginEmail_invalidClaims_success() { + void getUserByLoginEmail_invalidClaims_withOktaMigrationDisabled_success() { + initSampleData(); + String email = "invalid@example.com"; + + // we should be able to retrieve user info for a user with invalid claims (no facility access) + // without failing + UserInfo result = _service.getUserByLoginEmail(email); + verify(_dbOrgRoleClaimsService, times(0)).getOrganizationRoleClaims((ApiUser) any()); + assertThat(result.getFacilities()).isEmpty(); + assertEquals(List.of(OrganizationRole.NO_ACCESS, OrganizationRole.USER), result.getRoles()); + } + + @Test + @WithSimpleReportSiteAdminUser + void getUserByLoginEmail_invalidClaims_withOktaMigrationEnabled_success() { initSampleData(); String email = "invalid@example.com"; + when(_featureFlagsConfig.isOktaMigrationEnabled()).thenReturn(true); // we should be able to retrieve user info for a user with invalid claims (no facility access) // without failing UserInfo result = _service.getUserByLoginEmail(email); + verify(_dbOrgRoleClaimsService, times(1)).getOrganizationRoleClaims((ApiUser) any()); assertThat(result.getFacilities()).isEmpty(); + assertEquals(List.of(OrganizationRole.USER), result.getRoles()); } @Test diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/service/DbOrgRoleClaimsServiceTest.java b/backend/src/test/java/gov/cdc/usds/simplereport/service/DbOrgRoleClaimsServiceTest.java new file mode 100644 index 0000000000..e089f7a12f --- /dev/null +++ b/backend/src/test/java/gov/cdc/usds/simplereport/service/DbOrgRoleClaimsServiceTest.java @@ -0,0 +1,190 @@ +package gov.cdc.usds.simplereport.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import gov.cdc.usds.simplereport.api.model.errors.MisconfiguredUserException; +import gov.cdc.usds.simplereport.config.authorization.OrganizationRole; +import gov.cdc.usds.simplereport.config.authorization.OrganizationRoleClaims; +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 gov.cdc.usds.simplereport.test_util.OrganizationRoleClaimsTestUtils; +import gov.cdc.usds.simplereport.test_util.TestUserIdentities; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource(properties = {"spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true"}) +class DbOrgRoleClaimsServiceTest extends BaseServiceTest { + @Autowired OrganizationRoleClaimsTestUtils _orgRoleClaimsTestUtils; + @Autowired OrganizationService _organizationService; + @SpyBean ApiUserRepository _apiUserRepoSpy; + + @Test + void getOrganizationRoleClaims_withEmail_success() { + initSampleData(); + final String email = TestUserIdentities.STANDARD_USER; + Mockito.reset(_apiUserRepoSpy); + + List orgRoleClaims = _service.getOrganizationRoleClaims(email); + + verify(_apiUserRepoSpy, times(1)).findByLoginEmail(email); + assertEquals(1, orgRoleClaims.size()); + assertEquals( + Collections.unmodifiableSet(EnumSet.of(OrganizationRole.USER)), + orgRoleClaims.stream().findFirst().get().getGrantedRoles()); + + Set facilityIds = orgRoleClaims.stream().findFirst().get().getFacilities(); + assertEquals(1, facilityIds.size()); + Facility facility = + _organizationService.getFacilityById(facilityIds.stream().findFirst().get()).get(); + assertEquals("Injection Site", facility.getFacilityName()); + } + + @Test + void getOrganizationRoleClaims_withEmail_nonExistentUser_success() { + final String email = "nonexistentuser@fake.com"; + Mockito.reset(_apiUserRepoSpy); + + List orgRoleClaims = _service.getOrganizationRoleClaims(email); + + verify(_apiUserRepoSpy, times(1)).findByLoginEmail(email); + assertThat(orgRoleClaims).isEmpty(); + } + + @Test + void getOrganizationRoleClaims_withEmail_withMultipleOrgs_success() { + Organization gwu = + _dataFactory.saveOrganization("George Washington", "university", "gwu", true); + Organization gtown = _dataFactory.saveOrganization("Georgetown", "university", "gt", true); + final String email = TestUserIdentities.STANDARD_USER; + ApiUser mockApiUser = mock(ApiUser.class); + when(mockApiUser.getOrganizations()).thenReturn(Set.of(gwu, gtown)); + when(_apiUserRepoSpy.findByLoginEmail(email)).thenReturn(Optional.of(mockApiUser)); + + List orgRoleClaims = _service.getOrganizationRoleClaims(email); + assertThat(orgRoleClaims).isEmpty(); + } + + @Test + void getOrganizationRoleClaims_withApiUser_withMultipleOrgs_throws() { + Organization gwu = + _dataFactory.saveOrganization("George Washington", "university", "gwu", true); + Organization gtown = _dataFactory.saveOrganization("Georgetown", "university", "gt", true); + final String email = TestUserIdentities.STANDARD_USER; + ApiUser apiUser = _apiUserRepoSpy.findByLoginEmail(email).get(); + ApiUser mockApiUser = mock(ApiUser.class); + when(mockApiUser.getOrganizations()).thenReturn(Set.of(gwu, gtown)); + + assertThrows( + MisconfiguredUserException.class, () -> _service.getOrganizationRoleClaims(apiUser)); + } + + @Test + void checkOrgRoleClaimsEquality_withIdenticalOrgRoleClaims_inDifferentOrder_isTrue() { + OrganizationRoleClaims firstOktaClaim = + _orgRoleClaimsTestUtils.createOrgRoleClaims( + OrganizationRoleClaimsTestUtils.OKTA_ORG_EXTERNAL_ID, + OrganizationRoleClaimsTestUtils.OKTA_FACILITY_NAMES, + Set.of(OrganizationRole.NO_ACCESS, OrganizationRole.USER)); + + OrganizationRoleClaims firstDbClaim = + createClaimsForCreatedOrg( + OrganizationRoleClaimsTestUtils.OKTA_ORG_EXTERNAL_ID, Set.of(OrganizationRole.USER)); + + OrganizationRoleClaims secondOktaClaim = + _orgRoleClaimsTestUtils.createOrgRoleClaims( + OrganizationRoleClaimsTestUtils.DB_ORG_EXTERNAL_ID, + OrganizationRoleClaimsTestUtils.DB_FACILITY_NAMES, + Set.of( + OrganizationRole.NO_ACCESS, + OrganizationRole.ADMIN, + OrganizationRole.ALL_FACILITIES)); + + OrganizationRoleClaims secondDbClaim = + createClaimsForCreatedOrg( + OrganizationRoleClaimsTestUtils.DB_ORG_EXTERNAL_ID, + Set.of(OrganizationRole.ALL_FACILITIES, OrganizationRole.ADMIN)); + + assertTrue( + _service.checkOrgRoleClaimsEquality( + List.of(secondOktaClaim, firstOktaClaim), List.of(firstDbClaim, secondDbClaim))); + } + + @Test + void checkOrgRoleClaimsEquality_withDifferentRoleOrder_isTrue() { + OrganizationRoleClaims oktaClaim = + _orgRoleClaimsTestUtils.createOrgRoleClaims( + OrganizationRoleClaimsTestUtils.OKTA_ORG_EXTERNAL_ID, + OrganizationRoleClaimsTestUtils.OKTA_FACILITY_NAMES, + Set.of( + OrganizationRole.NO_ACCESS, + OrganizationRole.USER, + OrganizationRole.ALL_FACILITIES)); + + OrganizationRoleClaims dbClaim = + createClaimsForCreatedOrg( + OrganizationRoleClaimsTestUtils.OKTA_ORG_EXTERNAL_ID, + Set.of(OrganizationRole.ALL_FACILITIES, OrganizationRole.USER)); + + assertTrue(_service.checkOrgRoleClaimsEquality(List.of(oktaClaim), List.of(dbClaim))); + } + + @Test + void checkOrgRoleClaimsEquality_withDifferentOrgClaims_isFalse() { + OrganizationRoleClaims oktaClaim = + _orgRoleClaimsTestUtils.createOrgRoleClaims( + OrganizationRoleClaimsTestUtils.OKTA_ORG_EXTERNAL_ID, + OrganizationRoleClaimsTestUtils.OKTA_FACILITY_NAMES, + Set.of(OrganizationRole.NO_ACCESS, OrganizationRole.USER)); + OrganizationRoleClaims dbClaim = + _orgRoleClaimsTestUtils.createOrgRoleClaims( + OrganizationRoleClaimsTestUtils.DB_ORG_EXTERNAL_ID, + OrganizationRoleClaimsTestUtils.DB_FACILITY_NAMES, + Set.of(OrganizationRole.ADMIN, OrganizationRole.ALL_FACILITIES)); + + Mockito.reset(_apiUserRepoSpy); + + assertFalse(_service.checkOrgRoleClaimsEquality(List.of(oktaClaim), List.of(dbClaim))); + verify(_apiUserRepoSpy, times(1)).findByLoginEmail(any()); + } + + @Test + void checkOrgRoleClaimsEquality_withDifferentOrgClaimsSize_isFalse() { + OrganizationRoleClaims oktaClaim = + _orgRoleClaimsTestUtils.createOrgRoleClaims( + OrganizationRoleClaimsTestUtils.OKTA_ORG_EXTERNAL_ID, + OrganizationRoleClaimsTestUtils.OKTA_FACILITY_NAMES, + Set.of(OrganizationRole.NO_ACCESS, OrganizationRole.USER)); + + assertFalse(_service.checkOrgRoleClaimsEquality(List.of(oktaClaim), List.of())); + } + + private OrganizationRoleClaims createClaimsForCreatedOrg( + String orgExternalId, Set orgRoles) { + Organization org = _organizationService.getOrganization(orgExternalId); + List facilities = _organizationService.getFacilities(org); + Set facilityIds = + facilities.stream().map(Facility::getInternalId).collect(Collectors.toSet()); + return new OrganizationRoleClaims(orgExternalId, facilityIds, orgRoles); + } +} diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/service/LoggedInAuthorizationServiceTest.java b/backend/src/test/java/gov/cdc/usds/simplereport/service/LoggedInAuthorizationServiceTest.java index 196d425037..dd814d8a57 100644 --- a/backend/src/test/java/gov/cdc/usds/simplereport/service/LoggedInAuthorizationServiceTest.java +++ b/backend/src/test/java/gov/cdc/usds/simplereport/service/LoggedInAuthorizationServiceTest.java @@ -1,20 +1,146 @@ package gov.cdc.usds.simplereport.service; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import gov.cdc.usds.simplereport.config.AuthorizationProperties; +import gov.cdc.usds.simplereport.config.FeatureFlagsConfig; +import gov.cdc.usds.simplereport.config.authorization.OrganizationExtractor; +import gov.cdc.usds.simplereport.config.authorization.OrganizationRole; +import gov.cdc.usds.simplereport.config.authorization.OrganizationRoleClaims; +import gov.cdc.usds.simplereport.db.repository.ApiUserRepository; import gov.cdc.usds.simplereport.service.errors.NobodyAuthenticatedException; +import gov.cdc.usds.simplereport.test_util.OrganizationRoleClaimsTestUtils; +import gov.cdc.usds.simplereport.test_util.SliceTestConfiguration; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.security.core.context.SecurityContextHolder; -class LoggedInAuthorizationServiceTest extends BaseServiceTest { - @Autowired LoggedInAuthorizationService loggedInAuthorizationService; +class LoggedInAuthorizationServiceTest extends BaseServiceTest { + @Autowired TenantDataAccessService tenantDataAccessService; + @Autowired @SpyBean ApiUserRepository apiUserRepository; + @Autowired OrganizationRoleClaimsTestUtils _orgRoleClaimsTestUtils; @Test void findAllOrganizationRoles_NobodyAuthenticatedException() { SecurityContextHolder.getContext().setAuthentication(null); - assertThrows( - NobodyAuthenticatedException.class, - () -> loggedInAuthorizationService.findAllOrganizationRoles()); + assertThrows(NobodyAuthenticatedException.class, () -> _service.findAllOrganizationRoles()); + } + + @Test + void findAllOrganizationRoles_whenOktaMigrationDisabled_returnsRoleFromOkta() { + // GIVEN + AuthorizationService mockLoggedInAuthorizationService = getMockAuthService(false); + + // WHEN + List orgRoleClaims = + mockLoggedInAuthorizationService.findAllOrganizationRoles(); + + // THEN + assertEquals(1, orgRoleClaims.size()); + OrganizationRoleClaims orgRoleClaim = orgRoleClaims.get(0); + assertEquals( + Collections.unmodifiableSet(EnumSet.of(OrganizationRole.NO_ACCESS, OrganizationRole.USER)), + orgRoleClaim.getGrantedRoles()); + assertTrue( + _orgRoleClaimsTestUtils.facilitiesEqual( + OrganizationRoleClaimsTestUtils.OKTA_FACILITY_NAMES, orgRoleClaim.getFacilities())); + assertEquals( + OrganizationRoleClaimsTestUtils.OKTA_ORG_EXTERNAL_ID, + orgRoleClaim.getOrganizationExternalId()); + } + + @Test + void findAllOrganizationRoles_whenOktaMigrationEnabled_returnsRoleFromDB() { + // GIVEN + AuthorizationService mockLoggedInAuthorizationService = getMockAuthService(true); + + // WHEN + List orgRoleClaims = + mockLoggedInAuthorizationService.findAllOrganizationRoles(); + + // THEN + assertEquals(1, orgRoleClaims.size()); + OrganizationRoleClaims orgRoleClaim = orgRoleClaims.get(0); + assertEquals( + Collections.unmodifiableSet( + EnumSet.of(OrganizationRole.ALL_FACILITIES, OrganizationRole.ADMIN)), + orgRoleClaim.getGrantedRoles()); + assertTrue( + _orgRoleClaimsTestUtils.facilitiesEqual( + _orgRoleClaimsTestUtils.DB_FACILITY_NAMES, orgRoleClaim.getFacilities())); + assertEquals( + _orgRoleClaimsTestUtils.DB_ORG_EXTERNAL_ID, orgRoleClaim.getOrganizationExternalId()); + } + + @Test + @SliceTestConfiguration.WithSimpleReportSiteAdminUser + void findAllOrganizationRoles_whenOktaMigrationEnabled_doesNotFetchFromDB_forSiteAdmin() { + // GIVEN + AuthorizationProperties authProps = new AuthorizationProperties(null, "UNITTEST"); + OrganizationRoleClaims dbClaims = + _orgRoleClaimsTestUtils.createOrgRoleClaims( + _orgRoleClaimsTestUtils.DB_ORG_EXTERNAL_ID, + _orgRoleClaimsTestUtils.DB_FACILITY_NAMES, + Set.of(OrganizationRole.ADMIN, OrganizationRole.ALL_FACILITIES)); + DbOrgRoleClaimsService mockDbOrgRoleClaimsService = mock(DbOrgRoleClaimsService.class); + when(mockDbOrgRoleClaimsService.getOrganizationRoleClaims(anyString())) + .thenReturn(List.of(dbClaims)); + FeatureFlagsConfig mockFeatureFlags = mock(FeatureFlagsConfig.class); + when(mockFeatureFlags.isOktaMigrationEnabled()).thenReturn(false); + + LoggedInAuthorizationService mockLoggedInAuthorizationService = + new LoggedInAuthorizationService( + new OrganizationExtractor(authProps), + authProps, + mockDbOrgRoleClaimsService, + mockFeatureFlags); + + // WHEN + List orgRoleClaims = + mockLoggedInAuthorizationService.findAllOrganizationRoles(); + + // THEN + verify(mockDbOrgRoleClaimsService, times(0)).getOrganizationRoleClaims(anyString()); + assertEquals(0, orgRoleClaims.size()); + } + + private LoggedInAuthorizationService getMockAuthService(boolean isOktaMigrationEnabled) { + FeatureFlagsConfig mockFeatureFlags = mock(FeatureFlagsConfig.class); + when(mockFeatureFlags.isOktaMigrationEnabled()).thenReturn(isOktaMigrationEnabled); + + AuthorizationProperties authProps = new AuthorizationProperties(null, "UNITTEST"); + + OrganizationRoleClaims oktaClaims = + _orgRoleClaimsTestUtils.createOrgRoleClaims( + _orgRoleClaimsTestUtils.OKTA_ORG_EXTERNAL_ID, + _orgRoleClaimsTestUtils.OKTA_FACILITY_NAMES, + Set.of(OrganizationRole.NO_ACCESS, OrganizationRole.USER)); + OrganizationExtractor mockOrgExtractor = mock(OrganizationExtractor.class); + when(mockOrgExtractor.convert(any())).thenReturn(List.of(oktaClaims)); + + OrganizationRoleClaims dbClaims = + _orgRoleClaimsTestUtils.createOrgRoleClaims( + _orgRoleClaimsTestUtils.DB_ORG_EXTERNAL_ID, + _orgRoleClaimsTestUtils.DB_FACILITY_NAMES, + Set.of(OrganizationRole.ADMIN, OrganizationRole.ALL_FACILITIES)); + DbOrgRoleClaimsService mockDbOrgRoleClaimsService = mock(DbOrgRoleClaimsService.class); + when(mockDbOrgRoleClaimsService.getOrganizationRoleClaims(anyString())) + .thenReturn(List.of(dbClaims)); + + return new LoggedInAuthorizationService( + mockOrgExtractor, authProps, mockDbOrgRoleClaimsService, mockFeatureFlags); } } diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/test_util/OrganizationRoleClaimsTestUtils.java b/backend/src/test/java/gov/cdc/usds/simplereport/test_util/OrganizationRoleClaimsTestUtils.java new file mode 100644 index 0000000000..d462d05eda --- /dev/null +++ b/backend/src/test/java/gov/cdc/usds/simplereport/test_util/OrganizationRoleClaimsTestUtils.java @@ -0,0 +1,56 @@ +package gov.cdc.usds.simplereport.test_util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import gov.cdc.usds.simplereport.config.authorization.OrganizationRole; +import gov.cdc.usds.simplereport.config.authorization.OrganizationRoleClaims; +import gov.cdc.usds.simplereport.db.model.Facility; +import gov.cdc.usds.simplereport.db.model.Organization; +import gov.cdc.usds.simplereport.service.OrganizationService; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class OrganizationRoleClaimsTestUtils { + @Autowired OrganizationService _orgService; + + @Autowired TestDataFactory _dataFactory; + + public static final String OKTA_ORG_EXTERNAL_ID = "AIRPORT-ORG"; + public static final String DB_ORG_EXTERNAL_ID = "K12-ORG"; + + public static final List OKTA_FACILITY_NAMES = + List.of("Airport facility one", "Airport facility two"); + public static final List DB_FACILITY_NAMES = + List.of("K12 facility one", "K12 facility two"); + + public OrganizationRoleClaims createOrgRoleClaims( + String orgExternalId, List facilityNames, Set orgRoles) { + Organization createdOrg = + _dataFactory.saveOrganization(orgExternalId, "other", orgExternalId, true); + Set facilityIds = new HashSet<>(); + facilityNames.forEach( + facilityName -> { + Facility createdFacility = _dataFactory.createValidFacility(createdOrg, facilityName); + facilityIds.add(createdFacility.getInternalId()); + }); + + return new OrganizationRoleClaims(orgExternalId, facilityIds, orgRoles); + } + + public boolean facilitiesEqual(List expectedFacilityNames, Set actualFacilityIds) { + assertEquals(expectedFacilityNames.size(), actualFacilityIds.size()); + List actualFacilityNames = + actualFacilityIds.stream() + .map(facilityId -> _orgService.getFacilityById(facilityId).get().getFacilityName()) + .collect(Collectors.toList()); + + return CollectionUtils.isEqualCollection(expectedFacilityNames, actualFacilityNames); + } +} diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/test_util/SliceTestConfiguration.java b/backend/src/test/java/gov/cdc/usds/simplereport/test_util/SliceTestConfiguration.java index 49a35d0739..5c4aae888e 100644 --- a/backend/src/test/java/gov/cdc/usds/simplereport/test_util/SliceTestConfiguration.java +++ b/backend/src/test/java/gov/cdc/usds/simplereport/test_util/SliceTestConfiguration.java @@ -1,5 +1,7 @@ package gov.cdc.usds.simplereport.test_util; +import static org.mockito.Mockito.mock; + import gov.cdc.usds.simplereport.api.ApiUserContextHolder; import gov.cdc.usds.simplereport.api.CurrentAccountRequestContextHolder; import gov.cdc.usds.simplereport.api.CurrentOrganizationRolesContextHolder; @@ -22,6 +24,7 @@ import gov.cdc.usds.simplereport.service.ApiUserService; import gov.cdc.usds.simplereport.service.AuthorizationService; import gov.cdc.usds.simplereport.service.BaseServiceTest; +import gov.cdc.usds.simplereport.service.DbOrgRoleClaimsService; import gov.cdc.usds.simplereport.service.DiseaseCacheService; import gov.cdc.usds.simplereport.service.DiseaseService; import gov.cdc.usds.simplereport.service.LoggedInAuthorizationService; @@ -154,8 +157,13 @@ public IdentitySupplier testIdentityProvider() { @Bean public AuthorizationService realAuthorizationService(OrganizationExtractor extractor) { + FeatureFlagsConfig mockFeatureFlags = mock(FeatureFlagsConfig.class); + DbOrgRoleClaimsService mockDbOrgRoleClaimsService = mock(DbOrgRoleClaimsService.class); return new LoggedInAuthorizationService( - extractor, new AuthorizationProperties(null, "UNITTEST")); + extractor, + new AuthorizationProperties(null, "UNITTEST"), + mockDbOrgRoleClaimsService, + mockFeatureFlags); } @Retention(RetentionPolicy.RUNTIME)