From d067ed6748144ffc37f409a39c2b52f8d4711a53 Mon Sep 17 00:00:00 2001 From: Maksim Litvinov Date: Wed, 15 Nov 2023 15:52:46 +0300 Subject: [PATCH] add authorization --- .../io/hexlet/blog/config/SecurityConfig.java | 2 ++ .../blog/controller/api/PostsController.java | 8 +++++ .../blog/controller/api/UsersController.java | 10 ++++++ src/main/java/io/hexlet/blog/dto/UserDTO.java | 11 +++++++ .../exception/ResourceNotFoundException.java | 2 +- .../blog/mapper/JsonNullableMapper.java | 2 +- .../io/hexlet/blog/mapper/UserMapper.java | 29 ++++++++++++++--- .../controller/api/PostsControllerTest.java | 31 ++++++++++++++++++- .../controller/api/UsersControllerTest.java | 14 +++++++++ 9 files changed, 102 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/hexlet/blog/config/SecurityConfig.java b/src/main/java/io/hexlet/blog/config/SecurityConfig.java index 3765083..cd598d7 100644 --- a/src/main/java/io/hexlet/blog/config/SecurityConfig.java +++ b/src/main/java/io/hexlet/blog/config/SecurityConfig.java @@ -8,6 +8,7 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; @@ -21,6 +22,7 @@ @Configuration @EnableWebSecurity +@EnableMethodSecurity public class SecurityConfig { @Autowired private JwtDecoder jwtDecoder; diff --git a/src/main/java/io/hexlet/blog/controller/api/PostsController.java b/src/main/java/io/hexlet/blog/controller/api/PostsController.java index e7d34de..e15d6d4 100644 --- a/src/main/java/io/hexlet/blog/controller/api/PostsController.java +++ b/src/main/java/io/hexlet/blog/controller/api/PostsController.java @@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -28,6 +29,11 @@ @RestController @RequestMapping("/api") public class PostsController { + + private static final String ONLY_AUTHOR = """ + @postRepository.findById(#id).get().getAuthor().getEmail() == authentication.getName() + """; + @Autowired private PostRepository repository; @@ -71,6 +77,7 @@ PostDTO show(@PathVariable Long id) { @PutMapping("/posts/{id}") @ResponseStatus(HttpStatus.OK) + @PreAuthorize(ONLY_AUTHOR) PostDTO update(@RequestBody @Valid PostUpdateDTO postData, @PathVariable Long id) { var post = repository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Not Found")); @@ -82,6 +89,7 @@ PostDTO update(@RequestBody @Valid PostUpdateDTO postData, @PathVariable Long id @DeleteMapping("/posts/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) + @PreAuthorize(ONLY_AUTHOR) void destroy(@PathVariable Long id) { repository.deleteById(id); } diff --git a/src/main/java/io/hexlet/blog/controller/api/UsersController.java b/src/main/java/io/hexlet/blog/controller/api/UsersController.java index 459a6c7..bef2f43 100644 --- a/src/main/java/io/hexlet/blog/controller/api/UsersController.java +++ b/src/main/java/io/hexlet/blog/controller/api/UsersController.java @@ -2,11 +2,13 @@ import java.util.List; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -40,6 +42,14 @@ ResponseEntity> index() { .body(result); } + @PostMapping("/users") + @ResponseStatus(HttpStatus.CREATED) + UserDTO create(@Valid @RequestBody UserDTO userData) { + var user = userMapper.map(userData); + repository.save(user); + return userMapper.map(user); + } + @PutMapping("/users/{id}") @ResponseStatus(HttpStatus.OK) UserDTO update(@RequestBody UserUpdateDTO userData, @PathVariable Long id) { diff --git a/src/main/java/io/hexlet/blog/dto/UserDTO.java b/src/main/java/io/hexlet/blog/dto/UserDTO.java index 0eb1bd3..b74c782 100644 --- a/src/main/java/io/hexlet/blog/dto/UserDTO.java +++ b/src/main/java/io/hexlet/blog/dto/UserDTO.java @@ -1,5 +1,8 @@ package io.hexlet.blog.dto; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.Setter; @@ -7,7 +10,15 @@ @Getter public class UserDTO { private Long id; + + @Email + @NotBlank private String username; + private String firstName; + private String lastName; + + @Size(min = 3, max = 100) + private String password; } diff --git a/src/main/java/io/hexlet/blog/exception/ResourceNotFoundException.java b/src/main/java/io/hexlet/blog/exception/ResourceNotFoundException.java index 3576ad9..8bb3d64 100644 --- a/src/main/java/io/hexlet/blog/exception/ResourceNotFoundException.java +++ b/src/main/java/io/hexlet/blog/exception/ResourceNotFoundException.java @@ -4,7 +4,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(HttpStatus.NOT_FOUND) -public class ResourceNotFoundException extends RuntimeException{ +public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String message) { super(message); } diff --git a/src/main/java/io/hexlet/blog/mapper/JsonNullableMapper.java b/src/main/java/io/hexlet/blog/mapper/JsonNullableMapper.java index c6b8934..20a8217 100644 --- a/src/main/java/io/hexlet/blog/mapper/JsonNullableMapper.java +++ b/src/main/java/io/hexlet/blog/mapper/JsonNullableMapper.java @@ -20,7 +20,7 @@ public T unwrap(JsonNullable jsonNullable) { /** * Checks whether nullable parameter was passed explicitly. - * + * * @return true if value was set explicitly, false otherwise */ @Condition diff --git a/src/main/java/io/hexlet/blog/mapper/UserMapper.java b/src/main/java/io/hexlet/blog/mapper/UserMapper.java index 9b9c33a..ac4dc08 100644 --- a/src/main/java/io/hexlet/blog/mapper/UserMapper.java +++ b/src/main/java/io/hexlet/blog/mapper/UserMapper.java @@ -1,6 +1,9 @@ package io.hexlet.blog.mapper; +import org.mapstruct.BeforeMapping; +import org.mapstruct.InheritInverseConfiguration; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import org.mapstruct.MappingConstants; import org.mapstruct.MappingTarget; import org.mapstruct.NullValuePropertyMappingStrategy; @@ -9,18 +12,36 @@ import io.hexlet.blog.dto.UserDTO; import io.hexlet.blog.dto.UserUpdateDTO; import io.hexlet.blog.model.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @Mapper( - uses = { JsonNullableMapper.class, ReferenceMapper.class }, - nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, - componentModel = MappingConstants.ComponentModel.SPRING, - unmappedTargetPolicy = ReportingPolicy.IGNORE + uses = {JsonNullableMapper.class, ReferenceMapper.class}, + nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, + componentModel = MappingConstants.ComponentModel.SPRING, + unmappedTargetPolicy = ReportingPolicy.IGNORE ) public abstract class UserMapper { + + @Autowired + private BCryptPasswordEncoder encoder; + + @Mapping(target = "id", ignore = true) + @Mapping(target = "email", source = "username") + @Mapping(target = "passwordDigest", source = "password") public abstract User map(UserDTO model); + public abstract User map(UserUpdateDTO model); + @InheritInverseConfiguration + @Mapping(target = "password", ignore = true) public abstract UserDTO map(User model); public abstract void update(UserUpdateDTO update, @MappingTarget User destination); + + @BeforeMapping + public void encryptPassword(UserDTO data) { + var password = data.getPassword(); + data.setPassword(encoder.encode(password)); + } } diff --git a/src/test/java/io/hexlet/blog/controller/api/PostsControllerTest.java b/src/test/java/io/hexlet/blog/controller/api/PostsControllerTest.java index 1985fed..67154e5 100644 --- a/src/test/java/io/hexlet/blog/controller/api/PostsControllerTest.java +++ b/src/test/java/io/hexlet/blog/controller/api/PostsControllerTest.java @@ -110,6 +110,25 @@ public void testUpdate() throws Exception { assertThat(testPost.getName()).isEqualTo(data.getName().get()); } + @Test + public void testUpdateFailed() throws Exception { + postRepository.save(testPost); + + var data = new PostUpdateDTO(); + data.setName(JsonNullable.of("new name")); + + var request = put("/api/posts/" + testPost.getId()) + .with(jwt()) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsString(data)); + + mockMvc.perform(request) + .andExpect(status().isForbidden()); + + var actualPost = postRepository.findById(testPost.getId()).get(); + assertThat(actualPost.getName()).isEqualTo(testPost.getName()); + } + @Test public void testShow() throws Exception { postRepository.save(testPost); @@ -128,10 +147,20 @@ public void testShow() throws Exception { @Test public void testDestroy() throws Exception { postRepository.save(testPost); - var request = delete("/api/posts/" + testPost.getId()).with(jwt()); + var request = delete("/api/posts/" + testPost.getId()).with(token); mockMvc.perform(request) .andExpect(status().isNoContent()); assertThat(postRepository.existsById(testPost.getId())).isEqualTo(false); } + + @Test + public void testDestroyFailed() throws Exception { + postRepository.save(testPost); + var request = delete("/api/posts/" + testPost.getId()).with(jwt()); + mockMvc.perform(request) + .andExpect(status().isForbidden()); + + assertThat(postRepository.existsById(testPost.getId())).isEqualTo(true); + } } diff --git a/src/test/java/io/hexlet/blog/controller/api/UsersControllerTest.java b/src/test/java/io/hexlet/blog/controller/api/UsersControllerTest.java index d323a75..601ab8f 100644 --- a/src/test/java/io/hexlet/blog/controller/api/UsersControllerTest.java +++ b/src/test/java/io/hexlet/blog/controller/api/UsersControllerTest.java @@ -4,6 +4,7 @@ import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.util.HashMap; @@ -62,6 +63,19 @@ public void testIndex() throws Exception { .andExpect(status().isOk()); } + @Test + void testCreate() throws Exception { + var data = Instancio.of(modelGenerator.getUserModel()) + .create(); + + var request = post("/api/users") + .with(token) + .contentType(MediaType.APPLICATION_JSON) + .content(om.writeValueAsString(data)); + mockMvc.perform(request) + .andExpect(status().isCreated()); + } + @Test public void testUpdate() throws Exception {