diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/EnableMethodSecurityApplication.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/EnableMethodSecurityApplication.java new file mode 100644 index 0000000000..cb386b5fab --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/EnableMethodSecurityApplication.java @@ -0,0 +1,14 @@ +package com.baeldung.enablemethodsecurity; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@SpringBootApplication +@EnableWebMvc +public class EnableMethodSecurityApplication { + + public static void main(String[] args) { + SpringApplication.run(EnableMethodSecurityApplication.class, args); + } +} diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/authentication/CustomUserDetailService.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/authentication/CustomUserDetailService.java new file mode 100644 index 0000000000..db9fc53ea8 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/authentication/CustomUserDetailService.java @@ -0,0 +1,40 @@ +package com.baeldung.enablemethodsecurity.authentication; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import com.baeldung.enablemethodsecurity.user.SecurityUser; + +public class CustomUserDetailService implements UserDetailsService { + private final Map userMap = new HashMap<>(); + + public CustomUserDetailService(BCryptPasswordEncoder bCryptPasswordEncoder) { + userMap.put("user", createUser("user", bCryptPasswordEncoder.encode("userPass"), false, "USER")); + userMap.put("admin", createUser("admin", bCryptPasswordEncoder.encode("adminPass"), true, "ADMIN", "USER")); + } + + @Override + public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException { + return Optional.ofNullable(userMap.get(username)) + .orElseThrow(() -> new UsernameNotFoundException("User " + username + " does not exists")); + } + + private SecurityUser createUser(String userName, String password, boolean withRestrictedPolicy, String... role) { + return SecurityUser.builder() + .withUserName(userName) + .withPassword(password) + .withGrantedAuthorityList(Arrays.stream(role) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList())) + .withAccessToRestrictedPolicy(withRestrictedPolicy); + } +} \ No newline at end of file diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/authorization/CustomAuthorizationManager.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/authorization/CustomAuthorizationManager.java new file mode 100644 index 0000000000..695863ca44 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/authorization/CustomAuthorizationManager.java @@ -0,0 +1,48 @@ +package com.baeldung.enablemethodsecurity.authorization; + +import java.util.Optional; +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.authorization.AuthorizationDecision; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.core.Authentication; + +import com.baeldung.enablemethodsecurity.services.Policy; +import com.baeldung.enablemethodsecurity.services.PolicyEnum; +import com.baeldung.enablemethodsecurity.user.SecurityUser; + +public class CustomAuthorizationManager implements AuthorizationManager { + private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl(); + + @Override + public AuthorizationDecision check(Supplier authentication, MethodInvocation methodInvocation) { + + if (hasAuthentication(authentication.get())) { + + Policy policyAnnotation = AnnotationUtils.findAnnotation(methodInvocation.getMethod(), Policy.class); + + SecurityUser user = (SecurityUser) authentication.get() + .getPrincipal(); + + return new AuthorizationDecision(Optional.ofNullable(policyAnnotation) + .map(Policy::value) + .filter(policy -> policy == PolicyEnum.OPEN || (policy == PolicyEnum.RESTRICTED && user.hasAccessToRestrictedPolicy())) + .isPresent()); + + } + + return new AuthorizationDecision(false); + } + + private boolean hasAuthentication(Authentication authentication) { + return authentication != null && isNotAnonymous(authentication) && authentication.isAuthenticated(); + } + + private boolean isNotAnonymous(Authentication authentication) { + return !this.trustResolver.isAnonymous(authentication); + } +} diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/configuration/SecurityConfig.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/configuration/SecurityConfig.java new file mode 100644 index 0000000000..a2549c9122 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/configuration/SecurityConfig.java @@ -0,0 +1,74 @@ +package com.baeldung.enablemethodsecurity.configuration; + +import static org.springframework.beans.factory.config.BeanDefinition.ROLE_INFRASTRUCTURE; + +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.Advisor; +import org.springframework.aop.support.JdkRegexpMethodPointcut; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; +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; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +import com.baeldung.enablemethodsecurity.authentication.CustomUserDetailService; +import com.baeldung.enablemethodsecurity.authorization.CustomAuthorizationManager; + +@EnableWebSecurity +@EnableMethodSecurity +@Configuration +public class SecurityConfig { + @Bean + public AuthenticationManager authenticationManager(HttpSecurity httpSecurity, UserDetailsService userDetailsService, BCryptPasswordEncoder bCryptPasswordEncoder) throws Exception { + AuthenticationManagerBuilder authenticationManagerBuilder = httpSecurity.getSharedObject(AuthenticationManagerBuilder.class); + authenticationManagerBuilder.userDetailsService(userDetailsService) + .passwordEncoder(bCryptPasswordEncoder); + return authenticationManagerBuilder.build(); + } + + @Bean + public UserDetailsService userDetailsService(BCryptPasswordEncoder bCryptPasswordEncoder) { + return new CustomUserDetailService(bCryptPasswordEncoder); + } + + @Bean + public AuthorizationManager authorizationManager() { + return new CustomAuthorizationManager<>(); + } + + @Bean + @Role(ROLE_INFRASTRUCTURE) + public Advisor authorizationManagerBeforeMethodInterception(AuthorizationManager authorizationManager) { + JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut(); + pattern.setPattern("com.baeldung.enablemethodsecurity.services.*"); + return new AuthorizationManagerBeforeMethodInterceptor(pattern, authorizationManager); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf() + .disable() + .authorizeRequests() + .anyRequest() + .authenticated() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + return http.build(); + } + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/controller/ResourceController.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/controller/ResourceController.java new file mode 100644 index 0000000000..c0d99ebd46 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/controller/ResourceController.java @@ -0,0 +1,25 @@ +package com.baeldung.enablemethodsecurity.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.baeldung.enablemethodsecurity.services.PolicyService; + +@RestController +public class ResourceController { + private final PolicyService policyService; + + public ResourceController(PolicyService policyService) { + this.policyService = policyService; + } + + @GetMapping("/openPolicy") + public String openPolicy() { + return policyService.openPolicy(); + } + + @GetMapping("/restrictedPolicy") + public String restrictedPolicy() { + return policyService.restrictedPolicy(); + } +} diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/services/Policy.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/services/Policy.java new file mode 100644 index 0000000000..d748dd70cb --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/services/Policy.java @@ -0,0 +1,14 @@ +package com.baeldung.enablemethodsecurity.services; + +import static java.lang.annotation.ElementType.METHOD; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Policy { + PolicyEnum value(); +} + diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/services/PolicyEnum.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/services/PolicyEnum.java new file mode 100644 index 0000000000..11f8e22be2 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/services/PolicyEnum.java @@ -0,0 +1,5 @@ +package com.baeldung.enablemethodsecurity.services; + +public enum PolicyEnum { + RESTRICTED, OPEN +} diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/services/PolicyService.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/services/PolicyService.java new file mode 100644 index 0000000000..e015c98a39 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/services/PolicyService.java @@ -0,0 +1,16 @@ +package com.baeldung.enablemethodsecurity.services; + +import org.springframework.stereotype.Service; + +@Service +public class PolicyService { + @Policy(PolicyEnum.OPEN) + public String openPolicy() { + return "Open Policy Service"; + } + + @Policy(PolicyEnum.RESTRICTED) + public String restrictedPolicy() { + return "Restricted Policy Service"; + } +} \ No newline at end of file diff --git a/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/user/SecurityUser.java b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/user/SecurityUser.java new file mode 100644 index 0000000000..056eb32c38 --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/main/java/com/baeldung/enablemethodsecurity/user/SecurityUser.java @@ -0,0 +1,77 @@ +package com.baeldung.enablemethodsecurity.user; + +import java.util.Collection; +import java.util.List; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +public class SecurityUser implements UserDetails { + private String userName; + private String password; + private List grantedAuthorityList; + private boolean accessToRestrictedPolicy; + + public static SecurityUser builder() { + return new SecurityUser(); + } + + public SecurityUser withAccessToRestrictedPolicy(boolean restrictedPolicy) { + this.accessToRestrictedPolicy = restrictedPolicy; + return this; + } + + public boolean hasAccessToRestrictedPolicy() { + return accessToRestrictedPolicy; + } + + public SecurityUser withGrantedAuthorityList(List grantedAuthorityList) { + this.grantedAuthorityList = grantedAuthorityList; + return this; + } + + @Override + public Collection getAuthorities() { + return this.grantedAuthorityList; + } + + public SecurityUser withPassword(String password) { + this.password = password; + return this; + } + + @Override + public String getPassword() { + return this.password; + } + + public SecurityUser withUserName(String userName) { + this.userName = userName; + return this; + } + + @Override + public String getUsername() { + return this.userName; + } + + @Override + public boolean isAccountNonExpired() { + return false; + } + + @Override + public boolean isAccountNonLocked() { + return false; + } + + @Override + public boolean isCredentialsNonExpired() { + return false; + } + + @Override + public boolean isEnabled() { + return false; + } +} diff --git a/spring-security-modules/spring-security-web-boot-4/src/test/java/com/baeldung/enablemethodsecurity/EnableMethodSecurityIntegrationTest.java b/spring-security-modules/spring-security-web-boot-4/src/test/java/com/baeldung/enablemethodsecurity/EnableMethodSecurityIntegrationTest.java new file mode 100644 index 0000000000..148b1979dd --- /dev/null +++ b/spring-security-modules/spring-security-web-boot-4/src/test/java/com/baeldung/enablemethodsecurity/EnableMethodSecurityIntegrationTest.java @@ -0,0 +1,57 @@ +package com.baeldung.enablemethodsecurity; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest(classes = EnableMethodSecurityApplication.class) +public class EnableMethodSecurityIntegrationTest { + @Autowired + private WebApplicationContext context; + + private MockMvc mvc; + + @BeforeEach + public void setup() { + mvc = MockMvcBuilders.webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + + @Test + @WithUserDetails(value = "admin") + public void whenAdminAccessOpenEndpoint_thenOk() throws Exception { + mvc.perform(get("/openPolicy")) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails(value = "admin") + public void whenAdminAccessRestrictedEndpoint_thenOk() throws Exception { + mvc.perform(get("/restrictedPolicy")) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails() + public void whenUserAccessOpenEndpoint_thenOk() throws Exception { + mvc.perform(get("/openPolicy")) + .andExpect(status().isOk()); + } + + @Test + @WithUserDetails() + public void whenUserAccessRestrictedEndpoint_thenIsForbidden() throws Exception { + mvc.perform(get("/restrictedPolicy")) + .andExpect(status().isForbidden()); + } +}