diff --git a/.run/postgres14.run.xml b/.run/postgres14.run.xml deleted file mode 100644 index 8aae0626..00000000 --- a/.run/postgres14.run.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/compose.yml b/compose.yml new file mode 100644 index 00000000..4bf7c3f0 --- /dev/null +++ b/compose.yml @@ -0,0 +1,10 @@ +services: + database: + image: 'postgres:14-alpine' + ports: + - '5432' + environment: + - 'POSTGRES_USER=postgres' + - 'POSTGRES_DB=postgres' + - 'POSTGRES_PASSWORD=password' + - "SPRING_PROFILES_ACTIVE=local" \ No newline at end of file diff --git a/pom.xml b/pom.xml index b0d20a7a..fc4c5f48 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,18 @@ 17 + + org.zalando + logbook-spring-boot-starter + 3.7.2 + + + + org.springframework.boot + spring-boot-docker-compose + true + + org.springframework.boot spring-boot-starter-actuator @@ -121,7 +133,7 @@ com.tngtech.archunit archunit-junit5 - 1.0.1 + 1.2.0 test diff --git a/src/main/java/com/jitterted/mobreg/WebSecurityConfig.java b/src/main/java/com/jitterted/mobreg/WebSecurityConfig.java index 7a7e9061..032aecd6 100644 --- a/src/main/java/com/jitterted/mobreg/WebSecurityConfig.java +++ b/src/main/java/com/jitterted/mobreg/WebSecurityConfig.java @@ -1,17 +1,30 @@ package com.jitterted.mobreg; import com.jitterted.mobreg.adapter.in.web.member.MemberDeniedRedirectToUserOnboardingHandler; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler; +import org.springframework.security.web.firewall.FirewalledRequest; +import org.springframework.security.web.firewall.RequestRejectedException; +import org.springframework.security.web.firewall.StrictHttpFirewall; +import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter; + +import static org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive.COOKIES; @Configuration @EnableWebSecurity public class WebSecurityConfig { + private static final Logger LOGGER = LoggerFactory.getLogger(WebSecurityConfig.class); + private final GrantedAuthoritiesMapper userAuthoritiesMapper; public WebSecurityConfig(GrantedAuthoritiesMapper userAuthoritiesMapper) { @@ -19,26 +32,42 @@ public WebSecurityConfig(GrantedAuthoritiesMapper userAuthoritiesMapper) { } @Bean - SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + public WebSecurityCustomizer webSecurityCustomizer() { + StrictHttpFirewall firewall = new StrictHttpFirewall() { + @Override + public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException { + try { + return super.getFirewalledRequest(request); + } catch(RequestRejectedException rre) { + LOGGER.info("HttpRequest rejected URL: {}", request.getRequestURI()); + throw rre; + } + } + }; +// firewall.setAllowBackSlash(true); +// firewall.setAllowUrlEncodedSlash(true); +// firewall.setAllowUrlEncodedDoubleSlash(true); + return (web) -> web.httpFirewall(firewall); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // @formatter:off http .authorizeHttpRequests(requests -> requests - .requestMatchers("/**", "/error") - .permitAll() - .requestMatchers("/user/**", "/invite") - .hasAuthority("ROLE_USER") - .requestMatchers("/admin/**") - .hasAuthority("ROLE_ADMIN") - .requestMatchers("/member/**") - .hasAuthority("ROLE_MEMBER")) + .requestMatchers("/**", "/error").permitAll() + .requestMatchers("/user/**", "/invite").hasAuthority("ROLE_USER") + .requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN") + .requestMatchers("/member/**").hasAuthority("ROLE_MEMBER")) .exceptionHandling(handling -> handling .accessDeniedHandler(new MemberDeniedRedirectToUserOnboardingHandler())) .logout(logout -> logout + .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES))) .logoutSuccessUrl("/") .permitAll() - .deleteCookies("JSESSIONID") + .clearAuthentication(true) .invalidateHttpSession(true)) - .oauth2Login(login -> login + .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(endpoint -> endpoint .userAuthoritiesMapper(userAuthoritiesMapper))); return http.build(); diff --git a/src/main/java/com/jitterted/mobreg/adapter/in/web/WelcomeController.java b/src/main/java/com/jitterted/mobreg/adapter/in/web/WelcomeController.java index ba4d43bc..31f2587f 100644 --- a/src/main/java/com/jitterted/mobreg/adapter/in/web/WelcomeController.java +++ b/src/main/java/com/jitterted/mobreg/adapter/in/web/WelcomeController.java @@ -1,7 +1,10 @@ package com.jitterted.mobreg.adapter.in.web; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.AuthenticatedPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.annotation.CurrentSecurityContext; +import org.springframework.security.core.context.SecurityContext; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -10,7 +13,10 @@ public class WelcomeController { @GetMapping("/member") - public String memberHome() { + public String memberHome(@CurrentSecurityContext SecurityContext context) { + if (context.getAuthentication().getName().equalsIgnoreCase("anonymousUser")) { + throw new AccessDeniedException("Access Denied for Anonymous User"); + } return "redirect:/member/register"; } diff --git a/src/main/java/com/jitterted/mobreg/adapter/in/web/admin/AdminDashboardController.java b/src/main/java/com/jitterted/mobreg/adapter/in/web/admin/AdminDashboardController.java index d6fd2aa6..8d469691 100644 --- a/src/main/java/com/jitterted/mobreg/adapter/in/web/admin/AdminDashboardController.java +++ b/src/main/java/com/jitterted/mobreg/adapter/in/web/admin/AdminDashboardController.java @@ -6,8 +6,11 @@ import com.jitterted.mobreg.domain.EnsembleId; import com.jitterted.mobreg.domain.Member; import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.AuthenticatedPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.annotation.CurrentSecurityContext; +import org.springframework.security.core.context.SecurityContext; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -34,7 +37,9 @@ public AdminDashboardController(EnsembleService ensembleService, } @GetMapping("/dashboard") - public String dashboardView(Model model, @AuthenticationPrincipal AuthenticatedPrincipal principal) { + public String dashboardView(Model model, + @AuthenticationPrincipal AuthenticatedPrincipal principal, + @CurrentSecurityContext SecurityContext context) { if (principal instanceof OAuth2User oAuth2User) { String username = oAuth2User.getAttribute("login"); Member member = memberService.findByGithubUsername(username); @@ -42,7 +47,10 @@ public String dashboardView(Model model, @AuthenticationPrincipal AuthenticatedP model.addAttribute("name", member.firstName()); model.addAttribute("github_id", oAuth2User.getAttribute("id")); } else { - throw new IllegalStateException("Not an OAuth2User"); + if (context.getAuthentication().getName().equalsIgnoreCase("anonymousUser")) { + throw new AccessDeniedException("Access Denied for Anonymous User"); + } + throw new IllegalStateException("AuthenticationPrincipal is not an OAuth2User: " + principal); } List ensembles = ensembleService.allEnsemblesByDateTimeDescending(); List ensembleSummaryViews = EnsembleSummaryView.from(ensembles); diff --git a/src/main/java/com/jitterted/mobreg/adapter/in/web/member/MemberController.java b/src/main/java/com/jitterted/mobreg/adapter/in/web/member/MemberController.java index 2fe2d9fb..12a72b9c 100644 --- a/src/main/java/com/jitterted/mobreg/adapter/in/web/member/MemberController.java +++ b/src/main/java/com/jitterted/mobreg/adapter/in/web/member/MemberController.java @@ -6,8 +6,13 @@ import com.jitterted.mobreg.domain.EnsembleId; import com.jitterted.mobreg.domain.Member; import com.jitterted.mobreg.domain.MemberId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.AuthenticatedPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.annotation.CurrentSecurityContext; +import org.springframework.security.core.context.SecurityContext; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -18,6 +23,8 @@ @Controller public class MemberController { + private static final Logger LOGGER = LoggerFactory.getLogger(MemberController.class); + private final EnsembleService ensembleService; private final MemberLookup memberLookup; private final MemberService memberService; @@ -29,7 +36,13 @@ public MemberController(EnsembleService ensembleService, MemberService memberSer } @GetMapping("/member/register") - public String showEnsemblesForUser(Model model, @AuthenticationPrincipal AuthenticatedPrincipal principal) { + public String showEnsemblesForUser(Model model, + @AuthenticationPrincipal AuthenticatedPrincipal principal, + @CurrentSecurityContext SecurityContext context) { + if (context.getAuthentication().getName().equalsIgnoreCase("anonymousUser")) { + throw new AccessDeniedException("Access Denied for Anonymous User"); + } + Member member = memberLookup.findMemberBy(principal); model.addAttribute("githubUsername", member.githubUsername()); model.addAttribute("firstName", member.firstName()); diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties index 29c9c8d5..a9e7c743 100644 --- a/src/main/resources/application-local.properties +++ b/src/main/resources/application-local.properties @@ -5,6 +5,9 @@ spring.security.oauth2.client.registration.github.clientId=${github.oauth2.local.clientId} spring.security.oauth2.client.registration.github.clientSecret=${github.oauth2.local.clientSecret} +# Turn on for super detailed security logging +#logging.level.org.springframework.security.web=TRACE + # Local (Docker container) PostgreSQL (not a Testcontainer) spring.datasource.username=postgres spring.datasource.password=password diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3857b160..40487f90 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -15,6 +15,8 @@ zoom.client.secret=${zoom.client.secret} #logging.level.org.springframework.jdbc.core=TRACE logging.level.com.jitterted.mobreg.adapter.in.web.GitHubGrantedAuthoritiesMapper=DEBUG logging.level.com.jitterted.mobreg.adapter.out.zoom=DEBUG +logging.level.org.springframework.web=DEBUG +logging.level.org.springframework.security.web=DEBUG management.endpoints.web.exposure.include=health,info,metrics @@ -22,5 +24,5 @@ app.version=@project.version@ app.name=@project.name@ app.build.timestamp=@maven.build.timestamp@ -# Don't need to set the active profile, it should be an environment variable -#spring.profiles.active=railway \ No newline at end of file +# Set profile to local, should be overridden by environment when deployed +spring.profiles.active=local \ No newline at end of file diff --git a/src/test/java/com/jitterted/mobreg/adapter/in/web/admin/AdminDashboardControllerTest.java b/src/test/java/com/jitterted/mobreg/adapter/in/web/admin/AdminDashboardControllerTest.java index 5641fe81..5fc36a52 100644 --- a/src/test/java/com/jitterted/mobreg/adapter/in/web/admin/AdminDashboardControllerTest.java +++ b/src/test/java/com/jitterted/mobreg/adapter/in/web/admin/AdminDashboardControllerTest.java @@ -18,6 +18,10 @@ import com.jitterted.mobreg.domain.ZonedDateTimeFactory; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.ui.ConcurrentModel; import org.springframework.ui.Model; @@ -25,6 +29,7 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.List; +import java.util.Set; import static org.assertj.core.api.Assertions.*; @@ -43,7 +48,11 @@ void givenOneEnsembleResultsInEnsembleInViewModel() throws Exception { AdminDashboardController adminDashboardController = new AdminDashboardController(ensembleService, memberService); Model model = new ConcurrentModel(); - adminDashboardController.dashboardView(model, OAuth2UserFactory.createOAuth2UserWithMemberRole("tedyoung", "ROLE_MEMBER")); + OAuth2User oAuth2UserWithMemberRole = OAuth2UserFactory.createOAuth2UserWithMemberRole("tedyoung", "ROLE_MEMBER"); + SecurityContextImpl securityContext = new SecurityContextImpl(new OAuth2AuthenticationToken(oAuth2UserWithMemberRole, + Set.of(new SimpleGrantedAuthority("ROLE_MEMBER")), + "github")); + adminDashboardController.dashboardView(model, oAuth2UserWithMemberRole, securityContext); List ensembleSummaryViews = (List) model.getAttribute("ensembles"); assertThat(ensembleSummaryViews) diff --git a/src/test/java/com/jitterted/mobreg/adapter/in/web/member/MemberControllerTest.java b/src/test/java/com/jitterted/mobreg/adapter/in/web/member/MemberControllerTest.java index bb2ce664..5d06bc62 100644 --- a/src/test/java/com/jitterted/mobreg/adapter/in/web/member/MemberControllerTest.java +++ b/src/test/java/com/jitterted/mobreg/adapter/in/web/member/MemberControllerTest.java @@ -17,10 +17,15 @@ import com.jitterted.mobreg.domain.MemberStatus; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.ui.ConcurrentModel; import org.springframework.ui.Model; import java.time.ZonedDateTime; +import java.util.Set; import static org.assertj.core.api.Assertions.*; @@ -42,7 +47,13 @@ void ensembleFormContainsMemberIdForOAuth2User() throws Exception { MemberController memberController = new MemberController(ensembleService, memberService); Model model = new ConcurrentModel(); - memberController.showEnsemblesForUser(model, OAuth2UserFactory.createOAuth2UserWithMemberRole("ghuser", "ROLE_MEMBER")); + OAuth2User oAuth2UserWithMemberRole = OAuth2UserFactory.createOAuth2UserWithMemberRole("ghuser", "ROLE_MEMBER"); + SecurityContextImpl securityContext = new SecurityContextImpl(new OAuth2AuthenticationToken(oAuth2UserWithMemberRole, + Set.of(new SimpleGrantedAuthority("ROLE_MEMBER")), + "github")); + memberController.showEnsemblesForUser(model, + oAuth2UserWithMemberRole, + securityContext); assertThat((String) model.getAttribute("firstName")) .isEqualTo("name");