Skip to content

Commit

Permalink
Merge pull request #211 from xenit-eu/contentgrid-actuator-security
Browse files Browse the repository at this point in the history
Fix ActuatorEndpointsWebSecurityAutoConfiguration to allow policy and webhooks endpoint from remote addresses
  • Loading branch information
tgeens authored Apr 12, 2024
2 parents b665fef + 664dce6 commit efd628e
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ PolicyVariables policyVariables(ContentGridApplicationProperties applicationProp
PolicyActuator policyActuator(PolicyVariables policyVariables) {
return new PolicyActuator(applicationContext.getResource("classpath:rego/policy.rego"), policyVariables);
}

@Bean
@ConditionalOnBean(PolicyActuator.class)
ContentGridExposedActuatorEndpoint policyActuatorExposedEndpoint() {
return new ContentGridExposedActuatorEndpoint(PolicyActuator.class);
}
}


Expand All @@ -76,6 +82,12 @@ WebhookVariables webhookVariables(ContentGridApplicationProperties applicationPr
.userVariables(applicationProperties.getVariables())
.build();
}

@Bean
@ConditionalOnBean(WebHooksConfigActuator.class)
ContentGridExposedActuatorEndpoint webhooksActuatorExposedEndpoint() {
return new ContentGridExposedActuatorEndpoint(WebHooksConfigActuator.class);
}
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.contentgrid.spring.boot.autoconfigure.actuator;

import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.security.web.util.matcher.RequestMatcher;

