Skip to content

Commit

Permalink
Merge pull request #11 from cabinetoffice/feature/GAP-1922-migrate-ap…
Browse files Browse the repository at this point in the history
…plicant

Feature/gap 1922 migrate applicant
  • Loading branch information
dominicwest authored Aug 7, 2023
2 parents 09e7f34 + aeb4fe2 commit e92d651
Show file tree
Hide file tree
Showing 11 changed files with 373 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,26 +1,59 @@
package gov.cabinetoffice.gap.adminbackend.controllers;

import com.auth0.jwt.interfaces.DecodedJWT;
import gov.cabinetoffice.gap.adminbackend.dtos.MigrateUserDto;
import gov.cabinetoffice.gap.adminbackend.dtos.UserDTO;
import gov.cabinetoffice.gap.adminbackend.exceptions.ForbiddenException;
import gov.cabinetoffice.gap.adminbackend.exceptions.UnauthorizedException;
import gov.cabinetoffice.gap.adminbackend.mappers.UserMapper;
import gov.cabinetoffice.gap.adminbackend.models.AdminSession;
import gov.cabinetoffice.gap.adminbackend.security.AuthManager;
import gov.cabinetoffice.gap.adminbackend.services.JwtService;
import gov.cabinetoffice.gap.adminbackend.services.UserService;
import gov.cabinetoffice.gap.adminbackend.utils.HelperUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.*;

import java.util.Objects;

import static org.springframework.util.ObjectUtils.isEmpty;

