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}