@Value
@RequiredArgsConstructor
public class ContentGridExposedActuatorEndpoint {

Class<?> endpoint;

public RequestMatcher toRequestMatcher() {
return EndpointRequest.to(endpoint);
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.contentgrid.spring.boot.autoconfigure.security;

import com.contentgrid.spring.boot.autoconfigure.actuator.ContentGridExposedActuatorEndpoint;
import jakarta.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.stream.Stream;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration;
Expand All @@ -25,6 +28,7 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;


Expand Down Expand Up @@ -57,7 +61,8 @@ public class ActuatorEndpointsWebSecurityAutoConfiguration {

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SecurityFilterChain actuatorEndpointsSecurityFilterChain(HttpSecurity http, Environment environment)
SecurityFilterChain actuatorEndpointsSecurityFilterChain(HttpSecurity http, Environment environment,
ObjectProvider<ContentGridExposedActuatorEndpoint> exposedEndpoints)
throws Exception {

http
Expand All @@ -71,6 +76,16 @@ SecurityFilterChain actuatorEndpointsSecurityFilterChain(HttpSecurity http, Envi
new AndRequestMatcher(
EndpointRequest.toAnyEndpoint(),
new LoopbackInetAddressMatcher()
),
new AndRequestMatcher(
new OrRequestMatcher(
Stream.concat(
Stream.of(request -> false),
exposedEndpoints.stream()
.map(ContentGridExposedActuatorEndpoint::toRequestMatcher))
.toList()
),
request -> ManagementPortType.get(environment) == ManagementPortType.DIFFERENT
)
)
.permitAll());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;

import com.contentgrid.spring.boot.actuator.policy.PolicyActuator;
import com.contentgrid.spring.boot.actuator.webhooks.WebHooksConfigActuator;
import com.contentgrid.spring.boot.autoconfigure.actuator.ActuatorAutoConfiguration;
import com.contentgrid.spring.boot.autoconfigure.security.ManagementContextSupplierConfiguration.ManagementContextSupplier;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -31,6 +34,7 @@
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
Expand All @@ -57,6 +61,9 @@ class ActuatorEndpointsWebSecurityAutoConfigurationTest {
static final String ACTUATOR_METRICS = "/actuator/metrics";
static final String ACTUATOR_ENV = "/actuator/env";

static final String ACTUATOR_POLICY = "/actuator/policy";
static final String ACTUATOR_WEBHOOK = "/actuator/webhooks";

static class AutoConfigs {

static final AutoConfigurations ACTUATORS = AutoConfigurations.of(
Expand All @@ -82,16 +89,22 @@ static class AutoConfigs {
ServletWebServerFactoryAutoConfiguration.class,
DispatcherServletAutoConfiguration.class
);

static final AutoConfigurations CONTENTGRID_ACTUATORS = AutoConfigurations.of(
ActuatorAutoConfiguration.class
);
}

WebApplicationContextRunner runner = new WebApplicationContextRunner(
AnnotationConfigServletWebServerApplicationContext::new)
.withPropertyValues(
"server.port=0",
"management.endpoints.web.exposure.include=*"
"management.endpoints.web.exposure.include=*",
"contentgrid.system.policyPackage=foobar"
)
.withInitializer(new ServerPortInfoApplicationContextInitializer())
.withConfiguration(AutoConfigs.ACTUATORS)
.withConfiguration(AutoConfigs.CONTENTGRID_ACTUATORS)
.withConfiguration(AutoConfigs.MANAGEMENT)
.withConfiguration(AutoConfigurations.of(
SecurityAutoConfiguration.class,
Expand All @@ -105,7 +118,9 @@ void whenAccessFromRemoteAddress() {
runner.run(context -> {
assertThat(context)
.hasNotFailed()
.hasBean("actuatorEndpointsSecurityFilterChain");
.hasBean("actuatorEndpointsSecurityFilterChain")
.hasSingleBean(PolicyActuator.class)
.hasSingleBean(WebHooksConfigActuator.class);

withWebTestClient(context, remoteAddress("192.0.2.1"), assertHttp -> {
// public endpoints
Expand All @@ -114,6 +129,8 @@ void whenAccessFromRemoteAddress() {

// management on primary server port
assertHttp.get(ACTUATOR_METRICS).isForbidden();
assertHttp.get(ACTUATOR_POLICY).isForbidden();
assertHttp.get(ACTUATOR_WEBHOOK).isForbidden();

// other endpoints fall through if not from loopback-address
assertHttp.get(ACTUATOR_ENV).isForbidden();
Expand Down Expand Up @@ -152,6 +169,8 @@ void whenManagementOnDifferentPort() {

// management on different port
assertHttp.get(ACTUATOR_METRICS).isOk();
assertHttp.get(ACTUATOR_POLICY).isOk();
assertHttp.get(ACTUATOR_WEBHOOK).isOk();

// other endpoints fall through if not from loopback-address
assertHttp.get(ACTUATOR_ENV).isForbidden();
Expand All @@ -172,7 +191,8 @@ void whenManagementOnDifferentPort() {
assertThat(filterChain.matches(get("/api", servletContext))).isFalse();

// management context runs on a different port
var mgmtServletContext = context.getBean(ManagementContextSupplier.class).get().getServletContext();
var mgmtServletContext = context.getBean(ManagementContextSupplier.class).get()
.getServletContext();
assertThat(filterChain.matches(get("/actuator", mgmtServletContext))).isTrue();
assertThat(filterChain.matches(get("/actuator/health", mgmtServletContext))).isTrue();
assertThat(filterChain.matches(get("/api", mgmtServletContext))).isFalse();
Expand All @@ -194,6 +214,8 @@ void whenAccessFromLocalAddress() {
assertHttp.get(ACTUATOR_HEALTH).isOk();
assertHttp.get(ACTUATOR_INFO).isOk();
assertHttp.get(ACTUATOR_METRICS).isOk();
assertHttp.get(ACTUATOR_POLICY).isOk();
assertHttp.get(ACTUATOR_WEBHOOK).isOk();
assertHttp.get(ACTUATOR_ENV).isOk();

assertHttp.get(ACTUATOR_ROOT).isOk();
Expand All @@ -213,6 +235,44 @@ void whenAccessFromLocalAddress() {
});
}

@Test
void withoutContentGridActuatorsOnClasspath() {

runner
.withPropertyValues("management.server.port=0")
.withClassLoader(new FilteredClassLoader(PolicyActuator.class))
.run(context -> {
assertThat(context)
.hasNotFailed()
.hasBean("actuatorEndpointsSecurityFilterChain")
.doesNotHaveBean(PolicyActuator.class)
.doesNotHaveBean(WebHooksConfigActuator.class);

// security matcher on /actuator/** and only /actuator/** endpoints
// because contentgrid actuators are not loaded
// the contentgrid-actuators are not covered by EndpointRequest.anyEndpoint()
assertThat(context.getBean("actuatorEndpointsSecurityFilterChain", SecurityFilterChain.class))
.isNotNull()
.satisfies(filterChain -> {
var servletContext = context.getServletContext();
assertThat(context.getServletContext()).isNotNull();

assertThat(filterChain.matches(get(ACTUATOR_ROOT, servletContext))).isFalse();
assertThat(filterChain.matches(get(ACTUATOR_HEALTH, servletContext))).isFalse();
assertThat(filterChain.matches(get(ACTUATOR_POLICY, servletContext))).isFalse();
assertThat(filterChain.matches(get(ACTUATOR_WEBHOOK, servletContext))).isFalse();

// management context runs on a different port
var mgmtServletContext = context.getBean(ManagementContextSupplier.class).get()
.getServletContext();
assertThat(filterChain.matches(get(ACTUATOR_ROOT, mgmtServletContext))).isTrue();
assertThat(filterChain.matches(get(ACTUATOR_HEALTH, mgmtServletContext))).isTrue();
assertThat(filterChain.matches(get(ACTUATOR_POLICY, mgmtServletContext))).isFalse();
assertThat(filterChain.matches(get(ACTUATOR_WEBHOOK, mgmtServletContext))).isFalse();
});
});
}

private void withWebTestClient(ApplicationContext context, MockMvcConfigurer remoteAddress,
Consumer<HttpAssertClient> callback) {
WebTestClient client;
Expand All @@ -224,7 +284,7 @@ private void withWebTestClient(ApplicationContext context, MockMvcConfigurer rem
.build();

callback.accept((@NonNull String endpoint) ->
client.get().uri(endpoint).accept(MediaType.APPLICATION_JSON).exchange().expectStatus());
client.get().uri(endpoint).accept(MediaType.APPLICATION_JSON, MediaType.ALL).exchange().expectStatus());
}

private static HttpServletRequest get(String endpoint, ServletContext servletContext) {
Expand Down
Empty file.
Empty file.

0 comments on commit efd628e

Please sign in to comment.