DATAGEODE-194 - Add support to authenticate HTTP requests when using @EnableClusterConfiguration and @EnableSecurity.

This commit is contained in:
John Blum
2019-05-18 20:05:00 -07:00
parent 25701a9192
commit 2fb8ee28e3
5 changed files with 351 additions and 6 deletions

View File

@@ -101,7 +101,7 @@ public class ApacheShiroSecurityConfiguration extends AbstractAnnotationConfigSu
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
super.setBeanFactory(Optional.ofNullable(beanFactory)
.filter(it -> it instanceof ListableBeanFactory)
.filter(ListableBeanFactory.class::isInstance)
.orElseThrow(() -> newIllegalArgumentException(
"BeanFactory [%s] must be an instance of ListableBeanFactory",
ObjectUtils.nullSafeClassName(beanFactory))));
@@ -192,8 +192,8 @@ public class ApacheShiroSecurityConfiguration extends AbstractAnnotationConfigSu
try {
Map<String, Realm> realmBeans = getListableBeanFactory().getBeansOfType(Realm.class,
false, false);
Map<String, Realm> realmBeans =
getListableBeanFactory().getBeansOfType(Realm.class, false, false);
List<Realm> realms = new ArrayList<>(CollectionUtils.nullSafeMap(realmBeans).values());

View File

@@ -13,21 +13,33 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.config.annotation;
import java.io.IOException;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.util.Optional;
import java.util.Properties;
import org.apache.geode.management.internal.security.ResourceConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.data.gemfire.config.annotation.support.AutoConfiguredAuthenticationInitializer;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.StringUtils;
/**
@@ -59,9 +71,105 @@ public class AutoConfiguredAuthenticationConfiguration {
protected static final String AUTO_CONFIGURED_AUTH_INIT_STATIC_FACTORY_METHOD =
AutoConfiguredAuthenticationInitializer.class.getName().concat(".newAuthenticationInitializer");
protected static final String DEFAULT_USERNAME = "test";
protected static final String DEFAULT_PASSWORD = DEFAULT_USERNAME;
protected static final String HTTP_PROTOCOL = "HTTP";
protected static final String SECURITY_CLIENT_AUTH_INIT = "security-client-auth-init";
protected static final String SECURITY_PEER_AUTH_INIT = "security-peer-auth-init";
private Logger logger = LoggerFactory.getLogger(getClass());
@Bean("GemFireSecurityAuthenticator")
public Authenticator authenticator(Environment environment) {
Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
String username =
environment.getProperty(AutoConfiguredAuthenticationInitializer.SDG_SECURITY_USERNAME_PROPERTY,
DEFAULT_USERNAME);
String password =
environment.getProperty(AutoConfiguredAuthenticationInitializer.SDG_SECURITY_PASSWORD_PROPERTY,
DEFAULT_PASSWORD);
return new PasswordAuthentication(username, password.toCharArray());
}
};
Authenticator.setDefault(authenticator);
return authenticator;
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE)
public ClientHttpRequestInterceptor loggingAwareClientHttpRequestInterceptor() {
return (request, body, execution) -> {
logger.debug("HTTP Request URI [{}]", request.getURI());
Optional.ofNullable(request.getHeaders())
.ifPresent(httpHeaders -> {
CollectionUtils.nullSafeSet(httpHeaders.keySet()).forEach(httpHeaderName -> {
logger.debug("HTTP Request Header Name [{}] Value [{}]",
httpHeaderName, httpHeaders.get(httpHeaderName));
});
});
ClientHttpResponse response = execution.execute(request, body);
Optional.ofNullable(response)
.ifPresent(it -> {
try {
logger.debug("HTTP Response Status Code [{}] Message [{}]",
it.getRawStatusCode(), it.getStatusText());
}
catch (IOException cause) {
logger.debug("Error occurred getting HTTP Response Status Code and Message", cause);
}
});
return response;
};
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public ClientHttpRequestInterceptor securityAwareClientHttpRequestInterceptor(Authenticator authenticator) {
return (request, body, execution) -> {
URI uri = request.getURI();
PasswordAuthentication passwordAuthentication =
Authenticator.requestPasswordAuthentication(uri.getHost(), null, uri.getPort(),
HTTP_PROTOCOL, null, uri.getScheme());
String username = passwordAuthentication.getUserName();
char[] password = passwordAuthentication.getPassword();
if (isAuthenticationEnabled(username, password)) {
HttpHeaders requestHeaders = request.getHeaders();
requestHeaders.add(ResourceConstants.USER_NAME, username);
requestHeaders.add(ResourceConstants.PASSWORD, String.valueOf(password));
}
return execution.execute(request, body);
};
}
private boolean isAuthenticationEnabled(String username, char[] password) {
return StringUtils.hasText(username) && password != null && password.length > 0;
}
@Bean
public ClientCacheConfigurer authenticationCredentialsSettingClientCacheConfigurer(Environment environment) {
return (beanName, beanFactory) -> setAuthenticationCredentials(beanFactory.getProperties(), environment);

View File

@@ -14,7 +14,6 @@
* limitations under the License.
*
*/
package org.springframework.data.gemfire.config.annotation;
import java.lang.annotation.Annotation;
@@ -61,6 +60,7 @@ public class GeodeIntegratedSecurityConfiguration extends EmbeddedServiceConfigu
* @see #isShiroSecurityNotConfigured()
*/
protected boolean isShiroSecurityConfigured() {
try {
//return resolveBean(ApacheShiroSecurityConfiguration.class).isRealmsPresent();
return false;
@@ -92,6 +92,7 @@ public class GeodeIntegratedSecurityConfiguration extends EmbeddedServiceConfigu
(String) annotationAttributes.get("clientAuthenticationInitializer")));
if (isShiroSecurityNotConfigured()) {
gemfireProperties.setPropertyIfNotDefault(SECURITY_MANAGER,
annotationAttributes.get("securityManagerClass"), Void.class);

View File

@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.config.annotation.support;
import java.util.Properties;

View File

@@ -0,0 +1,237 @@
/*
* Copyright 2018 the original author or authors.
*
* 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 org.springframework.data.gemfire.config.annotation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.data.gemfire.config.annotation.ClusterConfigurationConfiguration.ClusterSchemaObjectInitializer;
import static org.springframework.data.gemfire.config.annotation.ClusterConfigurationConfiguration.SchemaObjectContext;
import java.lang.reflect.Field;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.util.Optional;
import java.util.Properties;
import org.apache.geode.management.internal.security.ResourceConstants;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.data.gemfire.config.admin.GemfireAdminOperations;
import org.springframework.data.gemfire.config.admin.remote.RestHttpGemfireAdminTemplate;
import org.springframework.data.gemfire.test.mock.annotation.EnableGemFireMockObjects;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.InterceptingClientHttpRequestFactory;
import org.springframework.lang.Nullable;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.client.RestTemplate;
/**
* Integration Test for {@link EnableClusterConfiguration} and {@link EnableSecurity}.
*
* @author John Blum
* @see java.net.Authenticator
* @see java.net.PasswordAuthentication
* @see java.net.URI
* @see java.util.Properties
* @see org.junit.Test
* @see org.mockito.Mockito
* @see org.springframework.context.ApplicationContextInitializer
* @see org.springframework.context.ConfigurableApplicationContext
* @see org.springframework.core.env.ConfigurableEnvironment
* @see org.springframework.core.env.PropertiesPropertySource
* @see org.springframework.data.gemfire.config.admin.GemfireAdminOperations
* @see org.springframework.data.gemfire.test.mock.annotation.EnableGemFireMockObjects
* @see org.springframework.http.client.ClientHttpRequestInterceptor
* @see org.springframework.http.client.InterceptingClientHttpRequestFactory
* @see org.springframework.test.context.ContextConfiguration
* @see org.springframework.test.context.junit4.SpringRunner
* @see org.springframework.web.client.RestTemplate
* @since 2.2.0
*/
@SuppressWarnings("unused")
@RunWith(SpringRunner.class)
@ContextConfiguration(initializers = EnableClusterConfigurationWithSecurityIntegrationTests.SecurityConfigurationApplicationContextInitializer.class)
public class EnableClusterConfigurationWithSecurityIntegrationTests {
@Autowired
@Qualifier("GemFireSecurityAuthenticator")
private Authenticator authenticator;
@Autowired
@Qualifier("loggingAwareClientHttpRequestInterceptor")
private ClientHttpRequestInterceptor loggingAwareClientHttpRequestInterceptor;
@Autowired
@Qualifier("securityAwareClientHttpRequestInterceptor")
private ClientHttpRequestInterceptor securityAwareClientHttpRequestInterceptor;
@Autowired
private ClusterSchemaObjectInitializer initializer;
@SuppressWarnings("unchecked")
private <T> T resolveFieldValue(Object target, String fieldName) throws NoSuchFieldException {
Field field = ReflectionUtils.findField(target.getClass(), fieldName);
return Optional.ofNullable(field)
.map(it -> {
ReflectionUtils.makeAccessible(it);
return field;
})
.map(it -> (T) ReflectionUtils.getField(it, target))
.orElseThrow(() ->
new NoSuchFieldException(String.format("Field with name [%s] was not found on Object of type [%s]",
fieldName, target.getClass().getName())));
}
@Before
public void setup() {
assertThat(this.authenticator).isNotNull();
assertThat(this.initializer).isNotNull();
assertThat(this.loggingAwareClientHttpRequestInterceptor).isNotNull();
assertThat(this.securityAwareClientHttpRequestInterceptor).isNotNull();
}
@Test
public void authenticatorReturnsPasswordAuthenticationWithGemFireSecurityProperties() {
PasswordAuthentication passwordAuthentication =
Authenticator.requestPasswordAuthentication("localhost", null, 80,
"https", null, "https");
assertThat(passwordAuthentication).isNotNull();
assertThat(passwordAuthentication.getUserName()).isEqualTo("skeletor");
assertThat(String.valueOf(passwordAuthentication.getPassword())).isEqualTo("s3cr3t");
}
@Test
public void initializerRestTemplateIncludesClientHttpRequestInterceptors() throws Exception {
SchemaObjectContext schemaObjectContext = this.initializer.getSchemaObjectContext();
assertThat(schemaObjectContext).isNotNull();
assertThat(schemaObjectContext.<GemfireAdminOperations>getGemfireAdminOperations())
.isInstanceOf(RestHttpGemfireAdminTemplate.class);
RestHttpGemfireAdminTemplate template = schemaObjectContext.getGemfireAdminOperations();
RestTemplate restTemplate = resolveFieldValue(template, "restTemplate");
assertThat(restTemplate).isNotNull();
assertThat(restTemplate.getInterceptors()).containsExactly(this.securityAwareClientHttpRequestInterceptor,
this.loggingAwareClientHttpRequestInterceptor);
assertThat(restTemplate.getRequestFactory()).isInstanceOf(InterceptingClientHttpRequestFactory.class);
}
@Test
public void securityAwareClientHttpRequestInterceptorAppliesGemFireSecurityPropertiesToHttpHeaders()
throws Exception {
byte[] body = new byte[0];
URI uri = URI.create("https://localhost:8080/gemfire/v1");
ClientHttpRequestExecution mockClientHttpRequestExecution = mock(ClientHttpRequestExecution.class);
HttpHeaders httpHeaders = new HttpHeaders();
HttpRequest mockHttpRequest = mock(HttpRequest.class);
when(mockHttpRequest.getHeaders()).thenReturn(httpHeaders);
when(mockHttpRequest.getURI()).thenReturn(uri);
this.securityAwareClientHttpRequestInterceptor.intercept(mockHttpRequest, body, mockClientHttpRequestExecution);
assertThat(httpHeaders).containsKeys(ResourceConstants.USER_NAME, ResourceConstants.PASSWORD);
assertThat(httpHeaders.getFirst(ResourceConstants.USER_NAME)).isEqualTo("skeletor");
assertThat(httpHeaders.getFirst(ResourceConstants.PASSWORD)).isEqualTo("s3cr3t");
verify(mockClientHttpRequestExecution, times(1)).execute(eq(mockHttpRequest), eq(body));
verify(mockHttpRequest, times(1)).getHeaders();
verify(mockHttpRequest, times(1)).getURI();
}
static class SecurityConfigurationApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
ConfigurableEnvironment environment = applicationContext.getEnvironment();
Properties gemfireSecurityProperties = new Properties();
gemfireSecurityProperties.setProperty("spring.data.gemfire.security.username", "skeletor");
gemfireSecurityProperties.setProperty("spring.data.gemfire.security.password", "s3cr3t");
environment.getPropertySources().
addFirst(new PropertiesPropertySource("EnableClusterConfigurationWithSecurityIntegrationTests",
gemfireSecurityProperties));
}
}
@ClientCacheApplication
@EnableSecurity
@EnableGemFireMockObjects
@EnableClusterConfiguration(useHttp = true)
static class TestConfiguration {
@Bean
BeanPostProcessor clusterSchemaObjectInitializerPostProcessor() {
return new BeanPostProcessor() {
@Nullable @Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ClusterSchemaObjectInitializer) {
ClusterSchemaObjectInitializer initializer = spy((ClusterSchemaObjectInitializer) bean);
doReturn(false).when(initializer).isAutoStartup();
bean = initializer;
}
return bean;
}
};
}
}
}