Update security config for boot 2.5 (#177)

* Add stream-application-integration-tests

* Fix HttpRequestProcessorTests

* Update security configuration to permit anonymous access to info and bindings

* Update README
This commit is contained in:
David Turanski
2021-07-12 19:37:23 -04:00
committed by GitHub
parent 87324d910d
commit d2ed5b401c
14 changed files with 125 additions and 49 deletions

View File

@@ -1,22 +1,27 @@
=== `App Starters Security` Common Module
Spring Boot auto-configuration to manage the web security of the application starters.
Spring Boot auto-configuration to manage the web security of the pre-packaged stream applications.
When the `app-starters-security-common` dependency is on the classpath, the `spring.cloud.streamapp.security.enabled` and `spring.cloud.streamapp.security.csrf-enabled` properties control the application security behavior.
By default the security is enabled allowing unauthorized access only to the `Info` and `Health` endpoints.
By default the security is enabled allowing unauthorized access only to the `actuator/info`,`actuator/health`, and `actuator/bindings` endpoints.
The `spring.cloud.streamapp.security.enabled = false` completely surpass the application security.
For secured application setting `spring.cloud.streamapp.security.csrf-enabled = false` disables security for the CSRF access.
With security enabled (`spring.cloud.streamapp.security.enabled = true`) and `actuator` dependency on the classpath, the `(Reactive)ManagementWebSecurityAutoConfiguration` is activated, providing unauthenticated access to the `HealthEndpoint` and `InfoEndpoint`.
With security enabled (`spring.cloud.streamapp.security.enabled = true`) and `actuator` dependency on the classpath, the `(Reactive)ManagementWebSecurityAutoConfiguration` is activated, providing unauthenticated access to the endpoints listed above.
If the user specifies their own `WebSecurityConfigurerAdapter` (for MVC application), this configuration will back-off completely and the user should specify all the bits that they want to configure as part of the custom security configuration.
If the user specifies their own `WebSecurityConfigurerAdapter` (for MVC application), this configuration will be overridden. The user must provide the complete security configuration.
For reactive (WebFlux) application the same effect can be achieved with a custom `WebFilterChainProxy` bean.
[NOTE]
As of Spring Boot 2.5, the `info` endpoint requires authentication by default in `spring-boot-starter-security`.
Since, the contents of the `info` endpoint is highly customizable, the boot team has decided that an `opt-in` approach is more prudent.
For backward compatibility, the stream apps continue to expose the `info` endpoint. The standard configuration contains no sensitive information.
=== Configuration
To include app starters security management for a stream app, just include a dependency on this module.
To include security management for a stream app, just include a dependency on this module.
[source,xml]
----
@@ -27,7 +32,7 @@ To include app starters security management for a stream app, just include a dep
----
All Spring Cloud Stream app starters that inherit form the `core` pom have the `app-starters-security-common` dependency included by default.
All Spring Cloud Stream applications that inherit form the `core` pom include the `stream-applications-security-common` dependency.
* `spring.cloud.streamapp.security.enabled` (default: `true`). If set to `false` it surpasses the boot security.
* `spring.cloud.streamapp.security.csrf-enabled` (default: `true`). If set to `false`, for secured applications it enables CQRS.

View File

@@ -36,21 +36,22 @@ import org.springframework.web.reactive.config.WebFluxConfigurer;
/**
* @author Artem Bilan
* @author David Turanski
* @since 3.0
*/
@Conditional(OnHttpCsrfOrSecurityDisabled.class)
@Configuration
@ConditionalOnClass({Flux.class, EnableWebFluxSecurity.class, WebFilterChainProxy.class, WebFluxConfigurer.class})
@ConditionalOnClass({ Flux.class, EnableWebFluxSecurity.class, WebFilterChainProxy.class, WebFluxConfigurer.class })
@ConditionalOnMissingBean(WebFilterChainProxy.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@AutoConfigureBefore({ReactiveManagementWebSecurityAutoConfiguration.class,
ReactiveSecurityAutoConfiguration.class})
@AutoConfigureBefore({ ReactiveManagementWebSecurityAutoConfiguration.class,
ReactiveSecurityAutoConfiguration.class })
@EnableConfigurationProperties(AppStarterWebSecurityAutoConfigurationProperties.class)
public class AppStarterWebFluxSecurityAutoConfiguration {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
AppStarterWebSecurityAutoConfigurationProperties securityProperties) {
AppStarterWebSecurityAutoConfigurationProperties securityProperties) {
if (!securityProperties.isCsrfEnabled()) {
http.csrf().disable();
}
@@ -59,6 +60,13 @@ public class AppStarterWebFluxSecurityAutoConfiguration {
.anyExchange()
.permitAll();
}
else {
http.authorizeExchange().pathMatchers("/actuator/health", "/actuator/info", "/actuator/bindings")
.permitAll().anyExchange().authenticated();
http.httpBasic();
http.formLogin();
}
return http.build();
}

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.stream.app.security.common;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -27,13 +28,13 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @author Christian Tzolov
* @author Artem Bilan
* @author David Turanski
* @since 2.1
*/
@Conditional(OnHttpCsrfOrSecurityDisabled.class)
@@ -41,7 +42,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@AutoConfigureBefore({ManagementWebSecurityAutoConfiguration.class, SecurityAutoConfiguration.class})
@AutoConfigureBefore({ ManagementWebSecurityAutoConfiguration.class, SecurityAutoConfiguration.class })
@EnableConfigurationProperties(AppStarterWebSecurityAutoConfigurationProperties.class)
@EnableWebSecurity
public class AppStarterWebSecurityAutoConfiguration {
@@ -50,24 +51,23 @@ public class AppStarterWebSecurityAutoConfiguration {
WebSecurityConfigurerAdapter appStarterWebSecurityConfigurerAdapter(
AppStarterWebSecurityAutoConfigurationProperties securityProperties) {
return new WebSecurityConfigurerAdapter() {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
if (!securityProperties.isCsrfEnabled()) {
http.csrf().disable();
}
}
@Override
public void configure(WebSecurity builder) {
if (!securityProperties.isEnabled()) {
builder.ignoring().antMatchers("/**");
if (securityProperties.isEnabled()) {
http.authorizeRequests()
.requestMatchers(EndpointRequest.to("health", "info", "bindings")).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint()).authenticated()
.and().formLogin().and().httpBasic();
}
else {
http.authorizeRequests().anyRequest().permitAll();
}
}
};
}

View File

@@ -32,11 +32,11 @@ class OnHttpCsrfOrSecurityDisabled extends AnyNestedCondition {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
@ConditionalOnProperty(name = "spring.cloud.streamapp.security.enabled", havingValue = "false")
@ConditionalOnProperty(name = "spring.cloud.streamapp.security.enabled", havingValue = "false", matchIfMissing = true)
static class SecurityDisabled {
}
@ConditionalOnProperty(name = "spring.cloud.streamapp.security.csrf-enabled", havingValue = "false")
@ConditionalOnProperty(name = "spring.cloud.streamapp.security.csrf-enabled", havingValue = "false", matchIfMissing = true)
static class HttpCsrfDisabled {
}

View File

@@ -16,8 +16,10 @@
package org.springframework.cloud.stream.app.security.common;
import java.util.List;
import java.util.Map;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
@@ -28,12 +30,13 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
/**
* @author Artem Bilan
* @author David Turanski
* @since 3.0
*/
@TestPropertySource(properties = {
"spring.main.web-application-type=reactive",
"spring.cloud.streamapp.security.enabled=false",
"management.endpoints.web.exposure.include=health,info,env",
"management.endpoints.web.exposure.include=health,info,bindings,env",
"info.name=MY TEST APP"})
public class ReactiveSecurityDisabledManagementSecurityEnabledTests extends AbstractSecurityCommonTests {
@@ -58,6 +61,13 @@ public class ReactiveSecurityDisabledManagementSecurityEnabledTests extends Abst
assertThat(info.get("name")).isEqualTo("MY TEST APP");
}
@Test
@SuppressWarnings("rawtypes")
public void testBindingsEndpoint() {
ResponseEntity<List> response = this.restTemplate.getForEntity("/actuator/bindings", List.class);
Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
@SuppressWarnings("rawtypes")
public void testEnvEndpoint() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2019-2020 the original author or authors.
* Copyright 2021-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package org.springframework.cloud.stream.app.security.common;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
@@ -32,21 +33,21 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
* @author David Turanski
* @since 3.0
*/
@TestPropertySource(properties = {
"spring.main.web-application-type=reactive",
"org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration"
+ ",org.springframework.cloud.stream.app.security.common.AppStarterWebFluxSecurityAutoConfiguration",
"management.endpoints.web.exposure.include=health,info,env",
"info.name=MY TEST APP"})
public class ReactiveSecurityEnabledManagementSecurityDisabledAuthorizedAccessTests extends AbstractSecurityCommonTests {
"management.endpoints.web.exposure.include=*",
"info.name=MY TEST APP" })
public class ReactiveSecurityEnabledAuthorizedAccessTests
extends AbstractSecurityCommonTests {
@Autowired
private SecurityProperties securityProperties;
@BeforeEach
public void before() {
public void authenticate() {
restTemplate.getRestTemplate().getInterceptors().add(new BasicAuthenticationInterceptor(
securityProperties.getUser().getName(), securityProperties.getUser().getPassword()));
}
@@ -72,12 +73,25 @@ public class ReactiveSecurityEnabledManagementSecurityDisabledAuthorizedAccessTe
assertThat(info.get("name")).isEqualTo("MY TEST APP");
}
@Test
@SuppressWarnings("rawtypes")
public void testBindingsEndpoint() {
ResponseEntity<List> response = this.restTemplate.getForEntity("/actuator/bindings", List.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
@SuppressWarnings("rawtypes")
public void testBeansEndpoint() {
ResponseEntity<?> response = this.restTemplate.getForEntity("/actuator/beans", Object.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
@SuppressWarnings("rawtypes")
public void testEnvEndpoint() {
ResponseEntity<Map> response = this.restTemplate.getForEntity("/actuator/env", Map.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.hasBody()).isTrue();
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.stream.app.security.common;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
@@ -29,14 +30,15 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
* @author Artem Bilan
* @Author David Turanski
* @since 3.0
*/
@TestPropertySource(properties = {
"spring.main.web-application-type=reactive",
"spring.autoconfigure.exclude=" +
"org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration"
+ ",org.springframework.cloud.stream.app.security.common.AppStarterWebFluxSecurityAutoConfiguration",
"management.endpoints.web.exposure.include=health,info"})
"org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration," +
"org.springframework.cloud.stream.app.security.common.AppStarterWebFluxSecurityAutoConfiguration",
"management.endpoints.web.exposure.include=health,info,bindings"})
public class ReactiveSecurityEnabledManagementSecurityDisabledUnauthorizedAccessTests extends AbstractSecurityCommonTests {
@Test
@@ -53,6 +55,13 @@ public class ReactiveSecurityEnabledManagementSecurityDisabledUnauthorizedAccess
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
@SuppressWarnings("rawtypes")
public void testBindingsEndpoint() {
ResponseEntity<List> response = this.restTemplate.getForEntity("/actuator/bindings", List.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
@SuppressWarnings("rawtypes")
public void testEnvEndpoint() {

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.stream.app.security.common;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
@@ -29,11 +30,12 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
* @author Artem Bilan
* @author David Turanski
* @since 3.0
*/
@TestPropertySource(properties = {
"spring.main.web-application-type=reactive",
"management.endpoints.web.exposure.include=health,info,env",
"management.endpoints.web.exposure.include=health,info,env,bindings",
"info.name=MY TEST APP"})
public class ReactiveSecurityEnabledManagementSecurityEnabledTests extends AbstractSecurityCommonTests {
@@ -52,10 +54,17 @@ public class ReactiveSecurityEnabledManagementSecurityEnabledTests extends Abstr
@SuppressWarnings("rawtypes")
public void testInfoEndpoint() {
ResponseEntity<Map> response = this.restTemplate.getForEntity("/actuator/info", Map.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
// The ManagementWebSecurityAutoConfiguration exposes only Info and Health endpoint not Env!
@Test
@SuppressWarnings("rawtypes")
public void testBindingsEndpoint() {
ResponseEntity<List> response = this.restTemplate.getForEntity("/actuator/bindings", List.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
// The ManagementWebSecurityAutoConfiguration exposes only info,health, bindings endpoint not env!
@Test
@SuppressWarnings("rawtypes")
public void testEnvEndpoint() {

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.stream.app.security.common;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
@@ -29,12 +30,13 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
* @author Artem Bilan
* @author David Turanski
* @since 3.0
*/
@TestPropertySource(properties = {
"spring.main.web-application-type=servlet",
"spring.cloud.streamapp.security.enabled=false",
"management.endpoints.web.exposure.include=health,info,env",
"management.endpoints.web.exposure.include=health,info,bindings,env",
"info.name=MY TEST APP"})
public class SecurityDisabledManagementSecurityEnabledTests extends AbstractSecurityCommonTests {
@@ -58,6 +60,13 @@ public class SecurityDisabledManagementSecurityEnabledTests extends AbstractSecu
assertThat(info.get("name")).isEqualTo("MY TEST APP");
}
@Test
@SuppressWarnings("rawtypes")
public void testBindingsEndpoint() {
ResponseEntity<List> response = this.restTemplate.getForEntity("/actuator/bindings", List.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
@SuppressWarnings("rawtypes")
public void testEnvEndpoint() {

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.stream.app.security.common;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
@@ -33,22 +34,20 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
* @author Artem Bilan
* @author David Turanski
* @since 3.0
*/
@TestPropertySource(properties = {
"spring.main.web-application-type=servlet",
"spring.autoconfigure.exclude=org.springframework.boot.actuate.autoconfigure.security.servlet" +
".ManagementWebSecurityAutoConfiguration"
+ ",org.springframework.cloud.stream.app.security.common.AppStarterWebSecurityAutoConfiguration",
"management.endpoints.web.exposure.include=health,info,env",
"management.endpoints.web.exposure.include=health,info,bindings,env",
"info.name=MY TEST APP"})
public class SecurityEnabledManagementSecurityDisabledAuthorizedAccessTests extends AbstractSecurityCommonTests {
public class SecurityEnabledAuthorizedAccessTests extends AbstractSecurityCommonTests {
@Autowired
private SecurityProperties securityProperties;
@BeforeEach
public void before() {
public void authenticate() {
restTemplate.getRestTemplate().getInterceptors().add(new BasicAuthenticationInterceptor(
securityProperties.getUser().getName(), securityProperties.getUser().getPassword()));
}
@@ -73,6 +72,13 @@ public class SecurityEnabledManagementSecurityDisabledAuthorizedAccessTests exte
assertThat(info.get("name")).isEqualTo("MY TEST APP");
}
@Test
@SuppressWarnings("rawtypes")
public void testBindingsEndpoint() {
ResponseEntity<List> response = this.restTemplate.getForEntity("/actuator/bindings", List.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
@SuppressWarnings("rawtypes")
public void testEnvEndpoint() {

View File

@@ -29,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
* @author Artem Bilan
* @Author David Turanski
* @since 3.0
*/
@TestPropertySource(properties = {
@@ -36,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
"spring.autoconfigure.exclude=" +
"org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration"
+ ",org.springframework.cloud.stream.app.security.common.AppStarterWebSecurityAutoConfiguration",
"management.endpoints.web.exposure.include=health,info"})
"management.endpoints.web.exposure.include=health,info,bindings,env" })
public class SecurityEnabledManagementSecurityDisabledUnauthorizedAccessTests extends AbstractSecurityCommonTests {
@Test
@@ -59,6 +60,13 @@ public class SecurityEnabledManagementSecurityDisabledUnauthorizedAccessTests ex
assertThat(info.get("error")).isEqualTo("Unauthorized");
}
@Test
@SuppressWarnings("rawtypes")
public void testBindingsEndpoint() {
ResponseEntity<?> response = this.restTemplate.getForEntity("/actuator/bindings", Object.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
@SuppressWarnings("rawtypes")
public void testEnvEndpoint() {

View File

@@ -51,7 +51,7 @@ public class SecurityEnabledManagementSecurityEnabledTests extends AbstractSecur
@SuppressWarnings("rawtypes")
public void testInfoEndpoint() {
ResponseEntity<Map> response = this.restTemplate.getForEntity("/actuator/info", Map.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
// The ManagementWebSecurityAutoConfiguration exposes only Info and Health endpoint not Env!

View File

@@ -37,7 +37,6 @@ class KafkaTimeSourceTests extends TimeSourceTests {
static StreamAppContainer source = new KafkaStreamAppContainer(StreamAppContainerTestUtils
.imageName(StreamAppContainerTestUtils.SPRINGCLOUDSTREAM_REPOSITOTRY, "time-source-kafka", VERSION))
.withCommand("--server.port", "8080")
.withEnv("SPRING_CLOUD_STREAMAPP_SECURITY_ENABLED", "false")
.withExposedPorts(8080);
@Test

View File

@@ -339,7 +339,6 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludedGroups>!integration</excludedGroups>
<groups>integration</groups>
</configuration>
</plugin>