diff --git a/web/src/main/java/org/springframework/security/web/server/authorization/ServerWebExchangeDelegatingServerAccessDeniedHandler.java b/web/src/main/java/org/springframework/security/web/server/authorization/ServerWebExchangeDelegatingServerAccessDeniedHandler.java new file mode 100644 index 0000000000..eba63a67ed --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/authorization/ServerWebExchangeDelegatingServerAccessDeniedHandler.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-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.security.web.server.authorization; + +import java.util.Arrays; +import java.util.List; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.util.Assert; +import org.springframework.web.server.ServerWebExchange; + +/** + * A {@link ServerAccessDeniedHandler} which delegates to multiple {@link ServerAccessDeniedHandler}s based + * on a {@link ServerWebExchangeMatcher} + * + * @author Josh Cummings + * @since 5.1 + */ +public class ServerWebExchangeDelegatingServerAccessDeniedHandler + implements ServerAccessDeniedHandler { + + private final List handlers; + + private ServerAccessDeniedHandler defaultHandler = (exchange, e) -> { + exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); + return exchange.getResponse().setComplete(); + }; + + /** + * Creates a new instance + * + * @param handlers a list of {@link ServerWebExchangeMatcher}/ + * {@link ServerAccessDeniedHandler} pairs that should be used. Each is considered + * in the order they are specified and only the first {@link ServerAccessDeniedHandler} + * is used. If none match, then the default {@link ServerAccessDeniedHandler} + * is used. + */ + public ServerWebExchangeDelegatingServerAccessDeniedHandler( + DelegateEntry... handlers) { + this(Arrays.asList(handlers)); + } + + /** + * Creates a new instance + * + * @param handlers a list of {@link ServerWebExchangeMatcher}/ + * {@link ServerAccessDeniedHandler} pairs that should be used. Each is considered + * in the order they are specified and only the first {@link ServerAccessDeniedHandler} + * is used. If none match, then the default {@link ServerAccessDeniedHandler} + * is used. + */ + public ServerWebExchangeDelegatingServerAccessDeniedHandler( + List handlers) { + Assert.notEmpty(handlers, "handlers cannot be null"); + this.handlers = handlers; + } + + @Override + public Mono handle(ServerWebExchange exchange, + AccessDeniedException denied) { + return Flux.fromIterable(this.handlers) + .filterWhen(entry -> isMatch(exchange, entry)) + .next() + .map(DelegateEntry::getAccessDeniedHandler) + .defaultIfEmpty(this.defaultHandler) + .flatMap(handler -> handler.handle(exchange, denied)); + } + + /** + * Use this {@link ServerAccessDeniedHandler} when no {@link ServerWebExchangeMatcher} + * matches. + * + * @param accessDeniedHandler - the default {@link ServerAccessDeniedHandler} to use + */ + public void setDefaultAccessDeniedHandler(ServerAccessDeniedHandler accessDeniedHandler) { + Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null"); + this.defaultHandler = accessDeniedHandler; + } + + public static class DelegateEntry { + private final ServerWebExchangeMatcher matcher; + private final ServerAccessDeniedHandler accessDeniedHandler; + + public DelegateEntry(ServerWebExchangeMatcher matcher, + ServerAccessDeniedHandler accessDeniedHandler) { + this.matcher = matcher; + this.accessDeniedHandler = accessDeniedHandler; + } + + public ServerWebExchangeMatcher getMatcher() { + return this.matcher; + } + + public ServerAccessDeniedHandler getAccessDeniedHandler() { + return this.accessDeniedHandler; + } + } + + private Mono isMatch(ServerWebExchange exchange, DelegateEntry entry) { + ServerWebExchangeMatcher matcher = entry.getMatcher(); + return matcher.matches(exchange) + .map(ServerWebExchangeMatcher.MatchResult::isMatch); + } +} diff --git a/web/src/test/java/org/springframework/security/web/server/authorization/ServerWebExchangeDelegatingServerAccessDeniedHandlerTests.java b/web/src/test/java/org/springframework/security/web/server/authorization/ServerWebExchangeDelegatingServerAccessDeniedHandlerTests.java new file mode 100644 index 0000000000..748424d3ec --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/authorization/ServerWebExchangeDelegatingServerAccessDeniedHandlerTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-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.security.web.server.authorization; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.web.server.ServerWebExchange; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.web.server.authorization.ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry; +import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.match; +import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.notMatch; + +public class ServerWebExchangeDelegatingServerAccessDeniedHandlerTests { + private ServerWebExchangeDelegatingServerAccessDeniedHandler delegator; + private List entries; + private ServerAccessDeniedHandler accessDeniedHandler; + private ServerWebExchange exchange; + + @Before + public void setup() { + this.accessDeniedHandler = mock(ServerAccessDeniedHandler.class); + this.entries = new ArrayList<>(); + this.exchange = mock(ServerWebExchange.class); + } + + @Test + public void handleWhenNothingMatchesThenOnlyDefaultHandlerInvoked() { + ServerAccessDeniedHandler handler = mock(ServerAccessDeniedHandler.class); + ServerWebExchangeMatcher matcher = mock(ServerWebExchangeMatcher.class); + when(matcher.matches(this.exchange)).thenReturn(notMatch()); + when(handler.handle(this.exchange, null)).thenReturn(Mono.empty()); + when(this.accessDeniedHandler.handle(this.exchange, null)).thenReturn(Mono.empty()); + + this.entries.add(new DelegateEntry(matcher, handler)); + this.delegator = new ServerWebExchangeDelegatingServerAccessDeniedHandler(this.entries); + this.delegator.setDefaultAccessDeniedHandler(this.accessDeniedHandler); + + this.delegator.handle(this.exchange, null).block(); + + verify(this.accessDeniedHandler).handle(this.exchange, null); + verify(handler, never()).handle(this.exchange, null); + } + + @Test + public void handleWhenFirstMatchesThenOnlyFirstInvoked() { + ServerAccessDeniedHandler firstHandler = mock(ServerAccessDeniedHandler.class); + ServerWebExchangeMatcher firstMatcher = mock(ServerWebExchangeMatcher.class); + ServerAccessDeniedHandler secondHandler = mock(ServerAccessDeniedHandler.class); + ServerWebExchangeMatcher secondMatcher = mock(ServerWebExchangeMatcher.class); + when(firstMatcher.matches(this.exchange)).thenReturn(match()); + when(firstHandler.handle(this.exchange, null)).thenReturn(Mono.empty()); + when(secondHandler.handle(this.exchange, null)).thenReturn(Mono.empty()); + + this.entries.add(new DelegateEntry(firstMatcher, firstHandler)); + this.entries.add(new DelegateEntry(secondMatcher, secondHandler)); + this.delegator = new ServerWebExchangeDelegatingServerAccessDeniedHandler(this.entries); + this.delegator.setDefaultAccessDeniedHandler(this.accessDeniedHandler); + + this.delegator.handle(this.exchange, null).block(); + + verify(firstHandler).handle(this.exchange, null); + verify(secondHandler, never()).handle(this.exchange, null); + verify(this.accessDeniedHandler, never()).handle(this.exchange, null); + verify(secondMatcher, never()).matches(this.exchange); + } + + @Test + public void handleWhenSecondMatchesThenOnlySecondInvoked() { + ServerAccessDeniedHandler firstHandler = mock(ServerAccessDeniedHandler.class); + ServerWebExchangeMatcher firstMatcher = mock(ServerWebExchangeMatcher.class); + ServerAccessDeniedHandler secondHandler = mock(ServerAccessDeniedHandler.class); + ServerWebExchangeMatcher secondMatcher = mock(ServerWebExchangeMatcher.class); + when(firstMatcher.matches(this.exchange)).thenReturn(notMatch()); + when(secondMatcher.matches(this.exchange)).thenReturn(match()); + when(firstHandler.handle(this.exchange, null)).thenReturn(Mono.empty()); + when(secondHandler.handle(this.exchange, null)).thenReturn(Mono.empty()); + + this.entries.add(new DelegateEntry(firstMatcher, firstHandler)); + this.entries.add(new DelegateEntry(secondMatcher, secondHandler)); + this.delegator = new ServerWebExchangeDelegatingServerAccessDeniedHandler(this.entries); + + this.delegator.handle(this.exchange, null).block(); + + verify(secondHandler).handle(this.exchange, null); + verify(firstHandler, never()).handle(this.exchange, null); + verify(this.accessDeniedHandler, never()).handle(this.exchange, null); + } +}