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");