Skip to content

Commit

Permalink
implement actuator for web ui server #3515 (#3612)
Browse files Browse the repository at this point in the history
  • Loading branch information
hamidonos authored Nov 14, 2024
1 parent 997d827 commit 8844fc2
Show file tree
Hide file tree
Showing 15 changed files with 443 additions and 40 deletions.
1 change: 1 addition & 0 deletions sechub-web-server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <code>403 Forbidden</code> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -33,13 +35,16 @@
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
@EnableMethodSecurity
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,
Expand All @@ -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");
Expand Down Expand Up @@ -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)
Expand All @@ -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 */
Expand All @@ -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();
}

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

}
21 changes: 21 additions & 0 deletions sechub-web-server/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
}
Expand All @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading

0 comments on commit 8844fc2

Please sign in to comment.