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}