From 8844fc2fe02617c72b45baad267143f3613e3b77 Mon Sep 17 00:00:00 2001 From: Hamid <94196804+hamidonos@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:01:17 +0100 Subject: [PATCH] implement actuator for web ui server #3515 (#3612) --- sechub-web-server/build.gradle | 1 + .../webserver/security/PortAccessGuard.java | 40 ++++++++ .../security/SecurityConfiguration.java | 83 +++++++++++----- .../server/ManagementServerProperties.java | 22 +++++ .../webserver/server/ServerProperties.java | 22 +++++ .../server/ServerPropertiesConfiguration.java | 11 +++ .../src/main/resources/application.yml | 21 ++++ .../webserver/page/HomeControllerTest.java | 12 ++- ...LoginControllerClassicAuthEnabledTest.java | 10 +- ...rollerOAuth2AndClassicAuthEnabledTest.java | 10 +- .../LoginControllerOAuth2EnabledTest.java | 10 +- .../security/PortAccessGuardTest.java | 70 +++++++++++++ .../security/SecurityConfigurationTest.java | 99 +++++++++++++++++++ .../security/TestSecurityController.java | 63 ++++++++++++ .../src/test/resources/application-test.yml | 9 +- 15 files changed, 443 insertions(+), 40 deletions(-) create mode 100644 sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/security/PortAccessGuard.java create mode 100644 sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/server/ManagementServerProperties.java create mode 100644 sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/server/ServerProperties.java create mode 100644 sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/server/ServerPropertiesConfiguration.java create mode 100644 sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/security/PortAccessGuardTest.java create mode 100644 sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/security/SecurityConfigurationTest.java create mode 100644 sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/security/TestSecurityController.java diff --git a/sechub-web-server/build.gradle b/sechub-web-server/build.gradle index d2980c1d3f..38c34fc1eb 100644 --- a/sechub-web-server/build.gradle +++ b/sechub-web-server/build.gradle @@ -20,6 +20,7 @@ dependencies { implementation library.thymeleaf_extras_springsecurity5 implementation library.springboot_starter_oauth2_client implementation library.springboot_starter_oauth2_resource_server + implementation library.springboot_starter_actuator testImplementation project(':sechub-testframework-spring') testImplementation library.springboot_starter_test diff --git a/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/security/PortAccessGuard.java b/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/security/PortAccessGuard.java new file mode 100644 index 0000000000..d01591513f --- /dev/null +++ b/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/security/PortAccessGuard.java @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.webserver.security; + +import java.io.IOException; + +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +/** + * Filter which checks if the request is targeting the allowed port. If not, it + * will return a 403 Forbidden response. + * + * @author hamidonos + */ +class PortAccessGuard extends OncePerRequestFilter { + + private final int allowedPort; + + public PortAccessGuard(int allowedPort) { + this.allowedPort = allowedPort; + } + + @Override + /* @formatter:off */ + protected void doFilterInternal(HttpServletRequest request, + @SuppressWarnings("NullableProblems") HttpServletResponse response, + @SuppressWarnings("NullableProblems") FilterChain filterChain) throws ServletException, IOException { + /* @formatter:on */ + int requestPort = request.getServerPort(); + if (allowedPort != requestPort) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + filterChain.doFilter(request, response); + } +} diff --git a/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/security/SecurityConfiguration.java b/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/security/SecurityConfiguration.java index cfe051984d..0fa92dda0c 100644 --- a/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/security/SecurityConfiguration.java +++ b/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/security/SecurityConfiguration.java @@ -8,6 +8,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; @@ -24,6 +25,7 @@ import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.context.SecurityContextHolderFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.NegatedRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; @@ -33,6 +35,8 @@ import com.mercedesbenz.sechub.webserver.ApplicationProfiles; import com.mercedesbenz.sechub.webserver.RequestConstants; import com.mercedesbenz.sechub.webserver.encryption.AES256Encryption; +import com.mercedesbenz.sechub.webserver.server.ManagementServerProperties; +import com.mercedesbenz.sechub.webserver.server.ServerProperties; @Configuration @EnableWebSecurity @@ -40,6 +44,7 @@ class SecurityConfiguration { static final String ACCESS_TOKEN = "access_token"; + private static final String ACTUATOR_PATH = "/actuator/**"; /* @formatter:off */ private static final String[] PUBLIC_PATHS = { RequestConstants.LOGIN, @@ -58,12 +63,14 @@ class SecurityConfiguration { private final OAuth2Properties oAuth2Properties; private final AES256Encryption aes256Encryption; - SecurityConfiguration(@Autowired Environment environment, @Autowired(required = false) OAuth2Properties oAuth2Properties, - @Autowired AES256Encryption aes256Encryption) { + /* @formatter:off */ + SecurityConfiguration(Environment environment, + @Autowired(required = false) OAuth2Properties oAuth2Properties, + AES256Encryption aes256Encryption) { + /* @formatter:on */ this.environment = environment; if (isOAuth2Enabled() && oAuth2Properties == null) { - throw new NoSuchBeanDefinitionException( - "No qualifying bean of type 'OAuth2Properties' available: expected at least 1 bean which qualifies as autowire candidate."); + throw new NoSuchBeanDefinitionException(OAuth2Properties.class); } if (!isOAuth2Enabled() && !isClassicAuthEnabled()) { throw new IllegalStateException("At least one authentication method must be enabled"); @@ -94,19 +101,39 @@ ClientRegistrationRepository clientRegistrationRepository() { return new InMemoryClientRegistrationRepository(clientRegistration); } + @Bean + @Order(1) + /* @formatter:off */ + SecurityFilterChain securityFilterChainActuator(HttpSecurity httpSecurity, + ManagementServerProperties managementServerProperties) throws Exception { + PortAccessGuard portAccessGuard = new PortAccessGuard(managementServerProperties.getPort()); + + httpSecurity + .securityMatcher(ACTUATOR_PATH) + .authorizeHttpRequests(authorizeRequests -> authorizeRequests + .requestMatchers(ACTUATOR_PATH) + .permitAll()) + .addFilterBefore(portAccessGuard, SecurityContextHolderFilter.class); + /* @formatter:on */ + return httpSecurity.build(); + } + @Bean @Profile(ApplicationProfiles.OAUTH2_ENABLED) - SecurityFilterChain securityFilterChainAuthenticated(HttpSecurity httpSecurity, @Autowired(required = false) AuthenticationManager authenticationManager) - throws Exception { + @Order(2) + /* @formatter:off */ + SecurityFilterChain securityFilterChainProtectedPaths(HttpSecurity httpSecurity, + @Autowired(required = false) AuthenticationManager authenticationManager, + ServerProperties serverProperties) throws Exception { AuthenticationEntryPoint authenticationEntryPoint = new MissingAuthenticationEntryPointHandler(); BearerTokenResolver bearerTokenResolver = new JwtCookieResolver(aes256Encryption); - /* @formatter:off */ RequestMatcher publicPathsMatcher = new OrRequestMatcher( Arrays.stream(PUBLIC_PATHS) .map(AntPathRequestMatcher::new) .toArray(AntPathRequestMatcher[]::new) ); RequestMatcher protectedPathsMatcher = new NegatedRequestMatcher(publicPathsMatcher); + PortAccessGuard portAccessGuard = new PortAccessGuard(serverProperties.getPort()); httpSecurity .securityMatcher(protectedPathsMatcher) @@ -125,16 +152,20 @@ SecurityFilterChain securityFilterChainAuthenticated(HttpSecurity httpSecurity, httpSecurity.authenticationManager(authenticationManager); } + httpSecurity.addFilterBefore(portAccessGuard, SecurityContextHolderFilter.class); + return httpSecurity.build(); } @Bean - SecurityFilterChain securityFilterChainAnonymous(HttpSecurity httpSecurity, - @Autowired(required = false) OAuth2AuthorizedClientService oAuth2AuthorizedClientService) throws Exception { - /* @formatter:off */ + @Order(3) + /* @formatter:on */ + SecurityFilterChain securityFilterChainPublicPaths(HttpSecurity httpSecurity, + @Autowired(required = false) OAuth2AuthorizedClientService oAuth2AuthorizedClientService, ServerProperties serverProperties) throws Exception { - httpSecurity - .securityMatcher(PUBLIC_PATHS) + PortAccessGuard portAccessGuard = new PortAccessGuard(serverProperties.getPort()); + + httpSecurity.securityMatcher(PUBLIC_PATHS) /* Disable CSRF */ .csrf(AbstractHttpConfigurer::disable) /* Make the application stateless */ @@ -143,34 +174,34 @@ SecurityFilterChain securityFilterChainAnonymous(HttpSecurity httpSecurity, if (isOAuth2Enabled()) { RestTemplate restTemplate = new RestTemplate(); - Base64EncodedClientIdAndSecretOAuth2AccessTokenClient base64EncodedClientIdAndSecretOAuth2AccessTokenClient = new Base64EncodedClientIdAndSecretOAuth2AccessTokenClient(restTemplate); + Base64EncodedClientIdAndSecretOAuth2AccessTokenClient base64EncodedClientIdAndSecretOAuth2AccessTokenClient = new Base64EncodedClientIdAndSecretOAuth2AccessTokenClient( + restTemplate); if (oAuth2AuthorizedClientService == null) { throw new NoSuchBeanDefinitionException( "No qualifying bean of type 'OAuth2AuthorizedClientService' available: expected at least 1 bean which qualifies as autowire candidate."); } - AuthenticationSuccessHandler authenticationSuccessHandler = new OAuth2LoginSuccessHandler(oAuth2Properties, oAuth2AuthorizedClientService, aes256Encryption); + AuthenticationSuccessHandler authenticationSuccessHandler = new OAuth2LoginSuccessHandler(oAuth2Properties, oAuth2AuthorizedClientService, + aes256Encryption); /* Enable OAuth2 */ - httpSecurity.oauth2Login(oauth2 -> oauth2 - .loginPage(RequestConstants.LOGIN) - .tokenEndpoint(token -> token.accessTokenResponseClient(base64EncodedClientIdAndSecretOAuth2AccessTokenClient)) - .successHandler(authenticationSuccessHandler)); + httpSecurity.oauth2Login(oauth2 -> oauth2.loginPage(RequestConstants.LOGIN) + .tokenEndpoint(token -> token.accessTokenResponseClient(base64EncodedClientIdAndSecretOAuth2AccessTokenClient)) + .successHandler(authenticationSuccessHandler)); } if (isClassicAuthEnabled()) { /* - Enable Classic Authentication - Note: This must be the last configuration in order to set the default 'loginPage' to oAuth2 - because spring uses the 'loginPage' from the first authentication method configured - */ + * Enable Classic Authentication Note: This must be the last configuration in + * order to set the default 'loginPage' to oAuth2 because spring uses the + * 'loginPage' from the first authentication method configured + */ AuthenticationSuccessHandler authenticationSuccessHandler = new ClassicLoginSuccessHandler(); - httpSecurity - .formLogin(form -> form - .loginPage(RequestConstants.LOGIN) - .successHandler(authenticationSuccessHandler)); + httpSecurity.formLogin(form -> form.loginPage(RequestConstants.LOGIN).successHandler(authenticationSuccessHandler)); } /* @formatter:on */ + httpSecurity.addFilterBefore(portAccessGuard, SecurityContextHolderFilter.class); + return httpSecurity.build(); } diff --git a/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/server/ManagementServerProperties.java b/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/server/ManagementServerProperties.java new file mode 100644 index 0000000000..fe83077882 --- /dev/null +++ b/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/server/ManagementServerProperties.java @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.webserver.server; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.ConstructorBinding; + +@ConfigurationProperties(prefix = ManagementServerProperties.PREFIX) +public final class ManagementServerProperties { + + static final String PREFIX = "management.server"; + + private final int port; + + @ConstructorBinding + ManagementServerProperties(int port) { + this.port = port; + } + + public int getPort() { + return port; + } +} \ No newline at end of file diff --git a/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/server/ServerProperties.java b/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/server/ServerProperties.java new file mode 100644 index 0000000000..6e6a6e892f --- /dev/null +++ b/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/server/ServerProperties.java @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.webserver.server; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.bind.ConstructorBinding; + +@ConfigurationProperties(prefix = ServerProperties.PREFIX) +public final class ServerProperties { + + static final String PREFIX = "server"; + + private final int port; + + @ConstructorBinding + ServerProperties(int port) { + this.port = port; + } + + public int getPort() { + return port; + } +} \ No newline at end of file diff --git a/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/server/ServerPropertiesConfiguration.java b/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/server/ServerPropertiesConfiguration.java new file mode 100644 index 0000000000..d283e7bd91 --- /dev/null +++ b/sechub-web-server/src/main/java/com/mercedesbenz/sechub/webserver/server/ServerPropertiesConfiguration.java @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.webserver.server; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties({ ServerProperties.class, ManagementServerProperties.class }) +public class ServerPropertiesConfiguration { + +} diff --git a/sechub-web-server/src/main/resources/application.yml b/sechub-web-server/src/main/resources/application.yml index 8d289f3365..87ab18e370 100644 --- a/sechub-web-server/src/main/resources/application.yml +++ b/sechub-web-server/src/main/resources/application.yml @@ -23,3 +23,24 @@ spring: web: resources: static-locations: classpath:/static + +# Spring Boot Actuators and Metrics +management: + server: + port: + 10250 + ssl: + enabled: false + endpoints: + web: + exposure: + include: "prometheus,health" + endpoint: + metrics: + enabled: true + prometheus: + enabled: true + prometheus: + metrics: + export: + enabled: true \ No newline at end of file diff --git a/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/HomeControllerTest.java b/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/HomeControllerTest.java index 82bdb672e5..720990ceee 100644 --- a/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/HomeControllerTest.java +++ b/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/HomeControllerTest.java @@ -16,27 +16,31 @@ import com.mercedesbenz.sechub.testframework.spring.WithMockJwtUser; import com.mercedesbenz.sechub.testframework.spring.YamlPropertyLoaderFactory; import com.mercedesbenz.sechub.webserver.security.SecurityTestConfiguration; +import com.mercedesbenz.sechub.webserver.server.ServerProperties; +import com.mercedesbenz.sechub.webserver.server.ServerPropertiesConfiguration; @WebMvcTest(HomeController.class) -@Import(SecurityTestConfiguration.class) +@Import({ SecurityTestConfiguration.class, ServerPropertiesConfiguration.class }) @TestPropertySource(locations = "classpath:application-test.yml", factory = YamlPropertyLoaderFactory.class) @ActiveProfiles("oauth2-enabled") class HomeControllerTest { private final MockMvc mockMvc; private final RequestPostProcessor requestPostProcessor; + private final String homePageUrl; @Autowired - HomeControllerTest(MockMvc mockMvc, RequestPostProcessor requestPostProcessor) { + HomeControllerTest(MockMvc mockMvc, RequestPostProcessor requestPostProcessor, ServerProperties serverProperties) { this.mockMvc = mockMvc; this.requestPostProcessor = requestPostProcessor; + this.homePageUrl = "http://localhost:%d/home".formatted(serverProperties.getPort()); } @Test void home_page_is_not_accessible_anonymously() throws Exception { /* @formatter:off */ mockMvc - .perform(get("/home")) + .perform(get(homePageUrl)) .andExpect(status().is3xxRedirection()); /* @formatter:on */ } @@ -48,7 +52,7 @@ void home_page_is_accessible_with_authenticated_user() throws Exception { /* @formatter:off */ mockMvc - .perform(get("/home").with(requestPostProcessor)) + .perform(get(homePageUrl).with(requestPostProcessor)) .andExpect(status().isOk()) .andReturn(); /* @formatter:on */ diff --git a/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/LoginControllerClassicAuthEnabledTest.java b/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/LoginControllerClassicAuthEnabledTest.java index 3072978cd1..4676619a29 100644 --- a/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/LoginControllerClassicAuthEnabledTest.java +++ b/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/LoginControllerClassicAuthEnabledTest.java @@ -18,25 +18,29 @@ import com.mercedesbenz.sechub.testframework.spring.YamlPropertyLoaderFactory; import com.mercedesbenz.sechub.webserver.security.SecurityTestConfiguration; +import com.mercedesbenz.sechub.webserver.server.ServerProperties; +import com.mercedesbenz.sechub.webserver.server.ServerPropertiesConfiguration; @WebMvcTest(LoginController.class) -@Import(SecurityTestConfiguration.class) +@Import({ SecurityTestConfiguration.class, ServerPropertiesConfiguration.class }) @TestPropertySource(locations = "classpath:application-test.yml", factory = YamlPropertyLoaderFactory.class) @ActiveProfiles("classic-auth-enabled") class LoginControllerClassicAuthEnabledTest { private final MockMvc mockMvc; + private final ServerProperties serverProperties; @Autowired - LoginControllerClassicAuthEnabledTest(MockMvc mockMvc) { + LoginControllerClassicAuthEnabledTest(MockMvc mockMvc, ServerProperties serverProperties) { this.mockMvc = mockMvc; + this.serverProperties = serverProperties; } @Test void login_page_is_accessible_anonymously() throws Exception { /* @formatter:off */ mockMvc - .perform(get("/login")) + .perform(get("http://localhost:%d/login".formatted(serverProperties.getPort()))) .andExpect(status().isOk()) .andExpect(model().attributeExists("isOAuth2Enabled")) .andExpect(model().attribute("isOAuth2Enabled", false)) diff --git a/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/LoginControllerOAuth2AndClassicAuthEnabledTest.java b/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/LoginControllerOAuth2AndClassicAuthEnabledTest.java index 3cbbc8a18b..6a5a2d4c80 100644 --- a/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/LoginControllerOAuth2AndClassicAuthEnabledTest.java +++ b/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/LoginControllerOAuth2AndClassicAuthEnabledTest.java @@ -18,27 +18,31 @@ import com.mercedesbenz.sechub.testframework.spring.YamlPropertyLoaderFactory; import com.mercedesbenz.sechub.webserver.security.OAuth2Properties; import com.mercedesbenz.sechub.webserver.security.SecurityTestConfiguration; +import com.mercedesbenz.sechub.webserver.server.ServerProperties; +import com.mercedesbenz.sechub.webserver.server.ServerPropertiesConfiguration; @WebMvcTest(LoginController.class) -@Import(SecurityTestConfiguration.class) +@Import({ SecurityTestConfiguration.class, ServerPropertiesConfiguration.class }) @TestPropertySource(locations = "classpath:application-test.yml", factory = YamlPropertyLoaderFactory.class) @ActiveProfiles({ "oauth2-enabled", "classic-auth-enabled" }) class LoginControllerOAuth2AndClassicAuthEnabledTest { private final MockMvc mockMvc; private final OAuth2Properties oAuth2Properties; + private final ServerProperties serverProperties; @Autowired - LoginControllerOAuth2AndClassicAuthEnabledTest(MockMvc mockMvc, OAuth2Properties oAuth2Properties) { + LoginControllerOAuth2AndClassicAuthEnabledTest(MockMvc mockMvc, OAuth2Properties oAuth2Properties, ServerProperties serverProperties) { this.mockMvc = mockMvc; this.oAuth2Properties = oAuth2Properties; + this.serverProperties = serverProperties; } @Test void login_page_is_accessible_anonymously() throws Exception { /* @formatter:off */ mockMvc - .perform(get("/login")) + .perform(get("http://localhost:%d/login".formatted(serverProperties.getPort()))) .andExpect(status().isOk()) .andExpect(model().attributeExists("isOAuth2Enabled")) .andExpect(model().attribute("isOAuth2Enabled", true)) diff --git a/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/LoginControllerOAuth2EnabledTest.java b/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/LoginControllerOAuth2EnabledTest.java index b1052d982a..4c73262411 100644 --- a/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/LoginControllerOAuth2EnabledTest.java +++ b/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/page/LoginControllerOAuth2EnabledTest.java @@ -19,27 +19,31 @@ import com.mercedesbenz.sechub.testframework.spring.YamlPropertyLoaderFactory; import com.mercedesbenz.sechub.webserver.security.OAuth2Properties; import com.mercedesbenz.sechub.webserver.security.SecurityTestConfiguration; +import com.mercedesbenz.sechub.webserver.server.ServerProperties; +import com.mercedesbenz.sechub.webserver.server.ServerPropertiesConfiguration; @WebMvcTest(LoginController.class) -@Import(SecurityTestConfiguration.class) +@Import({ SecurityTestConfiguration.class, ServerPropertiesConfiguration.class }) @TestPropertySource(locations = "classpath:application-test.yml", factory = YamlPropertyLoaderFactory.class) @ActiveProfiles("oauth2-enabled") class LoginControllerOAuth2EnabledTest { private final MockMvc mockMvc; private final OAuth2Properties oAuth2Properties; + private final ServerProperties serverProperties; @Autowired - LoginControllerOAuth2EnabledTest(MockMvc mockMvc, OAuth2Properties oAuth2Properties) { + LoginControllerOAuth2EnabledTest(MockMvc mockMvc, OAuth2Properties oAuth2Properties, ServerProperties serverProperties) { this.mockMvc = mockMvc; this.oAuth2Properties = oAuth2Properties; + this.serverProperties = serverProperties; } @Test void login_page_is_accessible_anonymously() throws Exception { /* @formatter:off */ mockMvc - .perform(get("/login")) + .perform(get("http://localhost:%d/login".formatted(serverProperties.getPort()))) .andExpect(status().isOk()) .andExpect(model().attributeExists("isOAuth2Enabled")) .andExpect(model().attribute("isOAuth2Enabled", true)) diff --git a/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/security/PortAccessGuardTest.java b/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/security/PortAccessGuardTest.java new file mode 100644 index 0000000000..428d52ce64 --- /dev/null +++ b/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/security/PortAccessGuardTest.java @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.webserver.security; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + +import java.io.IOException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +class PortAccessGuardTest { + + private static final HttpServletResponse httpServletResponse = mock(); + private static final FilterChain filterChain = mock(); + + @BeforeEach + void beforeEach() { + reset(httpServletResponse, filterChain); + } + + @Test + void do_filter_internal_does_not_send_error_when_requested_port_is_allowed() throws ServletException, IOException { + /* prepare */ + int port = 8080; + PortAccessGuard guard = new PortAccessGuard(port); + HttpServletRequest httpServletRequest = new MockHttpServletRequest() { + @Override + public int getServerPort() { + return port; + } + }; + + /* execute */ + guard.doFilterInternal(httpServletRequest, httpServletResponse, filterChain); + + /* test */ + verify(httpServletResponse, never()).sendError(anyInt()); + verify(filterChain).doFilter(httpServletRequest, httpServletResponse); + } + + @Test + void do_filter_internal_does_send_error_403_forbidden_when_requested_port_is_not_allowed() throws ServletException, IOException { + /* prepare */ + int allowedPort = 4443; + PortAccessGuard guard = new PortAccessGuard(allowedPort); + HttpServletRequest httpServletRequest = new MockHttpServletRequest() { + @Override + public int getServerPort() { + return allowedPort + 1; + } + }; + + /* execute */ + guard.doFilterInternal(httpServletRequest, httpServletResponse, filterChain); + + /* test */ + verify(httpServletResponse).sendError(HttpServletResponse.SC_FORBIDDEN); + verify(filterChain, never()).doFilter(httpServletRequest, httpServletResponse); + } +} \ No newline at end of file diff --git a/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/security/SecurityConfigurationTest.java b/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/security/SecurityConfigurationTest.java new file mode 100644 index 0000000000..6c7eef2b3e --- /dev/null +++ b/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/security/SecurityConfigurationTest.java @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.webserver.security; + +import static org.mockito.Mockito.mock; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import com.mercedesbenz.sechub.testframework.spring.YamlPropertyLoaderFactory; +import com.mercedesbenz.sechub.webserver.encryption.AES256Encryption; +import com.mercedesbenz.sechub.webserver.server.ManagementServerProperties; +import com.mercedesbenz.sechub.webserver.server.ServerProperties; +import com.mercedesbenz.sechub.webserver.server.ServerPropertiesConfiguration; + +@WebMvcTest +@ActiveProfiles("classic-auth-enabled") +@TestPropertySource(locations = "classpath:application-test.yml", factory = YamlPropertyLoaderFactory.class) +class SecurityConfigurationTest { + + private final MockMvc mockMvc; + private final ServerProperties serverProperties; + private final ManagementServerProperties managementServerProperties; + + @Autowired + SecurityConfigurationTest(MockMvc mockMvc, ServerProperties serverProperties, ManagementServerProperties managementServerProperties) { + this.mockMvc = mockMvc; + this.serverProperties = serverProperties; + this.managementServerProperties = managementServerProperties; + } + + @Test + void actuator_is_accessible_anonymously_at_management_port() throws Exception { + /* prepare */ + String url = "http://localhost:%d/actuator".formatted(managementServerProperties.getPort()); + + /* execute & test */ + getAndExpect(url, HttpStatus.OK); + } + + @Test + void actuator_is_not_accessible_at_server_port() throws Exception { + /* prepare */ + String url = "http://localhost:%d/actuator".formatted(serverProperties.getPort()); + + /* execute & test */ + getAndExpect(url, HttpStatus.FORBIDDEN); + } + + @Test + void public_path_is_accessible_at_management_port() throws Exception { + /* prepare */ + String url = "http://localhost:%d/login".formatted(managementServerProperties.getPort()); + + /* execute & test */ + getAndExpect(url, HttpStatus.FORBIDDEN); + } + + @Test + void public_path_is_accessible_at_server_port() throws Exception { + /* prepare */ + String url = "http://localhost:%d/login".formatted(serverProperties.getPort()); + + /* execute & test */ + getAndExpect(url, HttpStatus.OK); + } + + private void getAndExpect(String path, HttpStatus httpStatus) throws Exception { + /* @formatter:off */ + mockMvc + .perform(MockMvcRequestBuilders.get(path)) + .andExpect(status().is(httpStatus.value())); + /* @formatter:on */ + } + + @Configuration + @Import({ SecurityConfiguration.class, ServerPropertiesConfiguration.class }) + static class TestConfig { + + @Bean + TestSecurityController testSecurityController() { + return new TestSecurityController(); + } + + @Bean + AES256Encryption aes256Encryption() { + return mock(); + } + } +} diff --git a/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/security/TestSecurityController.java b/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/security/TestSecurityController.java new file mode 100644 index 0000000000..1aedaec910 --- /dev/null +++ b/sechub-web-server/src/test/java/com/mercedesbenz/sechub/webserver/security/TestSecurityController.java @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.webserver.security; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * This controller spins up a mock API for testing the + * {@link SecurityConfiguration} of the SecHub Web UI server application. + * + * @author hamidonos + */ +@RestController +class TestSecurityController { + + private static final String OK = HttpStatus.OK.getReasonPhrase(); + + @GetMapping("/actuator") + String actuator() { + return OK; + } + + @GetMapping("/login") + String login() { + return OK; + } + + @GetMapping("/home") + String home() { + return OK; + } + + @GetMapping("/css") + String css() { + return OK; + } + + @GetMapping("/js") + String js() { + return OK; + } + + @GetMapping("/images") + String images() { + return OK; + } + + @GetMapping("/oauth2") + String oauth2() { + return OK; + } + + @GetMapping("/sechub-logo.svg") + String sechubLogoSvg() { + return OK; + } + + @GetMapping("/error") + String errorPage() { + return OK; + } +} diff --git a/sechub-web-server/src/test/resources/application-test.yml b/sechub-web-server/src/test/resources/application-test.yml index a70cacdef5..efbc838bb7 100644 --- a/sechub-web-server/src/test/resources/application-test.yml +++ b/sechub-web-server/src/test/resources/application-test.yml @@ -14,6 +14,13 @@ sechub: jwk-set-uri: https://example.org/jwk-set-uri encryption: secret-key: test-test-test-test-test-test-32 + server: + port: + 4443 ssl: - enabled: false \ No newline at end of file + enabled: false + +management: + server: + port: 10250 \ No newline at end of file