Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions topic9/spring-security-db/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,6 +15,7 @@

@RequiredArgsConstructor
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

private final UserRepository userRepository;
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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";
Expand All @@ -26,6 +30,7 @@ public String catalog() {
return "catalog";
}

@PreAuthorize("isFullyAuthenticated()")
@GetMapping("/profile")
public String profile() {
return "profile";
Expand All @@ -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";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<GrantedAuthority> mapAuthorities(final List<PermissionEntity> permissions) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
alter table users
add column company_id int default null;
Original file line number Diff line number Diff line change
@@ -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;

}
Original file line number Diff line number Diff line change
@@ -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<MyMockUser> {

@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<GrantedAuthority> mapAuthorities(final Permission[] permissions) {
return Arrays.stream(permissions)
.map(Enum::name)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toUnmodifiableList());
}
}
Original file line number Diff line number Diff line change
@@ -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());
}


}