001/*
002 * Copyright 2012-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.actuate.autoconfigure.security.reactive;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.LinkedHashSet;
023import java.util.List;
024import java.util.Objects;
025import java.util.Set;
026import java.util.function.Supplier;
027import java.util.stream.Collectors;
028import java.util.stream.Stream;
029
030import reactor.core.publisher.Mono;
031
032import org.springframework.beans.factory.NoSuchBeanDefinitionException;
033import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
034import org.springframework.boot.actuate.endpoint.EndpointId;
035import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
036import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
037import org.springframework.boot.security.reactive.ApplicationContextServerWebExchangeMatcher;
038import org.springframework.core.annotation.AnnotatedElementUtils;
039import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher;
040import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
041import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
042import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
043import org.springframework.util.Assert;
044import org.springframework.util.StringUtils;
045import org.springframework.web.server.ServerWebExchange;
046
047/**
048 * Factory that can be used to create a {@link ServerWebExchangeMatcher} for actuator
049 * endpoint locations.
050 *
051 * @author Madhura Bhave
052 * @since 2.0.0
053 */
054public final class EndpointRequest {
055
056        private static final ServerWebExchangeMatcher EMPTY_MATCHER = (request) -> MatchResult
057                        .notMatch();
058
059        private EndpointRequest() {
060        }
061
062        /**
063         * Returns a matcher that includes all {@link Endpoint actuator endpoints}. It also
064         * includes the links endpoint which is present at the base path of the actuator
065         * endpoints. The {@link EndpointServerWebExchangeMatcher#excluding(Class...)
066         * excluding} method can be used to further remove specific endpoints if required. For
067         * example: <pre class="code">
068         * EndpointRequest.toAnyEndpoint().excluding(ShutdownEndpoint.class)
069         * </pre>
070         * @return the configured {@link ServerWebExchangeMatcher}
071         */
072        public static EndpointServerWebExchangeMatcher toAnyEndpoint() {
073                return new EndpointServerWebExchangeMatcher(true);
074        }
075
076        /**
077         * Returns a matcher that includes the specified {@link Endpoint actuator endpoints}.
078         * For example: <pre class="code">
079         * EndpointRequest.to(ShutdownEndpoint.class, HealthEndpoint.class)
080         * </pre>
081         * @param endpoints the endpoints to include
082         * @return the configured {@link ServerWebExchangeMatcher}
083         */
084        public static EndpointServerWebExchangeMatcher to(Class<?>... endpoints) {
085                return new EndpointServerWebExchangeMatcher(endpoints, false);
086        }
087
088        /**
089         * Returns a matcher that includes the specified {@link Endpoint actuator endpoints}.
090         * For example: <pre class="code">
091         * EndpointRequest.to("shutdown", "health")
092         * </pre>
093         * @param endpoints the endpoints to include
094         * @return the configured {@link ServerWebExchangeMatcher}
095         */
096        public static EndpointServerWebExchangeMatcher to(String... endpoints) {
097                return new EndpointServerWebExchangeMatcher(endpoints, false);
098        }
099
100        /**
101         * Returns a matcher that matches only on the links endpoint. It can be used when
102         * security configuration for the links endpoint is different from the other
103         * {@link Endpoint actuator endpoints}. The
104         * {@link EndpointServerWebExchangeMatcher#excludingLinks() excludingLinks} method can
105         * be used in combination with this to remove the links endpoint from
106         * {@link EndpointRequest#toAnyEndpoint() toAnyEndpoint}. For example:
107         * <pre class="code">
108         * EndpointRequest.toLinks()
109         * </pre>
110         * @return the configured {@link ServerWebExchangeMatcher}
111         */
112        public static LinksServerWebExchangeMatcher toLinks() {
113                return new LinksServerWebExchangeMatcher();
114        }
115
116        /**
117         * The {@link ServerWebExchangeMatcher} used to match against {@link Endpoint actuator
118         * endpoints}.
119         */
120        public static final class EndpointServerWebExchangeMatcher
121                        extends ApplicationContextServerWebExchangeMatcher<PathMappedEndpoints> {
122
123                private final List<Object> includes;
124
125                private final List<Object> excludes;
126
127                private final boolean includeLinks;
128
129                private volatile ServerWebExchangeMatcher delegate;
130
131                private EndpointServerWebExchangeMatcher(boolean includeLinks) {
132                        this(Collections.emptyList(), Collections.emptyList(), includeLinks);
133                }
134
135                private EndpointServerWebExchangeMatcher(Class<?>[] endpoints,
136                                boolean includeLinks) {
137                        this(Arrays.asList((Object[]) endpoints), Collections.emptyList(),
138                                        includeLinks);
139                }
140
141                private EndpointServerWebExchangeMatcher(String[] endpoints,
142                                boolean includeLinks) {
143                        this(Arrays.asList((Object[]) endpoints), Collections.emptyList(),
144                                        includeLinks);
145                }
146
147                private EndpointServerWebExchangeMatcher(List<Object> includes,
148                                List<Object> excludes, boolean includeLinks) {
149                        super(PathMappedEndpoints.class);
150                        this.includes = includes;
151                        this.excludes = excludes;
152                        this.includeLinks = includeLinks;
153                }
154
155                public EndpointServerWebExchangeMatcher excluding(Class<?>... endpoints) {
156                        List<Object> excludes = new ArrayList<>(this.excludes);
157                        excludes.addAll(Arrays.asList((Object[]) endpoints));
158                        return new EndpointServerWebExchangeMatcher(this.includes, excludes,
159                                        this.includeLinks);
160                }
161
162                public EndpointServerWebExchangeMatcher excluding(String... endpoints) {
163                        List<Object> excludes = new ArrayList<>(this.excludes);
164                        excludes.addAll(Arrays.asList((Object[]) endpoints));
165                        return new EndpointServerWebExchangeMatcher(this.includes, excludes,
166                                        this.includeLinks);
167                }
168
169                public EndpointServerWebExchangeMatcher excludingLinks() {
170                        return new EndpointServerWebExchangeMatcher(this.includes, this.excludes,
171                                        false);
172                }
173
174                @Override
175                protected void initialized(Supplier<PathMappedEndpoints> pathMappedEndpoints) {
176                        this.delegate = createDelegate(pathMappedEndpoints);
177                }
178
179                private ServerWebExchangeMatcher createDelegate(
180                                Supplier<PathMappedEndpoints> pathMappedEndpoints) {
181                        try {
182                                return createDelegate(pathMappedEndpoints.get());
183                        }
184                        catch (NoSuchBeanDefinitionException ex) {
185                                return EMPTY_MATCHER;
186                        }
187                }
188
189                private ServerWebExchangeMatcher createDelegate(
190                                PathMappedEndpoints pathMappedEndpoints) {
191                        Set<String> paths = new LinkedHashSet<>();
192                        if (this.includes.isEmpty()) {
193                                paths.addAll(pathMappedEndpoints.getAllPaths());
194                        }
195                        streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add);
196                        streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove);
197                        List<ServerWebExchangeMatcher> delegateMatchers = getDelegateMatchers(paths);
198                        if (this.includeLinks
199                                        && StringUtils.hasText(pathMappedEndpoints.getBasePath())) {
200                                delegateMatchers.add(new PathPatternParserServerWebExchangeMatcher(
201                                                pathMappedEndpoints.getBasePath()));
202                        }
203                        return new OrServerWebExchangeMatcher(delegateMatchers);
204                }
205
206                private Stream<String> streamPaths(List<Object> source,
207                                PathMappedEndpoints pathMappedEndpoints) {
208                        return source.stream().filter(Objects::nonNull).map(this::getEndpointId)
209                                        .map(pathMappedEndpoints::getPath);
210                }
211
212                private EndpointId getEndpointId(Object source) {
213                        if (source instanceof EndpointId) {
214                                return (EndpointId) source;
215                        }
216                        if (source instanceof String) {
217                                return (EndpointId.of((String) source));
218                        }
219                        if (source instanceof Class) {
220                                return getEndpointId((Class<?>) source);
221                        }
222                        throw new IllegalStateException("Unsupported source " + source);
223                }
224
225                private EndpointId getEndpointId(Class<?> source) {
226                        Endpoint annotation = AnnotatedElementUtils.getMergedAnnotation(source,
227                                        Endpoint.class);
228                        Assert.state(annotation != null,
229                                        () -> "Class " + source + " is not annotated with @Endpoint");
230                        return EndpointId.of(annotation.id());
231                }
232
233                private List<ServerWebExchangeMatcher> getDelegateMatchers(Set<String> paths) {
234                        return paths.stream().map(
235                                        (path) -> new PathPatternParserServerWebExchangeMatcher(path + "/**"))
236                                        .collect(Collectors.toList());
237                }
238
239                @Override
240                protected Mono<MatchResult> matches(ServerWebExchange exchange,
241                                Supplier<PathMappedEndpoints> context) {
242                        return this.delegate.matches(exchange);
243                }
244
245        }
246
247        /**
248         * The {@link ServerWebExchangeMatcher} used to match against the links endpoint.
249         */
250        public static final class LinksServerWebExchangeMatcher
251                        extends ApplicationContextServerWebExchangeMatcher<WebEndpointProperties> {
252
253                private volatile ServerWebExchangeMatcher delegate;
254
255                private LinksServerWebExchangeMatcher() {
256                        super(WebEndpointProperties.class);
257                }
258
259                @Override
260                protected void initialized(Supplier<WebEndpointProperties> properties) {
261                        this.delegate = createDelegate(properties.get());
262                }
263
264                private ServerWebExchangeMatcher createDelegate(
265                                WebEndpointProperties properties) {
266                        if (StringUtils.hasText(properties.getBasePath())) {
267                                return new PathPatternParserServerWebExchangeMatcher(
268                                                properties.getBasePath());
269                        }
270                        return EMPTY_MATCHER;
271                }
272
273                @Override
274                protected Mono<MatchResult> matches(ServerWebExchange exchange,
275                                Supplier<WebEndpointProperties> context) {
276                        return this.delegate.matches(exchange);
277                }
278
279        }
280
281}