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.servlet;
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 javax.servlet.http.HttpServletRequest;
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.autoconfigure.security.servlet.RequestMatcherProvider;
038import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher;
039import org.springframework.core.annotation.AnnotatedElementUtils;
040import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
041import org.springframework.security.web.util.matcher.OrRequestMatcher;
042import org.springframework.security.web.util.matcher.RequestMatcher;
043import org.springframework.util.Assert;
044import org.springframework.util.StringUtils;
045import org.springframework.web.context.WebApplicationContext;
046
047/**
048 * Factory that can be used to create a {@link RequestMatcher} for actuator endpoint
049 * locations.
050 *
051 * @author Madhura Bhave
052 * @author Phillip Webb
053 * @since 2.0.0
054 */
055public final class EndpointRequest {
056
057        private static final RequestMatcher EMPTY_MATCHER = (request) -> false;
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 EndpointRequestMatcher#excluding(Class...) excluding} method
066         * can be used to further remove specific endpoints if required. For example:
067         * <pre class="code">
068         * EndpointRequest.toAnyEndpoint().excluding(ShutdownEndpoint.class)
069         * </pre>
070         * @return the configured {@link RequestMatcher}
071         */
072        public static EndpointRequestMatcher toAnyEndpoint() {
073                return new EndpointRequestMatcher(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 RequestMatcher}
083         */
084        public static EndpointRequestMatcher to(Class<?>... endpoints) {
085                return new EndpointRequestMatcher(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 RequestMatcher}
095         */
096        public static EndpointRequestMatcher to(String... endpoints) {
097                return new EndpointRequestMatcher(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 EndpointRequestMatcher#excludingLinks() excludingLinks} method can be used
105         * 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 RequestMatcher}
111         */
112        public static LinksRequestMatcher toLinks() {
113                return new LinksRequestMatcher();
114        }
115
116        /**
117         * Base class for supported request matchers.
118         */
119        private abstract static class AbstractRequestMatcher
120                        extends ApplicationContextRequestMatcher<WebApplicationContext> {
121
122                private volatile RequestMatcher delegate;
123
124                AbstractRequestMatcher() {
125                        super(WebApplicationContext.class);
126                }
127
128                @Override
129                protected final void initialized(Supplier<WebApplicationContext> context) {
130                        this.delegate = createDelegate(context.get());
131                }
132
133                @Override
134                protected final boolean matches(HttpServletRequest request,
135                                Supplier<WebApplicationContext> context) {
136                        return this.delegate.matches(request);
137                }
138
139                private RequestMatcher createDelegate(WebApplicationContext context) {
140                        try {
141                                return createDelegate(context, new RequestMatcherFactory());
142                        }
143                        catch (NoSuchBeanDefinitionException ex) {
144                                return EMPTY_MATCHER;
145                        }
146                }
147
148                protected abstract RequestMatcher createDelegate(WebApplicationContext context,
149                                RequestMatcherFactory requestMatcherFactory);
150
151                protected List<RequestMatcher> getLinksMatchers(
152                                RequestMatcherFactory requestMatcherFactory,
153                                RequestMatcherProvider matcherProvider, String basePath) {
154                        List<RequestMatcher> linksMatchers = new ArrayList<>();
155                        linksMatchers.add(requestMatcherFactory.antPath(matcherProvider, basePath));
156                        linksMatchers
157                                        .add(requestMatcherFactory.antPath(matcherProvider, basePath, "/"));
158                        return linksMatchers;
159                }
160
161                protected RequestMatcherProvider getRequestMatcherProvider(
162                                WebApplicationContext context) {
163                        try {
164                                return context.getBean(RequestMatcherProvider.class);
165                        }
166                        catch (NoSuchBeanDefinitionException ex) {
167                                return AntPathRequestMatcher::new;
168                        }
169                }
170
171        }
172
173        /**
174         * The request matcher used to match against {@link Endpoint actuator endpoints}.
175         */
176        public static final class EndpointRequestMatcher extends AbstractRequestMatcher {
177
178                private final List<Object> includes;
179
180                private final List<Object> excludes;
181
182                private final boolean includeLinks;
183
184                private EndpointRequestMatcher(boolean includeLinks) {
185                        this(Collections.emptyList(), Collections.emptyList(), includeLinks);
186                }
187
188                private EndpointRequestMatcher(Class<?>[] endpoints, boolean includeLinks) {
189                        this(Arrays.asList((Object[]) endpoints), Collections.emptyList(),
190                                        includeLinks);
191                }
192
193                private EndpointRequestMatcher(String[] endpoints, boolean includeLinks) {
194                        this(Arrays.asList((Object[]) endpoints), Collections.emptyList(),
195                                        includeLinks);
196                }
197
198                private EndpointRequestMatcher(List<Object> includes, List<Object> excludes,
199                                boolean includeLinks) {
200                        this.includes = includes;
201                        this.excludes = excludes;
202                        this.includeLinks = includeLinks;
203                }
204
205                public EndpointRequestMatcher excluding(Class<?>... endpoints) {
206                        List<Object> excludes = new ArrayList<>(this.excludes);
207                        excludes.addAll(Arrays.asList((Object[]) endpoints));
208                        return new EndpointRequestMatcher(this.includes, excludes, this.includeLinks);
209                }
210
211                public EndpointRequestMatcher excluding(String... endpoints) {
212                        List<Object> excludes = new ArrayList<>(this.excludes);
213                        excludes.addAll(Arrays.asList((Object[]) endpoints));
214                        return new EndpointRequestMatcher(this.includes, excludes, this.includeLinks);
215                }
216
217                public EndpointRequestMatcher excludingLinks() {
218                        return new EndpointRequestMatcher(this.includes, this.excludes, false);
219                }
220
221                @Override
222                protected RequestMatcher createDelegate(WebApplicationContext context,
223                                RequestMatcherFactory requestMatcherFactory) {
224                        PathMappedEndpoints pathMappedEndpoints = context
225                                        .getBean(PathMappedEndpoints.class);
226                        RequestMatcherProvider matcherProvider = getRequestMatcherProvider(context);
227                        Set<String> paths = new LinkedHashSet<>();
228                        if (this.includes.isEmpty()) {
229                                paths.addAll(pathMappedEndpoints.getAllPaths());
230                        }
231                        streamPaths(this.includes, pathMappedEndpoints).forEach(paths::add);
232                        streamPaths(this.excludes, pathMappedEndpoints).forEach(paths::remove);
233                        List<RequestMatcher> delegateMatchers = getDelegateMatchers(
234                                        requestMatcherFactory, matcherProvider, paths);
235                        String basePath = pathMappedEndpoints.getBasePath();
236                        if (this.includeLinks && StringUtils.hasText(basePath)) {
237                                delegateMatchers.addAll(getLinksMatchers(requestMatcherFactory,
238                                                matcherProvider, basePath));
239                        }
240                        return new OrRequestMatcher(delegateMatchers);
241                }
242
243                private Stream<String> streamPaths(List<Object> source,
244                                PathMappedEndpoints pathMappedEndpoints) {
245                        return source.stream().filter(Objects::nonNull).map(this::getEndpointId)
246                                        .map(pathMappedEndpoints::getPath);
247                }
248
249                private EndpointId getEndpointId(Object source) {
250                        if (source instanceof EndpointId) {
251                                return (EndpointId) source;
252                        }
253                        if (source instanceof String) {
254                                return (EndpointId.of((String) source));
255                        }
256                        if (source instanceof Class) {
257                                return getEndpointId((Class<?>) source);
258                        }
259                        throw new IllegalStateException("Unsupported source " + source);
260                }
261
262                private EndpointId getEndpointId(Class<?> source) {
263                        Endpoint annotation = AnnotatedElementUtils.getMergedAnnotation(source,
264                                        Endpoint.class);
265                        Assert.state(annotation != null,
266                                        () -> "Class " + source + " is not annotated with @Endpoint");
267                        return EndpointId.of(annotation.id());
268                }
269
270                private List<RequestMatcher> getDelegateMatchers(
271                                RequestMatcherFactory requestMatcherFactory,
272                                RequestMatcherProvider matcherProvider, Set<String> paths) {
273                        return paths.stream().map(
274                                        (path) -> requestMatcherFactory.antPath(matcherProvider, path, "/**"))
275                                        .collect(Collectors.toList());
276                }
277
278        }
279
280        /**
281         * The request matcher used to match against the links endpoint.
282         */
283        public static final class LinksRequestMatcher extends AbstractRequestMatcher {
284
285                @Override
286                protected RequestMatcher createDelegate(WebApplicationContext context,
287                                RequestMatcherFactory requestMatcherFactory) {
288                        WebEndpointProperties properties = context
289                                        .getBean(WebEndpointProperties.class);
290                        String basePath = properties.getBasePath();
291                        if (StringUtils.hasText(basePath)) {
292                                return new OrRequestMatcher(getLinksMatchers(requestMatcherFactory,
293                                                getRequestMatcherProvider(context), basePath));
294                        }
295                        return EMPTY_MATCHER;
296                }
297
298        }
299
300        /**
301         * Factory used to create a {@link RequestMatcher}.
302         */
303        private static class RequestMatcherFactory {
304
305                public RequestMatcher antPath(RequestMatcherProvider matcherProvider,
306                                String... parts) {
307                        StringBuilder pattern = new StringBuilder();
308                        for (String part : parts) {
309                                pattern.append(part);
310                        }
311                        return matcherProvider.getRequestMatcher(pattern.toString());
312                }
313
314        }
315
316}