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 extends GrantedAuthority> 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());
+ }
+
+
+}