@RequiredArgsConstructor
@RestController
@RequestMapping("/users")
@Log4j2
public class UserController {

private final UserMapper userMapper;

private final JwtService jwtService;

private final UserService userService;

@GetMapping("/loggedInUser")
public ResponseEntity<UserDTO> getLoggedInUserDetails() {
AdminSession session = HelperUtils.getAdminSessionForAuthenticatedUser();
return ResponseEntity.ok(userMapper.adminSessionToUserDTO(session));
}

@PatchMapping("/migrate")
public ResponseEntity<String> migrateUser(@RequestBody MigrateUserDto migrateUserDto,
@RequestHeader("Authorization") String token) {
// Called from our user service only. Does not have an admin session so authing
// via the jwt
if (isEmpty(token) || !token.startsWith("Bearer "))
return ResponseEntity.status(401).body("Migrate user: Expected Authorization header not provided");
final DecodedJWT decodedJWT = jwtService.verifyToken(token.split(" ")[1]);
if (!Objects.equals(decodedJWT.getSubject(), migrateUserDto.getOneLoginSub()))
return ResponseEntity.status(403)
.body("User not authorized to migrate user: " + migrateUserDto.getOneLoginSub());

userService.migrateUser(migrateUserDto.getOneLoginSub(), migrateUserDto.getColaSub());
return ResponseEntity.ok("User migrated successfully");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gov.cabinetoffice.gap.adminbackend.dtos;

import lombok.Builder;
import lombok.Data;

import java.util.UUID;

@Data
@Builder
public class MigrateUserDto {

private String oneLoginSub;

private UUID colaSub;

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package gov.cabinetoffice.gap.adminbackend.exceptions;

import org.springframework.web.bind.annotation.ResponseStatus;

import static org.springframework.http.HttpStatus.UNAUTHORIZED;

@ResponseStatus(UNAUTHORIZED)
public class UnauthorizedException extends RuntimeException {

public UnauthorizedException() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

import gov.cabinetoffice.gap.adminbackend.entities.GapUser;

import java.util.Optional;

public interface GapUserRepository extends JpaRepository<GapUser, Integer> {

Optional<GapUser> findByUserSub(String userSub);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package gov.cabinetoffice.gap.adminbackend.repositories;

import gov.cabinetoffice.gap.adminbackend.dtos.submission.GrantApplicant;
import gov.cabinetoffice.gap.adminbackend.entities.GapUser;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface GrantApplicantRepository extends JpaRepository<GrantApplicant, Long> {

Optional<GrantApplicant> findByUserId(String userId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ public Authentication authenticate(Authentication authentication) throws Authent
JWTPayload = this.jwtService.getPayloadFromJwt(decodedJWT);
}

if (!JWTPayload.getRoles().contains("ADMIN")) {
throw new UnauthorizedException("User is not an admin");
}

Optional<GrantAdmin> grantAdmin = this.grantAdminRepository.findByGapUserUserSub(JWTPayload.getSub());

// if JWT is valid and admin doesn't already exist, create admin user in database
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ "}/export-batch/{batchExportId:" + UUID_REGEX_STRING + "}/signedUrl",
"/export-batch/{exportId:" + UUID_REGEX_STRING + "}/outstandingCount",
"/grant-advert/lambda/{grantAdvertId:" + UUID_REGEX_STRING + "}/publish",
"/grant-advert/lambda/{grantAdvertId:" + UUID_REGEX_STRING + "}/unpublish")
"/grant-advert/lambda/{grantAdvertId:" + UUID_REGEX_STRING + "}/unpublish",
"/users/migrate")
.permitAll()
.antMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**",
"/swagger-ui.html", "/webjars/**")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package gov.cabinetoffice.gap.adminbackend.services;

import gov.cabinetoffice.gap.adminbackend.repositories.GapUserRepository;
import gov.cabinetoffice.gap.adminbackend.repositories.GrantApplicantRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.UUID;

@Service
@RequiredArgsConstructor
public class UserService {

private final GapUserRepository gapUserRepository;

private final GrantApplicantRepository grantApplicantRepository;

@Transactional
public void migrateUser(final String oneLoginSub, final UUID colaSub) {
gapUserRepository.findByUserSub(colaSub.toString()).ifPresent(gapUser -> {
gapUser.setUserSub(oneLoginSub);
gapUserRepository.save(gapUser);
});

grantApplicantRepository.findByUserId(colaSub.toString()).ifPresent(grantApplicant -> {
grantApplicant.setUserId(oneLoginSub);
grantApplicantRepository.save(grantApplicant);
});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package gov.cabinetoffice.gap.adminbackend.controllers;

import com.auth0.jwt.interfaces.DecodedJWT;
import gov.cabinetoffice.gap.adminbackend.dtos.MigrateUserDto;
import gov.cabinetoffice.gap.adminbackend.exceptions.UnauthorizedException;
import gov.cabinetoffice.gap.adminbackend.mappers.UserMapper;
import gov.cabinetoffice.gap.adminbackend.mappers.ValidationErrorMapperImpl;
import gov.cabinetoffice.gap.adminbackend.services.JwtService;
import gov.cabinetoffice.gap.adminbackend.services.UserService;
import gov.cabinetoffice.gap.adminbackend.utils.HelperUtils;
import gov.cabinetoffice.gap.adminbackend.utils.TestDecodedJwt;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.context.WebApplicationContext;

import javax.annotation.Resource;
import java.util.UUID;

import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserController.class)
@AutoConfigureMockMvc(addFilters = false)
@ContextConfiguration(classes = { UserController.class, ControllerExceptionHandler.class })
class UserControllerTest {

@Resource
private WebApplicationContext context;

@MockBean
private JwtService jwtService;

@MockBean
private UserService userService;

@MockBean
private UserMapper userMapper;

@SpyBean
private ValidationErrorMapperImpl validationErrorMapper;

@Autowired
private MockMvc mockMvc;

@Test
void migrateUser_HappyPath() throws Exception {
final MigrateUserDto migrateUserDto = MigrateUserDto.builder().colaSub(UUID.randomUUID())
.oneLoginSub("oneLoginSub").build();
final DecodedJWT decodedJWT = TestDecodedJwt.builder().subject("oneLoginSub").build();
when(jwtService.verifyToken("jwt")).thenReturn(decodedJWT);

mockMvc.perform(MockMvcRequestBuilders.patch("/users/migrate").contentType(MediaType.APPLICATION_JSON)
.content(HelperUtils.asJsonString(migrateUserDto)).header(HttpHeaders.AUTHORIZATION, "Bearer jwt"))
.andExpect(status().isOk()).andReturn();
verify(userService).migrateUser("oneLoginSub", migrateUserDto.getColaSub());
}

@Test
void migrateUser_NoJwt() throws Exception {
final MigrateUserDto migrateUserDto = MigrateUserDto.builder().colaSub(UUID.randomUUID())
.oneLoginSub("oneLoginSub").build();
final DecodedJWT decodedJWT = TestDecodedJwt.builder().subject("oneLoginSub").build();
when(jwtService.verifyToken("jwt")).thenReturn(decodedJWT);

mockMvc.perform(MockMvcRequestBuilders.patch("/users/migrate").contentType(MediaType.APPLICATION_JSON)
.content(HelperUtils.asJsonString(migrateUserDto)).header(HttpHeaders.AUTHORIZATION, ""))
.andExpect(status().isUnauthorized()).andReturn();
verify(userService, times(0)).migrateUser("oneLoginSub", migrateUserDto.getColaSub());
}

@Test
void migrateUser_InvalidJwt() throws Exception {
final MigrateUserDto migrateUserDto = MigrateUserDto.builder().colaSub(UUID.randomUUID())
.oneLoginSub("oneLoginSub").build();
doThrow(new UnauthorizedException("Invalid JWT")).when(jwtService).verifyToken("jwt");

mockMvc.perform(MockMvcRequestBuilders.patch("/users/migrate").contentType(MediaType.APPLICATION_JSON)
.content(HelperUtils.asJsonString(migrateUserDto)).header(HttpHeaders.AUTHORIZATION, "Bearer jwt"))
.andExpect(status().isUnauthorized()).andReturn();
verify(userService, times(0)).migrateUser("oneLoginSub", migrateUserDto.getColaSub());
}

@Test
void migrateUser_JwtDoesNotMatchMUserToMigrate() throws Exception {
final MigrateUserDto migrateUserDto = MigrateUserDto.builder().colaSub(UUID.randomUUID())
.oneLoginSub("oneLoginSub").build();
final DecodedJWT decodedJWT = TestDecodedJwt.builder().subject("anotherUsersOneLoginSub").build();
when(jwtService.verifyToken("jwt")).thenReturn(decodedJWT);

mockMvc.perform(MockMvcRequestBuilders.patch("/users/migrate").contentType(MediaType.APPLICATION_JSON)
.content(HelperUtils.asJsonString(migrateUserDto)).header(HttpHeaders.AUTHORIZATION, "Bearer jwt"))
.andExpect(status().isForbidden()).andReturn();
verify(userService, times(0)).migrateUser("oneLoginSub", migrateUserDto.getColaSub());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package gov.cabinetoffice.gap.adminbackend.services;

import gov.cabinetoffice.gap.adminbackend.dtos.submission.GrantApplicant;
import gov.cabinetoffice.gap.adminbackend.entities.GapUser;
import gov.cabinetoffice.gap.adminbackend.repositories.GapUserRepository;
import gov.cabinetoffice.gap.adminbackend.repositories.GrantApplicantRepository;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import java.util.Optional;
import java.util.UUID;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@SpringJUnitConfig
class UserServiceTest {

@Spy
@InjectMocks
private UserService userService;

@Mock
private GapUserRepository gapUserRepository;

@Mock
private GrantApplicantRepository grantApplicantRepository;

private final String oneLoginSub = "oneLoginSub";

private final UUID colaSub = UUID.randomUUID();

@Test
void migrateUserNoMatches() {
when(gapUserRepository.findByUserSub(any())).thenReturn(Optional.empty());
when(grantApplicantRepository.findByUserId(any())).thenReturn(Optional.empty());

userService.migrateUser(oneLoginSub, colaSub);

verify(gapUserRepository, times(0)).save(any());
verify(grantApplicantRepository, times(0)).save(any());
}

@Test
void migrateUserMatchesGapUser() {
final GapUser gapUser = GapUser.builder().build();
when(gapUserRepository.findByUserSub(any())).thenReturn(Optional.of(gapUser));
when(grantApplicantRepository.findByUserId(any())).thenReturn(Optional.empty());

userService.migrateUser(oneLoginSub, colaSub);
gapUser.setUserSub(oneLoginSub);

verify(gapUserRepository, times(1)).save(gapUser);
verify(grantApplicantRepository, times(0)).save(any());
}

@Test
void migrateUserMatchesGrantApplicant() {
final GrantApplicant grantApplicant = GrantApplicant.builder().build();
when(grantApplicantRepository.findByUserId(any())).thenReturn(Optional.of(grantApplicant));
when(gapUserRepository.findByUserSub(any())).thenReturn(Optional.empty());

userService.migrateUser(oneLoginSub, colaSub);
grantApplicant.setUserId(oneLoginSub);

verify(gapUserRepository, times(0)).save(any());
verify(grantApplicantRepository, times(1)).save(grantApplicant);
}

@Test
void migrateUserMatchesGrantApplicantAndGapUser() {
final GrantApplicant grantApplicant = GrantApplicant.builder().build();
final GapUser gapUser = GapUser.builder().build();
when(grantApplicantRepository.findByUserId(any())).thenReturn(Optional.of(grantApplicant));
when(gapUserRepository.findByUserSub(any())).thenReturn(Optional.of(gapUser));

userService.migrateUser(oneLoginSub, colaSub);
grantApplicant.setUserId(oneLoginSub);
gapUser.setUserSub(oneLoginSub);

verify(gapUserRepository, times(1)).save(gapUser);
verify(grantApplicantRepository, times(1)).save(grantApplicant);
}

}
Loading

0 comments on commit e92d651

Please sign in to comment.