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.context.properties; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025 026import com.fasterxml.jackson.annotation.JsonInclude.Include; 027import com.fasterxml.jackson.core.JsonGenerator; 028import com.fasterxml.jackson.databind.BeanDescription; 029import com.fasterxml.jackson.databind.MapperFeature; 030import com.fasterxml.jackson.databind.ObjectMapper; 031import com.fasterxml.jackson.databind.SerializationConfig; 032import com.fasterxml.jackson.databind.SerializationFeature; 033import com.fasterxml.jackson.databind.SerializerProvider; 034import com.fasterxml.jackson.databind.introspect.Annotated; 035import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; 036import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; 037import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; 038import com.fasterxml.jackson.databind.ser.BeanSerializerFactory; 039import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; 040import com.fasterxml.jackson.databind.ser.PropertyWriter; 041import com.fasterxml.jackson.databind.ser.SerializerFactory; 042import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter; 043import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; 044import org.apache.commons.logging.Log; 045import org.apache.commons.logging.LogFactory; 046 047import org.springframework.beans.BeansException; 048import org.springframework.boot.actuate.endpoint.Sanitizer; 049import org.springframework.boot.actuate.endpoint.annotation.Endpoint; 050import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; 051import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata; 052import org.springframework.boot.context.properties.ConfigurationProperties; 053import org.springframework.context.ApplicationContext; 054import org.springframework.context.ApplicationContextAware; 055import org.springframework.util.ClassUtils; 056import org.springframework.util.StringUtils; 057 058/** 059 * {@link Endpoint} to expose application properties from {@link ConfigurationProperties} 060 * annotated beans. 061 * 062 * <p> 063 * To protect sensitive information from being exposed, certain property values are masked 064 * if their names end with a set of configurable values (default "password" and "secret"). 065 * Configure property names by using {@code endpoints.configprops.keys_to_sanitize} in 066 * your Spring Boot application configuration. 067 * 068 * @author Christian Dupuis 069 * @author Dave Syer 070 * @author Stephane Nicoll 071 * @since 2.0.0 072 */ 073@Endpoint(id = "configprops") 074public class ConfigurationPropertiesReportEndpoint implements ApplicationContextAware { 075 076 private static final String CONFIGURATION_PROPERTIES_FILTER_ID = "configurationPropertiesFilter"; 077 078 private final Sanitizer sanitizer = new Sanitizer(); 079 080 private ApplicationContext context; 081 082 private ObjectMapper objectMapper; 083 084 @Override 085 public void setApplicationContext(ApplicationContext context) throws BeansException { 086 this.context = context; 087 } 088 089 public void setKeysToSanitize(String... keysToSanitize) { 090 this.sanitizer.setKeysToSanitize(keysToSanitize); 091 } 092 093 @ReadOperation 094 public ApplicationConfigurationProperties configurationProperties() { 095 return extract(this.context); 096 } 097 098 private ApplicationConfigurationProperties extract(ApplicationContext context) { 099 Map<String, ContextConfigurationProperties> contextProperties = new HashMap<>(); 100 ApplicationContext target = context; 101 while (target != null) { 102 contextProperties.put(target.getId(), 103 describeConfigurationProperties(target, getObjectMapper())); 104 target = target.getParent(); 105 } 106 return new ApplicationConfigurationProperties(contextProperties); 107 } 108 109 private ContextConfigurationProperties describeConfigurationProperties( 110 ApplicationContext context, ObjectMapper mapper) { 111 ConfigurationBeanFactoryMetadata beanFactoryMetadata = getBeanFactoryMetadata( 112 context); 113 Map<String, Object> beans = getConfigurationPropertiesBeans(context, 114 beanFactoryMetadata); 115 Map<String, ConfigurationPropertiesBeanDescriptor> beanDescriptors = new HashMap<>(); 116 beans.forEach((beanName, bean) -> { 117 String prefix = extractPrefix(context, beanFactoryMetadata, beanName); 118 beanDescriptors.put(beanName, new ConfigurationPropertiesBeanDescriptor( 119 prefix, sanitize(prefix, safeSerialize(mapper, bean, prefix)))); 120 }); 121 return new ContextConfigurationProperties(beanDescriptors, 122 (context.getParent() != null) ? context.getParent().getId() : null); 123 } 124 125 private ConfigurationBeanFactoryMetadata getBeanFactoryMetadata( 126 ApplicationContext context) { 127 Map<String, ConfigurationBeanFactoryMetadata> beans = context 128 .getBeansOfType(ConfigurationBeanFactoryMetadata.class); 129 if (beans.size() == 1) { 130 return beans.values().iterator().next(); 131 } 132 return null; 133 } 134 135 private Map<String, Object> getConfigurationPropertiesBeans( 136 ApplicationContext context, 137 ConfigurationBeanFactoryMetadata beanFactoryMetadata) { 138 Map<String, Object> beans = new HashMap<>(); 139 beans.putAll(context.getBeansWithAnnotation(ConfigurationProperties.class)); 140 if (beanFactoryMetadata != null) { 141 beans.putAll(beanFactoryMetadata 142 .getBeansWithFactoryAnnotation(ConfigurationProperties.class)); 143 } 144 return beans; 145 } 146 147 /** 148 * Cautiously serialize the bean to a map (returning a map with an error message 149 * instead of throwing an exception if there is a problem). 150 * @param mapper the object mapper 151 * @param bean the source bean 152 * @param prefix the prefix 153 * @return the serialized instance 154 */ 155 @SuppressWarnings("unchecked") 156 private Map<String, Object> safeSerialize(ObjectMapper mapper, Object bean, 157 String prefix) { 158 try { 159 return new HashMap<>(mapper.convertValue(bean, Map.class)); 160 } 161 catch (Exception ex) { 162 return new HashMap<>(Collections.singletonMap("error", 163 "Cannot serialize '" + prefix + "'")); 164 } 165 } 166 167 /** 168 * Configure Jackson's {@link ObjectMapper} to be used to serialize the 169 * {@link ConfigurationProperties} objects into a {@link Map} structure. 170 * @param mapper the object mapper 171 */ 172 protected void configureObjectMapper(ObjectMapper mapper) { 173 mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); 174 mapper.configure(MapperFeature.USE_STD_BEAN_NAMING, true); 175 mapper.setSerializationInclusion(Include.NON_NULL); 176 applyConfigurationPropertiesFilter(mapper); 177 applySerializationModifier(mapper); 178 } 179 180 private ObjectMapper getObjectMapper() { 181 if (this.objectMapper == null) { 182 this.objectMapper = new ObjectMapper(); 183 configureObjectMapper(this.objectMapper); 184 } 185 return this.objectMapper; 186 } 187 188 /** 189 * Ensure only bindable and non-cyclic bean properties are reported. 190 * @param mapper the object mapper 191 */ 192 private void applySerializationModifier(ObjectMapper mapper) { 193 SerializerFactory factory = BeanSerializerFactory.instance 194 .withSerializerModifier(new GenericSerializerModifier()); 195 mapper.setSerializerFactory(factory); 196 } 197 198 private void applyConfigurationPropertiesFilter(ObjectMapper mapper) { 199 mapper.setAnnotationIntrospector( 200 new ConfigurationPropertiesAnnotationIntrospector()); 201 mapper.setFilterProvider(new SimpleFilterProvider() 202 .setDefaultFilter(new ConfigurationPropertiesPropertyFilter())); 203 } 204 205 /** 206 * Extract configuration prefix from {@link ConfigurationProperties} annotation. 207 * @param context the application context 208 * @param beanFactoryMetaData the bean factory meta-data 209 * @param beanName the bean name 210 * @return the prefix 211 */ 212 private String extractPrefix(ApplicationContext context, 213 ConfigurationBeanFactoryMetadata beanFactoryMetaData, String beanName) { 214 ConfigurationProperties annotation = context.findAnnotationOnBean(beanName, 215 ConfigurationProperties.class); 216 if (beanFactoryMetaData != null) { 217 ConfigurationProperties override = beanFactoryMetaData 218 .findFactoryAnnotation(beanName, ConfigurationProperties.class); 219 if (override != null) { 220 // The @Bean-level @ConfigurationProperties overrides the one at type 221 // level when binding. Arguably we should render them both, but this one 222 // might be the most relevant for a starting point. 223 annotation = override; 224 } 225 } 226 return annotation.prefix(); 227 } 228 229 /** 230 * Sanitize all unwanted configuration properties to avoid leaking of sensitive 231 * information. 232 * @param prefix the property prefix 233 * @param map the source map 234 * @return the sanitized map 235 */ 236 @SuppressWarnings("unchecked") 237 private Map<String, Object> sanitize(String prefix, Map<String, Object> map) { 238 map.forEach((key, value) -> { 239 String qualifiedKey = (prefix.isEmpty() ? prefix : prefix + ".") + key; 240 if (value instanceof Map) { 241 map.put(key, sanitize(qualifiedKey, (Map<String, Object>) value)); 242 } 243 else if (value instanceof List) { 244 map.put(key, sanitize(qualifiedKey, (List<Object>) value)); 245 } 246 else { 247 value = this.sanitizer.sanitize(key, value); 248 value = this.sanitizer.sanitize(qualifiedKey, value); 249 map.put(key, value); 250 } 251 }); 252 return map; 253 } 254 255 @SuppressWarnings("unchecked") 256 private List<Object> sanitize(String prefix, List<Object> list) { 257 List<Object> sanitized = new ArrayList<>(); 258 for (Object item : list) { 259 if (item instanceof Map) { 260 sanitized.add(sanitize(prefix, (Map<String, Object>) item)); 261 } 262 else if (item instanceof List) { 263 sanitized.add(sanitize(prefix, (List<Object>) item)); 264 } 265 else { 266 sanitized.add(this.sanitizer.sanitize(prefix, item)); 267 } 268 } 269 return sanitized; 270 } 271 272 /** 273 * Extension to {@link JacksonAnnotationIntrospector} to suppress CGLIB generated bean 274 * properties. 275 */ 276 @SuppressWarnings("serial") 277 private static class ConfigurationPropertiesAnnotationIntrospector 278 extends JacksonAnnotationIntrospector { 279 280 @Override 281 public Object findFilterId(Annotated a) { 282 Object id = super.findFilterId(a); 283 if (id == null) { 284 id = CONFIGURATION_PROPERTIES_FILTER_ID; 285 } 286 return id; 287 } 288 289 } 290 291 /** 292 * {@link SimpleBeanPropertyFilter} for serialization of 293 * {@link ConfigurationProperties} beans. The filter hides: 294 * 295 * <ul> 296 * <li>Properties that have a name starting with '$$'. 297 * <li>Properties that are self-referential. 298 * <li>Properties that throw an exception when retrieving their value. 299 * </ul> 300 */ 301 private static class ConfigurationPropertiesPropertyFilter 302 extends SimpleBeanPropertyFilter { 303 304 private static final Log logger = LogFactory 305 .getLog(ConfigurationPropertiesPropertyFilter.class); 306 307 @Override 308 protected boolean include(BeanPropertyWriter writer) { 309 return include(writer.getFullName().getSimpleName()); 310 } 311 312 @Override 313 protected boolean include(PropertyWriter writer) { 314 return include(writer.getFullName().getSimpleName()); 315 } 316 317 private boolean include(String name) { 318 return !name.startsWith("$$"); 319 } 320 321 @Override 322 public void serializeAsField(Object pojo, JsonGenerator jgen, 323 SerializerProvider provider, PropertyWriter writer) throws Exception { 324 if (writer instanceof BeanPropertyWriter) { 325 try { 326 if (pojo == ((BeanPropertyWriter) writer).get(pojo)) { 327 if (logger.isDebugEnabled()) { 328 logger.debug("Skipping '" + writer.getFullName() + "' on '" 329 + pojo.getClass().getName() 330 + "' as it is self-referential"); 331 } 332 return; 333 } 334 } 335 catch (Exception ex) { 336 if (logger.isDebugEnabled()) { 337 logger.debug("Skipping '" + writer.getFullName() + "' on '" 338 + pojo.getClass().getName() + "' as an exception " 339 + "was thrown when retrieving its value", ex); 340 } 341 return; 342 } 343 } 344 super.serializeAsField(pojo, jgen, provider, writer); 345 } 346 347 } 348 349 /** 350 * {@link BeanSerializerModifier} to return only relevant configuration properties. 351 */ 352 protected static class GenericSerializerModifier extends BeanSerializerModifier { 353 354 @Override 355 public List<BeanPropertyWriter> changeProperties(SerializationConfig config, 356 BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) { 357 List<BeanPropertyWriter> result = new ArrayList<>(); 358 for (BeanPropertyWriter writer : beanProperties) { 359 boolean readable = isReadable(beanDesc, writer); 360 if (readable) { 361 result.add(writer); 362 } 363 } 364 return result; 365 } 366 367 private boolean isReadable(BeanDescription beanDesc, BeanPropertyWriter writer) { 368 Class<?> parentType = beanDesc.getType().getRawClass(); 369 Class<?> type = writer.getType().getRawClass(); 370 AnnotatedMethod setter = findSetter(beanDesc, writer); 371 // If there's a setter, we assume it's OK to report on the value, 372 // similarly, if there's no setter but the package names match, we assume 373 // that its a nested class used solely for binding to config props, so it 374 // should be kosher. Lists and Maps are also auto-detected by default since 375 // that's what the metadata generator does. This filter is not used if there 376 // is JSON metadata for the property, so it's mainly for user-defined beans. 377 return (setter != null) 378 || ClassUtils.getPackageName(parentType) 379 .equals(ClassUtils.getPackageName(type)) 380 || Map.class.isAssignableFrom(type) 381 || Collection.class.isAssignableFrom(type); 382 } 383 384 private AnnotatedMethod findSetter(BeanDescription beanDesc, 385 BeanPropertyWriter writer) { 386 String name = "set" + determineAccessorSuffix(writer.getName()); 387 Class<?> type = writer.getType().getRawClass(); 388 AnnotatedMethod setter = beanDesc.findMethod(name, new Class<?>[] { type }); 389 // The enabled property of endpoints returns a boolean primitive but is set 390 // using a Boolean class 391 if (setter == null && type.equals(Boolean.TYPE)) { 392 setter = beanDesc.findMethod(name, new Class<?>[] { Boolean.class }); 393 } 394 return setter; 395 } 396 397 /** 398 * Determine the accessor suffix of the specified {@code propertyName}, see 399 * section 8.8 "Capitalization of inferred names" of the JavaBean specs for more 400 * details. 401 * @param propertyName the property name to turn into an accessor suffix 402 * @return the accessor suffix for {@code propertyName} 403 */ 404 private String determineAccessorSuffix(String propertyName) { 405 if (propertyName.length() > 1 406 && Character.isUpperCase(propertyName.charAt(1))) { 407 return propertyName; 408 } 409 return StringUtils.capitalize(propertyName); 410 } 411 412 } 413 414 /** 415 * A description of an application's {@link ConfigurationProperties} beans. Primarily 416 * intended for serialization to JSON. 417 */ 418 public static final class ApplicationConfigurationProperties { 419 420 private final Map<String, ContextConfigurationProperties> contexts; 421 422 private ApplicationConfigurationProperties( 423 Map<String, ContextConfigurationProperties> contexts) { 424 this.contexts = contexts; 425 } 426 427 public Map<String, ContextConfigurationProperties> getContexts() { 428 return this.contexts; 429 } 430 431 } 432 433 /** 434 * A description of an application context's {@link ConfigurationProperties} beans. 435 * Primarily intended for serialization to JSON. 436 */ 437 public static final class ContextConfigurationProperties { 438 439 private final Map<String, ConfigurationPropertiesBeanDescriptor> beans; 440 441 private final String parentId; 442 443 private ContextConfigurationProperties( 444 Map<String, ConfigurationPropertiesBeanDescriptor> beans, 445 String parentId) { 446 this.beans = beans; 447 this.parentId = parentId; 448 } 449 450 public Map<String, ConfigurationPropertiesBeanDescriptor> getBeans() { 451 return this.beans; 452 } 453 454 public String getParentId() { 455 return this.parentId; 456 } 457 458 } 459 460 /** 461 * A description of a {@link ConfigurationProperties} bean. Primarily intended for 462 * serialization to JSON. 463 */ 464 public static final class ConfigurationPropertiesBeanDescriptor { 465 466 private final String prefix; 467 468 private final Map<String, Object> properties; 469 470 private ConfigurationPropertiesBeanDescriptor(String prefix, 471 Map<String, Object> properties) { 472 this.prefix = prefix; 473 this.properties = properties; 474 } 475 476 public String getPrefix() { 477 return this.prefix; 478 } 479 480 public Map<String, Object> getProperties() { 481 return this.properties; 482 } 483 484 } 485 486}