/**
* Copyright (C) 2015 Orange
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.francetelecom.clara.cloud.spring;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.jasypt.properties.PropertyValueEncryptionUtils;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.mock.jndi.SimpleNamingContextBuilder;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
/**
* Test class in charge of instanciating target spring context for ElPaaso War.
*
* Test contained into this class should fail on several reason, but those reason need to be spring-specific.
* example :
* <ul>
* <li>a bean refer another inexisting bean</li>
* <li>a required property is not found</li>
* <li>a property is not decrypted</li>
* <li>more generally : there is a problem in spring configuration
* </ul>
*
* @author Ludovic Meurillon
*
*/
public class SpringContextLaunchTest {
private static final String DEFAULT_PLACEHOLDER_SUFFIX = PropertyPlaceholderConfigurer.DEFAULT_PLACEHOLDER_SUFFIX;
private static final String DEFAULT_PLACEHOLDER_PREFIX = PropertyPlaceholderConfigurer.DEFAULT_PLACEHOLDER_PREFIX;
private static final Logger logger = LoggerFactory.getLogger(SpringContextLaunchTest.class);
public static final String CREDENTIALS_REFERENCE_PROPERTIES = "com/francetelecom/clara/cloud/commons/testconfigurations/credentials-reference.properties";
/*
* Initialize a JNDI repository with all props from propertiesFile
*/
private Properties initJndiValues(String propertiesFile) throws NamingException, IOException {
InputStream propsStream = this.getClass().getClassLoader().getResourceAsStream(propertiesFile);
assertNotNull("no property file found for "+propertiesFile, propsStream);
Properties props = new Properties();
props.load(propsStream);
SimpleNamingContextBuilder.emptyActivatedContextBuilder();
InitialContext initialContext = new InitialContext();
for (Object key : props.keySet()) {
initialContext.bind((String)key, props.get(key));
}
return props;
}
@Test
public void spring_should_start_replacing_and_decrypting_properties_from_jndi() throws Exception {
//Set fictive jndi properties (some of them are encoded)
Properties properties = initJndiValues(CREDENTIALS_REFERENCE_PROPERTIES);
final Set<String> encryptedPropertiesKeys = new HashSet<String>();;
//We keep a trace of encoded properties (those that need to be decrypted via spring)
for (Object key : properties.keySet()) {
if(PropertyValueEncryptionUtils.isEncryptedValue((String)properties.get(key))){
encryptedPropertiesKeys.add((String) key);
}
}
//This multimap will contains all beans-propertyname couples depending on an encrypted property
final Multimap<String, String> encryptedBeansProperties = ArrayListMultimap.create();
PropertyTestApplicationContext context = new PropertyTestApplicationContext("classpath:/spring-config/application-context.xml"){
@Override
public void afterPostProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
collectEncodedBeans(beanFactory,encryptedPropertiesKeys, encryptedBeansProperties);
}
};
//Refresh start the context manually
context.refresh();
//Verify for all found beans that they do not have encrypted values for their properties
for (Entry<String, String> entry : encryptedBeansProperties.entries()) {
logger.info("verifying that {}:{} is correctly decrypted", entry.getKey(),entry.getValue());
assertIsDecrypted(context, entry.getKey(), entry.getValue());
}
}
/*
* Assert that a specific bean property value is not encrypted
*/
private void assertIsDecrypted(ClassPathXmlApplicationContext context, String beanName, String propertyName) {
TypedStringValue value = (TypedStringValue) context.getBeanFactory().getBeanDefinition(beanName).getPropertyValues().getPropertyValue(propertyName).getValue();
assertFalse(beanName+":"+propertyName+" was not decrypted by spring", PropertyValueEncryptionUtils.isEncryptedValue(value.getValue()));
}
/*
* Collect all property name from spring beans definitions where the property key is in searchedKeys collection
*
* All found beanName-PropertyName couple are stored in the multimap passed in parameter
*/
protected void collectEncodedBeans(ConfigurableListableBeanFactory beanFactory, Set<String> searchedKeys, Multimap<String, String> foundBeanProperties) {
for (String name : beanFactory.getBeanDefinitionNames()) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(name);
MutablePropertyValues beanProperties = beanDefinition.getPropertyValues();
for (PropertyValue value : beanProperties.getPropertyValueList()) {
String propertyKey = extractPropertyKey(value.getValue());
if(propertyKey != null && searchedKeys.contains(propertyKey)){
foundBeanProperties.put(name, value.getName());
}
}
}
}
/*
* Extract key from a spring property placeholder reference
*
* ex : value="${test.com}" => "test.com"
*
* return null if this is not a text placeholder
*/
private String extractPropertyKey(Object value) {
if(value instanceof TypedStringValue){
String propertyTextValue = ((TypedStringValue) value).getValue();
if(propertyTextValue != null){
propertyTextValue = propertyTextValue.trim();
if(isPropertyPlaceholder(propertyTextValue)){
return sanitizeProperty(propertyTextValue);
}
}
}
return null;
}
/* Return true if a String is a placeholder "${test.com}" for example */
private boolean isPropertyPlaceholder(String propertyTextValue) {
return propertyTextValue.startsWith(DEFAULT_PLACEHOLDER_PREFIX) && propertyTextValue.endsWith(DEFAULT_PLACEHOLDER_SUFFIX);
}
/* Remove placeholder separators from a placeholder "${test.com}" => "test.com" */
private String sanitizeProperty(String propertyTextValue) {
return propertyTextValue.replace(DEFAULT_PLACEHOLDER_PREFIX, "").replace(DEFAULT_PLACEHOLDER_SUFFIX, "");
}
}