Commit e57aafd6 authored by Madhura Bhave's avatar Madhura Bhave

Provide EndpointRequest for WebFlux-based Security

Closes gh-11022
parent 32557e49
/*
* Copyright 2012-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.security.reactive;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.EndpointPathProvider;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.security.reactive.ApplicationContextServerWebExchangeMatcher;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
/**
* Factory that can be used to create a {@link ServerWebExchangeMatcher} for actuator endpoint
* locations.
*
* @author Madhura Bhave
* @since 2.0.0
*/
public final class EndpointRequest {
private EndpointRequest() {
}
/**
* Returns a matcher that includes all {@link Endpoint actuator endpoints}. The
* {@link EndpointServerWebExchangeMatcher#excluding(Class...) excluding} method can be used to
* further remove specific endpoints if required. For example: <pre class="code">
* EndpointServerWebExchangeMatcher.toAnyEndpoint().excluding(ShutdownEndpoint.class)
* </pre>
* @return the configured {@link ServerWebExchangeMatcher}
*/
public static EndpointServerWebExchangeMatcher toAnyEndpoint() {
return new EndpointServerWebExchangeMatcher();
}
/**
* Returns a matcher that includes the specified {@link Endpoint actuator endpoints}.
* For example: <pre class="code">
* EndpointRequest.to(ShutdownEndpoint.class, HealthEndpoint.class)
* </pre>
* @param endpoints the endpoints to include
* @return the configured {@link ServerWebExchangeMatcher}
*/
public static EndpointServerWebExchangeMatcher to(Class<?>... endpoints) {
return new EndpointServerWebExchangeMatcher(endpoints);
}
/**
* Returns a matcher that includes the specified {@link Endpoint actuator endpoints}.
* For example: <pre class="code">
* EndpointRequest.to("shutdown", "health")
* </pre>
* @param endpoints the endpoints to include
* @return the configured {@link ServerWebExchangeMatcher}
*/
public static EndpointServerWebExchangeMatcher to(String... endpoints) {
return new EndpointServerWebExchangeMatcher(endpoints);
}
/**
* The {@link ServerWebExchangeMatcher} used to match against {@link Endpoint actuator endpoints}.
*/
public final static class EndpointServerWebExchangeMatcher
extends ApplicationContextServerWebExchangeMatcher<EndpointPathProvider> {
private final List<Object> includes;
private final List<Object> excludes;
private ServerWebExchangeMatcher delegate;
private EndpointServerWebExchangeMatcher() {
super(EndpointPathProvider.class);
this.includes = Collections.emptyList();
this.excludes = Collections.emptyList();
}
private EndpointServerWebExchangeMatcher(Class<?>[] endpoints) {
super(EndpointPathProvider.class);
this.includes = Arrays.asList((Object[]) endpoints);
this.excludes = Collections.emptyList();
}
private EndpointServerWebExchangeMatcher(String[] endpoints) {
super(EndpointPathProvider.class);
this.includes = Arrays.asList((Object[]) endpoints);
this.excludes = Collections.emptyList();
}
private EndpointServerWebExchangeMatcher(List<Object> includes, List<Object> excludes) {
super(EndpointPathProvider.class);
this.includes = includes;
this.excludes = excludes;
}
EndpointServerWebExchangeMatcher excluding(Class<?>... endpoints) {
List<Object> excludes = new ArrayList<>(this.excludes);
excludes.addAll(Arrays.asList((Object[]) endpoints));
return new EndpointServerWebExchangeMatcher(this.includes, excludes);
}
EndpointServerWebExchangeMatcher excluding(String... endpoints) {
List<Object> excludes = new ArrayList<>(this.excludes);
excludes.addAll(Arrays.asList((Object[]) endpoints));
return new EndpointServerWebExchangeMatcher(this.includes, excludes);
}
@Override
protected void initialized(EndpointPathProvider endpointPathProvider) {
Set<String> paths = new LinkedHashSet<>(this.includes.isEmpty()
? endpointPathProvider.getPaths() : Collections.emptyList());
streamPaths(this.includes, endpointPathProvider).forEach(paths::add);
streamPaths(this.excludes, endpointPathProvider).forEach(paths::remove);
this.delegate = new OrServerWebExchangeMatcher(getDelegateMatchers(paths));
}
private Stream<String> streamPaths(List<Object> source,
EndpointPathProvider endpointPathProvider) {
return source.stream().filter(Objects::nonNull).map(this::getPathId)
.map(endpointPathProvider::getPath);
}
private String getPathId(Object source) {
if (source instanceof String) {
return (String) source;
}
if (source instanceof Class) {
return getPathId((Class<?>) source);
}
throw new IllegalStateException("Unsupported source " + source);
}
private String getPathId(Class<?> source) {
Endpoint annotation = AnnotationUtils.findAnnotation(source, Endpoint.class);
Assert.state(annotation != null,
() -> "Class " + source + " is not annotated with @Endpoint");
return annotation.id();
}
private List<ServerWebExchangeMatcher> getDelegateMatchers(Set<String> paths) {
return paths.stream().map((path) -> new PathPatternParserServerWebExchangeMatcher(path + "/**"))
.collect(Collectors.toList());
}
@Override
protected Mono<MatchResult> matches(ServerWebExchange exchange,
EndpointPathProvider context) {
return this.delegate.matches(exchange);
}
}
}
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.security; package org.springframework.boot.actuate.autoconfigure.security.servlet;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
...@@ -28,10 +28,9 @@ import java.util.stream.Stream; ...@@ -28,10 +28,9 @@ import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.EndpointPathProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.web.EndpointPathProvider;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.security.ApplicationContextRequestMatcher; import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher;
...@@ -172,16 +171,6 @@ public final class EndpointRequest { ...@@ -172,16 +171,6 @@ public final class EndpointRequest {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@Override
public boolean matches(HttpServletRequest request) {
try {
return super.matches(request);
}
catch (BeanCreationException ex) {
return false;
}
}
@Override @Override
protected boolean matches(HttpServletRequest request, protected boolean matches(HttpServletRequest request,
EndpointPathProvider context) { EndpointPathProvider context) {
......
/*
* Copyright 2012-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.security.reactive;
import java.util.Arrays;
import java.util.List;
import org.assertj.core.api.AssertDelegateTarget;
import org.junit.Test;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.EndpointPathProvider;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link EndpointRequest}.
*
* @author Madhura Bhave
*/
public class EndpointRequestTests {
@Test
public void toAnyEndpointShouldMatchEndpointPath() {
ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint();
assertMatcher(matcher).matches("/actuator/foo");
assertMatcher(matcher).matches("/actuator/bar");
}
@Test
public void toAnyEndpointShouldNotMatchOtherPath() {
ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint();
assertMatcher(matcher).doesNotMatch("/actuator/baz");
}
@Test
public void toEndpointClassShouldMatchEndpointPath() {
ServerWebExchangeMatcher matcher = EndpointRequest.to(FooEndpoint.class);
assertMatcher(matcher).matches("/actuator/foo");
}
@Test
public void toEndpointClassShouldNotMatchOtherPath() {
ServerWebExchangeMatcher matcher = EndpointRequest.to(FooEndpoint.class);
assertMatcher(matcher).doesNotMatch("/actuator/bar");
}
@Test
public void toEndpointIdShouldMatchEndpointPath() {
ServerWebExchangeMatcher matcher = EndpointRequest.to("foo");
assertMatcher(matcher).matches("/actuator/foo");
}
@Test
public void toEndpointIdShouldNotMatchOtherPath() {
ServerWebExchangeMatcher matcher = EndpointRequest.to("foo");
assertMatcher(matcher).doesNotMatch("/actuator/bar");
}
@Test
public void excludeByClassShouldNotMatchExcluded() {
ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint()
.excluding(FooEndpoint.class);
assertMatcher(matcher).doesNotMatch("/actuator/foo");
assertMatcher(matcher).matches("/actuator/bar");
}
@Test
public void excludeByIdShouldNotMatchExcluded() {
ServerWebExchangeMatcher matcher = EndpointRequest.toAnyEndpoint().excluding("foo");
assertMatcher(matcher).doesNotMatch("/actuator/foo");
assertMatcher(matcher).matches("/actuator/bar");
}
private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher) {
return assertMatcher(matcher, new MockEndpointPathProvider());
}
private RequestMatcherAssert assertMatcher(ServerWebExchangeMatcher matcher,
EndpointPathProvider endpointPathProvider) {
StaticApplicationContext context = new StaticApplicationContext();
context.registerBean(EndpointPathProvider.class, () -> endpointPathProvider);
return assertThat(new RequestMatcherAssert(context, matcher));
}
private static class RequestMatcherAssert implements AssertDelegateTarget {
private final StaticApplicationContext context;
private final ServerWebExchangeMatcher matcher;
RequestMatcherAssert(StaticApplicationContext context, ServerWebExchangeMatcher matcher) {
this.context = context;
this.matcher = matcher;
}
void matches(String path) {
ServerWebExchange exchange = webHandler().createExchange(MockServerHttpRequest.get(path).build(), new MockServerHttpResponse());
matches(exchange);
}
private void matches(ServerWebExchange exchange) {
assertThat(this.matcher.matches(exchange).block().isMatch())
.as("Matches " + getRequestPath(exchange)).isTrue();
}
void doesNotMatch(String path) {
ServerWebExchange exchange = webHandler().createExchange(MockServerHttpRequest.get(path).build(), new MockServerHttpResponse());
doesNotMatch(exchange);
}
private void doesNotMatch(ServerWebExchange exchange) {
assertThat(this.matcher.matches(exchange).block().isMatch())
.as("Does not match " + getRequestPath(exchange)).isFalse();
}
private TestHttpWebHandlerAdapter webHandler() {
TestHttpWebHandlerAdapter adapter = new TestHttpWebHandlerAdapter(mock(WebHandler.class));
adapter.setApplicationContext(this.context);
return adapter;
}
private String getRequestPath(ServerWebExchange exchange) {
return exchange.getRequest().getPath().toString();
}
}
private static class TestHttpWebHandlerAdapter extends HttpWebHandlerAdapter {
TestHttpWebHandlerAdapter(WebHandler delegate) {
super(delegate);
}
@Override
protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) {
return super.createExchange(request, response);
}
}
private static class MockEndpointPathProvider implements EndpointPathProvider {
@Override
public List<String> getPaths() {
return Arrays.asList("/actuator/foo", "/actuator/bar");
}
@Override
public String getPath(String id) {
if ("foo".equals(id)) {
return "/actuator/foo";
}
if ("bar".equals(id)) {
return "/actuator/bar";
}
return null;
}
}
@Endpoint(id = "foo")
private static class FooEndpoint {
}
}
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.actuate.autoconfigure.security; package org.springframework.boot.actuate.autoconfigure.security.servlet;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
......
...@@ -27,7 +27,7 @@ import java.util.stream.Stream; ...@@ -27,7 +27,7 @@ import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.security.ApplicationContextRequestMatcher; import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher;
......
...@@ -2882,6 +2882,17 @@ messages. Otherwise, the default password is not printed. ...@@ -2882,6 +2882,17 @@ messages. Otherwise, the default password is not printed.
You can change the username and password by providing a `spring.security.user.name` and You can change the username and password by providing a `spring.security.user.name` and
`spring.security.user.password`. `spring.security.user.password`.
The basic features you get by default in a web application are:
* A `UserDetailsService` (or `ReactiveUserDetailsService` in case of a WebFlux application)
bean with in-memory store and a single user with a generated password (see
{dc-spring-boot}/autoconfigure/security/SecurityProperties.User.html[`SecurityProperties.User`]
for the properties of the user).
* Form-based login or HTTP Basic security (depending on Content-Type) for the entire
application (including actuator endpoints if actuator is on the classpath).
== MVC Security
The default security configuration is implemented in `SecurityAutoConfiguration` and in The default security configuration is implemented in `SecurityAutoConfiguration` and in
the classes imported from there (`SpringBootWebSecurityConfiguration` for web security the classes imported from there (`SpringBootWebSecurityConfiguration` for web security
and `AuthenticationManagerConfiguration` for authentication configuration, which is also and `AuthenticationManagerConfiguration` for authentication configuration, which is also
...@@ -2894,15 +2905,6 @@ To also switch off the authentication manager configuration, you can add a bean ...@@ -2894,15 +2905,6 @@ To also switch off the authentication manager configuration, you can add a bean
There are several secure applications in the {github-code}/spring-boot-samples/[Spring There are several secure applications in the {github-code}/spring-boot-samples/[Spring
Boot samples] to get you started with common use cases. Boot samples] to get you started with common use cases.
The basic features you get by default in a web application are:
* A `UserDetailsService` bean with in-memory store and a single user with a generated
password (see
{dc-spring-boot}/autoconfigure/security/SecurityProperties.User.html[`SecurityProperties.User`]
for the properties of the user).
* Form-based login or HTTP Basic security (depending on Content-Type) for the entire
application (including actuator endpoints if actuator is on the classpath).
Access rules can be overridden by adding a custom `WebSecurityConfigurerAdapter`. Spring Access rules can be overridden by adding a custom `WebSecurityConfigurerAdapter`. Spring
Boot provides convenience methods that can be used to override access rules for actuator Boot provides convenience methods that can be used to override access rules for actuator
endpoints and static resources. `EndpointRequest` can be used to create a `RequestMatcher` endpoints and static resources. `EndpointRequest` can be used to create a `RequestMatcher`
...@@ -2910,7 +2912,22 @@ that is based on the `management.endpoints.web.base-path` property. ...@@ -2910,7 +2912,22 @@ that is based on the `management.endpoints.web.base-path` property.
`StaticResourceRequest` can be used to create a `RequestMatcher` for static resources in `StaticResourceRequest` can be used to create a `RequestMatcher` for static resources in
commonly used locations. commonly used locations.
== WebFlux Security
The default security configuration is implemented in `ReactiveSecurityAutoConfiguration` and in
the classes imported from there (`WebFluxSecurityConfiguration` for web security
and `ReactiveAuthenticationManagerConfiguration` for authentication configuration, which is also
relevant in non-web applications). To switch off the default web application security
configuration completely, you can add a bean of type `WebFilterChainProxy` (doing
so does not disable the authentication manager configuration or Actuator's security).
To also switch off the authentication manager configuration, you can add a bean of type
`ReactiveUserDetailsService` or `ReactiveAuthenticationManager`.
Access rules can be configured by adding a custom `SecurityWebFilterChain`. Spring
Boot provides convenience methods that can be used to override access rules for actuator
endpoints and static resources. `EndpointRequest` can be used to create a `ServerWebExchangeMatcher`
that is based on the `management.endpoints.web.base-path` property.
[[boot-features-security-oauth2]] [[boot-features-security-oauth2]]
=== OAuth2 === OAuth2
......
/*
* Copyright 2012-2018 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.security.reactive;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
/**
* {@link ApplicationContext} backed {@link ServerWebExchangeMatcher}. Can work directly with the
* {@link ApplicationContext}, obtain an existing bean or
* {@link AutowireCapableBeanFactory#createBean(Class, int, boolean) create a new bean}
* that is autowired in the usual way.
*
* @param <C> The type of the context that the match method actually needs to use. Can be
* an {@link ApplicationContext}, a class of an {@link ApplicationContext#getBean(Class)
* existing bean} or a custom type that will be
* {@link AutowireCapableBeanFactory#createBean(Class, int, boolean) created} on demand.
* @author Madhura Bhave
* @since 2.0.0
*/
public abstract class ApplicationContextServerWebExchangeMatcher<C> implements ServerWebExchangeMatcher {
private final Class<? extends C> contextClass;
private C context;
private Object contextLock = new Object();
public ApplicationContextServerWebExchangeMatcher(Class<? extends C> contextClass) {
Assert.notNull(contextClass, "Context class must not be null");
this.contextClass = contextClass;
}
@Override
public final Mono<MatchResult> matches(ServerWebExchange exchange) {
return matches(exchange, getContext(exchange));
}
/**
* Decides whether the rule implemented by the strategy matches the supplied exchange.
* @param exchange the source exchange
* @param context the context instance
* @return if the exchange matches
*/
protected abstract Mono<MatchResult> matches(ServerWebExchange exchange, C context);
protected C getContext(ServerWebExchange exchange) {
if (this.context == null) {
synchronized (this.contextLock) {
this.context = createContext(exchange);
initialized(this.context);
}
}
return this.context;
}
/**
* Called once the context has been initialized.
* @param context the initialized context
*/
protected void initialized(C context) {
}
@SuppressWarnings("unchecked")
private C createContext(ServerWebExchange exchange) {
ApplicationContext context = exchange.getApplicationContext();
if (context == null) {
throw new IllegalStateException("No WebApplicationContext found.");
}
if (this.contextClass.isInstance(context)) {
return (C) context;
}
try {
return context.getBean(this.contextClass);
}
catch (NoSuchBeanDefinitionException ex) {
return (C) context.getAutowireCapableBeanFactory().createBean(
this.contextClass, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR,
false);
}
}
}
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.security; package org.springframework.boot.security.servlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
...@@ -53,7 +53,7 @@ public abstract class ApplicationContextRequestMatcher<C> implements RequestMatc ...@@ -53,7 +53,7 @@ public abstract class ApplicationContextRequestMatcher<C> implements RequestMatc
} }
@Override @Override
public boolean matches(HttpServletRequest request) { public final boolean matches(HttpServletRequest request) {
return matches(request, getContext(request)); return matches(request, getContext(request));
} }
......
/*
* Copyright 2012-2017 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.security.reactive;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import reactor.core.publisher.Mono;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ApplicationContextServerWebExchangeMatcher}.
*
* @author Madhura Bhave
*/
public class ApplicationContextServerWebExchangeMatcherTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void createWhenContextClassIsNullShouldThrowException() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Context class must not be null");
new TestApplicationContextServerWebExchangeMatcher<>(null);
}
@Test
public void matchesWhenContextClassIsApplicationContextShouldProvideContext() {
ServerWebExchange exchange = createHttpWebHandlerAdapter();
StaticApplicationContext context = (StaticApplicationContext) exchange.getApplicationContext();
assertThat(new TestApplicationContextServerWebExchangeMatcher<>(ApplicationContext.class)
.callMatchesAndReturnProvidedContext(exchange)).isEqualTo(context);
}
@Test
public void matchesWhenContextClassIsExistingBeanShouldProvideBean() {
ServerWebExchange exchange = createHttpWebHandlerAdapter();
StaticApplicationContext context = (StaticApplicationContext) exchange.getApplicationContext();
context.registerSingleton("existingBean", ExistingBean.class);
assertThat(new TestApplicationContextServerWebExchangeMatcher<>(ExistingBean.class)
.callMatchesAndReturnProvidedContext(exchange))
.isEqualTo(context.getBean(ExistingBean.class));
}
@Test
public void matchesWhenContextClassIsNewBeanShouldProvideBean() {
ServerWebExchange exchange = createHttpWebHandlerAdapter();
StaticApplicationContext context = (StaticApplicationContext) exchange.getApplicationContext();
context.registerSingleton("existingBean", ExistingBean.class);
assertThat(new TestApplicationContextServerWebExchangeMatcher<>(NewBean.class)
.callMatchesAndReturnProvidedContext(exchange).getBean())
.isEqualTo(context.getBean(ExistingBean.class));
}
@Test
public void matchesWhenContextIsNull() {
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path").build());
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("No WebApplicationContext found.");
new TestApplicationContextServerWebExchangeMatcher<>(ExistingBean.class)
.callMatchesAndReturnProvidedContext(exchange);
}
private ServerWebExchange createHttpWebHandlerAdapter() {
StaticApplicationContext context = new StaticApplicationContext();
TestHttpWebHandlerAdapter adapter = new TestHttpWebHandlerAdapter(mock(WebHandler.class));
adapter.setApplicationContext(context);
return adapter.createExchange(MockServerHttpRequest.get("/path").build(), new MockServerHttpResponse());
}
static class TestHttpWebHandlerAdapter extends HttpWebHandlerAdapter {
TestHttpWebHandlerAdapter(WebHandler delegate) {
super(delegate);
}
@Override
protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) {
return super.createExchange(request, response);
}
}
static class ExistingBean {
}
static class NewBean {
private final ExistingBean bean;
NewBean(ExistingBean bean) {
this.bean = bean;
}
public ExistingBean getBean() {
return this.bean;
}
}
static class TestApplicationContextServerWebExchangeMatcher<C>
extends ApplicationContextServerWebExchangeMatcher<C> {
private C providedContext;
TestApplicationContextServerWebExchangeMatcher(Class<? extends C> context) {
super(context);
}
C callMatchesAndReturnProvidedContext(ServerWebExchange exchange) {
matches(exchange);
return getProvidedContext();
}
@Override
protected Mono<MatchResult> matches(ServerWebExchange exchange, C context) {
this.providedContext = context;
return MatchResult.match();
}
C getProvidedContext() {
return this.providedContext;
}
}
}
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.boot.security; package org.springframework.boot.security.servlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
package sample.actuator.customsecurity; package sample.actuator.customsecurity;
import org.springframework.boot.actuate.autoconfigure.security.EndpointRequest; import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.boot.autoconfigure.security.StaticResourceRequest; import org.springframework.boot.autoconfigure.security.StaticResourceRequest;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
......
spring.security.user.name=user spring.security.user.name=user
spring.security.user.password=password spring.security.user.password=password
\ No newline at end of file management.endpoints.web.expose=*
\ No newline at end of file
/*
* Copyright 2012-2018 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.secure.webflux;
import java.util.Base64;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.security.reactive.EndpointRequest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* Integration tests for a secure reactive application with custom security.
*
* @author Madhura Bhave
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { SampleSecureWebFluxCustomSecurityTests.SecurityConfiguration.class,
SampleSecureWebFluxApplication.class })
public class SampleSecureWebFluxCustomSecurityTests {
@Autowired
private WebTestClient webClient;
@Test
public void userDefinedMappingsSecure() {
this.webClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange()
.expectStatus().isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
public void healthAndInfoDontRequireAuthentication() {
this.webClient.get().uri("/actuator/health").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk();
this.webClient.get().uri("/actuator/info").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk();
}
@Test
public void actuatorsSecuredByRole() {
this.webClient.get().uri("/actuator/env").accept(MediaType.APPLICATION_JSON)
.header("Authorization", "basic " + getBasicAuth()).exchange()
.expectStatus().isForbidden();
}
@Test
public void actuatorsAccessibleOnCorrectLogin() {
this.webClient.get().uri("/actuator/env").accept(MediaType.APPLICATION_JSON)
.header("Authorization", "basic " + getBasicAuthForAdmin()).exchange()
.expectStatus().isOk();
}
@Configuration
static class SecurityConfiguration {
@Bean
public MapReactiveUserDetailsService userDetailsService() {
return new MapReactiveUserDetailsService(
User.withDefaultPasswordEncoder().username("user").password("password")
.authorities("ROLE_USER").build(),
User.withDefaultPasswordEncoder().username("admin").password("admin")
.authorities("ROLE_ACTUATOR", "ROLE_USER").build());
}
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.matchers(EndpointRequest.to("health", "info")).permitAll()
.matchers(EndpointRequest.toAnyEndpoint()).hasRole("ACTUATOR")
.pathMatchers("/login").permitAll()
.anyExchange().authenticated()
.and()
.httpBasic();
return http.build();
}
}
private String getBasicAuth() {
return new String(Base64.getEncoder().encode(("user:password").getBytes()));
}
private String getBasicAuthForAdmin() {
return new String(Base64.getEncoder().encode(("admin:admin").getBytes()));
}
}
...@@ -19,7 +19,7 @@ package sample.security.method; ...@@ -19,7 +19,7 @@ package sample.security.method;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.Map;
import org.springframework.boot.actuate.autoconfigure.security.EndpointRequest; import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment