From 35c46c2405dfdf60c8bdc5230c3d22da584723f5 Mon Sep 17 00:00:00 2001 From: Oleh Kurpiak Date: Wed, 20 Apr 2022 10:11:22 +0300 Subject: [PATCH] [topic8][2022] spring security test --- topic9/spring-security-db/pom.xml | 11 +++ .../config/WebSecurityConfig.java | 8 +- .../controller/WebController.java | 11 +++ .../domain/entities/UserEntity.java | 3 + .../service/MyCustomUser.java | 21 ++++ .../service/MyUserDetailsService.java | 8 +- .../db/migration/V2__user_company.sql | 2 + .../controller/MyMockUser.java | 23 +++++ .../MyUserSecurityContextFactory.java | 38 ++++++++ .../controller/WebControllerTest.java | 95 +++++++++++++++++++ 10 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/service/MyCustomUser.java create mode 100644 topic9/spring-security-db/src/main/resources/db/migration/V2__user_company.sql create mode 100644 topic9/spring-security-db/src/test/java/com/kma/practice8/springsecuritydb/controller/MyMockUser.java create mode 100644 topic9/spring-security-db/src/test/java/com/kma/practice8/springsecuritydb/controller/MyUserSecurityContextFactory.java create mode 100644 topic9/spring-security-db/src/test/java/com/kma/practice8/springsecuritydb/controller/WebControllerTest.java diff --git a/topic9/spring-security-db/pom.xml b/topic9/spring-security-db/pom.xml index 71c05ad..7ced930 100644 --- a/topic9/spring-security-db/pom.xml +++ b/topic9/spring-security-db/pom.xml @@ -53,6 +53,17 @@ lombok true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + diff --git a/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/config/WebSecurityConfig.java b/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/config/WebSecurityConfig.java index cc643f9..ff8b1aa 100644 --- a/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/config/WebSecurityConfig.java +++ b/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/config/WebSecurityConfig.java @@ -2,6 +2,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; @@ -14,6 +15,7 @@ @RequiredArgsConstructor @Configuration +@EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepository userRepository; @@ -22,9 +24,9 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { protected void configure(final HttpSecurity http) throws Exception { http .authorizeRequests() - .antMatchers("/admin", "/admin/**").hasAuthority(Permission.VIEW_ADMIN.name()) - .antMatchers("/catalog").hasAuthority(Permission.VIEW_CATALOG.name()) - .antMatchers("/profile").authenticated() +// .antMatchers("/admin", "/admin/**").hasAuthority(Permission.VIEW_ADMIN.name()) +// .antMatchers("/catalog").hasAuthority(Permission.VIEW_CATALOG.name()) +// .antMatchers("/profile").authenticated() .anyRequest().permitAll() .and() .formLogin().permitAll() diff --git a/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/controller/WebController.java b/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/controller/WebController.java index b41a0e0..ca764c2 100644 --- a/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/controller/WebController.java +++ b/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/controller/WebController.java @@ -1,7 +1,9 @@ package com.kma.practice8.springsecuritydb.controller; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; @Controller public class WebController { @@ -11,11 +13,13 @@ public String index() { return "index"; } + @PreAuthorize("hasAuthority('VIEW_ADMIN') || hasAuthority('VIEW_CATALOG')") @GetMapping("/admin") public String admin() { return "admin_root"; } + @PreAuthorize("hasAuthority('VIEW_ADMIN')") @GetMapping("/admin/subpage") public String adminSubpage() { return "admin_sub"; @@ -26,6 +30,7 @@ public String catalog() { return "catalog"; } + @PreAuthorize("isFullyAuthenticated()") @GetMapping("/profile") public String profile() { return "profile"; @@ -36,4 +41,10 @@ public String other() { return "other"; } + @PreAuthorize("hasAuthority('VIEW_ADMIN') || authentication.principal.companyId == #companyId") + @GetMapping("/company/{companyId}/edit") + public String editCompany(@PathVariable("companyId") int companyId) { + return "other"; + } + } diff --git a/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/domain/entities/UserEntity.java b/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/domain/entities/UserEntity.java index 929ca66..f4e43fb 100644 --- a/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/domain/entities/UserEntity.java +++ b/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/domain/entities/UserEntity.java @@ -36,6 +36,9 @@ public class UserEntity { @Column(name = "password") private String password; + @Column(name = "company_id") + private Integer companyId; + @ManyToMany @JoinTable( name = "user_to_permissions", diff --git a/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/service/MyCustomUser.java b/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/service/MyCustomUser.java new file mode 100644 index 0000000..989c401 --- /dev/null +++ b/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/service/MyCustomUser.java @@ -0,0 +1,21 @@ +package com.kma.practice8.springsecuritydb.service; + +import java.util.Collection; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import lombok.Getter; +import lombok.ToString; + +@ToString(callSuper = true) +public class MyCustomUser extends User { + + @Getter + private final Integer companyId; + + public MyCustomUser(final String username, final String password, final Collection authorities, final Integer companyId) { + super(username, password, authorities); + this.companyId = companyId; + } +} diff --git a/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/service/MyUserDetailsService.java b/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/service/MyUserDetailsService.java index 6adca7e..907a32c 100644 --- a/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/service/MyUserDetailsService.java +++ b/topic9/spring-security-db/src/main/java/com/kma/practice8/springsecuritydb/service/MyUserDetailsService.java @@ -5,11 +5,9 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; import com.kma.practice8.springsecuritydb.domain.entities.PermissionEntity; import com.kma.practice8.springsecuritydb.domain.entities.UserEntity; @@ -27,11 +25,7 @@ public UserDetails loadUserByUsername(final String username) throws UsernameNotF final UserEntity user = userRepository.findByLogin(username) .orElseThrow(() -> new UsernameNotFoundException("No user with login: " + username)); - return User.builder() - .username(username) - .password(user.getPassword()) - .authorities(mapAuthorities(user.getPermissions())) - .build(); + return new MyCustomUser(user.getLogin(), user.getPassword(), mapAuthorities(user.getPermissions()), user.getCompanyId()); } private static List mapAuthorities(final List permissions) { diff --git a/topic9/spring-security-db/src/main/resources/db/migration/V2__user_company.sql b/topic9/spring-security-db/src/main/resources/db/migration/V2__user_company.sql new file mode 100644 index 0000000..c766ab6 --- /dev/null +++ b/topic9/spring-security-db/src/main/resources/db/migration/V2__user_company.sql @@ -0,0 +1,2 @@ +alter table users + add column company_id int default null; diff --git a/topic9/spring-security-db/src/test/java/com/kma/practice8/springsecuritydb/controller/MyMockUser.java b/topic9/spring-security-db/src/test/java/com/kma/practice8/springsecuritydb/controller/MyMockUser.java new file mode 100644 index 0000000..d2b457d --- /dev/null +++ b/topic9/spring-security-db/src/test/java/com/kma/practice8/springsecuritydb/controller/MyMockUser.java @@ -0,0 +1,23 @@ +package com.kma.practice8.springsecuritydb.controller; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.security.test.context.support.WithSecurityContext; + +import com.kma.practice8.springsecuritydb.domain.type.Permission; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = MyUserSecurityContextFactory.class) +public @interface MyMockUser { + + String username() default "username"; + + Permission[] authorities() default {}; + + int companyId() default -1; + +} diff --git a/topic9/spring-security-db/src/test/java/com/kma/practice8/springsecuritydb/controller/MyUserSecurityContextFactory.java b/topic9/spring-security-db/src/test/java/com/kma/practice8/springsecuritydb/controller/MyUserSecurityContextFactory.java new file mode 100644 index 0000000..19be4c3 --- /dev/null +++ b/topic9/spring-security-db/src/test/java/com/kma/practice8/springsecuritydb/controller/MyUserSecurityContextFactory.java @@ -0,0 +1,38 @@ +package com.kma.practice8.springsecuritydb.controller; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +import com.kma.practice8.springsecuritydb.domain.type.Permission; +import com.kma.practice8.springsecuritydb.service.MyCustomUser; + +public class MyUserSecurityContextFactory implements WithSecurityContextFactory { + + @Override + public SecurityContext createSecurityContext(final MyMockUser annotation) { + MyCustomUser principal = new MyCustomUser( + annotation.username(), "password", mapAuthorities(annotation.authorities()), annotation.companyId() < 0 ? null : annotation.companyId() + ); + + Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities()); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authentication); + return context; + } + + private static List mapAuthorities(final Permission[] permissions) { + return Arrays.stream(permissions) + .map(Enum::name) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toUnmodifiableList()); + } +} diff --git a/topic9/spring-security-db/src/test/java/com/kma/practice8/springsecuritydb/controller/WebControllerTest.java b/topic9/spring-security-db/src/test/java/com/kma/practice8/springsecuritydb/controller/WebControllerTest.java new file mode 100644 index 0000000..cade58f --- /dev/null +++ b/topic9/spring-security-db/src/test/java/com/kma/practice8/springsecuritydb/controller/WebControllerTest.java @@ -0,0 +1,95 @@ +package com.kma.practice8.springsecuritydb.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import com.kma.practice8.springsecuritydb.repositories.UserRepository; + +import lombok.SneakyThrows; + +@WebMvcTest +class WebControllerTest { + + @Autowired + private MockMvc mockMvc; + @MockBean + private UserRepository userRepository; + + @Test + @SneakyThrows + @WithMockUser(authorities = "VIEW_ADMIN") + void shouldAccessAdminPage() { + mockMvc.perform( + MockMvcRequestBuilders.get("/admin") + ) + .andExpect(MockMvcResultMatchers.status().isOk()); + } + + @Test + @SneakyThrows + @WithMockUser(authorities = "VIEW_CATALOG") + void shouldAccessAdminPage_withCatalogPermission() { + mockMvc.perform( + MockMvcRequestBuilders.get("/admin") + ) + .andExpect(MockMvcResultMatchers.status().isOk()); + } + + @Test + @SneakyThrows + @WithMockUser + void shouldReturnForbidden_whenNoAdminPermission() { + mockMvc.perform( + MockMvcRequestBuilders.get("/admin") + ) + .andExpect(MockMvcResultMatchers.status().isForbidden()); + } + + @Test + @SneakyThrows + void shouldRedirectToLogin_whenNoUser() { + mockMvc.perform( + MockMvcRequestBuilders.get("/admin") + ) + .andExpect(MockMvcResultMatchers.status().isFound()) + .andExpect(MockMvcResultMatchers.header().string("Location", "http://localhost/login")); + } + + @Test + @SneakyThrows + @WithMockUser(authorities = "VIEW_ADMIN") + void shouldAccessCompanyEditPage_admin() { + mockMvc.perform( + MockMvcRequestBuilders.get("/company/10/edit") + ) + .andExpect(MockMvcResultMatchers.status().isOk()); + } + + @Test + @SneakyThrows + @MyMockUser(companyId = 10) + void shouldAccessCompanyEditPage_notAdmin() { + mockMvc.perform( + MockMvcRequestBuilders.get("/company/10/edit") + ) + .andExpect(MockMvcResultMatchers.status().isOk()); + } + + @Test + @SneakyThrows + @MyMockUser(companyId = 20) + void shouldAccessCompanyEditPage_notAdminAndDifferentCompanyId() { + mockMvc.perform( + MockMvcRequestBuilders.get("/company/10/edit") + ) + .andExpect(MockMvcResultMatchers.status().isForbidden()); + } + + +}