Skip to content

Commit

Permalink
Add mutation to clear user roles and facilities
Browse files Browse the repository at this point in the history
  • Loading branch information
emyl3 committed Aug 2, 2024
1 parent fc71af9 commit d550010
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,10 @@ public User updateUserPrivilegesAndGroupAccess(
username, orgExternalId, accessAllFacilities, facilityIdsToAssign, role);
return new User(_us.getUserByLoginEmail(username));
}

@AuthorizationConfiguration.RequireGlobalAdminUser
@MutationMapping
public ApiUser markUserRolesAndFacilitiesAsDeleted(@Argument String username) {
return _us.markUserRolesAndFacilitiesAsDeleted(username);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ public void setFacilities(Set<Facility> facilities) {
}
}

public Set<OrganizationRole> getRoles() {
return this.roleAssignments.stream().map(ApiUserRole::getRole).collect(Collectors.toSet());
}

public void setRoles(Set<OrganizationRole> newOrgRoles, Organization org) {
this.roleAssignments.clear();
for (OrganizationRole orgRole : newOrgRoles) {
Expand All @@ -82,4 +86,10 @@ public void setRoles(Set<OrganizationRole> newOrgRoles, Organization org) {
this.roleAssignments.add(new ApiUserRole(this, org, orgRole));
}
}

public ApiUser clearRolesAndFacilities() {
this.roleAssignments.clear();
this.facilityAssignments.clear();
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import jakarta.persistence.Enumerated;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

Expand All @@ -24,6 +25,7 @@ public class ApiUserRole extends AuditedEntity {
@Column(nullable = false, columnDefinition = "organization_role")
@JdbcTypeCode(SqlTypes.NAMED_ENUM)
@Enumerated(EnumType.STRING)
@Getter
private OrganizationRole role;

protected ApiUserRole() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,21 @@ public UserInfo resendActivationEmail(UUID userId) {
return new UserInfo(apiUser, Optional.of(orgRoles), false);
}

/**
* Clear a user's roles and facilities Intended to use on site admin users only because it will
* put other users in a misconfigured state
*
* @param username
* @return ApiUser
*/
@AuthorizationConfiguration.RequireGlobalAdminUser
public ApiUser markUserRolesAndFacilitiesAsDeleted(String username) {
ApiUser foundUser =
_apiUserRepo.findByLoginEmail(username).orElseThrow(NonexistentUserException::new);
foundUser.clearRolesAndFacilities();
return foundUser;
}

private ApiUser getApiUser(UUID id) {
return getApiUser(id, false);
}
Expand Down
3 changes: 3 additions & 0 deletions backend/src/main/resources/graphql/admin.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ extend type Mutation {
accessAllFacilities: Boolean = false,
facilities: [ID] = [],
role: Role!): User!
markUserRolesAndFacilitiesAsDeleted(
username: String!
): ApiUser
updateFeatureFlag(name: String!, value: Boolean!):FeatureFlag
deleteE2EOktaOrganizations(orgExternalId: String!): Organization
sendOrgAdminEmailCSV(type: String!, state: String!): Boolean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package gov.cdc.usds.simplereport.api.apiuser;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
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.Role;
import gov.cdc.usds.simplereport.api.model.User;
import gov.cdc.usds.simplereport.api.model.errors.NonexistentUserException;
import gov.cdc.usds.simplereport.db.model.ApiUser;
import gov.cdc.usds.simplereport.db.model.Organization;
import gov.cdc.usds.simplereport.db.repository.ApiUserRepository;
import gov.cdc.usds.simplereport.service.ApiUserService;
import gov.cdc.usds.simplereport.service.BaseServiceTest;
import gov.cdc.usds.simplereport.service.model.UserInfo;
Expand All @@ -21,6 +26,7 @@

@WithSimpleReportOrgAdminUser
class UserMutationResolverTest extends BaseServiceTest<ApiUserService> {
@Autowired ApiUserRepository apiUserRepository;
@Mock ApiUserService mockedApiUserService;

@Autowired private TestDataFactory _dataFactory;
Expand All @@ -32,11 +38,11 @@ class UserMutationResolverTest extends BaseServiceTest<ApiUserService> {
@BeforeEach
void setup() {
Organization org = _dataFactory.saveValidOrganization();
orgUserInfo = _dataFactory.createValidApiUser("[email protected]", org);
orgUserInfo = _dataFactory.createValidApiUser("[email protected]", org, Role.USER);
}

@Test
void reactivateUserAndResetPassword_orgAdmin_success() {
void reactivateUserAndResetPassword_success() {
UUID userInfoInternalId = orgUserInfo.getInternalId();

// GIVEN
Expand All @@ -50,4 +56,34 @@ void reactivateUserAndResetPassword_orgAdmin_success() {
assertThat(resetUser.getInternalId()).isEqualTo(userInfoInternalId);
verify(mockedApiUserService, times(1)).reactivateUserAndResetPassword(userInfoInternalId);
}

@Test
void markUserRolesAndFacilitiesAsDeleted_nonExistentUser_throwException() {
String username = orgUserInfo.getEmail();

// GIVEN
ApiUser foundUser = apiUserRepository.findByLoginEmail(username).get();
when(mockedApiUserService.markUserRolesAndFacilitiesAsDeleted(username)).thenReturn(foundUser);

// WHEN
ApiUser resetUser = userMutationResolver.markUserRolesAndFacilitiesAsDeleted(username);

// THEN
assertThat(resetUser.getLoginEmail()).isEqualTo(orgUserInfo.getEmail());
verify(mockedApiUserService, times(1)).markUserRolesAndFacilitiesAsDeleted(username);
}

@Test
void markUserRolesAndFacilitiesAsDeleted_failure() {
String username = "[email protected]";

when(mockedApiUserService.markUserRolesAndFacilitiesAsDeleted(username))
.thenThrow(new NonexistentUserException());

assertThrows(
NonexistentUserException.class,
() -> {
userMutationResolver.markUserRolesAndFacilitiesAsDeleted(username);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import gov.cdc.usds.simplereport.test_util.SliceTestConfiguration.WithSimpleReportOrgAdminUser;
import gov.cdc.usds.simplereport.test_util.SliceTestConfiguration.WithSimpleReportSiteAdminUser;
import gov.cdc.usds.simplereport.test_util.TestDataFactory;
import gov.cdc.usds.simplereport.test_util.TestUserIdentities;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
Expand All @@ -47,7 +48,9 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.test.context.TestPropertySource;

@TestPropertySource(properties = {"spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true"})
class ApiUserServiceTest extends BaseServiceTest<ApiUserService> {

@Autowired @SpyBean ApiUserRepository _apiUserRepo;
Expand Down Expand Up @@ -564,6 +567,48 @@ void updateUserPrivilegesAndGroupAccess_facilityToMoveNotFoundInOrg_throwsExcept
assertEquals(expectedError, caught.getMessage());
}

@Test
@WithSimpleReportOrgAdminUser
void orgAdminUser_markUserRolesAndFacilitiesAsDeleted_error() {
String username = "[email protected]";
assertThrows(
AccessDeniedException.class, () -> _service.markUserRolesAndFacilitiesAsDeleted(username));
}

@Test
@WithSimpleReportSiteAdminUser
void siteAdminUser_markUserRolesAndFacilitiesAsDeleted_nonExistentUser_throws() {
final String email = "[email protected]";

assertThrows(
NonexistentUserException.class,
() -> {
_service.markUserRolesAndFacilitiesAsDeleted(email);
});
}

@Test
@WithSimpleReportSiteAdminUser
void siteAdminUser_markUserRolesAndFacilitiesAsDeleted_success() {
initSampleData();
final String email = TestUserIdentities.STANDARD_USER;
ApiUser foundUser = _apiUserRepo.findByLoginEmail(email).get();

// check initial facilities and roles
int initialFacilitiesCount = foundUser.getFacilities().size();
Set<OrganizationRole> initialOrgRoles = foundUser.getRoles();
assertEquals(1, initialFacilitiesCount);
assertEquals(1, initialOrgRoles.size());
assertEquals("USER", initialOrgRoles.stream().findFirst().get().getName());

ApiUser updatedUser = _service.markUserRolesAndFacilitiesAsDeleted(email);
// check facilities and roles after deletion
int updatedFacilitiesCount = updatedUser.getFacilities().size();
int updatedOrgRolesCount = updatedUser.getRoles().size();
assertEquals(0, updatedFacilitiesCount);
assertEquals(0, updatedOrgRolesCount);
}

private void roleCheck(final UserInfo userInfo, final Set<OrganizationRole> expected) {
EnumSet<OrganizationRole> actual = EnumSet.copyOf(userInfo.getRoles());
assertEquals(expected, actual);
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export type Mutation = {
markFacilityAsDeleted?: Maybe<Scalars["String"]["output"]>;
markOrganizationAsDeleted?: Maybe<Scalars["String"]["output"]>;
markPendingOrganizationAsDeleted?: Maybe<Scalars["String"]["output"]>;
markUserRolesAndFacilitiesAsDeleted?: Maybe<ApiUser>;
reactivateUser?: Maybe<User>;
reactivateUserAndResetPassword?: Maybe<User>;
removePatientFromQueue?: Maybe<Scalars["String"]["output"]>;
Expand Down Expand Up @@ -382,6 +383,10 @@ export type MutationMarkPendingOrganizationAsDeletedArgs = {
orgExternalId: Scalars["String"]["input"];
};

export type MutationMarkUserRolesAndFacilitiesAsDeletedArgs = {
username: Scalars["String"]["input"];
};

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

0 comments on commit d550010

Please sign in to comment.